ASP.NET 7中如何避免记录取消令牌引发的数据库查询异常?
问题场景
我正在开发ASP.NET 7.0应用,会向数据库发起请求并传递CancellationToken,核心代码如下:
public async Task OnGetAsync(CancellationToken cancellationToken) { var ls = await _db.MyTable .ToListAsync(cancellationToken: cancellationToken); }
当取消令牌触发时,系统会生成以下截断后的异常日志:
fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1] => SpanId:222e09bb9f77d813, TraceId:fe07899d8a16234fe2ea9c4d5f987e7f, ParentId:0000000000000000 => ConnectionId:0HMOV3KNV8H7R => RequestPath:/ RequestId:0HMOV3KNV8H7R:00000047 An unhandled exception has occurred while executing the request. System.Threading.Tasks.TaskCanceledException: A task was canceled. at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync() at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken) at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken) at myApp.Areas.UsersAccess.Pages.SummaryModel.OnGetAsync(CancellationToken cancellationToken) in C:\Users\me\source\repos\me\myApp\myApp\Areas\UsersAccess\Pages\Summary\Summary.cshtml.cs:line 100 at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory.NonGenericTaskHandlerMethod.Execute(Object receiver, Object[] arguments) at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeHandlerMethodAsync() at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeNextPageFilterAsync() at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Rethrow(PageHandlerExecutedContext context) at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeInnerFilterAsync() at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at Program.<<<Main>$>b__0_8>d.MoveNext() in C:\Users\me\source\repos\me\myApp\myApp\Program.cs:line 117 --- End of stack trace from previous location --- at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddlewareImpl.<Invoke>g__Awaited|8_0(ExceptionHandlerMiddlewareImpl middleware, HttpContext context, Task task)
这类日志会造成日志污染,我希望避免记录该异常。由于存在大量类似查询,为保证代码简洁,不想每次都使用try/catch块。
核心问题:是否有无需使用try/catch块即可过滤此特定日志消息的方法?
已尝试的方案
1. 修改日志配置
在appsettings.json的LogLevel部分添加以下配置,但未生效:
"System.Threading.Tasks.TaskCanceledException": "None"
推测原因是:需要将ExceptionHandlerMiddleware的日志级别设为None,但这样会过滤掉该中间件的所有日志,无法仅针对任务取消异常。
2. 自定义异常处理中间件
添加了自定义异常处理中间件,但错误仍会被记录:
app.UseExceptionHandler(errorApp => { errorApp.Run(async context => { var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error; if (exception?.Message == "A task was canceled.") { await context.Response.WriteAsync("Task cancelled."); // 不知道如何在此之后忽略该错误 } else { // 重定向到错误页面 context.Response.Redirect("/Error"); } }); });
尝试修改context变量,但context.Features.Get<IExceptionHandlerFeature>().Error是只读的,无法直接设为null。
更新:查看.NET默认异常处理源码
查看.NET默认异常处理程序的HandleException函数,发现其中已处理操作取消的情况:
private async Task HandleException(HttpContext context, ExceptionDispatchInfo edi) { if ((edi.SourceException is OperationCanceledException || edi.SourceException is IOException) && context.RequestAborted.IsCancellationRequested) { _logger.RequestAbortedException(); if (!context.Response.HasStarted) { context.Response.StatusCode = StatusCodes.Status499ClientClosedRequest; } return; } _logger.UnhandledException(edi.SourceException); ... }
但我的场景中代码并未进入该if分支,而是直接调用了UnhandledException方法。如果进入分支,会调用RequestAbortedException记录Debug级别的日志,这正是我需要的。不清楚为何代码未进入该分支,是操作有误、逻辑错误还是.NET代码问题?
解决方案
原因分析
默认中间件的判断条件是(异常是OperationCanceledException/IOException) 并且 context.RequestAborted.IsCancellationRequested。你遇到的TaskCanceledException虽然继承自OperationCanceledException,但如果传递的CancellationToken不是HttpContext.RequestAborted(而是自定义令牌),会导致context.RequestAborted.IsCancellationRequested为false,从而不满足进入分支的条件。
具体解决方法
方法1:确保传递的是HttpContext.RequestAborted
将自定义令牌替换为请求上下文自带的取消令牌,这样当请求被取消时,context.RequestAborted.IsCancellationRequested会自动为true,默认中间件会自动处理,仅记录Debug级日志:
// Razor Page中直接使用上下文的取消令牌 public async Task OnGetAsync() { var ls = await _db.MyTable.ToListAsync(HttpContext.RequestAborted); }
方法2:自定义日志过滤器
如果必须使用自定义令牌,可通过自定义日志过滤器过滤TaskCanceledException的日志:
- 实现日志过滤器:
public class TaskCanceledExceptionLoggerProvider : ILoggerProvider { private readonly ILoggerProvider _innerProvider; public TaskCanceledExceptionLoggerProvider(ILoggerProvider innerProvider) { _innerProvider = innerProvider; } public ILogger CreateLogger(string categoryName) { var innerLogger = _innerProvider.CreateLogger(categoryName); return new TaskCanceledExceptionLogger(innerLogger); } public void Dispose() => _innerProvider.Dispose(); private class TaskCanceledExceptionLogger : ILogger { private readonly ILogger _innerLogger; public TaskCanceledExceptionLogger(ILogger innerLogger) { _innerLogger = innerLogger; } public IDisposable BeginScope<TState>(TState state) => _innerLogger.BeginScope(state); public bool IsEnabled(LogLevel logLevel) => _innerLogger.IsEnabled(logLevel); public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { // 过滤ExceptionHandlerMiddleware中TaskCanceledException的Error级日志 if (exception is TaskCanceledException && eventId.Id == 1 && logLevel == LogLevel.Error && typeof(ExceptionHandlerMiddleware).FullName == state.ToString().Split(' ')[0]) { return; } _innerLogger.Log(logLevel, eventId, state, exception, formatter); } } }
- 在
Program.cs中注册过滤器:
var loggerFactory = app.Services.GetRequiredService<ILoggerFactory>(); var consoleProvider = loggerFactory.GetProviders().First(p => p is ConsoleLoggerProvider); loggerFactory.AddProvider(new TaskCanceledExceptionLoggerProvider(consoleProvider));
方法3:修改自定义异常中间件,标记异常已处理
在自定义中间件中移除IExceptionHandlerFeature,阻止后续日志记录:
app.UseExceptionHandler(errorApp => { errorApp.Run(async context => { var exceptionFeature = context.Features.Get<IExceptionHandlerFeature>(); var exception = exceptionFeature?.Error; if (exception is TaskCanceledException) { context.Response.StatusCode = StatusCodes.Status499ClientClosedRequest; await context.Response.WriteAsync("Task cancelled."); // 移除异常特性,阻止默认中间件记录日志 context.Features.Set<IExceptionHandlerFeature>(null); } else { context.Response.Redirect("/Error"); } }); });
注意:需确保该中间件在默认异常处理中间件之前注册,或直接替换默认异常处理逻辑。
内容的提问来源于stack exchange,提问作者S. Delsad

