Reputation: 2041
I have a component which handles errors using return values as opposed to standard exception handling. In addition to the error code, it also returns a stack trace of where the error has occurred. A wrapper I use to invoke the component will interpret return codes and throw an exception.
I'd like to have the wrapper throw an exception that includes the captured stack trace information from the component. I'd like it to appear as if the exception had been thrown from the original site of the error even though it was thrown elsewhere. More specifically, I'd like the stack trace displayed by the Visual Studio test runner to reflect the proper location.
Is there any way to do this? It would also be nice if I could avoid low-level reflection tricks accessing private members but I will take what I can get.
I'm not concerned with how to capture a stack trace, I'm concerned with attaching a stack trace that's already been captured, to an Exception.
I tried overriding the StackTrace property, but Visual Studio is pulling the stack trace data from somewhere else, and seems to ignore the overridden property completely
CustomException GenerateExcpetion()
{
return new CustomException();
}
void ThrowException(Exception ex)
{
Trace.WriteLine("Displaying Exception");
Trace.WriteLine(ex.ToString());
var edi = ExceptionDispatchInfo.Capture(ex);
edi.Throw();
}
[TestMethod]
public void Test006()
{
var ex = GenerateExcpetion();
ThrowException(ex);
}
public class CustomException : Exception
{
string _stackTrace;
public CustomException()
{
_stackTrace = Environment.StackTrace;
}
public override string StackTrace
{
get
{
return base.StackTrace;
}
}
}
The Exception.ToString()
method pulls stack trace data from a private property, and so the stack trace coming from the override doesn't get shown.
CustomException: Exception of type 'CustomException' was thrown.
The ExceptionDispatchInfo looks for the stack trace data from a private property as well, and so it can't find any of that data, and when you throw this custom exception a new stack trace is attached to the exception with the location that it is being thrown from. If you use throw directly, the private stack information is set to the place where the throw happened.
Upvotes: 57
Views: 34665
Reputation: 26518
Here's a modern version of hannasm's answer that leverages the new [UnsafeAccessor]
attribute, thus requiring .NET 8.
using System.Runtime.CompilerServices;
namespace System;
internal static class ExceptionExtensions
{
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_stackTraceString")]
private static extern ref string _stackTraceString(Exception exception);
public static Exception SetStackTrace(this Exception exception, string stackTrace)
{
_stackTraceString(exception) = stackTrace;
return exception;
}
}
See Accessing private members without reflection in C# by Gérald Barré (a.k.a. Meziantou) for more information.
If you don't mind having an extra line with --- End of stack trace from previous location ---
at the end of the provided stack trace string then using ExceptionDispatchInfo.SetRemoteStackTrace(exception, stackTrace)
as suggested by Zdeněk Jelínek is probably a better solution since it uses only a public API and doesn't access a private field.
Upvotes: 0
Reputation: 2913
Since .NET 5, there is ExceptionDispatchInfo.SetCurrentStackTrace which populates an exception with the current stack trace.
.NET 6 additionally introduced ExceptionDispatchInfo.SetRemoteStackTrace which takes an arbitrary string.
Note, however, that these methods set the _remoteStackTraceString
field on the Exception
type which is opposed to the other answers here, where the _stackTraceString
field is typically set. The resulting behavior may therefore be different but should suffice for the purpose of the original question.
Upvotes: 6
Reputation: 1845
There is an even more sophisticated solution that avoids any runtime overhead of the reflection part:
public static class ExceptionExtensions
{
public static Exception SetStackTrace(this Exception target, StackTrace stack) => _SetStackTrace(target, stack);
private static readonly Func<Exception, StackTrace, Exception> _SetStackTrace = new Func<Func<Exception, StackTrace, Exception>>(() =>
{
ParameterExpression target = Expression.Parameter(typeof(Exception));
ParameterExpression stack = Expression.Parameter(typeof(StackTrace));
Type traceFormatType = typeof(StackTrace).GetNestedType("TraceFormat", BindingFlags.NonPublic);
MethodInfo toString = typeof(StackTrace).GetMethod("ToString", BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { traceFormatType }, null);
object normalTraceFormat = Enum.GetValues(traceFormatType).GetValue(0);
MethodCallExpression stackTraceString = Expression.Call(stack, toString, Expression.Constant(normalTraceFormat, traceFormatType));
FieldInfo stackTraceStringField = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance);
BinaryExpression assign = Expression.Assign(Expression.Field(target, stackTraceStringField), stackTraceString);
return Expression.Lambda<Func<Exception, StackTrace, Exception>>(Expression.Block(assign, target), target, stack).Compile();
})();
}
Upvotes: 8
Reputation: 2613
You can use: Environment.StackTrace
to capture the stacktrace at the occurence of the error in the component and then return it together with the other error informations or rethrow.
You can manually build stackframes to create a full trace. See StackFrame/StackTrace for more information.
Upvotes: 15
Reputation: 113
var error = new Exception(ex.Message);
var STACK_TRACE_STRING_FI = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance);
STACK_TRACE_STRING_FI.SetValue(error, ex.StackTrace);
Upvotes: -3
Reputation: 8852
For me following seems to be the best way to do it. This approach works well with loggers since .ToString()
for Exception
uses private field for stack trace.
public class CustomException : Exception
{
public CustomException()
{
var stackTraceField = typeof(CustomException).BaseType
.GetField("_stackTraceString", BindingFlags.Instance | BindingFlags.NonPublic);
stackTraceField.SetValue(this, Environment.StackTrace);
}
}
Upvotes: 7
Reputation: 2041
Well with nothing elegant available, here is my reflection based approach.
public static class ExceptionUtilities
{
private static readonly FieldInfo STACK_TRACE_STRING_FI = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance);
private static readonly Type TRACE_FORMAT_TI = Type.GetType("System.Diagnostics.StackTrace").GetNestedType("TraceFormat", BindingFlags.NonPublic);
private static readonly MethodInfo TRACE_TO_STRING_MI = typeof(StackTrace).GetMethod("ToString", BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { TRACE_FORMAT_TI }, null);
public static Exception SetStackTrace(this Exception target, StackTrace stack)
{
var getStackTraceString = TRACE_TO_STRING_MI.Invoke(stack, new object[] { Enum.GetValues(TRACE_FORMAT_TI).GetValue(0) });
STACK_TRACE_STRING_FI.SetValue(target, getStackTraceString);
return target;
}
}
Writing a formatted StackTrace string to the _stackTraceString property seems to be enough to fool visual studio test runner and the Exception.ToString() methods into believing the stack was generated by a throw (without actually throwing anything).
See below for usage:
StackTrace GetDeeperStackTrace(int depth)
{
if (depth > 0)
{
return GetDeeperStackTrace(depth - 1);
}
else
{
return new StackTrace(0, true);
}
}
[TestMethod]
public void Test007()
{
Exception needStackTrace = new Exception("Some exception");
var st = GetDeeperStackTrace(3);
needStackTrace.SetStackTrace(st);
Trace.Write(needStackTrace.ToString());
throw new Exception("Nested has custom stack trace", needStackTrace);
}
Upvotes: 12
Reputation: 13652
Just create your own Exception
type and override the StackTrace
property:
class MyException : Exception
{
private string oldStackTrace;
public MyException(string message, string stackTrace) : base(message)
{
this.oldStackTrace = stackTrace;
}
public override string StackTrace
{
get
{
return this.oldStackTrace;
}
}
}
Upvotes: 49