为何从新作用域解析的 DbContext 会注入根 IServiceProvider?
通过新作用域取得由 AddDbContextPool<EntityService>() 管理的 EntityService,并不意味着它的构造函数依赖也由该作用域创建。如果上下文需要使用当前作用域的依赖,可以改用 AddDbContext<EntityService>(),并在启动任务完成后释放作用域。
为什么会注入根 IServiceProvider?
对于由默认 DI 容器在当前作用域内直接创建的普通 scoped 服务,你的理解是对的。但 AddDbContextPool 使用一个池来管理上下文实例。作用域负责取得和归还实例,构造函数不会在每次取得实例时重新执行。官方文档也说明,池化上下文的构造函数依赖不会随每次使用重新解析。参见 AddDbContextPool API 说明。
以 EF Core 8.0.0 的实现为例:
AddDbContextPool将IDbContextPool<TContext>注册为 singleton,同时通过 scoped 租约提供上下文。- 池创建时接收
IServiceProvider。在默认 DI 容器下,这个 singleton 使用的是根 provider。 - 池需要创建新上下文时,使用保存的 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 组件中的上下文共享和并发使用仍需单独评估。
备注:内容仅供参考。
