异步方法中IDisposable先于异常过滤器释放?详解.NET Exception Filters机制
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
throwoccurs, it immediately bubbles up out of the delegate and into the middleware’scatchblock before theusingblock’sfinallyclause (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
catchblock complete, theusingblock’sDisposefinally 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:
- The
usingblock creates the log scope. - When
throwexecutes,AsyncTaskMethodBuildercaptures the exception and stores it in the returnedTaskinstead of letting it bubble up synchronously. - Since the exception is captured, the method continues executing to the end of the
usingblock, triggering thefinallyclause and disposing the log scope immediately. - The middleware’s
await next()then observes the faultedTask, re-throws the exception into thecatchblock—but by this point, the log scope is already gone. - The exception filter runs after the scope has been disposed, so the logging output loses the
UserId:100scope.
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
usingscopes stay active during exception filter execution. - For async code: exceptions are captured by the
Task, sousingscopes 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

