Stay Hungry,Stay Foolish!

Understanding CQRS in .NET

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!

posted @ 2026-08-25 22:57  lightsong  阅读(2)  评论(0)    收藏  举报
千山鸟飞绝,万径人踪灭