Предотвращение исключения внешнего исключения при броске из BeginInvoke

У меня есть обработчик Application.ThreadException, но я обнаружил, что исключения не всегда правильно передаются ему. В частности, если я выкидываю исключение-with-inner-exception из обратного вызова BeginInvoke, мой обработчик ThreadException не получает внешнее исключение - он получает только внутреннее исключение.

Пример кода:

public Form1()
{
    InitializeComponent();
    Application.ThreadException += (sender, e) =>
        MessageBox.Show(e.Exception.ToString());
}
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; }));
}

Если я раскомментирую строку throw outer; и нажмите кнопку, то в окне сообщений отображается внешнее исключение (вместе со своим внутренним исключением):

System.Exception: Outer --- > System.Exception: Внутренний
  --- Конец внутренней проверки стека исключений ---
  в WindowsFormsApplication1.Form1.button1_Click (отправитель объекта, EventArgs e) в C:\svn\trunk\Code Base\Source.NET\WindowsFormsApplication1\Form1.cs: строка 55
  в System.Windows.Forms.Control.OnClick(EventArgs e)
  в System.Windows.Forms.Button.OnClick(EventArgs e)
  в System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent)
  в System.Windows.Forms.Control.WmMouseUp(Message & m, кнопка MouseButtons, клики Int32)
  в System.Windows.Forms.Control.WndProc(Message & m)
  в System.Windows.Forms.ButtonBase.WndProc(Message & m)
  в System.Windows.Forms.Button.WndProc(Message & m)
  в System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message & m)
  в System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message & m)
  в System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)

Но если throw outer; находится внутри вызова BeginInvoke, как в приведенном выше коде, тогда обработчик ThreadException получает только внутреннее исключение. Внешнее исключение удаляется до вызова ThreadException, и все, что я получаю, это:

System.Exception: Внутренний

(Здесь нет стека вызовов, потому что inner никогда не попадался. В более реалистичном примере, когда я поймал одно исключение и завернул его в повторный бросок, будет стек вызовов.)

То же самое происходит, если я использую SynchronizationContext.Current.Post вместо BeginInvoke: внешнее исключение удаляется, а обработчик ThreadException получает только внутреннее исключение.

Я попробовал обернуть больше слоев исключений извне, в случае, если он просто отменил самое внешнее исключение, но это не помогло: по-видимому, где-то там цикл делал что-то вдоль строк while (e.InnerException != null) e = e.InnerException;.

Я использую BeginInvoke, потому что у меня есть код, который должен вызывать необработанное исключение, которое немедленно обрабатывается ThreadException, но этот код находится внутри блока catch выше стека вызовов (в частности, он внутри действия для a Task, а Task поймает исключение и прекратит его распространение). Я пытаюсь использовать BeginInvoke для задержки throw, пока в следующий раз сообщения не будут обработаны в цикле сообщений, когда я больше не буду внутри catch. Я не привязан к конкретному решению BeginInvoke; Я просто хочу бросить необработанное исключение.

Как я могу заставить исключение, включая его внутреннее исключение, достичь ThreadException, даже если я внутри кого-то еще catch -all?

(Я не могу вызвать метод ThreadException -handler напрямую из-за зависимостей сборки: обработчик подключен кодом запуска EXE, тогда как моя текущая проблема находится в DLL более низкого уровня.)

Ответ 1

Один из способов сделать это - поместить ссылку на внутреннее исключение в пользовательское свойство или Data словарь, то есть оставить InnerException свойство null и нести ссылку другим способом.

Конечно, для этого требуется установить какое-то соглашение, которое можно разделить между метательным кодом и кодом обработки. Лучшим было бы, вероятно, определить настраиваемый класс исключений с настраиваемым свойством в проекте, на который ссылаются обе части кода.

Пример кода (хотя ему нужно больше комментариев, чтобы объяснить, почему он делает сумасшедшие вещи, которые он делает):

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

Ответ 2

Я предполагаю, что вы видите это поведение в системе x64 Windows, и это - довольно неизвестная деталь реализации x64 Windows. Прочитайте на нем здесь

В статье подробно рассказывается, как решить эту проблему, применив некоторое исправление, которое предположительно было отправлено с помощью Win7 SP1, но я столкнулся с этой проблемой несколько недель назад на Win7 SP1.

Кроме того, вы можете прикрепить событие AppDomain.FirstChanceException, которое дает вам доступ к каждому исключению, прежде чем оно будет передано в CLR для обработки

Ответ 3

Рекомендуемый способ распространения Исключения на более высокий уровень (помимо неявного перебора по ожиданию задачи) заключается в том, чтобы удалить catch-all из тела Task и вместо этого зарегистрировать продолжение Fault в Задаче, используя Task.ContinueWith, указав TaskContinuationOptions.OnlyOnFaulted. Если вы работаете через промежуточный уровень и не имеете доступа к задаче, вы можете дополнительно обернуть это в свои собственные события UnhandledException, чтобы передать объект Exception вверх.