使用Task.Run调用DbContext时触发上下文已释放异常的问题
在ASP.NET Core应用中,使用Task.Run执行后台逻辑时,调用依赖注入的DbContext会触发对象已释放异常。替换为Hangfire的BackgroundJob.Enqueue可正常运行,但因方法调用频繁,短时间内Redis会产生大量(>1k)条目,现寻求Task.Run场景下的解决办法。
代码调用链
UserController.cs
[Route("api/[controller]")] public class UsersController : ControllerBase { private readonly UserService userService; public UsersController(UserService userService) { this.userService = userService; } public async Task<ActionResult> DoSomething() { await this.userService.MyMethod(); return this.Ok(); } }
UserService.cs
public class UserService { private readonly UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public async Task MyMethod() { // some logic Task.Run(() => MethodCallAsync()); } void MethodCallAsync() { // some logic // calls UserRepository, which uses the DbContext by DI } }
UserRepository.cs
public class UserRepository { private MyDbContext dbContext; public UserRepository(MyDbContext dbContext) { this.dbContext = dbContext; } public async Task DoSomethingToo(string username) { var user = await this.dbContext.Users.SingleOrDefaultAsync(u => u.Username == username); // some logic } }
异常信息
Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.
服务配置
Startup.cs
public void ConfigureServices(IServiceCollection services) { // some logic if (MyDbContextFactory.GetConnectionString() != null) { services.AddDbContext<MyDbContext>(options => options.UseMySQL(MyDbContextFactory.GetConnectionString()) .LogTo(s => System.Diagnostics.Debug.WriteLine(s))); } services.AddScoped(typeof(UserService)); services.AddScoped(typeof(UserRepository)); // some logic } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // some logic using (var serviceScope = app.ApplicationServices.CreateScope()) { var dbContext = serviceScope.ServiceProvider.GetService<MyDbContext>(); if (dbContext != null) { dbContext.Database.Migrate(); } } // some logic } public class MysqlEntityFrameworkDesignTimeServices : IDesignTimeServices { public void ConfigureDesignTimeServices(IServiceCollection serviceCollection) { serviceCollection.AddEntityFrameworkMySQL(); new EntityFrameworkRelationalDesignServicesBuilder(serviceCollection) .TryAddCoreServices(); } }
MyDbContextFactory.cs
public MyDbContext CreateDbContext(string[] args) { var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>(); optionsBuilder.UseMySQL(GetConnectionString()); return new MyDbContext(optionsBuilder.Options); }
- ASP.NET Core中
AddDbContext默认注册为Scoped生命周期,DbContext会在请求结束时被DI容器自动释放。 Task.Run开启的后台线程脱离了原请求的Scope,当原请求完成后,Scoped的DbContext被释放,后台线程再调用时就会触发异常。- Hangfire之所以正常,是因为它会为每个后台任务创建独立的Scope,确保
DbContext在任务生命周期内有效。
方案一:手动创建独立Scope获取DbContext
修改UserService,注入IServiceScopeFactory,在后台任务中创建独立Scope获取所需服务:
public class UserService { private readonly IServiceScopeFactory _scopeFactory; public UserService(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } public async Task MyMethod() { // some logic Task.Run(async () => await MethodCallAsync()); } async Task MethodCallAsync() { using (var scope = _scopeFactory.CreateScope()) { var userRepository = scope.ServiceProvider.GetRequiredService<UserRepository>(); // 调用repository的方法 await userRepository.DoSomethingToo("username"); } } }
说明:
- 使用
IServiceScopeFactory创建独立Scope,确保后台任务拥有自己的Scoped服务实例(包括DbContext)。 - 用
using语句自动释放Scope,避免资源泄漏。 - 将
MethodCallAsync改为async Task,符合异步规范(原void返回类型会导致异常无法被捕获)。
方案二:将DbContext注册为Transient(不推荐)
修改Startup.cs中DbContext的注册方式:
services.AddDbContext<MyDbContext>(options => options.UseMySQL(MyDbContextFactory.GetConnectionString()), ServiceLifetime.Transient);
说明:
- Transient生命周期的
DbContext每次请求都会创建新实例,不会被原请求Scope释放影响。 - 但此方式会导致频繁创建
DbContext实例,可能带来性能问题,且需手动管理释放(若DI容器未处理),仅适合简单场景。
方案三:使用Hosted Service处理高频后台任务
如果方法调用频繁,推荐使用IHostedService(或BackgroundService)管理后台任务队列,避免每次调用都开启新线程:
- 创建后台任务队列:
public class BackgroundTaskQueue : IBackgroundTaskQueue { private readonly ConcurrentQueue<Func<CancellationToken, Task>> _workItems = new(); private readonly SemaphoreSlim _signal = new(0); public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem) { if (workItem == null) throw new ArgumentNullException(nameof(workItem)); _workItems.Enqueue(workItem); _signal.Release(); } public async Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken) { await _signal.WaitAsync(cancellationToken); _workItems.TryDequeue(out var workItem); return workItem; } } public interface IBackgroundTaskQueue { void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem); Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken); }
- 创建Hosted Service:
public class QueuedHostedService : BackgroundService { private readonly IBackgroundTaskQueue _taskQueue; private readonly ILogger<QueuedHostedService> _logger; public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILogger<QueuedHostedService> logger) { _taskQueue = taskQueue; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var workItem = await _taskQueue.DequeueAsync(stoppingToken); try { await workItem(stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "后台任务执行出错"); } } } }
- 在
Startup中注册:
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>(); services.AddHostedService<QueuedHostedService>();
- 修改
UserService使用队列:
public class UserService { private readonly IBackgroundTaskQueue _taskQueue; private readonly IServiceScopeFactory _scopeFactory; public UserService(IBackgroundTaskQueue taskQueue, IServiceScopeFactory scopeFactory) { _taskQueue = taskQueue; _scopeFactory = scopeFactory; } public async Task MyMethod() { // some logic _taskQueue.QueueBackgroundWorkItem(async token => { using (var scope = _scopeFactory.CreateScope()) { var userRepository = scope.ServiceProvider.GetRequiredService<UserRepository>(); await userRepository.DoSomethingToo("username"); } }); } }
说明:
- 通过队列缓冲高频任务,避免短时间内创建大量线程。
- 后台服务统一管理任务执行,更高效且易于监控。
- 同样需在任务中创建独立Scope获取Scoped服务。
内容的提问来源于stack exchange,提问作者Jimy Weiss

