Changes to reflection invoke API exceptions

The exceptions thrown when calling reflection invoke APIs have changed.

Previous behavior

  • Previously, when an invoked method that returns a value by reference returned null, a NullReferenceException was thrown.

  • For constructors, the following exceptions were thrown:

  • When null was passed for a byref-like parameter without the ref modifier (that is, passed by value), no exception was thrown, and the runtime substituted a default value for the null value.

New behavior

Starting in .NET 7:

Version introduced

.NET 7

Type of breaking change

This change can affect binary compatibility.

Reason for change

Throwing TargetInvocationException instead of the originating exception makes the experience more consistent. It properly layers exceptions caused by the validation of the incoming parameters (which aren't wrapped with TargetInvocationException) versus exceptions thrown due to the implementation of the target method (which are wrapped). Consistent rules provide more consistent experiences across different implementations of the CLR and of Invoke APIs.

The change to throw NotSupportedException when a byref-like type is passed to an Invoke() API fixes an oversight of the original implementation, which did not throw. The original implementation gave the appearance that ref struct types are supported by the Invoke() APIs, when they aren't. Since the current Invoke() APIs use System.Object for parameter types, and a ref struct type can't be boxed to System.Object, it's an unsupported scenario.

If you don't use BindingFlags.DoNotWrapExceptions when calling Invoke(), and you have catch statements around the Invoke() APIs for exceptions other than TargetInvocationException, consider changing or removing those catch statements. The other exceptions will no longer be thrown as a result of the invocation. If, however, you're catching exceptions from argument validation that happens before attempting to invoke the target method, you should keep those catch statements. Invalid arguments that are validated before attempting to invoke are thrown without being wrapped with TargetInvocationException and did not change semantics.

Consider using BindingFlags.DoNotWrapExceptions so that TargetInvocationException is never thrown. In this case, the originating exception won't be wrapped by a TargetInvocationException. In most cases, not wrapping the exception improves the chances of the diagnosing the actual issue since not all exception reporting tools display the inner exception. In addition, by using BindingFlags.DoNotWrapExceptions, the same exceptions will be thrown as when calling the method directly (without reflection). This is desirable in most cases, since the choice of whether reflection is used or not can be arbitrary or an implementation detail that does not need to surface to the caller.

In the rare case that you need to pass a default value to a method through reflection that contains a byref-like parameter that's passed "by value", you can add a wrapper method that omits the parameter and calls the target method with a default value for that parameter.

Affected APIs