Skip to content

# Bug: AppDbContextBase 构造函数注入 IMediatorAddDbContextPool 不兼容 #355

Description

@ximenchuifeng

Bug: AppDbContextBase 构造函数注入 IMediatorAddDbContextPool 不兼容

Summary

AppDbContextBase 构造函数要求注入 IMediator(Scoped 服务),但 EF Core 的 AddDbContextPool 会跨请求复用 DbContext 实例,构造函数仅在首次创建时调用一次。复用的 DbContext 持有过期的 IMediator 引用(底层 IServiceProvider 已随原始 Scope 释放),导致 DispatchDomainEventsAsync 通过该引用调用 mediator.Publish() 时无法解析 Scoped 服务。

此问题不仅影响 Aspire 的 AddNpgsqlDbContext(内部调用 AddDbContextPool),也影响任何直接使用 AddDbContextPool 的场景。

Environment

Item Version
NetCorePal Cloud Framework v3.0.3
.NET 10.0
MediatR 12.x

Root Cause

核心矛盾

EF Core 官方文档对 DbContext Pooling 的限制:

Constructor parameters must be resolvable from the root IServiceProvider (i.e. must be Singleton services).
EF Core Performance: DbContext Pooling

IMediator 在 ASP.NET Core 中注册为 Scoped 服务,违反了这个约束。

复现路径

AddDbContextPool<ApplicationDbContext>()
  → Pool 首次创建实例:构造函数注入 IMediator(scope-1)
  → 请求结束:DbContext 归还 Pool,scope-1 被 Dispose
  → 下一个请求:从 Pool 取出同一 DbContext(_mediator 仍指向 scope-1 的 IMediator)
  → SaveEntitiesAsync → DispatchDomainEventsAsync → _mediator.Publish()
  → 尝试从已释放的 scope-1 解析 IPipelineBehavior / IRequestHandler → 异常

关键代码

// src/NetCorePal.Extensions.Repository.EntityFrameworkCore/AppDbContextBase.cs
public abstract class AppDbContextBase : DbContext, ITransactionUnitOfWork
{
    private readonly IMediator _mediator;  // ← Scoped 服务被缓存为 readonly 字段

    protected AppDbContextBase(DbContextOptions options, IMediator mediator) : base(options)
    {
        _mediator = mediator;
    }

    // SaveEntitiesAsync → DispatchDomainEventsAsync → _mediator.Publish()
}

Reproduction

方式一:直接使用 AddDbContextPool

var builder = WebApplication.CreateBuilder(args);

// ❌ 与 AppDbContextBase 不兼容
builder.Services.AddDbContextPool<ApplicationDbContext>(options =>
    options.UseNpgsql(connectionString));

builder.Services.AddMediatR(cfg =>
    cfg.RegisterServicesFromAssemblies(Assembly.GetExecutingAssembly()));

方式二:通过 Aspire(Aspire 内部调用 AddDbContextPool)

// ❌ 同样不兼容,Aspire 的 AddNpgsqlDbContext 强制使用 Pooling
// 参考: https://github.com/dotnet/aspire/discussions/5154
builder.AddNpgsqlDbContext<ApplicationDbContext>("PostgreSQL",
    configureSettings: settings => settings.DisableRetry = true);

两种方式本质相同:AddDbContextPool 复用实例,构造函数不重新调用,IMediator 引用过期。

Current Workaround

使用普通 AddDbContext(Scoped 生命周期,每次请求创建新实例):

// ✅ 不使用 Pooling,每次请求创建新 DbContext,IMediator 始终来自当前 Scope
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
    options.UseNpgsql(builder.Configuration.GetConnectionString("PostgreSQL"));
    options.EnableDetailedErrors();
});

// 如使用 Aspire,补回 health check / OpenTelemetry / metrics(不含 pooling)
builder.EnrichNpgsqlDbContext<ApplicationDbContext>(settings => settings.DisableRetry = true);

代价: 放弃 DbContext Pooling 的性能优化。但对于大多数场景,数据库连接池(ADO.NET 层,如 Npgsql 内置连接池)已足够,DbContext 本身的创建开销很小。

Suggested Fix

Option A: 延迟解析 IMediator(推荐)

将构造函数参数从 IMediator 改为 IServiceProvider,在需要时才解析 IMediator

public abstract class AppDbContextBase : DbContext, ITransactionUnitOfWork
{
    private readonly IServiceProvider _serviceProvider;

    protected AppDbContextBase(DbContextOptions options, IServiceProvider serviceProvider) : base(options)
    {
        _serviceProvider = serviceProvider;
    }

    private IMediator Mediator => _serviceProvider.GetRequiredService<IMediator>();
}

IServiceProvider 可由 root provider 注入(Singleton 兼容),GetRequiredService<IMediator>() 每次从当前 Scope 解析,不持有过期引用。DbContext Pooling 场景下安全。

Option B: 文档说明

在框架文档中明确标注:AppDbContextBase 的构造函数注入了 Scoped 服务(IMediator),不可与 AddDbContextPool 搭配使用。

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions