现代 Web 应用开发中,读写操作耦合易导致代码繁琐、维护困难。CQRS(命令查询职责分离)模式通过分离数据变更(命令)与读取(查询)操作,为构建清晰、可扩展的 ASP.NET Core Web API 提供了可行方案。
CQRS 模式核心概念解析
CQRS 核心是将系统操作划分为两个独立模型,各司其职、互不干扰。
命令(Command):负责“改变数据”(创建、更新、删除),通常返回操作结果或状态码,告知调用方执行情况。
查询(Query):负责“读取数据”(查询、筛选),仅获取并返回数据,无任何数据修改副作用。
通俗类比:银行“存款”是命令(改余额),“查余额”是查询(只读),CQRS 只是将这种天然分离通过架构固定下来。①
为何在 ASP.NET Core 中采用 CQRS
传统架构的局限性
传统架构中,控制器承载所有逻辑(参数接收、验证、业务执行、数据库操作),存在三大问题:单元测试困难(依赖多、模拟复杂);读写性能无法分别优化(缓存与一致性相互制约);团队协作低效(边界模糊、易冲突)。
CQRS 带来的架构收益
CQRS 与传统混合模式的核心区别如下:
基于 MediatR 的 CQRS 实现步骤
ASP.NET Core 中实现 CQRS 最简洁的方式是结合 MediatR,简化调度逻辑、减少样板代码,以下是完整实现步骤。
步骤 1:创建项目并安装依赖
创建 Web API 项目,安装 MediatR 依赖,终端执行命令:
dotnet new webapi -n CQRS.Demo
cd CQRS.Demo
dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
提示:.NET 8+ 版本仅需安装 MediatR 主包,已内置依赖注入集成扩展。③
步骤 2:配置 MediatR 服务容器
在 Program.cs 中配置 MediatR,自动扫描处理器并注册:
// Program.cs
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
该配置会扫描当前程序集,注册所有实现 IRequestHandler<TRequest, TResponse> 的处理器。
步骤 3:定义命令与查询契约
定义命令、查询及视图模型(DTO),明确参数与返回类型。
命令(创建产品,返回ID):
// Commands/CreateProductCommand.cs
public class CreateProductCommand : IRequest<Guid>
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Stock { get; set; }
}
查询(根据ID查产品,返回DTO):
// Queries/GetProductQuery.cs
public class GetProductQuery : IRequest<ProductDto>
{
public Guid Id { get; set; }
}
视图模型(仅含前端所需字段):
// Shared/ProductDto.cs
public record ProductDto(Guid Id, string Name, decimal Price, int Stock);
建议:命令明确返回类型,查询结果用不可变 record 类型,避免意外修改。
步骤 4:实现命令处理器
命令处理器负责接收参数、执行业务逻辑、写入数据库:
// Handlers/CreateProductHandler.cs
publicclassCreateProductHandler : IRequestHandler<CreateProductCommand, Guid>
{
privatereadonly IProductRepository _repository;
privatereadonly ILogger<CreateProductHandler> _logger;
public CreateProductHandler(IProductRepository repository, ILogger<CreateProductHandler> logger)
{
_repository = repository;
_logger = logger;
}
public async Task<Guid> Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
var product = new Product(request.Name, request.Price, request.Stock);
var id = await _repository.AddAsync(product, cancellationToken);
_logger.LogInformation("Product created with ID: {ProductId}", id);
return id;
}
}
IProductRepository 为写操作仓储接口,可通过 EF Core 等 ORM 实现。
步骤 5:实现查询处理器
查询处理器负责读取数据、转换为视图模型并返回:
// Handlers/GetProductHandler.cs
publicclassGetProductHandler : IRequestHandler<GetProductQuery, ProductDto>
{
privatereadonly IProductReadRepository _readRepository;
public GetProductHandler(IProductReadRepository readRepository)
{
_readRepository = readRepository;
}
public async Task<ProductDto> Handle(GetProductQuery request, CancellationToken cancellationToken)
{
var product = await _readRepository.GetByIdAsync(request.Id, cancellationToken);
if (product isnull)
thrownew NotFoundException($"Product {request.Id} not found");
returnnew ProductDto(product.Id, product.Name, product.Price, product.Stock);
}
}
关键:读写仓储物理分离(IProductReadRepository/ IProductRepository),可选用不同技术实现(如写用SQL Server,读用Elasticsearch)。④
步骤 6:控制器精简为协调层
控制器仅负责协调,不承担业务逻辑,代码简洁:
// Controllers/ProductsController.cs
[ApiController]
[Route("api/[controller]")]
publicclassProductsController : ControllerBase
{
privatereadonly IMediator _mediator;
public ProductsController(IMediator mediator) => _mediator = mediator;
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Create([FromBody] CreateProductCommand command, CancellationToken ct)
{
var id = await _mediator.Send(command, ct);
return CreatedAtAction(nameof(Get), new { id }, id);
}
[HttpGet("{id}")]
[ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
public async Task<IActionResult> Get(Guid id, CancellationToken ct)
{
var result = await _mediator.Send(new GetProductQuery { Id = id }, ct);
return Ok(result);
}
}
控制器核心职责:接收请求、构造命令/查询、委托 MediatR 处理、返回 HTTP 响应。
生产环境增强实践
基础实现需补充命令验证、查询缓存、事务管理,满足生产环境需求。
1. 命令验证管道(Pipeline Behavior)
通过 MediatR 管道实现全局验证,避免重复编码:
// Behaviors/ValidationBehavior.cs
publicclassValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
whereTRequest : IRequest<TResponse>
{
privatereadonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators) => _validators = validators;
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
if (!_validators.Any()) returnawait next();
var failures = _validators
.Select(v => v.Validate(new ValidationContext<TRequest>(request)))
.SelectMany(r => r.Errors)
.Where(f => f != null).ToList();
if (failures.Count != 0) thrownew ValidationException(failures);
returnawait next();
}
}
注册验证管道(Program.cs):
builder.Services.AddMediatR(cfg => {
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
});
需安装 FluentValidation 库,并为命令创建对应验证器。
2. 查询缓存策略
通过管道实现查询缓存,提升读多写少场景性能:
// Behaviors/CachingBehavior.cs
publicclassCachingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
whereTRequest : IRequest<TResponse>
{
privatereadonly IDistributedCache _cache;
privatereadonly ILogger<CachingBehavior<TRequest, TResponse>> _logger;
public CachingBehavior(IDistributedCache cache, ILogger<CachingBehavior<TRequest, TResponse>> logger)
{
_cache = cache;
_logger = logger;
}
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
if (request is not ICacheableQuery cacheable) returnawait next();
var cacheKey = $"query:{typeof(TRequest).Name}:{GenerateHash(request)}";
var cached = await _cache.GetAsync(cacheKey, cancellationToken);
if (cached?.Length > 0)
{
_logger.LogDebug("Cache hit for {RequestType}", typeof(TRequest).Name);
return JsonSerializer.Deserialize<TResponse>(cached)!;
}
var response = await next();
_ = Task.Run(async () =>
{
var data = JsonSerializer.SerializeToUtf8Bytes(response);
await _cache.SetAsync(cacheKey, data, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = cacheable.CacheDuration
}, cancellationToken);
}, cancellationToken);
return response;
}
private static string GenerateHash(object obj) =>
Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(obj))));
}
定义可缓存查询接口:
public interface ICacheableQuery
{
TimeSpan CacheDuration { get; }
}
查询类实现接口(设置缓存有效期):
public class GetProductQuery : IRequest<ProductDto>, ICacheableQuery
{
public Guid Id { get; set; }
public TimeSpan CacheDuration => TimeSpan.FromMinutes(5);
}
在 Program.cs 中注册缓存管道即可生效。
3. 事务边界管理(命令侧)
通过管道实现命令事务管理,保证数据一致性:
// Behaviors/TransactionBehavior.cs
publicclassTransactionBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
whereTRequest : IRequest<TResponse>
{
privatereadonly IDbContextFactory<AppDbContext> _dbContextFactory;
public TransactionBehavior(IDbContextFactory<AppDbContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
if (request is not ICommand) returnawait next();
var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
awaitusingvar transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
try
{
var response = await next();
await transaction.CommitAsync(cancellationToken);
return response;
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
}
}
定义命令标记接口:
public interface ICommand : IRequest { }
public interface ICommand<TResponse> : IRequest<TResponse> { }
命令类实现接口:
public class CreateProductCommand : ICommand<Guid>
{
// 命令参数...
}
注册事务管道,确保命令操作在事务中执行。
常见实践误区与规避方案
误区一:过度分离导致维护成本上升
问题:简单 CRUD 强行拆分读写模型,导致代码冗余、维护繁琐。
方案:渐进式落地,初期实现逻辑分离(共享实体和数据库,独立仓储接口),后续随业务复杂度升级为物理分离。
误区二:忽略最终一致性挑战
问题:读写库分离时,数据同步延迟导致查询到旧数据,影响体验。
方案:关键场景显式刷新缓存;非关键场景提示同步中;高级场景用事件总线实现事件驱动同步。
误区三:验证逻辑重复实现
问题:控制器与处理器重复实现验证,易遗漏修改。
方案:用 FluentValidation + MediatR 管道,集中实现验证逻辑,一次编写、多处复用。
架构决策参考矩阵
根据项目特征选择合适架构,参考如下:
总结
CQRS 核心价值是通过读写分离提升架构清晰度和扩展性,其落地关键的是选对场景、渐进实施。
核心要点:
工具链:以 MediatR 为核心,结合 FluentValidation 等组件,减少样板代码。
边界控制:控制器仅做协议转换,业务逻辑下沉,仓储层隔离数据访问。
演进思维:从逻辑分离起步,逐步过渡到物理分离,避免过早优化。
可观测性:添加日志和指标埋点,便于性能调优和问题排查。
CQRS 的最终目标是构建读写分离、选型灵活、协作高效的可演进架构,应对业务增长和高并发挑战。
① Microsoft Architecture Center - CQRS 模式详解:https://learn.microsoft.com/zh-cn/azure/architecture/patterns/cqrs ② Martin Fowler - CQRS 原始论文与适用性分析:https://martinfowler.com/bliki/CQRS.html
③ MediatR 官方文档 - .NET 8+ 集成指南:https://github.com/jbogard/MediatR/wiki
④ eShop Reference Application - CQRS 实战示例(微软官方):https://github.com/dotnet/eShop