C# WebApi多请求复用同一DbContext引发并发异常问题排查
问题场景
基于ASP.NET Core Web API模板+EF7构建的接口,单请求运行正常,但多请求并发时抛出System.InvalidOperationException:
A second operation was started on this context instance before a previous operation completed.
This is usually caused by different threads concurrently using the same instance of DbContext.
例如启动项目后,Swagger文档页并行请求多个JS/CSS文件时触发异常。期望每个请求对应独立的DbContext实例,但实际未实现。
当前DbContext注册方式:
webAppBuilder.Services.AddDbContext<IDbContext, MyDbContext>( options => options.UseSqlServer( webAppBuilder.Configuration.GetConnectionString("Default"), x => x.UseNetTopologySuite() ) );
尝试修改生命周期为Transient/Singleton均无效。
中间件代码(构造注入DbContext):
public MyMiddleware(RequestDelegate next, IWebHostEnvironment env, IDbContext dbContext, Configuration configuration) { Env = env; Next = next; RecyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); DbContext = dbContext; Configuration = configuration; } public async Task InvokeAsync(HttpContext context) { // 从context获取api密钥 var apiKey = ...; var apiKeyInformation = DbContext.ApiKeys.FirstOrDefault(x => x.ApiKey == apiKey); // 处理api密钥逻辑 ... await Next(context); }
中间件注册:
webApplication.UseMiddleware<MyMiddleware>();
根因分析
ASP.NET Core中,中间件默认是单例生命周期——程序启动时只会创建一次实例。如果在中间件的构造函数中注入DbContext,无论DbContext本身注册的是Scoped/Transient/Singleton,都会被绑定到中间件的单例实例上,导致所有请求共用同一个DbContext。而DbContext设计为非线程安全,并发操作自然会抛出异常。
解决方案
方案1:在InvokeAsync中获取当前请求的Scoped DbContext
不在构造函数注入DbContext,而是在每次请求的InvokeAsync方法中,通过HttpContext.RequestServices获取当前请求范围内的DbContext实例:
修改后的中间件代码:
public MyMiddleware(RequestDelegate next, IWebHostEnvironment env, Configuration configuration) { Env = env; Next = next; RecyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); Configuration = configuration; } public async Task InvokeAsync(HttpContext context) { // 获取当前请求的Scoped DbContext using var dbContext = context.RequestServices.GetRequiredService<IDbContext>(); // 从context获取api密钥 var apiKey = ...; var apiKeyInformation = dbContext.ApiKeys.FirstOrDefault(x => x.ApiKey == apiKey); // 处理api密钥逻辑 ... await Next(context); }
该方式利用了RequestServices的Scoped特性,每个请求都会拿到独立的DbContext实例,完美适配并发场景。
方案2:使用DbContext工厂
注册DbContext工厂,通过工厂在每次请求时创建新的DbContext实例:
- 修改DbContext注册方式为工厂模式:
webAppBuilder.Services.AddDbContextFactory<IDbContext, MyDbContext>( options => options.UseSqlServer( webAppBuilder.Configuration.GetConnectionString("Default"), x => x.UseNetTopologySuite() ) );
- 修改中间件代码,注入工厂并创建实例:
private readonly IDbContextFactory<IDbContext> _dbContextFactory; public MyMiddleware(RequestDelegate next, IWebHostEnvironment env, IDbContextFactory<IDbContext> dbContextFactory, Configuration configuration) { Env = env; Next = next; RecyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); _dbContextFactory = dbContextFactory; Configuration = configuration; } public async Task InvokeAsync(HttpContext context) { // 创建新的DbContext实例 using var dbContext = _dbContextFactory.CreateDbContext(); // 从context获取api密钥 var apiKey = ...; var apiKeyInformation = dbContext.ApiKeys.FirstOrDefault(x => x.ApiKey == apiKey); // 处理api密钥逻辑 ... await Next(context); }
工厂模式会确保每次调用CreateDbContext()都生成新实例,避免并发冲突。
内容的提问来源于stack exchange,提问作者NLAnaconda

