.NET Core 3.1 Scoped上下文在路由执行完成前被释放问题
Scoped生命周期上下文在路由执行完成前被提前释放问题
环境信息
- 运行环境:Windows 10 + .NET Core 3.1 MVC
- 异常表现:注册为Scoped生命周期的数据库上下文、工作单元实例,在控制器逻辑及响应完成回调未执行完毕时就被调用Dispose,调整服务生命周期无法解决。
复现代码
Unit Of Work 实现
public interface IUnitOfWork { void Dispose(); bool GetContextDisposed(); } public class UnitOfWork : IDisposable, IUnitOfWork { private ISomeContext _context; public ISomeRepository someRepository; public UnitOfWork(ISomeContext context) { _context = context; someRepository = new SomeRepository(_context); } public void Dispose() { _context.Dispose(); } public bool GetContextDisposed() { return _context.GetDisposed(); } }
上下文定义与实现
public interface ISomeContext { void Dispose(); public bool GetDisposed(); } public class SomeContext : IDisposable, ISomeContext { private bool isDisposed = false; public void Dispose() { Console.WriteLine("Dispose being called on Context"); isDisposed = true; } public bool GetDisposed() { return isDisposed; } }
服务注册
services.AddScoped<ISomeContext, SomeContext>(); services.AddScoped<IUnitOfWork, UnitOfWork>();
控制器逻辑
[Route("api/[controller]")] [ApiController] public class ValuesController : ControllerBase { private readonly IUnitOfWork _uow; public ValuesController(IUnitOfWork uow) { _uow = uow; } [Route("Test")] public async Task<IActionResult> GetTest() { Task myTask = SomeLongRunningThing(); try { return Ok(); } finally { Response.OnCompleted(async () => { Task continueTask = myTask.ContinueWith(async (task) => { await Task.Delay(5000); Console.WriteLine("Checking if context is disposed..."); Console.WriteLine(_uow.GetContextDisposed()); }); await myTask; await continueTask; }); } Console.WriteLine("End of route"); } private Task SomeLongRunningThing() { return Task.Run(() => { Task.Delay(10000); }); } }
运行输出
Dispose being called on Context
Dispose being called on Context
Dispose being called on Context
Dispose being called on Context
Checking if context is disposed...
True
根本原因
- ASP.NET Core 中Scoped生命周期的服务,生命周期绑定到当前请求对应的独立
IServiceScope。框架会在请求主处理管道执行完成(即Action方法返回结果、响应开始向客户端发送的节点),立刻释放该请求的Scope,所有Scope内注册的Scoped服务会被批量触发Dispose,这个释放时机和Response.OnCompleted回调的执行没有绑定关系。 Response.OnCompleted的回调触发时机是响应内容完全发送到客户端之后,此时请求对应的Scope已经被释放,回调中直接引用控制器注入的Scoped服务实例,自然会得到已释放的状态。- 代码中额外存在两个逻辑问题:
SomeLongRunningThing方法内的Task.Delay(10000)没有await,实际执行时会立刻返回,10秒延迟不会生效ContinueWith没有指定任务调度器,回调执行的线程上下文不可控,容易出现跨线程访问请求对象的异常
解决方案
- 不要在
Response.OnCompleted这类脱离请求主管道的回调中,直接使用请求Scope注入的Scoped服务。如果需要在响应发送后执行后续逻辑,需要手动创建独立的服务Scope,从新Scope中解析所需服务,用完后自行释放:
Response.OnCompleted(async () => { // 创建独立服务Scope,和原请求Scope生命周期隔离 using var independentScope = HttpContext.RequestServices.CreateScope(); // 从新Scope解析服务,不要复用控制器注入的_uow var uow = independentScope.ServiceProvider.GetRequiredService<IUnitOfWork>(); await myTask; await Task.Delay(5000); Console.WriteLine("Checking if context is disposed..."); Console.WriteLine(uow.GetContextDisposed()); // 返回False,服务会在using代码块结束后才释放 });
- 如果是需要执行超过数秒的长耗时任务,不要放在请求回调中执行,应该实现后台任务队列,将任务投递到
IHostedService实现的托管后台服务中执行,完全脱离请求生命周期,避免请求回收导致任务中断、对象释放问题。 - 禁止为了规避释放问题将服务生命周期改为Singleton,会导致数据库上下文等有状态服务被所有请求共享,引发并发冲突、数据脏读等严重问题。
内容的提问来源于stack exchange,提问作者Jeremy Farmer
相关产品推荐
相关产品推荐

