告别MediatR:构建极简CQRS架构的终极指南

作者:微信公众号:【架构师老卢】
6-7 9:4
10

Jimmy Bogard近期宣布,MediatR将对达到一定规模的企业采用商业许可模式。这一变化促使许多团队重新评估其使用方案,并开始寻找替代方案。

现在正是转型的好时机。尽管CQRS与MediatR并非同一概念,但MediatR几乎已成为.NET生态中CQRS模式的代名词。大多数项目仅将其用作命令和查询的简单分发层——这种用例完全可以通过几个直白的抽象来实现。

摒弃MediatR,您将获得:

  • 对CQRS基础设施的完全掌控
  • 可预测的显式处理器分发机制
  • 更简单的调试和新人上手流程
  • 更清晰的DI配置和更好的可测试性

本文将带您构建一个极简CQRS架构,仅需少量接口即可支持装饰器模式。没有隐藏的DI魔法,只有清晰可预测的代码。

我们将涵盖:

  • 定义ICommand、IQuery及处理器契约
  • 添加装饰器支持(日志、验证等)
  • 通过DI完成注册
  • 实际场景的完整示例

命令、查询与处理器

首先定义命令和查询的基础契约:

// ICommand.cs
public interface ICommand;
public interface ICommand<TResponse>;

// IQuery.cs
public interface IQuery<TResponse>;

这些标记接口让我们能够围绕操作意图构建应用逻辑——写操作通过ICommand,读操作通过IQuery。

处理器接口遵循相同模式:

// ICommandHandler.cs
public interface ICommandHandler<in TCommand>
    where TCommand : ICommand
{
    Task<Result> Handle(TCommand command, CancellationToken cancellationToken);
}

public interface ICommandHandler<in TCommand, TResponse>
    where TCommand : ICommand<TResponse>
{
    Task<Result<TResponse>> Handle(TCommand command, CancellationToken cancellationToken);
}

// IQueryHandler.cs
public interface IQueryHandler<in TQuery, TResponse>
    where TQuery : IQuery<TResponse>
{
    Task<Result<TResponse>> Handle(TQuery query, CancellationToken cancellationToken);
}

这些接口与MediatR的IRequest和IRequestHandler API几乎相同,便于迁移。我们使用Result包装器处理返回类型(可选),这能促进显式的成功/失败处理。

实战示例:命令处理器

让我们通过实现"标记待办事项为完成"命令来演示这些抽象:

// CompleteTodoCommand.cs
public sealed record CompleteTodoCommand(Guid TodoItemId) : ICommand;

// CompleteTodoCommandHandler.cs
internal sealed class CompleteTodoCommandHandler(
    IApplicationDbContext context,
    IDateTimeProvider dateTimeProvider,
    IUserContext userContext)
    : ICommandHandler<CompleteTodoCommand>
{
    public async Task<Result> Handle(CompleteTodoCommand command, CancellationToken cancellationToken)
    {
        TodoItem? todoItem = await context.TodoItems
            .SingleOrDefaultAsync(
                t => t.Id == command.TodoItemId && t.UserId == userContext.UserId,
                cancellationToken);

        if (todoItem is null)
        {
            return Result.Failure(TodoItemErrors.NotFound(command.TodoItemId));
        }

        if (todoItem.IsCompleted)
        {
            return Result.Failure(TodoItemErrors.AlreadyCompleted(command.TodoItemId));
        }

        todoItem.IsCompleted = true;
        todoItem.CompletedAt = dateTimeProvider.UtcNow;

        todoItem.Raise(new TodoItemCompletedDomainEvent(todoItem.Id));

        await context.SaveChangesAsync(cancellationToken);

        return Result.Success();
    }
}

关键点:

  • 命令是不可变值对象(纯数据无行为)
  • 处理器封装所有业务逻辑
  • 直接通过自定义抽象调用处理器,无中介层

装饰器模式

通过装饰器模式实现横切关注点(如日志、验证):

// 日志装饰器示例
internal sealed class LoggingCommandHandler<TCommand, TResponse>(
    ICommandHandler<TCommand, TResponse> innerHandler,
    ILogger<CommandHandler<TCommand, TResponse>> logger)
    : ICommandHandler<TCommand, TResponse>
    where TCommand : ICommand<TResponse>
{
    public async Task<Result<TResponse>> Handle(TCommand command, CancellationToken cancellationToken)
    {
        string commandName = typeof(TCommand).Name;
        logger.LogInformation("Processing command {Command}", commandName);
        Result<TResponse> result = await innerHandler.Handle(command, cancellationToken);
        // 处理日志输出...
        return result;
    }
}

// 验证装饰器示例
internal sealed class ValidationCommandHandler<TCommand, TResponse>(
    ICommandHandler<TCommand, TResponse> innerHandler,
    IEnumerable<IValidator<TCommand>> validators)
    : ICommandHandler<TCommand, TResponse>
    where TCommand : ICommand<TResponse>
{
    public async Task<Result<TResponse>> Handle(TCommand command, CancellationToken cancellationToken)
    {
        ValidationFailure[] validationFailures = await ValidateAsync(command, validators);
        if (validationFailures.Length == 0)
        {
            return await innerHandler.Handle(command, cancellationToken);
        }
        return Result.Failure<TResponse>(CreateValidationError(validationFailures));
    }
    // 验证方法实现...
}

DI配置

使用Scrutor进行注册:

services.Scan(scan => scan.FromAssembliesOf(typeof(DependencyInjection))
    .AddClasses(classes => classes.AssignableTo(typeof(IQueryHandler<,>)), publicOnly: false)
        .AsImplementedInterfaces()
        .WithScopedLifetime()
    // 其他处理器注册...

// 装饰器配置
services.Decorate(typeof(ICommandHandler<,>), typeof(ValidationDecorator.CommandHandler<,>));
services.Decorate(typeof(IQueryHandler<,>), typeof(LoggingDecorator.QueryHandler<,>));
// 其他装饰器...

Minimal API集成

在端点中直接使用处理器:

internal sealed class Complete : IEndpoint
{
    public void MapEndpoint(IEndpointRouteBuilder app)
    {
        app.MapPut("todos/{id:guid}/complete", async (
            Guid id,
            ICommandHandler<CompleteTodoCommand> handler,
            CancellationToken cancellationToken) =>
        {
            var command = new CompleteTodoCommand(id);
            Result result = await handler.Handle(command, cancellationToken);
            return result.Match(Results.NoContent, CustomResults.Problem);
        })
        .WithTags(Tags.Todos)
        .RequireAuthorization();
    }
}

CQRS不需要复杂框架。通过少量接口、装饰器类和清晰的DI配置,您就能构建灵活的命令查询处理管道。这种方案易于理解、测试和扩展。

相关留言评论
昵称:
邮箱:
阅读排行