Come ricodificare InnerException senza perdere la traccia dello stack in C #?


305

Chiamo, attraverso la riflessione, un metodo che può causare un'eccezione. Come posso passare l'eccezione al mio chiamante senza che la riflessione del wrapper lo metta?
Sto ridisegnando InnerException, ma questo distrugge la traccia dello stack.
Codice di esempio:

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
C'è un altro modo per farlo che non richiede alcun voodoo. Dai un'occhiata alla risposta qui: stackoverflow.com/questions/15668334/…
Timothy Shields

L'eccezione generata nel metodo chiamato dinamicamente è l'eccezione interna dell'eccezione "Eccezione è stata generata dal target di una chiamata". Ha una propria traccia di stack. Non c'è davvero molto altro di cui preoccuparsi.
aje,

Risposte:


481

In .NET 4.5 ora c'è la ExceptionDispatchInfoclasse.

Ciò consente di catturare un'eccezione e rilanciarla senza modificare lo stack-trace:

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

Funziona su qualsiasi eccezione, non solo AggregateException.

È stato introdotto a causa della awaitfunzionalità del linguaggio C #, che scosta le eccezioni interne dalle AggregateExceptionistanze al fine di rendere le funzionalità del linguaggio asincrono più simili alle funzionalità del linguaggio sincrono.


11
Buon candidato per un metodo di estensione Exception.Rethrow ()?
nmarler,

8
Si noti che la classe ExceptionDispatchInfo si trova nello spazio dei nomi System.Runtime.ExceptionServices e non è disponibile prima di .NET 4.5.
yoyo

53
Potrebbe essere necessario inserire un normale throw;dopo la riga .Throw (), poiché il compilatore non saprà che .Throw () genera sempre un'eccezione. throw;non verrà mai chiamato come risultato, ma almeno il compilatore non si lamenterà se il tuo metodo richiede un oggetto return o è una funzione asincrona.
Todd,

5
@Taudris Questa domanda riguarda in particolare il rilancio dell'eccezione interiore, che non può essere gestita in modo speciale throw;. Se si utilizza throw ex.InnerException;lo stack-trace viene reinizializzato nel punto in cui viene rilanciato.
Paul Turner,

5
@amitjhaExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw();
Vedran,

86

Si è possibile conservare la traccia dello stack prima rethrowing senza riflessione:

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
}

Questo spreca molti cicli rispetto alla chiamata InternalPreserveStackTracetramite delegato memorizzato nella cache, ma ha il vantaggio di fare affidamento solo sulla funzionalità pubblica. Ecco alcuni schemi di utilizzo comuni per le funzioni di conservazione dello stack-trace:

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

Sembra bello, cosa deve succedere dopo aver eseguito queste funzioni?
vdboor,

2
In realtà, non è molto più lento di invocare InternalPreserveStackTrace(circa il 6% più lento con 10000 iterazioni). L'accesso diretto ai campi tramite la riflessione è circa il 2,5% più veloce rispetto all'invocazioneInternalPreserveStackTrace
Thomas Levesque,

1
Consiglierei di usare il e.Datadizionario con una stringa o una chiave oggetto univoca ( static readonly object myExceptionDataKey = new object (), ma non farlo se devi serializzare le eccezioni ovunque). Evita di modificarlo e.Message, perché potresti avere del codice da qualche parte che analizza e.Message. L'analisi e.Messageè malvagia, ma potrebbe non esserci altra scelta, ad esempio se è necessario utilizzare una libreria di terze parti con pratiche di eccezione scadenti.
Anton Tykhyy,

10
DoFixups si rompe per eccezioni personalizzate se non hanno il ctor di serializzazione
ruslander

3
La soluzione suggerita non funziona se l'eccezione non ha un costruttore di serializzazione. Suggerisco di utilizzare la soluzione proposta su stackoverflow.com/a/4557183/209727 che funziona bene in ogni caso. Per .NET 4.5 considerare l'utilizzo della classe ExceptionDispatchInfo.
Davide Icardi,

33

Penso che la tua scommessa migliore sarebbe quella di mettere questo nel tuo blocco di cattura:

throw;

E poi estrarre l'innerexception più tardi.


21
Oppure rimuovi del tutto il tentativo / cattura.
Daniel Earwicker,

6
@Earwicker. La rimozione di try / catch non è una buona soluzione in generale poiché ignora i casi in cui è richiesto il codice di pulizia prima di propagare l'eccezione nello stack di chiamate.
Giordania,

12
@Jordan - Il codice di pulizia dovrebbe trovarsi in un blocco infine non in un blocco catch
Paolo

17
@Paolo - Se dovrebbe essere eseguito in ogni caso, sì. Se si suppone che debba essere eseguito solo in caso di fallimento, no.
Chiccodoro,

