为什么Visual Studio调试器会破坏WPF的异步方法中的未处理异常而不会破坏ASP.NET?

时间:2021-05-20 03:36:21

I'm having a bit of trouble understanding why the Visual Studio debugger is capable of breaking on unhandled exceptions for events in WPF applications, but not for controller actions in web applications. If I have a handler like so:

我在理解为什么Visual Studio调试器能够打破WPF应用程序中事件的未处理异常,而不是Web应用程序中的控制器操作时遇到了一些麻烦。如果我有这样的处理程序:

private async void SomeButton_Click(object sender, RoutedEventArgs e)
{
    throw new NotImplementedException();
}

The debugger will gladly break on the unhandled exception. However, if I have a similar action on a controller:

调试器很乐意打破未处理的异常。但是,如果我在控制器上有类似的操作:

public async Task<SomeReturnValue> SomeAction()
{
    throw new NotImplementedException();
}

the debugger passes over the exception and ultimately the MVC Framework handles it and transforms it into a 500 Server Error to return to the client.

调试器通过异常,最终MVC框架处理它并将其转换为500 Server Error以返回到客户端。

It's worth noting that console apps seem to have similar behavior to web apps when it comes to exceptions in async methods. I know that for ASP.NET I can disable Just My Code and break on all thrown exceptions instead of just unhandled exceptions, but I don't have a firm grasp on the architectural differences here that allow us to break on unhandled exceptions for WPF events and not ASP.NET requests. It seems that the exceptions illustrated above are observed exceptions for WPF but unobserved for ASP.NET. Can someone elaborate on why?

值得注意的是,当涉及异步方法中的异常时,控制台应用程序似乎与Web应用程序具有相似的行为。我知道对于ASP.NET我可以禁用Just My Code并中断所有抛出的异常,而不仅仅是未处理的异常,但是我没有牢固掌握这里的架构差异,这使我们可以打破WPF事件的未处理异常而不是ASP.NET请求。似乎上面说明的异常是观察到的WPF异常,但是对于ASP.NET没有观察到。有人可以详细说明原因吗?

1 个解决方案

#1


4  

.NET exceptions are built on a Win32 concept called structured exception handling (SEH).

.NET异常建立在称为结构化异常处理(SEH)的Win32概念之上。

