×

ASP.NET Core + CQRS:构建可扩展 Web API 就这么简单

独孤求败 独孤求败 发表于2026-04-16 09:26:08 浏览5 评论0

抢沙发发表评论

现代 Web 应用开发中,读写操作耦合易导致代码繁琐、维护困难。CQRS(命令查询职责分离)模式通过分离数据变更(命令)与读取(查询)操作,为构建清晰、可扩展的 ASP.NET Core Web API 提供了可行方案。

CQRS 模式核心概念解析

CQRS 核心是将系统操作划分为两个独立模型,各司其职、互不干扰。

命令(Command):负责“改变数据”(创建、更新、删除),通常返回操作结果或状态码,告知调用方执行情况。

查询(Query):负责“读取数据”(查询、筛选),仅获取并返回数据,无任何数据修改副作用。

通俗类比:银行“存款”是命令(改余额),“查余额”是查询(只读),CQRS 只是将这种天然分离通过架构固定下来。①

为何在 ASP.NET Core 中采用 CQRS

传统架构的局限性

传统架构中,控制器承载所有逻辑(参数接收、验证、业务执行、数据库操作),存在三大问题:单元测试困难(依赖多、模拟复杂);读写性能无法分别优化(缓存与一致性相互制约);团队协作低效(边界模糊、易冲突)。

CQRS 带来的架构收益

CQRS 与传统混合模式的核心区别如下:

维度
传统混合模式
CQRS 分离模式
关注点分离
读写耦合,代码混乱
读写独立,专注自身职责
可测试性
测试难度大,需模拟完整上下文
处理器可独立测试,依赖少
扩展能力
只能整体扩容,浪费资源
读写可独立伸缩,读多写少场景优势明显
技术选型
受单一模型约束,灵活性差
读写可选用不同存储(如读用Elasticsearch)
注意:CQRS 非万能。中大型应用、高并发或 DDD 项目引入收益明显;简单 CRUD 系统强行使用则属于过度设计,增加维护成本。②


基于 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 { getset; } = string.Empty;
    public decimal Price { getset; }
    public int Stock { getset; }
}

查询(根据ID查产品,返回DTO):

// Queries/GetProductQuery.cs
public class GetProductQuery : IRequest<ProductDto>
{
    public Guid Id { getset; }
}

视图模型(仅含前端所需字段):

// Shared/ProductDto.cs
public record ProductDto(Guid Id, string Name, decimal Price, int Stock);

建议:命令明确返回类型,查询结果用不可变 record 类型,避免意外修改。

步骤 4:实现命令处理器

命令处理器负责接收参数、执行业务逻辑、写入数据库:

// Handlers/CreateProductHandler.cs
publicclassCreateProductHandler : IRequestHandler<CreateProductCommandGuid>
{
    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<GetProductQueryProductDto>
{
    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<TRequestTResponse
    : IPipelineBehavior<TRequestTResponse>
    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 != 0thrownew 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<TRequestTResponse
    : IPipelineBehavior<TRequestTResponse>
    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 { getset; }
    public TimeSpan CacheDuration => TimeSpan.FromMinutes(5);
}

在 Program.cs 中注册缓存管道即可生效。

3. 事务边界管理(命令侧)

通过管道实现命令事务管理,保证数据一致性:

// Behaviors/TransactionBehavior.cs
publicclassTransactionBehavior<TRequestTResponse
    : IPipelineBehavior<TRequestTResponse>
    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 管道,集中实现验证逻辑,一次编写、多处复用。

架构决策参考矩阵

根据项目特征选择合适架构,参考如下:

项目特征
推荐架构
理由
初创产品/MVP
传统分层架构
快速迭代,避免过早优化
读多写少内容平台
CQRS + 查询缓存
独立优化查询性能,提升响应速度
金融/电商核心系统
CQRS + 事件溯源
保障一致性,支持审计追踪
微服务边界模块
CQRS + gRPC 通信
独立伸缩,提升服务间交互效率

总结

CQRS 核心价值是通过读写分离提升架构清晰度和扩展性,其落地关键的是选对场景、渐进实施。

核心要点:

  1. 工具链:以 MediatR 为核心,结合 FluentValidation 等组件,减少样板代码。

  2. 边界控制:控制器仅做协议转换,业务逻辑下沉,仓储层隔离数据访问。

  3. 演进思维:从逻辑分离起步,逐步过渡到物理分离,避免过早优化。

  4. 可观测性:添加日志和指标埋点,便于性能调优和问题排查。

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


群贤毕至

访客