在C# / .NET中不抛出异常时附加StackTrace到异常

51

我有一个组件,它使用返回值处理错误,而不是标准的异常处理。除了错误代码之外,它还返回错误发生位置的堆栈跟踪信息。我用来调用该组件的包装器将解释返回代码并抛出异常。

我希望包装器抛出一个异常,其中包含从组件捕获的堆栈跟踪信息。我希望它看起来就像异常是从错误的原始位置抛出的,即使它是从其他地方抛出的。 更具体地说,我希望Visual Studio测试运行器显示的堆栈跟踪反映正确的位置。

有没有办法做到这一点?如果我可以避免访问私有成员的低级反射技巧,那就太好了,但我会尽力而为。

我不关心如何捕获堆栈跟踪,我关心的是将已经捕获的堆栈跟踪附加到异常上。

我尝试重写StackTrace属性,但是Visual Studio似乎从其他地方提取堆栈跟踪数据,并完全忽略重写的属性

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;
        }
    }
}
Exception.ToString() 方法会从一个私有属性中提取堆栈跟踪数据,因此不会显示覆盖版本的堆栈跟踪。CustomException: Exception of type 'CustomException' was thrown. ExceptionDispatchInfo 也会查找私有属性中的堆栈跟踪数据,所以找不到任何该数据,当您抛出这个自定义异常时,一个新的堆栈跟踪会附加到异常上,并带有其被抛出的位置。如果直接使用 throw,那么私有的堆栈信息将设置为 throw 发生的位置。

为什么你不直接放弃错误码呢? - wingerse
@EmperorAiman 我猜这是第三方组件? - adjan
@adjan,那一定是来自90年代。 - wingerse
7个回答

47

只需创建自己的异常类型并覆盖StackTrace属性:

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;
        }
    }
}

3
这种方法的问题在于当异常被传递给日志记录器时,堆栈跟踪仍然是旧的而不是被覆盖的。唯一能使此方法起作用的方式是通过反射。 - Mayank
1
实际上不需要创建 oldStackTrace。通过重写 StackTrace,它可以在构造函数中设置。 - Kyle G.
3
当你调用异常对象的.ToString()方法时,这种方法并不好用,因为其底层的ToString()方法使用了对象的私有字段而非属性。 - Mayank
@Mayank 只需要覆盖 ToString() 方法吗? - adjan
@adjan,那样做不能正常工作,因为如果代码的某些部分明确使用堆栈,则会有无效信息。 - Mayank

13

您可以使用Environment.StackTrace捕获组件中错误发生时的堆栈跟踪,并将其与其他错误信息一起返回或重新抛出。

您可以手动构建堆栈帧以创建完整的跟踪。有关更多信息,请参见StackFrame/StackTrace


12

由于没有更好的方法可用,这里是基于我的经验反思的方法。

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;
    }
}

将格式化的StackTrace字符串写入_stackTraceString属性似乎足以欺骗Visual Studio测试运行程序和Exception.ToString()方法,使它们相信堆栈是通过抛出异常生成的(而实际上并没有抛出任何异常)。

请参阅以下用法:

    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);
    }

为了满足探究精神,提供更多信息:StackTrace.TraceFormat 是一个枚举类型(通过 Enum.GetValues 可以看出),第一个值是 TraceFormat.Normal。可以在这里查看 StackTrace.TraceFormat 的参考源代码:https://referencesource.microsoft.com/#mscorlib/system/diagnostics/stacktrace.cs,625,并且通过这里调用 ToString 方法:https://referencesource.microsoft.com/#mscorlib/system/diagnostics/stacktrace.cs,637。 - mhand
这个可行。单元测试现在通过了。有一个问题,如果有人知道答案,请回答。尽管堆栈跟踪具有我使用此函数分配的值,但是如果我使用断点打开异常,我仍然会看到堆栈跟踪上的旧值。就像VS没有刷新一样。 - hal9000

8
对我而言,以下方法似乎是最佳的实现方式。使用这种方法可以很好地处理日志记录器的异常信息,因为Exception.ToString()方法使用私有字段来获取堆栈跟踪信息。
public class CustomException : Exception
{
    public CustomException()
    {
        var stackTraceField = typeof(CustomException).BaseType
            .GetField("_stackTraceString", BindingFlags.Instance | BindingFlags.NonPublic);

        stackTraceField.SetValue(this, Environment.StackTrace);
    }
}

这非常适合我的需求(单元测试以确保方法在测试下使用了异常的堆栈跟踪)。 - Kyle G.
1
你可以使用 typeof(Exception) - Alex Kovanev

7
有一种更为复杂的解决方案,它避免了反射部分的任何运行时开销
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();
    })();
}

Net Core仍然没有内置的方法吗? - sommmen
@Marcel,如果堆栈跟踪以“字符串”对象而不是StackTrace形式接收,您有什么修改此代码的建议吗? - SR8
@SR8 你不能从绞肉中制造出一头牛。只有少数(大多数是简单的)对象允许使用 ToString() 进行转换。StackTrace 不属于其中之一。它甚至没有公共构造函数,无法从反射数据中自己创建它。 - Marcel

1
自从.NET 5以来,有一个ExceptionDispatchInfo.SetCurrentStackTrace方法,它可以将异常与当前的堆栈跟踪信息关联起来。
而.NET 6还引入了ExceptionDispatchInfo.SetRemoteStackTrace方法,它接受一个任意字符串作为参数。
需要注意的是,这些方法会在Exception类型上设置_remoteStackTraceString字段,而不是通常情况下设置的_stackTraceString字段。因此,结果行为可能会有所不同,但对于原始问题的目的应该足够满足要求。

我发现使用反射来设置_stackTraceString(就像其他地方那样),在使用ExceptionDispatchInfo.Capture/Throw时无法正常工作。 - Zdeněk Jelínek

-3
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);

这就是另一个答案所建议的。 - Wai Ha Lee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接