首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >告别MediatR:构建极简CQRS架构的终极指南

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

作者头像
郑子铭
发布2025-07-24 10:16:41
发布2025-07-24 10:16:41
2470
举报

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

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

摒弃MediatR,您将获得:

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

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

我们将涵盖:

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

命令、查询与处理器

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

代码语言:javascript
复制
// ICommand.cs
public interface ICommand;
public interface ICommand<TResponse>;

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

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

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

代码语言:javascript
复制
// ICommandHandler.cs
publicinterfaceICommandHandler<inTCommand>
    whereTCommand : ICommand
{
    Task<Result> Handle(TCommand command, CancellationToken cancellationToken);
}

publicinterfaceICommandHandler<inTCommand, TResponse>
    whereTCommand : ICommand<TResponse>
{
    Task<Result<TResponse>> Handle(TCommand command, CancellationToken cancellationToken);
}

// IQueryHandler.cs
publicinterfaceIQueryHandler<inTQuery, TResponse>
    whereTQuery : IQuery<TResponse>
{
    Task<Result<TResponse>> Handle(TQuery query, CancellationToken cancellationToken);
}

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

实战示例:命令处理器

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

代码语言:javascript
复制
// 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 isnull)
        {
            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();
    }
}

关键点:

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

装饰器模式

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

代码语言:javascript
复制
// 日志装饰器示例
internal sealed class LoggingCommandHandler<TCommand, TResponse>(
    ICommandHandler<TCommand, TResponse> innerHandler,
    ILogger<CommandHandler<TCommand, TResponse>> logger)
    : ICommandHandler<TCommand, TResponse>
    where TCommand : ICommand<TResponse>
{
    publicasync 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>
{
    publicasync Task<Result<TResponse>> Handle(TCommand command, CancellationToken cancellationToken)
    {
        ValidationFailure[] validationFailures = await ValidateAsync(command, validators);
        if (validationFailures.Length == )
        {
            returnawait innerHandler.Handle(command, cancellationToken);
        }
        return Result.Failure<TResponse>(CreateValidationError(validationFailures));
    }
    // 验证方法实现...
}

DI配置

使用Scrutor进行注册:

代码语言:javascript
复制
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集成

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

代码语言:javascript
复制
internal sealedclassComplete : 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配置,您就能构建灵活的命令查询处理管道。这种方案易于理解、测试和扩展。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-07-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 命令、查询与处理器
  • 实战示例:命令处理器
  • 装饰器模式
  • DI配置
  • Minimal API集成
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档