You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

异步方法中IDisposable先于异常过滤器释放?详解.NET Exception Filters机制

.NET Exception Filters: Async Methods, Using Blocks, and Log Scope Loss Explained

Let’s break down exactly what’s happening with your exception filters, async methods, and log scope disposal—including why just marking a lambda as async changes the entire flow, even without an await call.

Core Difference: Synchronous vs. Async Exception Propagation

The key lies in how .NET handles exceptions in synchronous vs. asynchronous methods, and how that interacts with using blocks (which rely on try/finally under the hood).

Scenario 1: Synchronous RequestDelegate

Your synchronous Run delegate executes entirely synchronously when await next() is called in the middleware:

subApp.Run((context) => {
    using (logger.BeginScope("UserId:100")) {
        throw new InvalidOperationException("Something went wrong");
    }
});
  • When the throw occurs, it immediately bubbles up out of the delegate and into the middleware’s catch block before the using block’s finally clause (which disposes the log scope) runs.
  • The exception filter (when (LogError(...))) executes first, logging the error while the scope is still active.
  • After the filter and catch block complete, the using block’s Dispose finally runs to clean up the scope.

Scenario 2: Async RequestDelegate (Even Without await)

When you mark the lambda as async, even if there’s no await call, the compiler wraps it in an asynchronous method using AsyncTaskMethodBuilder:

subApp.Run(async (context) => {
    using (logger.BeginScope("UserId:100")) {
        throw new InvalidOperationException("Something went wrong");
    }
});

Here’s the critical flow change:

  1. The using block creates the log scope.
  2. When throw executes, AsyncTaskMethodBuilder captures the exception and stores it in the returned Task instead of letting it bubble up synchronously.
  3. Since the exception is captured, the method continues executing to the end of the using block, triggering the finally clause and disposing the log scope immediately.
  4. The middleware’s await next() then observes the faulted Task, re-throws the exception into the catch block—but by this point, the log scope is already gone.
  5. The exception filter runs after the scope has been disposed, so the logging output loses the UserId:100 scope.

Why async Alone Triggers This Behavior

You don’t need await Task.Delay(100) to see this issue—any async method (even one without await) uses the asynchronous method builder. This builder’s core job is to handle exceptions by associating them with the returned Task, rather than letting them propagate synchronously. This changes the order of operations: the using block cleans up before the exception reaches the middleware’s error handling.

To clarify your final question: the exception filter does execute—it just runs after the log scope has been disposed. The AsyncTaskMethodBuilder doesn’t prevent the filter from running; it delays when the exception is thrown to the middleware, after the using block has cleaned up.

Key Takeaway

  • For synchronous code: exceptions bubble immediately, so using scopes stay active during exception filter execution.
  • For async code: exceptions are captured by the Task, so using scopes are disposed before the exception reaches upper-level error handling.

If you need to preserve the log scope for async error handling, you’d need to extend the scope’s lifetime to cover the exception processing—for example, moving the BeginScope call into the middleware’s error handling block, or ensuring the exception is handled within the using block itself.

内容的提问来源于stack exchange,提问作者Aidan Steele

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:47:59