DbContext 是由 Root Provider 创建的,为什么不能解析 Scoped Provider?


在 ASP.NET Core + EF Core 的项目中,经常会遇到这样一个看似“反直觉”的结论:

DbContext 的“创建规则”由 Root Provider 持有, 但 DbContext 的“实例”必须始终由 Scope Provider 创建和管理。

很多问题——
包括 Domain Event 分发、后台任务、并发异常、生命周期错乱——
本质都源于对这句话理解不够深入。

这篇文章把这个问题一次讲清楚。


一、先说结论(非常重要)

DbContext 的生命周期规则只有一条:

DbContext 必须始终运行在一个明确的 Scope 中,而不能依赖 Root Provider 的隐式 Scope。

如果你在 Root Provider 上直接解析 DbContext,或者让 DbContext 反向依赖 Scoped 服务,一定会出问题


二、Root Provider 和 Scope Provider 到底是什么

1️⃣ Root Provider(根容器)

  • 应用启动时创建
  • 生命周期贯穿整个进程
  • 只能安全解析:
    • Singleton
    • Transient(但 Transient 如果依赖 Scoped 也会炸)
Application Start
└── Root ServiceProvider

2️⃣ Scope Provider(作用域容器)

  • 通常由以下场景创建:

    • HTTP 请求
    • BackgroundService
    • 手动 CreateScope()
  • 用来管理 Scoped 生命周期

HTTP Request
└── IServiceScope
    └── Scoped ServiceProvider

三、DbContext 的真实创建方式

虽然我们通常这样注册:

services.AddDbContext<AppDbContext>();

DbContext 并不是“随便就能 new 出来” 的。

实际行为是:

  • DbContext 是 Scoped
  • 只有在 Scope Provider 中才能被安全解析
  • Root Provider 没有资格管理 DbContext 的生命周期

所以这段代码是危险的:

var db = rootProvider.GetRequiredService<AppDbContext>(); // ❌

原因不是语法,而是 生命周期语义错误


四、为什么 DbContext 不能反向依赖 Scoped Provider

很多人在 DDD / CQRS 场景下会写出类似代码:

public class SomeService
{
    public SomeService(AppDbContext db, IOtherScopedService service)
    {
    }
}

如果这个 SomeService 是:

  • Singleton
  • 或者由 Root Provider 创建

那么问题就来了。

本质原因只有一句话:

Root Provider 无法感知 Scoped 生命周期的开始与结束。

一旦 Root Provider 解析了 Scoped 对象,就会导致:

  • DbContext 被“提升”为全局共享
  • 并发请求共用同一个 DbContext
  • 随机异常、脏数据、线程安全问题

五、这也是为什么 DomainEventDispatcher 要 CreateScope

你前面遇到的那个经典问题,其实正好印证了这一点:

为什么 DomainEventDispatcher 里必须重新 CreateScope?

答案很简单:

  • Domain Event 可能在:

    • 命令执行后
    • 事务提交后
    • 非 HTTP 上下文中触发
  • 此时 没有现成的 Scope

所以正确写法一定是:

using var scope = _serviceScopeFactory.CreateScope();
var handlers = scope.ServiceProvider.GetServices<IDomainEventHandler<T>>();

这是在显式声明生命周期边界


六、一个非常重要的工程原则

可以把规则总结成一句工程铁律:

谁创建 Scope,谁负责 Scope 内的一切依赖。

因此:

场景正确做法
HTTP 请求框架自动创建 Scope
Command / Query在 Application 层执行
Domain EventDispatcher 内部 CreateScope
Background Job手动 CreateScope
DbContext永远只活在 Scope 内

七、这和 DDD / CQRS 的关系

这也是为什么在干净的架构里

  • Domain 层 不注入 DbContext

  • Domain 层 不感知 DI

  • Application / Infrastructure 才负责:

    • Scope
    • DbContext
    • 事务
    • 事件分发

生命周期不是细节,而是架构的一部分。


八、总结一句话

DbContext 不是不能用, 是 不能被 Root Provider 用错方式用

一旦你开始用 Scope 明确边界 的方式设计系统, 很多“莫名其妙的问题”都会自动消失。

微信订阅号二维码