joe*_*dev 3 .net debugging wpf asp.net-mvc async-await
我在理解为什么 Visual Studio 调试器能够中断 WPF 应用程序中事件的未处理异常,而不是 Web 应用程序中的控制器操作时遇到了一些麻烦。如果我有这样的处理程序:
private async void SomeButton_Click(object sender, RoutedEventArgs e)
{
    throw new NotImplementedException();
}
调试器很乐意在未处理的异常上中断。但是,如果我对控制器有类似的操作:
public async Task<SomeReturnValue> SomeAction()
{
    throw new NotImplementedException();
}
调试器传递异常,最终 MVC 框架处理它并将其转换为 500 服务器错误以返回给客户端。
值得注意的是,当涉及到异步方法中的异常时,控制台应用程序似乎具有与 Web 应用程序相似的行为。我知道对于 ASP.NET,我可以禁用 Just My Code 并中断所有抛出的异常,而不仅仅是未处理的异常,但我没有牢牢掌握此处的架构差异,这些差异允许我们中断 WPF 事件的未处理异常而不是 ASP.NET 请求。上面说明的异常似乎是 WPF 观察到的异常,但 ASP.NET 未观察到。有人可以详细说明为什么吗?
.NET 异常建立在称为结构化异常处理 (SEH) 的 Win32 概念之上。
SEH 有一个“两步”系统来管理抛出的异常。第一步是在堆栈中向上搜索处理程序。这第一步很奇怪,因为它会在展开堆栈之前执行异常过滤器。(附带说明,异常过滤器是 C# 6 / VS2015 中 C# 语言的新增功能)。因此,第一步沿着执行过滤器的堆栈向上走,直到找到catch具有匹配过滤器的块。只有这样,第二步才会发生:展开堆栈并开始执行catch块。(顺便提一下,SEH 异常过滤器也可以返回一个值,意思是“在它被抛出的点恢复执行”,但这是高级和深奥的行为,在 C# 中不可用)。
最后一块拼图是我的猜测,但它似乎是合理的:调试器挂钩(或监视/拦截/其他)所有线程入口点,如果异常逃逸到该点,它将启动其“未处理的异常”工作流程。如果我不得不猜测,调试器将它的“钩子”实现为一个 SEH 过滤器(至少,它的行为看起来像一个)。
这足以描述观察到的行为。
例如,考虑当一个catch块包含一条throw;语句时会发生什么:
static void Main()
{
    TestA();
}
static void TestA()
{
    try
    {
        TestB();
    }
    catch
    {
        throw; // Debugger breaks here
    }
}
static void TestB()
{
    throw new InvalidCastException();
}
当抛出异常时,SEH 的第一步是向上搜索调用栈并找到catch块。然后 SEH 的第二步将堆栈展开到该catch块并执行它。
当throw;执行时,它实际上启动了一个不同的SEH 异常。SEH 的第一步是搜索调用堆栈并触发调试器挂钩。调试器然后停止一切并在堆栈展开之前介入。但请注意,调试器在该throw;语句处中断,因为这实际上是堆栈所在的位置。
现在,我们没有践踏Exception对象中的堆栈(换句话说,我们肯定会使用throw;而不是throw exception;),如果您检查异常详细信息,您确实会看到整个堆栈。这个堆栈被捕获并放置在Exception对象最初被抛出的点上。但是调试器不会检查Exception对象实例;它只查看以.结尾的实际线程堆栈throw;。
请注意新异常过滤器的行为差异:
static void Main(string[] args)
{
    TestA();
}
static void TestA()
{
    try
    {
        TestB();
    }
    catch when (false)
    {
        throw;
    }
}
static void TestB()
{
    throw new InvalidCastException(); // Debugger breaks here
}
现在,当抛出异常时,SEH 执行它的第一步并向上搜索调用堆栈。它找到 ,catch但过滤器返回false,所以它只是继续搜索并运行到调试器线程钩子中。在这一点上,调试器停止一切并在当前堆栈处中断,这是抛出异常的地方。堆栈从未展开到 ,catch因此它最终不会在那里破裂。
因此,要将其应用于您的特定问题......
WPF 事件处理程序与 UI 线程 proc 之间没有任何东西可以捕获异常,因此任何异常都将一路走到调试器钩子的第一步:
private async void SomeButton_Click(object sender, RoutedEventArgs e)
{
    throw new NotImplementedException();
}
在控制器操作上,情况完全不同:
public async Task<SomeReturnValue> SomeAction()
{
    throw new NotImplementedException();
}
首先,该方法返回一个Task<T>. 当您有一个async返回任务的async方法时,该方法的编译器重写将捕获任何异常并将它们放在任务上。在内部,ASP.NET 然后观察这些异常并发送 500。即使这是一个同步方法,ASP.NET 仍会自动捕获您的异常并将它们转换为 500 响应。
这些异常永远不会一直引发到线程过程。这是一件好事,因为这会导致您的整个 Web 应用程序崩溃。:)
不幸的是,这意味着调试器“未处理的异常”工作流也永远不会启动。