MediatR中命令与查询须严格分离:命令继承IRequest(无泛型),用IRequestHandler<TRequest>;查询继承IRequest<TResponse>,用IRequestHandler<TRequest,TResponse>;事务需手动管理,DbContext必须Scoped注入;INotification仅用于非关键异步通知;查询严禁new DbContext,须构造注入并AsNoTracking()。

MediatR 里 IRequestHandler<TRequest, TResponse> 和 IRequestHandler<TRequest> 别混用
命令和查询在 MediatR 中都走 IRequest,但返回值决定它是“查”还是“改”。用错接口会导致编译通过但运行时行为异常——比如本该无返回的命令(如删除)却写了 Task<bool> 处理器,MediatR 会尝试序列化返回值,而调用方可能忽略它,造成逻辑错觉。
实操建议:
- 命令(Command):继承
IRequest(无泛型),处理器实现IRequestHandler<TRequest>,方法返回Task或Task<Unit>(推荐Unit,语义明确) - 查询(Query):继承
IRequest<TResponse>,处理器必须实现IRequestHandler<TRequest, TResponse>,且TResponse不能为void - 别为了“统一”强行让所有请求都带返回类型;CQRS 的核心是职责分离,不是 API 形式统一
为什么 IMediator.Send() 默认不支持事务自动包装
很多人以为用了 MediatR 就天然获得“命令执行+数据库提交”原子性,其实 Send() 只是管道调度,事务得自己管。常见错误是:在 Handler 里直接 new 一个 DbContext,或依赖注入的上下文没配置作用域生命周期,导致事务跨 Handler 泄露或失效。
实操建议:
- 注册
DbContext时务必用AddDbContext<AppDbContext>(ServiceLifetime.Scoped) - 在 Web API 层(如 Controller 或 Minimal API endpoint)开启事务,用
context.Database.BeginTransaction()包住mediator.Send()调用,而非在 Handler 内部开事务 - 若需多 Handler 协同事务(如“创建订单 + 扣库存”),它们必须共享同一个
DbContext实例——这依赖 DI 容器的 Scoped 生命周期正确传递,而不是每个 Handler 自己 resolve 新实例
INotification 和 INotificationHandler<TNotification> 不是 CQRS 查询/命令的一部分
新手常把领域事件(如 OrderCreatedNotification)当成“查询结果”或“命令副作用”的标准通道,误以为它能替代查询或保证顺序。实际上,INotification 是发布-订阅机制,无返回值、不保证执行顺序、不参与主流程事务(默认异步 fire-and-forget)。
实操建议:
- 仅用
INotification做**非关键路径**的衍生操作:发邮件、写日志、更新搜索索引 - 不要在
INotificationHandler里修改聚合根或触发另一条命令——这会让流程不可控、难以测试,也破坏 CQRS 的边界 - 如需强一致性通知(比如“扣款成功后必须同步更新余额视图”),应由主命令 Handler 显式调用查询写入服务,或使用事务性发件箱(Outbox pattern),而非依赖
MediatR.Publish()
查询 Handler 里直接 new DbContext 是性能雷区
看似简单安全的写法:using var context = new AppDbContext(...),实际会绕过 DI 生命周期管理,导致连接池滥用、上下文未被正确释放、甚至并发下状态污染。更隐蔽的问题是:它让查询无法享受 EF Core 的变更跟踪优化(比如同一请求内多次查同一实体,Scoped 上下文可复用实例)。
实操建议:
- 所有 Handler 必须通过构造函数注入
DbContext,确保与请求生命周期一致 - 查询类 Handler 应标记为
AsNoTracking(),除非你真需要后续 Update —— 这能省掉大量内存和性能开销 - 避免在查询中调用
.ToList()或.Count()后再做 LINQ 过滤;EF Core 无法将后续操作转成 SQL,会拉全量到内存
真正难的不是写几个 Handler,而是守住命令与查询的物理隔离:数据库连接、缓存策略、异常处理粒度、监控指标埋点,都要按角色区分。一旦开始在查询 Handler 里写 SaveChangesAsync(),或者让命令返回 DTO 给前端渲染,CQRS 就已经退化成普通分层架构了。


















