Wie kann ich InnerException erneut auslösen, ohne die Stapelverfolgung in C # zu verlieren?

305

Ich rufe durch Reflexion eine Methode auf, die eine Ausnahme verursachen kann. Wie kann ich die Ausnahme an meinen Anrufer weitergeben, ohne dass die Wrapper-Reflexion sie umgibt?
Ich werfe die InnerException erneut, aber dies zerstört die Stapelverfolgung.
Beispielcode:

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;
    }
}
Skolima
quelle
1
Es gibt einen anderen Weg, der kein Voodoo erfordert. Schauen Sie sich die Antwort hier an: stackoverflow.com/questions/15668334/…
Timothy Shields
Die in der dynamisch aufgerufenen Methode ausgelöste Ausnahme ist die innere Ausnahme der Ausnahme "Ausnahme wurde vom Ziel eines Aufrufs ausgelöst". Es hat eine eigene Stapelverfolgung. Es gibt wirklich nicht viel zu befürchten.
Ajeh

Antworten:

481

In .NET 4.5 gibt es jetzt die ExceptionDispatchInfoKlasse.

Auf diese Weise können Sie eine Ausnahme erfassen und erneut auslösen, ohne die Stapelverfolgung zu ändern:

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

Dies funktioniert mit jeder Ausnahme, nicht nur AggregateException.

Es wurde aufgrund der awaitC # -Sprachenfunktion eingeführt, die die inneren Ausnahmen von AggregateExceptionInstanzen auspackt , um die asynchronen Sprachfunktionen den synchronen Sprachfunktionen ähnlicher zu machen.

Paul Turner
quelle
11
Guter Kandidat für eine Exception.Rethrow () -Erweiterungsmethode?
Nmarler
8
Beachten Sie, dass sich die ExceptionDispatchInfo-Klasse im Namespace System.Runtime.ExceptionServices befindet und vor .NET 4.5 nicht verfügbar ist.
Jojo
53
Möglicherweise müssen Sie throw;nach der Zeile .Throw () eine reguläre Zeile einfügen, da der Compiler nicht weiß, dass .Throw () immer eine Ausnahme auslöst. throw;wird daher nie aufgerufen, aber zumindest wird sich der Compiler nicht beschweren, wenn Ihre Methode ein Rückgabeobjekt erfordert oder eine asynchrone Funktion ist.
Todd
5
@Taudris Bei dieser Frage geht es speziell um das erneute Auslösen der inneren Ausnahme, die von nicht speziell behandelt werden kann throw;. Wenn Sie throw ex.InnerException;den Stack-Trace verwenden, wird er an dem Punkt neu initialisiert, an dem er erneut ausgelöst wird.
Paul Turner
5
@amitjhaExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw();
Vedran
86

Es ist möglich, die Stapelspur vor dem erneuten Werfen ohne Reflexion beizubehalten:

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
}

Dies verschwendet viele Zyklen im Vergleich zum Aufrufen InternalPreserveStackTraceüber einen zwischengespeicherten Delegaten, hat jedoch den Vorteil, dass nur öffentliche Funktionen verwendet werden. Hier sind einige gängige Verwendungsmuster für Stack-Trace-Erhaltungsfunktionen:

