Comment renvoyer InnerException sans perdre la trace de la pile en C #?


305

J'appelle, par réflexion, une méthode qui peut provoquer une exception. Comment puis-je transmettre l'exception à mon appelant sans que la réflexion du wrapper ne l'entoure?
Je rejette l'InnerException, mais cela détruit la trace de la pile.
Exemple de code:

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
Il existe une autre façon de faire cela qui ne nécessite aucun vaudou. Jetez un œil à la réponse ici: stackoverflow.com/questions/15668334/…
Timothy Shields

L'exception levée dans la méthode appelée dynamiquement est l'exception interne de l'exception "L'exception a été levée par la cible d'une invocation". Il a sa propre trace de pile. Il n'y a vraiment pas grand-chose d'autre à craindre.
ajeh

Réponses:


481

Dans .NET 4.5, il y a maintenant la ExceptionDispatchInfoclasse.

Cela vous permet de capturer une exception et de la renvoyer sans modifier la trace de pile:

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

Cela fonctionne sur n'importe quelle exception, pas seulement AggregateException.

Il a été introduit en raison de la awaitfonctionnalité du langage C #, qui déballe les exceptions internes des AggregateExceptioninstances afin de rendre les fonctionnalités du langage asynchrone plus semblables aux fonctionnalités du langage synchrone.


11
Bon candidat pour une méthode d'extension Exception.Rethrow ()?
nmarler

8
Notez que la classe ExceptionDispatchInfo se trouve dans l'espace de noms System.Runtime.ExceptionServices et n'est pas disponible avant .NET 4.5.
yoyo

53
Vous devrez peut-être mettre un régulier throw;après la ligne .Throw (), car le compilateur ne saura pas que .Throw () lève toujours une exception. throw;ne sera jamais appelé en conséquence, mais au moins le compilateur ne se plaindra pas si votre méthode nécessite un objet de retour ou est une fonction asynchrone.
Todd

5
@Taudris Cette question concerne spécifiquement le retour de l'exception interne, qui ne peut pas être spécialement gérée par throw;. Si vous utilisez throw ex.InnerException;le stack-trace est réinitialisé au point où il est relancé.
Paul Turner

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

86

Il est possible de conserver la trace de la pile avant de relancer sans réflexion:

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
}

Cela gaspille beaucoup de cycles par rapport à un appel InternalPreserveStackTracevia un délégué mis en cache, mais a l'avantage de ne s'appuyer que sur des fonctionnalités publiques. Voici quelques modèles d'utilisation courants pour les fonctions de conservation de trace de pile:

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

Ça a l'air cool, que doit-il se passer après avoir exécuté ces fonctions?
vdboor

2
En fait, ce n'est pas beaucoup plus lent que l'invocation InternalPreserveStackTrace(environ 6% plus lent avec 10000 itérations). L'accès direct aux champs par réflexion est environ 2,5% plus rapide que l'invocationInternalPreserveStackTrace
Thomas Levesque

1
Je recommanderais d'utiliser le e.Datadictionnaire avec une chaîne ou une clé d'objet unique ( static readonly object myExceptionDataKey = new object (), mais ne le faites pas si vous devez sérialiser des exceptions n'importe où). Évitez de modifier e.Message, car vous pourriez avoir du code quelque part qui analyse e.Message. L'analyse e.Messageest mauvaise, mais il n'y a peut-être pas d'autre choix, par exemple si vous devez utiliser une bibliothèque tierce avec de mauvaises pratiques d'exception.
Anton Tykhyy

10
DoFixups se casse pour les exceptions personnalisées s'ils n'ont pas le ctor de sérialisation
ruslander

3
La solution suggérée ne fonctionne pas si l'exception n'a pas de constructeur de sérialisation. Je suggère d'utiliser la solution proposée sur stackoverflow.com/a/4557183/209727 qui fonctionne bien dans tous les cas. Pour .NET 4.5, envisagez d'utiliser la classe ExceptionDispatchInfo.
Davide Icardi

33

Je pense que votre meilleur pari serait de simplement mettre cela dans votre bloc de capture:

throw;

Et puis extrayez plus tard l'exception interne.


21
Ou supprimez complètement le try / catch.
Daniel Earwicker

6
@Earwicker. Supprimer le try / catch n'est pas une bonne solution en général car il ignore les cas où du code de nettoyage est requis avant de propager l'exception dans la pile des appels.
Jordan

12
@Jordan - Le code de nettoyage devrait être dans un bloc finalement pas un bloc catch
Paolo

17
@Paolo - S'il est censé être exécuté dans tous les cas, oui. S'il est censé être exécuté uniquement en cas d'échec, non.
chiccodoro

4
Gardez à l'esprit que InternalPreserveStackTrace n'est pas sûr pour les threads, donc si vous avez 2 threads dans l'un de ces états d'exception ... que Dieu ait pitié de nous tous.
Rob

14

Personne n'a expliqué la différence entre ExceptionDispatchInfo.Capture( ex ).Throw()et une plaine throw, alors la voici.

La manière complète de renvoyer une exception interceptée consiste à utiliser ExceptionDispatchInfo.Capture( ex ).Throw()(uniquement disponible à partir de .Net 4.5).

Vous trouverez ci-dessous les cas nécessaires pour tester cela:

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

Le cas 1 et le cas 2 vous donneront une trace de pile où le numéro de ligne de code source pour la CallingMethodméthode est le numéro de throw new Exception( "TEST" )ligne de la ligne.

Cependant, le cas 3 vous donnera une trace de pile où le numéro de ligne de code source pour la CallingMethodméthode est le numéro de ligne de l' throwappel. Cela signifie que si la throw new Exception( "TEST" )ligne est entourée d'autres opérations, vous ne savez pas à quel numéro de ligne l'exception a été levée.

Le cas 4 est similaire au cas 2 car le numéro de ligne de l'exception d'origine est conservé, mais il ne s'agit pas d'un véritable renversement car il modifie le type de l'exception d'origine.


5
J'ai toujours pensé que «throw» ne réinitialisait pas la trace de pile (par opposition à «throw e»).
Jesper Matthiesen

@JesperMatthiesen Je peux me tromper, mais j'ai entendu que cela dépend si l'exception a été levée et interceptée dans le même fichier. S'il s'agit du même fichier, la trace de la pile sera perdue, s'il s'agit d'un autre fichier, il sera conservé.
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 );
    }
}

