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

ASP.NET Core Web API:如何在异常处理中安全注入数据库上下文

解决异常处理中DbContext线程安全问题的方案

问题根源

你当前的代码通过静态类持有IErrorLoggerService实例,但IErrorLoggerService依赖的DbContext默认是Scoped生命周期(每个请求创建一个实例)。静态类的字段是全局共享的,这会导致多个请求复用同一个DbContext实例,引发线程安全问题(比如并发写入时的实体状态混乱、数据库操作异常)。

方案一:改用非静态中间件(推荐,符合ASP.NET Core规范)

将原来的静态扩展类改成标准的中间件,通过请求上下文获取Scoped服务,确保每个请求使用独立的DbContext实例。

改造后的中间件代码

public class ExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILog _logger;

    public ExceptionMiddleware(RequestDelegate next, ILog logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            // 传递请求到下一个中间件
            await _next(context);
        }
        catch (Exception ex)
        {
            // 从当前请求的服务容器获取Scoped的IErrorLoggerService
            var errorLoggerService = context.RequestServices.GetRequiredService<IErrorLoggerService>();
            await HandleExceptionAsync(context, ex, errorLoggerService);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception ex, IErrorLoggerService errorLoggerService)
    {
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        context.Response.ContentType = "application/json";

        // 拼接请求详情
        var requestString = $"Query: {context.Request.QueryString.Value}";
        if (context.Request.Method != "GET")
        {
            context.Request.Body.Position = 0;
            using var reader = new StreamReader(context.Request.Body, Encoding.UTF8, true, 1024, true);
            requestString += $" Body: {await reader.ReadToEndAsync()}";
        }

        // 获取用户邮箱
        var email = context.User.Claims.FirstOrDefault(x =>
            x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
                StringComparison.OrdinalIgnoreCase))?.Value;

        // 写入错误到数据库
        if (ex.StackTrace != null)
        {
            var error = new ERROR
            {
                Message = ex.Message,
                StackTrace = ex.StackTrace,
                RequestDetails = requestString,
                UserEmail = email,
                CreatedAt = DateTime.UtcNow
                // 补充其他实体属性
            };

            await errorLoggerService.CreateAsync(error);
        }

        // 返回错误响应
        await context.Response.WriteAsync(System.Text.Json.JsonSerializer.Serialize(
            new { error = "An unexpected error occurred." }));
    }
}

Startup.cs 配置

  1. 移除原来的ExceptionMiddleware.ExceptionMiddlewareConstructor调用
  2. 在Configure方法中注册中间件(注意顺序,要放在其他业务中间件之前):
app.UseMiddleware<ExceptionMiddleware>();

方案二:改造原扩展方法(最小改动)

如果你不想重构整个中间件,可以直接在原扩展方法中,从当前请求的上下文获取Scoped服务,替换静态字段的使用。

改造后的扩展方法代码

public static class ExceptionMiddleware
{
    public static void ConfigureExceptionHandler(this IApplicationBuilder app, ILog logger)
    {
        app.UseExceptionHandler(appError =>
        {
            appError.Run(async context =>
            {
                if (context == null) return;

                context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                context.Response.ContentType = "application/json";

                var requestString = $"Query: {context.Request.QueryString.Value}";
                if (context.Request.Method != "GET")
                {
                    context.Request.Body.Position = 0;
                    using var reader = new StreamReader(context.Request.Body, Encoding.UTF8, true, 1024, true);
                    requestString += $" Body: {await reader.ReadToEndAsync()}";
                }

                var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
                if (contextFeature == null) return;

                var email = context.User.Claims.FirstOrDefault(x =>
                    x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
                        StringComparison.OrdinalIgnoreCase))?.Value;

                if (contextFeature.Error.StackTrace != null)
                {
                    // 关键:从当前请求的服务容器获取Scoped实例
                    var errorLoggerService = context.RequestServices.GetRequiredService<IErrorLoggerService>();
                    var error = new ERROR
                    {
                        // 赋值实体属性
                    };

                    await errorLoggerService.CreateAsync(error);
                }
            });
        });
    }
}

Startup.cs 配置

移除原来的ExceptionMiddleware.ExceptionMiddlewareConstructor调用,直接保留:

app.ConfigureExceptionHandler(logger);

关键注意事项

  1. 生命周期校验:确保IErrorLoggerService是用AddScoped注册的,这样每个请求会获取到独立的实例,对应的DbContext也是线程安全的。
  2. 请求Body读取:设置context.Request.Body.Position = 0是必要的,因为部分前置中间件可能已经读取过Body流。
  3. 异常处理完整性:确保在写入数据库后正确返回响应,避免请求长时间挂起。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 07:35:21