Jimmy Bogard近期宣布,MediatR将对达到一定规模的企业采用商业许可模式。这一变化促使许多团队重新评估其使用方案,并开始寻找替代方案。
现在正是转型的好时机。尽管CQRS与MediatR并非同一概念,但MediatR几乎已成为.NET生态中CQRS模式的代名词。大多数项目仅将其用作命令和查询的简单分发层——这种用例完全可以通过几个直白的抽象来实现。
摒弃MediatR,您将获得:
本文将带您构建一个极简CQRS架构,仅需少量接口即可支持装饰器模式。没有隐藏的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));
}
// 验证方法实现...
}
使用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<,>));
// 其他装饰器...
在端点中直接使用处理器:
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配置,您就能构建灵活的命令查询处理管道。这种方案易于理解、测试和扩展。