Empêcher que l’exception externe ne soit supprimée lors du lancement de BeginInvoke

J’ai un gestionnaire pour Application.ThreadException , mais je constate que les exceptions ne lui sont pas toujours transmises correctement. Plus précisément, si je lève une exception avec with inner exception à partir d’un rappel BeginInvoke , mon gestionnaire ThreadException ne reçoit pas l’exception externe , mais uniquement l’exception interne .

Exemple de code:

 public Form1() { InitializeComponent(); Application.ThreadException += (sender, e) => MessageBox.Show(e.Exception.ToSsortingng()); } private void button1_Click(object sender, EventArgs e) { var inner = new Exception("Inner"); var outer = new Exception("Outer", inner); //throw outer; BeginInvoke(new Action(() => { throw outer; })); } 

Si je dégage le throw outer; ligne et cliquez sur le bouton, la boîte de message affiche l’exception externe (avec son exception interne):

System.Exception: Outer —> System.Exception: Inner
— Fin de trace de stack d’exception interne —
sur WindowsFormsApplication1.Form1.button1_Click (Expéditeur d’object, EventArgs e) dans C: \ svn \ trunk \ Code Base \ Source.NET \ WindowsFormsApplication1 \ Form1.cs: ligne 55
sur System.Windows.Forms.Control.OnClick (EventArgs e)
sur System.Windows.Forms.Button.OnClick (EventArgs e)
sur System.Windows.Forms.Button.OnMouseUp (MouseEventArgs mevent)
sur System.Windows.Forms.Control.WmMouseUp (Message & m, bouton MouseButtons, clics Int32)
sur System.Windows.Forms.Control.WndProc (Message & m)
sur System.Windows.Forms.ButtonBase.WndProc (Message & m)
sur System.Windows.Forms.Button.WndProc (Message & m)
sur System.Windows.Forms.Control.ControlNativeWindow.OnMessage (Message & m)
sur System.Windows.Forms.Control.ControlNativeWindow.WndProc (Message & m)
à System.Windows.Forms.NativeWindow.Callback (IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)

Mais si le throw outer; se trouve dans un appel BeginInvoke , comme dans le code ci-dessus, le gestionnaire ThreadException n’obtient que l’exception interne. L’exception externe est supprimée avant l’ ThreadException et tout ce que je reçois est:

System.Exception: Inner

(Il n’y a pas de stack d’appels ici parce que l’ inner n’a jamais été jeté. Dans un exemple plus réaliste, où j’ai attrapé une exception et que je l’ai emballée pour la relancer, il y aurait une stack d’appels.)

La même chose se produit si j’utilise SynchronizationContext.Current.Post au lieu de BeginInvoke : l’exception externe est supprimée et le gestionnaire ThreadException obtient uniquement l’exception interne.

J’ai essayé de placer plusieurs couches d’exceptions autour de l’extérieur, au cas où il supprimerait l’exception la plus externe, mais cela n’a pas aidé: apparemment, quelque part, une boucle effectue quelque chose dans le sens de while (e.InnerException != null) e = e.InnerException; .

J’utilise BeginInvoke car j’ai du code qui doit BeginInvoke une exception non gérée qui sera immédiatement gérée par ThreadException , mais ce code est ThreadException dans un bloc d’ catch plus haut dans la stack d’appels (plus précisément dans l’action d’une Task , et Task intercepte l’exception et l’empêche de se propager). J’essaie d’utiliser BeginInvoke pour retarder le throw jusqu’à la prochaine fois que les messages sont traités dans la boucle de messages, lorsque je ne suis plus dans cette position. Je ne suis pas attaché à la solution particulière de BeginInvoke ; Je veux juste lancer une exception non gérée.

Comment faire en sorte qu’une exception – y compris son exception interne – parvienne à ThreadException même lorsque je suis dans le ThreadException quelqu’un d’autre?

(Je ne peux pas appeler ThreadException ma méthode ThreadException -handler, en raison de dépendances d’assemblage: le gestionnaire est lié au code de démarrage de l’EXE, alors que mon problème actuel concerne une DLL de couche inférieure.)

Je suppose que vous observez ce problème sur un système Windows x64 et qu’il s’agit d’un détail d’implémentation – plutôt inconnu – de Windows x64. Lisez-le ici

L’article explique en détail comment résoudre ce problème en appliquant un correctif, qui aurait été fourni avec Win7 SP1, mais je suis tombé sur ce problème il y a quelques semaines sur Win7 SP1.

De plus, vous pouvez attacher à l’ événement AppDomain.FirstChanceException qui vous donne access à toutes les exceptions avant qu’elles ne soient transmises au CLR pour le traitement.

La méthode recommandée pour propager l’exception sur une couche supérieure (hormis le renvoi implicite par attente de la tâche) consiste à supprimer la fourre-tout dans le corps de la tâche et à enregistrer une continuation de défaillance sur la tâche à l’aide de Task.ContinueWith , en spécifiant TaskContinuationOptions.OnlyOnFaulted . Si vous travaillez sur une couche intermédiaire et que vous n’avez pas access à la tâche, vous pouvez l’envelopper dans vos propres événements UnhandledException pour transmettre l’object Exception vers le haut.

Une solution consiste à placer la référence à l’exception interne dans une propriété personnalisée ou dans le dictionnaire de Data , c’est-à-dire à laisser la InnerException nulle et à utiliser la référence d’une autre manière.

Bien entendu, cela nécessite d’établir une sorte de convention pouvant être partagée entre le code de lancement et le code de traitement. Le mieux serait probablement de définir une classe d’exception personnalisée avec une propriété personnalisée, dans un projet référencé par les deux morceaux de code.

Exemple de code (bien qu’il ait besoin de plus de commentaires pour expliquer pourquoi il fait les choses folles qu’il fait):

 public class ExceptionDecorator : Exception { public ExceptionDecorator(Exception exception) : base(exception.Message) { Exception = exception; } public Exception Exception { get; private set; } } // To throw an unhandled exception without losing its InnerException: BeginInvoke(new Action(() => { throw new ExceptionDecorator(outer); })); // In the ThreadException handler: private void OnUnhandledException(object sender, ThreadExceptionEventArgs e) { var exception = e.Exception; if (exception is ExceptionDecorator) exception = ((ExceptionDecorator) exception).Exception; // ... }