惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
Martin Fowler
Martin Fowler
Last Week in AI
Last Week in AI
罗磊的独立博客
阮一峰的网络日志
阮一峰的网络日志
博客园 - 【当耐特】
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 三生石上(FineUI控件)
S
SegmentFault 最新的问题
V
Visual Studio Blog
Hugging Face - Blog
Hugging Face - Blog
雷峰网
雷峰网
博客园_首页
人人都是产品经理
人人都是产品经理
量子位
美团技术团队
The Cloudflare Blog
小众软件
小众软件
WordPress大学
WordPress大学
有赞技术团队
有赞技术团队
M
MIT News - Artificial intelligence
Microsoft Security Blog
Microsoft Security Blog
D
DataBreaches.Net
博客园 - Franky

博客园 - lightsong

ML Serving/编排工具 Introducing Gemma 3 270M: The compact model for hyper-efficient AI Utopia -- 企业世界模型 trustgraph semantica semantica vs graphti Industrial-Strength Natural Language Processing seata reference with springboot and other valuable demo outbox pattern with springboot Saga pattern with springboot 基于 Sentence Transformers 的具体应用案例 Vault with Keycloak as workload IAM Ontology Reasoning System ADR Claude Code的hook The AI-Native SDLC playbook Introduction to Dapper Introduction to FluentValidation Introduction to AutoFixture Introduction to FluentAssertions Understanding Return Types: IEnumerable, IReadOnlyCollection, and List Introduction to Refit Introduction to Carter Introduction to Minimal APIs Introduction to MediaTr Building Resilient .NET Applications with Polly Understanding Event-Driven Architecture Comprehensive Guide to Domain-Driven Design (DDD) The Transactional Outbox Pattern A Complete Guide to Clean Architecture
Understanding CQRS in .NET
lightsong · 2026-08-25 · via 博客园 - lightsong

Understanding CQRS in .NET

https://jdaniel1987.github.io/CQRS#link-to-example-project

https://github.com/fanqingsong/GameShop.CQRS

这是一篇基于你提供的网页内容整理的深度技术博客文章。为了便于读者理解,我重新梳理了结构,并保留了原文中的架构图示和代码示例。


🚀 深入理解 .NET 中的 CQRS 架构模式

CQRS (Command Query Responsibility Segregation,命令查询职责分离) 是一种架构模式,它将应用程序中的“读取”(Query)和“写入”(Command)操作分离开来。

这种分离可以简化设计,提高性能和可扩展性,并有助于在复杂系统中进行数据管理。在这篇文章中,我们将深入探讨如何在 .NET 项目中实施 CQRS。

📌 核心概念:什么是 CQRS?

CQRS 模式将系统的逻辑拆分为两个截然不同的部分:

  • Commands (命令):负责修改应用程序的状态(如创建、更新或删除数据)。
  • Queries (查询):负责检索数据,且不修改任何状态。

这种分离允许我们独立优化每一项职责,这对于高性能应用或管理复杂数据的系统特别有用。

image

🏗️ 架构示意图

CQRS 的基本流向是将读写路径物理或逻辑上分开:

架构流向:
Client (客户端) ➡️ Write API (写API) ➡️ Write DB (写数据库)
⬇️ (同步 SYNC)
Read DB (读数据库) ⬅️ Read API (读API) ⬅️ Client (客户端)


🤔 何时使用 CQRS?

CQRS 在用户体验和性能至关重要的场景中非常有益。以下是实施 CQRS 的关键场景:

  1. 高用户流量:例如在线商店,顾客浏览产品的次数远多于购买次数。分离读写操作允许独立扩展,确保在高峰期浏览和搜索功能依然流畅。
  2. 复杂的业务逻辑:在工作流复杂的应用中,CQRS 将读取和执行命令分离,使系统更易于维护和演进。
  3. 提升用户体验:如果快速获取产品详情和库存水平是优先事项,CQRS 可以优化读取操作的速度,减少加载时间。
  4. 事件驱动架构:CQRS 支持事件驱动方法,允许命令触发领域事件进行通知或日志记录,而不影响核心逻辑。
  5. 微服务架构:在微服务设置中,CQRS 允许跨服务独立管理读写操作。
  6. 数据一致性需求:对于订单下达等需要数据一致性的场景,CQRS 确保写入的事务处理,同时保持读取的高效。
  7. image

