DDD + Clean Architecture 下 EF 多线程最佳实践


DDD + Clean Architecture 下 EF 多线程最佳实践

在复杂系统(尤其是 DDD + Clean Architecture 架构)中,多线程使用 EF Core 是一个非常容易踩坑的问题。

很多文章会直接推荐:

services.AddDbContextFactory<AppDbContext>();

然后在并行任务中:

using var db = _factory.CreateDbContext();

看起来合理。

但如果你正在构建:

  • DDD 分层架构
  • CQRS
  • UnitOfWork
  • DomainEvents
  • 多租户系统
  • 后台调度服务(Scheduler)

那么事情并没有这么简单。


一、问题的本质

EF Core 的 DbContext

  • ❌ 不是线程安全的
  • ❌ 不能跨线程共享
  • ❌ 不允许并发操作

官方建议:

每个工作单元使用独立 DbContext。

在 Web 请求中:

HTTP Request

Scoped DbContext

结束释放

完全没问题。

问题出在:

  • BackgroundService
  • Quartz Job
  • Task.WhenAll
  • Parallel.ForEachAsync

这些非 HTTP 生命周期场景。


二、两种方案对比

方案一:IDbContextFactory

注册:

services.AddDbContextFactory<AppDbContext>();

使用:

using var db = _factory.CreateDbContext();

优点

  • 线程安全
  • 简单
  • 官方推荐

架构问题

在 DDD + Clean Architecture 下,它会带来几个隐患:

  1. 破坏 Scoped 生命周期语义
  2. Repository / UoW 难以统一管理
  3. Application 层可能被迫感知 Infrastructure
  4. DomainEvents 生命周期失控

如果你的 CommandHandler 直接创建 DbContext:

Application → Infrastructure(直接依赖)

分层就被打穿了。


方案二(推荐):为每个并行任务创建独立 Scope

注册方式仍然保持标准:

services.AddDbContext<AppDbContext>(options =>
{
    options.UseNpgsql(configuration.GetConnectionString("postgres"));
});

在多线程场景中:

public class MyBackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public MyBackgroundService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public async Task RunAsync()
    {
        await Task.WhenAll(
            Enumerable.Range(0, 5)
                .Select(_ => ExecuteAsync())
        );
    }

    private async Task ExecuteAsync()
    {
        using var scope = _scopeFactory.CreateScope();

        var db = scope.ServiceProvider
            .GetRequiredService<AppDbContext>();

        // 依然可以通过 Repository / UoW
        await db.SaveChangesAsync();
    }
}

三、为什么 Scope 才是架构级解法?

1️⃣ 生命周期一致

无论是:

  • Web 请求
  • 后台任务
  • 调度服务
  • 并行处理

模型都变成:

Scope

DbContext

Repository

UnitOfWork

架构完全一致。


2️⃣ 保持 Clean Architecture 纯度

分层依然保持:

Domain
Application
Infrastructure
Web / Worker

Application 层:

  • 不直接创建 DbContext
  • 不依赖 Factory
  • 只依赖抽象 Repository

边界干净。


3️⃣ CQRS 更自然

在命令处理中:

CommandHandler

Repository

DbContext

如果用 Factory:

  • Handler 直接创建 DbContext
  • UoW 无法统一
  • 事务管理分散

如果用 Scope:

  • 依赖注入自动完成
  • 事务一致
  • DomainEvents 正常派发

四、性能问题?

很多人担心:

CreateScope 会不会很重?

答案是:不会。

Scope 只是创建一个轻量级子容器。

真正耗时的是:

  • SQL
  • 网络 IO
  • 磁盘
  • 序列化

而不是 Scope。


五、什么时候可以用 IDbContextFactory?

适合场景:

  • Blazor WASM
  • 控制台工具
  • 简单数据导入程序
  • 无复杂分层约束

不适合:

  • 严格 DDD
  • 多模块 CQRS
  • 多租户系统
  • 复杂 UoW 管理系统

六、最终建议(架构总结)

在 DDD + Clean Architecture 中:

✅ Web 请求

由 ASP.NET 自动创建 Scope。

✅ 后台并行任务

手动创建 Scope。

❌ 不推荐

在 Application 层直接使用 DbContextFactory。


七、一句话总结

在复杂系统中:

多线程问题本质不是“如何创建 DbContext”, 而是“如何保持生命周期的一致性”。

DbContextFactory 解决线程安全。

Scope 解决架构一致性。

在架构型系统中:

生命周期 > 线程安全


如果你正在构建一个:

  • 多租户
  • DDD
  • CQRS
  • Scheduler 微服务
  • AI 向量入库任务

请优先选择:

IServiceScopeFactory.CreateScope()

这才是可持续演进的解法。

微信订阅号二维码