商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > C#怎么实现CQRS模式_C# MediatR命令查询分离教程【高级】

C#怎么实现CQRS模式_C# MediatR命令查询分离教程【高级】

  发布于2026-07-18 阅读(0)

扫一扫,手机访问

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

C#怎么实现CQRS模式_C# MediatR命令查询分离教程【高级】

CQRS的落地实践中,MediatR是最常用的工具之一。但工具越顺手,越容易踩坑——很多人写着写着,命令和查询就开始“暧昧不清”,事务也管得稀里糊涂。今天就把几个最核心的陷阱和应对方案掰开揉碎了说清楚。

接口签名:命令和查询的“身份边界”

MediatR里,`IRequestHandler`和`IRequestHandler`是两个截然不同的接口,用错了不会编译报错,但运行时的行为会让你摸不着头脑。比如一个本该无返回的命令(例如删除操作),你随手给它写了`Task`的处理器,MediatR会尝试序列化这个返回值,而调用方很可能忽略它——逻辑上的错觉就埋下了。 实操上怎么区分? - **命令(Command)**:继承`IRequest`(不带泛型),处理器实现`IRequestHandler`。返回类型推荐用`Task`,比`Task`语义更清晰,一看就知道“这事儿干完了,没数据要回”。 - **查询(Query)**:继承`IRequest`,处理器必须实现`IRequestHandler`。这里的`TResponse`绝对不能是`void`。 - 别为了“统一API风格”而强行让所有请求都带上返回类型。CQRS的核心是职责分离,不是形式上的统一。命令就是命令,查询就是查询,各走各的道。

事务管理:别指望Send()自带原子性

一个常见的误解是:用了MediatR的`Send()`,命令执行和数据库提交就天然有了原子性。醒醒,`Send()`只是个管道调度器,事务得自己动手管。 最容易翻车的地方有两个:一是在Handler里直接`new DbContext()`,二是依赖注入的上下文没配对生命周期,导致事务跨Handler泄露或失效。 正确的做法并不复杂: - 注册`DbContext`时,老老实实用`AddDbContext(ServiceLifetime.Scoped)`,确保每个请求周期内是同一个实例。 - 事务的控制权放在Web API层——比如Controller或Minimal API的endpoint里。用`context.Database.BeginTransaction()`包住`mediator.Send()`调用,而不是在Handler内部开事务。 - 如果多个Handler需要协同完成一个事务(典型场景:创建订单 + 扣库存),它们必须共享同一个`DbContext`实例。这依赖DI容器的Scoped生命周期正确传递,而不是每个Handler各自去解析一个新实例。

INotification:是“广播”不是“命令通道”

新手容易把领域事件(比如`OrderCreatedNotification`)当成查询结果或命令副作用的标配通道,甚至幻想它能保证执行顺序或参与事务。但`INotification`本质上是个发布-订阅机制——无返回值、不保证顺序、默认异步fire-and-forget,典型的一次性通知。 所以它的适用范围很窄:非关键路径的衍生操作。比如发邮件、写日志、更新搜索索引。这些事情晚几秒甚至丢一两条,系统都能容忍。 有几个绝对不能做的事情: - 不要在`INotificationHandler`里修改聚合根或触发另一条命令。一旦这么干,流程就变得不可控,测试也成了噩梦,CQRS的边界形同虚设。 - 如果有强一致性的需求(比如“扣款成功后必须同步更新余额视图”),那应该由主命令的Handler显式调用写入服务,或者用事务性发件箱(Outbox pattern)来处理。依赖`MediatR.Publish()`是想多了。

查询Handler里的new DbContext:隐蔽的性能冲击波

有人图省事,在查询Handler里直接写`using var context = new AppDbContext(...)`。看起来简单安全,实际上这是性能雷区——绕过DI生命周期管理,连接池滥用、上下文释放不及时、并发下状态污染,问题一串串。更隐蔽的是,它让查询享受不到EF Core的变更跟踪优化。比如同一请求内多次查同一个实体,Scoped上下文本来可以复用实例,结果每次都重新创建,白白浪费。 解决方案很直接: - 所有Handler一律通过构造函数注入`DbContext`,确保和请求生命周期一致。 - 查询类Handler要显式标记`AsNoTracking()`——除非你真的打算后续再Update。这一行代码能省掉大量内存和性能开销。 - 还有个常见低级错误:先`.ToList()`或`.Count()`拉数据到内存,再在内存里做LINQ过滤。EF Core没办法把后续操作转成SQL,最终全量加载,性能直接拉胯。 说到底,CQRS真正难的不是写几个Handler,而是守住命令与查询的物理隔离。数据库连接、缓存策略、异常处理粒度、监控指标埋点——通通要按角色区分对待。一旦在查询Handler里写了`Sa veChangesAsync()`,或者让命令返回DTO给前端渲染,CQRS就已经退化成普通分层架构了。
本文转载于:https://www.php.cn/faq/2344351.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注