💡 专家提示:
将项目拆分为独立的读写组件并非强制性的;你可以将它们组织在同一项目的不同文件夹中。但是,为了在 Kubernetes 上部署时利用可扩展性优势,我强烈建议将它们分开。
同样,拥有独立的读写数据库也不是绝对必须的。不过,针对读取优化的数据库(如只读副本)可以独立扩展以满足需求。


🌟 CQRS 的优势

  1. 可扩展性 (Scalability):你可以独立扩展查询和命令。例如,可以复制读数据库以更好地分担查询负载。
  2. 简化的查询 (Simplified queries):查询可以针对性能进行优化,而无需担心更新操作的影响。
  3. 灵活性 (Flexibility):允许对读取和写入使用不同的数据模型,从而提高效率。

🛠️ 如何在 CQRS 中分离读写操作

  1. 创建独立项目:建立单独的项目来处理读写操作。例如,一个专注于查询数据的 Read Project 和一个专注于处理命令的 Write Project
  2. 选择合适的存储方案
    • 读项目:可能受益于 NoSQL 数据库或内存缓存以实现快速访问。
    • 写项目:可能使用关系型数据库以确保数据一致性和完整性。
  3. 建立通信模式:写项目可以在数据更改时发布领域事件,允许读项目相应地更新其模型。
    • 注:如果你使用像 MongoDB 这样的可扩展数据库,可能不需要复杂的通信,因为它可以通过只读副本集(Replica Sets)直接服务查询。

MongoDB 副本集模式示例

架构流向:
Write API ➡️ MongoDB (Primary)
⬇️ (Replication)
MongoDB (Replica Set) ➡️ Read API
MongoDB (Replica Set) ➡️ Read API

image


💻 实战:如何在 .NET 中实现 CQRS

我们将结合 Clean Architecture (整洁架构) 来演示。以下是推荐的解决方案结构:

📂 解决方案结构 (Solution Tree)

Solution 'GameShop'
├── src
│   ├── Application
│   │   ├── GameShop.Application.Read    (读应用层)
│   │   └── GameShop.Application.Write   (写应用层)
│   ├── Domain                           (领域层 - 共享)
│   ├── Infrastructure
│   │   ├── GameShop.Infrastructure.Data
│   │   ├── GameShop.Infrastructure.Read (读基础设施)
│   │   └── GameShop.Infrastructure.Write(写基础设施)
│   └── Presentation
│       ├── GameShop.API.Read            (读接口)
│       └── GameShop.API.Write           (写接口)

1️⃣ 实现查询/读取操作 (Query/Read)

我们将定义一个 API 端点来检索名称包含指定字符串的所有游戏信息。

Presentation (表现层)

使用 Minimal APIs 和 MediatR。

public class GetGamesByNameConsolesModule : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapGet("api/GamesByName/{GameName}", async (IMediator mediator, string gameName) =>
        {
            // 1. 创建查询对象
            var query = new GetGamesByNameQuery(gameName);
            
            // 2. 通过 MediatR 发送查询
            var result = await mediator.Send(query);

            // 3. 返回结果
            return result.IsSuccess 
                ? Results.Ok(result.Value.ToGetGamesByNameResponse()) 
                : Results.BadRequest(result.Error);
        });
    }
}

Application (应用层)

核心逻辑位于 Handler 中。

public class GetGamesByNameHandler(IGameReadRepository gameReadRepository) 
    : IRequestHandler<GetGamesByNameQuery, IResult<GetGamesByNameQueryResponse>>
{
    private readonly IGameReadRepository _gameReadRepository = gameReadRepository;

    public async Task<IResult<GetGamesByNameQueryResponse>> Handle(GetGamesByNameQuery query, CancellationToken cancellationToken)
    {
        // 从基础设施层获取数据
        var games = await _gameReadRepository.GetGamesByName(query.GameName, cancellationToken);
        
        // 应用业务规则(如果有)...

        return Result.Success(games.ToGetGamesByNameQueryResponse());
    }
}

数据模型 (使用 Record 实现不可变性):

