服务器架构

CQRS (ADR-0008)

我们的服务器架构使用命令和查询职责分离 (Command and Query Responsibility Segregation - CQRS) 的模式。

此模式的主要目标是分解专注于单个实体的大型服务(例如 CipherService ),并转向基于操作或任务的更小、可重用的类(例如 CreateCipher )。将来,这可能会带来其他好处,例如将命令排队执行,但目前的重点是拥有更小的、可重用的代码块。

命令与查询

命令是写入操作,例如 RotateOrganizationApiKeyCommand。他们永远不应该从数据库中读取数据。

查询是读取操作,例如 GetOrganizationApiKeyQuery。他们永远不应该写入数据到数据库。

数据库是我们处理的最常见的数据源,但其他数据源也是可能的。例如,查询还可以从远程服务器获取数据。

每个查询或命令应该有一个单一的职责。例如:删除用户、获取许可证文件、轮换 API 密钥。它们是围绕动词或动作(例如 RotateOrganizationApiKeyCommand )而不是域名或实体(例如 ApiKeyService )设计的。

编写命令或查询

一个简单的查询可能只是一个从数据库获取数据的存储库调用。(我们已经使用存储库,这不是我们在这里关心的)但是,更复杂的查询可能需要围绕存储库调用的附加逻辑,这将需要它们自己的类。命令总是需要自己的类。

类、接口和公共方法应以操作命名。例如:

namespace Bit.Core.OrganizationFeatures.OrganizationApiKeys;

public class RotateOrganizationApiKeyCommand : IRotateOrganizationApiKeyCommand
{
  public async Task<OrganizationApiKey> RotateApiKeyAsync(OrganizationApiKey organizationApiKey)
  {
    ...
  }
}

查询/命令应该只公开运行完整操作的公共方法。它不应该有公共助手方法。

目录结构和命名空间应按功能组织。接口应存储在单独的子文件夹中。例如:

  Core/
    └── OrganizationFeatures/
        └── OrganizationApiKeys/
            ├── Interfaces/
            │   └── IRotateOrganizationApiKeyCommand.cs
            └── RotateOrganizationApiKeyCommand.cs

维护命令/查询的卓越性

通过分离读写操作,CQRS 鼓励我们保持类之间的松散耦合。在我们的代码库中使用 CQRS 时,需要遵循两条黄金法则:

  • 命令永远不应该读取,查询永远不应该写入

  • 命令和查询不应该互相调用

这两者都会导致类之间的紧密耦合,减少代码重用的机会,并将命令/查询的区别混为一谈。

通常可以通过以下方式避免这些问题:

  • 编写命令,以便它们在参数中接收所需的所有数据,而不是自己获取数据

  • 按顺序调用查询和命令(一个接一个),沿调用链传递结果

例如,如果我们需要更新组织的 API 密钥,可能会倾向于使用 UpdateApiKeyCommand 来获取当前 API 密钥然后更新它。但是,我们可以将其分解为两个单独的查询/命令,分别调用:

var currentApiKey = await _getOrganizationApiKeyQuery.GetOrganizationApiKeyAsync(orgId);
await _rotateOrganizationApiKeyCommand.RotateApiKeyAsync(currentApiKey);

这也有单元测试的好处 - 您可以简单地使用 Autodata 属性提供不同的参数值,而不是在模拟查询结果时进行冗长的「编排」阶段。

在实际情况下,您的命令和查询应该获取并返回整个对象(例如 User )而不是单个属性(例如 userId)。

避免过多的可选参数

过多的可选参数很快就会变得难以使用。相反,请考虑使用方法重载来为命令或查询提供不同的入口点。

最后更新于