// 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 ;
}
Anton Tykhyy
quelle
Sieht cool aus, was muss nach dem Ausführen dieser Funktionen passieren?
Vdboor
2
Tatsächlich ist es nicht viel langsamer als das Aufrufen InternalPreserveStackTrace(ungefähr 6% langsamer mit 10000 Iterationen). Der direkte Zugriff auf die Felder durch Reflexion ist etwa 2,5% schneller als das AufrufenInternalPreserveStackTrace
Thomas Levesque
1
Ich würde empfehlen, das e.DataWörterbuch mit einer Zeichenfolge oder einem eindeutigen Objektschlüssel zu verwenden ( static readonly object myExceptionDataKey = new object ()aber tun Sie dies nicht, wenn Sie irgendwo Ausnahmen serialisieren müssen). Vermeiden Sie Änderungen e.Message, da Sie möglicherweise irgendwo Code haben, der analysiert wird e.Message. Das Parsen e.Messageist böse, aber es gibt möglicherweise keine andere Wahl, z. B. wenn Sie eine Bibliothek eines Drittanbieters mit schlechten Ausnahmepraktiken verwenden müssen.
Anton Tykhyy
10
DoFixups bricht für benutzerdefinierte Ausnahmen ab, wenn sie nicht die Serialisierung ctor
ruslander
3
Die vorgeschlagene Lösung funktioniert nicht, wenn die Ausnahme keinen Serialisierungskonstruktor hat. Ich schlage vor, die unter stackoverflow.com/a/4557183/209727 vorgeschlagene Lösung zu verwenden , die auf jeden Fall gut funktioniert. Verwenden Sie für .NET 4.5 die ExceptionDispatchInfo-Klasse.
Davide Icardi
33

Ich denke, Ihre beste Wette wäre, dies einfach in Ihren Fangblock zu legen:

throw;

Und extrahieren Sie später die innere Ausnahme.

GEOCHET
quelle
21
Oder entfernen Sie den Versuch / Fang ganz.
Daniel Earwicker
6
@Earwicker. Das Entfernen von try / catch ist im Allgemeinen keine gute Lösung, da Fälle ignoriert werden, in denen Bereinigungscode erforderlich ist, bevor die Ausnahme auf den Aufrufstapel übertragen wird.
Jordanien
12
@Jordan - Abgleich - Code in einem finally - Block nicht einen catch - Block sein sollte
Paolo
17
@Paolo - Wenn es in jedem Fall ausgeführt werden soll, ja. Wenn es nur im Fehlerfall ausgeführt werden soll, nein.
Chiccodoro
4
Denken Sie daran, dass InternalPreserveStackTrace nicht threadsicher ist. Wenn Sie also 2 Threads in einem dieser Ausnahmezustände haben ... möge Gott uns allen gnädig sein.
Rob
14

Niemand hat den Unterschied zwischen ExceptionDispatchInfo.Capture( ex ).Throw()und einer Ebene erklärtthrow , also hier ist es.

Der vollständige Weg, um eine abgefangene Ausnahme erneut auszulösen, ist die Verwendung ExceptionDispatchInfo.Capture( ex ).Throw() (nur ab .NET 4.5 verfügbar).

Nachfolgend sind die Fälle aufgeführt, die zum Testen erforderlich sind:

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

Fall 1 und Fall 2 geben Ihnen eine Stapelverfolgung, wobei die Quellcode-Zeilennummer für die CallingMethodMethode die Zeilennummer der istthrow new Exception( "TEST" ) Zeile ist.

In Fall 3 erhalten Sie jedoch eine Stapelverfolgung, bei der die Quellcode-Zeilennummer für die CallingMethodMethode die Zeilennummer des throwAufrufs ist. Dies bedeutet, dass throw new Exception( "TEST" )Sie keine Ahnung haben, bei welcher Zeilennummer die Ausnahme tatsächlich ausgelöst wurde , wenn die Zeile von anderen Operationen umgeben ist.

Fall 4 ist ähnlich wie Fall 2, da die Zeilennummer der ursprünglichen Ausnahme beibehalten wird, jedoch kein echter Neuauswurf ist, da der Typ der ursprünglichen Ausnahme geändert wird.

jeuoekdcwzfwccu
quelle
5
Ich habe immer gedacht, dass 'throw' den Stacktrace nicht zurücksetzt (im Gegensatz zu 'throw e').
Jesper Matthiesen
@JesperMatthiesen Ich könnte mich irren, aber ich habe gehört, dass es davon abhängt, ob die Ausnahme ausgelöst und in derselben Datei abgefangen wurde. Wenn es sich um dieselbe Datei handelt, geht die Stapelverfolgung verloren. Wenn es sich um eine andere Datei handelt, bleibt sie erhalten.
Jahu
13
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 );
    }
}

