不要再混淆 CQRS 和 MediatR

banq

.NET 生态系统逐渐将CQRS 和 MediatR两个概念融合在一起,形成了一种几乎反射性的响应:CQRS 等于 MediatR。

这种思维捷径让无数团队陷入不必要的复杂性。其他团队则完全避免使用 CQRS,担心又多了一个消息传递框架的开销。
在本文中,我们将消除一些常见的误解并强调每种模式的优点。

理解纯粹形式的 CQRS
CQRS是一种在应用程序中分离读取和写入操作的模式。

该模式建议用于读取数据的模型应与用于写入数据的模型不同,就是这些。

没有具体的实施细节,没有规定的库,只有简单的架构原理。

这种模式源于这样的认识:在许多应用程序中,尤其是那些具有复杂域的应用程序,读取和写入数据的要求根本不同。读取操作通常需要组合来自多个来源的数据或以特定格式呈现数据以供 UI 使用。写入操作需要执行业务规则、保持一致性并管理域状态。

这种分离有几个好处:

  • 针对特定目的优化读写模型
  • 由于读写问题独立发展,维护得到简化
  • 增强了读写操作的可扩展性选项
  • 领域逻辑和表现需求之间的界限更加清晰


MediatR:针对不同问题的不同工具
MediatR中介模式的一种实现。其主要目的是通过提供中心通信点来减少组件之间的直接依赖关系。中介连接组件,而不是让组件相互了解。
该库提供了几个功能:

  • 组件之间的进程内消息传递
  • 跨切关注点的行为管道
  • 通知处理(发布/订阅)

MediatR 引入的间接性是其最受诟病的方面。它会使代码更难理解,尤其是对于代码库的新手来说。但是,您可以通过在与处理程序相同的文件中定义请求来轻松解决此问题。

为什么他们经常一起出现
CQRS 和 MediatR 频繁搭配使用并非毫无道理。MediatR 的请求/响应模型与 CQRS 的命令/查询分离非常契合。命令和查询可以作为 MediatR 请求实现,处理程序包含实际实现逻辑。

以下是使用 MediatR 的示例命令:

public record CreateHabit(string Name, string? Description, int Priority) : IRequest<HabitDto>;

public sealed class CreateHabitHandler(ApplicationDbContext dbContext, IValidator<CreateHabit> validator)
    : IRequestHandler<CreateHabit, HabitDto>
{
    public async Task<HabitDto> Handle(CreateHabit request, CancellationToken cancellationToken)
    {
        await validator.ValidateAndThrowAsync(createHabitDto);

        Habit habit = createHabitDto.ToEntity();

        dbContext.Habits.Add(habit);

        await dbContext.SaveChangesAsync(cancellationToken);

        return habit.ToDto();
    }
}

带有 MediatR 的CQRS具有以下几个优点:

  • 命令和查询的一致处理
  • 用于日志记录、验证和错误处理的管道行为
  • 通过处理程序类明确分离关注点
  • 通过处理程序隔离简化测试

然而,这种便利是以额外的抽象和复杂性为代价的。我们必须定义请求/响应类和处理程序,编写发送请求的代码等等。对于简单的应用程序来说,这可能有点过头了。
问题不在于这种权衡是否普遍好或坏,而在于它是否适合您的具体情况。

无需 MediatR 的 CQRS
无需 MediatR 即可轻松实现 CQRS。下面是一个简单的示例。

您可以将命令和查询定义为简单的接口:

public interface ICommandHandler<in TCommand, TResult>
{
    Task<TResult> Handle(TCommand command, CancellationToken cancellationToken = default);
}

// Same thing for IQueryHandler

然后,您可以实现处理程序并使用依赖注入注册它们:

public record CreateOrderCommand(string CustomerId, List<OrderItem> Items)
    : ICommand<CreateOrderResult>;

public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand, CreateOrderResult>
{
    public async Task<CreateOrderResult> Handle(
        CreateOrderCommand command,
        CancellationToken cancellationToken = default)
    {
        // implementation
    }
}

// DI registration...
builder.Services
    .AddScoped<ICommandHandler<CreateOrderCommand, CreateOrderResult>, CreateOrderCommandHandler>();

最后,你可以在控制器中使用该处理程序:

[ApiController]
[Route("orders")]
public class OrdersController : ControllerBase
{
    [HttpPost]
    public async Task<ActionResult<CreateOrderResult>> CreateOrder(
        CreateOrderCommand command,
        ICommandHandler<CreateOrderCommand, CreateOrderResult> handler)
    {
        var result = await handler.Handle(command);

        return Ok(result);
    }
}

这与 MediatR 方法有什么区别?
这种方法提供了相同的关注点分离,但没有间接性。它直接、明确,并且通常足以满足许多应用程序的需求。
但是,它缺少 MediatR 提供的一些便利功能,例如管道行为和自动注册处理程序。您还需要将特定的处理程序注入控制器,这对于较大的应用程序来说可能很麻烦。

总结
CQRS 和 MediatR 是解决不同问题的不同工具。虽然它们可以很好地协同工作,但将它们视为不可分割的对两者都不利。CQRS 分离读取和写入关注点,而 MediatR 通过中介器解耦组件。

关键在于了解每种模式提供的功能,并根据您的具体情况做出明智的决定。有时,您会同时需要这两种模式,有时只需要其中一种,有时两种模式都不需要。这就是深思熟虑的架构的本质:根据您的特定需求选择合适的工具。

如果您想了解有关如何有效地将 CQRS 作为干净、可维护架构的一部分实施的更多信息,请查看实用的清洁架构。您将学习如何在实际场景中应用这些模式,在构建可扩展应用程序时避免常见的陷阱和过度设计。