应当前者而非后者当方法混读写语义或出现性能/事务冲突时; icommandhandler 专注业务逻辑与持久化,不组装响应,须无状态,mediatr+pipelinebehavior 是最稳起点。

什么时候该用 ICommandHandler<t></t> 而不是直接调用服务方法
因为 CQRS 的核心不是“拆接口”,而是“分离读写语义”。当你发现一个方法既修改状态又返回复杂视图模型,或者同一领域对象的读写路径开始出现性能/缓存/事务冲突时,才值得引入命令处理器。
典型信号包括:SaveChanges() 后立刻查库拼装响应、读写共用同一个 DbContext 导致 N+1 或延迟加载意外触发、审计日志需要精确到命令粒度。
- 命令处理器应只负责执行业务逻辑和持久化,不组装 API 响应(那是 Controller 或 MediatR 的事)
- 不要为每个 CRUD 都建
ICommand——例如UpdateUserEmailCommand合理,但UpdateUserLastLoginTimeCommand就过度了 -
ICommandHandler<t></t>实现类必须是无状态的,避免在构造函数里捕获HttpContext或ClaimsPrincipal
MediatR + IPipelineBehavior<trequest tresponse></trequest> 是最稳的起点
它不强制你改架构,又能快速落地命令验证、事务、日志等横切关注点。6 种方式里,只有这一种能让你在 20 行内跑通端到端流程,且后续可平滑替换底层实现。
常见踩坑:
- 注册时漏掉
AddMediatR(Assembly.GetExecutingAssembly()),导致IMediator解析失败,错误信息是InvalidOperationException: No service for type 'MediatR.IMediator' - 在
IPipelineBehavior中 await 异步操作后没 return,导致后续 handler 不执行(行为像静默丢包) - 把 EF Core 的
SaveChangesAsync()放在 handler 外层统一提交——这会让领域事件发布时机错乱,必须在 handler 内部显式调用
示例:事务行为中正确提交
public class UnitOfWorkBehavior<trequest tresponse> : IPipelineBehavior<trequest tresponse>
{
private readonly ApplicationDbContext _context;
public UnitOfWorkBehavior(ApplicationDbContext context) => _context = context;
public async Task<tresponse> Handle(TRequest request, RequestHandlerDelegate<tresponse> next)
{
var response = await next();
// 注意:仅当 response 是 ValueTask 或 Task 且 handler 确实改了数据才 Save
if (response is Task { IsCompleted: true } || response is ValueTask { IsCompleted: true })
await _context.SaveChangesAsync();
return response;
}
}</tresponse></tresponse></trequest></trequest>
IQueryHandler<tquery tresult></tquery> 别碰 EF Core 的 AsNoTracking() 默认开关
很多人以为“查询就该关跟踪”,结果在需要导航属性懒加载或变更检测的场景下出问题(比如 Admin 后台编辑页预加载关联数据)。是否关闭跟踪,取决于你是否要复用实体做更新,而不是“这是 Query”这个标签。
- 纯 DTO 投影(
Select(x => new UserDto {...}))可以放心AsNoTracking(),性能提升明显 - 如果 query handler 返回的是
User实体,并且后续可能调用_context.Entry(user).State = EntityState.Modified,就必须保留跟踪 - 别在 DbContext 层级全局设
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking——这会让所有查询失效,连FindAsync()都返回 null
6 种方式里,只有 3 种值得认真考虑
所谓“6 种”常指:手写接口+实现、MediatR、CQRSLite、Brighter、EventFlow、自研消息总线。其中 CQRSLite 已归档,Brighter 依赖重且文档割裂,EventFlow 绑死事件溯源,对纯 CQRS 场景反而臃肿。
真正可选的就三个:
- MediatR:适合 90% 的 Web API 场景,生态成熟,调试友好
- 手写轻量接口(
ICommandDispatcher+ 字典路由):适合嵌入式或极简微服务,可控性强,但得自己写 pipeline 和异常映射 - 基于
System.Threading.Channels的内存队列:适合高吞吐后台作业(如批量导入),但放弃强一致性,需接受最终一致
别低估 MediatR 的扩展成本——一旦你开始给每个命令加 [Authorize] 特性、注入不同租户 DbContext、或按请求头分流到不同数据库,那些“简单”的抽象就会迅速变脆。这时候,不是换框架,而是该重新审视:你到底需要 CQRS,还是只需要更好的分层?











