基于MediatR实现CQRS模式时,如何保持.NET API代码DRY?
CQRS + MediatR + EF 中消除代码重复的方案探讨
我正在开发遵循CQRS模式的.NET API,使用MediatR与Entity Framework,目前遇到了代码重复的问题。
场景复现
初始创建Customer的处理器
最初只有创建Customer的需求,处理器代码如下:
public class CreateCustomerHandler(DbContext context): IRequestHandler<CreateCustomerCommand, Customer> { public async Task<Customer> Handle(CreateCustomerCommand request, CancellationToken cancellationToken) { var c = new Customer() { Name = request.Name }; await context.Customer.AddAsync(c, cancellationToken); await context.SaveChangesAsync(cancellationToken); return c; } }
新增关联User后的处理器
后来需求变更:创建Customer时必须关联一个User实体,且后续支持为已有Customer添加User。此时CreateCustomerHandler修改为:
public class CreateCustomerHandler(DbContext context): IRequestHandler<CreateCustomerCommand, Customer> { public async Task<Customer> Handle(CreateCustomerCommand request, CancellationToken cancellationToken) { var c = new Customer() { Name = request.Name }; await context.Customer.AddAsync(c, cancellationToken); var u = new User() { CustomerId = c.Guid }; await context.User.AddAsync(u, cancellationToken); await context.SaveChangesAsync(cancellationToken); return c; } }
同时新增了为已有Customer添加User的处理器:
public class CreateUserHandler(DbContext context): IRequestHandler<CreateUserCommand, User> { public async Task<User> Handle(CreateUserCommand request, CancellationToken cancellationToken) { var u = new User() { CustomerId = request.CustomerId }; await context.User.AddAsync(u, cancellationToken); await context.SaveChangesAsync(cancellationToken); return u; } }
现在的问题是:如果创建User的逻辑需要修改,必须同时修改两个处理器,违反了DRY原则。虽然示例简单,但肯定有办法消除重复。
已尝试的方案
1. 使用MediatR事件
将创建User的逻辑放到CustomerCreatedNotification的处理器中:
public class CreateCustomerHandler(DbContext context, IMediator mediator): IRequestHandler<CreateCustomerCommand, Customer> { public async Task<Customer> Handle(CreateCustomerCommand request, CancellationToken cancellationToken) { var c = new Customer() { Name = request.Name }; await context.Customer.AddAsync(c, cancellationToken); await mediator.Publish( new CustomerCreatedNotification{ Customer = c }, cancellationToken ); await context.SaveChangesAsync(cancellationToken); return c; } }
问题:无法保证Customer与User创建的事务性,因为事件处理是异步执行的,可能出现Customer创建成功但User创建失败的情况。
2. 在处理器内部调用其他Command
在CreateCustomerHandler中直接发送CreateUserCommand:
public class CreateCustomerHandler(DbContext context, IMediator mediator): IRequestHandler<CreateCustomerCommand, Customer> { public async Task<Customer> Handle(CreateCustomerCommand request, CancellationToken cancellationToken) { var c = new Customer() { Name = request.Name }; await context.Customer.AddAsync(c, cancellationToken); var createUserCommand = new CreateUserCommand() { CustomerId = c.Guid }; await mediator.Send(createUserCommand, cancellationToken); await context.SaveChangesAsync(cancellationToken); return c; } }
问题:多篇文章指出这是代码异味,因为Command不再具备自包含性,调用方需要了解内部依赖的其他Command,破坏了CQRS中Command的单一职责原则。
3. 在控制器中执行多个Command
将创建Customer和User的逻辑放到控制器动作中:
[HttpPost] public async Task<IActionResult> CreateCustomer(CreateCustomerRequest request, CancellationToken cancellationToken) { var command = request.Adapt<CreateCustomerCommand>(); var customer = await mediator.Send(command, cancellationToken); var createUserCommand = new CreateUserCommand { CustomerId = customer.Guid, }; var user = await mediator.Send(createUserCommand, cancellationToken); var response = customer.Adapt<CreateCustomerResponse>(); return Ok(response); }
问题:控制器中混入了业务逻辑,且两个Command的执行不在同一个事务中,同样存在数据不一致的风险,显然不合理。
疑问
是否有合理的方法避免上述代码重复?这是使用CQRS模式的必然代价,还是我的思路存在偏差?
内容的提问来源于stack exchange,提问作者BetaByte
相关产品推荐
相关产品推荐