SEH has a "two-step" system for managing thrown exceptions. The first step is walking up the stack searching for a handler. This first step is weird because it will execute exception filters before unwinding the stack. (On a side note, exception filters are a new addition to the C# language in C# 6 / VS2015). So the first step walks up the stack executing filters until it finds a catch block with a matching filter. Only then does the second step take place: unwinding the stack and starting execution of the catch block. (On a side note, SEH exception filters can also return a value meaning "resume execution at the point where it was thrown", but this is advanced and esoteric behavior, not available in C#).

SEH有一个“两步”系统来管理抛出异常。第一步是在堆栈中向上搜索处理程序。第一步很奇怪,因为它会在展开堆栈之前执行异常过滤器。 (另一方面,异常过滤器是C#6 / VS2015中C#语言的新增功能)。因此,第一步是执行过滤器的堆栈,直到找到带有匹配过滤器的catch块。只有这样才会发生第二步:展开堆栈并开始执行catch块。 (另一方面,SEH异常过滤器也可以返回一个值,意思是“在它被抛出时恢复执行”,但这是高级和深奥的行为,在C#中不可用)。

The final piece of the puzzle is conjecture on my part, but it seems reasonable: the debugger hooks (or watches / intercepts / whatever) all thread entry points, and if an exception escapes to that point it will kick off its "unhandled exception" workflow. If I had to guess, the debugger implements its "hook" as an SEH filter (at least, it appears to behave like one).

这个难题的最后一部分是我的猜想,但它似乎是合理的:调试器挂钩(或监视/拦截/无论)所有线程入口点,如果异常逃脱到该点,它将启动其“未处理的异常”流程。如果我不得不猜测,调试器将其“钩子”实现为SEH过滤器(至少,它看起来像一个)。

That's sufficient to describe the observed behavior.

这足以描述观察到的行为。


For example, consider what happens when a catch block contains a throw; statement:

例如,考虑当catch块包含throw时会发生什么;声明:

static void Main()
{
    TestA();
}

static void TestA()
{
    try
    {
        TestB();
    }
    catch
    {
        throw; // Debugger breaks here
    }
}

static void TestB()
{
    throw new InvalidCastException();
}

When the exception is thrown, the first step of SEH searches up the call stack and finds the catch block. Then the second step of SEH unwinds the stack up to that catch block and executes it.

抛出异常时,SEH的第一步搜索调用堆栈并找到catch块。然后SEH的第二步将堆栈展开到该catch块并执行它。

When the throw; executes, it actually kicks off a different SEH exception. The first step of SEH searches up the call stack and triggers the debugger hook. The debugger then stops everything and steps in before the stack is unwound. But note that the debugger breaks at the throw; statement, because that's where the stack actually is.

扔的时候;执行,它实际上启动了一个不同的SEH例外。 SEH的第一步搜索调用堆栈并触发调试器挂钩。然后调试器在堆栈展开之前停止所有操作并进入步骤。但请注意,调试器会在抛出时中断;声明,因为那是堆栈的实际位置。

Now, we did not stomp the stack in the Exception object (in other words, we were sure to use throw; and not throw exception;), and if you examine the exception details you will indeed see the entire stack. This stack was captured and placed on the Exception object at the point it was originally thrown. But the debugger does not examine the Exception object instance; it's only looking at the actual thread stack, which ends at throw;.

现在,我们没有在Exception对象中踩踏堆栈(换句话说,我们肯定会使用throw;而不是抛出异常;),如果你检查异常细节,你确实会看到整个堆栈。捕获此堆栈并将其放置在最初抛出的Exception对象上。但调试器不检查Exception对象实例;它只是查看实际的线程堆栈,它以throw结束;

Note the difference in behavior with the new exception filters:

请注意新异常过滤器的行为差异:

static void Main(string[] args)
{
    TestA();
}

static void TestA()
{
    try
    {
        TestB();
    }
    catch when (false)
    {
        throw;
    }
}

static void TestB()
{
    throw new InvalidCastException(); // Debugger breaks here
}

Now, when the exception is thrown, SEH performs its first step and searches up the call stack. It finds the catch but the filter returns false, so it just continues searching and runs into the debugger thread hook. At that point, the debugger stops everything and breaks at the current stack, which is where the exception was thrown. The stack was never unwound to the catch so it doesn't end up breaking there.

现在,当抛出异常时,SEH执行其第一步并搜索调用堆栈。它找到了catch但过滤器返回false,所以它只是继续搜索并运行到调试器线程钩子。此时,调试器会停止所有内容并在当前堆栈处中断,这是抛出异常的位置。堆栈从未解开,因此它不会最终破坏。


So, to apply this to your specific question...

因此,将此应用于您的具体问题......

The WPF event handler has nothing between it and the UI thread proc to catch the exception, so any exceptions will take their first step all the way to the debugger hook:

WPF事件处理程序在它和UI线程proc之间没有任何内容来捕获异常,所以任何异常都会一直到调试器钩子的第一步:

private async void SomeButton_Click(object sender, RoutedEventArgs e)
{
    throw new NotImplementedException();
}

On the controller action, things are quite different:

在控制器动作上,情况完全不同:

public async Task<SomeReturnValue> SomeAction()
{
    throw new NotImplementedException();
}

First, the method is returning a Task<T>. When you have an async method that returns a task, the compiler rewriting of the async method will catch any exceptions and just place them on the task. Internally, ASP.NET then observes those exceptions and sends a 500. Even if this was a synchronous method, ASP.NET would still catch your exceptions automatically and translate them into a 500 response.

首先,该方法返回Task 。当你有一个返回任务的异步方法时,编译器重写异步方法将捕获任何异常并将它们放在任务上。在内部,ASP.NET然后观察这些异常并发送500.即使这是一个同步方法,ASP.NET仍然会自动捕获您的异常并将它们转换为500响应。

Those exceptions are never raised all the way to the thread proc. Which is a good thing, because that would cause your entire web app to crash. :)

这些异常永远不会一直提到线程proc。这是一件好事,因为这会导致整个Web应用程序崩溃。 :)

Unfortunately, this means that the debugger "unhandled exception" workflow is never kicked off, either.

不幸的是,这意味着调试器“未处理的异常”工作流程也从未启动过。

#1


4  

.NET exceptions are built on a Win32 concept called structured exception handling (SEH).

.NET异常建立在称为结构化异常处理(SEH)的Win32概念之上。

