ASP Core 7+EF Core 7请求取消时数据库异常未被中间件捕获
基于ASP Core 7 API、EF Core 7和MSSQL开发的系统中,加载仪表板数据的接口在HTTP请求被取消时,会抛出TaskCanceledException,同时Serilog会记录一条未被全局异常中间件捕获的数据库连接错误日志:An error occurred using the connection to database 'test-db-12-jan-2023' on server 'DESKTOP-xxx'。
相关代码
仓库层获取仪表板数据实现
public async Task<ResponseResult<DashboardPendingBillsDto>> PendingBillDetails(CancellationToken token) { var venderInvoicelQuery = _context.Set<VendorInvoice>().AsQueryable(); var pendingBillsQuery = venderInvoicelQuery.Where(a => !a.IsBilled); var today = DateTimeOffset.UtcNow; var fourthDay = today.AddDays(-3); var seventhDay = today.AddDays(-7); var pendingBillsData = await pendingBillsQuery.Select(s => new { BetweenOneToFiveDays = pendingBillsQuery .Where(a => fourthDay.Date < a.CreatedOn.Date && a.CreatedOn.Date <= today.Date).Count(), BetweenSixToTenDays = pendingBillsQuery .Where(a => seventhDay.Date < a.CreatedOn.Date && a.CreatedOn.Date <= fourthDay.Date).Count(), AboveTenDays = pendingBillsQuery .Where(a => a.CreatedOn.Date <= seventhDay.Date).Count() }).FirstOrDefaultAsync(token); var dashboardPendingBillsDto = new DashboardPendingBillsDto { BetweenZeroToThreeDays = pendingBillsData?.BetweenOneToFiveDays ?? 0, BetweenFourToSevenDays = pendingBillsData?.BetweenSixToTenDays ?? 0, AboveSevenDays = pendingBillsData?.AboveTenDays ?? 0, }; return new ResponseResult<DashboardPendingBillsDto>(dashboardPendingBillsDto); }
全局异常中间件处理逻辑
private Task ConvertException(HttpContext context, Exception exception) { var activityId = Activity.Current?.Id ?? "N/A"; _telemetry.TrackException(exception); ErrorResponse errorResponse = new() { TraceId = activityId }; int httpStatusCode = StatusCodes.Status500InternalServerError; context.Response.ContentType = "application/json"; var result = string.Empty; switch (exception) { case TaskCanceledException: //if client closes the connection httpStatusCode = StatusCodes.Status200OK; result = JsonConvert.SerializeObject(new ResponseResult<string>("Client closed the connecion")); break; case Exception: httpStatusCode = StatusCodes.Status500InternalServerError; errorResponse.Errors.Add(new KeyValuePair<string, IEnumerable<string>>(nameof(HttpStatusCode.InternalServerError), new[] { "Something went wrong, please try again" })); result = JsonConvert.SerializeObject(errorResponse); LogError(exception, activityId); break; } context.Response.StatusCode = httpStatusCode; return context.Response.WriteAsync(result); }
异常栈跟踪
中间件捕获到的异常信息为A task was cancelled,栈跟踪如下:
at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.<OpenInternalAsync>d__70.MoveNext() at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.<OpenInternalAsync>d__70.MoveNext() at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.<OpenAsync>d__66.MoveNext() at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.<ExecuteReaderAsync>d__19.MoveNext() at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.<InitializeReaderAsync>d__21.MoveNext() at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.<ExecuteAsync>d__7`2.MoveNext() at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.<MoveNextAsync>d__20.MoveNext() at System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable`1.ConfiguredValueTaskAwaiter.GetResult() at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.<SingleOrDefaultAsync>d__15`1.MoveNext() at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.<SingleOrDefaultAsync>d__15`1.MoveNext() at VPMS.Persistence.Repositories.Dashboard.DashboardRepository.<PendingBillDetails>d__3.MoveNext() in C:\Projects\Legacy HealthCare - Vendor Payment Management System\VPMS\VPMS.Persistence\Repositories\DashboardRepository\DashboardRepository.cs:line 162 at VPMS.Application.DashBoard.Services.DashboardService.<GetPendingBillsAsync>d__3.MoveNext() in C:\Projects\Legacy HealthCare - Vendor Payment Management System\VPMS\VPMS.Application\DashBoard\Services\DashboardService.cs:line 25 at VPMS.Api.Controllers.Dashboard.DashboardController.<GetPendingBills>d__2.MoveNext() in C:\Projects\Legacy HealthCare - Vendor Payment Management System\VPMS\VPMS.Api\Controllers\DashBoardController.cs:line 30 at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfActionResultExecutor.<Execute>d__0.MoveNext() at System.Threading.Tasks.ValueTask`1.get_Result() at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<<InvokeActionMethodAsync>g__Logged|12_1>d.MoveNext() at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<<InvokeNextActionFilterAsync>g__Awaited|10_0>d.MoveNext() at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<<InvokeFilterPipelineAsync>g__Awaited|20_0>d.MoveNext() at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<<InvokeAsync>g__Logged|17_1>d.MoveNext() at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<<InvokeAsync>g__Logged|17_1>d.MoveNext() at Microsoft.AspNetCore.Routing.EndpointMiddleware.<<Invoke>g__AwaitRequestTask|6_0>d.MoveNext() at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.<Invoke>d__9.MoveNext() at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.<Invoke>d__6.MoveNext() at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.<Invoke>d__5.MoveNext() at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.<Invoke>d__4.MoveNext() at VPMS.Api.Middleware.ExceptionHandlerMiddleware.<Invoke>d__3.MoveNext() in C:\Projects\Legacy HealthCare - Vendor Payment Management System\VPMS\VPMS.Api\Middleware\ExceptionHandlerMiddleware.cs:line 25
需求
解决以下问题:要么阻止Serilog记录这条未被中间件捕获的数据库连接错误日志,要么让该异常被全局中间件正确处理。
原因分析
- EF Core在请求被取消时,内部会抛出
TaskCanceledException,但同时会直接通过自身的ILogger实例记录数据库连接错误日志,这条日志是EF Core内部输出的,并未通过抛出异常的方式传递到全局中间件,因此中间件无法拦截。 - 当前中间件仅处理
TaskCanceledException,但TaskCanceledException是OperationCanceledException的子类,部分场景下EF Core可能抛出的是OperationCanceledException而非直接的TaskCanceledException。
具体解决步骤
1. 过滤Serilog中的EF Core取消操作日志
在Serilog的配置代码中添加过滤规则,忽略EF Core因请求取消产生的连接错误日志:
// 示例:在CreateLogger或UseSerilog配置中添加 Log.Logger = new LoggerConfiguration() // 其他配置... .Filter.ByExcluding(logEvent => logEvent.Level == LogEventLevel.Error && logEvent.Properties.TryGetValue("SourceContext", out var sourceContext) && sourceContext.ToString().Contains("Microsoft.EntityFrameworkCore.Storage") && logEvent.Exception is OperationCanceledException) .CreateLogger();
该规则会精准过滤掉EF Core存储层因请求取消产生的错误日志,不会影响其他正常的错误日志记录。
2. 完善全局异常中间件的处理逻辑
修改中间件的异常分支,覆盖所有取消类异常,并使用更贴合HTTP语义的状态码:
private Task ConvertException(HttpContext context, Exception exception) { var activityId = Activity.Current?.Id ?? "N/A"; _telemetry.TrackException(exception); ErrorResponse errorResponse = new() { TraceId = activityId }; int httpStatusCode = StatusCodes.Status500InternalServerError; context.Response.ContentType = "application/json"; var result = string.Empty; switch (exception) { case OperationCanceledException: // 覆盖TaskCanceledException,因为它是OperationCanceledException的子类 httpStatusCode = StatusCodes.Status499ClientClosedRequest; // 使用499表示客户端主动断开连接,比200更合理 result = JsonConvert.SerializeObject(new ResponseResult<string>("请求已取消")); break; case Exception ex: httpStatusCode = StatusCodes.Status500InternalServerError; errorResponse.Errors.Add(new KeyValuePair<string, IEnumerable<string>>(nameof(HttpStatusCode.InternalServerError), new[] { "服务器出错,请稍后重试" })); result = JsonConvert.SerializeObject(errorResponse); LogError(ex, activityId); break; } context.Response.StatusCode = httpStatusCode; return context.Response.WriteAsync(result); }
注:HTTP标准中没有499状态码,但它是Nginx等服务器常用的非标准状态码,用于表示客户端在服务器返回响应前主动关闭连接,语义上比200更准确。如果需要使用标准状态码,可以改用StatusCodes.Status408RequestTimeout。
3. 验证EF Core操作的取消令牌传递
确保所有EF Core异步操作都正确传入了CancellationToken,当前代码已经做到这一点,但需检查其他仓库方法是否遵循同样的规范,避免遗漏导致的异常未被正确触发。
内容的提问来源于stack exchange,提问作者thanzeel

