# 服务器架构

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/server/)
{% endhint %}

## CQRS ([ADR-0008](https://contributing.bitwarden.com/architecture/adr/server-CQRS-pattern)) <a href="#cqrs-adr-0008" id="cqrs-adr-0008"></a>

我们的服务器架构使用命令和查询职责分离 ([Command and Query Responsibility Segregation - CQRS](https://learn.microsoft.com/zh-cn/azure/architecture/patterns/cqrs)) 的模式。

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

### 命令与查询 <a href="#commands-vs-queries" id="commands-vs-queries"></a>

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

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

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

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

### 编写命令或查询 <a href="#writing-commands-or-queries" id="writing-commands-or-queries"></a>

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

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

```javascript
namespace Bit.Core.OrganizationFeatures.OrganizationApiKeys;

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

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

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

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

### 维护命令/查询的卓越性 <a href="#maintaining-the-command-query-distinction" id="maintaining-the-command-query-distinction"></a>

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

* **命令永远不应该读取，查询永远不应该写入**
* **命令和查询不应该互相调用**

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

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

* 编写命令，以便它们在参数中接收所需的所有数据，而不是自己获取数据
* 按顺序调用查询和命令（一个接一个），沿调用链传递结果

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

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

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

### 避免 [Primitive Obsession](https://refactoring.guru/smells/primitive-obsession) <a href="#avoid-primitive-obsession" id="avoid-primitive-obsession"></a>

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

### 避免过多的可选参数 <a href="#avoid-excessive-optional-parameters" id="avoid-excessive-optional-parameters"></a>

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://dev.ppgg.in/architecture/server.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
