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 (查询):负责检索数据,且不修改任何状态。
这种分离允许我们独立优化每一项职责,这对于高性能应用或管理复杂数据的系统特别有用。

🏗️ 架构示意图
CQRS 的基本流向是将读写路径物理或逻辑上分开:
架构流向:
Client (客户端) ➡️ Write API (写API) ➡️ Write DB (写数据库)
⬇️ (同步 SYNC)
Read DB (读数据库) ⬅️ Read API (读API) ⬅️ Client (客户端)
🤔 何时使用 CQRS?
CQRS 在用户体验和性能至关重要的场景中非常有益。以下是实施 CQRS 的关键场景:
- 高用户流量:例如在线商店,顾客浏览产品的次数远多于购买次数。分离读写操作允许独立扩展,确保在高峰期浏览和搜索功能依然流畅。
- 复杂的业务逻辑:在工作流复杂的应用中,CQRS 将读取和执行命令分离,使系统更易于维护和演进。
- 提升用户体验:如果快速获取产品详情和库存水平是优先事项,CQRS 可以优化读取操作的速度,减少加载时间。
- 事件驱动架构:CQRS 支持事件驱动方法,允许命令触发领域事件进行通知或日志记录,而不影响核心逻辑。
- 微服务架构:在微服务设置中,CQRS 允许跨服务独立管理读写操作。
- 数据一致性需求:对于订单下达等需要数据一致性的场景,CQRS 确保写入的事务处理,同时保持读取的高效。
-

💡 专家提示:
将项目拆分为独立的读写组件并非强制性的;你可以将它们组织在同一项目的不同文件夹中。但是,为了在 Kubernetes 上部署时利用可扩展性优势,我强烈建议将它们分开。
同样,拥有独立的读写数据库也不是绝对必须的。不过,针对读取优化的数据库(如只读副本)可以独立扩展以满足需求。
🌟 CQRS 的优势
- 可扩展性 (Scalability):你可以独立扩展查询和命令。例如,可以复制读数据库以更好地分担查询负载。
- 简化的查询 (Simplified queries):查询可以针对性能进行优化,而无需担心更新操作的影响。
- 灵活性 (Flexibility):允许对读取和写入使用不同的数据模型,从而提高效率。
🛠️ 如何在 CQRS 中分离读写操作
- 创建独立项目:建立单独的项目来处理读写操作。例如,一个专注于查询数据的 Read Project 和一个专注于处理命令的 Write Project。
- 选择合适的存储方案:
- 读项目:可能受益于 NoSQL 数据库或内存缓存以实现快速访问。
- 写项目:可能使用关系型数据库以确保数据一致性和完整性。
- 建立通信模式:写项目可以在数据更改时发布领域事件,允许读项目相应地更新其模型。
- 注:如果你使用像 MongoDB 这样的可扩展数据库,可能不需要复杂的通信,因为它可以通过只读副本集(Replica Sets)直接服务查询。
MongoDB 副本集模式示例
架构流向:
Write API ➡️ MongoDB (Primary)
⬇️ (Replication)
MongoDB (Replica Set) ➡️ Read API
MongoDB (Replica Set) ➡️ Read API
💻 实战:如何在 .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!


浙公网安备 33010602011771号