Как сбросить InnerException без потери трассировки стека в С#?

Я размышляю о методе, который может вызвать исключение. Как я могу передать исключение своему вызывающему абоненту без использования обертки?
Я перебрасываю InnerException, но это уничтожает трассировку стека.
Пример кода:

public void test1()
{
    // Throw an exception for testing purposes
    throw new ArgumentException("test1");
}

void test2()
{
    try
    {
        MethodInfo mi = typeof(Program).GetMethod("test1");
        mi.Invoke(this, null);
    }
    catch (TargetInvocationException tiex)
    {
        // Throw the new exception
        throw tiex.InnerException;
    }
}

Ответ 1

В .NET 4.5 теперь существует класс ExceptionDispatchInfo.

Это позволяет вам фиксировать исключение и повторно бросать его без изменения трассировки стека:

try
{
    task.Wait();
}
catch(AggregateException ex)
{
    ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
}

Это работает для любого исключения, а не только AggregateException.

Он был введен благодаря функции языка await С#, которая разворачивает внутренние исключения из экземпляров AggregateException, чтобы сделать асинхронные языковые функции более похожими на синхронные языковые функции.

Ответ 2

можно сохранить трассировку стека, прежде чем реконструировать без отражения:

static void PreserveStackTrace (Exception e)
{
    var ctx = new StreamingContext  (StreamingContextStates.CrossAppDomain) ;
    var mgr = new ObjectManager     (null, ctx) ;
    var si  = new SerializationInfo (e.GetType (), new FormatterConverter ()) ;

    e.GetObjectData    (si, ctx)  ;
    mgr.RegisterObject (e, 1, si) ; // prepare for SetObjectData
    mgr.DoFixups       ()         ; // ObjectManager calls SetObjectData

    // voila, e is unmodified save for _remoteStackTraceString
}

Это отнимает много циклов по сравнению с вызовом InternalPreserveStackTrace через кэшированный делегат, но имеет преимущество полагаться только на общедоступные функции. Вот несколько общих шаблонов использования для функций сохранения стека:

// usage (A): cross-thread invoke, messaging, custom task schedulers etc.
catch (Exception e)
{
    PreserveStackTrace (e) ;

    // store exception to be re-thrown later,
    // possibly in a different thread
    operationResult.Exception = e ;
}

// usage (B): after calling MethodInfo.Invoke() and the like
catch (TargetInvocationException tiex)
{
    PreserveStackTrace (tiex.InnerException) ;

    // unwrap TargetInvocationException, so that typed catch clauses 
    // in library/3rd-party code can work correctly;
    // new stack trace is appended to existing one
    throw tiex.InnerException ;
}

Ответ 3

Думаю, ваш лучший выбор - просто поместить это в свой блок catch:

throw;

И затем извлеките innerexception позже.

Ответ 4

public static class ExceptionHelper
{
    private static Action<Exception> _preserveInternalException;

    static ExceptionHelper()
    {
        MethodInfo preserveStackTrace = typeof( Exception ).GetMethod( "InternalPreserveStackTrace", BindingFlags.Instance | BindingFlags.NonPublic );
        _preserveInternalException = (Action<Exception>)Delegate.CreateDelegate( typeof( Action<Exception> ), preserveStackTrace );            
    }

    public static void PreserveStackTrace( this Exception ex )
    {
        _preserveInternalException( ex );
    }
}

Вызовите метод расширения в своем исключении перед его броском, он сохранит исходную трассировку стека.

Ответ 5

Еще большее отражение...

catch (TargetInvocationException tiex)
{
    // Get the _remoteStackTraceString of the Exception class
    FieldInfo remoteStackTraceString = typeof(Exception)
        .GetField("_remoteStackTraceString",
            BindingFlags.Instance | BindingFlags.NonPublic); // MS.Net

    if (remoteStackTraceString == null)
        remoteStackTraceString = typeof(Exception)
        .GetField("remote_stack_trace",
            BindingFlags.Instance | BindingFlags.NonPublic); // Mono

    // Set the InnerException._remoteStackTraceString
    // to the current InnerException.StackTrace
    remoteStackTraceString.SetValue(tiex.InnerException,
        tiex.InnerException.StackTrace + Environment.NewLine);

    // Throw the new exception
    throw tiex.InnerException;
}

