EF Core在后台任务中怎么使用 EF Core后台服务(IHostedService)中的用法

必须用 IServiceScope 显式创建新作用域来使用 DbContext,因后台服务长生命周期且 DbContext 非线程安全;需在 ExecuteAsync 中每次循环创建作用域、获取上下文、执行操作、及时释放,并响应取消令牌。

在后台任务中使用 EF Core,核心是解决生命周期管理数据库上下文线程安全性问题。直接在 IHostedService(尤其是 BackgroundService)里复用 Web 请求作用域的 DbContext 会出错——因为后台服务是长生命周期,而默认注册的 DbContext 是 Scoped(每个请求一个实例),不能跨线程/跨作用域共享。

必须用 IServiceScope 显式创建新作用域

后台服务运行在独立线程,不自动拥有作用域。你需要从 IServiceProvider 创建一个新作用域,在其内获取 DbContext,用完立即释放。

  • 不要把 DbContext 声明为类字段(避免跨周期复用)
  • 每次执行任务逻辑前,用 serviceProvider.CreateScope() 开启新作用域
  • 从该作用域中解析 DbContext,执行查询或保存
  • 作用域对象(IServiceScope)用 using 确保及时释放,触发 DbContext 释放和连接归还

推荐继承 BackgroundService 而非直接实现 IHostedService

BackgroundService 封装了启动/停止逻辑和取消令牌传递,更安全易用。你的任务逻辑写在 ExecuteAsync(CancellationToken) 中。

  • 构造函数注入 IServiceProvider(不是 DbContext
  • ExecuteAsync 内部按需创建作用域和上下文
  • 务必监听传入的 CancellationToken,在取消时及时退出循环并释放资源
  • await Task.Delay(..., cancellationToken) 防止忙等

注意 DbContext 不是线程安全的

即使你在每次循环中都新建作用域和 DbContext,也不能在同一个 DbContext 实例上并发调用 SaveChangesAsync 或多个异步查询

  • 一个 DbContext 实例只用于一次“单元工作”(比如查+改+保存)
  • 若需并行操作(如同时拉取多个 API 并分别存库),应为每个操作单独创建作用域和上下文
  • 避免在 async/await 链中跨 await 继续使用同一上下文(EF Core 6+ 对部分场景放宽,但不建议依赖)

示例:每 5 秒检查待处理订单

代码结构示意(省略异常处理和日志):

public class OrderProcessingService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;

    public OrderProcessingService(IServiceProvider serviceProvider)
        => _serviceProvider = serviceProvider;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                using var scope = _serviceProvider.CreateScope();
                var context = scope.ServiceProvider.GetRequiredService();

                var pendingOrders = await context.Orders
                    .Where(o => o.Status == OrderStatus.Pending)
                    .ToListAsync(stoppingToken);

                foreach (var order in pendingOrders)
                {
                    order.Status = OrderStatus.Processing;
                    // ... 其他业务逻辑
                }

                await context.SaveChangesAsync(stoppingToken);
            }
            catch (OperationCanceledException)
            {
                break; // 正常退出
            }
            catch (Exception ex)
            {
                // 记录日志,但不要 throw,否则 BackgroundService 会终止
            }

            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }
    }
}

注册方式(Program.cs):

services.AddHostedService();

基本上就这些。关键就是:不共享上下文、每次用都新开作用域、及时释放、响应取消信号。