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 配置
- 移除原来的
ExceptionMiddleware.ExceptionMiddlewareConstructor调用 - 在
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);
关键注意事项
- 生命周期校验:确保
IErrorLoggerService是用AddScoped注册的,这样每个请求会获取到独立的实例,对应的DbContext也是线程安全的。 - 请求Body读取:设置
context.Request.Body.Position = 0是必要的,因为部分前置中间件可能已经读取过Body流。 - 异常处理完整性:确保在写入数据库后正确返回响应,避免请求长时间挂起。
内容的提问来源于stack exchange,提问作者JohnDiGriz
相关产品推荐
相关产品推荐

