将 StackTrace 附加到异常而不抛出 C# / .NET

问题描述 投票:0回答:8

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

我想让包装器抛出一个异常,其中包括从组件捕获的堆栈跟踪信息。我希望它看起来好像异常是从错误的原始站点抛出的,即使它是在其他地方抛出的。 更具体地说,我希望 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:抛出了“CustomException”类型的异常。

ExceptionDispatchInfo 也会从私有属性中查找堆栈跟踪数据,因此它找不到任何该数据,并且当您抛出此自定义异常时,新的堆栈跟踪将附加到异常及其所在位置被抛出。如果直接使用 throw,私有堆栈信息将设置为发生 throw 的位置。

c# .net
8个回答
50
投票

只需创建您自己的

Exception
类型并覆盖
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;
        }
    }
}

15
投票

您可以使用:

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() 方法,使其相信堆栈是由 throw 生成的(实际上没有抛出任何内容)。

使用方法见下文:

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

8
投票

有一个更复杂的解决方案,避免反射部分的任何运行时开销

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

7
投票

对我来说,以下似乎是最好的方法。这种方法非常适合记录器,因为

.ToString()
Exception
使用私有字段进行堆栈跟踪。

public class CustomException : Exception
{
    public CustomException()
    {
        var stackTraceField = typeof(CustomException).BaseType
            .GetField("_stackTraceString", BindingFlags.Instance | BindingFlags.NonPublic);

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

4
投票

自 .NET 5 起,有 ExceptionDispatchInfo.SetCurrentStackTrace,它使用当前堆栈跟踪填充异常。

.NET 6 还引入了 ExceptionDispatchInfo.SetRemoteStackTrace,它接受任意字符串。

但请注意,这些方法在

_remoteStackTraceString
类型上设置
Exception
字段,这与此处通常设置
_stackTraceString
字段的其他答案相反。因此,结果行为可能会有所不同,但应该足以满足原始问题的目的。


0
投票

这是 hannasm 答案的现代版本,它利用新的

[UnsafeAccessor]
属性,因此需要 .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;
    }
}

请参阅 Gérald Barré(又名 Meziantou)的 C# 中无需反射的情况下访问私有成员,了解更多信息。

如果您不介意在提供的堆栈跟踪字符串末尾添加额外的

--- End of stack trace from previous location ---
行,那么按照 Zdeněk Jelínek 的建议使用
ExceptionDispatchInfo.SetRemoteStackTrace(exception, stackTrace)
可能是更好的解决方案,因为它仅使用公共 API,并且不使用访问私人领域。


-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);
© www.soinside.com 2019 - 2024. All rights reserved.