4
Tieni presente che InternalPreserveStackTrace non è thread-safe, quindi se hai 2 thread su questi stati di eccezione ... Dio potrebbe avere pietà di tutti noi.
Rob

14

Nessuno ha spiegato la differenza tra ExceptionDispatchInfo.Capture( ex ).Throw()e una pianura throw, quindi eccola qui.

Il modo completo per ricodificare un'eccezione rilevata è utilizzare ExceptionDispatchInfo.Capture( ex ).Throw()(disponibile solo da .Net 4.5).

Di seguito sono riportati i casi necessari per testare questo:

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

Il caso 1 e il caso 2 forniscono una traccia dello stack in cui il numero di riga del codice sorgente per il CallingMethodmetodo è il numero di riga delthrow new Exception( "TEST" ) riga riga.

Tuttavia, il caso 3 fornisce una traccia dello stack in cui il numero di riga del codice sorgente per il CallingMethodmetodo è il numero di riga della throwchiamata. Ciò significa che se la throw new Exception( "TEST" )linea è circondata da altre operazioni, non si ha idea di quale numero di riga sia stata effettivamente generata l'eccezione.

Il caso 4 è simile al caso 2 perché viene conservato il numero di riga dell'eccezione originale, ma non è un vero e proprio rilancio perché modifica il tipo di eccezione originale.


5
Ho sempre pensato che "lancio" non ripristinasse lo stacktrace (al contrario di "lancio e").
Jesper Matthiesen,

@JesperMatthiesen Potrei sbagliarmi, ma ho sentito che dipende se l'eccezione è stata generata e catturata nello stesso file. Se è lo stesso file, la traccia dello stack andrà persa, se è un altro file, verrà conservata.
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 );
    }
}

Chiama il metodo di estensione sulla tua eccezione prima di lanciarlo, manterrà la traccia dello stack originale.


Tieni presente che in .Net 4.0, InternalPreserveStackTrace è ora un no-op - guarda in Reflector e vedrai che il metodo è completamente vuoto!
Samuel Jack,

Scratch that: stavo guardando l'RC: nella beta, hanno ripristinato di nuovo l'implementazione!
Samuel Jack,

3
suggerimento: cambia PreserveStackTrace per restituire ex - quindi per lanciare un'eccezione puoi semplicemente dire: lanciare ex.PreserveStackTrace ();
Simon_Weaver

Perché l'uso Action<Exception>? Qui usa il metodo statico
Kiquenet,

10

Ancora più riflessione ...

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

Tieni presente che ciò può interrompersi in qualsiasi momento, poiché i campi privati ​​non fanno parte dell'API. Vedi ulteriori discussioni su Mono bugzilla .


28
Questa è una pessima idea, in quanto dipende da dettagli interni privi di documenti sulle classi di framework.
Daniel Earwicker,

1
Si scopre che è possibile conservare la traccia dello stack senza Reflection, vedere di seguito.
Anton Tykhyy,

1
Chiamare il InternalPreserveStackTracemetodo interno sarebbe meglio, dal momento che fa la stessa cosa ed è meno probabile che cambi in futuro ...
Thomas Levesque,

1
In realtà, sarebbe peggio, poiché InternalPreserveStackTrace non esiste su Mono.
skolima,

5
@daniel - beh, è ​​davvero una pessima idea da buttare; reimpostare lo stacktrace quando ogni sviluppatore .net è addestrato a credere che non lo farà. è anche una cosa molto, molto, molto brutta se non riesci a scoprire la fonte di NullReferenceException e perdi un cliente / ordine perché non riesci a trovarlo. per me che supera i "dettagli non documentati" e decisamente mono.
Simon_Weaver

10

Primo: non perdere TargetInvocationException - sono informazioni preziose quando vorrai eseguire il debug delle cose.
Secondo: avvolgi TIE come InnerException nel tuo tipo di eccezione e inserisci una proprietà OriginalException che si collega a ciò di cui hai bisogno (e mantieni intatto l'intero callstack).
Terzo: lascia uscire il TIE dal tuo metodo.


5

Ragazzi, siete forti .. Presto sarò un negromante.

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

    }

1
Bella idea, ma non controlli sempre il codice che chiama .Invoke().
Anton Tykhyy,

1
E non sempre conosci anche i tipi degli argomenti / risultati al momento della compilazione.
Roman Starkov

3

Un altro codice di esempio che utilizza la serializzazione / deserializzazione delle eccezioni. Non richiede che il tipo di eccezione effettivo sia serializzabile. Inoltre utilizza solo metodi pubblici / protetti.

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

non è necessario che il tipo di eccezione effettivo sia serializzabile?
Kiquenet,

3

Basato sulla risposta di Paul Turners, ho creato un metodo di estensione

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

l' return exIST non ha mai raggiunto, ma il vantaggio è che posso usare throw ex.Capture()come uno di linea in modo che il compilatore non genererà un not all code paths return a valueerrore.

    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();
        }
    }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.