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

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记录这条未被中间件捕获的数据库连接错误日志,要么让该异常被全局中间件正确处理。


解决方案

原因分析

  1. EF Core在请求被取消时,内部会抛出TaskCanceledException,但同时会直接通过自身的ILogger实例记录数据库连接错误日志,这条日志是EF Core内部输出的,并未通过抛出异常的方式传递到全局中间件,因此中间件无法拦截。
  2. 当前中间件仅处理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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 13:30:53