Bug: AppDbContextBase 构造函数注入 IMediator 与 AddDbContextPool 不兼容
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
Bug:
AppDbContextBase构造函数注入IMediator与AddDbContextPool不兼容Summary
AppDbContextBase构造函数要求注入IMediator(Scoped 服务),但 EF Core 的AddDbContextPool会跨请求复用 DbContext 实例,构造函数仅在首次创建时调用一次。复用的 DbContext 持有过期的IMediator引用(底层IServiceProvider已随原始 Scope 释放),导致DispatchDomainEventsAsync通过该引用调用mediator.Publish()时无法解析 Scoped 服务。此问题不仅影响 Aspire 的
AddNpgsqlDbContext(内部调用AddDbContextPool),也影响任何直接使用AddDbContextPool的场景。Environment
Root Cause
核心矛盾
EF Core 官方文档对 DbContext Pooling 的限制:
IMediator在 ASP.NET Core 中注册为 Scoped 服务,违反了这个约束。复现路径
关键代码
Reproduction
方式一:直接使用 AddDbContextPool
方式二:通过 Aspire(Aspire 内部调用 AddDbContextPool)
两种方式本质相同:
AddDbContextPool复用实例,构造函数不重新调用,IMediator引用过期。Current Workaround
使用普通
AddDbContext(Scoped 生命周期,每次请求创建新实例):代价: 放弃 DbContext Pooling 的性能优化。但对于大多数场景,数据库连接池(ADO.NET 层,如 Npgsql 内置连接池)已足够,DbContext 本身的创建开销很小。
Suggested Fix
Option A: 延迟解析 IMediator(推荐)
将构造函数参数从
IMediator改为IServiceProvider,在需要时才解析IMediator:IServiceProvider可由 root provider 注入(Singleton 兼容),GetRequiredService<IMediator>()每次从当前 Scope 解析,不持有过期引用。DbContext Pooling 场景下安全。Option B: 文档说明
在框架文档中明确标注:
AppDbContextBase的构造函数注入了 Scoped 服务(IMediator),不可与AddDbContextPool搭配使用。References