Имейте в виду, что это может сломаться в любое время, поскольку частные поля не являются частью API. См. Дальнейшее обсуждение Mono bugzilla.

Ответ 6

Во-первых: не теряйте TargetInvocationException - это ценная информация, когда вы хотите отлаживать вещи.
Во-вторых: оберните TIE как InnerException в свой собственный тип исключения и поместите свойство OriginalException, которое ссылается на то, что вам нужно (и сохраните весь столбец без изменений).
В-третьих: пусть пузырь TIE из вашего метода.

Ответ 7

Никто не объяснил разницу между ExceptionDispatchInfo.Capture( ex ).Throw() и простой throw, так что вот оно.

Полный способ перебора пойманного исключения - использовать ExceptionDispatchInfo.Capture( ex ).Throw() (доступно только из .Net 4.5).

Ниже приведены случаи, необходимые для проверки этого:

1.

void CallingMethod()
{
    //try
    {
        throw new Exception( "TEST" );
    }
    //catch
    {
    //    throw;
    }
}

2.

void CallingMethod()
{
    try
    {
        throw new Exception( "TEST" );
    }
    catch( Exception ex )
    {
        ExceptionDispatchInfo.Capture( ex ).Throw();
        throw; // So the compiler doesn't complain about methods which don't either return or throw.
    }
}

3.

void CallingMethod()
{
    try
    {
        throw new Exception( "TEST" );
    }
    catch
    {
        throw;
    }
}

4.

void CallingMethod()
{
    try
    {
        throw new Exception( "TEST" );
    }
    catch( Exception ex )
    {
        throw new Exception( "RETHROW", ex );
    }
}

Случай 1 и случай 2 дадут вам трассировку стека, где номер строки исходного кода для метода CallingMethod - номер строки строки throw new Exception( "TEST" ).

Однако случай 3 даст вам трассировку стека, где номер строки исходного кода для метода CallingMethod - номер строки вызова throw. Это означает, что если строка throw new Exception( "TEST" ) окружена другими операциями, вы не имеете представления о том, какой номер строки было фактически выбрано.

Случай 4 аналогичен случаю 2, потому что номер строки исходного исключения сохраняется, но не является реальным ретроном, поскольку он изменяет тип исходного исключения.

Ответ 8

Ребята, вы классные.. Я скоро стану некромантом.

    public void test1()
    {
        // Throw an exception for testing purposes
        throw new ArgumentException("test1");
    }

    void test2()
    {
            MethodInfo mi = typeof(Program).GetMethod("test1");
            ((Action)Delegate.CreateDelegate(typeof(Action), mi))();

    }

Ответ 9

Код примера, который использует сериализацию/десериализацию исключений. Это не требует, чтобы фактический тип исключения был сериализуемым. Также он использует только общедоступные/защищенные методы.

    static void PreserveStackTrace(Exception e)
    {
        var ctx = new StreamingContext(StreamingContextStates.CrossAppDomain);
        var si = new SerializationInfo(typeof(Exception), new FormatterConverter());
        var ctor = typeof(Exception).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(SerializationInfo), typeof(StreamingContext) }, null);

        e.GetObjectData(si, ctx);
        ctor.Invoke(e, new object[] { si, ctx });
    }

Ответ 10

На основании ответа Пола Тернерса я сделал метод расширения

    public static Exception Capture(this Exception ex)
    {
        ExceptionDispatchInfo.Capture(ex).Throw();
        return ex;
    }

return ex ist никогда не достигается, но преимущество в том, что я могу использовать throw ex.Capture() в качестве однострочника, так что компилятор не будет вызывать, и not all code paths return a value ошибку not all code paths return a value.

    public static object InvokeEx(this MethodInfo method, object obj, object[] parameters)
    {
        {
            return method.Invoke(obj, parameters);
        }
        catch (TargetInvocationException ex) when (ex.InnerException != null)
        {
            throw ex.InnerException.Capture();
        }
    }