In the AOP framework I've been blogging about lately, all member invocations are done through reflection. One side-effect of this, is that if you have any exception handling in your application which expects some specific exception type, it will fail because all exceptions thrown are wrapped in a TargetInvocationException as a result of the reflection. Now the solution might sound simple - the framework should just catch any TargetInvocationExceptions and 'unwrap' them, re-throwing the inner exception instead. There's one huge pitfall here though - catching and re-throwing an exception causes the stack trace to be reset.
Let's look at an example. Here's a program that'll reproduce the scenario:
using System;
using System.Reflection;
namespace PreserveStackTrace
{
class Program
{
static void Main(string[] args)
{
InvokeRealMethodByReflection(args);
}
public static void RealMethod(object arg)
{
throw new ArgumentNullException("arg");
}
private static void InvokeRealMethodByReflection(string[] args)
{
MethodInfo method = typeof(Program).GetMethod("RealMethod");
object arg = null;
method.Invoke(null, new object[] { arg });
}
}
}
Running this, we get a "TargetInvocationException was unhandled" error, with the ArgumentNullException as the inner exception, with the following stack trace:
at ConsoleApplication3.Program.RealMethod(Object arg) in {...}\\Program.cs:line 15
With line 15 being the "throw new ArgumentNullException("arg");" in the above code (count them if you want :p). We don't want a TargetInvocationException to be thrown, though; we want the ArgumentNullException to be the one the calling code has to deal with.
Re-Throw to the Re-scue?
To fix this, we might try refactoring the InvokeRealMethodByReflection method to the following:
private static void InvokeRealMethodByReflection(string[] args)
{
MethodInfo method = typeof(Program).GetMethod("RealMethod");
object arg = null;
try
{
method.Invoke(null, new object[] {arg});
}
catch(TargetInvocationException ex)
{
throw ex.InnerException;
}
}
Here, we've unwrapped the TargetInvocationException and re-thrown the inner exception. Thus, the exception that bubbles up to the Main method is the ArgumentNullException - but if we look at the stack trace, we've lost valuable information about the where it occurred:
at PreserveStackTrace.Program.InvokeRealMethodByReflection(String[] args) in {..}\Program.cs:line 30
at PreserveStackTrace.Program.Main(String[] args) in {..}\Program.cs:line 10
at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.ThreadHelper.ThreadStart()
According to this, the cause of the exception was the InvokeRealMethodByReflection method, on line 30 - but that is the "throw ex.InnerException;" line! We've lost the information we had earlier about what was really the offending method and line of code, because the stack trace got reset when we re-threw the inner exception.
Solving Our Reflection Woes with More Reflection
A mountain bike mechanic I once knew used to say that if a bit of force won't straighten a rim, you're just not applying enough of it. Chris Taylor uses the same mentality to solve the problem I've described here; he's applying a bit more reflection to straighten out the stack trace problem caused by the use of reflection in the first place. His solution is basically to copy across the stack trace from the TargetInvocationException and sticking it into the inner exception before re-throwing it - quite brilliant in it's simplicity (if the rim is bent, just unbend it!).
private static void InvokeRealMethodByReflection(string[] args)
{
MethodInfo method = typeof(Program).GetMethod("RealMethod");
object arg = null;
try
{
method.Invoke(null, new object[] {arg});
}
catch(TargetInvocationException ex)
{
FieldInfo remoteStackTraceString = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.Instance | BindingFlags.NonPublic);
remoteStackTraceString.SetValue(ex.InnerException, ex.InnerException.StackTrace + Environment.NewLine);
throw ex.InnerException;
}
}
Now, if we re-run the app, we get the desired ArgumentNullException and the desired stack trace:
at PreserveStackTrace.Program.RealMethod(Object arg) in {...}\Program.cs:line 15
at PreserveStackTrace.Program.InvokeRealMethodByReflection(String[] args) in {...}\Program.cs:line 33
at PreserveStackTrace.Program.Main(String[] args) in {...}\Program.cs:line 10
at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.ThreadHelper.ThreadStart()