Appelez la méthode d'extension sur votre exception avant de la lancer, elle conservera la trace de pile d'origine.


Sachez que dans .Net 4.0, InternalPreserveStackTrace est maintenant un no-op - regardez dans Reflector et vous verrez que la méthode est complètement vide!
Samuel Jack

Scratch that: Je regardais le RC: dans la version bêta, ils ont remis l'implémentation!
Samuel Jack

3
suggestion: changez PreserveStackTrace pour retourner ex - puis pour lever une exception vous pouvez simplement dire: throw ex.PreserveStackTrace ();
Simon_Weaver

Pourquoi utiliser Action<Exception>? Ici, utilisez la méthode statique
Kiquenet

10

Encore plus de réflexion ...

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

Gardez à l'esprit que cela peut se casser à tout moment, car les champs privés ne font pas partie de l'API. Voir plus de discussion sur Mono bugzilla .


28
C'est une très, très mauvaise idée, car cela dépend de détails internes non documentés sur les classes de framework.
Daniel Earwicker

1
Il s'avère qu'il est possible de conserver la trace de la pile sans réflexion, voir ci-dessous.
Anton Tykhyy

1
Il InternalPreserveStackTraceserait préférable d' appeler la méthode interne , car elle fait la même chose et est moins susceptible de changer à l'avenir ...
Thomas Levesque

1
En fait, ce serait pire, car InternalPreserveStackTrace n'existe pas sur Mono.
skolima

5
@daniel - eh bien c'est vraiment, vraiment, vraiment une mauvaise idée de lancer; pour réinitialiser la stacktrace lorsque chaque développeur .net est formé à croire que ce ne sera pas le cas. c'est aussi une très, très, très mauvaise chose si vous ne pouvez pas trouver la source d'une NullReferenceException et perdre un client / une commande parce que vous ne pouvez pas la trouver. pour moi qui l'emporte sur les «détails non documentés» et définitivement mono.
Simon_Weaver

10

Premièrement: ne perdez pas l'exception TargetInvocationException - ce sont des informations précieuses lorsque vous voudrez déboguer des choses.
Deuxièmement: Enveloppez le TIE en tant qu'InnerException dans votre propre type d'exception et mettez une propriété OriginalException qui lie à ce dont vous avez besoin (et conservez l'intégralité de la pile d'appels intacte).
Troisièmement: laissez la bulle TIE sortir de votre méthode.


5

Les gars, vous êtes cool .. je vais bientôt devenir nécromancien.

    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
Bonne idée, mais vous ne contrôlez pas toujours le code qui appelle .Invoke().
Anton Tykhyy

1
Et vous ne connaissez pas toujours non plus les types des arguments / résultats au moment de la compilation.
Roman Starkov

3

Un autre exemple de code qui utilise la sérialisation / désérialisation d'exception. Il ne nécessite pas que le type d'exception réel soit sérialisable. De plus, il utilise uniquement des méthodes publiques / protégées.

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

pas besoin que le type d'exception réel soit sérialisable?
Kiquenet

3

Sur la base de la réponse de Paul Turners, j'ai fait une méthode d'extension

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

les return ex ist n'a jamais atteint, mais l'avantage est que je peux l'utiliser throw ex.Capture()comme une ligne pour que le compilateur ne génère pas d' not all code paths return a valueerreur.

    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();
        }
    }
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.