Skip to content

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

@ximenchuifeng

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions