MediatR 仅是请求路由工具,不实现 CQRS;CQRS 需手动结合 MediatR、读写分离、事务控制与约定达成。

MediatR 本身不实现 CQRS,它只是帮你把 IRequest 路由到唯一 IRequestHandler 的管道工具;CQRS 是你用 MediatR + 约定 + 手动事务 + 读写模型分离搭出来的架构风格。
为什么 IRequest 和 IRequest<TResponse> 必须严格配对
编译器不会帮你校验泛型参数是否一致,运行时才报 Handler was not found for request。这不是配置遗漏,而是类型系统在拒绝错误匹配。
-
GetUserQuery : IRequest<UserDto>→ Handler 必须是IRequestHandler<GetUserQuery, UserDto>,写成IRequestHandler<GetUserQuery, User>(实体类)就失败 -
DeleteUserCommand : IRequest→ Handler 必须是IRequestHandler<DeleteUserCommand>,返回Task或Task<Unit>;若误写为IRequestHandler<DeleteUserCommand, bool>,Send 调用会静默跳过 Handler - 泛型不区分大小写但区分结构:
IRequest<int>和IRequest<Int32>在 .NET 中等价,但IRequest<int?>和IRequest<int>不匹配
DbContext 生命周期错配导致 Cannot access a disposed object
Handler 默认是 Transient,而 AppDbContext 必须注册为 Scoped —— 这不是“建议”,是强制约束。错配后,Handler 构造函数注入的 DbContext 可能在 Handle 执行中途被释放。
- 注册必须显式写:
services.AddDbContext<AppDbContext>(ServiceLifetime.Scoped),不能只写AddDbContext<AppDbContext>()(.NET 默认是 Scoped,但显式声明更安全) - 查询 Handler 中禁止
new DbContext(),也禁止从IDbContextFactory创建后不Dispose;正确做法是构造注入AppDbContext并调用.AsNoTracking() - 命令 Handler 若需跨多个操作共享上下文(如“创建订单 + 扣库存”),必须确保它们在同一个 Scoped 容器中执行 —— 即同一个 HTTP 请求生命周期内
Send() 不自动包装事务,别在 Handler 里开事务
很多人以为 Send 是原子操作,其实它只是调度器。事务边界必须由调用方控制,否则会出现部分成功、数据不一致。
- 事务应在 Controller 或 Minimal API endpoint 层开启:
using var tx = await context.Database.BeginTransactionAsync();,再await mediator.Send(cmd),最后await tx.CommitAsync() - Handler 内部调用
SaveChangesAsync()不等于事务提交 —— 它只把变更刷进当前 DbContext 缓存,真正落库靠外层CommitAsync() - 若一个请求需多个 Handler 协同(如发命令 + 发通知),且都需事务一致性,它们必须共用同一个
AppDbContext实例 —— 这依赖 Scoped 生命周期正确传递,而非每个 Handler 自己 resolve 新实例
别把 INotification 当成查询或命令的替代品
Publish(new OrderCreatedNotification()) 是 fire-and-forget,不参与主流程、无返回值、不保证顺序、默认不加入事务。把它当“异步命令”用,等于放弃 CQRS 的核心契约。
- 仅用于非关键路径:发邮件、写审计日志、更新 Elasticsearch 索引
- 禁止在
INotificationHandler<OrderCreatedNotification>里调用mediator.Send(new UpdateInventoryCommand())—— 这会隐式引入嵌套命令链,破坏职责边界 - 如果需要“命令执行后同步触发另一操作”,应该在命令 Handler 内部直接调用,而不是 Publish 后再由另一个 Handler 捕获处理
最常被忽略的点:CQRS 不是“多写几个类”,而是读写路径彻底隔离 —— 查询不能改状态,命令不能返回实时聚合数据,DTO 和 Entity 必须物理分离。一旦 Handler 里出现 context.Entry(x).State = EntityState.Modified 或 AsNoTracking().FirstOrDefault() 混用,就已偏离模式本意。


















