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

StackOverflow https://stackoverflow.com/questions/57383

  •  09-06-2019
  •  | 
  •  

Вопрос

Я вызываю посредством отражения метод, который может вызвать исключение.Как я могу передать исключение вызывающему объекту без отражения оболочки?
Я повторно вызываю 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;
    }
}
Это было полезно?

Решение

В .NET 4.5 теперь есть ExceptionDispatchInfo сорт.

Это позволяет вам перехватить исключение и повторно выдать его, не меняя трассировку стека:

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

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

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

Другие советы

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

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

Я думаю, лучше всего было бы просто поместить это в блок catch:

throw;

А затем извлеките внутреннее исключение позже.

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

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

Еще больше размышлений...

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.См. дальнейшее обсуждение Моно багзилла.

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

Никто не объяснил разницу между 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, поскольку номер строки исходного исключения сохраняется, но не является реальным повторным созданием, поскольку меняет тип исходного исключения.

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

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

    }

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

    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 });
    }
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top