public record GetGamesByNameQuery(string GameName) : IRequest<IResult<GetGamesByNameQueryResponse>>;

public record GetGamesByNameQueryResponse(IReadOnlyCollection<GetGamesByNameQueryResponseItem> Games);

public record GetGamesByNameQueryResponseItem(
    int Id, string Name, string Publisher, double Price, int GameConsoleId, string GameConsoleName);

Infrastructure (基础设施层)

只读存储库实现。

public class GameReadRepository(IDbContextFactory<ReadOnlyDatabaseContext> readOnlyDatabaseContextFactory) 
    : IGameReadRepository
{
    private readonly IDbContextFactory<ReadOnlyDatabaseContext> _readOnlyDatabaseContextFactory = readOnlyDatabaseContextFactory;

    public async Task<IReadOnlyCollection<Game>> GetGamesByName(string gameName, CancellationToken cancellationToken)
    {
        var readOnlyDbContext = await _readOnlyDatabaseContextFactory.CreateDbContextAsync(cancellationToken);

        return await readOnlyDbContext
            .Games
            .Include(g => g.GameConsole)
            .Where(g => EF.Functions.Like(g.Name, $"%{gameName}%"))
            .ToArrayAsync(cancellationToken);
    }
}

2️⃣ 实现命令/写入操作 (Command/Write)

我们将定义一个 API 端点向系统添加新的游戏主机 (Game Console)。

Presentation (表现层)

public class AddGameConsoleModule : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapPost("api/AddGameConsole", async (IMediator mediator, AddGameConsoleRequest request, CancellationToken cancellationToken) =>
        {
            // 1. 转换为命令对象
            var command = request.ToCommand();
            
            // 2. 发送命令
            var result = await mediator.Send(command, cancellationToken);

            // 3. 返回结果
            return result.IsSuccess 
                ? Results.Created() 
                : Results.BadRequest(result.Error);
        });
    }
}

Application (应用层)

public class AddGameConsoleHandler(IGameConsoleWriteRepository gameConsoleWriteRepository) 
    : IRequestHandler<AddGameConsoleCommand, IResult<AddGameConsoleCommandResponse>>
{
    private readonly IGameConsoleWriteRepository _gameConsoleWriteRepository = gameConsoleWriteRepository;

    public async Task<IResult<AddGameConsoleCommandResponse>> Handle(AddGameConsoleCommand command, CancellationToken cancellationToken)
    {
        // 转换为领域对象
        var gameConsole = command.ToDomain();
        
        // 保存到基础设施层
        await _gameConsoleWriteRepository.AddGameConsole(gameConsole, cancellationToken);

        return Result.Success(gameConsole.ToAddGameConsoleCommandResponse());
    }
}

数据模型:

public record AddGameConsoleCommand(string Name, string Manufacturer, double Price) 
    : IRequest<IResult<AddGameConsoleCommandResponse>>;

public record AddGameConsoleCommandResponse(int Id, string Name);

Infrastructure (基础设施层)

写入存储库实现。

public class GameConsoleWriteRepository(IDbContextFactory<WriteReadDatabaseContext> writeReadDbContextFactory) 
    : IGameConsoleWriteRepository
{
    private readonly IDbContextFactory<WriteReadDatabaseContext> _writeReadDbContextFactory = writeReadDbContextFactory;

    public async Task AddGameConsole(GameConsole gameConsole, CancellationToken cancellationToken)
    {
        var writeReadDbContext = await _writeReadDbContextFactory.CreateDbContextAsync(cancellationToken);
        
        await writeReadDbContext.AddAsync(gameConsole, cancellationToken);
        await writeReadDbContext.SaveChangesAsync(cancellationToken);
    }
}

📌 总结

CQRS 是一个强大的模式,通过分离与读写数据相关的关注点来简化复杂系统。在 .NET 中采用此模式,可以显著提高应用程序的可扩展性、灵活性和可维护性。

  • 核心思想:读写分离。
  • 技术栈:.NET, MediatR, Clean Architecture。
  • 适用场景:高并发、复杂业务逻辑、微服务。

希望这篇指南能帮助你更好地理解并在项目中应用 CQRS!

出处:http://www.cnblogs.com/lightsong/ 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。