SEH has a "two-step" system for managing thrown exceptions. The first step is walking up the stack searching for a handler. This first step is weird because it will execute exception filters before unwinding the stack. (On a side note, exception filters are a new addition to the C# language in C# 6 / VS2015). So the first step walks up the stack executing filters until it finds a catch block with a matching filter. Only then does the second step take place: unwinding the stack and starting execution of the catch block. (On a side note, SEH exception filters can also return a value meaning "resume execution at the point where it was thrown", but this is advanced and esoteric behavior, not available in C#).

SEH有一个“两步”系统来管理抛出异常。第一步是在堆栈中向上搜索处理程序。第一步很奇怪,因为它会在展开堆栈之前执行异常过滤器。 (另一方面,异常过滤器是C#6 / VS2015中C#语言的新增功能)。因此,第一步是执行过滤器的堆栈,直到找到带有匹配过滤器的catch块。只有这样才会发生第二步:展开堆栈并开始执行catch块。 (另一方面,SEH异常过滤器也可以返回一个值,意思是“在它被抛出时恢复执行”,但这是高级和深奥的行为,在C#中不可用)。

The final piece of the puzzle is conjecture on my part, but it seems reasonable: the debugger hooks (or watches / intercepts / whatever) all thread entry points, and if an exception escapes to that point it will kick off its "unhandled exception" workflow. If I had to guess, the debugger implements its "hook" as an SEH filter (at least, it appears to behave like one).

这个难题的最后一部分是我的猜想,但它似乎是合理的:调试器挂钩(或监视/拦截/无论)所有线程入口点,如果异常逃脱到该点,它将启动其“未处理的异常”流程。如果我不得不猜测,调试器将其“钩子”实现为SEH过滤器(至少,它看起来像一个)。

That's sufficient to describe the observed behavior.

这足以描述观察到的行为。


For example, consider what happens when a catch block contains a throw; statement:

例如,考虑当catch块包含throw时会发生什么;声明:

static void Main()
{
    TestA();
}

static void TestA()
{
    try
    {
        TestB();
    }
    catch
    {
        throw; // Debugger breaks here
    }
}

static void TestB()
{
    throw new InvalidCastException();
}

When the exception is thrown, the first step of SEH searches up the call stack and finds the catch block. Then the second step of SEH unwinds the stack up to that catch block and executes it.

抛出异常时,SEH的第一步搜索调用堆栈并找到catch块。然后SEH的第二步将堆栈展开到该catch块并执行它。

When the throw; executes, it actually kicks off a different SEH exception. The first step of SEH searches up the call stack and triggers the debugger hook. The debugger then stops everything and steps in before the stack is unwound. But note that the debugger breaks at the throw; statement, because that's where the stack actually is.

扔的时候;执行,它实际上启动了一个不同的SEH例外。 SEH的第一步搜索调用堆栈并触发调试器挂钩。然后调试器在堆栈展开之前停止所有操作并进入步骤。但请注意,调试器会在抛出时中断;声明,因为那是堆栈的实际位置。

Now, we did not stomp the stack in the Exception object (in other words, we were sure to use throw; and not throw exception;), and if you examine the exception details you will indeed see the entire stack. This stack was captured and placed on the Exception object at the point it was originally thrown. But the debugger does not examine the Exception object instance; it's only looking at the actual thread stack, which ends at throw;.

现在,我们没有在Exception对象中踩踏堆栈(换句话说,我们肯定会使用throw;而不是抛出异常;),如果你检查异常细节,你确实会看到整个堆栈。捕获此堆栈并将其放置在最初抛出的Exception对象上。但调试器不检查Exception对象实例;它只是查看实际的线程堆栈,它以throw结束;

Note the difference in behavior with the new exception filters:

请注意新异常过滤器的行为差异:

static void Main(string[] args)
{
    TestA();
}

static void TestA()
{
    try
    {
        TestB();
    }
    catch when (false)
    {
        throw;
    }
}

static void TestB()
{
    throw new InvalidCastException(); // Debugger breaks here
}

Now, when the exception is thrown, SEH performs its first step and searches up the call stack. It finds the catch but the filter returns false, so it just continues searching and runs into the debugger thread hook. At that point, the debugger stops everything and breaks at the current stack, which is where the exception was thrown. The stack was never unwound to the catch so it doesn't end up breaking there.

现在,当抛出异常时,SEH执行其第一步并搜索调用堆栈。它找到了catch但过滤器返回false,所以它只是继续搜索并运行到调试器线程钩子。此时,调试器会停止所有内容并在当前堆栈处中断,这是抛出异常的位置。堆栈从未解开,因此它不会最终破坏。


So, to apply this to your specific question...

因此,将此应用于您的具体问题......

The WPF event handler has nothing between it and the UI thread proc to catch the exception, so any exceptions will take their first step all the way to the debugger hook:

WPF事件处理程序在它和UI线程proc之间没有任何内容来捕获异常,所以任何异常都会一直到调试器钩子的第一步:

private async void SomeButton_Click(object sender, RoutedEventArgs e)
{
    throw new NotImplementedException();
}

On the controller action, things are quite different:

在控制器动作上,情况完全不同:

public async Task<SomeReturnValue> SomeAction()
{
    throw new NotImplementedException();
}

First, the method is returning a Task<T>. When you have an async method that returns a task, the compiler rewriting of the async method will catch any exceptions and just place them on the task. Internally, ASP.NET then observes those exceptions and sends a 500. Even if this was a synchronous method, ASP.NET would still catch your exceptions automatically and translate them into a 500 response.

首先,该方法返回Task 。当你有一个返回任务的异步方法时,编译器重写异步方法将捕获任何异常并将它们放在任务上。在内部,ASP.NET然后观察这些异常并发送500.即使这是一个同步方法,ASP.NET仍然会自动捕获您的异常并将它们转换为500响应。

Those exceptions are never raised all the way to the thread proc. Which is a good thing, because that would cause your entire web app to crash. :)

这些异常永远不会一直提到线程proc。这是一件好事,因为这会导致整个Web应用程序崩溃。 :)

Unfortunately, this means that the debugger "unhandled exception" workflow is never kicked off, either.

不幸的是,这意味着调试器“未处理的异常”工作流程也从未启动过。