CQRS在C#中最简手动实现是通过分离ICommand/IQuery接口及对应处理器,命令只改状态无返回,查询只读数据不修改,类型不可变且职责明确。

什么是CQRS在C#里最简可行的手动实现
CQRS(Command Query Responsibility Segregation)本质是把“改数据”和“读数据”彻底拆开——不是靠框架,而是靠接口分离、类型隔离和调用路径隔离。不依赖 MediatR 时,核心就是自己定义 ICommand / IQuery 接口,再配两个独立的处理器抽象,不共享输入/输出模型,也不共用执行管道。
手动定义命令与查询的接口和处理器
关键不是写得多,而是分得清。命令不返回业务数据(只返回 void 或 Task),查询不修改状态(方法体里不能有 SaveChanges、Update 等)。所有类型都应显式声明职责:
-
ICommandHandler:只接受一个TCommand,无返回值 -
IQueryHandler:接受TQuery,必须返回TResult - 命令和查询类型本身是
record或不可变class,不继承、不带行为
示例:
public record CreateOrderCommand(string CustomerId, decimal Amount); public interface ICommandHandler{ Task Handle(TCommand command); } public class CreateOrderCommandHandler : ICommandHandler { private readonly OrderDbContext _db; public CreateOrderCommandHandler(OrderDbContext db) => _db = db; public async Task Handle(CreateOrderCommand command) { var order = new Order { CustomerId = command.CustomerId, Amount = command.Amount }; await _db.Orders.AddAsync(order); await _db.SaveChangesAsync(); } }
如何避免手写大量 if-else 或 switch 路由逻辑
不用 MediatR 就意味着没有自动泛型解析,但也不必硬写反射调度。推荐两种轻量方案:
- DI 容器直接注册具体处理器,按需注入——比如在 Controller 里明确构造
CreateOrderCommandHandler,不追求“统一路由” - 若真需要统一入口(如 API 层只暴露一个
Dispatch()),可用Dictionary静态缓存已注册的处理器实例,首次访问时通过Activator.CreateInstance构建并缓存,后续直接Cast调用 - 切忌在调度层做运行时类型判断 + 反射调用——性能差、堆分配多、调试困难
注意:IServiceProvider.GetService(Type) 可用,但必须确保该 Type 已在 DI 中注册为具体实现,否则返回 null 不报错,容易漏测。
查询侧容易忽略的隔离细节
很多人只拆了命令,查询仍用 EF 的 DbSet 直接暴露给 API,这等于没 CQRS。真正隔离要体现在三处:
- 查询 DTO 必须和实体类物理分离(不同命名空间、不同程序集更佳),禁止
select new OrderDto()之外的任何对实体的引用 - 查询 Handler 内部应使用
AsNoTracking(),且不复用命令侧的DbContext实例(哪怕同一请求周期) - 避免在查询中调用
Include()加载深层导航——那是命令侧或领域服务的事;查询应只投射所需字段,用SELECT x,y,z级别控制
一个典型错误是:在 GetOrderSummaryQueryHandler 里调用了 _db.Orders.Include(x => x.Items),结果无意中触发了延迟加载或全表扫描——这不是 CQRS,这是披着查询外衣的命令副作用。










