PAYATHON 2026

为何从新作用域解析的 DbContext 会注入根 IServiceProvider?

数据阿明

通过新作用域取得由 AddDbContextPool<EntityService>() 管理的 EntityService,并不意味着它的构造函数依赖也由该作用域创建。如果上下文需要使用当前作用域的依赖,可以改用 AddDbContext<EntityService>(),并在启动任务完成后释放作用域。

为什么会注入根 IServiceProvider?

对于由默认 DI 容器在当前作用域内直接创建的普通 scoped 服务,你的理解是对的。但 AddDbContextPool 使用一个池来管理上下文实例。作用域负责取得和归还实例,构造函数不会在每次取得实例时重新执行。官方文档也说明,池化上下文的构造函数依赖不会随每次使用重新解析。参见 AddDbContextPool API 说明。

以 EF Core 8.0.0 的实现为例:

  1. AddDbContextPool 将 IDbContextPool<TContext> 注册为 singleton,同时通过 scoped 租约提供上下文。
  2. 池创建时接收 IServiceProvider。在默认 DI 容器下,这个 singleton 使用的是根 provider。
  3. 池需要创建新上下文时,使用保存的 provider 解析额外的构造函数参数,包括你的 IServiceProvider 和 IEncryptionService。

因此,sp.GetRequiredService<EntityService>() 可以正常取得上下文,但通过上下文中的构造函数 provider 再次解析 scoped 服务时,会触发根作用域校验。这个过程可以在 服务注册源码 和 DbContextPool 创建实例的源码 中确认。问题没有提供 EF Core 版本,具体内部实现应以项目实际使用的版本为准。

需要当前作用域的依赖时,改用 AddDbContext

将原有的池化注册替换为:

builder.Services.AddDbContext<EntityService>(opts =>
    opts.UseSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddHostedService<StartupCleaner>();

AddDbContext 默认将上下文注册为 scoped。如果没有其他注册覆盖,且使用默认 DI 容器,上下文就由当前作用域创建,构造函数中的 IServiceProvider 也对应这个作用域。在该作用域中再次解析 EntityService,会得到同一个实例。参见 DbContext 生命周期与依赖注入。

不过,启动任务已经持有 entityService,通常可以直接使用,无需通过上下文暴露 provider 再解析一次。

启动任务要持有并释放作用域

AddHostedService 注册的服务是 singleton,默认不会为其创建业务作用域。可以注入 IServiceScopeFactory,在任务中创建作用域,并保存和释放作用域对象。参见 在托管服务中使用 scoped 服务。

下面是作用域的管理方式,清理逻辑放在注释位置:

public class StartupCleaner(IServiceScopeFactory serviceScopeFactory)
    : IHostedService
{
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        await using var scope = serviceScopeFactory.CreateAsyncScope();

        var entityService = scope.ServiceProvider
            .GetRequiredService<EntityService>();

        cancellationToken.ThrowIfCancellationRequested();

        // 在这里使用 entityService 执行启动清理。
        // 异步操作应在作用域释放前完成并 await。
    }

    public Task StopAsync(CancellationToken cancellationToken)
        => Task.CompletedTask;
}

原代码只保存了 .ServiceProvider,丢失了作用域对象,因此没有在任务结束时显式释放作用域。这个生命周期问题也需要修正,但它不是构造函数注入根 provider 的原因。

如果仍要保留 AddDbContextPool

可以继续从 scope.ServiceProvider 取得并使用上下文,但应移除上下文中保存和暴露当前作用域 provider 的设计。需要 scoped 依赖的业务逻辑可以放到单独的 scoped 服务中,由该服务注入上下文和其他依赖。池化实例可能在不同作用域之间复用,不能依赖构造函数捕获每次使用时的作用域。参见 DbContext 池化与状态管理。

还要检查 IEncryptionService:题目没有提供它的注册方式,无法判断其生命周期是否适合池化。如果它依赖当前用户、租户或其他 scoped 状态,仅移除 IServiceProvider 参数仍可能留下问题。

不要通过关闭作用域校验或把 EntityService 改为 singleton 来消除异常。对于这个启动任务,优先让每次操作拥有明确的作用域,并在操作结束后释放上下文。该调整只解决这里的启动任务问题;Blazor Server 组件中的上下文共享和并发使用仍需单独评估。

备注:内容仅供参考。