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