当常规CRUD不再奏效:CQRS和事件溯源入门
\n\n想象一下,您正在为银行编写一个系统。每秒都有数百名用户检查余额、转账、开立存款。您的C#应用程序使用标准的Entity Framework和典型的CRUD操作运行。突然——数据库崩溃了。为什么?因为一个繁重的报表在写入事务时锁定了表。熟悉吗?
在C#中开发复杂系统的开发人员迟早会遇到经典架构的瓶颈。两个强大的模式可以帮到您:CQRS(命令查询职责分离)和事件溯源。让我们通过实际示例(而非枯燥的理论)来实践它们。
\n\n什么是CQRS,为什么它不仅仅是“分离”?
\n\nCQRS并不是简单地将方法放入不同的文件夹。它是一种理念:命令(改变状态)和查询(读取状态)应该分开存在。在经典的CRUD中,您只有一个数据模型用于读取和写入。而在CQRS中,您至少有两个。
经典方法的问题
\n假设您有一个UserRepository,其中包含GetUserById方法。当您读取用户以显示在屏幕上时,您需要3个字段。当您更新时,需要10个字段和验证。结果,一个模型变成了一个充满null字段和混乱映射器的怪物。
在CQRS中,您分离了职责:
\n- \n
- Command — 执行操作(例如
CreateOrderCommand) \n - Query — 返回数据(例如
GetOrderQuery) \n - Read Model — 扁平化的DTO,针对特定屏幕优化 \n
- Write Model — 包含业务逻辑的丰富领域模型 \n
使用MediatR在C#中编写CQRS
\n在C#中实现CQRS最流行的方法是使用MediatR库。它实现了中介者模式,并完美契合命令和查询的概念。
步骤1:定义命令
\npublic record CreateOrderCommand(\n Guid CustomerId,\n List<OrderItemDto> Items,\n string ShippingAddress\n) : IRequest<Guid>; // 返回创建的订单ID\n\n步骤2:命令处理器
\npublic class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>\n{\n private readonly IOrderRepository _orderRepository;\n private readonly IUnitOfWork _unitOfWork;\n\n public CreateOrderCommandHandler(IOrderRepository orderRepository, IUnitOfWork unitOfWork)\n {\n _orderRepository = orderRepository;\n _unitOfWork = unitOfWork;\n }\n\n public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)\n {\n // 业务逻辑:检查、验证、创建\n var order = new Order(request.CustomerId, request.ShippingAddress);\n \n foreach (var item in request.Items)\n {\n order.AddItem(item.ProductId, item.Quantity, item.Price);\n }\n\n await _orderRepository.AddAsync(order);\n await _unitOfWork.SaveChangesAsync(cancellationToken);\n \n return order.Id;\n }\n}\n\n步骤3:定义查询
\npublic record GetOrderSummaryQuery(Guid OrderId) : IRequest<OrderSummaryDto>;\n\n步骤4:查询处理器(从单独的数据库读取!)
\npublic class GetOrderSummaryQueryHandler : IRequestHandler<GetOrderSummaryQuery, OrderSummaryDto>\n{\n private readonly IReadDbContext _readDbContext; // 用于读取的单独上下文\n\n public GetOrderSummaryQueryHandler(IReadDbContext read