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