.NET Core异步后台任务中如何避免上下文被释放问题
问题根因
这个上下文被释放的问题,本质是入队的异步委托直接捕获了HTTP请求作用域里的服务实例:
- ASP.NET Core 每个进来的HTTP请求都会生成独立的DI依赖注入作用域,请求处理完成后,这个作用域里所有实现了
IDisposable的服务,不管注册为Scoped还是Transient,都会被容器自动释放。 - 现有入队代码直接使用了请求处理过程中注入的
_projectRepository,等后台任务真正执行到Update逻辑时,原HTTP请求早就处理完成,对应的作用域已经被销毁,仓储持有的数据库上下文自然已经被回收,哪怕把服务和上下文都改成Transient注册也没用——Transient实例是归解析它的作用域管释放的,只要是在请求作用域里解析出来的,请求结束照样会被回收。
无第三方依赖修复方案
核心思路:不要在入队时直接捕获请求里的仓储、数据库上下文实例,改为后台任务执行时主动创建独立于HTTP请求的DI作用域,从新作用域里解析需要的服务。
- 首先确保你的后台队列执行服务(即继承
BackgroundService的队列消费类),构造函数里注入了根级IServiceProvider实例,这个实例随应用启动生成,不会随请求结束释放。 - 把原来的入队代码改成如下写法:
_backgroundWorkerQueue.QueueBackgroundWorkItem(async (cancellationToken) => { // 长耗时API调用逻辑保持,注意如果是HttpClient建议用IHttpClientFactory创建,不要捕获请求中的实例 await client.ExecuteAsync(request, CancellationToken.None); // 为当前后台任务创建独立的DI作用域,using结束后作用域及其中解析的服务会自动释放 using var taskScope = _serviceProvider.CreateScope(); // 从独立作用域解析仓储实例,这个实例归当前任务作用域所有,不受原请求生命周期影响 var scopedProjectRepository = taskScope.ServiceProvider.GetRequiredService<IProjectRepository>(); await scopedProjectRepository.Update(id, "Update", "unlock"); });
注意:用来创建作用域的
_serviceProvider必须是从后台服务构造函数注入的根容器实例,不能用请求上下文中的RequestServices,否则创建出来的作用域还是会绑定到原请求生命周期,依然会出现释放问题。
避坑提醒
- 后台任务里用到的所有依赖数据库上下文、有生命周期要求的服务,都必须从新建的任务作用域里解析,绝对不要直接捕获请求作用域里的实例
- 任务执行时建议用后台服务传递的取消令牌,不要硬写
CancellationToken.None,这样应用正常关停时可以优雅通知任务取消,避免数据不一致 - 不要为了省事把DbContext或者仓储注册成单例来绕开问题,会导致上下文缓存污染、并发操作异常等更难排查的故障
- 如果任务里需要发送HTTP请求,不要手动new HttpClient,同样从作用域解析
IHttpClientFactory来创建客户端即可,避免套接字资源泄漏。
为什么把服务改成Transient依然报错
很多人遇到这个问题第一反应是改服务生命周期,但实际上DI容器的释放规则是:
- 单例服务由根作用域管理,应用退出时才会释放
- Scoped服务由创建它的作用域管理,作用域销毁时服务就被回收
- Transient服务和Scoped服务的释放逻辑完全一致:哪个作用域解析的它,就由哪个作用域负责释放,只要是在HTTP请求作用域里解析的Transient实例,请求结束后照样会被回收,根本存活不到后台任务执行的时点,所以改Transient完全解决不了问题。
内容的提问来源于stack exchange,提问作者anthino12
相关产品推荐
相关产品推荐