Rufen Sie die Erweiterungsmethode für Ihre Ausnahme auf, bevor Sie sie auslösen. Dadurch bleibt die ursprüngliche Stapelverfolgung erhalten.

Eric
quelle
Beachten Sie, dass InternalPreserveStackTrace in .Net 4.0 jetzt ein No-Op ist. Schauen Sie in Reflector nach und Sie werden sehen, dass die Methode vollständig leer ist!
Samuel Jack
Scratch that: Ich habe mir den RC angesehen: In der Beta haben sie die Implementierung wieder zurückgesetzt!
Samuel Jack
3
Vorschlag: Ändern Sie PreserveStackTrace, um ex zurückzugeben. Um dann eine Ausnahme auszulösen, können Sie einfach sagen: throw ex.PreserveStackTrace ();
Simon_Weaver
Warum verwenden Action<Exception>? Hier verwenden Sie statische Methode
Kiquenet
10

Noch mehr Reflexion ...

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

Beachten Sie, dass dies jederzeit unterbrochen werden kann, da private Felder nicht Teil der API sind. Weitere Diskussion zu Mono Bugzilla .

Skolima
quelle
28
Dies ist eine sehr, sehr schlechte Idee, da sie von internen undokumentierten Details zu Framework-Klassen abhängt.
Daniel Earwicker
1
Es stellt sich heraus, dass es möglich ist, die Stapelspur ohne Reflexion beizubehalten, siehe unten.
Anton Tykhyy
1
Die interne InternalPreserveStackTraceMethode aufzurufen wäre besser, da sie dasselbe tut und sich in Zukunft weniger wahrscheinlich ändert ...
Thomas Levesque
1
Eigentlich wäre es schlimmer, da InternalPreserveStackTrace auf Mono nicht existiert.
Skolima
5
@ Daniel - nun, es ist eine wirklich, wirklich, wirklich schlechte Idee zum Werfen; um den Stacktrace zurückzusetzen, wenn jeder .net-Entwickler darauf trainiert ist, zu glauben, dass dies nicht der Fall ist. Es ist auch eine sehr, sehr, sehr schlechte Sache, wenn Sie die Quelle einer NullReferenceException nicht herausfinden und einen Kunden / eine Bestellung verlieren können, weil Sie sie nicht finden können. Für mich übertrumpft das "undokumentierte Details" und definitiv Mono.
Simon_Weaver
10

Erstens: Verlieren Sie nicht die TargetInvocationException - es sind wertvolle Informationen, wenn Sie Dinge debuggen möchten.
Zweitens: Wickeln Sie die TIE als InnerException in Ihren eigenen Ausnahmetyp ein und fügen Sie eine OriginalException-Eigenschaft ein, die mit dem verknüpft ist, was Sie benötigen (und den gesamten Callstack intakt halten).
Drittens: Lassen Sie die KRAWATTE aus Ihrer Methode heraussprudeln.

Kokos
quelle
5

Leute, ihr seid cool. Ich werde bald ein Nekromant sein.

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

    }
Boris Treukhov
quelle
1
Gute Idee, aber Sie kontrollieren nicht immer den Code, der aufruft .Invoke().
Anton Tykhyy
1
Und Sie kennen auch nicht immer die Typen der Argumente / Ergebnisse zur Kompilierungszeit.
Roman Starkov
3

Ein weiterer Beispielcode, der die Serialisierung / Deserialisierung von Ausnahmen verwendet. Der tatsächliche Ausnahmetyp muss nicht serialisierbar sein. Außerdem werden nur öffentliche / geschützte Methoden verwendet.

    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 });
    }
Hühnerprodukt
quelle
Der tatsächliche Ausnahmetyp muss nicht serialisierbar sein?
Kiquenet
3

Basierend auf der Antwort von Paul Turners habe ich eine Erweiterungsmethode erstellt

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

Das return exist nie erreicht, aber der Vorteil ist, dass ich es throw ex.Capture()als Einzeiler verwenden kann, damit der Compiler keinen not all code paths return a valueFehler auslöst.

    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();
        }
    }
Jürgen Steinblock
quelle