> This is part 1 of 2 of the full documentation (pages 1–100 of 181).
> The content is paginated: fetch every part to see all of it.
> Next part: https://docs.zongsoft.com/llms-full.txt/1
> Page index: https://docs.zongsoft.com/llms.txt

# Zongsoft 开发框架

Zongsoft 开发框架、宿主程序与工具链的中文文档中心。

![Zongsoft 文档封面](https://847714710-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FdTV8LtunzSisrDXg3zNv%2Fuploads%2Fgit-blob-5a6f8c458b365c1c82a9ace6f706ae6fe84c17cb%2Fzongsoft-docs-cover.png?alt=media)

Zongsoft 是一组面向 .NET 的开源框架、宿主程序和开发工具，核心目标是帮助团队构建可插件化、可部署、可维护的业务应用。

主要内容包括：

{% columns %}
{% column %}

### 开发框架

提供核心抽象、插件框架、数据引擎、Web 基础、安全、诊断、消息、自动升级以及常见第三方服务适配。
{% endcolumn %}

{% column %}

### 宿主与工具

宿主程序负责承载插件式应用；工具链负责部署插件、制作安装包、发布升级包和辅助开发调试。
{% endcolumn %}
{% endcolumns %}

## 快速导航

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref">页面</th><th data-hidden data-card-cover data-type="image">封面</th></tr></thead><tbody><tr><td><strong>了解整体设计</strong></td><td>从框架、宿主和工具链的边界开始建立全局图景。</td><td><a href="/overview/what-is-zongsoft">什么是 Zongsoft</a></td><td><a href="https://847714710-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FdTV8LtunzSisrDXg3zNv%2Fuploads%2Fgit-blob-5a6f8c458b365c1c82a9ace6f706ae6fe84c17cb%2Fzongsoft-docs-cover.png?alt=media">zongsoft-docs-cover.png</a></td></tr><tr><td><strong>从本地环境开始</strong></td><td>准备 SDK、源码、目录和可选容器环境。</td><td><a href="/get-started/prerequisites">准备环境</a></td><td><a href="https://847714710-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FdTV8LtunzSisrDXg3zNv%2Fuploads%2Fgit-blob-10be71c1aee0af4b6e8f004be7e2c5ffeb32b5e9%2Fzongsoft-start-cover.png?alt=media">zongsoft-start-cover.png</a></td></tr><tr><td><strong>理解插件化应用</strong></td><td>理解插件树、构件、服务注册和宿主集成。</td><td><a href="/framework/plugins">插件框架</a></td><td><a href="https://847714710-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FdTV8LtunzSisrDXg3zNv%2Fuploads%2Fgit-blob-9196bec8b8df6c12712d5b0dd515deeb0d57f7cf%2Fzongsoft-plugins-cover.png?alt=media">zongsoft-plugins-cover.png</a></td></tr><tr><td><strong>学习数据访问</strong></td><td>用数据模式、映射文件和驱动完成对象图读写。</td><td><a href="/framework/data">数据引擎</a></td><td><a href="https://847714710-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FdTV8LtunzSisrDXg3zNv%2Fuploads%2Fgit-blob-4bba74b4b746d0421774fe57bfbad6ef4524fa51%2Fzongsoft-data-cover.png?alt=media">zongsoft-data-cover.png</a></td></tr></tbody></table>

## 阅读路径

如果你是第一次接触 Zongsoft，建议按下面顺序阅读：

1. 从 [什么是 Zongsoft](/overview/what-is-zongsoft) 了解整体边界。
2. 阅读 [插件化](/overview/pluginization)，理解业务能力为什么以插件方式组织。
3. 按 [准备环境](/get-started/prerequisites) 和 [安装包](/get-started/install) 完成本地准备。
4. 选择一个 [宿主程序](/get-started/hosting)，完成[最小部署](/get-started/deploy-first-plugin)和[首个业务插件](/get-started/first-business-plugin)。
5. 进入框架指南，按需要阅读[核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core)、插件框架、数据引擎、Web 基础等主题。

{% hint style="info" %}
本文档优先以“如何构建一个插件式应用”的路径组织内容，而不是按 NuGet 包逐个罗列。包名、源码路径和模块关系可以在 [包与模块索引](/references/packages) 中查阅。
{% endhint %}

## 按问题深入

* 需要确定模块边界、设计扩展契约或逐步改造已有系统：阅读[插件化设计专题](https://docs.zongsoft.com/pages/W18xChMa7LOngtHuNewi#插件化设计专题)。
* 不清楚宿主、模块、服务与提供者的关系：阅读[基础概念](/overview/concepts)和[服务定位](/framework/core/services/locating)。
* 需要业务数据与 HTTP 接口：从[首次查询](/framework/data/quickstart)到[数据服务](/framework/data/services)，再连接[Web 控制器](/framework/web/controllers)。
* 需要异步工作和外部基础设施：阅读[消息投递概念](/framework/messaging/concepts)、[任务调度](/framework/externals/execution)及[扩展索引](/framework/externals)。
* 需要模型调用、诊断或应用交付：进入[智能化](/framework/intelligences)、[诊断](/framework/diagnostics)和[升级流程](/framework/upgrading/workflow)。


# 什么是 Zongsoft

了解 Zongsoft 开源框架系列的组成、定位与适用场景。

Zongsoft 是面向 .NET 应用开发的一组开源框架、宿主程序与工具链。它的核心能力围绕插件化应用展开：宿主程序只负责建立运行环境，业务能力通过插件、配置、数据映射和附属资源部署到宿主中。

## 组成部分

### Framework

[`Zongsoft/framework`](https://github.com/Zongsoft/framework) 是框架代码仓库，包含[核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core)、插件框架、数据引擎、Web 基础库、安全、诊断、消息队列、自动升级和第三方服务适配。

### Hosting

[`Zongsoft/hosting`](https://github.com/Zongsoft/hosting) 提供终端、后台服务和 Web 三类宿主程序。宿主程序本身不承载业务逻辑，它们通过插件框架加载 `plugins/` 目录中的插件文件来组成应用。

### Tools

[`Zongsoft/tools`](https://github.com/Zongsoft/tools) 提供部署和打包工具，包括 `dotnet-deploy`、`dotnet-pack` 和正则表达式 GUI 工具。自动升级打包器 `dotnet-upgrade` 位于 framework 仓库的 `upgrading/tool` 目录。

## 适用场景

Zongsoft 更适合需要长期演进、模块边界清晰、部署形态多样的业务系统。例如：

* 多业务模块组合的后台服务。
* 需要按站点、环境、客户或部署方案组合功能的大型应用。
* 需要将通用能力沉淀为插件并跨项目复用的系统。
* 需要部署、安装包和自动升级流程配套的 .NET 应用。

## 与常见框架的关系

Zongsoft 与 [ABP](https://abp.io) 一样属于偏完整应用框架与工具链的体系，但它的组织重心更偏向“插件式应用运行时”。阅读本文档时，可以先把 Zongsoft 理解为：

1. 一套基础开发抽象。
2. 一套插件化应用模型。
3. 一套宿主程序模板。
4. 一套部署、打包和升级工具。


# 设计理念

理解 Zongsoft 对公共契约、插件装配、显式元数据和部署组合的设计取舍。

Zongsoft 围绕可插件化的业务应用组织能力。设计的重点是让业务意图、技术实现、运行宿主和交付方式能够分别演进，同时保留清楚的连接契约。

## 宿主与业务分离

宿主负责启动、配置、服务容器和生命周期。订单处理、消息订阅、报表输出等能力由插件提供。同一个不依赖 Web 请求的业务服务可以被终端命令、后台[工作器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)或 Web 控制器调用。

这种复用有前提：业务契约不能直接绑定到某个宿主的输入输出方式。终端的提示、HTTP 状态码和后台任务的重试策略应留在各自的适配层，而把业务规则放在共享服务中。参见[宿主概览](/hosting/hosting)。

## 面向契约选择实现

业务代码通常表达“我要一个缓存”或“我要发布消息”，实现选择由服务注册、提供者及配置完成。这样部署方案可以为不同环境选择不同能力，而不用在业务中散布第三方 SDK 的构造代码。

抽象也有边界。[核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core) 保留跨实现共同语义；数据库方言、消息协议、云平台参数留给相应驱动或适配器。一个选项出现在公共接口上，并不说明每个驱动都支持它。调用前应阅读对应实现的限制。

{% hint style="info" %}
💡 可替换性需要同时满足“接口兼容”和“业务语义兼容”。例如消息发布成功可能只表示已进入客户端队列，不能据此推断消费者已经完成业务处理。
{% endhint %}

## 用扩展点连接双方

插件树允许能力拥有者公开稳定的扩展路径，由其它插件贡献对象。提供命令执行器的插件不需要引用所有业务命令，业务命令只需要遵守命令契约和扩展路径约定。

服务容器解决对象依赖，插件树解决贡献与装配，两者各有用途。需要被业务按契约调用的对象适合注册为服务；需要加入某个驱动、命令或过滤器集合的对象适合贡献到扩展点。详见[构件与服务](/framework/plugins/builtins-and-services)。

## 显式元数据让结构可审查

`.plugin` 描述装配，`.option` 描述运行参数，`.mapping` 描述数据关系，`.deploy` 描述交付文件。把这些结构从业务调用中提取出来，有助于审查配置、复用方案和定位环境差异。

代价是元数据本身也必须维护。修改程序集名称时要检查清单，修改模型字段时要检查映射，修改部署产物时要检查包内路径。编译通过只覆盖其中一部分契约。

## 模块表达业务边界

模块拥有服务解析域、事件等应用组织能力，可以优先提供自己的服务，同时复用应用共享服务。模块并不自动隔离数据库、线程和进程权限；需要租户隔离、数据权限或进程隔离时，必须另行设计。

模块名、插件名、服务别名和连接名应有明确命名约定，但不必强制相同。它们承担的职责见[基础概念](/overview/concepts#module-service-provider)。

## 部署是应用组成的一部分

应用实际运行的是“宿主发布产物 + 选定插件 + 配置和资源”。维护可重复执行的部署清单，可以减少手工复制遗漏，也让开发、测试和发布使用同一套组合定义。

部署、安装打包和自动升级承担不同阶段的职责，详见[部署模型](/overview/deployment)。依赖版本、原生资源与配置覆盖应在交付阶段验证；运行环境发现缺失的插件时不会自动通过 NuGet 补齐。

## 实践中的取舍

先从一个可运行的最小宿主和一个业务插件开始，再按真实需求引入数据库、消息和外部服务。每增加一个实现，都验证清单加载、服务解析、配置生效和首次调用这条完整路径。这样出现问题时，可以沿着明确的边界定位，而不用把所有依赖一起启动后猜测原因。


# 架构总览

从依赖方向和运行流程理解 核心类库、插件、宿主与基础设施的职责。

理解 Zongsoft，可以先沿着一个业务请求观察：Web 宿主接收请求，业务插件处理用例，通过公共接口取得数据访问器、缓存或消息队列，具体插件再访问数据库和外部系统。宿主提供运行环境，业务表达意图，实现插件负责技术细节。

## 依赖方向

```mermaid
flowchart BT
	Business[业务插件] --> Core[核心类库 公共契约]
	Data[Data 与数据库驱动] --> Core
	Adapters[消息与第三方适配] --> Core
	Plugins[Plugins 装配运行时] --> Core
	Web[Plugins.Web] --> Plugins
	Web --> WebBase[Web 基础]
	Host[终端 / 后台 / Web 宿主] --> Plugins
```

图中箭头表示主要依赖方向，省略了部分项目引用。它与调用顺序不同：业务代码运行时可以调用 Redis 提供的缓存，而编译时只引用 [核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core) 的缓存契约。实现由[部署](/overview/deployment)和[服务解析](/framework/core/services)连接起来。

### 核心类库：稳定的共同语言

[核心类库](/framework/core)包含应用与模块、服务、数据访问、消息、文件系统、安全、配置等公共契约，也包含集合和转换等通用实现。业务依赖哪个接口，应由需要表达的能力决定，而不是由准备采用的数据库或 SDK 决定。

这不意味着所有能力都有完全相同的语义。例如多个消息插件实现同一接口，但消息确认、持久化和顺序性仍有差别，详见[消息基础概念](/framework/messaging/concepts)。

### Plugins：把声明变成运行对象

[插件框架](/framework/plugins)读取插件清单，解析依赖，组织[插件树](/overview/concepts#plugin-tree)，加载声明的程序集并注册服务。插件树上的构件可由构建器创建，也可以暴露已有实例。

文件目录组织部署文件和插件父子关系；`/Workbench/Modules` 等扩展路径组织运行时对象。两种层次分别服务于文件部署和对象装配，这是完全不同的组织结构。

### 功能库与适配器：实现可替换的能力

[数据引擎](/framework/data)负责映射、查询表达式和执行管线，数据库驱动负责方言与数据库客户端。[Web](/framework/web)、[安全](/framework/security)、[诊断](/framework/diagnostics)等库提供面向特定领域的实现。[外部扩展](/framework/externals)负责接入第三方基础设施。

可替换的前提是双方满足同一业务语义。换掉驱动名称后，仍需验证数据类型、查询功能、事务与部署依赖；换掉脚本引擎后也需要验证脚本语言。

## 从文件到应用

```mermaid
flowchart LR
	Build[编译引用] --> Publish[发布宿主]
	Manifest[.deploy 组合方案] --> Deploy[部署插件和资源]
	Publish --> Runtime[启动宿主]
	Deploy --> Runtime
	Runtime --> Load[.plugin 依赖与程序集]
	Load --> Config[.option 配置与服务注册]
	Config --> Workbench[应用上下文与工作台]
	Workbench --> Invoke[命令 / API / 后台工作器]
```

1. **编译**确认代码能调用目标 API；框架源码中的 Debug 引用还可能依赖本机 [核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core) 输出。
2. **部署**复制清单、程序集、选项、映射和附属资源，构成一个完整的运行目录。
3. **启动**建立内容根、配置与容器，加载插件并初始化应用。
4. **调用**才会触发部分延迟创建的服务、驱动或外部连接。

{% hint style="info" %}
💡 插件出现在列表中，只证明它已被发现或加载。验证一个功能还需要检查服务解析、有效配置和首次业务调用。
{% endhint %}

## 生命周期由谁管理

应用上下文连接宿主的启动和停止，[工作台](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins/src/IWorkbenchBase.cs)承载需要运行的组件。普通服务和可启动[工作器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)承担不同职责：服务提供操作；工作器负责持续运行、取消和收尾。构造函数应尽量只建立对象状态，避免把不可控的外部工作放进装配阶段。

模块服务容器提供名称域和回退规则，不是每个 HTTP 请求的作用域。共享服务不能保存某个请求独占的可变状态，调用方也不应随意释放从容器取得的共享实例。详见[模块、服务与提供者](/overview/concepts#module-service-provider)。

## 选择阅读路径

* 新建应用：从[准备环境](/get-started/prerequisites)到[部署第一个插件](/get-started/deploy-first-plugin)。
* 开发可组合业务：先看[基础概念](/overview/concepts)，再看[插件应用模型](/framework/plugins/application-model)。
* 接入数据库：先理解[对象关系与数据访问](/framework/data/concepts)，再完成映射、连接和查询。
* 运维交付：从[部署模型](/overview/deployment)进入[宿主部署](/hosting/deployment)与[工具](/tools/tools)。

源码入口：[核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src)、[应用构建器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins/src/Hosting/ApplicationBuilder.cs)、[Web 插件入口](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins.Web/src/Application.cs)。


# 基础概念与协作方式

区分宿主、插件、模块、服务、提供者、构件和四类元数据文件。

本页以“论坛模块提供主题查询接口”为例，说明框架中的几个相似名词。先分清这些概念，再读配置和代码，会更容易判断一个问题发生在哪个环节。

## 宿主与内容根 <a href="#host" id="host"></a>

**宿主**是启动应用的进程入口，例如终端、后台服务和 Web 应用。**内容根**是应用查找配置、插件等内容的基准目录。用绝对路径启动 DLL，并不会自动把当前工作目录切换到 DLL 所在目录。

因此，部署后应进入运行目录再启动，或通过宿主配置明确内容根。找不到 `plugins/` 时，先检查内容根，不要立即改业务程序集。

## 插件与程序集 <a href="#plugin" id="plugin"></a>

**程序集**包含可执行的 .NET 类型。**插件**由 `.plugin` 清单描述，声明依赖、程序集和扩展点；一个插件可以包含多个程序集，也可以只贡献装配配置。

项目引用让编译器看见类型；插件清单让运行时知道加载什么；部署文件让必要的文件出现在正确位置。这三个环节互不替代。比如 [Discussions.Web](https://github.com/Zongsoft/discussions/tree/main/src/api) 项目能编译，但漏部署 [Zongsoft.Discussions.Web.plugin](https://github.com/Zongsoft/discussions/blob/main/src/api/Zongsoft.Discussions.Web.plugin)，运行时仍不会按清单加载其控制器。

## 插件树与构件 <a href="#plugin-tree" id="plugin-tree"></a>

**插件树**是运行时扩展点的路径结构。一个插件可以向已有路径追加构件，不需要修改路径拥有者的源码。例如 Discussions 向数据验证器扩展点添加站点验证器，数据驱动向 `/Workbench/Data/Drivers` 添加驱动。

**构件**是插件树上的可构建定义。构建器决定如何创建对象，解析器解释属性中的表达式。下面的真实清单创建 Discussions 数据验证器，并将它挂到数据扩展点。

来源：[src/Zongsoft.Discussions.plugin](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.plugin#L31)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.plugin" %}

```xml
<extension path="/Workbench/Data/Validators">
	<object name="Discussions" type="Zongsoft.Discussions.Data.DataValidator, Zongsoft.Discussions" />
</extension>
```

{% endcode %}

构件名称是树节点名称，不自动成为 DI 中的服务别名。文件目录也不是插件树路径。详细语法见[构件与服务](/framework/plugins/builtins-and-services)。

## 模块、服务与提供者 <a href="#module-service-provider" id="module-service-provider"></a>

**模块**表达应用中的业务边界，例如 Discussions。一个模块可以由几个插件共同提供；插件加载也不会自动为每个插件创建业务模块。应用自行定义模块；需要将模块纳入[应用模块](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationModule.cs)集合时，可把模块对象挂载到 `/Workbench/Modules`。

> 💡 把模块挂载到 `/Workbench/Modules` 等同于加入到 [`ApplicationContext.Current.Modules`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/ApplicationContext.cs) 集合，虽然这不是必须的，但统一归拢的模块集有利于系统提供通用一致的模块化能力输出。譬如 [Zongsoft.Web.OpenApi](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Web/openapi) 就会使用[应用模块](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationModule.cs)集进行 API 文档的模块化分类。

**服务**是通过契约提供某种操作的对象。**服务容器**负责按类型、别名或匹配参数解析这些对象。模块容器优先查找模块服务，再回退应用共享服务；其生命周期不等于 Web 请求生命周期。

**提供者**解决另一类问题：某一实现需要按名称供应多个实例。例如 Redis 提供者可以按连接名返回不同缓存。此时先选提供者，再取具名实例。

| 名称     | 示例                         | 决定什么         |
| ------ | -------------------------- | ------------ |
| 插件名    | `Zongsoft.Externals.Redis` | 清单依赖指向哪个插件   |
| 模块名    | `Discussions`              | 模块服务解析域      |
| 提供者名   | `Redis`                    | 由哪种实现创建或查找实例 |
| 连接/实例名 | 由实际环境选项定义                  | 采用哪一组连接参数    |
| 构件名    | `Discussions` 验证器节点        | 插件树中对象的位置    |

{% hint style="warning" %}
🚨 服务定位表达式中的提供者与插件表达式指定的模块容器不是同一概念，名称不能互换。完整规则见[服务定位与所有权](/framework/core/services/locating)。
{% endhint %}

## 配置、映射与部署 <a href="#metadata" id="metadata"></a>

| 文件         | 回答的问题            | 读取者        |
| ---------- | ---------------- | ---------- |
| `.plugin`  | 加载哪些程序集？向哪里贡献对象？ | 插件框架       |
| `.option`  | 采用什么连接、参数和运行选项？  | 配置提供程序     |
| `.mapping` | 业务实体如何对应数据源？     | 数据引擎元数据加载器 |
| `.deploy`  | 哪些文件或包复制到哪里？     | 部署工具       |

Discussions 插件的清单可以保持稳定，测试和生产环境使用不同连接配置。部署方案决定是否包含 Redis 和 MySQL，运行配置决定使用哪个已部署实现及连接。配置中的名称必须与注册、连接项和业务调用形成一致关系。

`.option` 并非任意放进目录就会加载。插件配置与已加载清单的文件主名关联，详见[选项配置文件](/references/option-files)。

## 工作器、初始化器与普通服务 <a href="#lifetime" id="lifetime"></a>

[初始化器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationInitializer.cs)用于应用初始化阶段的配置或装配；[工作器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)用于需要启动、持续运行并在关闭时停止的任务；普通服务用于按调用执行某项操作。将“订阅消息并保持监听”写进构造函数，会让对象装配、外部连接和故障处理纠缠在一起，通常应交由工作器管理。

通过服务特性扫描注册的普通类型默认是单例。共享实例的创建与释放由注册和提供者约定决定。需要每次调用独立状态时，应明确作用域或使用工厂，不要从“具名”“模块化”推导出“每次新建”。

## 插件化的适用边界

插件化适合按业务能力拆分、按环境组合基础设施、让多个宿主复用业务。它也要求团队维护公共契约、清单、配置及部署兼容性；一个非常小的独立程序未必需要全部机制。

{% hint style="warning" %}
🚨 插件在宿主进程中以宿主权限执行。模块边界、插件目录和服务名称都不构成安全沙箱；配置变更通知也不代表程序集能够安全热替换。更换 DLL 后应按应用的停止、部署、启动流程验证。
{% endhint %}

继续阅读：[设计理念](/overview/conception)、[插件应用模型](/framework/plugins/application-model)、[术语表](/references/glossary)。


# 插件化

Zongsoft 插件化应用的基本概念和运行方式。

插件化是 Zongsoft 的核心特征。一个插件式应用由宿主程序和多个插件共同组成，宿主负责启动和承载，插件负责提供业务能力与扩展点。

## 插件是什么

在 Zongsoft 中，插件不是单个程序集的别名，而是一组文件和元数据的组合。最常见的插件内容包括：

* 插件描述文件 `*.plugin`。
* 插件程序集 `*.dll`。
* 选项配置 `*.option`。
* 数据映射 `*.mapping`。
* 本地化资源目录，如 `zh-Hans`、`zh-CN`。
* 证书、模板、静态资源等附属文件。

## 插件如何加载

宿主程序启动后，插件框架会扫描宿主目录下的 `plugins/` 目录，读取插件描述文件，加载程序集并建立应用上下文。插件可以注册服务、命令、事件处理器、Web 控制器或其它模块能力。

## 为什么使用插件化

插件化解决的是大型业务系统的组织问题：

* 业务模块可以独立发布。
* 运行宿主可以保持轻量和稳定。
* 不同环境可以通过部署文件组合不同插件。
* 不依赖特定交互或协议的业务插件可以在不同宿主形态中复用；控制器及 ASP.NET 中间件等则必须附属 Web 宿主。
* 配置、映射和资源可以随插件一起部署。

## 插件化设计专题

以下文章结合论坛模块的实际实现，讨论设计选择及其代价。建议按顺序阅读，也可以从正在面对的问题进入。配图中的“设计示意”表示待设计的能力或方案，具体实现状态以正文说明为准。

1. [为什么需要插件化](/overview/pluginization/why-pluginization)：巨石应用的痛点，分层、切块与按业务组织目录带来的变化。
2. [从业务变化确定模块边界](/overview/pluginization/business-boundaries)：以审核、正文和统计的共同变化判断业务归属。
3. [让业务能力跨宿主复用](/overview/pluginization/host-independent-business)：明确业务动作、入口适配、身份和生命周期的分工。
4. [把扩展点设计成协作契约](/overview/pluginization/extension-contracts)：设计扩展路径、服务调用、命名和失败语义。
5. [从局部改造到可验证的插件交付](/overview/pluginization/evolutionary-delivery)：验证发布组合，并处理数据兼容与进程拆分的取舍。

## 文档阅读建议

先阅读 [插件框架](/framework/plugins) 理解应用模型，再阅读 [部署第一个插件](/get-started/deploy-first-plugin) 了解插件如何进入宿主。需要深入数据访问时，再进入 [数据引擎](/framework/data)。

## 扩展点也是契约

一个插件提供服务或集合挂载点，另一个插件声明依赖并贡献实现。依赖不仅决定程序集可见性，也说明“需要先有哪些组件才能工作”。扩展路径、服务名和配置键应像公开 API 一样维护，变更时同步消费者。

插件化不等于任意热替换，也不等于进程级隔离。加载到同一宿主的组件共享进程资源，版本冲突或错误[工作器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)可能影响整个应用。需要独立故障边界时，应考虑独立进程和通信协议。

背景术语见[基础概念](/overview/concepts)，完整组合路径见[首个业务插件](/get-started/first-business-plugin)。


# 为什么需要插件化

从复杂 SaaS 的支付、交易与设备协作出发，理解巨石应用的维护困境，以及分层、业务分块和插件装配带来的变化。

消费者在机器前扫码、付款、取走商品，看起来只做了一件事：买东西。系统却要接连回答许多问题：订单属于哪个商家？用什么支付方式？外部平台的通知是否可信？钱已经付了，设备却没有应答，能不能重发出货指令？只出了一部分商品，该退多少钱？人工处理后，迟到的设备消息还能不能继续改变订单？

这还只是 **Automao** 的贩售场景。作为可以公有部署、也可以私有部署的商业 SaaS 系统，它还包含交易、支付、会员、物联、工单、营销、报表、统计、调查等业务模块；既有微信、支付宝等外部支付平台接入，也有现金、储值卡等支付方式；不同入口又面对平台管理员、商家员工、消费者或设备。

业务复杂并不可怕。可怕的是，**每增加一种支付方式、一个产品变体或一类入口，都需要重新理解半个系统**。插件化要解决的，就是怎样让这些变化各有归属，并让已经实现的能力能够重新组合。

{% hint style="info" %}
本文结合 Automao 的模块目录、插件清单、处理器和业务流程资料提炼架构案例。Automao 是闭源商业系统，本文不提供它的外部链接、源码摘录或生产配置；文中的假设需求和组合示意，也不代表已经交付的产品套餐。
{% endhint %}

## 如果它是巨石系统：一场维护梦魇

假设我们把上述能力放进一个缺少业务边界的应用：所有模型归 `Models/`，所有服务归 `Services/`，所有控制器归 `Controllers/`；一套公共工具类谁都能调用，一张业务表谁都能修改。目录按文件职能摆得很整齐，却没有回答最关键的问题：**这条规则到底归谁负责？**

最初这通常很方便。支付成功就在回调里更新订单，顺手发出设备指令；需要积分，再补一次会员调用。每一笔小改动都能交差，但几年后，支付回调就成了交易、设备、会员和营销共同依赖的“总开关”。

### 改一个地方，要检查半个系统

现在要接入新的支付通道。开发者找到一段“更新支付状态”的代码，却不敢直接复用：它是否还会修改订单？是否假设某个支付平台的凭证格式？是否会触发出货？一次重复通知，会不会重复发货？

真正昂贵的不是多写几行适配代码，而是每次都要查清这些隐藏的前提。测试范围也跟着扩大：改的是通道，回归的却可能是整个购买流程。

### 想做局部重构，却找不到能取出来的一块

支付实现老了，想换掉；但交易服务直接使用它的内部类型，报表直接读取它的中间状态，设备回调又绕过服务更新它的数据。此时所谓“只重构支付”，实际上要求所有使用者同时配合。

业务不能停下来等一次大重写，开发者便继续往旧结构里补分支。代码越难改，越不敢改；越不敢改，下一次改动需要理解的历史就越多。维护梦魇往往是这样形成的。

### 客户只要一部分功能，却带走了整个系统

假设一位私有部署客户只需要基础交易和指定支付方式，不需要营销，也没有设备接入。如果购买流程写死了对这些能力的调用，“不显示菜单”就不等于“可以不部署”。删掉一个包，启动可能失败；保留所有包，又必须维护一批客户根本不用的配置和依赖。

再加上商家端与客户端的权限差异、产品定制和外部平台差异，散落的条件判断会越来越难盘点。复制一套代码可以暂时交付，却把下一次公共缺陷修复变成了多个客户版本的同步工作。

![同一个应用中，创建订单、确认支付、发送指令等功能从散落状态归入交易、支付、物联三个业务模块。](https://847714710-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FdTV8LtunzSisrDXg3zNv%2Fuploads%2Fgit-blob-ab37c0b912f0d122cf512d12469c7131289332c2%2Fwhy-pluginization-boundaries.svg?alt=media)

*图 1：上方功能散落，下方把同样的功能按业务归入交易、支付、物联模块。功能没有减少，应用也没有被拆成多个进程，变化的是规则的归属和协作边界。*

{% hint style="info" %}
本文所说的“巨石”，指边界模糊、变化相互牵连的组织方式。**单体描述部署形态，模块化描述内部结构**：单体也可以有良好的模块边界，多个服务也可能高度耦合。`Models/` 等目录本身没有错，问题在于它们是否成了整个应用唯一的组织依据。
{% endhint %}

## 先横切一刀：分层而治

横向分层与纵向划分业务模块，是两种互补的拆分方式。在服务端，可以先按技术职责辨认三个层次：入口适配、业务规则、数据访问与资源。

入口负责理解外界的请求，业务负责决定允许发生什么，数据访问负责保存和读取结果。例如，“收到支付平台的通知”与“这笔支付是否可以从处理中变为成功”是两个问题，不能因为它们先后执行，就把它们写成无法分开的逻辑。

Automao 的支付接入能看到这样的分工：外部平台接入与支付提供者处理平台差异，把结果交给支付模块；支付模块声明支付、退款事件；交易后台插件把自己的处理器注册到支付成功事件上，负责后续订单处理。订单如何推进，便不必由每个支付通道各写一遍。

这里要保护的是**不同原因引起的变化**。支付平台调整通知格式，通常应落在平台适配处；订单结算规则调整，应落在交易服务或处理器里。业务规则也会频繁变化，分层并不是假设“业务永远稳定”，而是让协议变化不必顺带重写业务。

![宿主提供进程、配置与生命周期，承载交易、支付、物联模块；每个模块内部依次为入口适配、业务规则和数据与资源。](https://847714710-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FdTV8LtunzSisrDXg3zNv%2Fuploads%2Fgit-blob-8ebc59e58fa5f8558116d77fe3c06320e3baae40%2Fwhy-pluginization-layers.svg?alt=media)

*图 2：宿主承载交易、支付、物联三个模块，每个模块保留入口适配、业务规则、数据与资源三个层次。虚线表示宿主承载与装配，实线表示模块内部调用方向；不表示进程或数据库隔离。*

## 再纵切一刀：按业务模块切块

只分层还不够。所有订单、退款、会员和设备规则如果仍然挤在同一个业务库里，开发者虽然知道去“业务层”找代码，却依然很难把支付完整地拿出来维护。

纵切要回答的是：**围绕一件业务，哪些规则、数据和资源应当一起负责？** 支付模块维护支付与退款的业务含义，交易模块维护订单与履约的业务含义，物联模块维护设备接入与通信。模块内部继续分层，模块之间通过明确的服务契约、事件或扩展点协作。

业务模块应把一个业务领域的代码和资源聚合起来，保持职责的完整性。划分模块不能只看文件数量：映射、参数、接入代码都留在别处，单独拆出的几个业务类仍然很难独立维护。

### 从业务模块到插件装配

在 Zongsoft 中，插件机制进一步把业务分工落实为运行时装配：`.plugin` 声明依赖、程序集与扩展挂载；相关程序集提供实现；需要时，配套的 `.option`、`.mapping` 和资源一起交付。并非每个插件都需要全部这些文件，也不是每个插件都要有自己的程序集。

以 Automao 的支付模块为例，可以辨认出三组协作产物：

* **基础能力**：支付、退款等模型、服务基类、模块定义与事件，以及相关映射和选项。
* **站点实现**：管理端、商家端、客户端各自的服务与 Web 接入，承担对应入口的授权、校验和可写性要求。
* **支付提供者**：微信、支付宝、现金、储值卡等实现。微信提供者依赖支付基础能力和微信平台集成，面向网关的清单再挂载支付与退款回调处理器。

![交易、支付、物联模块分别归集自己的模型、服务、站点接入、插件清单、映射、选项和资源，以及相应扩展、提供者或协议。](https://847714710-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FdTV8LtunzSisrDXg3zNv%2Fuploads%2Fgit-blob-0bc1e7f9fe3761be4f6c7328854ec77ad971484d%2Fwhy-pluginization-anatomy.svg?alt=media)

*图 3：每个模块都归集完成本领域业务所需的实现与配套文件。中间的支付模块同时包含支付与退款模型、服务、事件、站点接入及支付提供者，对应上文三组协作产物。一个模块可以跨多个程序集和插件，图中的文件也不是每个插件都必须具备。*

因此，一个支付业务模块可以由基础插件、站点服务插件、Web 插件、提供者插件和后台插件共同组成。应用模块、程序集与插件的区别见[基础概念](/overview/concepts#module-service-provider)。

新增通道时，优先实现提供者并补齐配置、依赖和部署项；只有通用契约不足以表达新通道的业务语义时，才需要协调修改核心契约。裁剪通道也要确认没有其他能力依赖它，并处理好存量支付和退款，而不是简单删除一个文件。

### 分开以后，一笔交易怎样完成

回到“钱已付，货还没出”的场景。支付负责确认钱的状态；交易负责判断订单能否推进并组织履约；设备接入负责指令发送与设备上报；贩售产品的扩展负责把这些通用能力放进具体产品流程。源码中的支付事件、交易处理器及贩售与物联的扩展目录，提供了这些分工的实际落点。

它们没有把业务链条切断，而是让每一段都有负责人。设备指令需要重试时，讨论可以从设备通信与履约衔接处开始，而不必让微信、支付宝、储值卡各维护一套出货重试逻辑。若要为会员增加某种购买奖励，应另行约定消费哪个业务事件，不能把“付款成功”自动当成“交易最终完成”。

{% hint style="warning" %}
事件让双方不必直接依赖彼此的具体实现，但不会自动保证消息可靠、处理幂等或跨模块事务一致。重复通知、部分出货、退款补偿和人工介入仍然需要明确设计。如何交付事件见[可靠投递与消息存储](/framework/messaging/reliability)，如何处理数据一致性见[事务与一致性](/framework/data/transactions)。
{% endhint %}

## 复杂度来自维度叠加，插件化让维度可组合

同一个“支付”功能，会同时遇到几种不同的问题：它属于哪个业务模块？给谁使用？接什么通道？在哪个产品中运行？部署到哪里？如果把这些问题都塞进支付服务的一串条件分支，服务就会逐渐承担整个产品的组合工作。

Automao 的组织方式提供了另一种观察角度：

| 维度     | 已有组织或场景                                 | 主要回答的问题                |
| ------ | --------------------------------------- | ---------------------- |
| 业务     | 交易、支付、会员、物联、工单等模块                       | 谁拥有规则，谁负责数据和行为         |
| 领域与站点  | `domain/default` 下的站点实现，以及公共、产品目录中的站点实现 | 哪些规则可以共享，哪些授权与接入行为需要区分 |
| 提供者与协议 | 支付提供者、设备协议和外部平台集成                       | 如何适配外部系统的差异            |
| 产品     | 贩售产品的公共、支付、交易、物联及交叉扩展                   | 哪些通用能力需要组合，哪些流程归产品负责   |
| 部署     | 不同宿主、运行环境及公有或私有部署要求                     | 实际交付哪些产物，使用哪些配置和基础设施   |

这些维度可以分别组织，但**组合必须满足依赖关系**，并不是任意排列都能运行，也不意味着每个模块已经实现了所有领域和站点版本。

比如，一个贩售交付方案可能需要交易、支付、会员、物联和报表，再加入贩售自己的流程扩展。另一种私有部署方案，若业务允许，可以减少可选能力并选择指定支付提供者。相同业务规则继续复用，差异主要落在扩展、选项和交付清单中；客户定制也就不必一开始就复制整套业务代码。

![业务模块、六类站点与领域变体汇入部署清单，按产品和部署方式形成公有云 SaaS 或私有部署组合。](https://847714710-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FdTV8LtunzSisrDXg3zNv%2Fuploads%2Fgit-blob-ed29d5f0b2afae7322af615af555677096dccd1a%2Fwhy-pluginization-composition.svg?alt=media)

*图 4：业务模块、站点与领域共同作为组合输入，由部署清单组织为公有云或私有部署。站点包括管理、商家、客户、伙伴、网关与设备六类入口；教育、医疗等领域为候选示例，不代表每个模块都已有对应实现。实际组合必须满足依赖关系，租户授权另行设计。*

这里还要区分两张“清单”：**部署清单决定把什么放进运行目录，插件清单决定这些产物如何接入应用**。`.deploy` 不等于租户功能授权，`.plugin` 的依赖声明也不会自动下载包。具体机制见[部署工具](/tools/deployer)和[插件文件与加载](/framework/plugins/plugin-file)。

公有 SaaS 中“某租户可以使用哪些能力”，还需要租户配置与权限规则；私有部署也仍然需要合适的数据、基础设施和运维方案。插件化提供组合的基础，不替代这些设计。

积木组合能更直观地说明这种复用：方块、长条、三角与半圆各有自己的形状，却可以搭出不同的整体。业务模块也是如此：交易、支付、会员等基础能力可以参与不同产品，产品再补上自己的流程与扩展。**复用的是模块所提供的能力，产品的差异则由选择、组合和扩展来表达。**

![方块、长条、三角和半圆积木拼成 A、B、C、D 四种造型，说明基础部件的复用与整体组合的变化。](https://847714710-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FdTV8LtunzSisrDXg3zNv%2Fuploads%2Fgit-blob-f3e602ce9f90776b4f59282cf348f2652c1c0c44%2Fwhy-pluginization-building-blocks.svg?alt=media)

*图 5：相同类型的基础积木，形成不同的组合。这是组合复用的比喻，软件模块之间仍须遵守契约与依赖关系。*

## 把目录反过来：从职能分类到业务分类

当业务是第一层组织依据时，目录也应帮助开发者先找到业务，再找到实现职责。下面对照展示组织思路，省略无关文件；第二个示例按当前案例目录简化，不是完整部署模板。

{% code title="按文件职能组织的示意目录" %}

```
src/
	Models/       # 各种业务的模型集中在一起
	Services/     # 各种业务的服务集中在一起
	Controllers/  # 各种业务的控制器集中在一起
	Mappings/     # 各种业务的映射集中在一起
```

{% endcode %}

{% code title="按业务归属组织的示意目录" %}

```
modules/
	trading/
		abstraction/src/         # 模型、服务基类、事件、映射、基础插件
		domain/default/sites/    # 各站点的 src 服务与 api 接入
	paying/
		abstraction/src/
		domain/default/sites/
		providers/               # 微信、支付宝、现金、储值卡等实现
	things/
		abstraction/
		protocols/               # 设备协议
		providers/               # 物联提供者
products/
	vending/
		common/                  # 贩售产品公共能力
		paying/extensions/things/  # 产品中的支付与物联衔接
		trading/extensions/things/ # 产品中的交易与物联衔接
		things/                  # 贩售设备能力
hosting/                         # 承载、启动、配置与插件加载
```

{% endcode %}

这样，支付模型与支付映射虽然是不同类型的文件，却归同一项业务负责；设备协议变动有自己的落点；贩售特有的跨模块协作也有产品目录承接。`Models/`、`Services/` 等职能目录仍可保留在模块内部。

不过，移动目录只是把边界显现出来。**如果交易模块仍然随意修改支付内部状态，文件搬家并没有完成解耦。** 还需要用项目引用、公共契约、数据访问约定和评审把边界落实；也不要把所有跨模块逻辑重新堆进一个无限膨胀的“公共模块”。

## 插件化带来了哪些变化

衡量收益，最有用的办法是拿下一次业务变化来问：现在能否更快找到负责人，更准确地判断影响范围，更有把握地交付？

| 变化    | 缺少边界时              | 建立模块与装配边界后              |
| ----- | ------------------ | ----------------------- |
| 定位问题  | 支付异常要沿全局调用链查找      | 先区分通道、支付状态、交易衔接，再定位负责产物 |
| 渐进重构  | 实现细节四处被引用，替换要多人同时改 | 保持对外契约，先替换一个提供者或模块实现    |
| 产品组合  | 隐藏菜单后仍需带上整套依赖      | 按依赖选择能力，记录实际交付的版本与配置    |
| 跨入口复用 | 商家端、客户端各写一套公共规则    | 共享基础规则，在站点实现中保留权限与行为差异  |
| 交付验证  | 编译通过，却漏了映射、选项或处理器  | 把代码与配套元数据作为一个可验证的交付组合   |
| 团队协作  | 改动谁都沾边，验收没人全负责     | 模块有负责人，跨模块变化有明确的契约参与方   |

例如，在提供者契约不变时，可以先重构某个支付通道，再验证它与支付服务、网关和交易流程的衔接。范围仍然跨越必要的参与者，但不需要顺便重写会员或报表。这种**业务持续迭代时仍能逐块改进的能力**，比一开始就猜对所有需求更重要。

{% hint style="warning" %}
插件可以分别开发、打包，但不保证能无协调发布、独立回滚或不停机替换。同一宿主中的插件通常共享进程资源；程序集版本、公共契约、数据结构和运行状态都会限制升级方式。插件也不是安全沙箱，模块容器不提供租户隔离。交付边界见[从局部改造到可验证的插件交付](/overview/pluginization/evolutionary-delivery)。
{% endhint %}

## 代价：插件化不是免费的

插件化减少的是无关变化的互相干扰，同时增加了一份明确的装配与协作工作：

* **契约要维护**：服务接口、事件含义、扩展路径和配置键被使用之后，就需要考虑兼容与迁移。
* **元数据要同步**：代码、插件清单、选项、映射和部署项必须共同演进；清单存在不等于里面引用的文件仍然有效。
* **组合要验证**：基础模块测试通过，还要检查实际产品组合能否启动、发现服务并完成关键业务流程。
* **粒度要节制**：本来必须一起修改、一起保证一致性的规则，拆得过细只会增加协调成本。模块变多不应成为设计成绩。

如果应用规则少、改动集中、维护周期短，简单分层可能已经足够。当你反复遇到“一个能力要被多个入口复用”“一个客户只需要部分功能”“一个通道想单独演进”时，才有了引入相应插件边界的具体理由。

对 Automao 这样的复杂系统，插件化的价值不在于消除交易、支付和设备协作本身的复杂性，而在于让开发者能说清：**这次变化归谁，经过什么契约影响别人，需要带着哪些产物一起验证。** 从一条真正需要变化的业务链开始划界，比先把整个系统拆成几十个插件更可靠。

## 延伸阅读

* 如何找到合适的切分位置：[从业务变化确定模块边界](/overview/pluginization/business-boundaries)。
* 如何在不同入口复用能力：[让业务能力跨宿主复用](/overview/pluginization/host-independent-business)。
* 如何约定模块间协作：[把扩展点设计成协作契约](/overview/pluginization/extension-contracts)。
* 如何把组织方式落实到运行环境：[插件应用模型](/framework/plugins/application-model)、[插件文件与加载](/framework/plugins/plugin-file)。


# 从业务变化确定模块边界

先找业务变化，再划分模块：规则、装配、编译与交付各回答不同的问题。

论坛要做“主题审核”：一次需求改动往往要同时调整主题状态、正文可见性、作者统计和相关查询。如果这些代码已经按“模型库”“服务库”“控制器库”分放，改动仍然要跨好几个目录甚至好几个团队，那么文件分类并没有替业务找到一个清晰的负责人。

模块边界应当由业务变化决定，而不是由代码职能或目录位置决定。Zongsoft 的插件化提供了应用模块、程序集、插件清单和部署文件四层组织手段，但每层只回答一个问题。先弄清楚“哪次变化、哪些规则、一起验收”，再决定用什么手段承载它。

![Discussions 业务归属与待设计的扩展能力](https://847714710-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FdTV8LtunzSisrDXg3zNv%2Fuploads%2Fgit-blob-948afeabe163e53072aba688664d2676eb475363%2Fzongsoft-plugin-business-boundaries.png?alt=media)

*审核、正文与统计规则按业务归属组织，清单、映射、选项与部署文件等元数据随模块一起交付；搜索索引是需要另行约定的候选扩展。图示表达业务归属，不代表数据库或进程隔离。*

## 先跟着一次变化走

“批准主题”不是把某个布尔字段写成真。[ThreadService.Approve](https://github.com/Zongsoft/discussions/blob/main/src/Services/ThreadService.cs) 把主题编号、未审核状态和版主资格一起组成更新条件，同时更新主题对应正文的批准状态。审核规则、数据条件和权限判断写在同一个服务里，控制器只负责把结果映射成 HTTP 响应。

这说明边界可以从真实的业务动作里读出来：

* 哪些数据总是**一起变化**（主题与它的正文、统计字段）；
* 哪些规则必须由**同一处解释**（版主资格、可见性、审核状态）；
* 哪些失败必须**一起处理**（写正文失败时主题不能半提交）；
* 哪些能力要**一起发布、一起验收**（部署到宿主后一次调用就能走通）。

反过来，只看名字分组是靠不住的。两个类型都含“用户”，未必属于同一业务：登录身份、论坛作者统计和客户档案是三种不同的归属。边界是“变化与责任的边界”，不是“名字的边界”。

## 四层组织手段各回答一个问题

| 手段   | 回答的问题                       | 论坛中的落点                                                                                           |
| ---- | --------------------------- | ------------------------------------------------------------------------------------------------ |
| 应用模块 | 这套能力以什么业务身份存在，模块服务如何解析、事件归谁 | [Module](https://github.com/Zongsoft/discussions/blob/main/src/Module.cs) 定义 Discussions 模块及其访问器 |
| 程序集  | 哪些类型一起编译，允许引用哪些依赖           | 领域库与 [Discussions.Web](https://github.com/Zongsoft/discussions/tree/main/src/api) 分开编译           |
| 插件清单 | 运行时加载哪些程序集，把对象挂到哪些扩展点       | 领域清单声明模块与过滤器；Web 清单声明领域依赖并交付控制器程序集                                                               |
| 部署方案 | 交付哪些版本、配置、映射和资源             | 领域与 Web 分别用 `.deploy` 描述产物布局                                                                     |

应用模块和插件**不是一一对应**。模块是业务身份，插件是装配单位：论坛的领域清单把模块实例挂到 `/Workbench/Modules`，Web 清单依赖领域插件并把控制器随程序集交付。拆出一个 Web 程序集是为了约束协议依赖，不必因此再造一个“Web 业务模块”。名词关系见[基础概念](/overview/concepts#module-service-provider)与[插件应用模型](/framework/plugins/application-model)。

{% hint style="info" %}
💡 边界四层可以独立变化：可以先拆程序集、暂不拆部署；也可以先由两个插件共同装配一个模块。每层是否引入，取决于当时是否出现对应的维护或交付问题。
{% endhint %}

## 完整性：规则连同交付资源一起负责

一个业务模块要能独立工作，责任范围应覆盖实现规则所需的模型、服务、映射、选项和资源。新增一个字段如果只改 C# 类型，却漏掉[映射文件](https://github.com/Zongsoft/discussions/blob/main/src/Zongsoft.Discussions.mapping)与数据库脚本，编译成功的模块仍然无法运行。

完整性不等于把所有文件放进同一个程序集。论坛的[领域部署文件](https://github.com/Zongsoft/discussions/blob/main/src/Zongsoft.Discussions.deploy)交付清单、选项、映射与程序集；[Web 部署文件](https://github.com/Zongsoft/discussions/blob/main/src/api/Zongsoft.Discussions.Web.deploy)额外交付 Web 清单与表格模板。两份产物必须一起进运行目录，Discussions 模块才可用。因此“分文件发布”不等于“彼此无需兼容”，维护者必须知道两份产物如何组成一个可用功能。

边界内部仍可分层：业务服务负责规则，控制器负责 HTTP 适配，数据过滤负责结果加工。职责清楚后，一次审核规则调整才能主要落在服务及其验证上，而不是散落在每个控制器里。跨入口复用的问题见[让业务能力跨宿主复用](/overview/pluginization/host-independent-business)。

## 用“待拆分”检验候选边界

假设产品希望新增外部搜索索引。它有独立的查询技术和重建流程，表面上是独立插件的候选；但拆分之前要先回答：索引延迟是否可以接受、主题隐藏后多久不可被搜到、删除如何传播、索引故障时如何恢复。

如果产品要求内容隐藏立即生效，那么搜索结果就不能只依赖旧索引。要么返回结果前回查业务状态，要么设计满足时效的更新机制。此时可见性规则仍然归论坛，索引插件只承担搜索实现——“拆开了”不会自动替双方完成这个约定。

反过来，如果想把“批准主题”和“批准主题正文”拆成两个插件，却给不出各自独立的使用场景、失败处理和发布节奏，那么新增的协作成本通常大于收益。可以先保留类型内的分工，等真正需要独立组合时再建立装配边界。

{% hint style="warning" %}
🚨 模块服务容器提供的是名称解析域，不是安全边界。同进程内的代码仍可引用其他模块的实现或访问其数据库；需要租户隔离、数据权限或进程隔离时，要另行设计，详见[设计理念](/overview/conception)。
{% endhint %}

## 工程约束补足模块边界

模块容器有助于组织服务查找，但它不能强制团队“只通过公共契约协作”。如果确实需要多个模块独立演进，应同时落实项目引用审查、接口评审和契约测试。核心类库适合承载通用契约；产品特有的跨模块契约应由产品团队明确归属，避免把所有共享类型堆进一个无限增长的公共库。

{% hint style="info" %}
模块划分可以随理解加深而调整。规则少、改动集中、维护周期短的应用，保持简单结构更合适；出现反复共同变化、多人协作或独立交付需求时，再引入相应边界。评审候选模块时，试着说明它维护的业务规则、允许外部使用的入口、缺少依赖时的行为，以及替换它需要共同验证哪些产物。
{% endhint %}

## 延伸阅读

* 从业务动作出发，进一步讨论[如何让业务能力跨宿主复用](/overview/pluginization/host-independent-business)。
* Elux 的[微模块设计](https://github.com/hiisea/elux/blob/main/docs/designed/micro-module.md)与作者对[前端业务模块化](https://www.cnblogs.com/hiisea/p/16624472.html)的探索，从按业务组织代码与资源的角度与本篇呼应；本篇按 Zongsoft 服务端的模块、装配与交付机制展开，两种框架的运行方式应分别理解。
* 模块、服务与提供者的名词边界见[基础概念](/overview/concepts#module-service-provider)。
* 应用模块与插件装配的实现见[插件应用模型](/framework/plugins/application-model)。


# 让业务能力跨宿主复用

把业务动作留在服务里，让控制器、命令和后台入口共用同一套规则。

论坛的审核最初只通过 Web 界面完成：版主打开页面点击“批准”。如果后续要把审核能力交给批量命令、定时任务或合作系统，最值得复用的不是页面，也不是控制器，而是那套“什么条件下允许批准、批准后更新什么”的业务动作。

服务承载业务动作，入口负责把外部输入翻译成业务调用、再把结果翻译回各自的输出方式。Zongsoft 把宿主、插件装配和业务服务分开，正是为了让同一条业务规则能够被不同入口复用。关键前提是：**业务服务不能绑定某个入口的输入输出方式，也不该反向依赖某个入口**。

![同一业务能力可以由多种入口调用](https://847714710-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FdTV8LtunzSisrDXg3zNv%2Fuploads%2Fgit-blob-ec95d0b370ccaf9cbc070d8cb667422d3597d5d4%2Fzongsoft-plugin-host-independent-business.png?alt=media)

*各入口先补齐身份、业务范围与配置，再调用同一个审核服务；图中批量命令与后台入口属于待设计的使用方式。中间栏表示共同的设计责任，不是框架新增的网关组件。*

## 从一次真实调用读出职责

[ThreadController](https://github.com/Zongsoft/discussions/blob/main/src/api/Controllers/ThreadController.cs) 的审核动作调用 [ThreadService.Approve](https://github.com/Zongsoft/discussions/blob/main/src/Services/ThreadService.cs)，再把布尔结果映射为 HTTP 204 或 404。控制器薄而直白：它不重新实现“版主才能批准”“未审核才能批准”，只决定“批准成功返回 204，否则返回 404”。相关实现见[请求与数据服务接口](/framework/web/data-services)。

这次分离留下一个值得注意的结果约定：`Approve` 返回 `false` 表示没有发生符合条件的更新，它不区分主题不存在、已经批准或当前用户不是版主。如果新的命令入口需要向操作者展示更细的原因，应当评估扩展业务结果契约，并注意权限信息暴露的范围；命令不应自行猜测失败原因。

## 依赖方向决定复用是否成立

让入口和服务可以拆开使用的前提是依赖方向一致：业务库不引用 Web 库，Web 库引用业务库。Discussions 的领域程序集保持自身依赖边界，[Discussions.Web](https://github.com/Zongsoft/discussions/tree/main/src/api) 只做适配。业务服务返回领域结果，不产生 HTTP 响应；终端命令也不该为了让服务执行而模拟一次 HTTP 请求。

依赖方向同样约束元数据：领域能力需要的服务、映射、选项随领域插件交付；Web 入口需要的能力（控制器、认证中间件）依赖 Web 宿主。把 HTTP 语义写进领域服务，就等于要求所有复用入口都提供 HTTP 环境。

{% hint style="info" %}
💡 分层不需要为每个方法机械地增加一层接口。当前控制器直接使用具体数据服务；当确有跨模块消费者需要稳定契约、或确有多个实现需要替换时，再引入接口并明确其所有者。可维护性来自职责清楚，也来自避免无意义的转发层。
{% endhint %}

## 新入口要自己补齐调用上下文

从 Web 入口进入服务时，应用已经建立了认证主体、请求环境和已装配的插件。换成命令或后台入口，这些条件不会自动出现——尤其是“当前调用者是谁”这件事。

| 需要明确的内容 | Web 入口中的来源 | 新入口需要安排的事情              |
| ------- | ---------- | ----------------------- |
| 调用身份    | 认证与授权流程    | 建立受信任的执行身份及对应权限，后台不等于版主 |
| 业务范围    | 请求参数及服务约束  | 明确站点、目标资源与允许处理的范围       |
| 配置与服务   | 插件宿主装配     | 部署领域依赖、配置连接并验证服务解析      |
| 完成与失败   | HTTP 响应    | 约定命令结果、任务记录与失败处理        |
| 中止与重试   | 请求生命周期     | 采用入口支持的取消方式，并验证重复执行语义   |

表中的“需要明确的内容”是设计责任，不表示当前所有服务方法都接收统一上下文参数。实现新入口时，应按现有 API 显式传参或建立执行上下文；需要补充契约时，把兼容性作为变更的一部分。

## 按生命周期放置状态与工作

“审核状态”可能指页面上选中的主题、批处理已完成的条目、或数据库中正式批准的结果。它们分别跟随交互、任务执行与持久数据的生命周期，不应都塞进同一个共享模块实例。

持续监听消息的入口可以用[工作器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)管理启动与停止，再在每次收到消息时调用服务；应用启动阶段的装配使用[初始化器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationInitializer.cs)。不要把长期任务放进服务构造函数来替代生命周期管理。共享服务也不要保存“这一次审核”的主题编号或用户身份，调用数据应保持在明确的执行范围内，详见[服务定位与所有权](/framework/core/services/locating)。

拟新增的批量审核入口若需要断点续办，应设计持久任务记录保存进度，并在恢复时重新核验权限与目标状态；仅保存在进程字段里的进度会随进程退出而丢失，恢复内存快照也不能撤销已经提交的审核结果。

## 用两类验证确认复用成立

第一类验证直接针对业务结果：非版主被拒绝、重复批准符合约定、正文批准状态一致。只要提供必要的配置、身份与数据依赖，这类验证应能调用业务服务完成，不需要为验证规则而启动完整 HTTP 管线。

第二类验证针对入口本身：控制器如何映射结果、命令如何取得身份、后台任务中断后如何恢复。新入口可以共用第一类验证，但仍需要自己的适配验证。这样才分得清一次变化是改变了业务规则，还是只改变了业务的使用方式。

HTTP 入口遵守 [REST API 设计规范](https://github.com/Zongsoft/Guidelines/blob/main/zongsoft.rest-api.guidelines.md)，C# 实现遵守 [C# 编码规范](https://github.com/Zongsoft/Guidelines/blob/main/zongsoft.csharp.guidelines.md)。接下来阅读[把扩展点设计成协作契约](/overview/pluginization/extension-contracts)，判断新能力应该通过服务调用、扩展集合还是事件协作。

## 延伸阅读

* 选择承载业务能力的宿主：终端、后台服务与 Web 的差异见[宿主概览](/hosting/hosting)。
* 模块服务如何解析与回退见[插件应用模型](/framework/plugins/application-model)。
* 服务所有权与作用域见[服务定位与所有权](/framework/core/services/locating)。
* 需要多入口共享的业务应先划清边界，见[从业务变化确定模块边界](/overview/pluginization/business-boundaries)。


# 把扩展点设计成协作契约

扩展点由装配路径与行为约定共同构成，为过滤器、验证器和事件定义可维护的协作契约。

一个插件能够被加载，只说明它进入了运行环境。要让另一个团队或插件为它增加能力，还需要说清楚：对象挂在哪里、遵守什么接口、何时被调用、失败后由谁负责。扩展点若想长期可用，就不能只提供一个“能加东西的列表”，而要成为双方都能独立工作的**契约**。

Zongsoft 的插件树把装配位置显式化，服务容器提供能力查找，数据引擎、Web 和安全模块再各自定义扩展点的业务语义。本篇以论坛过滤器为例，说明设计者需要为这些机制补齐哪些约定。机制语法见[插件文件与加载](/framework/plugins/plugin-file)与[构件与服务](/framework/plugins/builtins-and-services)。

## 从一个现有扩展点读出两方责任

论坛的 [Zongsoft.Discussions.plugin](https://github.com/Zongsoft/discussions/blob/main/src/Zongsoft.Discussions.plugin)在 `/Workbench/Modules` 下引用已有模块实例，并把模块访问器的过滤器集合暴露为可挂载节点；随后把 `PostFilter`、`ThreadFilter` 两个过滤器挂到 `/Workbench/Modules/Discussions/Accessor/Filters`。

这段装配关系包含两种责任：

* 模块一侧提供**可被扩展的集合**：`expose` 把访问器的 `Filters` 集合接入插件树，作为稳定的挂载目标。
* 过滤器一侧提供**符合要求的对象**：`object` 的 `type` 描述需要构建的构件类型，构件名是树节点名。

路径只是契约的一部分。现有过滤器还按模型匹配：`PostFilter` 只处理帖子，`ThreadFilter` 处理主题及正文。挂载成功不意味着所有查询都会执行某个过滤器。外部插件若继续向这个集合贡献对象，必须理解匹配条件、处理阶段和对象生命周期，不能只复制路径字符串。

![过滤器装配与其需要明确的行为约定](https://847714710-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FdTV8LtunzSisrDXg3zNv%2Fuploads%2Fgit-blob-607034606d8e072ef89f27f96bba1dd2e8db35b9%2Fzongsoft-plugin-extension-contracts.png?alt=media)

*左侧是过滤器在插件树中的装配位置，右侧是扩展作者需要写清楚的行为约定；挂载成功不等于这些约定自动成立。*

## 按“完成条件”选择协作方式

模块之间协作不只有一种机制。先问“这次协作必须达到什么完成条件”，再选方式：

| 协作需要             | 可以采用的方式          | 需要写清楚的约定          |
| ---------------- | ---------------- | ----------------- |
| 调用方必须取得本次操作结果    | 通过明确契约调用服务       | 输入、结果、权限、超时及失败语义  |
| 能力拥有者允许别人加入处理步骤  | 向扩展点贡献过滤器、处理器或驱动 | 适用范围、顺序、共享状态和异常处理 |
| 一个事实发生后通知多个关注方   | 定义事件及处理方式        | 触发时机、载荷、订阅者失败的影响  |
| 工作需要跨进程或在故障后继续投递 | 采用消息机制并安排持久化策略   | 确认、重试、重复处理及恢复责任   |

这些方式可以组合。例如业务操作通过服务完成，提交后再安排外部索引更新。但一个必须成功才能确认交易的步骤，不应仅因为希望“解耦”就改成没有完成约定的通知。事件减少了对具体订阅者的引用，也会把一部分依赖转移到事件语义和运行诊断上。

{% hint style="info" %}
💡 论坛当前 [Module](https://github.com/Zongsoft/discussions/blob/main/src/Module.cs) 的 `Events` 只是暴露事件注册表，并不表示发帖或审核已经发布具体业务事件，更不表示已可靠投递到消息队列。新增这类能力仍需要完整实现，见[事件](/framework/core/components/events)。
{% endhint %}

## 为扩展点写一份使用说明

假设产品准备允许第三方为“审核完成”增加处理器。评审时至少需要形成下列说明。这是扩展设计建议，不是论坛当前已有的扩展 API：

* **所有者与入口**：由谁维护扩展点；贡献插件需要依赖哪个插件；目标集合接收什么契约。
* **输入与适用范围**：提供已提交的业务标识还是可变对象；如何标识站点；哪些动作会触发处理。
* **顺序与并发**：多个处理器是否有先后关系，是否并发执行，能否重复调用。文件排列顺序不是业务顺序保证。
* **失败与生命周期**：异常是否影响主操作；谁记录和重试；停机时是否等待在途任务。
* **兼容与验证**：如何处理新增字段；旧插件能否继续工作；没有贡献处理器时业务是否仍可用。

扩展点公开的可变对象越多，外部插件越容易依赖内部细节。需要长期兼容的扩展，宜提供与动作目的相称的输入输出，避免把整个服务容器、数据访问器和内部对象图都变成公共协议。

## 插件依赖与项目引用一起审查

论坛的 [Web 清单](https://github.com/Zongsoft/discussions/blob/main/src/api/Zongsoft.Discussions.Web.plugin)依赖领域插件。程序集引用负责让类型可编译，清单依赖负责运行时装配顺序，部署方案负责把文件带到目标环境。三者都应和真实调用关系一致；清单中声明名称不会自动下载 NuGet 包。

如果两个业务插件需要互相引用内部服务，并在启动时等待对方构建对象，先检查协作职责能否重新归属：提取稳定公共契约，或让一个负责完整流程的上层组件协调双方。需要立即结果的依赖仍应明确保留，改成字符串服务名并不会让依赖消失。模块服务解析的回退行为见[插件应用模型](/framework/plugins/application-model)。

## 让名称可定位，但不混淆名称域

故障记录里只出现一个 `Discussions`，读者可能不知道它指模块、插件还是配置节。论坛提供了直观例子：

* 插件名：`Zongsoft.Discussions.Web`（清单依赖与装配顺序使用）。
* 模块名：`Discussions`（模块服务解析域）。
* 构件名：`PostFilter`（插件树中过滤器集合下的节点）。
* 配置名：`/Discussions` 开头的选项节。

它们分别参与插件依赖、模块查找、树节点定位和配置读取，不能互相替代。新增公开路径、服务别名或配置键时，应说明其所有者与适用范围，并维护既有消费者使用的名称。

## 验证装配结果，也验证业务效果

扩展点的测试应覆盖“插件已经加载，但扩展没有按预期工作”的情况：模型不匹配、重复注册、目标节点缺失、处理器异常、顺序变化，以及共享实例被并发调用。只验证类型能构建，会遗漏这些协作问题。

对于需要可靠通知的扩展，还要检查数据库提交与消息发送之间的失败窗口。可根据一致性需求设计持久化待发送记录、补偿或对账机制；这些属于应用方案，不能从“框架支持事件”推断为自动获得。投递边界见[消息概念](/framework/messaging/concepts)。

契约清楚以后，维护者才能判断新插件是否兼容、缺少可选插件是否允许启动、某次失败由谁接手。接下来把这些约定带到[可验证的插件交付](/overview/pluginization/evolutionary-delivery)，检验它们能否支撑实际发布。

## 延伸阅读

* Elux 的[微模块设计](https://github.com/hiisea/elux/blob/main/docs/designed/micro-module.md)讨论了模块自治与模块间协作（含其事件化 Action 机制）；本篇关注的是 Zongsoft 的插件树、服务解析与失败处理，运行方式以链接中的 Zongsoft 实现为准。
* 扩展路径与服务语法的实现见[插件文件与加载](/framework/plugins/plugin-file)。
* 模块服务与构件的协作见[构件与服务](/framework/plugins/builtins-and-services)。


# 从局部改造到可验证的插件交付

用可独立验收的业务切口渐进引入插件，让每一步都可运行、可观察、可回退。

运行中的业务系统很少有机会停掉所有需求来做一次彻底重构。引入 Zongsoft 插件化也一样：不必一次把所有代码都搬进新结构，而是选一条责任清晰的业务路径，验证它的服务、装配与部署，再逐步扩大范围。每一步都应留下可运行、可观察、出了故障知道如何处置的版本。

## 先固定基线，再选切口

改造前先固定可观察的行为：给定输入返回什么、哪些身份允许调用、异常如何表现、资源消耗大致如何。后续每一步都与这个基线比较，才能知道结构调整是否悄悄改变了业务。

首个切口最好具备这些特征：

* 输入输出明确，调用链可以追踪；
* 数据所有权与依赖相对清楚；
* 有独立的验收方式，不依赖整套环境才能验证；
* 尽量先选无副作用或副作用可控的路径。

论坛的用户导出是一个可以参照的例子：现有[模板模型提供者](https://github.com/Zongsoft/discussions/blob/main/src/Features/Archiving/UserDataTemplateModelProvider.cs)通过用户服务取得数据。若要改造类似能力，可以先保留调用入口和业务规则，把模板适配与资源交付组织成插件，再验证结果、筛选范围和权限。导出是“只读”也不意味着可以绕过数据范围约束。

## 让每一步都有可运行的结果

| 阶段     | 主要改变           | 继续推进的依据              |
| ------ | -------------- | -------------------- |
| 提取业务入口 | 原入口改为调用职责明确的服务 | 原有结果、权限和失败行为仍可验证     |
| 声明插件装配 | 补齐清单、依赖和必要扩展   | 在目标宿主中能加载并解析服务       |
| 固定交付文件 | 编写部署规则，收集配置与资源 | 在干净运行目录中完成一次真实调用     |
| 切换调用路径 | 新实现接管明确范围的调用   | 能观察错误、时延和新旧结果差异      |
| 收回旧实现  | 清理重复入口和不再需要的依赖 | 调用者已迁移，运行记录证明旧路径不再使用 |

“切换”是一项发布安排，可以通过应用路由、配置或发布批次实现，不表示框架自动支持在线替换已加载的程序集。若需要并行比较新旧实现，优先从无副作用且允许比较的数据读取开始：写操作不能各自执行一遍来比对结果，重复发送通知、重复扣减或重复创建文件，可能造成无法靠回退程序集恢复的业务影响。

## 把发布组合当成验证对象

![从选择版本组合到组装产物、验证运行，并贯穿数据兼容检查](https://847714710-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FdTV8LtunzSisrDXg3zNv%2Fuploads%2Fgit-blob-4170e57fad6e50f9938fc37446e26ef0073a799e%2Fzongsoft-plugin-evolutionary-delivery.png?alt=media)

*宿主与插件版本确定以后，还要交付配置、映射和资源，并验证启动、业务调用与恢复方案。数据兼容与迁移需要单独设计，不能由文件回退替代。*

论坛的[领域部署文件](https://github.com/Zongsoft/discussions/blob/main/src/Zongsoft.Discussions.deploy)包含清单、选项、映射和程序集规则；[Web 部署文件](https://github.com/Zongsoft/discussions/blob/main/src/api/Zongsoft.Discussions.Web.deploy)另含 Web 清单与模板。这说明可用的交付单元要覆盖运行所依赖的元数据和资源，而不只是 DLL。

对有多种客户方案的产品，建议记录每种受支持组合使用的宿主、插件版本、数据驱动、配置要求和迁移步骤。发布验证至少要覆盖实际支持的最小组合与关键扩展组合，并区分“遗漏可选插件”与“遗漏必需插件”的结果。不必把理论上所有排列都宣称成产品能力。

独立发布一个插件包，意味着可以单独产出和管理它；该版本能否投入使用，仍取决于宿主、共享依赖、扩展契约和数据结构是否兼容。同一进程内共享程序集版本冲突，不能靠给插件目录改名解决。

## 数据变化决定恢复边界

插件版本回退只覆盖程序文件时，数据库与外部文件可能已经改变。发布前需要回答：新增映射字段后旧版本能否忽略它；转换存储格式后旧版本能否继续读取；新版本是否已发出不可撤回的外部通知。

可以按需求采用“先增加兼容字段、逐步迁移数据、切换读写、最后移除旧结构”的过程。对无法兼容旧版本的数据变化，应明确恢复方案和维护窗口。部署工具负责交付产物，不会替应用推导数据迁移与业务补偿，分工见[部署模型](/overview/deployment)与[升级接入与故障恢复](/framework/upgrading/workflow)。

## 何时值得进一步拆成独立进程

插件化帮助组织代码与运行时装配；进程拆分则增加新的故障与通信边界。二者可以按实际需要组合：

| 出现的需求          | 首先评估的方案        | 额外成本或限制         |
| -------------- | -------------- | --------------- |
| 多产品选用不同能力      | 插件与部署方案组合      | 需要管理受支持的版本组合    |
| 多种入口共享业务规则     | 分离业务服务与宿主适配    | 仍需接续身份、配置及运行依赖  |
| 某项工作需独立扩容或故障隔离 | 把相关能力放入独立宿主进程  | 需要协议、超时、观测及故障恢复 |
| 不可信扩展需要权限隔离    | 在部署与运行环境建立安全边界 | 同进程插件不能提供这类安全保证 |

把本地调用迁到网络后，失败不再只有成功或异常两种直观结果：请求超时时，远端可能已经完成写入。需要重新约定幂等、重试、数据所有权和结果查询，而不是只把接口实现换成远程客户端。进程拆分不应成为每个插件必须经历的终点。

## 用一次发布检验设计效果

完成首个切口后，检查下一次需求能否主要落在自己的业务范围内、运行目录能否重复组装、故障能否定位到服务调用或装配阶段、交付负责人能否说明恢复步骤。这些结果比“新增了多少个项目”更能反映改造是否有效。

边界尚不清楚时，返回[从业务变化确定模块边界](/overview/pluginization/business-boundaries)重新检查变化与规则的归属；模块间的扩展约定见[把扩展点设计成协作契约](/overview/pluginization/extension-contracts)。实践路径从[编写第一个业务插件](/get-started/first-business-plugin)和[部署第一个插件](/get-started/deploy-first-plugin)开始。

## 延伸阅读

* Elux 作者关于[拆解大型前端应用](https://www.cnblogs.com/hiisea/p/16914890.html)的文章用“分层与切块”讨论了渐进式拆分大型应用的思路，与本篇的服务端插件化演进相呼应；前端模块的加载机制不等于插件升级保证。
* 部署模型见[部署模型](/overview/deployment)，宿主交付见[部署宿主](/hosting/deployment)。
* 运行问题的分层排查见[运行与调试](/get-started/run-and-debug)。


# 部署模型

了解 Zongsoft 中部署、打包和升级的整体模型。

Zongsoft 的部署模型围绕插件式应用展开。部署不是简单复制编译输出，而是把插件、配置、映射、证书和资源按规则组装到宿主程序的目标目录中。

## 部署

部署由 `dotnet-deploy` 工具执行，规则写在 `.deploy` 文件中。部署文件描述了源文件、NuGet 包、目标目录和过滤条件。

典型部署内容包括：

* 主插件文件。
* 插件程序集。
* 选项配置文件。
* 数据映射文件。
* 本地化资源。
* 证书或站点配置。

## 打包

部署完成后，可以用 `dotnet-pack` 将发布目录制作成 Linux 常见包格式：

* `.tar.gz`
* `.deb`
* `.rpm`

打包阶段关注安装路径、systemd 服务、生命周期脚本、包元数据和文件权限。

## 自动升级

自动升级由 framework 仓库中的 upgrading 组件和 `dotnet-upgrade` 工具支持。升级包通常包含应用发布文件、清单、校验码和部署执行器信息。

## 推荐路径

开发阶段优先使用 `terminal` 宿主调试插件；联调 Web API 时使用 `web/default` 宿主；准备部署时使用 `daemon` 或 `web` 宿主配合部署、打包和升级工具。

## 三类产物不要混用

| 阶段  | 输入              | 输出与验收                       |
| --- | --------------- | --------------------------- |
| 部署  | 宿主发布文件、插件包和方案配置 | 可启动的运行目录，完成一次业务调用           |
| 安装包 | 已验证运行目录和安装元数据   | tar/deb/rpm，核对路径、服务及升级/卸载脚本 |
| 升级包 | 新版运行文件和发布身份     | ZIP/manifest，核对发现、交接和新进程健康  |

部署参数中的编译配置、安装包的产品身份、升级包的版本分发名各有用途，不应只因选项重名就套用相同值。持久数据和环境秘密也不应无差别收集进程序包。

具体操作：[部署宿主](/hosting/deployment)、[打包工具](/tools/packager)、[自动升级](/framework/upgrading)。


# 准备环境

根据所选教程准备 SDK、工具和源码输出，区分包使用与框架源码开发。

![准备环境](https://847714710-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FdTV8LtunzSisrDXg3zNv%2Fuploads%2Fgit-blob-10be71c1aee0af4b6e8f004be7e2c5ffeb32b5e9%2Fzongsoft-start-cover.png?alt=media)

学习插件使用可以直接从 NuGet 获取框架，不必先构建全部仓库。只有需要调试框架源码、使用本地未发布改动或构建现有 hosting 时，才需要准备相邻源码输出。

## 先选择一条路径

| 目标         | 必需内容                            | 起点                                        |
| ---------- | ------------------------------- | ----------------------------------------- |
| 创建最小插件应用   | 匹配 SDK、部署工具、可用包源                | [第一个插件](/get-started/deploy-first-plugin) |
| 调试框架与现有宿主  | Git、framework/hosting 源码、匹配编译输出 | [宿主概览](/hosting/hosting)                  |
| 连接数据库或消息系统 | 前面内容及所选外部服务                     | 对应驱动与连接专题                                 |

## 开发规范

开始编写业务插件或 Web 接口前，开发者应阅读并遵守以下规范：

* [C# 编码规范](https://github.com/Zongsoft/Guidelines/blob/main/zongsoft.csharp.guidelines.md)：编写和审查 C# 代码时遵守统一的命名、格式与编码约定。
* [REST API 设计规范](https://github.com/Zongsoft/Guidelines/blob/main/zongsoft.rest-api.guidelines.md)：设计和实现 HTTP 接口时遵守资源命名、HTTP 方法、状态码与参数约定。

规范以链接中的最新内容为准；学习示例时也应结合规范理解，开发与代码审查时按规范核对。

## SDK 版本

当前 hosting 根配置为 .NET 10；本库最小教程也以 `net10.0` 编写。framework 多个类库支持 .NET 8、9、10，但具体项目、示例和工具需以自己的 `.csproj`、`Directory.Build.props` 及包依赖为准。

{% code title="CheckDotnet.ps1" %}

```powershell
dotnet --info
dotnet --list-sdks
dotnet --list-runtimes
```

{% endcode %}

SDK 用于编译，运行时用于执行。安装较新运行时不代表机器已具备所有旧目标所需运行时；Web 和 Windows 桌面工具还各有运行环境要求。

## 源码目录

需要源码路径时，建议把仓库放在同一个父目录：

{% code title="Workspace.layout" %}

```
Zongsoft/
	framework/
	discussions/
	hosting/
	tools/
	documentation.zh/
```

{% endcode %}

{% code title="CloneRepositories.ps1" %}

```powershell
git clone https://github.com/Zongsoft/framework.git
git clone https://github.com/Zongsoft/hosting.git
git clone https://github.com/Zongsoft/Zongsoft.Discussions.git discussions
git clone https://github.com/Zongsoft/tools.git
git -C framework submodule update --init --recursive
```

{% endcode %}

framework 的 OpenTelemetry 协议来源使用子模块；构建相关诊断项目时需要对应内容。hosting 默认引用 framework 的相邻输出，源码目录存在还不够，必须先编译所需类库的对应配置和目标。

## 工具与可选环境

安装步骤见[安装包](/get-started/install)。编辑器可使用支持 .NET 的 IDE，Shell 命令应按 PowerShell 或 Bash 的各自语法执行，不能混用续行和变量插值。

仅编译 Discussions 不要求运行外部服务；完整论坛查询需要按真实映射与宿主方案准备数据库、身份与相关依赖。MQTT 和模型服务器不是论坛业务的固有前置条件。按需要准备依赖服务，并先检查端点就绪；使用 Podman 时阅读[容器化环境](/hosting/containerization)。

准备完成后，应能明确回答：目标框架是什么、包从哪里来、输出目录在哪里、实际启动哪个程序。接着[选择宿主](/get-started/hosting)并完成第一个运行闭环。


# 安装包

安装 Zongsoft 相关 NuGet 包和全局工具。

Zongsoft 框架组件主要通过 NuGet 包分发，工具链通过 .NET 全局工具分发。

## 编译引用与运行部署

`dotnet add package` 让项目能够编译使用相应 API；插件宿主还需要 `.plugin`、`.option`、映射及依赖等运行产物。业务模块通常只引用需要的契约，具体驱动由宿主部署方案选择。例如只使用数据访问契约的模块不必直接引用所有数据库驱动。

团队项目应固定兼容版本。不同版本包混合部署时，最终 DLL 和附属资源必须匹配；不能把 NuGet 还原成功当作插件部署成功。

## 安装框架包

按你的应用需要安装对应包。例如，插件式应用通常会用到：

```bash
dotnet add package Zongsoft.Core
dotnet add package Zongsoft.Plugins
```

如果需要 Web 能力：

```bash
dotnet add package Zongsoft.Web
dotnet add package Zongsoft.Plugins.Web
```

如果需要数据访问：

```bash
dotnet add package Zongsoft.Data
dotnet add package Zongsoft.Data.MySql
```

可用驱动包括 SQL Server、MySQL、SQLite、DuckDB、PostgreSQL、InfluxDB、TDengine 和 ClickHouse 等，具体见 [包与模块索引](/references/packages)。

## 安装部署工具

`dotnet-deploy` 用于根据 `.deploy` 文件部署插件和附属文件。

```bash
dotnet tool install -g Zongsoft.Tools.Deployer
```

更新已安装工具：

```bash
dotnet tool update -g Zongsoft.Tools.Deployer
```

## 安装打包工具

`dotnet-pack` 用于制作 `.tar.gz`、`.deb`、`.rpm` 安装包。

```bash
dotnet tool install -g Zongsoft.Tools.Packager
```

## 检查工具

```bash
dotnet tool list -g
```

确认列表中包含需要的工具后，再继续阅读 [选择宿主程序](/get-started/hosting)。

## 工具找不到时

先检查 `dotnet tool list -g` 和工具目录是否进入 PATH，再打开新终端重试。全局安装属于当前账号，系统服务或其他账号不一定能找到同一路径。离线或私有源环境还需预先安排包缓存和访问权限。

仅制作插件应用时先安装部署器即可，Linux 安装包和自动升级工具在相应阶段再准备。不要为了运行一个无外部依赖的示例启动全部容器。


# 选择宿主程序

根据调试、服务运行和 Web API 场景选择合适的 Zongsoft 宿主程序。

Zongsoft 的宿主程序位于 [`Zongsoft/hosting`](https://github.com/Zongsoft/hosting) 仓库。宿主程序只负责启动运行时和承载插件，不应该直接写入业务代码。

## 宿主类型

### 终端宿主

终端宿主通过控制台运行，适合交互式调试、命令行复现和观察插件式应用行为。

源码目录：

```
hosting/terminal
```

### 后台服务宿主

后台服务宿主适合部署为 Windows Service 或 Linux systemd 服务，面向长期运行的后台应用。

源码目录：

```
hosting/daemon
```

### Web 宿主

Web 宿主基于 ASP.NET，适合 Web API、认证授权、HTTP 管线、站点配置和接口调测。

源码目录：

```
hosting/web/default
```

## 如何选择

开发和调试插件时，优先使用终端宿主；需要验证 HTTP 接口时，使用 Web 宿主；准备部署为后台服务时，使用后台服务宿主。

## 下一步

选择宿主后，继续阅读 [部署第一个插件](/get-started/deploy-first-plugin)，了解如何把插件部署到宿主的 `plugins/` 目录。

## 复用条件

共享业务契约可以在不同宿主中复用，控制器、中间件和交互命令则需要相应宿主能力。建立业务服务时尽量把协议适配放在边缘，让终端命令与 HTTP 控制器调用同一服务。

现有 hosting 默认组合了多种基础设施，首次学习可先使用教程中的独立最小宿主。需要现成运行方案时，分别阅读[终端](/hosting/terminal)、[后台服务](/hosting/daemon)和[Web](/hosting/web)的构建及部署说明。


# 部署第一个插件

沿 hosting 已有方案部署 Discussions 领域与 Web 插件。

本篇采用 hosting 仓库的真实 Web 宿主和 Discussions 部署项。需要同级 framework、hosting、discussions 源码，以及自己的隔离数据库、身份与文件存储配置。

## 1. 确认现有方案已经包含业务插件

来源：[hosting/web/web.deploy](https://github.com/Zongsoft/hosting/blob/main/web/web.deploy#L29)（节选；上下文见源文件）。

{% code title="web.deploy" %}

```ini
[plugins zongsoft discussions]
nuget:Zongsoft.Discussions@0.8.0
nuget:Zongsoft.Discussions.Web@0.8.0
```

{% endcode %}

0.8.0 是当前方案锁定的包版本，不表示 NuGet 上永远最新。领域库和 Web 库要一起部署；数据、安全和其他公共插件由同一方案的其余条目提供。

## 2. 区分宿主发布与业务构建

宿主项目位于 hosting/web/default，入口是 Zongsoft.Hosting.Web.dll。默认从同级 framework 输出引用 [核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core)、Web、Plugins、Plugins.Web，因此发布前要先构建这些项目的相同目标框架和配置。命令从 hosting/web/default 执行：

{% code title="发布真实 Web 宿主" %}

```powershell
dotnet publish Zongsoft.Hosting.Web.csproj -c Release -f net10.0 -o ./out
```

{% endcode %}

该命令只准备宿主运行文件。本地构建的 discussions 不会自动替代部署清单中的 NuGet 0.8.0；本地源码构建步骤见[业务插件](/get-started/first-business-plugin)。

## 3. 为隔离环境准备配置

检查 hosting/.deploy/default/options 中的应用、数据、安全和文件配置，替换为自己的测试端点。Discussions 的映射含外部序号和实体驱动选择，数据库脚本必须与实际部署匹配，见[首次查询](/framework/data/quickstart)。

不要直接运行 deploy.cmd 作为学习验证：该脚本还会清理插件目录并可能继续打包。下面使用同一部署器和已存在的方案，目标设为上一步的 out 目录：

{% code title="从 hosting/web/default 收集插件" %}

```powershell
dotnet deploy .deploy --destination:./out --host:web --site:default --scheme:default --environment:development --debug:off --edition:Release --framework:net10.0 --platform:win --architecture:x64
```

{% endcode %}

参数来自真实部署脚本；平台与架构应按实际运行环境调整。环境目录里的配置必须先审阅，命令不会替你建立安全的测试数据库。

## 4. 核对本地修改是否进入运行目录

使用 NuGet 清单时运行的是包内版本。调试本地修改，应停止宿主后，把 discussions 构建的 DLL/PDB 以及相应 plugin、option、mapping 更新到 out/plugins/zongsoft/discussions，并保留 Web 模板目录。不能把旧包和新源码混在一起后依然按同一版本排障。

领域与 Web 的资源清单见[部署文件格式](/references/deploy-files)。部署后应同时看到两个插件清单、领域映射、选项和用户列表模板。

## 5. 从运行目录启动并验证

{% code title="启动 Web 宿主" %}

```powershell
Set-Location ./out
dotnet ./Zongsoft.Hosting.Web.dll
```

{% endcode %}

监听地址由宿主配置提供。先确认 /Application，再用 discussions/docs/http/forum.http 的只读查询验证论坛接口。401/403、404、连接失败和空结果代表不同问题，按[运行与调试](/get-started/run-and-debug)逐层排查。

这条路径会连接配置中的业务依赖。文档迁移阶段的离线编译和回归检查不能替代你所在环境的完整部署验收。


# 编写第一个业务插件

从 Discussions 已有模块理解业务模型、服务注册、插件清单和交付产物。

本篇直接阅读并构建 Discussions 业务插件。先从已有实现理解框架的分工，再在同一个业务模型上扩展需求。前置条件是[准备环境](/get-started/prerequisites)所需 SDK 与 discussions 源码。

## 1. 构建现有业务库

项目支持 .NET 8、9、10；NuGet 版本来自根目录 Directory.Packages.props。源码目录和 SDK 的准备见[准备环境](/get-started/prerequisites)。下面演示本地引用方式：从 discussions 根目录先构建相邻 framework 的[核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core)和 Web，再以本地引用构建 Discussions；统一使用默认 Debug 配置和 .NET 10，并关闭构建时打包：

{% code title="构建 Discussions" %}

```powershell
dotnet build ../framework/Zongsoft.Core/src/Zongsoft.Core.csproj -f net10.0 -p:GeneratePackageOnBuild=false
dotnet build ../framework/Zongsoft.Web/src/Zongsoft.Web.csproj -f net10.0 -p:GeneratePackageOnBuild=false
dotnet build src/Zongsoft.Discussions.csproj -f net10.0 -p:ZongsoftFrameworkPathReferenced=true -p:GeneratePackageOnBuild=false
dotnet build src/api/Zongsoft.Discussions.Web.csproj -f net10.0 -p:ZongsoftFrameworkPathReferenced=true -p:GeneratePackageOnBuild=false
```

{% endcode %}

`-p:ZongsoftFrameworkPathReferenced=true` 选择本地程序集，但不会自动构建 framework，目录、配置和目标框架必须与其输出一致。如果所用 NuGet 源已包含项目要求的[核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core)等依赖版本，可省略本地引用参数并使用默认包引用方式。

## 2. 模块是装配入口

来源：[src/Module.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Module.cs#L31)（节选；上下文见源文件）。

{% code title="Module.cs" %}

```csharp
[assembly: ApplicationModule(Zongsoft.Discussions.Module.NAME)]

namespace Zongsoft.Discussions;

public class Module : ApplicationModule<Module.EventRegistry>
{
	#region 常量定义
	/// <summary>表示论坛模块的名称常量值。</summary>
	public const string NAME = nameof(Discussions);
	#endregion
```

{% endcode %}

程序集声明把服务归到 Discussions 模块。Module.Current 提供共享模块实例；不要为每次请求重新创建它。模块访问器和服务容器的关系见[服务定位](/framework/core/services/locating)。

## 3. 服务承载业务动作

来源：[src/Services/ThreadService.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/ThreadService.cs#L41)（节选；上下文见源文件）。

{% code title="ThreadService.cs" %}

```csharp
[Service(nameof(ThreadService))]
[DataService(typeof(ThreadCriteria))]
public class ThreadService : DataServiceBase<Models.Thread>
{
	#region 成员字段
	private PostService _posting;
	#endregion

	#region 构造函数
	public ThreadService(IServiceProvider serviceProvider) : base(serviceProvider) { }
```

{% endcode %}

ThreadService 的数据模型是 Thread，查询条件模型是 ThreadCriteria。服务还实现审核、锁定、置顶等业务动作，控制器只负责把请求转给服务。先读[数据服务](/framework/data/services)，不要把审核规则直接复制到每个控制器。

## 4. 清单把模块接入框架

来源：[src/Zongsoft.Discussions.plugin](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.plugin#L20)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.plugin" %}

```xml
<extension path="/Workbench/Modules">
	<object name="Discussions" value="{static:Zongsoft.Discussions.Module.Current, Zongsoft.Discussions}">
		<expose name="Accessor" value="{path:../@Accessor}">
			<expose name="Filters" value="{path:../@Filters}" />
		</expose>

		<expose name="Events" value="{path:../@Events}" />
		<expose name="Properties" value="{path:../@Properties}" />
	</object>
</extension>
```

{% endcode %}

这里注册的是现有 Module.Current。Accessor.Filters 让插件树能够继续挂载查询过滤器；Events 与 Properties 分别暴露事件注册表和模块属性。清单另有数据验证器、安全质询器与身份转换器，详见[插件文件](/framework/plugins/plugin-file)。

## 5. 交付程序集与元数据

来源：[src/Zongsoft.Discussions.deploy](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.deploy#L1)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.deploy" %}

```ini
artifacts/Zongsoft.Discussions.plugin
artifacts/Zongsoft.Discussions.option
artifacts/Zongsoft.Discussions.mapping
lib/$(Framework)/Zongsoft.Discussions.*
```

{% endcode %}

只复制 DLL 不足以运行论坛。映射定义实体关系，选项定义文件存储基础路径，插件清单负责挂载。Web 层还有独立清单和归档模板，按[部署第一个插件](/get-started/deploy-first-plugin)一起交付。

## 扩展时从哪里开始

增加业务字段时同时检查模型、映射、四种 SQL 脚本和查询模式；增加业务动作时先放到服务，再做 HTTP 适配。对于论坛数据，任何扩展都必须保留 SiteId 隔离、审核可见性与作者权限；参见[真实案例](/cases)。


# 运行与调试

按进程、插件、配置、服务和业务调用逐层定位运行问题。

调试应针对实际运行目录。源码已修改、项目已编译和宿主已经加载新版本是三件事；先确认进程路径及最终 DLL，再分析业务行为。

## 从最小可观察结果开始

完成 [Discussions 部署](/get-started/deploy-first-plugin)后，先检查 Web 宿主启动和两个业务清单加载，再使用 discussions/docs/http/forum.http 中的论坛只读请求。确认运行目录中的 DLL/PDB 与本地构建一致。

Web 场景先检查监听地址和 `/Application`，随后验证 Discussions 的[业务控制器](/framework/web/controllers)。连接不上时不要先排查控制器；404 时也不要先修改数据库。

## 分层排障

| 层次   | 核对内容              | 常见原因                     |
| ---- | ----------------- | ------------------------ |
| 进程   | 入口路径、运行时、退出日志     | 启动了错误 DLL、运行时缺失          |
| 插件   | 扫描目录、清单与依赖        | 只复制 DLL、依赖名称不一致          |
| 配置   | 文件基名、环境、host/site | 修改了未匹配的选项文件              |
| 服务   | 注册契约、别名、模块/提供者    | Resolve/Find/Locate 用法混淆 |
| 首次调用 | 外部地址、权限、依赖 DLL    | 延迟连接失败、传递依赖覆盖            |
| 业务结果 | 数据、事务、确认与状态       | 查询形状、幂等或权限范围不符           |

## 断点与符号

附加调试器到实际宿主进程，确保 DLL、PDB 和源码版本一致。修改插件后先停止宿主、复制对应产物再重启；框架未承诺任意已加载程序集都能通过文件覆盖立即热替换。

若宿主使用本地 framework 引用，还需匹配 Debug/Release 与目标框架。仅重建一个项目而遗漏它的依赖，可能导致符号不匹配或首次调用缺少方法。

## 配置与日志

记录应用名、内容根目录、环境和插件路径，再检查[选项文件匹配](/references/option-files)。诊断配置时只输出必要键和非秘密信息，避免打印完整连接字符串。

日志应保留异常类型和调用链。异步查询、流式响应或消息处理的错误可能在后续枚举/回调中出现，错误处理范围需要覆盖真实执行阶段。

## 建立可复现步骤

保存脱敏输入、运行版本、配置差异和预期/实际结果。先检查进程与插件装配，再执行论坛只读查询，再加入外部依赖和并发。涉及写入时使用独立测试数据，并明确清理对象。

进一步阅读：[宿主部署](/hosting/deployment)、[服务定位](/framework/core/services/locating)、[常见问题](/faq)。


# 核心类库

Zongsoft.Core 核心类库的职责和主要命名空间。

[`Zongsoft.Core`](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core) 是 Zongsoft 框架的核心类库，不依赖第三方类库。它提供框架中被其它模块共享的基础抽象、工具类、服务模型和运行时能力。

## 主要能力

* 集合、转换、随机、枚举和常用扩展。
* 通讯、执行管线和组件模型。
* 配置、选项、INI Profile 和资源处理。
* 缓存、序列化、事务和轻量状态机。
* 安全、凭证、授权基础抽象。
* 服务容器、命令模型、后台工作者。
* 终端应用支持。

## 代码位置

```
framework/Zongsoft.Core
```

## 命名空间

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref">页面</th></tr></thead><tbody><tr><td><strong>通用基础</strong></td><td>集合、常用工具、反射、资源和文本处理。</td><td><a href="/framework/core/common">Zongsoft.Common</a></td></tr><tr><td><strong>组件与服务</strong></td><td>组件模型、命令、应用上下文、服务访问和分布式协作。</td><td><a href="/framework/core/components">Zongsoft.Components</a></td></tr><tr><td><strong>版本表达</strong></td><td>语义化版本、四段式数值版本号和版本比较。</td><td><a href="/framework/core/version">Zongsoft.Versioning</a></td></tr><tr><td><strong>配置与数据抽象</strong></td><td>配置绑定、选项文件、数据访问抽象和数据元数据。</td><td><a href="/framework/core/configuration">Zongsoft.Configuration</a></td></tr><tr><td><strong>运行时能力</strong></td><td>缓存、通讯、消息、调度、序列化、事务和状态流转。</td><td><a href="/framework/core/caching">Zongsoft.Caching</a></td></tr><tr><td><strong>应用支撑</strong></td><td>诊断、安全、终端、IO 和表达式处理。</td><td><a href="/framework/core/diagnostics">Zongsoft.Diagnostics</a></td></tr></tbody></table>

## 何时阅读本节

当你需要理解其它模块里的通用类型、服务抽象、命令模型、选项配置或终端能力时，应先回到[核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core)。插件框架、数据引擎、Web 基础库等模块都会复用这里的基础抽象。

## 相关资源

* [Zongsoft.Core 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core)
* [Zongsoft.Core README](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/README.md)
* [Zongsoft.Core NuGet 包](https://www.nuget.org/packages/Zongsoft.Core)


# Zongsoft.Caching

Zongsoft.Caching 命名空间的职责和主要类型。

`Zongsoft.Caching` 提供[核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core)中的缓存抽象和内存缓存实现，用于支撑进程内缓存、缓存过期、容量控制、缓冲刷新和缓存事件通知。

## 主要职责

* 定义 `IDistributedCache` 等缓存访问抽象。
* 提供 `MemoryCache`、`MemoryCacheOptions` 等进程内缓存实现和配置。
* 表达缓存过期策略、缓存优先级、淘汰原因和缓存变更事件。
* 提供 `Spooler<T>` 异步缓冲器，用于高频写入的批量刷新。
* 为其它模块提供统一的缓存依赖，而不是直接绑定某个具体缓存实现。

## 类型

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref">页面</th></tr></thead><tbody><tr><td><strong>MemoryCache</strong></td><td>进程内缓存、过期策略、依赖令牌、淘汰事件和容量提醒。</td><td><a href="/framework/core/caching/memory-cache">缓存</a></td></tr><tr><td><strong>Spooler&#x3C;T></strong></td><td>基于 Channel 的异步缓冲器，用于高频写入的批量刷新。</td><td><a href="/framework/core/caching/spooler">缓冲器</a></td></tr></tbody></table>

## 相关资源

* [Caching 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Caching)


# 缓存

MemoryCache 进程内缓存、过期策略、依赖令牌和淘汰事件。

`MemoryCache` 是 `Zongsoft.Caching` 命名空间中的进程内缓存实现。它包装 [`Microsoft.Extensions.Caching.Memory.MemoryCache`](https://learn.microsoft.com/zh-cn/dotnet/api/microsoft.extensions.caching.memory.memorycache) [*源码*](https://source.dot.net/#Microsoft.Extensions.Caching.Memory/MemoryCache.cs)，并在此基础上增加 Zongsoft 风格的过期描述、依赖令牌、优先级、淘汰事件和数量限制提醒。

{% hint style="info" %}
`MemoryCache` 适合缓存进程内可重建的数据，例如元数据、描述符、解析结果或轻量对象。跨进程共享、分布式一致性和服务间缓存同步应使用 `IDistributedCache` 的具体实现。
{% endhint %}

## 类型关系

| 类型                       | 说明                                               |
| ------------------------ | ------------------------------------------------ |
| `MemoryCache`            | 进程内缓存容器，提供读取、写入、删除、获取或创建、清理和事件通知。                |
| `MemoryCacheOptions`     | 配置扫描频率和数量限制；`MemoryCache.Shared` 使用不可变选项。        |
| `MemoryCacheScanner`     | 周期性调用 `Compact(0)`，触发过期项扫描。                      |
| `MemoryCache.Expiration` | 描述滑动过期、绝对过期，或二者组合。                               |
| `CachePriority`          | 映射到底层缓存项优先级：`Low`、`Normal`、`High`、`NeverRemove`。 |
| `CacheEvictedEventArgs`  | `Evicted` 事件参数，包含键、值、淘汰原因和状态对象。                  |
| `CacheLimitedEventArgs`  | `Limited` 事件参数，包含超出的数量和当前数量。                     |

## 创建缓存

Discussions 通过数据访问器间接使用框架的缓存复用机制。缓存本身的独立演示来自 [核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core) 的 memorycache 交互程序：频率一秒、滑动过期三十秒、数量提醒阈值五项。可以直接创建独立缓存实例，也可以使用共享实例。

来源：[framework/Zongsoft.Core/samples/memorycache/Program.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/samples/memorycache/Program.cs#L13)（节选；上下文见源文件）。

{% code title="Program.cs" %}

```csharp
const int FREQUENCY  = 1;
const int EXPIRATION = 30;
const int LIMIT      = 5;

using var cache = new MemoryCache(TimeSpan.FromSeconds(FREQUENCY), LIMIT);
using var scanner = new MemoryCacheScanner(cache);

cache.Limited += Cache_Limited;
cache.Evicted += Cache_Evicted;
```

{% endcode %}

`MemoryCache.Shared` 是全局共享实例，它的 `Options` 是不可变的，不能在运行时修改。

## 读取与写入

`SetValue` 用于直接写入缓存项，`GetValue` 和 `TryGetValue` 用于读取缓存项，`Remove` 用于删除缓存项。

来源：[framework/Zongsoft.Core/test/Caching/MemoryCacheTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Caching/MemoryCacheTest.cs#L14)（节选；上下文见源文件）。

{% code title="MemoryCacheTest.cs" %}

```csharp
public void Test()
{
	object value;

	var cache = new MemoryCache();
	Assert.Equal(0, cache.Count);
	Assert.False(cache.Contains("KEY"));
	Assert.False(cache.Remove("KEY", out _));
	Assert.False(cache.TryGetValue("KEY", out _));

	value = cache.GetOrCreate("K1", () => "V1");
	Assert.NotNull(value);
	Assert.True(cache.Contains("K1"));
	Assert.Equal("V1", value);
	Assert.True(cache.Remove("K1", out value));
	Assert.Equal("V1", value);
	Assert.Equal(0, cache.Count);

	const int COUNT = 10000;
	Parallel.For(0, COUNT, index => cache.SetValue($"KEY#{index}", $"Value#{index}@{Environment.CurrentManagedThreadId}"));
	Assert.Equal(COUNT, cache.Count);
}
```

{% endcode %}

当需要“没有就创建”的语义时，使用 `GetOrCreate` 或 `GetOrCreateAsync`。这些方法在读取未命中时调用工厂；并发未命中不等于工厂一定只执行一次，不应依赖它完成业务上的唯一写入。下面是数据访问器提供者的真实用途：缓存创建出的访问器，并把其 Disposed 通知作为缓存失效依赖。

来源：[framework/Zongsoft.Core/src/Data/DataAccessProviderBase.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Data/DataAccessProviderBase.cs#L52)（节选；上下文见源文件）。

{% code title="DataAccessProviderBase.cs" %}

```csharp
public TDataAccess GetAccessor(string name, IDataAccessOptions options = null)
{
	if(string.IsNullOrEmpty(name) || options == null || options.Settings == null || !options.Settings.Any())
		name = GetName(name);

	return _accesses.GetOrCreate(name, key =>
	{
		var accessor = this.CreateAccessor(name, options);
		return (accessor, accessor.Disposed);
	});
}
```

{% endcode %}

## 过期策略

`MemoryCache.Expiration` 可以表达滑动过期、绝对过期，或同时表达二者。下表是参数形式参考；其后的实际范例把终端输入 text 写入 Key#序号，使用 EXPIRATION 常量和同一类的 Now 属性记录状态。

| 写法                                                              | 说明                     |
| --------------------------------------------------------------- | ---------------------- |
| `TimeSpan.FromMinutes(10)`                                      | 滑动过期，缓存项在指定时长内未被访问才过期。 |
| `DateTimeOffset.UtcNow.AddHours(1)`                             | 绝对过期，缓存项到指定时间点后过期。     |
| `(TimeSpan.FromMinutes(10), DateTimeOffset.UtcNow.AddHours(1))` | 同时设置滑动过期和绝对过期。         |

来源：[framework/Zongsoft.Core/samples/memorycache/Program.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/samples/memorycache/Program.cs#L72)（节选；上下文见源文件）。

{% code title="Program.cs" %}

```csharp
cache.SetValue($"Key#{++count}", text, TimeSpan.FromSeconds(EXPIRATION), Now);
```

{% endcode %}

## 依赖令牌

缓存项可以依赖 [`IChangeToken`](https://source.dot.net/#Microsoft.Extensions.Primitives/IChangeToken.cs)。当令牌变更时，缓存项会被标记为失效，并触发淘汰回调。

来源：[framework/Zongsoft.Core/test/Caching/MemoryCacheTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Caching/MemoryCacheTest.cs#L38)（节选；上下文见源文件）。

{% code title="MemoryCacheTest.cs" %}

```csharp
public void TestDependency()
{
	var cache = new MemoryCache();
	cache.Evicted += this.Cache_Evicted;

	var cancellation = new CancellationTokenSource();
	var value = cache.GetOrCreate("KEY", key =>
	{
		return ("Value1", new CancellationChangeToken(cancellation.Token));
	});

	Assert.NotNull(value);
	Assert.Equal("Value1", value);
	Assert.Equal(1, cache.Count);

	//通知缓存项过期
	cancellation.Cancel();
	Assert.False(cache.Contains("KEY"));
	Assert.Equal(0, cache.Count);

	//清理缓存
	cache.Compact();

	//等待缓存项过期事件的触发
	Assert.True(SpinWait.SpinUntil(() => Volatile.Read(ref _reason) >= 0, 10_000), "等待缓存项过期事件回调超时。");

	//确认缓存过期的原因
	Assert.Equal(CacheEvictedReason.Depended, (CacheEvictedReason)Volatile.Read(ref _reason));

	//创建一个已经过期的缓存项
	value = cache.GetOrCreate("KEY", key =>
	{
		return ("Value2", new CancellationChangeToken(cancellation.Token));
	});

	Assert.NotNull(value);
	Assert.Equal("Value2", value);
	Assert.False(cache.Contains("KEY"));
	Assert.Equal(0, cache.Count);

	//创建一个依赖失效的缓存项
	value = cache.GetOrCreate("KEY", key =>
	{
		return ("Value3", Common.Notification.GetToken());
	});

	Assert.NotNull(value);
	Assert.Equal("Value3", value);
	Assert.False(cache.Contains("KEY"));
	Assert.Equal(0, cache.Count);
}
```

{% endcode %}

示例中的 [`CancellationChangeToken`](https://source.dot.net/#Microsoft.Extensions.Primitives/CancellationChangeToken.cs) 会把取消令牌转换为缓存依赖令牌。

依赖失效对应的淘汰原因是 `CacheEvictedReason.Depended`。

## 事件

`MemoryCache` 提供两个事件。上面的 TestDependency 依赖同一测试类的 Cache\_Evicted 回调记录原因，并等待异步通知；复用测试时应保留整个夹具。交互范例则在 Limited 回调中主动 Clear：这是范例选择的清理策略，并非缓存内部自动执行。

事件含义如下：

| 事件        | 触发时机                                              |
| --------- | ------------------------------------------------- |
| `Evicted` | 缓存项因为过期、依赖失效、删除、替换或容量压力被淘汰。                       |
| `Limited` | 设置或创建缓存项后，当前数量超过 `MemoryCacheOptions.CountLimit`。 |

来源：[framework/Zongsoft.Core/samples/memorycache/Program.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/samples/memorycache/Program.cs#L96)（节选；上下文见源文件）。

{% code title="Program.cs" %}

```csharp
private static void Cache_Limited(object sender, CacheLimitedEventArgs e)
{
	var content = CommandOutletContent.Create(CommandOutletColor.Magenta, "** Limited **\t")
		.Append(CommandOutletColor.DarkYellow, e.Limit.ToString())
		.Append(CommandOutletColor.DarkGray, "/")
		.Append(CommandOutletColor.DarkYellow, e.Count.ToString());

	Terminal.WriteLine(content);

	//清空缓存
	((MemoryCache)sender).Clear();
}
```

{% endcode %}

{% hint style="warning" %}
`CountLimit` 是 Zongsoft 层面的提醒阈值。超过阈值时会触发 `Limited` 事件，但不会自动删除缓存项；需要应用根据场景调用 `Compact`、`Clear` 或调整缓存写入策略。
{% endhint %}

## 清理与扫描

`Compact` 用于触发底层缓存清理。因为缓存项过期或依赖失效后不一定立即从底层容器中移除，主动调用 `Compact(0)` 可以促使缓存执行扫描。

`MemoryCacheScanner` 封装了一个定时器，按 `MemoryCacheOptions.ScanFrequency` 周期调用 `Compact(0)`。

来源：[framework/Zongsoft.Core/src/Caching/MemoryCacheScanner.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Caching/MemoryCacheScanner.cs#L52)（节选；上下文见源文件）。

{% code title="MemoryCacheScanner.cs" %}

```csharp
public void Start() => _timer.Change(TimeSpan.Zero, _cache.Options.ScanFrequency);
public void Stop() => _timer.Change(Timeout.Infinite, Timeout.Infinite);
```

{% endcode %}

## 相关资源

* [MemoryCache.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Caching/MemoryCache.cs)
* [MemoryCacheOptions.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Caching/MemoryCacheOptions.cs)
* [MemoryCacheScanner.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Caching/MemoryCacheScanner.cs)
* [MemoryCacheTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Caching/MemoryCacheTest.cs)


# 缓冲器

从框架日志实现和现有测试理解批量缓冲、触发条件、并发刷新与数据消费。

Spooler 把连续到达的条目暂存起来，再交给批量回调处理。框架的文件日志器使用它减少逐条写文件的开销。它是进程内缓冲，条目尚未交给持久化系统时，进程退出仍可能造成丢失。

## 真实使用位置：文件日志

来源：[framework/Zongsoft.Core/src/Diagnostics/FileLogger.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Diagnostics/FileLogger.cs#L57)（节选；上下文见源文件）。

{% code title="FileLogger.cs" %}

```csharp
protected FileLogger(TimeSpan period, int capacity, string filePath, int fileLimit = FILE_LIMIT)
{
	this.FilePath = filePath?.Trim();
	this.FileLimit = Math.Max(fileLimit, 0);
	this.Logging = period > TimeSpan.Zero || capacity > 1 ? new(this.OnFlushAsync, period, capacity) : null;
}
```

{% endcode %}

构造函数根据 period 和 capacity 决定是否启用 Logging 缓冲。真正的写入和文件大小管理由日志器的 OnFlushAsync 实现；Spooler 本身只组织条目的暂存和交付。日志业务范例见[诊断日志](/framework/core/diagnostics)。

## 从放入到显式刷新

Discussions 没有直接使用 Spooler，下面采用框架测试。Flusher 是同一文件中的测试接收器，它消费收到的条目并累加数量；TestContext 提供测试取消令牌。

来源：[framework/Zongsoft.Core/test/Caching/SpoolerTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Caching/SpoolerTest.cs#L30)（节选；上下文见源文件）。

{% code title="SpoolerTest.cs" %}

```csharp
public async Task TestFlushAsync()
{
	const int COUNT = 1000;

	var flusher = new Flusher<string>();
	using var spooler = new Spooler<string>(flusher.OnFlushAsync, TimeSpan.FromHours(1));
	Assert.True(spooler.IsEmpty);
	Assert.Equal(0, flusher.Count);

	await spooler.FlushAsync(TestContext.Current.CancellationToken);
	Assert.True(spooler.IsEmpty);
	Assert.Equal(0, flusher.Count);

	#if NET8_0_OR_GREATER
	await Parallel.ForAsync(0, COUNT, TestContext.Current.CancellationToken, async (index, cancellation) => await spooler.PutAsync($"Value#{index}", cancellation));
	#else
	for(int i = 0; i < COUNT; i++)
		await spooler.PutAsync($"Value#${i}", TestContext.Current.CancellationToken);
	#endif

	Assert.Equal(COUNT, spooler.Count);

	await spooler.FlushAsync(TestContext.Current.CancellationToken);
	Assert.True(spooler.IsEmpty);
	Assert.Equal(COUNT, flusher.Count);
}
```

{% endcode %}

PutAsync 完成说明条目已被接收，或者写入容量触发了刷新；它不统一代表外部持久化完成。FlushAsync 则等待本次刷新的回调结束。回调必须真正消费收到的序列，才能把条目从缓冲中取走。

## 容量与周期分别怎样触发

来源：[framework/Zongsoft.Core/test/Caching/SpoolerTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Caching/SpoolerTest.cs#L58)（节选；上下文见源文件）。

{% code title="SpoolerTest.cs" %}

```csharp
public async Task TestLimitAsync()
{
	var flusher = new Flusher<string>();
	using var spooler = new Spooler<string>(flusher.OnFlushAsync, TimeSpan.FromHours(1), 3);
	Assert.True(spooler.IsEmpty);
	Assert.Equal(0, flusher.Count);

	await spooler.PutAsync("A", TestContext.Current.CancellationToken);
	await spooler.PutAsync("B", TestContext.Current.CancellationToken);
	await spooler.PutAsync("C", TestContext.Current.CancellationToken);
	Assert.Equal(3, spooler.Count);
	Assert.Equal(0, flusher.Count);

	//触发数量限制
	await spooler.PutAsync("D", TestContext.Current.CancellationToken);

	Assert.False(spooler.IsEmpty);
	Assert.Equal(1, spooler.Count);
	Assert.Equal(3, flusher.Count);
}
```

{% endcode %}

这个测试把容量设为 3。前三条进入缓冲，第 4 条放入时触发前三条的刷新，随后第 4 条留在缓冲中。不要把容量理解为“第 3 条加入后立即全部落盘”。容量为零时使用无界缓冲，应结合消费速度观察内存增长。

周期触发由内部计时器调用 FlushAsync。在支持修改 Period 的目标框架上，现有 [TestPeriodAsync](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Caching/SpoolerTest.cs) 把一小时周期调整为 1 毫秒，并等待回调完成。这里的时间用于测试，不是推荐部署参数。

## 并发刷新与回调责任

来源：[framework/Zongsoft.Core/test/Caching/SpoolerTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Caching/SpoolerTest.cs#L105)（节选；上下文见源文件）。

{% code title="SpoolerTest.cs" %}

```csharp
public async Task TestConcurrentFlushAsync()
{
	const int COUNT = 256;
	const int CONCURRENCY = 16;

	var flusher = new RecordingFlusher<int>(TimeSpan.FromMilliseconds(10));
	using var spooler = new Spooler<int>(flusher.OnFlushAsync, TimeSpan.FromHours(1));

	for(int i = 0; i < COUNT; i++)
		await spooler.PutAsync(i, TestContext.Current.CancellationToken);

	var tasks = Enumerable.Range(0, CONCURRENCY).Select(_ => spooler.FlushAsync(TestContext.Current.CancellationToken).AsTask()).ToArray();
	await Task.WhenAll(tasks);

	Assert.True(spooler.IsEmpty);
	Assert.Equal(1, flusher.Calls);
	Assert.Equal(1, flusher.MaximumConcurrency);
	Assert.Equal(COUNT, flusher.Count);
	Assert.Equal(Enumerable.Range(0, COUNT), flusher.Values.OrderBy(value => value));
}
```

{% endcode %}

RecordingFlusher 在同一个测试文件中记录并发数、调用次数和条目集合。测试验证多个 FlushAsync 请求不会并发进入回调，且这一批条目只被消费一次。这不等于外部系统具有恰好一次交付语义；超时重试和幂等仍由实际接收端负责。

{% hint style="warning" %}
🚨 Spooler 的枚举会消费缓冲中的条目，刷新回调拿到的序列也按读取取走条目。不要为“查看当前内容”遍历它，也不要在同一回调里先 Count 再第二次遍历进行写入。需要多次使用一批数据时，应在回调内一次性物化，再操作这份本地集合。
{% endhint %}

回调抛异常不会自动把已经读出的条目放回缓冲。需要可靠投递时应使用持久化消息或事务机制，并阅读[消息可靠性](/framework/messaging/reliability)。

## 清空、停机与释放

Clear 取出并丢弃当前条目，不调用刷新回调。Dispose 停止计时器并结束通道，不替代业务要求的最终刷新。停机时先停止生产者，再等待需要的刷新结束，最后释放拥有的实例；无法接受丢失的数据应先持久化。

period 应根据允许延迟确定，limit 应结合单批耗时与内存占用确定。框架 [Spooler.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Caching/Spooler.cs) 给出具体边界，命令行交互用例在 [samples/spooler](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/samples/spooler)。


# Zongsoft.Collections

Zongsoft.Collections 命名空间的职责和主要类型。

`Zongsoft.Collections` 提供集合增强、层级集合、参数集合、同步集合和常用集合扩展，是很多框架内部对象树和配置树的基础。

## 主要职责

* 提供分类、层级、参数、队列、字典等集合类型。
* 提供集合事件、集合工具和枚举扩展。
* 支撑逻辑路径、命名集合、同步访问和只读包装等常见集合场景。

## 类型

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref">页面</th></tr></thead><tbody><tr><td><strong>Category</strong></td><td>分类树、排序分类集合、本地化标题和描述。</td><td><a href="/framework/core/collections/category">分层类</a></td></tr><tr><td><strong>Hierarchical</strong></td><td>层级节点、节点集合、路径查找和层级表达式。</td><td><a href="/framework/core/collections/hierarchical">层次化</a></td></tr><tr><td><strong>Parameters</strong></td><td>字符串键、类型键、链式构建和 JSON 序列化。</td><td><a href="/framework/core/collections/parameters">参数集</a></td></tr><tr><td><strong>同步集合</strong></td><td>线程安全列表和字典包装。</td><td><a href="/framework/core/collections/synchronized">同步集合</a></td></tr><tr><td><strong>集合扩展</strong></td><td>枚举、集合、字典、键值对、哈希集和列表工具。</td><td><a href="/framework/core/collections/extensions">集合扩展</a></td></tr></tbody></table>

## 相关资源

* [Collections 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Collections)


# 分层类

Category 分类树及 CategoryBase、CategoryCollection、CategoryCollectionBase 的用法。

`Category` 表示一个可嵌套的分类节点。它继承自 `CategoryBase<Category>`，通过 `Categories` 属性持有子分类集合，常用于菜单、功能分组、导航分类、模块能力分组等树状结构。

## 类型关系

| 类型                                  | 说明                                         |
| ----------------------------------- | ------------------------------------------ |
| `Category`                          | 具体分类节点，内置 `CategoryCollection` 子集合。        |
| `CategoryBase<TSelf>`               | 分类节点基类，提供标题、描述、图标、排序、标签和资源本地化。             |
| `CategoryCollection`                | `Category` 的默认子分类集合，提供按名称快速新增分类的 `Add` 方法。 |
| `CategoryCollectionBase<TCategory>` | 分类集合基类，负责父节点维护和按 `Ordinal` 自动排序。           |

`Category` 同时也是层级节点，因此它继承了路径、完整路径、根节点判断和路径查找等能力。

## 创建分类树

来源：[framework/Zongsoft.Core/test/Collections/CategoryTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/CategoryTest.cs#L119)（节选；上下文见源文件）。

{% code title="CategoryTest.cs" %}

```csharp
private static Category Initialize()
{
	var root = new Category();

	var file = root.Categories.Add("File");
	var edit = root.Categories.Add("Edit");
	var help = root.Categories.Add("Help");

	file.Categories.Add("Open");
	file.Categories.Add("Close");
	file.Categories.Add("Save");
	file.Categories.Add("SaveAs");
	file.Categories.Add("Recents").Categories.AddRange(
		new Category("Document-1"),
		new Category("Document-2")
	);

	return root;
}
```

{% endcode %}

无参构造的 `Category` 是根节点，名称为 `/`，完整路径也是 `/`。普通节点的名称不能包含 `/`、`\`、`*`、`?`、`!`、`@` 等层级路径非法字符。

## 路径查找

`Category` 支持按层级路径查找节点。路径可以包含空白、相对路径、根路径和父级跳转。

来源：[framework/Zongsoft.Core/test/Collections/CategoryTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/CategoryTest.cs#L36)（节选；上下文见源文件）。

{% code title="CategoryTest.cs" %}

```csharp
public void TestFind()
{
	var root = Initialize();

	var found = root.Find("File");
	Assert.NotNull(found);
	Assert.Equal("File", found.Name);
	Assert.Equal("/", found.Path);
	Assert.Equal("/File", found.FullPath);

	found = root.Find("Edit");
	Assert.NotNull(found);
	Assert.Equal("Edit", found.Name);
	Assert.Equal("/", found.Path);
	Assert.Equal("/Edit", found.FullPath);

	found = root.Find("Help");
	Assert.NotNull(found);
	Assert.Equal("Help", found.Name);
	Assert.Equal("/", found.Path);
	Assert.Equal("/Help", found.FullPath);

	found = root.Find(" File / Save");
	Assert.NotNull(found);
	Assert.Equal("Save", found.Name);
	Assert.Equal("/File", found.Path);
	Assert.Equal("/File/Save", found.FullPath);

	Assert.NotNull(root.Find(" File/ Recents"));
	Assert.NotNull(root.Find(" File /Recents / Document-1"));

	Assert.NotNull(root.Find(" /File/ Save"));
	Assert.NotNull(root.Find("/ File  /Recents"));
	Assert.NotNull(root.Find(" / File  /  Recents/Document-2"));

	found = root.Find("File").Find(" Open");
	Assert.NotNull(found);
	Assert.Equal("Open", found.Name);
	Assert.Equal("/File", found.Path);
	Assert.Equal("/File/Open", found.FullPath);

	found = root.Find("File").Find("./ Recents");
	Assert.NotNull(found);
	Assert.Equal("Recents", found.Name);
	Assert.Equal("/File", found.Path);
	Assert.Equal("/File/Recents", found.FullPath);

	found = root.Find("File").Find("Recents").Find("Document-2  ");
	Assert.NotNull(found);
	Assert.Equal("Document-2", found.Name);
	Assert.Equal("/File/Recents", found.Path);
	Assert.Equal("/File/Recents/Document-2", found.FullPath);

	found = root.Find("Edit").Find(".. / File / Save");
	Assert.NotNull(found);
	Assert.Equal("Save", found.Name);
	Assert.Equal("/File", found.Path);
	Assert.Equal("/File/Save", found.FullPath);
}
```

{% endcode %}

`Find` 返回匹配节点，找不到时返回 `null`。路径分段前后的空白会被忽略。

## 排序

`CategoryBase<TSelf>.Ordinal` 表示分类的排列顺序。向 `CategoryCollectionBase<TCategory>` 添加节点时，集合会按 `Ordinal` 插入到合适位置。

来源：[framework/Zongsoft.Core/test/Collections/CategoryTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/CategoryTest.cs#L97)（节选；上下文见源文件）。

{% code title="CategoryTest.cs" %}

```csharp
public void TestOrdinal()
{
	const int COUNT = 100;

	var root = new Category();

	for(int i = 0; i < COUNT; i++)
	{
		var ordinal = Random.Shared.Next() % COUNT;
		root.Categories.Add(new Category($"A{(i + 1):000}") { Ordinal = ordinal });
	}

	for(int i = 1; i < root.Categories.Count; i++)
	{
		Assert.NotNull(root.Categories[i]);
		Assert.NotNull(root.Categories[i - 1]);
		Assert.True(root.Categories[i].Ordinal >= root.Categories[i - 1].Ordinal);
	}
}
```

{% endcode %}

测试随机设置 100 个节点的 Ordinal，随后逐项验证排序值不递减。上面的 Initialize 和 TestFind 属于同一测试夹具；复用代码时需要保留它们的关系。

## 本地化标题和描述

`CategoryBase<TSelf>` 可以接收 `Zongsoft.Resources.IResource`。当 `Title` 或 `Description` 没有显式设置时，会按完整路径和节点名称查找资源键。

`Title` 的资源键查找顺序包含：

* `{path}.Category.Title`
* `{path}.Category`
* `{path}.Title`
* `{path}`
* `{name}.Category.Title`
* `{name}.Category`
* `{name}.Title`
* `{name}`

`Description` 的资源键查找顺序包含：

* `{path}.Category.Description`
* `{path}.Description`
* `{name}.Category.Description`
* `{name}.Description`

显式设置 `Title`、`Description`、`Icon`、`Tags` 时，会触发属性变更通知。

## 相关资源

* [Category.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/Category.cs)
* [CategoryBase.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/CategoryBase.cs)
* [CategoryCollection.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/CategoryCollection.cs)
* [CategoryCollectionBase.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/CategoryCollectionBase.cs)
* [CategoryTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/CategoryTest.cs)


# 层次化

Hierarchical 层级节点、节点集合、路径查找和层级表达式。

`Hierarchical` 相关类型提供树状节点、节点集合、路径查找和表达式解析能力。`Category`、配置树、选项树等需要“按路径定位节点”的模型，都可以复用这套基础设施。

## 类型关系

| 类型                                  | 说明                                 |
| ----------------------------------- | ---------------------------------- |
| `HierarchicalNode`                  | 非泛型层级节点基类，提供名称、路径、完整路径和非法字符约束。     |
| `HierarchicalNode<TNode>`           | 泛型层级节点基类，提供父节点、子节点集合和 `Find` 查找逻辑。 |
| `HierarchicalNodeCollection<TNode>` | 基于名称键的子节点集合，忽略大小写，禁止添加根节点。         |
| `HierarchicalNodeUtility`           | 提供 `IsRoot` 等层级节点扩展方法。             |
| `HierarchicalExpression`            | 表示“层级路径 + 成员访问表达式”的解析结果。           |
| `HierarchicalExpressionParser`      | 解析层级表达式文本。                         |

## 层级节点

层级节点以 `/` 作为路径分隔符。根节点名称为 `/`，根节点的 `Path` 为空字符串，`FullPath` 为 `/`。

来源：[framework/Zongsoft.Core/test/Collections/CategoryTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/CategoryTest.cs#L11)（节选；上下文见源文件）。

{% code title="CategoryTest.cs" %}

```csharp
public void TestName()
{
	var root = Initialize();

	Assert.True(root.IsRoot());
	Assert.Equal("/", root.Name);
	Assert.Equal(string.Empty, root.Path);
	Assert.Equal("/", root.FullPath);

	var category = new Category();
	Assert.True(root.IsRoot());

	Assert.IsType<ArgumentException>(Record.Exception(() => root.Categories.Add(category)));
	Assert.IsType<ArgumentException>(Record.Exception(() => new Category("/")));
	Assert.IsType<ArgumentException>(Record.Exception(() => new Category("ABC\\")));
	Assert.IsType<ArgumentException>(Record.Exception(() => new Category("ABC/DEF")));
	Assert.IsType<ArgumentNullException>(Record.Exception(() => new Category(string.Empty)));
	Assert.IsType<ArgumentNullException>(Record.Exception(() => new Category(" ")));
	Assert.IsType<ArgumentNullException>(Record.Exception(() => new Category("\t")));
	Assert.IsType<ArgumentNullException>(Record.Exception(() => new Category("\n")));
	Assert.IsType<ArgumentNullException>(Record.Exception(() => new Category("\r")));
	Assert.IsType<ArgumentNullException>(Record.Exception(() => new Category(Environment.NewLine)));
}
```

{% endcode %}

节点名称不能为空，不能包含路径分隔符和层级表达式保留字符。

## 路径查找

`HierarchicalNode<TNode>.Find` 支持以下路径形式：

| 路径     | 说明             |
| ------ | -------------- |
| `/A/B` | 从根节点开始查找。      |
| `A/B`  | 从当前节点的子节点开始查找。 |
| `./A`  | 从当前节点开始查找。     |
| `../A` | 从父节点开始查找。      |
| 空字符串   | 返回当前节点。        |

完整的创建与查找过程见 [Category 的真实测试范例](https://docs.zongsoft.com/framework/core/collections/pages/OYazPDInHv2Y5bAr3sGw#路径查找)，这里复用同一棵 File / Edit / Help 分类树。

路径分段两端的空白会被忽略。找不到节点时返回 `null`。

## 层级表达式

`HierarchicalExpression` 用来解析“节点路径 + 成员访问器”。表达式由路径和可选成员访问两部分组成。

来源：[framework/Zongsoft.Core/test/Collections/HierarchicalExpressionTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/HierarchicalExpressionTest.cs#L16)（节选；上下文见源文件）。

{% code title="HierarchicalExpressionTest.cs" %}

```csharp
var TEXT = @"/";
var expression = HierarchicalExpressionParser.Parse(TEXT);

Assert.NotNull(expression);
Assert.Null(expression.Accessor);
Assert.Equal(PathAnchor.Root, expression.Anchor);
Assert.Equal("/", expression.Path);
Assert.True(expression.Segments == null || expression.Segments.Length == 0);
```

{% endcode %}

常见格式包括：

* `/root/node@property`
* `../sibling/node@property`
* `child/node[index]`
* `@property1.property2`

成员访问部分由 `Zongsoft.Reflection.Expressions` 解析，因此可以表达属性、索引器和链式成员访问。

## 相关资源

* [HierarchicalExpression.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/HierarchicalExpression.cs)
* [HierarchicalExpressionParser.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/HierarchicalExpressionParser.cs)
* [HierarchicalNode.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/HierarchicalNode.cs)
* [HierarchicalNode\<T>.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/HierarchicalNode%601.cs)
* [HierarchicalNodeCollection.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/HierarchicalNodeCollection.cs)
* [HierarchicalNodeUtility.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/HierarchicalNodeUtility.cs)
* [HierarchicalExpressionTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/HierarchicalExpressionTest.cs)


# 参数集

Parameters 参数包、类型键、字符串键、链式构建和 JSON 序列化。

`Parameters` 是一个轻量参数包，既可以按字符串名称保存参数，也可以按 [`Type`](https://learn.microsoft.com/zh-cn/dotnet/api/system.type) [*源码*](https://source.dot.net/#System.Private.CoreLib/Type.cs) 保存参数。它常用于命令调用、服务上下文、数据访问选项、事件参数和跨层传递少量上下文数据。

## 类型关系

| 类型                  | 说明                                                                                                                                                                                                    |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Parameters`        | 参数容器，实现 [`IDictionary<object, object>`](https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.generic.idictionary-2) [*源码*](https://source.dot.net/#System.Private.CoreLib/IDictionary.cs)。 |
| `Parameters.Json`   | `Parameters` 的 JSON 转换器实现。                                                                                                                                                                            |
| `ParametersUtility` | 提供链式添加参数的扩展方法。                                                                                                                                                                                        |

## 字符串键与并发初始化

字符串键忽略大小写。`null` 名称会被转换为空字符串。

来源：[framework/Zongsoft.Core/test/Collections/ParametersTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/ParametersTest.cs#L18)（节选；上下文见源文件）。

{% code title="ParametersTest.cs" %}

```csharp
public async Task GetOrAdd_ConcurrentSameName_CreatesSingleValueAsync()
{
	const int COUNT = 64;
	var parameters = new Parameters();
	var factoryCalls = 0;
	var start = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
	var tasks = new Task<object>[COUNT];

	for(var index = 0; index < tasks.Length; index++)
	{
		tasks[index] = Task.Run(async () =>
		{
			await start.Task;
			return parameters.GetOrAdd("Shared", _ =>
			{
				Interlocked.Increment(ref factoryCalls);
				return new object();
			});
		});
	}

	start.SetResult();
	var results = await Task.WhenAll(tasks);

	Assert.Equal(1, factoryCalls);
	Assert.Single(parameters);
	Assert.Same(results[0], parameters.GetValue("shared"));
	Assert.All(results, result => Assert.Same(results[0], result));
}
```

{% endcode %}

`TryGetValue<TValue>(string name, out TValue value)` 会尝试将原始值转换为目标类型。

## 类型键与值复用

按类型保存参数时，可以直接通过泛型读取。读取时如果没有找到完全匹配的类型键，`Parameters` 会查找可赋值的类型键。

来源：[framework/Zongsoft.Core/test/Collections/ParametersTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/ParametersTest.cs#L49)（节选；上下文见源文件）。

{% code title="ParametersTest.cs" %}

```csharp
public void GetOrAdd_TypeKey_ReusesExistingValueWithoutCallingFactory()
{
	var parameters = new Parameters();
	var expected = parameters.GetOrAdd(() => new ParameterMarker());

	var actual = parameters.GetOrAdd<ParameterMarker>(() => throw new InvalidOperationException("The existing value must win."));

	Assert.Same(expected, actual);
	Assert.Same(expected, parameters.GetValue<ParameterMarker>());
}
```

{% endcode %}

按类型设置参数时，值必须能赋给声明类型；如果声明类型是非空值类型，则不能设置为 `null`。

## 链式构建

`ParametersUtility` 让已有参数包可以继续链式追加参数。

Discussions 的 PostService.OnInsert 读取 options.Parameters 中名为 Thread 的关联对象，用于区分主题内容贴与普通回帖。这个分支的调用方必须显式传入关联对象；映射中存在 Post 复合属性本身并不会自动填充 Parameters。参阅 [帖子写入](/framework/data/writing)。

`Append` 可以把另一个 `Parameters` 中的条目合并到当前实例。

## JSON 序列化

下面节选同一测试文件中的 TestJson。parameters 来自该方法前半部分构造的参数包，涵盖有名参数、类型键、时间值、字节数组和 IPerson 测试模型；不是一个独立的顶层程序。类型键测试中的 ParameterMarker 也定义在同一文件中。

`Parameters.Json` 为 `Parameters` 提供 JSON 转换器。字符串键按属性名输出，类型键会使用 `$` 前缀加类型别名输出。

来源：[framework/Zongsoft.Core/test/Collections/ParametersTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/ParametersTest.cs#L107)（节选；上下文见源文件）。

{% code title="ParametersTest.cs" %}

```csharp
var json = Serializer.Json.Serialize(parameters);
Assert.NotNull(json);
Assert.NotEmpty(json);

var result = Serializer.Json.Deserialize<Parameters>(json);
Assert.NotNull(result);
Assert.NotEmpty(result);
```

{% endcode %}

复杂对象会以包含 `$type` 和 `$value` 的对象形式保存，读取时再按类型别名恢复值。

## 相关资源

* [Parameters.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/Parameters.cs)
* [Parameters.Json.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/Parameters.Json.cs)
* [ParametersUtility.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/ParametersUtility.cs)
* [ParametersTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/ParametersTest.cs)


# 同步集合

SynchronizedList 和 SynchronizedDictionary 同步集合。

同步集合用于包装常规列表或字典，并通过读写锁保护并发访问。它们适合框架内部少量共享状态、缓存索引、注册表、描述符集合等场景。

## 类型关系

| 类型                                     | 说明                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SynchronizedList<T>`                  | 线程安全列表，实现 [`IList<T>`](https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.generic.ilist-1) [*源码*](https://source.dot.net/#System.Private.CoreLib/IList.cs)、[`ICollection<T>`](https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.generic.icollection-1) [*源码*](https://source.dot.net/#System.Private.CoreLib/ICollection.cs) 和非泛型 [`ICollection`](https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.icollection) [*源码*](https://source.dot.net/#System.Private.CoreLib/ICollection.cs)。 |
| `SynchronizedDictionary<TKey, TValue>` | 线程安全字典，实现 [`IDictionary<TKey, TValue>`](https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.generic.idictionary-2) [*源码*](https://source.dot.net/#System.Private.CoreLib/IDictionary.cs)，并提供并发风格的增改方法。                                                                                                                                                                                                                                                                                                               |
| `ListUtility`                          | 提供 `IList<T>.Synchronize()` 包装方法。                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `DictionaryExtension`                  | 提供 `IDictionary<TKey, TValue>.Synchronize()` 包装方法。                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |

## SynchronizedList

`SynchronizedList<T>` 使用读写锁保护读取和写入。读操作进入读锁，写操作进入写锁，枚举期间也保持读锁。

来源：[framework/Zongsoft.Core/test/Collections/SynchronizedListTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/SynchronizedListTest.cs#L13)（节选；上下文见源文件）。

{% code title="SynchronizedListTest.cs" %}

```csharp
public void Add()
{
	const int COUNT = 1_0000;

	var list = new SynchronizedList<int>(COUNT);

	Parallel.For(0, COUNT, index =>
	{
		list.Add(index);

		if(index % 100 == 0)
		{
			foreach(var item in list)
			{
				Assert.True(item >= 0);
			}

			for(int i = 0; i < list.Count; i++)
			{
				Assert.True(list[i] >= 0);
			}
		}
	});

	Assert.Equal(COUNT, list.Count);
	var hashset = new HashSet<int>(list);
	Assert.Equal(COUNT, hashset.Count);
}
```

{% endcode %}

也可以用扩展方法包装已有列表。

现有列表的包装入口是 ListUtility.Synchronize；包装后应统一经由返回对象访问，共享代码若继续直接修改原列表，就会绕过锁。

{% hint style="warning" %}
枚举 `SynchronizedList<T>` 时会持有读锁。不要在枚举循环内部执行可能长时间阻塞的操作，也不要在同一枚举过程中写入该列表。
{% endhint %}

## SynchronizedDictionary

`SynchronizedDictionary<TKey, TValue>` 除了常规字典操作，还提供类似并发字典的 `TryAdd`、`GetOrAdd`、`AddOrUpdate`、`TryUpdate` 方法。

来源：[framework/Zongsoft.Core/test/Collections/SynchronizedDictionaryTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/SynchronizedDictionaryTest.cs#L139)（节选；上下文见源文件）。

{% code title="SynchronizedDictionaryTest.cs" %}

```csharp
public void GetOrAdd()
{
	const int COUNT = 1_0000;

	var keys = System.Linq.Enumerable.Range(0, COUNT).ToArray();
	Random.Shared.Shuffle(keys);

	var dictionary = new SynchronizedDictionary<int, string>(COUNT);
	for(int i = 0; i < COUNT; i++)
	{
		if(i % 2 == 1)
			dictionary[i] = $"Value#{i}";
	}

	Assert.Equal(COUNT / 2, dictionary.Count);

	Parallel.For(0, COUNT, index =>
	{
		var key = keys[index];
		var value = dictionary.GetOrAdd(key, key => $"Added#{key}");

		if(key % 2 == 0)
			Assert.Equal($"Added#{key}", value);
		else
			Assert.Equal($"Value#{key}", value);

		if(index % 100 == 0)
		{
			foreach(var item in dictionary)
			{
				Assert.True(item.Key >= 0);
				Assert.True(dictionary.ContainsKey(item.Key));
			}
		}
	});

	Assert.Equal(COUNT, dictionary.Count);
}
```

{% endcode %}

`GetOrAdd` 和 `AddOrUpdate` 使用可升级读锁减少写锁范围。`Keys` 和 `Values` 返回的是快照集合，避免调用方拿到内部集合视图。

## 选择建议

| 场景                                                                                                                                                                                                      | 建议                                         |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| 需要完整 [`IList<T>`](https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.generic.ilist-1) [*源码*](https://source.dot.net/#System.Private.CoreLib/IList.cs) 语义并控制并发访问                            | 使用 `SynchronizedList<T>`。                  |
| 需要完整 [`IDictionary<TKey, TValue>`](https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.generic.idictionary-2) [*源码*](https://source.dot.net/#System.Private.CoreLib/IDictionary.cs) 语义和条件更新 | 使用 `SynchronizedDictionary<TKey, TValue>`。 |
| 高吞吐无锁或细粒度并发字典场景                                                                                                                                                                                         | 优先评估 .NET 官方并发集合。                          |

## 相关资源

* [SynchronizedList.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/SynchronizedList.cs)
* [SynchronizedDictionary.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/SynchronizedDictionary.cs)
* [SynchronizedListTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/SynchronizedListTest.cs)
* [SynchronizedDictionaryTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/SynchronizedDictionaryTest.cs)


# 集合扩展

Enumerable、CollectionUtility、DictionaryUtility 以及集合扩展方法。

集合扩展类型提供一组偏底层的工具方法，用于处理同步/异步枚举、反射式集合操作、字典转换、键值对构建和同步包装。

## 类型关系

| 类型                      | 说明                                                                                                                                                                                         |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Enumerable`            | 同步/异步枚举适配、空枚举、单值枚举和类型化枚举。                                                                                                                                                                  |
| `CollectionUtility`     | 对未知集合对象执行反射式 `Add` 和 `Remove`。                                                                                                                                                             |
| `DictionaryUtility`     | 对未知字典对象执行反射式 `Add`、`Remove` 和键值提取。                                                                                                                                                         |
| `KeyValuePairExtension` | 创建键值对数组，并在键值对数组和对象数组之间转换。                                                                                                                                                                  |
| `DictionaryExtension`   | 字典读取、转换、同步包装和枚举转字典项。                                                                                                                                                                       |
| `HashSetExtension`      | 将 [`HashSet<T>`](https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.generic.hashset-1) [*源码*](https://source.dot.net/#System.Private.CoreLib/HashSet.cs) 复制为数组。               |
| `ListUtility`           | 将 [`IList<T>`](https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.generic.ilist-1) [*源码*](https://source.dot.net/#System.Private.CoreLib/IList.cs) 包装为 `SynchronizedList<T>`。 |

## Enumerable

`Enumerable` 主要处理“对象可能是一个值，也可能是同步枚举，也可能是异步枚举”的场景。

来源：[framework/Zongsoft.Core/test/Collections/EnumerableTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/EnumerableTest.cs#L241)（节选；上下文见源文件）。

{% code title="EnumerableTest.cs" %}

```csharp
public async Task EnumerateAdapters_PrioritizeCollectionsAndConvertScalarValues()
{
	var items = new List<int>([1, 2]);
	var syncObjects = Enumerable.Enumerate<object>(items);
	var asyncObjects = Enumerable.EnumerateAsync<object>(items);
	var syncDecimals = Enumerable.Enumerate<decimal>(10);
	var asyncDecimals = Enumerable.EnumerateAsync<decimal>(10);

	Assert.Collection(syncObjects,
		item => Assert.Equal(1, item),
		item => Assert.Equal(2, item));
	Assert.Collection(await CollectAsync(asyncObjects),
		item => Assert.Equal(1, item),
		item => Assert.Equal(2, item));
	Assert.Equal([10m], syncDecimals);
	Assert.Equal([10m], await CollectAsync(asyncDecimals));
}
```

{% endcode %}

常用能力包括：

* `Empty(Type)`：按运行时类型创建空枚举。
* `Empty<T>()`：创建空异步枚举。
* `First<T>()`、`FirstOrDefault<T>()`：异步取得首元素，并传递取消和释放枚举器；空序列时前者抛出异常，后者返回该元素类型的默认值。Discussions 的真实用法见[异步查询](/framework/data/querying)。
* `Asynchronize<T>()`：把同步枚举包装为异步枚举。
* `Synchronize<T>()`：把异步枚举转为阻塞同步枚举。
* `Enumerate<T>(object)`：把对象转换为 [`IEnumerable<T>`](https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.generic.ienumerable-1) [*源码*](https://source.dot.net/#System.Private.CoreLib/IEnumerable.cs)。
* `EnumerateAsync<T>(object)`：把对象转换为 [`IAsyncEnumerable<T>`](https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.generic.iasyncenumerable-1) [*源码*](https://source.dot.net/#System.Private.CoreLib/IAsyncEnumerable.cs)。

这些方法优先把同步或异步集合识别为序列，只有非集合对象才按单值进行类型转换；单值枚举器只借用该对象，释放枚举器不会释放元素本身。

`Asynchronize<T>` 和 `EnumerateAsync<T>` 只进行枚举形态适配，不会把同步枚举安排到后台线程。异步枚举器收到取消标记后会抛出 [`OperationCanceledException`](https://learn.microsoft.com/zh-cn/dotnet/api/system.operationcanceledexception) [*源码*](https://source.dot.net/#System.Private.CoreLib/OperationCanceledException.cs)；`EnumerateAsync<T>` 同时收到方法级和枚举器级取消标记时，两者任一取消都会终止枚举。`Synchronize<T>` 会阻塞当前线程等待异步枚举，异步调用链中应优先直接使用 `await foreach`。

当源序列实现 `IPageable` 时，`Asynchronize<T>`、`EnumerateAsync<T>` 和 `Synchronize<T>` 的适配结果继续实现 `IPageable`，并转发动态的 `Suppressed` 状态和 `Paginated` 事件；事件 sender 为调用方持有的外层结果对象。

## CollectionUtility

`CollectionUtility` 用于目标对象类型不确定，但可能实现了 [`ICollection<T>`](https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.generic.icollection-1) [*源码*](https://source.dot.net/#System.Private.CoreLib/ICollection.cs) 或非泛型 [`IList`](https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.ilist) [*源码*](https://source.dot.net/#System.Private.CoreLib/IList.cs) 的场景。

来源：[framework/Zongsoft.Core/test/Collections/CollectionTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/CollectionTest.cs#L12)（节选；上下文见源文件）。

{% code title="CollectionTest.cs" %}

```csharp
public void TestGenericCollection()
{
	object list = new List<int>();

	Assert.True(CollectionUtility.TryAdd(list, 1));
	Assert.NotEmpty((ICollection<int>)list);
	Assert.Equal(1, ((IList<int>)list)[0]);

	Assert.True(CollectionUtility.TryAdd(list, 2));
	Assert.Equal(2, ((IList<int>)list).Count);
	Assert.Equal(1, ((IList<int>)list)[0]);
	Assert.Equal(2, ((IList<int>)list)[1]);

	Assert.True(CollectionUtility.TryRemove(list, 1));
	Assert.Single((IList<int>)list);
	Assert.True(CollectionUtility.TryRemove(list, 2));
	Assert.Empty((IList<int>)list);
}
```

{% endcode %}

它会尝试把传入值转换成集合元素类型，再调用对应的 `Add` 或 `Remove`。

## DictionaryUtility

`DictionaryUtility` 用于目标对象类型不确定，但可能实现了 [`IDictionary<TKey, TValue>`](https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.generic.idictionary-2) [*源码*](https://source.dot.net/#System.Private.CoreLib/IDictionary.cs) 或非泛型 [`IDictionary`](https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.idictionary) [*源码*](https://source.dot.net/#System.Private.CoreLib/IDictionary.cs) 的场景。

来源：[framework/Zongsoft.Core/test/Collections/DictionaryTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/DictionaryTest.cs#L12)（节选；上下文见源文件）。

{% code title="DictionaryTest.cs" %}

```csharp
public void TestGenericDictionary()
{
	object dictionary = new Dictionary<string, int>();

	Assert.True(DictionaryUtility.TryAdd(dictionary, "K1", 1));
	Assert.NotEmpty((IDictionary<string, int>)dictionary);
	Assert.Equal(1, ((IDictionary<string, int>)dictionary)["K1"]);

	Assert.True(DictionaryUtility.TryAdd(dictionary, "K2", 2));
	Assert.Equal(2, ((IDictionary<string, int>)dictionary).Count);
	Assert.Equal(1, ((IDictionary<string, int>)dictionary)["K1"]);
	Assert.Equal(2, ((IDictionary<string, int>)dictionary)["K2"]);

	Assert.True(DictionaryUtility.TryRemove(dictionary, "K1"));
	Assert.Single((IDictionary<string, int>)dictionary);
	Assert.True(DictionaryUtility.TryRemove(dictionary, "K2"));
	Assert.Empty((IDictionary<string, int>)dictionary);
}
```

{% endcode %}

`TryGetEntry` 可以从 [`DictionaryEntry`](https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.dictionaryentry) [*源码*](https://source.dot.net/#System.Private.CoreLib/DictionaryEntry.cs) 或 [`KeyValuePair<TKey, TValue>`](https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.generic.keyvaluepair-2) [*源码*](https://source.dot.net/#System.Private.CoreLib/KeyValuePair.cs) 中提取键和值。

## 字典扩展

`DictionaryExtension` 提供非泛型字典的 `TryGetValue`、字典类型转换和同步包装。

数据服务、配置绑定等反射调用方可使用这些扩展。具体转换规则见 [DictionaryExtension 源码](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/DictionaryExtension.cs)。

泛型字典可以通过 `Synchronize` 包装为 `SynchronizedDictionary<TKey, TValue>`。

## 键值对与哈希集扩展

`KeyValuePairExtension.CreatePairs` 可以按键名数组和值数组创建键值对数组。

需要构造键值对时，键数组与值数组必须按同一顺序组织；其长度检查及组合方式见 [KeyValuePairExtension 源码](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/KeyValuePairExtension.cs)。

`HashSetExtension.ToArray` 会创建一个新数组并调用 `HashSet<T>.CopyTo`，适合需要数组快照的场景。

## 相关资源

* [Enumerable.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/Enumerable.cs)
* [CollectionUtility.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/CollectionUtility.cs)
* [DictionaryUtility.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/DictionaryUtility.cs)
* [KeyValuePairExtension.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/KeyValuePairExtension.cs)
* [DictionaryExtension.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/DictionaryExtension.cs)
* [HashSetExtension.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/HashSetExtension.cs)
* [ListUtility.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/ListUtility.cs)
* [EnumerableTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/EnumerableTest.cs)
* [DictionaryTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Collections/DictionaryTest.cs)


# Zongsoft.Common

Zongsoft.Common 命名空间的职责和主要类型。

`Zongsoft.Common` 放置[核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core)中最通用的工具类型和扩展方法，包括类型转换、随机、校验、序列、时间、字符串、URI 和类型别名等基础能力。

## 主要职责

* 提供增强版 `Convert`、`Randomizer`、`EnumUtility` 等常用工具。
* 提供字符串、数组、日期时间、类型、URI 等扩展方法。
* 提供校验码、顺序号、别名集合、位向量和注解工具。
* 为上层模块减少对重复小工具类的散落依赖。

## 类型

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref">页面</th></tr></thead><tbody><tr><td><strong>AggregateExceptionUtility</strong></td><td>聚合异常中的特定异常处理。</td><td><a href="/framework/core/common/aggregate-exception-utility">聚合异常</a></td></tr><tr><td><strong>AnnotationUtility</strong></td><td>读取成员分类、显示名称和描述注解。</td><td><a href="/framework/core/common/annotation-utility">注解工具</a></td></tr><tr><td><strong>BitVector</strong></td><td>32 位和 64 位位标记。</td><td><a href="/framework/core/common/bit-vector">位向量</a></td></tr><tr><td><strong>Buffer</strong></td><td>内存租赁、编码解码和二进制读取。</td><td><a href="/framework/core/common/buffer">缓冲工具</a></td></tr><tr><td><strong>Checksum</strong></td><td>哈希校验值计算、解析和验证。</td><td><a href="/framework/core/common/checksum">校验码</a></td></tr><tr><td><strong>Convert</strong></td><td>类型转换和十六进制转换。</td><td><a href="/framework/core/common/convert">类型转换</a></td></tr><tr><td><strong>EnumUtility</strong></td><td>枚举项元数据、别名和描述。</td><td><a href="/framework/core/common/enum-utility">枚举工具</a></td></tr><tr><td><strong>HierarchyVector32</strong></td><td>四级层级编码和父子关系判断。</td><td><a href="/framework/core/common/hierarchy-vector32">分层向量</a></td></tr><tr><td><strong>Locker</strong></td><td>同步和异步互斥锁。</td><td><a href="/framework/core/common/locker">异步锁</a></td></tr><tr><td><strong>Notification</strong></td><td>变更令牌和立即失效令牌。</td><td><a href="/framework/core/common/notification">通知</a></td></tr><tr><td><strong>OperationException</strong></td><td>带原因码的操作异常。</td><td><a href="/framework/core/common/operation-exception">操作异常</a></td></tr><tr><td><strong>Predication</strong></td><td>异步条件断言、断言基类和组合集合。</td><td><a href="/framework/core/common/predication">断言</a></td></tr><tr><td><strong>Randomizer</strong></td><td>随机字节、数字和字符串生成。</td><td><a href="/framework/core/common/randomizer">随机器</a></td></tr><tr><td><strong>Sequence</strong></td><td>序列号接口和号段增长器。</td><td><a href="/framework/core/common/sequence">序号器</a></td></tr><tr><td><strong>Timer</strong></td><td>周期任务计时器。</td><td><a href="/framework/core/common/timer">计时器</a></td></tr><tr><td><strong>Timestamp</strong></td><td>时间戳转换。</td><td><a href="/framework/core/common/timestamp">时间戳</a></td></tr><tr><td><strong>TypeAlias</strong></td><td>类型别名解析和生成。</td><td><a href="/framework/core/common/type-alias">类型别名</a></td></tr><tr><td><strong>Validator</strong></td><td>同步和异步数据有效性验证接口。</td><td><a href="/framework/core/common/validator">验证器</a></td></tr><tr><td><strong>常用扩展</strong></td><td>数组、字符串、时间、类型和 URI 扩展。</td><td><a href="/framework/core/common/extensions">常用扩展</a></td></tr></tbody></table>

## 相关资源

* [Common 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Common)


# 聚合异常

AggregateExceptionUtility 聚合异常处理工具。

`AggregateExceptionUtility` 提供 `Handle<TException>` 扩展方法，用于从 [`AggregateException`](https://learn.microsoft.com/zh-cn/dotnet/api/system.aggregateexception) [*源码*](https://source.dot.net/#System.Private.CoreLib/AggregateException.cs) 中查找指定异常类型并调用处理函数。

来源：[framework/externals/wechat/gateway/Controllers/FallbackController.cs](https://github.com/Zongsoft/framework/blob/main/externals/wechat/gateway/Controllers/FallbackController.cs#L72)（节选；上下文见源文件）。

{% code title="FallbackController.cs" %}

```csharp
return (IActionResult)ae.Handle<OperationException>(ex => ex.Reason switch
{
	nameof(OperationException.Unfound) => this.NotFound(new { ex.Reason, ex.Message }),
	nameof(OperationException.Unsupported) => this.BadRequest(new { ex.Reason, ex.Message }),
	nameof(OperationException.Unprocessed) => this.UnprocessableEntity(new { ex.Reason, ex.Message }),
	nameof(OperationException.Unsatisfied) => this.StatusCode(StatusCodes.Status412PreconditionFailed, new { ex.Reason, ex.Message }),
	_ => this.StatusCode(StatusCodes.Status500InternalServerError, new { ex.Reason, ex.Message }),
});
```

{% endcode %}

## 实际调用与返回值

Discussions 未直接使用这个工具；上面是微信回调控制器捕获 System.AggregateException 后的真实处理片段。它把内部 OperationException 的 Reason 转为对应 HTTP 结果，返回对象就是扩展方法的返回值。完整控制器还分别处理直接抛出的 OperationException。

## 匹配边界

工具先 Flatten 展开嵌套异常，再寻找第一个匹配类型。找到后立即调用委托并返回，不会继续遍历剩余异常；委托返回 true 也不表示“处理完所有异常”。如果没有任何匹配项，会把未处理项重新组成聚合异常抛出。输入异常为空时返回空；处理委托为空则抛出参数异常。

{% hint style="warning" %}
🚨 这与 System.AggregateException 自带的逐项 Handle 语义不同。需要完整记录批处理的所有失败时，应保留原始异常集合；也不要假设 await 一个失败任务总会抛出聚合异常。本文采用控制器已经捕获聚合异常后的处理分支。
{% endhint %}

## 相关资源

* [AggregateExceptionUtility.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/AggregateExceptionUtility.cs)


# 注解工具

AnnotationUtility 成员注解读取工具。

`AnnotationUtility` 用于从类型成员读取显示相关注解，包括分类、显示名称和描述。

| 方法               | 说明                                                                                                                                                                                                                                              |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GetCategory`    | 读取 [`CategoryAttribute`](https://learn.microsoft.com/zh-cn/dotnet/api/system.componentmodel.categoryattribute) [*源码*](https://source.dot.net/#System.ComponentModel.Primitives/CategoryAttribute.cs)。                                           |
| `GetDisplayName` | 读取 [`DisplayNameAttribute`](https://learn.microsoft.com/zh-cn/dotnet/api/system.componentmodel.displaynameattribute) [*源码*](https://source.dot.net/#System.ComponentModel.Primitives/DisplayNameAttribute.cs) 或 `DisplayAttribute.Name`。        |
| `GetDescription` | 读取 [`DescriptionAttribute`](https://learn.microsoft.com/zh-cn/dotnet/api/system.componentmodel.descriptionattribute) [*源码*](https://source.dot.net/#System.ComponentModel.Primitives/DescriptionAttribute.cs) 或 `DisplayAttribute.Description`。 |

来源：[framework/externals/aliyun/src/Telecom/PhoneTransmitter.cs](https://github.com/Zongsoft/framework/blob/main/externals/aliyun/src/Telecom/PhoneTransmitter.cs#L71)（节选；上下文见源文件）。

{% code title="PhoneTransmitter.cs" %}

```csharp
_descriptor = new TransmitterDescriptor(this.Name, AnnotationUtility.GetDisplayName(this.GetType()), AnnotationUtility.GetDescription(this.GetType()));
```

{% endcode %}

## 从注解到显示元数据

上面是框架 Aliyun PhoneTransmitter 创建发送器描述符的实际代码。该类型使用 DisplayName 和 Description 特性声明资源键，AnnotationUtility 读取注解并配合资源机制得到标题与说明。完整示例见[模板发送器](/framework/core/communication/transmitter)，资源查找见[资源管理](/framework/core/resources)。Discussions 的模型字段和枚举也依赖资源描述，但没有这段发送器代码。

## 什么时候使用

当模型、命令或配置项的说明由类型声明统一维护时，读取注解能避免在每个界面重复保存名称。注解负责展示元数据，不参与数据库列名匹配，也不能代替参数验证。缺失注解或资源时应按调用方显示约定处理，不要把返回的标题用于权限键或持久标识。

## 相关资源

* [AnnotationUtility.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/AnnotationUtility.cs)


# 位向量

BitVector32 和 BitVector64 位标记结构。

`BitVector32` 和 `BitVector64` 用于把多个布尔状态压缩到一个整数值中，适合保存标志位、状态位和权限位这类低成本标记。

## 类型关系

| 类型            | 说明                |
| ------------- | ----------------- |
| `BitVector32` | 使用 32 位整数保存一组位标记。 |
| `BitVector64` | 使用 64 位整数保存一组位标记。 |

## BitVector32 和 BitVector64

位向量可以把多个布尔状态压缩到一个整数中。索引器中的 `bit` 参数表示位掩码，不是从零开始的序号。

来源：[framework/Zongsoft.Core/test/Common/ConvertTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/ConvertTest.cs#L94)（节选；上下文见源文件）。

{% code title="ConvertTest.cs" %}

```csharp
public void TestBitVector32()
{
	Zongsoft.Common.BitVector32 vector = 1;

	Assert.Equal(1, vector.Data);
	Assert.True(vector[1]);
	Assert.False(vector[2]);
	Assert.False(vector[3]);
	Assert.False(vector[4]);
	Assert.False(vector[5]);

	vector[5] = true;
	Assert.Equal(5, vector.Data);
	Assert.True(vector[1]);
	Assert.False(vector[2]);
	Assert.False(vector[3]);
	Assert.True(vector[4]);
	Assert.True(vector[5]);
}
```

{% endcode %}

`BitVector64` 可从 `BitVector32` 隐式转换，适合需要更多标志位的场景。

{% hint style="info" %}
`BitVector32` 和 `BitVector64` 更关注紧凑存储；如果要表达固定层级编码，请使用 [HierarchyVector32](/framework/core/common/hierarchy-vector32)。
{% endhint %}

以上是框架 ConvertTest 的原始位向量测试；数值表示位掩码，不能把索引参数直接理解为从零开始的位序号。Discussions 没有直接调用此结构。

## 相关资源

* [BitVector32.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/BitVector32.cs)
* [BitVector64.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/BitVector64.cs)


# 缓冲工具

Buffer 内存租赁、编码解码和二进制读取工具。

`Buffer` 提供围绕 [`IMemoryOwner<T>`](https://learn.microsoft.com/zh-cn/dotnet/api/system.buffers.imemoryowner-1) [*源码*](https://source.dot.net/#System.Private.CoreLib/IMemoryOwner.cs)、数组租赁、文本编码解码和 [`ReadOnlySequence<byte>`](https://learn.microsoft.com/zh-cn/dotnet/api/system.buffers.readonlysequence-1) [*源码*](https://source.dot.net/#System.Memory/ReadOnlySequence.cs) 数值读取的工具方法。

## 内存租赁

来源：[framework/Zongsoft.Core/test/Common/BufferTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/BufferTest.cs#L10)（节选；上下文见源文件）。

{% code title="BufferTest.cs" %}

```csharp
public void Lease_Array_CopiesValidPrefixAndDoesNotObserveCallerMutation()
{
	var source = new[] { 1, 2, 3, 4 };
	using var owner = source.Lease(3);

	source[0] = 10;
	source[3] = 40;

	Assert.Equal(3, owner.Memory.Length);
	Assert.Equal([1, 2, 3], owner.Memory.ToArray());
}
```

{% endcode %}

上述框架回归用例验证 Lease 复制有效前缀，不会随着原数组随后修改而变化；不能把它当成原数组的共享视图。释放时会归还内部数组池。

## 释放与所有权

来源：[framework/Zongsoft.Core/test/Common/BufferTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/BufferTest.cs#L35)（节选；上下文见源文件）。

{% code title="BufferTest.cs" %}

```csharp
public void Lease_Array_DisposeIsIdempotentAndMakesOwnerInaccessible()
{
	var source = new byte[] { 1, 2, 3 };
	var owner = source.Lease();

	owner.Dispose();
	owner.Dispose();

	Assert.Equal([1, 2, 3], source);
	Assert.Throws<ObjectDisposedException>(() => _ = owner.Memory);
}
```

{% endcode %}

测试验证释放可重复调用，释放后不能继续读取租赁内存。编码和解码方法同样返回需要释放的拥有者；它们的默认文本编码是 UTF-8。

## 二进制读取

`Buffer` 提供大端和小端读取方法，例如 `TryGetInt32BigEndian`、`TryGetUInt64LittleEndian`、`TryGetDoubleBigEndian`。

这些方法适合网络协议、二进制文件和跨平台数据包解析。

## 相关资源

* [Buffer.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/Buffer.cs)


# 校验码

Checksum 校验和结构的计算、解析和验证。

`Checksum` 表示命名哈希校验值，由算法名称和字节数组组成。它支持计算、解析、验证、字符串转换、类型转换和 JSON 序列化。

## 核心成员

| 成员                         | 说明                                 |
| -------------------------- | ---------------------------------- |
| `Name`                     | 哈希算法名称，例如 `MD5`、`SHA256`、`SHA512`。 |
| `Value`                    | 校验值字节数组。                           |
| `IsEmpty`                  | 是否为空校验值。                           |
| `Compute` / `ComputeAsync` | 从字节数据或流计算校验值。                      |
| `Verify` / `VerifyAsync`   | 验证数据是否匹配当前校验值。                     |
| `Parse` / `TryParse`       | 从 `Name:Hex` 文本解析校验值。              |

本页使用框架 ChecksumTest 的现有测试；共享字段 \_data 由该测试类生成 1024 字节数据，断言用于明确预期结果。Discussions 当前没有直接使用 Checksum。

## 计算校验和

来源：[framework/Zongsoft.Core/test/Common/ChecksumTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/ChecksumTest.cs#L71)（节选；上下文见源文件）。

{% code title="ChecksumTest.cs" %}

```csharp
public void Compute()
{
	var checksum1 = Checksum.Compute("SHA256", _data);
	Assert.False(checksum1.IsEmpty);
	Assert.False(checksum1.Value.IsEmpty);
	Assert.Equal("SHA256", checksum1.Name, true);
	Assert.Equal(SHA256.HashSizeInBytes, checksum1.Value.Length);
	Assert.NotEmpty(checksum1.ToString());
	Assert.StartsWith("SHA256:", checksum1.ToString());

	var checksum2 = new Checksum(SHA256.HashData(_data));
	Assert.False(checksum2.IsEmpty);
	Assert.False(checksum2.Value.IsEmpty);
	Assert.Equal("SHA256", checksum2.Name, true);
	Assert.Equal(SHA256.HashSizeInBytes, checksum2.Value.Length);
	Assert.NotEmpty(checksum2.ToString());
	Assert.StartsWith("SHA256:", checksum2.ToString());

	Assert.Equal(checksum1.Name, checksum2.Name, true);
	Assert.True(checksum1.Value.Span.SequenceEqual(checksum2.Value.Span));
	Assert.Equal(checksum1, checksum2);
}
```

{% endcode %}

`Compute` 会根据名称创建哈希算法，并计算对应的校验值。

## 验证数据

来源：[framework/Zongsoft.Core/test/Common/ChecksumTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/ChecksumTest.cs#L111)（节选；上下文见源文件）。

{% code title="ChecksumTest.cs" %}

```csharp
public void Verify()
{
	var checksum = Checksum.Compute("SHA3-512", _data);
	Assert.False(checksum.IsEmpty);
	Assert.False(checksum.Value.IsEmpty);

	var data = new byte[_data.Length];
	Array.Copy(_data, data, data.Length);

	Assert.True(checksum.Verify(data));
	Assert.True(checksum.Verify(_data));
	Assert.False(checksum.Verify(Randomizer.Generate(512)));
	Assert.False(checksum.Verify(Randomizer.Generate(data.Length)));
}
```

{% endcode %}

流数据可以使用 `Verify(Stream)` 或 `VerifyAsync(Stream)`。

## 解析和转换

`Checksum.ToString()` 输出 `Name:Hex` 格式，`Parse` 可以还原该格式。

来源：[framework/Zongsoft.Core/test/Common/ChecksumTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/ChecksumTest.cs#L95)（节选；上下文见源文件）。

{% code title="ChecksumTest.cs" %}

```csharp
public void Parse()
{
	var checksum = Checksum.Compute("SHA512", _data);
	Assert.False(checksum.IsEmpty);
	Assert.False(checksum.Value.IsEmpty);
	Assert.Equal("SHA512", checksum.Name, true);
	Assert.Equal(SHA512.HashSizeInBytes, checksum.Value.Length);
	Assert.NotEmpty(checksum.ToString());
	Assert.StartsWith("SHA512:", checksum.ToString());

	var result = Checksum.Parse(checksum.ToString());
	Assert.False(result.IsEmpty);
	Assert.Equal(checksum, result);
}
```

{% endcode %}

`Checksum` 内置类型转换器和 JSON 转换器，因此可以与 `Convert` 和 `Serializer.Json` 协同使用。

## 相关资源

* [Checksum.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/Checksum.cs)
* [ChecksumTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/ChecksumTest.cs)


# 类型转换

Convert 类型转换和十六进制转换工具。

`Zongsoft.Common.Convert` 是框架内的增强转换工具，覆盖常规类型转换、可空类型、枚举别名、[`TimeSpan`](https://learn.microsoft.com/zh-cn/dotnet/api/system.timespan) [*源码*](https://source.dot.net/#System.Private.CoreLib/TimeSpan.cs) 简写格式、十六进制文本和自定义转换器。

## 转换值

`ConvertValue` 支持常规类型转换、枚举别名转换、[`TimeSpan`](https://learn.microsoft.com/zh-cn/dotnet/api/system.timespan) [*源码*](https://source.dot.net/#System.Private.CoreLib/TimeSpan.cs) 简写格式转换、自定义 [`TypeConverter`](https://learn.microsoft.com/zh-cn/dotnet/api/system.componentmodel.typeconverter) [*源码*](https://source.dot.net/#System.ComponentModel.TypeConverter/TypeConverter.cs)。

来源：[framework/Zongsoft.Core/test/Common/ConvertTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/ConvertTest.cs#L15)（节选；上下文见源文件）。

{% code title="ConvertTest.cs" %}

```csharp
Assert.Null(Zongsoft.Common.Convert.ConvertValue<int?>("", default(int?)));
Assert.Null(Zongsoft.Common.Convert.ConvertValue<int?>("x", () => default(int?)));
Assert.NotNull(Zongsoft.Common.Convert.ConvertValue<int?>("123", () => default(int?)));
Assert.Equal(123, Zongsoft.Common.Convert.ConvertValue<int?>("123", () => default(int?)));

object value = 123;
Assert.Equal(123.0f, Zongsoft.Common.Convert.ConvertValue<float>(value));
Assert.Equal(123.0d, Zongsoft.Common.Convert.ConvertValue<double>(value));
Assert.Equal(123.0m, Zongsoft.Common.Convert.ConvertValue<decimal>(value));

value = "123";
Assert.Equal(123, Zongsoft.Common.Convert.ConvertValue<int>(value));
```

{% endcode %}

`TryConvertValue` 适合不希望转换失败抛异常的场景。下面的框架测试还展示了时间跨度简写格式与类型转换的配合。

来源：[framework/Zongsoft.Core/test/Common/ConvertTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/ConvertTest.cs#L33)（节选；上下文见源文件）。

{% code title="ConvertTest.cs" %}

```csharp
var duration = TimeSpan.Parse("1:20:30");
Assert.Equal(duration, Zongsoft.Common.Convert.ConvertValue<TimeSpan>("1:20:30"));
Assert.True(TimeSpanUtility.TryParse("15S", out duration));
Assert.Equal(duration, Zongsoft.Common.Convert.ConvertValue<TimeSpan>("15s"));
Assert.True(TimeSpanUtility.TryParse("20m", out duration));
Assert.Equal(duration, Zongsoft.Common.Convert.ConvertValue<TimeSpan>("20M"));
Assert.True(TimeSpanUtility.TryParse("30h", out duration));
Assert.Equal(duration, Zongsoft.Common.Convert.ConvertValue<TimeSpan>("30H"));
Assert.True(TimeSpanUtility.TryParse("40D", out duration));
Assert.Equal(duration, Zongsoft.Common.Convert.ConvertValue<TimeSpan>("40d"));
```

{% endcode %}

二进制和十六进制文本可以互转。

来源：[framework/Zongsoft.Core/test/Common/ConvertTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/ConvertTest.cs#L70)（节选；上下文见源文件）。

{% code title="ConvertTest.cs" %}

```csharp
public void TestToHexString()
{
	var source = new byte[16];

	for(int i = 0; i < source.Length; i++)
		source[i] = (byte)i;

	var hexString1 = Zongsoft.Common.Convert.ToHexString(source);
	var hexString2 = Zongsoft.Common.Convert.ToHexString(source, '-');

	Assert.Equal("000102030405060708090A0B0C0D0E0F", hexString1);
	Assert.Equal("00-01-02-03-04-05-06-07-08-09-0A-0B-0C-0D-0E-0F", hexString2);

	var bytes1 = Zongsoft.Common.Convert.FromHexString(hexString1);
	var bytes2 = Zongsoft.Common.Convert.FromHexString(hexString2, '-');

	Assert.Equal(source.Length, bytes1.Length);
	Assert.Equal(source.Length, bytes2.Length);

	Assert.True(BinaryCompare(source, bytes1));
	Assert.True(BinaryCompare(source, bytes2));
}
```

{% endcode %}

{% content-ref url="/pages/aV8VGpDtTqOxiv3VtQlY" %}
[枚举工具](/framework/core/common/enum-utility)
{% endcontent-ref %}

这些片段来自框架 ConvertTest。十六进制测试中的 BinaryCompare 是该测试类的辅助方法；前两个片段是方法内部节选。Discussions 自定义标签转换的实际实现可见 [TagsConverter](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Models/TagsConverter.cs)。

## 相关资源

* [Convert.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/Convert.cs)
* [ConvertTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/ConvertTest.cs)


# 枚举工具

EnumUtility 和 EnumEntry 枚举元数据工具。

`EnumUtility` 用于读取枚举项的名称、别名、描述和值，并以 `EnumEntry` 结构返回。

下列代码来自框架测试，Gender 是 Zongsoft.Tests 中的测试枚举，不是 Discussions.Models.Gender。前者的别名和描述必须与测试定义一起阅读。

## EnumEntry

`EnumEntry` 包含枚举项的元数据：

| 字段                                                                                                                                | 说明                |
| --------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| [`Type`](https://learn.microsoft.com/zh-cn/dotnet/api/system.type) [*源码*](https://source.dot.net/#System.Private.CoreLib/Type.cs) | 枚举类型。             |
| `Name`                                                                                                                            | 枚举项名称。            |
| `Value`                                                                                                                           | 枚举项值；可选择枚举值或基础数值。 |
| `Aliases`                                                                                                                         | 枚举项别名。            |
| `Description`                                                                                                                     | 枚举项说明。            |

## 读取枚举项

来源：[framework/Zongsoft.Core/test/Common/EnumUtilityTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/EnumUtilityTest.cs#L12)（节选；上下文见源文件）。

{% code title="EnumUtilityTest.cs" %}

```csharp
public void TestGetEnumEntry()
{
	var entry = EnumUtility.GetEnumEntry(Gender.Female);

	Assert.Equal("Female", entry.Name);
	Assert.Equal(Gender.Female, entry.Value); //注意：entry.Value 为枚举类型
	Assert.True(entry.HasAlias("F"));
	Assert.Equal("女士", entry.Description);
	Assert.Equal("女士", EnumUtility.GetEnumDescription(Gender.Female));

	entry = EnumUtility.GetEnumEntry(Gender.Male, true);

	Assert.Equal("Male", entry.Name);
	Assert.Equal((byte)1, entry.Value); //注意：entry.Value 为枚举项的基元类型
	Assert.True(entry.HasAlias("M"));
	Assert.Equal("男士", entry.Description);
	Assert.Equal("男士", EnumUtility.GetEnumDescription(Gender.Male));
}
```

{% endcode %}

`GetEnumEntries` 可以读取整个枚举类型，也可以为可空枚举追加空值项。

来源：[framework/Zongsoft.Core/test/Common/EnumUtilityTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/EnumUtilityTest.cs#L32)（节选；上下文见源文件）。

{% code title="EnumUtilityTest.cs" %}

```csharp
public void TestGetEnumEntries()
{
	var entries = EnumUtility.GetEnumEntries(typeof(Gender), true);

	Assert.Equal(2, entries.Length);
	Assert.Contains(entries, entry => entry.Name == "Male");
	Assert.Contains(entries, entry => entry.Name == "Female");

	entries = EnumUtility.GetEnumEntries(typeof(Nullable<Gender>), true, null, "<Unknown>");

	Assert.Equal(3, entries.Length);
	Assert.Equal("", entries[0].Name);
	Assert.Null(entries[0].Value);
	Assert.Equal("<Unknown>", entries[0].Description);

	Assert.Contains(entries, entry => entry.Name == "Male");
	Assert.Contains(entries, entry => entry.Name == "Female");
}
```

{% endcode %}

## 相关资源

* [EnumUtility.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/EnumUtility.cs)
* [EnumEntry.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/EnumEntry.cs)
* [EnumUtilityTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/EnumUtilityTest.cs)


# 分层向量

HierarchyVector32 四级层级编码结构。

`HierarchyVector32` 使用一个 32 位无符号整数表达最多四级层级编码，每级占用一个字节。

来源：[framework/Zongsoft.Data/test/Models/AddressConditionConverter.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Data/test/Models/AddressConditionConverter.cs#L10)（节选；上下文见源文件）。

{% code title="AddressConditionConverter.cs" %}

```csharp
public override ICondition Convert(ConditionConverterContext context)
{
	if(context.Value == null)
		return null;

	static ICondition GetCondition(string name, uint id)
	{
		if(id == 0)
			return Condition.Equal(name, 0u);

		return Zongsoft.Data.Range.Create((HierarchyVector32)id).ToCondition(name);
	}

	if(context.Names.Length == 1)
		return GetCondition(context.GetFullName(), Zongsoft.Common.Convert.ConvertValue<uint>(context.Value));

	return ConditionCollection.Or(context.Names.Select(name => GetCondition(context.GetFullName(name), Zongsoft.Common.Convert.ConvertValue<uint>(context.Value))));
}
```

{% endcode %}

## 常用成员

| 成员                    | 说明               |
| --------------------- | ---------------- |
| `Depth`               | 当前编码深度。          |
| `Value`               | 原始 32 位值。        |
| `Minimum` / `Maximum` | 当前层级覆盖的值范围。      |
| `Contains`            | 判断一个编码是否包含另一个编码。 |
| `IsChild`             | 判断是否为直接或间接子级。    |
| `GetParent`           | 获取父级编码。          |
| `GetAncestors`        | 获取祖先编码列表。        |

## 实际用途：层级范围查询

Discussions 的论坛与文件夹使用自身的数据模型，没有把编号编码为 HierarchyVector32。上面采用框架数据测试的 AddressConditionConverter：将地址编号解释为层级向量，再通过 Range.Create 生成范围查询条件；多字段条件以 Or 合并。输入零被单独转换为等值条件，避免把根节点范围误当成具体地址。

## 编码约束

四个字节从高位到低位对应四层。末尾非零字节决定深度，剩余低位范围表示后代；第四层的 Minimum 和 Maximum 相同。零向量深度为零。构造函数本身不会验证前面的层级是否连续，调用方应保持编码规则一致。

这适合层级上限已固定、编码规则由系统统一分配的区域或机构查询；不适合任意深度、频繁移动节点的树。需要动态父子关系时，先阅读[层次化模型](/framework/core/collections/hierarchical)，不要把普通自增 ID 强制转换后当成有效层级编码。

## 相关资源

* [HierarchyVector32.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/HierarchyVector32.cs)


# 异步锁

Locker 同步和异步互斥锁。

`Locker` 是一个轻量互斥锁封装，用于把同步代码和异步代码中的临界区串行化。

两个范例摘自 LockerTest；COUNT 是夹具中定义的循环次数 500。测试通过并发递增最终计数验证互斥效果，而不是用延时猜测执行顺序。

## 同步临界区

同步代码使用 `Lock()` 获取一个可释放的锁句柄，离开 `using` 作用域时自动释放。

来源：[framework/Zongsoft.Core/test/Common/LockerTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/LockerTest.cs#L135)（节选；上下文见源文件）。

{% code title="LockerTest.cs" %}

```csharp
public void Lock()
{
	var count = 0;
	var locker = new Locker();

	Parallel.For(0, COUNT, i =>
	{
		using(locker.Lock())
		{
			count++;
		}
	});

	Assert.Equal(COUNT, count);
}
```

{% endcode %}

## 异步临界区

异步代码使用 `LockAsync()`，并通过 `await using` 释放锁。

来源：[framework/Zongsoft.Core/test/Common/LockerTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/LockerTest.cs#L189)（节选；上下文见源文件）。

{% code title="LockerTest.cs" %}

```csharp
public async Task LockAsync2()
{
	const int TIMES = 50;

	var count = 0;
	var locker = new Locker();

	await Parallel.ForAsync(0, TIMES, async (_, cancellation) =>
	{
		for(int i = 0; i < COUNT; i++)
		{
			await using(await locker.LockAsync(cancellation))
			{
				count++;
			}
		}
	});

	Assert.Equal(COUNT * TIMES, count);
}
```

{% endcode %}

LockAsync 支持取消令牌，适合需要取消等待锁的后台任务。

{% hint style="info" %}
如果需要创建变更令牌，请参阅 [Notification](/framework/core/common/notification)；如果需要表达操作失败原因，请参阅 [OperationException](/framework/core/common/operation-exception)。
{% endhint %}

## 相关资源

* [Locker.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/Locker.cs)
* [LockerTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/LockerTest.cs)


# 通知

Notification 变更令牌工具。

`Notification` 用于创建或获取变更令牌，常与缓存依赖、配置刷新和状态观察配合。

| 成员                                  | 说明                                   |
| ----------------------------------- | ------------------------------------ |
| `Notified`                          | 永远处于已变更状态的令牌。                        |
| `GetToken(CancellationToken)`       | 将可取消令牌包装为变更令牌。                       |
| `GetToken(CancellationTokenSource)` | 从取消源创建变更令牌；传入 `null` 时返回 `Notified`。 |

来源：[framework/Zongsoft.Core/src/Data/DataAccessBase.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Data/DataAccessBase.cs#L120)（节选；上下文见源文件）。

{% code title="DataAccessBase.cs" %}

```csharp
public IChangeToken Disposed => Notification.GetToken(_cancellation);
```

{% endcode %}

上面是框架数据访问器的 Disposed 属性。Discussions 从 Module.Accessor 取得访问器，提供者缓存该访问器时使用此属性作为依赖：释放访问器会触发失效，使下一次取得访问器时重新创建。完整链路见[内存缓存](/framework/core/caching/memory-cache)。

Notification 本身不发送业务消息，也不持有重新加载逻辑；它把一次取消通知转换为缓存和配置组件可观察的变更信号。令牌失效后不能复位，需要下一轮观察时应创建新的取消源。

`Notified` 适合表达立即失效的依赖，例如需要强制缓存项创建后即过期的场景。

## 相关资源

* [Notification.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/Notification.cs)


# 操作异常

OperationException 操作异常。

`OperationException` 是框架级操作异常。它通过 `Reason` 表达失败类型，并提供一组静态工厂方法。

| 工厂方法          | 语义      |
| ------------- | ------- |
| `Argument`    | 参数不正确。  |
| `Unknown`     | 未知错误。   |
| `Unfound`     | 目标不存在。  |
| `Unsatisfied` | 条件不满足。  |
| `Unprocessed` | 操作未处理。  |
| `Unsupported` | 不支持该操作。 |

来源：[framework/Zongsoft.Core/test/Messaging/MessageQueueBaseTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Messaging/MessageQueueBaseTest.cs#L59)（节选；上下文见源文件）。

{% code title="MessageQueueBaseTest.cs" %}

```csharp
public async Task UnsupportedDelayFailsBeforeDriverOperation()
{
	using var queue = new TestQueue();
	var options = new MessageEnqueueOptions(TimeSpan.FromSeconds(1));

	var exception = await Assert.ThrowsAsync<OperationException>(() => queue.ProduceAsync("tests/delay", ReadOnlyMemory<byte>.Empty, options).AsTask());

	Assert.Equal(nameof(OperationException.Unsupported), exception.Reason);
	Assert.Contains(MessageQueueFeature.Delay.Name, exception.Message);
	Assert.Equal(0, queue.ProduceCount);
}
```

{% endcode %}

## 实际用例

上面是框架消息抽象的能力测试：不支持延迟的队列拒绝带延迟选项的发送请求。测试检查 Reason 为 Unsupported，同时确认驱动尚未执行。TestQueue 属于测试夹具，不连接实际中间件。Discussions 没有这个队列实现，相关业务异常应以其服务为准。

## 边界与错误响应

Reason 是供程序判断的分类，Message 面向诊断或用户说明；不要通过匹配错误文本决定业务分支。工厂只创建异常，不会自动抛出、重试或转换 HTTP 状态。网关如何转成响应见[聚合异常处理](/framework/core/common/aggregate-exception-utility)。

调用方可以通过 `IsArgument`、`IsUnknown`、`IsUnfound`、`IsUnsupported` 等属性判断异常原因。

## 相关资源

* [OperationException.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/OperationException.cs)


# 断言

从安全模块事件与日志过滤说明可组合的条件断言。

Predication 将“条件是否成立”表达为可同步或异步执行的对象。Discussions 的业务条件主要使用数据引擎 Condition；它没有自定义 PredicationBase。因此这里采用 framework 安全模块的事件注册作为真实用例。

## 将委托用于事件描述

来源：[framework/Zongsoft.Security/src/Module.Events.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Security/src/Module.Events.cs#L63)（节选；上下文见源文件）。

{% code title="Module.Events.cs" %}

```csharp
public static readonly EventDescriptor<Privileges.AuthenticatedEventArgs> Authenticated = new(Predication.Predicate<Privileges.AuthenticatedEventArgs>(OnAuthenticated), $"{nameof(Privileges.Authentication)}.{nameof(Privileges.Authentication.Authenticated)}");
public static readonly EventDescriptor<Privileges.AuthenticatingEventArgs> Authenticating = new(Predication.Predicate<Privileges.AuthenticatingEventArgs>(OnAuthenticating), $"{nameof(Privileges.Authentication)}.{nameof(Privileges.Authentication.Authenticating)}");
```

{% endcode %}

安全模块用 Predication.Predicate 将方法包装成对应参数类型的断言。事件描述器和绑定逻辑仍由模块负责，断言工厂不自动完成事件注册。

来源：[framework/Zongsoft.Security/src/Module.Events.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Security/src/Module.Events.cs#L114)（节选；上下文见源文件）。

{% code title="Module.Events.cs" %}

```csharp
private static bool OnAuthenticating(Privileges.AuthenticatingEventArgs args)
{
	Current.Meter.Authentication.Authenticating.Add(1,
		new KeyValuePair<string, object>(nameof(args.Scheme), args.Scheme),
		new KeyValuePair<string, object>(nameof(args.Scenario), args.Scenario));

	return true;
}
```

{% endcode %}

这个真实回调先记录认证指标，再返回 true。它说明断言可能包含副作用；组合和短路会影响后续回调是否执行，因此不能无条件把带副作用断言当作纯函数重排。

## 类型与组合

| 类型                            | 责任             |
| ----------------------------- | -------------- |
| IPredication、IPredication\<T> | 非泛型与强类型判断契约    |
| Predication                   | 从委托创建实现        |
| PredicationBase\<T>           | 命名、参数转换与服务匹配   |
| PredicationCollection         | 组合多个断言         |
| PredicationCombination        | AND 或 OR 的短路规则 |

集合为空时返回成功；AND 在失败时短路，OR 在成功时短路。默认对象转换会影响弱类型入口，扩展时应检查参数类型、附加 Parameters 与取消语义。

## 日志中的实际使用

框架 LoggerPredication 根据日志来源、异常类型和级别进行判断。配置由 Initialize 读取，具体实现见 [LoggerPredication](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Diagnostics/LoggerPredication.cs)。这类规则适合输出筛选，不应代替业务授权。

需要返回具体失败原因时使用[验证器](/framework/core/common/validator)；需要让数据库筛选记录时使用[数据条件](/framework/data/conditions-and-operands)。


# 随机器

以 Discussions 的正文文件名说明随机字符串的真实用途。

Randomizer 提供随机字节、整数和字符串。Discussions 使用随机字符串作为文件名的一部分，避免新建内容尚未取得数据库编号时共用同一个文件名。

来源：[src/Services/PostService.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/PostService.cs#L222)（节选；上下文见源文件）。

{% code title="PostService.cs" %}

```csharp
protected virtual string GetContentFilePath(ulong postId, string contentType)
{
	return Utility.GetFilePath(string.Format("posts/post-{0}-{1}.txt", postId.ToString(), Zongsoft.Common.Randomizer.GenerateString()));
}
```

{% endcode %}

PostId 提供业务线索，随机后缀降低同一编号或未分配编号产生路径冲突的机会。路径还会经过 Utility.GetFilePath 加上配置和用户范围。随机文件名不能代替访问控制，也不构成绝对唯一性保证。

## 方法选择

| 方法                                           | 作用            |
| -------------------------------------------- | ------------- |
| Generate                                     | 指定长度的随机字节     |
| GenerateInt16、GenerateInt32、GenerateInt64    | 有符号整数         |
| GenerateUInt16、GenerateUInt32、GenerateUInt64 | 无符号整数         |
| GenerateSecret                               | 密钥风格字符串       |
| GenerateString                               | 普通随机字符串，可限定数字 |

本业务没有直接使用 GenerateSecret 作为凭证，因此不能把文件命名做法直接推广为认证令牌方案。字符串长度和字符范围的框架用例见 [RandomizerTest](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/RandomizerTest.cs)。

关联阅读：[文件系统](/framework/core/io)、[写入操作](/framework/data/writing)。


# 序号器

以 Discussions 映射中的外部序号和站点分域说明生成责任。

序号器负责按名称或业务范围生成编号。Discussions 没有自行实现一个演示序号服务，而是在映射中声明外部序号需求，由数据引擎和部署的提供者完成生成。

## 主题使用外部序号

来源：[src/Zongsoft.Discussions.mapping](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.mapping#L293)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.mapping" %}

```xml
<property name="ThreadId" type="ulong" nullable="false" sequence="#" />
```

{% endcode %}

`#` 表示外部序号。调用业务新增方法时，编号可能尚未生成，所以不能依赖默认编号为新文件提供唯一名字；当前正文文件名还使用随机后缀，见[随机数](/framework/core/common/randomizer)。

## 论坛按站点分域

来源：[src/Zongsoft.Discussions.mapping](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.mapping#L203)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.mapping" %}

```xml
<property name="ForumId" type="ushort" nullable="false" sequence="#(SiteId)" />
```

{% endcode %}

ForumId 的编号范围与 SiteId 关联。模型关系和数据库键也需要包含站点，否则不同站点内相同编号会发生混淆。

## 基础序号与本地号段

ISequenceBase 表达基础递增、重置等能力；框架还可通过 Variate 包装获得号段分配。号段降低远端调用次数，但进程退出可能留下未使用号码，不能据此承诺连续无间隙。

Discussions 清单没有固定序号器实现，部署者必须核对实际提供者和名称。框架 [SequenceTest](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/SequenceTest.cs) 验证号段增长与重置，[Redis SequenceTest](https://github.com/Zongsoft/framework/blob/main/externals/redis/test/SequenceTest.cs) 提供具体实现参考。不要把测试中的固定区间作为生产配置。

## 使用边界

编号生成成功不代表业务写入已经提交。回滚、重试和跨节点并发都会影响编号使用情况。重置已有业务编号还可能导致键冲突，必须与持久化数据和缓存号段一起评估。


# 计时器

通过框架已有 TimerTest 理解周期回调、取消、停止与释放。

Timer 用于周期性调用异步回调。它适合宿主内的轻量轮询；跨进程持久化调度和失败重试应阅读[任务调度](/framework/core/scheduling)。Discussions 没有直接使用计时器的业务范例，这里采用框架现有测试。

## 建立回调与完成信号

TimerTest 把周期设为 1 毫秒，并用完成信号等待第 10 次回调。LIMIT、\_count、\_timer 和 \_completion 都是这个测试夹具的成员；该周期是测试参数，实际任务需要依据工作耗时配置。

来源：[framework/Zongsoft.Core/test/Common/TimerTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/TimerTest.cs#L22)（节选；上下文见源文件）。

{% code title="TimerTest.cs" %}

```csharp
public TimerTest()
{
	_timer = new Timer(TimeSpan.FromMilliseconds(1), this.OnTick);
	_completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
}
```

{% endcode %}

## 启动并等待结束

来源：[framework/Zongsoft.Core/test/Common/TimerTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/TimerTest.cs#L31)（节选；上下文见源文件）。

{% code title="TimerTest.cs" %}

```csharp
public async Task Test()
{
	Assert.False(_timer.IsRunning);
	_timer.Start(TestContext.Current.CancellationToken);
	Assert.True(_timer.IsRunning);

	await _completion.Task.WaitAsync(TimeSpan.FromSeconds(30), TestContext.Current.CancellationToken);
	Assert.Equal(LIMIT, _count);
	Assert.False(_timer.IsRunning);
}
```

{% endcode %}

测试检查启动前、启动后和结束后的 IsRunning。等待上限 30 秒用于防止失败测试无限悬挂，不代表计时器精度或业务超时承诺。

## 在回调中停止

来源：[framework/Zongsoft.Core/test/Common/TimerTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/TimerTest.cs#L44)（节选；上下文见源文件）。

{% code title="TimerTest.cs" %}

```csharp
private ValueTask OnTick(object state, CancellationToken cancellation)
{
	if(Interlocked.Increment(ref _count) >= LIMIT)
	{
		_timer.Stop();
		_completion.TrySetResult();
	}

	return ValueTask.CompletedTask;
}
```

{% endcode %}

回调达到 LIMIT 后调用 Stop 并通知等待方。停止周期不等于撤销已经产生的业务副作用；回调里的外部操作仍应遵守自己的取消和幂等约定。

{% hint style="info" %}
💡 创建计时器的组件应负责最终释放它。在支持运行时设置 Period 的目标框架上，可以调整后续周期；实际条件编译和释放逻辑见 [Timer.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/Timer.cs)。
{% endhint %}


# 时间戳

以 Discussions 附件命名理解纪元与经过时间。

Timestamp 以指定纪元计算时间点或经过时间。Discussions 的附件上传使用 Millennium 纪元的累计天数作为文件名的一部分，与随机后缀组合。

来源：[src/api/Controllers/FileController.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/api/Controllers/FileController.cs#L79)（节选；上下文见源文件）。

{% code title="FileController.cs" %}

```csharp
var infos = this.Accessor.Write(this.Request,
							  this.DataService.GetDirectory(id),
							  args => args.FileName = $"{Timestamp.Millennium.Epoch.GetElapsed().Days}-{Randomizer.GenerateString()}", cancellation);
```

{% endcode %}

这段代码位于 UploadAsync：Request 来自当前 HTTP 请求，目录由 FileService 决定，cancellation 来自调用方。它没有把文件名中的天数当作业务主键或访问权限。

## 纪元、单位与时区

Unix 和 Millennium 是不同起点。跨系统传递时间值时，必须同时约定起点和单位；仅传一个数字不足以判断它表示秒、毫秒、天数还是 ticks。时区展示也应与持久化时间约定区分。

## 与业务审计时间的区别

Discussions 的 CreatedTime、ModifiedTime 等审计字段由 DataValidator 使用 DateTime.Now 填写。附件名称使用纪元天数，不代表整个模块已经统一采用 Unix 时间戳。审计策略见[数据服务](/framework/data/services)。

方法和边界可核对 [Timestamp 源码](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/Timestamp.cs)及[对应测试](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/Timestamp.cs)。


# 验证器

从框架安全模块的密码策略验证器理解结果、失败信息和调用上下文。

IValidator\<T> 返回是否通过，并允许通过 failure 回调报告原因。Discussions 的 DataValidator 实现的是数据访问验证契约，负责站点和审计字段；两者不能仅因名称相近就混为一谈。

## 真实密码验证器

来源：[framework/Zongsoft.Security/src/Validators/PasswordValidator.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Security/src/Validators/PasswordValidator.cs#L42)（节选；上下文见源文件）。

{% code title="PasswordValidator.cs" %}

```csharp
[Service(typeof(IValidator<string>))]
public class PasswordValidator : IValidator<string>, IMatchable
```

{% endcode %}

安全模块注册 PasswordValidator 供服务发现。策略并未写死成“至少八位”，而是由 IdentityOptions 参数提供。

来源：[framework/Zongsoft.Security/src/Validators/PasswordValidator.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Security/src/Validators/PasswordValidator.cs#L58)（节选；上下文见源文件）。

{% code title="PasswordValidator.cs" %}

```csharp
var options = parameter as Configuration.IdentityOptions;

//如果没有设置密码验证策略，则返回验证成功
if(options == null || options.PasswordLength < 1)
	return true;

//如果如果密码长度小于配置要求的长度，则返回验证失败
if(string.IsNullOrEmpty(data) || data.Length < options.PasswordLength)
{
	failure?.Invoke($"The password length must be no less than {options.PasswordLength} characters.");
	return false;
}

bool isValidate;
```

{% endcode %}

没有提供策略或长度小于一时返回成功；长度不足时通过 failure 返回原因。这是当前实现的重要边界，部署者必须提供符合自身要求的策略，不能把未配置理解成采用了默认强策略。

## 强度与失败消息

后续逻辑根据配置分别判断纯数字、最低、普通和最高强度。完整方法见同一源文件。调用方可以收集失败消息显示给用户，也可以只关心返回值；不要把输入的密码本身写入日志。

## 同步、异步与上下文

验证接口有同步和异步形式，并可以传递自定义参数。当前密码检查是本地计算；需要调用外部依赖的验证器应传播取消令牌并控制调用成本。同步成功不代表持久化或认证已经成功，这些属于后续业务步骤。

| 需求             | 应使用的机制                                                |
| -------------- | ----------------------------------------------------- |
| 输入值是否符合规则并返回原因 | IValidator                                            |
| 是否满足条件         | [Predication](/framework/core/common/predication)     |
| 数据查询站点约束与审计字段  | [Discussions DataValidator](/framework/data/services) |
| 用户能否执行资源动作     | [认证与授权](/framework/security/authentication)           |

调用位置可核对框架 [UserServiceBase.Password](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Privileges/UserServiceBase.Password.cs)。


# 类型别名

TypeAlias 类型别名、泛型类型、可空类型和数组类型解析。

`TypeAlias` 用于在类型和可读字符串之间转换。它支持内置类型别名、可空类型、数组、泛型类型、程序集简写和嵌套类型。

## 常见别名

`TypeAlias` 为常见 CLR 类型提供简短名称，例如：

| 类型                | 别名示例              |
| ----------------- | ----------------- |
| `System.Object`   | `object`          |
| `System.String`   | `string`          |
| `System.Int32`    | `int32`、`int`     |
| `System.Guid`     | `guid`            |
| `System.DateOnly` | `date`、`dateOnly` |
| `System.TimeOnly` | `time`、`timeOnly` |
| `System.TimeSpan` | `timeSpan`        |

## 解析类型

来源：[framework/Zongsoft.Core/test/Common/TypeAliasTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/TypeAliasTest.cs#L15)（节选；上下文见源文件）。

{% code title="TypeAliasTest.cs" %}

```csharp
Assert.Same(typeof(void), TypeAlias.Parse("void"));
Assert.Same(typeof(object), TypeAlias.Parse(" object"));
Assert.Same(typeof(object[]), TypeAlias.Parse("object []"));
Assert.Same(typeof(object), TypeAlias.Parse("System.object"));
Assert.Same(typeof(object[]), TypeAlias.Parse("System.object []"));

Assert.Same(typeof(string), TypeAlias.Parse(" string "));
Assert.Same(typeof(string[]), TypeAlias.Parse("string[ ]"));
Assert.Same(typeof(string), TypeAlias.Parse("System.string"));
Assert.Same(typeof(string[]), TypeAlias.Parse("System.string [ ]"));

Assert.Same(typeof(int), TypeAlias.Parse(" int "));
Assert.Same(typeof(int), TypeAlias.Parse("int32"));
Assert.Same(typeof(int), TypeAlias.Parse("System.Int32"));
Assert.Same(typeof(int?), TypeAlias.Parse("int? "));
Assert.Same(typeof(int[]), TypeAlias.Parse("int [ ] "));
Assert.Same(typeof(int?[]), TypeAlias.Parse(" int?[]"));
```

{% endcode %}

复杂类型支持泛型和程序集简写。

来源：[framework/Zongsoft.Core/test/Common/TypeAliasTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/TypeAliasTest.cs#L107)（节选；上下文见源文件）。

{% code title="TypeAliasTest.cs" %}

```csharp
public void TestParseNested()
{
	Assert.Same(typeof(NestedClass), TypeAlias.Parse("Zongsoft.Common.Tests.TypeAliasTest+NestedClass@Zongsoft.Core.Tests"));
	Assert.Same(typeof(NestedStruct), TypeAlias.Parse("Zongsoft.Common.Tests.TypeAliasTest+NestedStruct@Zongsoft.Core.Tests"));

	Assert.Same(typeof(NestedClass.DeepenedClass), TypeAlias.Parse("Zongsoft.Common.Tests.TypeAliasTest+NestedClass+DeepenedClass@Zongsoft.Core.Tests"));
	Assert.Same(typeof(NestedClass.DeepenedStruct), TypeAlias.Parse("Zongsoft.Common.Tests.TypeAliasTest+NestedClass+DeepenedStruct@Zongsoft.Core.Tests"));

	Assert.Same(typeof(NestedStruct.DeepenedClass), TypeAlias.Parse("Zongsoft.Common.Tests.TypeAliasTest+NestedStruct+DeepenedClass@Zongsoft.Core.Tests"));
	Assert.Same(typeof(NestedStruct.DeepenedStruct), TypeAlias.Parse("Zongsoft.Common.Tests.TypeAliasTest+NestedStruct+DeepenedStruct@Zongsoft.Core.Tests"));
}
```

{% endcode %}

`@AssemblyName` 是程序集名简写，等价于常见的逗号程序集限定形式。

## 生成别名

来源：[framework/Zongsoft.Core/test/Common/TypeAliasTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/TypeAliasTest.cs#L120)（节选；上下文见源文件）。

{% code title="TypeAliasTest.cs" %}

```csharp
public void TestGetAlias()
{
	Assert.Equal("object", TypeAlias.GetAlias(typeof(object)), true);
	Assert.Equal("object[]", TypeAlias.GetAlias(typeof(object[])), true);
	Assert.Equal("DBNull", TypeAlias.GetAlias(typeof(DBNull)), true);
	Assert.Equal("DBNull[]", TypeAlias.GetAlias(typeof(DBNull[])), true);

	Assert.Equal("void", TypeAlias.GetAlias(typeof(void)), true);
	Assert.Equal("string", TypeAlias.GetAlias(typeof(string)), true);
	Assert.Equal("string[]", TypeAlias.GetAlias(typeof(string[])), true);

	Assert.Equal("int32", TypeAlias.GetAlias(typeof(int)), true);
	Assert.Equal("int32?", TypeAlias.GetAlias(typeof(int?)), true);
	Assert.Equal("int32[]", TypeAlias.GetAlias(typeof(int[])), true);
	Assert.Equal("int32?[]", TypeAlias.GetAlias(typeof(int?[])), true);

	Assert.Equal("Single", TypeAlias.GetAlias(typeof(float)), true);
	Assert.Equal("Single?", TypeAlias.GetAlias(typeof(float?)), true);
	Assert.Equal("Single[]", TypeAlias.GetAlias(typeof(float[])), true);
	Assert.Equal("Single?[]", TypeAlias.GetAlias(typeof(float?[])), true);

	Assert.Equal("Date", TypeAlias.GetAlias(typeof(DateOnly)), true);
	Assert.Equal("Date?", TypeAlias.GetAlias(typeof(DateOnly?)), true);
	Assert.Equal("date[]", TypeAlias.GetAlias(typeof(DateOnly[])), true);
	Assert.Equal("date?[]", TypeAlias.GetAlias(typeof(DateOnly?[])), true);

	Assert.Equal("Range<Timestamp>", TypeAlias.GetAlias(typeof(Zongsoft.Data.Range<DateTimeOffset>)), true);
	Assert.Equal("Range<Timestamp>?", TypeAlias.GetAlias(typeof(Zongsoft.Data.Range<DateTimeOffset>?)), true);
	Assert.Equal("Range<Timestamp>[]", TypeAlias.GetAlias(typeof(Zongsoft.Data.Range<DateTimeOffset>[])), true);
	Assert.Equal("Range<Timestamp>?[]", TypeAlias.GetAlias(typeof(Zongsoft.Data.Range<DateTimeOffset>?[])), true);

	Assert.Equal("Zongsoft.Tests.Gender@Zongsoft.Core.Tests", TypeAlias.GetAlias(typeof(Gender)), true);
	Assert.Equal("Zongsoft.Tests.Gender?@Zongsoft.Core.Tests", TypeAlias.GetAlias(typeof(Gender?)), true);
	Assert.Equal("Zongsoft.Tests.Gender[]@Zongsoft.Core.Tests", TypeAlias.GetAlias(typeof(Gender[])), true);
	Assert.Equal("Zongsoft.Tests.Gender?[]@Zongsoft.Core.Tests", TypeAlias.GetAlias(typeof(Gender?[])), true);

	Assert.Equal("IEnumerable<Zongsoft.Tests.Gender@Zongsoft.Core.Tests>", TypeAlias.GetAlias(typeof(IEnumerable<Gender>)), true);
	Assert.Equal("IEnumerable<Zongsoft.Tests.Gender?@Zongsoft.Core.Tests>", TypeAlias.GetAlias(typeof(IEnumerable<Gender?>)), true);
	Assert.Equal("IEnumerable<Zongsoft.Tests.Gender[]@Zongsoft.Core.Tests>", TypeAlias.GetAlias(typeof(IEnumerable<Gender[]>)), true);
	Assert.Equal("IEnumerable<Zongsoft.Tests.Gender?[]@Zongsoft.Core.Tests>", TypeAlias.GetAlias(typeof(IEnumerable<Gender?[]>)), true);

	Assert.Equal("List<Zongsoft.Tests.Gender@Zongsoft.Core.Tests>", TypeAlias.GetAlias(typeof(List<Gender>)), true);
	Assert.Equal("List<Zongsoft.Tests.Gender?@Zongsoft.Core.Tests>", TypeAlias.GetAlias(typeof(List<Gender?>)), true);
	Assert.Equal("List<Zongsoft.Tests.Gender[]@Zongsoft.Core.Tests>", TypeAlias.GetAlias(typeof(List<Gender[]>)), true);
	Assert.Equal("List<Zongsoft.Tests.Gender?[]@Zongsoft.Core.Tests>", TypeAlias.GetAlias(typeof(List<Gender?[]>)), true);

	Assert.Equal("IDictionary<String, Zongsoft.Tests.Gender@Zongsoft.Core.Tests>", TypeAlias.GetAlias(typeof(IDictionary<string, Gender>)), true);
	Assert.Equal("IDictionary<String, Zongsoft.Tests.Gender?@Zongsoft.Core.Tests>", TypeAlias.GetAlias(typeof(IDictionary<string, Gender?>)), true);
	Assert.Equal("IDictionary<String, Zongsoft.Tests.Gender[]@Zongsoft.Core.Tests>", TypeAlias.GetAlias(typeof(IDictionary<string, Gender[]>)), true);
	Assert.Equal("IDictionary<String, Zongsoft.Tests.Gender?[]@Zongsoft.Core.Tests>", TypeAlias.GetAlias(typeof(IDictionary<string, Gender?[]>)), true);

	Assert.Equal("Dictionary<Range<DateTime>, Zongsoft.Tests.Gender@Zongsoft.Core.Tests>", TypeAlias.GetAlias(typeof(Dictionary<Zongsoft.Data.Range<DateTime>, Gender>)), true);
	Assert.Equal("Dictionary<Range<DateTime>?, Zongsoft.Tests.Gender?@Zongsoft.Core.Tests>", TypeAlias.GetAlias(typeof(Dictionary<Zongsoft.Data.Range<DateTime>?, Gender?>)), true);
	Assert.Equal("Dictionary<Range<DateTime>[], Zongsoft.Tests.Gender[]@Zongsoft.Core.Tests>", TypeAlias.GetAlias(typeof(Dictionary<Zongsoft.Data.Range<DateTime>[], Gender[]>)), true);
	Assert.Equal("Dictionary<Range<DateTime>?[], Zongsoft.Tests.Gender?[]@Zongsoft.Core.Tests>", TypeAlias.GetAlias(typeof(Dictionary<Zongsoft.Data.Range<DateTime>?[], Gender?[]>)), true);

	var tupleType = typeof(Tuple<ValueTuple<Zongsoft.Data.Range<int>>, ValueTuple<Zongsoft.Data.Range<DateTime>?>, ValueTuple<Zongsoft.Data.Range<DateTime>[]>, ValueTuple<Zongsoft.Data.Range<DateTime>?[]>>);
	var tupleAlias = "Tuple<ValueTuple<Range<Int32>>, ValueTuple<Range<DateTime>?>, ValueTuple<Range<DateTime>[]>, ValueTuple<Range<DateTime>?[]>>";
	Assert.Equal(tupleAlias, tupleType.GetAlias());

	tupleType = typeof(ValueTuple<string, DateOnly?, byte[], Guid?[], Zongsoft.Data.Range<DateTime>?[], Zongsoft.Data.ConditionOperator?[]>);
	tupleAlias = "ValueTuple<String, Date?, Byte[], Guid?[], Range<DateTime>?[], Zongsoft.Data.ConditionOperator?[]@Zongsoft.Core>";
	Assert.Equal(tupleAlias, tupleType.GetAlias());

	tupleType = typeof(Nullable<>).MakeGenericType(tupleType);
	tupleAlias += '?';
	Assert.Equal(tupleAlias, tupleType.GetAlias());

	tupleType = tupleType.MakeArrayType();
	tupleAlias += "[]";
	Assert.Equal(tupleAlias, tupleType.GetAlias());
}
```

{% endcode %}

`GetAlias(assemblyless: true)` 可以省略程序集信息，适合只在当前上下文内展示类型名。

## 自定义别名

可以通过 `TypeAlias.Aliases.Map` 注册自定义别名。

自定义映射应通过 TypeAlias.Aliases 的注册契约维护。当前业务案例没有注册自定义类型别名；请结合 [TypeAlias 源码](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/TypeAlias.cs) 核对映射名称与类型解析范围。

自定义别名适合配置文件、脚本、参数包和数据模型映射中的短类型名。

这里采用框架 TypeAliasTest 中的实际断言，Zongsoft.Tests 类型来自测试项目。不要把测试程序集名复制到论坛的业务类型配置中。

## 相关资源

* [TypeAlias.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/TypeAlias.cs)
* [TypeAlias.Parser.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/TypeAlias.Parser.cs)
* [TypeAliasTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/TypeAliasTest.cs)


# 常用扩展

ArrayExtension、StringExtension、DateTimeExtension、TimeSpanUtility、TypeExtension、UriExtension 常用扩展。

`Zongsoft.Common` 提供一组常用扩展方法，用于数组、字符串、时间、类型和 URI 的日常处理。

## 类型关系

| 类型                  | 说明                           |
| ------------------- | ---------------------------- |
| `ArrayExtension`    | 数组空值判断和按运行时类型创建空数组。          |
| `StringExtension`   | 字符移除、字符串修剪、数字检测、脱敏、分片解析。     |
| `DateTimeExtension` | 时间重置、耗时计算。                   |
| `TimeSpanUtility`   | 时间间隔解析、限制到指定范围。              |
| `TypeExtension`     | 类型兼容性、泛型定义、集合类型、标量类型、默认值等判断。 |
| `UriExtension`      | 从 URL 查询字符串读取指定键。            |

## 字符串扩展

来源：[framework/Zongsoft.Core/test/Common/StringExtensionTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/StringExtensionTest.cs#L44)（节选；上下文见源文件）。

{% code title="StringExtensionTest.cs" %}

```csharp
public void TestSlice()
{
	var parts = StringExtension.Slice("a - b --  c  ", '-').ToArray();

	Assert.NotEmpty(parts);
	Assert.Equal(3, parts.Length);
	Assert.Equal("a", parts[0]);
	Assert.Equal("b", parts[1]);
	Assert.Equal("c", parts[2]);

	var hasColon = false;
	parts = StringExtension.Slice("issue-100-park:10001-1", chr => hasColon ? false : !(hasColon = chr == ':') && chr == '-').ToArray();

	Assert.NotEmpty(parts);
	Assert.Equal(3, parts.Length);
	Assert.Equal("issue", parts[0]);
	Assert.Equal("100", parts[1]);
	Assert.Equal("park:10001-1", parts[2]);
}
```

{% endcode %}

上面直接引用 [核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core) 的分片测试，覆盖空白、连续分隔符和带状态的分隔判断。RemoveAny、Trim、IsDigits 的实际输入与断言也在同一文件。`Slice` 会忽略空白分片，适合解析简单分隔文本。

## 时间扩展

`TimeSpanUtility` 支持 `ms`、`s`、`m`、`h`、`d` 等简写格式，也支持标准时间格式。

来源：[framework/Zongsoft.Core/test/Common/TimeSpanTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/TimeSpanTest.cs#L10)（节选；上下文见源文件）。

{% code title="TimeSpanTest.cs" %}

```csharp
public void Clamp()
{
	var minimum = TimeSpan.FromHours(1);
	var maximum = TimeSpan.FromHours(12);

	var duration = TimeSpanUtility.Clamp(new TimeSpan(0, 2, 30, 59), minimum, maximum);
	Assert.Equal(new TimeSpan(0, 2, 30, 59), duration);

	duration = TimeSpanUtility.Clamp(TimeSpan.Zero, minimum, maximum);
	Assert.Equal(minimum, duration);
	duration = TimeSpanUtility.Clamp(TimeSpan.MinValue, minimum, maximum);
	Assert.Equal(minimum, duration);
	duration = TimeSpanUtility.Clamp(new TimeSpan(0, 0, 30, 40), minimum, maximum);
	Assert.Equal(minimum, duration);

	duration = TimeSpanUtility.Clamp(TimeSpan.MaxValue, minimum, maximum);
	Assert.Equal(maximum, duration);
	duration = TimeSpanUtility.Clamp(new TimeSpan(1, 0, 0, 1), minimum, maximum);
	Assert.Equal(maximum, duration);
}
```

{% endcode %}

`DateTimeExtension.GetElapsed` 可以计算从某个时间点到现在的耗时。Discussions 的文件上传控制器以千年纪元至今的天数加随机串生成文件名；下列是 UploadAsync 回调参数片段，完整方法见源码链接。这个日期差使用墙上时钟，测量代码执行耗时应使用专门的计时工具。

来源：[src/api/Controllers/FileController.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/api/Controllers/FileController.cs#L81)（节选；上下文见源文件）。

{% code title="FileController.cs" %}

```csharp
  args => args.FileName = $"{Timestamp.Millennium.Epoch.GetElapsed().Days}-{Randomizer.GenerateString()}", cancellation);
```

{% endcode %}

## 类型扩展

`TypeExtension` 提供比 `Type.IsAssignableFrom` 更灵活的泛型判断能力。

来源：[framework/Zongsoft.Core/test/Common/TypeExtensionTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/TypeExtensionTest.cs#L14)（节选；上下文见源文件）。

{% code title="TypeExtensionTest.cs" %}

```csharp
public void TestIsAssignableFrom()
{
	var baseType = typeof(ICollection<Person>);
	var instanceType = typeof(Collection<Person>);

	Assert.True(TypeExtension.IsAssignableFrom(baseType, instanceType));
	Assert.True(baseType.IsAssignableFrom(instanceType));

	baseType = typeof(ICollection<>);

	Assert.True(TypeExtension.IsAssignableFrom(baseType, instanceType));
	Assert.False(baseType.IsAssignableFrom(instanceType));

	Assert.True(TypeExtension.IsAssignableFrom(typeof(IService<>), typeof(EmployeeService)));
	Assert.True(TypeExtension.IsAssignableFrom(typeof(PersonServiceBase<>), typeof(EmployeeService)));

	Assert.True(TypeExtension.IsAssignableFrom(typeof(IService<>), typeof(EmployeeService), out var genericTypes));
	Assert.NotEmpty(genericTypes);
	Assert.Single(genericTypes);
	Assert.Same(genericTypes[0], typeof(IService<Employee>));

	Assert.True(TypeExtension.IsAssignableFrom(typeof(PersonServiceBase<>), typeof(EmployeeService), out genericTypes));
	Assert.NotEmpty(genericTypes);
	Assert.Single(genericTypes);
	Assert.Same(genericTypes[0], typeof(PersonServiceBase<Employee>));
}
```

{% endcode %}

上面来自 TypeExtensionTest，所用集合类型都是测试输入；可复用完整测试来核对泛型匹配行为。它还可以判断集合、列表、字典、哈希集、数值类型、可空类型和标量类型。

## URI 扩展

Discussions 与框架项目中暂未找到该扩展的调用用例。[UriExtension 的实现](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/UriExtension.cs)按 & 和 = 拆分 Query，并忽略键名大小写；它没有完整的 URI 解码流程，也没有主动移除 Query 首部的问号。Web 请求应优先使用框架控制器的请求查询集合，避免将此工具当成完整的 URL 参数解析器。

## 相关资源

* [ArrayExtension.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/ArrayExtension.cs)
* [StringExtension.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/StringExtension.cs)
* [DateTimeExtension.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/DateTimeExtension.cs)
* [TimeSpanUtility.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/TimeSpanUtility.cs)
* [TypeExtension.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/TypeExtension.cs)
* [UriExtension.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Common/UriExtension.cs)
* [StringExtensionTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/StringExtensionTest.cs)
* [TimeSpanTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/TimeSpanTest.cs)
* [TypeExtensionTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Common/TypeExtensionTest.cs)


# Zongsoft.Communication

Zongsoft.Communication 命名空间的职责和主要类型。

`Zongsoft.Communication` 提供通讯通道、监听器、收发器、请求器和协议包解析等基础抽象。它不绑定具体传输协议，具体网络实现通常由 `Zongsoft.Net` 等模块承接。

## 主要职责

* 定义 `IChannel`、`IListener`、`IReceiver`、`IRequester` 等通讯抽象。
* 提供通道基类、通道集合、通道选择器和通知器模型。
* 提供 `IPacketizer` 等协议包解析扩展点。
* 为网络层或自定义通讯协议提供统一的设计入口。

## 类型

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref">页面</th></tr></thead><tbody><tr><td><strong>常规通讯</strong></td><td>收发器、监听器、通道和协议包解析。</td><td><a href="/framework/core/communication/general">常规通讯</a></td></tr><tr><td><strong>请求应答</strong></td><td>多响应请求、响应关联和 ZeroMQ 实现。</td><td><a href="/framework/core/communication/request-response">请求应答</a></td></tr><tr><td><strong>Notifier</strong></td><td>通知激发器抽象和当前实现状态。</td><td><a href="/framework/core/communication/notifier">通知器</a></td></tr><tr><td><strong>Transmitter</strong></td><td>模板通知发送器、描述符和 GUI 配置场景。</td><td><a href="/framework/core/communication/transmitter">发射器</a></td></tr></tbody></table>

## 相关资源

* [Communication 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Communication)
* [Zongsoft.Net 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Net)


# 常规通讯

ISender、IReceiver、Listener、Channel 和 IPacketizer 常规通讯抽象。

常规通讯抽象把“如何发送、如何接收、如何监听、如何关闭通道、如何打包拆包”拆成几组小接口。核心框架只定义协议边界，具体网络连接、消息队列连接、事件通道等实现由 `Zongsoft.Net`、`Zongsoft.Messaging.*` 或其它扩展项目承接。

## 类型关系

| 类型                                 | 说明                                                                                                                                                                                    |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ISender`                          | 发送字节数据。                                                                                                                                                                               |
| `ISender<T>`                       | 发送强类型数据包。                                                                                                                                                                             |
| `IReceiver`                        | 接收 [`ReadOnlySequence<byte>`](https://learn.microsoft.com/zh-cn/dotnet/api/system.buffers.readonlysequence-1) [*源码*](https://source.dot.net/#System.Memory/ReadOnlySequence.cs) 字节序列。 |
| `IListener<T>`                     | 监听并处理强类型通讯包。                                                                                                                                                                          |
| `ListenerBase<T>`                  | 监听器基类，实现接收、拆包和交给处理器的流程。                                                                                                                                                               |
| `IChannel`                         | 可关闭、可异步释放的通讯通道。                                                                                                                                                                       |
| `ChannelBase`                      | 通道基类，统一关闭状态、关闭事件和释放流程。                                                                                                                                                                |
| `IChannelSelector<T>`              | 根据键选择一个通道。                                                                                                                                                                            |
| `IPacketizer<TPackage>`            | 强类型通讯包的打包和拆包接口。                                                                                                                                                                       |
| `IPacketizer<TRequest, TResponse>` | 请求/应答模式下的请求打包和响应拆包接口。                                                                                                                                                                 |

{% hint style="info" %}
源码中没有独立的 `Listener` 类型；当前监听器概念由 `IListener<T>` 和 `ListenerBase<T>` 承载，`Zongsoft.Net.TcpServer<T>` 是一个典型实现。
{% endhint %}

## Channel 设计思想

`Channel` 是“通讯端点”的生命周期抽象，而不是某一种协议的代名词。它表达的是：这个端点是否关闭、如何关闭、关闭后如何通知外部，以及是否可以被选择器或管理器持有。

```mermaid
flowchart LR
	Listener["ListenerBase<T>"] --> Packetizer["IPacketizer<T>"]
	Packetizer --> Handler["IHandler<T>"]
	Channel["ChannelBase / IChannel"] --> Sender["ISender / ISender<T>"]
	Channel --> Close["CloseAsync / Closed"]
	Selector["IChannelSelector<T>"] --> Channel
```

这个设计让不同通讯形态可以共享同一个生命周期词汇：

* TCP 连接通道：`TcpChannelBase<T>` 继承 `ChannelBase`，同时实现 `ISender` 和 `ISender<T>`。
* 事件通道：`IEventChannel` 继承 `IChannel` 和 `ISender<EventContext>`，可把事件发送到远端。
* 消息队列通道：`ZeroQueueEventChannel` 继承 `ChannelBase`，把事件编码后投递到 ZeroMQ 队列。
* 消息消费者：`MessageConsumerBase<TQueue>` 继承 `ChannelBase`，把订阅生命周期也纳入通道关闭模型。

## 接收与拆包

`ListenerBase<T>` 实现了从字节到强类型包的模板流程：收到字节序列后调用 `IPacketizer<T>.Unpack`，拆包成功后再调用处理器。

来源：[framework/Zongsoft.Core/src/Communication/ListenerBase.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/ListenerBase.cs#L61)（节选；上下文见源文件）。

{% code title="ListenerBase.cs" %}

```csharp
protected virtual ValueTask OnReceiveAsync(in ReadOnlySequence<byte> data, CancellationToken cancellation)
{
	var message = data;

	if(this.OnDeserialize(ref message, out var value))
	{
		var task = this.OnHandleAsync(value, cancellation);

		if(!task.IsCompletedSuccessfully)
			return new ValueTask(task.AsTask());
	}

	return ValueTask.CompletedTask;
}
```

{% endcode %}

`IPacketizer<T>` 适合处理粘包、半包、长度头、压缩、加密和自定义二进制协议。它的 `Unpack` 接收 `ref ReadOnlySequence<byte>`，实现者可以在拆出一个包后推进序列，让上层继续处理缓冲区中的后续包。

来源：[framework/Zongsoft.Core/src/Communication/IPacketizer.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/IPacketizer.cs#L39)（节选；上下文见源文件）。

{% code title="IPacketizer.cs" %}

```csharp
public interface IPacketizer<TPackage>
{
	/// <summary>打包，将通讯包对象序列化到发送缓存。</summary>
	/// <param name="writer">缓存写入器。</param>
	/// <param name="package">待打包的通讯包。</param>
	void Pack(IBufferWriter<byte> writer, in TPackage package);

	/// <summary>拆包，将字节流反序列化成通讯包对象。</summary>
	/// <param name="data">待拆包的字节流。</param>
	/// <param name="package">拆包成功的通讯包。</param>
	/// <returns>如果拆包完成则返回真(<c>True</c>)，否则返回假(<c>False</c>)。</returns>
	bool Unpack(ref ReadOnlySequence<byte> data, out TPackage package);
}
```

{% endcode %}

## Zongsoft.Net 中的实现

`Zongsoft.Net` 把这些抽象落到 TCP 网络通讯上：

* `TcpServer<T>` 继承 `ListenerBase<T>`，持有 `IPacketizer<T>`，负责监听并创建 TCP 通道。
* `TcpChannelBase<T>` 继承 `ChannelBase`，负责单连接的发送、接收、打包、拆包和关闭。
* `TcpServerChannelManager<T>` 维护服务端通道集合，并在客户端连接时创建通道。
* `Packetizer` 提供无头包和长度头包两种基础拆包实现。

来源：[framework/Zongsoft.Net/samples/server/Program.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Net/samples/server/Program.cs#L18)（节选；上下文见源文件）。

{% code title="Program.cs" %}

```csharp
var server = TcpServer.Headed;
server.Handler = new Handler(server);
await server.StartAsync(args.Length == 0 ? ["127.0.0.1", "7969"] : args);
```

{% endcode %}

上面来自 framework 的 TCP 服务端样例；Handler 是同一 Program.cs 的内部类型，接收文本后广播 ACK 回复。Discussions 没有直接托管 TCP 通道。配套客户端、启动参数和关闭行为见[网络通讯](/framework/net)。

## 现实场景

通道模型适合任何“有打开/关闭生命周期，并可发送或接收数据”的场景。网络连接只是其中一种；事件总线、消息队列订阅、设备连接、网关会话也可以成为通道。

<details>

<summary>什么时候需要实现自己的 Channel</summary>

* 需要管理一条可关闭的远端连接，例如 WebSocket、TCP、串口或设备会话。
* 需要把消息队列订阅封装成可释放对象。
* 需要统一处理关闭事件，避免调用方直接依赖底层驱动。
* 需要通过 `IChannelSelector<T>` 根据租户、用户、设备或主题选择发送通道。

</details>

## 相关资源

* [ISender.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/ISender.cs)
* [ISender\<T>.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/ISender%601.cs)
* [IReceiver.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/IReceiver.cs)
* [IListener.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/IListener.cs)
* [ListenerBase.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/ListenerBase.cs)
* [IChannel.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/IChannel.cs)
* [ChannelBase.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/ChannelBase.cs)
* [IChannelSelector.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/IChannelSelector.cs)
* [IPacketizer.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/IPacketizer.cs)
* [Zongsoft.Net README](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Net/README.md)
* [TcpChannelBase.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Net/src/TcpChannelBase.cs)
* [ZeroQueueEventChannel.cs](https://github.com/Zongsoft/framework/blob/main/messaging/zero/src/ZeroQueueEventChannel.cs)


# 请求应答

IRequest、IRequester 和 IResponse、IResponder 请求应答抽象。

请求应答抽象用于消息型通讯中的“发起请求、等待一个或多个响应、按请求标识关联结果”。它不是 HTTP 请求模型，也不是只能一问一答；一次请求可以收到多次响应，调用方通过 `IRequestToken` 获取已到达的响应。

## 类型关系

| 类型              | 说明                                |
| --------------- | --------------------------------- |
| `IRequest`      | 请求对象，包含 `Url`、`Identifier` 和请求数据。 |
| `IRequestToken` | 请求令牌，负责枚举当前请求对应的响应。               |
| `IRequester`    | 请求发起方，负责发送请求并接收响应回调。              |
| `IResponder`    | 请求处理方，负责接收请求并发送响应。                |
| `IResponse`     | 响应对象，包含响应地址、响应数据和关联请求。            |

## 它解决什么问题

消息队列、ZeroMQ、事件总线这类系统通常没有天然的同步返回值。请求应答模型把一次业务请求拆成三件事：

1. 请求方发送带 `Identifier` 的请求消息。
2. 响应方处理请求，并把同一个 `Identifier` 写回响应消息。
3. 请求方收到响应后，根据 `Identifier` 找到请求令牌，把响应放入令牌。

```mermaid
sequenceDiagram
	participant Caller as Requester
	participant Queue as Queue/Topic
	participant Worker as Responder
	Caller->>Queue: request(url, identifier, data)
	Worker->>Queue: subscribe(url)
	Queue->>Worker: request
	Worker->>Queue: response(url/reply, identifier, data)
	Queue->>Caller: response
	Caller->>Caller: token.Response(response)
```

## 发起请求

Discussions 的 HTTP 控制器不使用这组请求应答抽象。下面采用 ZeroMQ 已有的 rpc/echo 测试：ZeroServerScope 启动测试 Broker，两个队列分别负责请求与应答，EchoHandler 原样返回载荷。完整测试依赖同目录夹具，并受 Global.IsTestingEnabled 开关控制。

请求方调用 `RequestAsync` 后会得到 `IRequestToken`。这个 token 不是响应本身，而是响应收集器；调用方可以立即枚举已到达的响应，也可以给一个等待时长。

来源：[framework/messaging/zero/test/ZeroRequesterTests.cs](https://github.com/Zongsoft/framework/blob/main/messaging/zero/test/ZeroRequesterTests.cs#L16)（节选；上下文见源文件）。

{% code title="ZeroRequesterTests.cs" %}

```csharp
public async Task RequesterReceivesImmediateResponses()
{
	if(!Global.IsTestingEnabled)
		return;

	using var server = await ZeroServerScope.StartAsync();
	using var requesterQueue = ZeroTestUtility.CreateQueue(server.Port, "requester");
	using var responderQueue = ZeroTestUtility.CreateQueue(server.Port, "responder");

	var responder = new ZeroResponder { Queue = responderQueue };
	responder.Handlers.Add(new EchoHandler());
	await responder.StartAsync([]);

	try
	{
		await using var requester = new ZeroRequester { Queue = requesterQueue };

		for(int i = 0; i < 20; i++)
		{
			using var token = await requester.RequestAsync("rpc/echo", Encoding.UTF8.GetBytes($"message-{i}"));
			Assert.NotNull(token);

			var response = token.GetResponses(TimeSpan.FromSeconds(5)).FirstOrDefault();
			Assert.NotNull(response);
			Assert.Equal($"message-{i}", Encoding.UTF8.GetString(response.Data.Span));
			Assert.Equal(token.Request.Identifier, response.Request.Identifier);
		}
	}
	finally
	{
		await responder.StopAsync([]);
		((IDisposable)responder).Dispose();
	}
}
```

{% endcode %}

如果业务只允许一个响应，遍历时取第一条即可；如果允许多个服务节点同时响应，就可以把多个响应一起收集、排序、聚合或择优。

## 处理请求并响应

响应方通常把 `IResponder` 注入到请求处理器的参数中，处理器拿到请求后调用 `request.Response(...)` 创建关联响应，再调用 `RespondAsync` 发回。

来源：[framework/messaging/zero/test/ZeroTestUtility.cs](https://github.com/Zongsoft/framework/blob/main/messaging/zero/test/ZeroTestUtility.cs#L357)（节选；上下文见源文件）。

{% code title="ZeroTestUtility.cs" %}

```csharp
[Handler("rpc/echo")]
internal sealed class EchoHandler : HandlerBase<IRequest>
{
	protected override async ValueTask OnHandleAsync(IRequest request, Parameters parameters, CancellationToken cancellation)
	{
		var responder = parameters.GetValue<IResponder>();
		Assert.NotNull(responder);
		await responder.RespondAsync(request.Response(request.Data), cancellation);
	}
}
```

{% endcode %}

EchoHandler 的路由声明是 rpc/echo，回应由 request.Response(request.Data) 产生，保留请求关联信息。它是测试夹具，不是 Discussions 业务处理器。

## ZeroMQ 实现要点

`messaging/zero` 中的实现可以帮助理解这组接口：

* `ZeroRequest` 把请求标识和请求数据打包成 `identifier + '\n' + data`。
* `ZeroRequester` 先把 token 登记到待处理集合，再在发送请求前订阅 `url + "/reply"`，再向 `url` 投递请求。
* `ZeroResponse` 同样把 `identifier + '\n' + data` 打包到响应消息里。
* `ZeroResponder` 根据处理器 URL 订阅请求主题，收到请求后调用处理器。
* `ZeroRequester.Adapter` 收到响应后解包标识，找到对应 token，并把响应放入 token。

来源：[framework/messaging/zero/src/ZeroRequester.cs](https://github.com/Zongsoft/framework/blob/main/messaging/zero/src/ZeroRequester.cs#L97)（节选；上下文见源文件）。

{% code title="ZeroRequester.cs" %}

```csharp
public async ValueTask<IRequestToken> RequestAsync(string url, ReadOnlyMemory<byte> data, CancellationToken cancellation = default)
{
	if(Volatile.Read(ref _disposed) != 0)
		throw new ObjectDisposedException(nameof(ZeroRequester));

	var queue = this.Queue;
	if(queue == null)
		return null;

	var request = new ZeroRequest(url, data);
	var token = new Token(request, request => this.Remove(request.Identifier));

	if(!_tokens.TryAdd(request.Identifier, token))
		return null;

	try
	{
		await this.SubscribeAsync(queue, url + "/reply", cancellation);
		await queue.ProduceAsync(url, request.Pack(), null, cancellation);
		return token;
	}
	catch
	{
		token.Dispose();
		throw;
	}
}
```

{% endcode %}

{% hint style="warning" %}
`IRequestToken.GetResponses(timeout)` 会在遍历期间等待响应，适合短时等待；长时间监听或实时处理响应时，更适合通过响应处理器处理 `IRequester.OnRespondedAsync` 的回调。
{% endhint %}

请求登记发生在发送之前，因而即使响应立即到达，适配器也能找到 token。发送或订阅失败时会释放本次 token；同一回复主题的初始化任务会复用。调用方仍应在用完响应后释放 token，不能假定所有无响应请求都会被定时清理；当前过期缓存是在收到响应时设置的。

ZeroRequester 拥有回复订阅和待处理请求，释放它会清理这些资源；它使用的队列由外部提供。已经建立订阅后不允许把 Queue 替换为另一个实例。

## 相关资源

* [IRequest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/IRequest.cs)
* [IRequestToken.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/IRequestToken.cs)
* [IRequester.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/IRequester.cs)
* [IResponse.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/IResponse.cs)
* [IResponder.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/IResponder.cs)
* [ZeroRequester.cs](https://github.com/Zongsoft/framework/blob/main/messaging/zero/src/ZeroRequester.cs)
* [ZeroResponder.cs](https://github.com/Zongsoft/framework/blob/main/messaging/zero/src/ZeroResponder.cs)
* [ZeroRequest.cs](https://github.com/Zongsoft/framework/blob/main/messaging/zero/src/ZeroRequest.cs)
* [ZeroResponse.cs](https://github.com/Zongsoft/framework/blob/main/messaging/zero/src/ZeroResponse.cs)


# 通知器

INotifier 通知激发器接口和实现状态。

`INotifier` 是通知激发器接口，用于向指定接收者激发一条命名通知。它比 `ITransmitter` 更泛化：通知内容、接收者、设置对象和返回结果都由具体实现自行定义。

## 接口成员

| 成员            | 说明      |
| ------------- | ------- |
| `Notify`      | 同步激发通知。 |
| `NotifyAsync` | 异步激发通知。 |

来源：[framework/Zongsoft.Core/src/Communication/INotifier.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/INotifier.cs#L38)（节选；上下文见源文件）。

{% code title="INotifier.cs" %}

```csharp
public interface INotifier
{
	/// <summary>激发一个通知给指定的接受者。</summary>
	/// <param name="name">指定要激发的通知名。</param>
	/// <param name="content">指定的通知内容。</param>
	/// <param name="destination">指定通知的接受者，由具体实现者定义支持的接受者类型。</param>
	/// <param name="settings">指定的通知设置。</param>
	/// <returns>返回通知结果对象，由具体实现者定义。</returns>
	object Notify(string name, object content, object destination, object settings = null);

	/// <summary>异步激发一个通知给指定的接受者。</summary>
	/// <param name="name">指定要激发的通知名。</param>
	/// <param name="content">指定的通知内容。</param>
	/// <param name="destination">指定通知的接受者，由具体实现者定义支持的接受者类型。</param>
	/// <param name="settings">指定的通知设置。</param>
	/// <returns>返回通知结果对象，由具体实现者定义。</returns>
	ValueTask<object> NotifyAsync(string name, object content, object destination, object settings = null);
}
```

{% endcode %}

## 与 Transmitter 的区别

`INotifier` 和 `ITransmitter` 都可以用于“通知”，但关注点不同：

| 抽象             | 关注点                     | 适合场景                    |
| -------------- | ----------------------- | ----------------------- |
| `INotifier`    | 激发一条命名通知，目标、设置和结果由实现定义。 | 站内通知、会话通知、桌面推送、自定义业务提醒。 |
| `ITransmitter` | 按发送器、通道、模板和参数发送模板化信息。   | 模板短信、语音通知、微信模板消息、验证码发送。 |

## Discussions 的站内消息路径

当前 Discussions 和 framework 中没有可供复用的 INotifier 完整实现。Discussions 使用 MessageService 保存站内消息与接收者关系；它不是 INotifier 的实现，也不负责把消息实时推送到在线会话。

来源：[src/Services/MessageService.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/MessageService.cs#L49)（节选；上下文见源文件）。

{% code title="MessageService.cs" %}

```csharp
public int Send(Message message, IEnumerable<uint> users)
{
	if(message == null)
		throw new ArgumentNullException(nameof(message));

	if(users == null || !users.Any())
		return 0;

	using(var transaction = new Transaction())
	{
		//插入消息
		if(this.Insert(message) < 1)
			return 0;

		//插入用户消息
		var count = this.DataAccess.InsertMany(users.Select(uid => new UserMessage(uid, message.MessageId)));

		//提交事务
		transaction.Commit();

		return count;
	}
}
```

{% endcode %}

Send 接收消息对象及用户编号集合，在事务中写入消息和 UserMessage。读取状态由 MessageService 的查询钩子更新。若需要理解通用模板短信的真实发送过程，请继续阅读 [Transmitter](/framework/core/communication/transmitter)。

## 相关资源

* [INotifier.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/INotifier.cs)
* [Communication 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Communication)


# 发射器

ITransmitter、TransmitterDescriptor、TransmitterHandler 和模板通知发送。

`Transmitter` 是模板化消息发送模型。它把“由谁发送、通过哪个通道发送、使用哪个模板、模板需要哪些参数、发给谁”拆成可描述、可配置、可执行的结构，适合短信、语音、微信模板消息、验证码和业务通知。

## 类型关系

| 类型                                         | 说明                    |
| ------------------------------------------ | --------------------- |
| `ITransmitter`                             | 模板信息发送器接口。            |
| `ITransmitterArgumenter`                   | 将处理器参数集转换为模板参数对象。     |
| `TransmitterDescriptor`                    | 发送器元数据，描述名称、标题、通道和模板。 |
| `TransmitterDescriptor.Channel`            | 发送通道，例如短信、语音、微信模板消息。  |
| `TransmitterDescriptor.Template`           | 模板定义。                 |
| `TransmitterDescriptor.Template.Parameter` | 模板参数定义。               |
| `TransmitterHandler`                       | 处理器入口，按参数查找发送器并调用发送。  |
| `TransmitterUtility`                       | 构建描述符的链式扩展方法。         |

## 为什么需要 Descriptor

`TransmitterDescriptor` 不是单纯的说明文字。它可以让后台或 GUI 动态知道：

* 系统里有哪些发送器，例如 `Phone`、`Wechat`。
* 每个发送器有哪些通道，例如 `message`、`voice`。
* 每个通道有哪些模板。
* 每个模板需要哪些参数。

这对“用户在界面上定义通知规则”很关键。界面可以读取描述符后，生成通道下拉框、模板下拉框、参数输入表单，再把用户配置保存为可执行的 `TransmitterHandler.Argument`。

## 构建描述符

Discussions 当前没有模板短信或语音发送用例。framework 的 Aliyun PhoneTransmitter 从真实选项集合读取短信、语音模板，创建描述符。模板标识和参数来自已配置的服务，不在文档中另造收件人或告警模板。

来源：[framework/externals/aliyun/src/Telecom/PhoneTransmitter.cs](https://github.com/Zongsoft/framework/blob/main/externals/aliyun/src/Telecom/PhoneTransmitter.cs#L65)（节选；上下文见源文件）。

{% code title="PhoneTransmitter.cs" %}

```csharp
public TransmitterDescriptor Descriptor
{
	get
	{
		if(_descriptor == null)
		{
			_descriptor = new TransmitterDescriptor(this.Name, AnnotationUtility.GetDisplayName(this.GetType()), AnnotationUtility.GetDescription(this.GetType()));

			var channel = _descriptor.Channel(MESSAGE_CHANNEL, Properties.Resources.Text_Phone_Message);
			foreach(var option in this.Phone.Options.Message.Templates)
			{
				var template = channel.Template(option.Name);

				foreach(var parameter in option.Parameters)
					template.Parameter(parameter.Name, parameter.Title, parameter.Description);
			}

			channel = _descriptor.Channel(VOICE_CHANNEL, Properties.Resources.Text_Phone_Voice);
			foreach(var option in this.Phone.Options.Voice.Templates)
			{
				var template = channel.Template(option.Name);

				foreach(var parameter in option.Parameters)
					template.Parameter(parameter.Name, parameter.Title, parameter.Description);
			}
		}

		return _descriptor;
	}
}
```

{% endcode %}

`TransmitterUtility` 提供 `Channel`、`Template`、`Parameter` 扩展方法，方便发送器在运行时从配置或远端服务中构造描述符。

## 执行发送

`ITransmitter` 的核心方法是 `TransmitAsync`。`destination` 是目的地，例如手机号、OpenId、邮箱或自定义地址；`channel` 是通道；`template` 是模板标识；`data` 是模板参数对象。下面是框架 Secretor 完成方案和验证码验证后生成确认码、委托发送器投递的实际调用片段；其变量来自外层 TransmitAsync 方法，不能脱离验证步骤直接暴露为公共发送接口。

来源：[framework/Zongsoft.Core/src/Security/Secretor.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Secretor.cs#L355)（节选；上下文见源文件）。

{% code title="Secretor.cs" %}

```csharp
var token = GetKey(scheme, destination, template, scenario, channel);
var value = await _secretor.GenerateAsync(token, null, extra, cancellation);

//发送验证码到目的地
await transmitter.TransmitAsync(destination, channel, template, new SecretTemplateData(value), cancellation);
```

{% endcode %}

`TransmitterHandler` 则把发送动作包装成通用处理器。它会按 `Argument.Name` 查找 `ITransmitter`，再按 `Argument.Channel`、`Argument.Template` 和 `Argument.Destination` 执行发送。

来源：[framework/Zongsoft.Core/src/Communication/TransmitterHandler.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/TransmitterHandler.cs#L49)（节选；上下文见源文件）。

{% code title="TransmitterHandler.cs" %}

```csharp
protected override ValueTask OnHandleAsync(Argument argument, Collections.Parameters parameters, CancellationToken cancellation)
{
	if(argument == null)
		return ValueTask.CompletedTask;

	//获取指定名称的发送器
	var transmitter = _serviceProvider.FindRequired<ITransmitter>(argument.Name);

	//获取指定的发送通道
	if(!transmitter.Descriptor.Channels.TryGetValue(argument.Channel ?? string.Empty, out var channel))
		channel = transmitter.Descriptor.Channels.Count > 0 ? transmitter.Descriptor.Channels[0] : null;

	//如果没有指定参数对象则尝试通过参数转换器将参数集转换成发送模板的参数对象
	if(argument.Parameter == null)
	{
		//获取指定的发送器参数转换器
		var argumenter = _serviceProvider.Find<ITransmitterArgumenter>(argument);

		if(argumenter != null)
			argument.Parameter = argumenter.GetArgument(transmitter, channel?.Name ?? argument.Channel, argument.Template, argument.Parameter, parameters);
		else
			argument.Parameter = parameters;
	}

	//执行发送任务
	return transmitter.TransmitAsync(argument.Destination, argument.Channel, argument.Template, argument.Parameter, cancellation);
}
```

{% endcode %}

如果 `Argument.Parameter` 为空，`TransmitterHandler` 会尝试从服务容器查找 `ITransmitterArgumenter`，把当前处理器参数集转换为模板参数对象。这让 GUI 可以只保存“模板参数映射规则”，运行时再由参数转换器生成最终对象。

## Aliyun PhoneTransmitter

`externals/aliyun` 中的 `PhoneTransmitter` 是最典型的实现：

* `Name` 固定为 `Phone`。
* `Descriptor` 根据 `Phone.Options.Message.Templates` 生成 `message` 通道。
* `Descriptor` 根据 `Phone.Options.Voice.Templates` 生成 `voice` 通道。
* `TransmitAsync` 中，`message` 通道调用 `Phone.SendAsync`，`voice` 通道调用 `Phone.CallAsync`。

来源：[framework/externals/aliyun/src/Telecom/PhoneTransmitter.cs](https://github.com/Zongsoft/framework/blob/main/externals/aliyun/src/Telecom/PhoneTransmitter.cs#L98)（节选；上下文见源文件）。

{% code title="PhoneTransmitter.cs" %}

```csharp
public async ValueTask TransmitAsync(string destination, string channel, string template, object data, CancellationToken cancellation)
{
	if(data == null)
		throw new ArgumentNullException(nameof(data));

	if(string.IsNullOrEmpty(channel) || string.Equals(channel, MESSAGE_CHANNEL, StringComparison.OrdinalIgnoreCase))
		await this.Phone.SendAsync(template, new[] { destination }, data, cancellation:cancellation);
	else if(string.Equals(channel, VOICE_CHANNEL, StringComparison.OrdinalIgnoreCase))
		await this.Phone.CallAsync(template, destination, data, cancellation:cancellation);
	else
		throw new ArgumentException($"Unsupported ‘{channel}’ channel.", nameof(channel));
}
```

{% endcode %}

## GUI 配置场景

以下是基于描述符能力的界面设计建议，Discussions 尚未实现这样的通知规则界面：

{% stepper %}
{% step %}
读取服务容器中所有 `ITransmitter`，展示发送器列表，例如 `Phone`、`Wechat`。
{% endstep %}

{% step %}
读取选中发送器的 `Descriptor.Channels`，展示短信、语音、微信模板消息等通道。
{% endstep %}

{% step %}
读取选中通道的 `Templates`，展示模板列表和参数定义。
{% endstep %}

{% step %}
保存用户选择的发送器、通道、模板、目的地表达式和参数映射。
{% endstep %}

{% step %}
运行时构造 `TransmitterHandler.Argument`，交给 `TransmitterHandler` 或直接调用 `ITransmitter.TransmitAsync`。
{% endstep %}
{% endstepper %}

这套模型特别适合验证码、用户绑定手机/邮箱、找回密码、告警通知、审批提醒等“模板固定、参数变化、接收人动态”的场景。

## 相关资源

* [ITransmitter.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/ITransmitter.cs)
* [ITransmitterArgumenter.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/ITransmitterArgumenter.cs)
* [TransmitterDescriptor.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/TransmitterDescriptor.cs)
* [TransmitterDescriptor.partials.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/TransmitterDescriptor.partials.cs)
* [TransmitterHandler.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/TransmitterHandler.cs)
* [TransmitterUtility.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Communication/TransmitterUtility.cs)
* [PhoneTransmitter.cs](https://github.com/Zongsoft/framework/blob/main/externals/aliyun/src/Telecom/PhoneTransmitter.cs)
* [Wechat Transmitter.cs](https://github.com/Zongsoft/framework/blob/main/externals/wechat/src/Transmitter.cs)
* [Secretor.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Secretor.cs)


# Zongsoft.Components

Zongsoft.Components 命名空间及其子命名空间的职责。

`Zongsoft.Components` 是[核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core)中的组件模型命名空间，覆盖命令、执行管线、处理器、过滤器、转换器、可监管对象、工作者、标识和事件交换等基础构件。它的目标不是提供某个具体业务能力，而是为不同领域的业务对象提供一组可组合、可扩展、可被宿主程序复用的运行时抽象。

## 主要职责

* 定义组件标识、别名、权重、命名对象和服务描述，让组件可以被发现、描述和选择。
* 提供命令模式、命令树、命令表达式解析、命令出口和参数绑定，用于跨领域、低耦合地触发业务动作。
* 提供执行管线、处理器和过滤器，把上下文、执行逻辑和横切特性组织为可扩展的执行单元。
* 提供[工作器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)、监视器、均衡器、尝试器等运行时组件模式，用于后台任务、状态观测和失败控制。
* 提供常用转换器、状态机和组件特性扩展，降低基础设施代码的重复实现。

## 命令模式

命令模式是 `Zongsoft.Components` 中最常被跨模块复用的组件模型之一。它把一次操作描述为一段命令表达式，并由 `CommandExecutor` 在命令树中定位 `CommandNode`、创建 `CommandContext`、调用 `ICommand` 实例。命令不依赖特定容器或宿主：同一组命令既可以被后台服务或 Web 程序直接调用，也可以被终端宿主加载为交互式命令。

这种模式特别适合跨领域调用。调用方只需要知道“要执行什么动作”和“传递哪些参数”，不必引用具体服务接口。例如短信发送、队列订阅、文件操作、配置读取等能力，都可以通过命令表达式进入统一执行管线。通用命令的具体实现主要位于 `Zongsoft.Commands` 项目，终端程序中的命令交互则由 `Zongsoft.Terminals` 基于同一套模型实现。

{% content-ref url="/pages/NrKxcv3CuzLXR9b1lXTf" %}
[命令](/framework/core/components/commands)
{% endcontent-ref %}

## 子命名空间

| 命名空间                             | 说明                                                                                                                                   |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `Zongsoft.Components`            | 命令模型的核心类型，例如 `ICommand`、`CommandBase`、`CommandContext`、`CommandNode`、`CommandLine` 和 `CommandExecutor`。                              |
| `Zongsoft.Components.Commands`   | 基于命令模型实现的内置组件命令，例如[工作器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)的启动、停止、暂停、恢复和状态查询命令。 |
| `Zongsoft.Components.Converters` | 布尔、枚举、版本、架构等常用转换器。                                                                                                                   |
| `Zongsoft.Components.Features`   | Retry、Fallback、Breaker、Throttle、Timeout 等执行特性模型。                                                                                     |
| `Zongsoft.Components.States`     | 状态机、状态图、状态上下文、状态处理器和状态流转。                                                                                                            |

## 类型

<table data-view="cards"><thead><tr><th></th><th data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td>标识：对象标识抽象和统一标识值。</td><td><a href="/framework/core/components/identifier">标识</a></td></tr><tr><td>AliasAttribute：为类型、成员、参数等声明别名。</td><td><a href="/framework/core/components/alias-attribute">别名</a></td></tr><tr><td>命令：低耦合的跨领域调用模型、命令行解析和终端交互基础。</td><td><a href="/framework/core/components/commands">命令</a></td></tr><tr><td>事件：事件描述、事件定位、事件交换和插件化处理器挂载。</td><td><a href="/framework/core/components/events">事件</a></td></tr><tr><td>Worker：可启动、停止、暂停、恢复的后台工作者模型。</td><td><a href="/framework/core/components/worker">工作器</a></td></tr><tr><td>Handler：处理器抽象、选择器和插件化处理器集合。</td><td><a href="/framework/core/components/handler">处理器</a></td></tr><tr><td>Filter：围绕上下文执行前后处理的过滤器接口。</td><td><a href="/framework/core/components/filter">过滤器</a></td></tr><tr><td>Superviser：面向可监管对象的观察、失效和恢复模型。</td><td><a href="/framework/core/components/superviser">监视器</a></td></tr><tr><td>Weighter：平滑加权轮询选择器。</td><td><a href="/framework/core/components/weighter">均衡器</a></td></tr><tr><td>Discriminator：让容器按内容识别子集合或目标类型。</td><td><a href="/framework/core/components/discriminator">鉴别器</a></td></tr><tr><td>Attempter：失败尝试次数统计和锁定窗口控制。</td><td><a href="/framework/core/components/attempter">尝试器</a></td></tr><tr><td>Converters：组件层常用类型转换器。</td><td><a href="/framework/core/components/converters">转换器</a></td></tr><tr><td>状态机：状态图、状态上下文、状态处理器和状态流转。</td><td><a href="/framework/core/components/states">状态机</a></td></tr><tr><td>执行管线：把 Executor、上下文和 Feature 特性组合为可执行单元。</td><td><a href="/framework/core/components/executor">执行管线</a></td></tr></tbody></table>

## 相关资源

* [Components 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Components)


# 标识

从 Discussions 用户身份理解类型和值共同组成的标识。

Identifier 将标识类别与标识值放在一起，使通用安全组件不必依赖具体的论坛用户类。Discussions 的 UserIdentity 实现用户契约，将 UserId 转换为通用标识。

来源：[src/Security/UserIdentity.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Security/UserIdentity.cs#L102)（节选；上下文见源文件）。

{% code title="UserIdentity.cs" %}

```csharp
Identifier IIdentifiable.Identifier
{
	get => new(typeof(IUser), this.UserId);
	set => this.UserId = value.Validate<uint>(out var id) ? id : this.UserId;
}
#endregion
```

{% endcode %}

这里的类型是 IUser，而不是 UserIdentity。设置标识时先验证值能否作为 uint，再更新 UserId；不符合要求时保持原编号。标识用于回答对象是谁，不会自动加载用户资料或授予权限。

## 相关契约

| 类型             | 责任             |
| -------------- | -------------- |
| IIdentifiable  | 提供统一标识属性       |
| Identifier     | 保存类型、值及可选标签、描述 |
| Identifier\<T> | 值类型已确定的标识      |

UserIdentity 还包含 SiteId。用户编号和站点范围承担不同责任，跨模块传递 Identifier 后，业务操作仍需恢复或验证当前站点和资源权限。参见[认证与授权](/framework/security/authentication)。

框架定义：[Identifier](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/Identifier.cs)、[IIdentifiable](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IIdentifiable.cs)。


# 别名

Zongsoft.Components AliasAttribute 别名声明和读取。

`AliasAttribute` 用于为程序集、模块、类型、成员或参数声明一个或多个别名。它常用于把外部输入、配置名称、命令名称或插件名称映射到内部成员。

别名的设计重点是“稳定匹配”，不是展示文本。一个组件可以有多个历史名称、缩写或外部配置名，但运行时仍通过同一个类型或成员处理。

## 关键能力

| 能力     | 说明                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 多重声明   | `AllowMultiple = true`，同一个目标可以拥有多个别名。                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| 继承读取   | 默认支持继承读取。                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| 静态读取方法 | `GetAliases(...)` 可从 [`MemberInfo`](https://learn.microsoft.com/zh-cn/dotnet/api/system.reflection.memberinfo) [*源码*](https://source.dot.net/#System.Private.CoreLib/MemberInfo.cs)、[`Assembly`](https://learn.microsoft.com/zh-cn/dotnet/api/system.reflection.assembly) [*源码*](https://source.dot.net/#System.Private.CoreLib/Assembly.cs)、[`Module`](https://learn.microsoft.com/zh-cn/dotnet/api/system.reflection.module) [*源码*](https://source.dot.net/#System.Private.CoreLib/Module.cs)、[`ParameterInfo`](https://learn.microsoft.com/zh-cn/dotnet/api/system.reflection.parameterinfo) [*源码*](https://source.dot.net/#System.Private.CoreLib/ParameterInfo.cs) 或对象读取别名。 |

来源：[framework/Zongsoft.Core/test/Models.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Models.cs#L247)（节选；上下文见源文件）。

{% code title="Models.cs" %}

```csharp
[DefaultValue(Female)]
public enum Gender : byte
{
	[Zongsoft.Components.Alias("F")]
	Female,

	[Zongsoft.Components.Alias("M")]
	[Description("Gender.Male")]
	Male,
}
```

{% endcode %}

Discussions 没有直接声明 AliasAttribute；这里引用 [核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core) 测试模型中的 Gender 枚举。F、M 是枚举成员别名，读取时应对相应成员调用 GetAliases，而不是读取枚举类型就假定能获得所有成员的别名。该测试枚举与 Discussions.Models.Gender 是不同类型。

## 使用建议

* 别名适合兼容旧名称、缩写名称或外部配置名称。
* 不要把别名当作本地化标题；它应当是稳定、可匹配的标识。
* 当一个组件需要被命令、配置、插件和 UI 同时引用时，别名可以减少硬编码名称差异。
* 如果别名会参与用户输入解析，建议统一大小写和分隔符约定，避免同一个概念出现多套写法。

{% hint style="info" %}
`AliasAttribute.GetAliases(object)` 会根据目标对象类型分派到程序集、模块、成员、参数或目标类型读取；传入普通对象时读取的是其运行时类型上的别名。
{% endhint %}

## 参考实现

* [AliasAttribute.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/AliasAttribute.cs)


# 命令

以 Discussions 的 MessageSendCommand 理解参数、选项、服务依赖和执行。

命令把文本入口、参数解析与业务调用组织起来。Discussions 已有 MessageSendCommand，用于构造站内消息并调用 MessageService；它是命令类的真实用例，但当前插件清单尚未将它挂载到终端命令树。

## 选项与服务依赖

来源：[src/Services/Commands/MessageSendCommand.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/Commands/MessageSendCommand.cs#L37)（节选；上下文见源文件）。

{% code title="MessageSendCommand.cs" %}

```csharp
[CommandOption(SUBJECT_OPTION, typeof(string), Required = true)]
[CommandOption(CONTENT_OPTION, typeof(string), Required = true)]
[CommandOption(CONTENTTYPE_OPTION, typeof(string))]
[CommandOption(MESSAGETYPE_OPTION, typeof(string))]
[CommandOption(SOURCE_OPTION, typeof(string))]
public class MessageSendCommand : CommandBase<CommandContext>
```

{% endcode %}

来源：[src/Services/Commands/MessageSendCommand.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/Commands/MessageSendCommand.cs#L58)（节选；上下文见源文件）。

{% code title="MessageSendCommand.cs" %}

```csharp
[ServiceDependency(Provider = Module.NAME)]
public MessageService Service { get; set; }
```

{% endcode %}

主题和正文是必需选项，内容类型、消息类型和来源为可选。服务依赖来自 Discussions 模块，不应在命令里自行创建另一个 MessageService 和访问器。

## 执行时构造业务模型

来源：[src/Services/Commands/MessageSendCommand.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/Commands/MessageSendCommand.cs#L63)（节选；上下文见源文件）。

{% code title="MessageSendCommand.cs" %}

```csharp
protected override ValueTask<object> OnExecuteAsync(CommandContext context, CancellationToken cancellation)
{
	if(context.Arguments == null || context.Arguments.IsEmpty)
		throw new CommandException("Missing arguments of the command.");

	var content = context.Options.GetValue<string>(CONTENT_OPTION);
	var contentType = context.Options.GetValue<string>(CONTENTTYPE_OPTION);

	//根据内容类型解析得到真实内容
	content = GetContent(content, ref contentType);

	var message = Zongsoft.Data.Model.Build<Models.Message>(entity =>
	{
		entity.Content = content;
		entity.ContentType = contentType;
		entity.Referer = context.Options.GetValue<string>(SOURCE_OPTION);
		entity.Subject = context.Options.GetValue<string>(SUBJECT_OPTION);
		entity.MessageType = context.Options.GetValue<string>(MESSAGETYPE_OPTION);
	});

	if(this.Service.Send(message, GetUsers(context.Arguments)) > 0)
		return ValueTask.FromResult<object>(message);

	return ValueTask.FromResult<object>(null);
}
```

{% endcode %}

位置参数解析为收件人编号，选项形成 Message 模型。方法调用的是站内消息服务，不是消息队列发布器。返回模型供上层处理；命令本身不保证终端如何格式化输出。

## 文件正文参数

来源：[src/Services/Commands/MessageSendCommand.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/Commands/MessageSendCommand.cs#L91)（节选；上下文见源文件）。

{% code title="MessageSendCommand.cs" %}

```csharp
private static string GetContent(string content, ref string contentType)
{
	if(string.IsNullOrWhiteSpace(content) || string.IsNullOrWhiteSpace(contentType))
		return content;

	if(contentType.Length > 5 && contentType.EndsWith("+file", StringComparison.OrdinalIgnoreCase))
	{
		contentType = contentType.Substring(0, contentType.Length - 5);

		if(Zongsoft.IO.FileSystem.File.Exists(content))
		{
			using(var stream = Zongsoft.IO.FileSystem.File.Open(content, System.IO.FileMode.Open))
			{
				using(var reader = new System.IO.StreamReader(stream))
				{
					content = reader.ReadToEnd();
				}
			}
		}
	}

	return content;
}
```

{% endcode %}

带 +file 后缀的内容类型表示从给定路径读取正文，并移除这个输入标记。它与持久化内容使用的 +embedded 约定不同，不能互相替代。当前逻辑在文件不存在时保留原值，调用方需要核对输入，不能假定总会读到文件内容。

## 从类到可调用命令

要让命令进入执行器，宿主还需要命令树挂载和相应权限上下文。文档不提供一个仓库中尚不存在的命令名称。现有终端命令的完整装配可参考[终端](/framework/core/terminals/commands)和框架 Main.plugin。


# 事件

从 Discussions 的事件注册表入口理解模块事件的装配边界。

模块事件为业务组件提供显式的事件入口。Discussions 的 Module 继承带事件注册表类型参数的 [ApplicationModule](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/ApplicationModule.cs)，并在插件树中暴露 Events。

来源：[src/Module.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Module.cs#L56)（节选；上下文见源文件）。

{% code title="Module.cs" %}

```csharp
public sealed class EventRegistry : EventRegistryBase
{
	#region 构造函数
	public EventRegistry() : base(NAME)
	{
	}
	#endregion
```

{% endcode %}

当前 EventRegistry 没有声明论坛业务事件，因此不能把发帖、审核或消息发送描述为已经发布了事件。它只提供扩展位置。

## 插件暴露事件入口

来源：[src/Zongsoft.Discussions.plugin](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.plugin#L26)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.plugin" %}

```xml
<expose name="Events" value="{path:../@Events}" />
<expose name="Properties" value="{path:../@Properties}" />
```

{% endcode %}

暴露注册表让插件体系能发现模块事件。添加事件仍需定义事件名称、载荷、触发时机、处理方式及失败策略。尤其要区分事务提交前和提交后，避免把未提交的数据发送给外部消费者。

## 框架中的完整参考

Discussions 尚无完整业务事件流程，可继续核对框架[组件事件实现](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Components)与[安全模块](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Security/src)的实际事件定义。事件与消息队列之间需要明确桥接，不能因为框架支持事件就推断论坛已经使用 Broker。

相关概念：[处理器](/framework/core/components/handler)、[消息队列](/framework/messaging)、[事务](/framework/data/transactions)。


# 工作器

Zongsoft.Components Worker 工作器模型与工作器命令。

`Worker` 是 Zongsoft 中可被启动、停止、暂停和恢复的运行时组件模型。它适合表达后台服务、事件交换器、调度服务器、消息监听器、设备采集器等具有生命周期的对象。

[工作器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)把生命周期命令和运行状态统一起来。宿主程序、终端命令或插件启动流程可以用同一组方法控制不同运行时组件。

## 关键类型

| 类型                                                                                                                                                                                                                                                                        | 说明                                                |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| [`IWorker`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)                                                                                                                                                                      | 工作者接口，定义 `Start`、`Stop`、`Pause`、`Resume` 及对应异步方法。 |
| [`WorkerBase`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/WorkerBase.cs)                                                                                                                                                                | 工作者基类，负责状态切换、重入控制、启用判断和状态变化事件。                    |
| [`WorkerState`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/WorkerState.cs)                                                                                                                                                              | 工作者状态枚举，包括停止、运行、暂停等状态。                            |
| [`WorkerCommandBase`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/Commands/WorkerCommandBase.cs)                                                                                                                                         | 工作者命令基类，用于把工作者生命周期操作暴露成命令。                        |
| [`WorkerStartCommand`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/Commands/WorkerStartCommand.cs)、[`WorkerStopCommand`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/Commands/WorkerStopCommand.cs)     | 启动和停止工作者。                                         |
| [`WorkerPauseCommand`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/Commands/WorkerPauseCommand.cs)、[`WorkerResumeCommand`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/Commands/WorkerResumeCommand.cs) | 暂停和恢复工作者。                                         |
| [`WorkerInfoCommand`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/Commands/WorkerInfoCommand.cs)                                                                                                                                         | 查看工作者状态和基本信息。                                     |

## 生命周期

{% stepper %}
{% step %}

## Start

[`WorkerBase.StartAsync(...)`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/WorkerBase.cs) 检查启用状态和当前状态，进入 `Starting`，然后调用派生类的启动逻辑，成功后进入 `Running`。
{% endstep %}

{% step %}

## Pause / Resume

如果工作者支持暂停和继续，调用暂停或恢复命令会切换状态，并触发对应的模板方法。
{% endstep %}

{% step %}

## Stop

停止过程进入 `Stopping`，释放运行中的资源，例如关闭事件通道、停止调度服务器、注销订阅或释放外部连接，最终回到 `Stopped`。
{% endstep %}
{% endstepper %}

## Hangfire 示例

`Zongsoft.Externals.Hangfire` 中的 `Server` 继承自 [`WorkerBase`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/WorkerBase.cs)，启动时创建 `BackgroundJobServer`，停止时释放服务器实例。它还暴露 `Handlers` 集合，让插件把调度处理器挂载到 `/Workbench/Scheduler/Handlers`。

来源：[framework/externals/hangfire/src/Zongsoft.Externals.Hangfire-daemon.plugin](https://github.com/Zongsoft/framework/blob/main/externals/hangfire/src/Zongsoft.Externals.Hangfire-daemon.plugin#L19)（节选；上下文见源文件）。

{% code title="Zongsoft.Externals.Hangfire-daemon.plugin" %}

```xml
	<extension path="/Workspace/Externals/Hangfire">
		<object name="Server" type="Zongsoft.Externals.Hangfire.Server, Zongsoft.Externals.Hangfire">
			<expose name="Handlers" value="{path:../@Handlers}" />
		</object>
	</extension>

	<extension path="/Workbench/Scheduler">
		<object name="Handlers" value="{path:/Workspace/Externals/Hangfire/Server/Handlers}" />
	</extension>

	<extension path="/Workbench/Startup">
		<object name="Hangfire" value="{path:/Workspace/Externals/Hangfire/Server}" />
	</extension>
```

{% endcode %}

这种设计让服务器自身只关心生命周期，而处理器和启动方式由插件文件组合。命令系统还能把 `start`、`stop`、`pause`、`resume` 等操作暴露给终端或管理程序。

[工作器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)适合生命周期明确、可能由宿主统一启动和停止的组件。一次性任务、普通业务服务或没有持续状态的对象，通常不需要实现 `IWorker`。暂停和恢复能力由 `CanPauseAndContinue` 约束；不支持暂停的工作器不应强行暴露暂停语义。

## 参考实现

* [WorkerBase.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/WorkerBase.cs)
* [Components/Commands 工作者命令](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Components/Commands)
* [Hangfire Server.cs](https://github.com/Zongsoft/framework/blob/main/externals/hangfire/src/Server.cs)


# 处理器

Zongsoft.Components Handler 处理器抽象、定位和选择。

`Handler` 用于表达“能够处理某类上下文或请求的对象”。它比具体服务接口更松散，适合在插件树、消息系统、事件通道和调度器中挂载可扩展处理器集合。

处理器模型关注“谁能处理这件事”。调用方可以拿到一个处理器集合，再由定位器、选择器或宿主流程挑选合适处理器执行。

## 关键类型

| 类型                                                       | 说明                  |
| -------------------------------------------------------- | ------------------- |
| `IHandler`、`IHandler<TContext>`                          | 处理器接口。              |
| `IHandler<TArgument, TResult>`                           | 带返回值的处理器接口。         |
| `IHandleable`、`IHandleable<TContext>`                    | 可处理对象接口，用于暴露处理能力。   |
| `IHandlerLocator`                                        | 处理器定位器。             |
| `HandlerBase<TContext>`、`HandlerBase<TContext, TResult>` | 处理器基类。              |
| `HandlerSelector`                                        | 根据 URL 或上下文选择处理器。   |
| `HandlerAttribute`                                       | 为处理器声明 URL 模板和顺序。   |
| `HandlerUtility`                                         | 处理器 URL、名称和元数据辅助方法。 |
| `Handler`                                                | 静态工厂，可把委托包装成处理器代理。  |

来源：[framework/Zongsoft.Diagnostics/protocols/server/samples/MetricHandler.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Diagnostics/protocols/server/samples/MetricHandler.cs#L12)（节选；上下文见源文件）。

{% code title="MetricHandler.cs" %}

```csharp
public class MetricHandler : HandlerBase<IEnumerable<Zongsoft.Diagnostics.Telemetry.Metrics.Meter>>
{
	protected override ValueTask OnHandleAsync(IEnumerable<Telemetry.Metrics.Meter> meters, Parameters parameters, CancellationToken cancellation)
	{
		foreach(var meter in meters)
			Terminal.WriteLine(CommandOutletDumper.Dump(meter));

		return ValueTask.CompletedTask;
	}
}
```

{% endcode %}

这个处理器来自诊断协议服务端样例：输入是一组已经转换为框架模型的 Meter，处理方法逐项输出到终端。它没有声明 HandlerAttribute；监听器通过插件集合取得它。HandlerUtility 的 URL 模板能力属于另一种按地址选择处理器的机制，不能据此推断所有处理器都需要 URL。

## 插件化处理器集合

诊断样例把 MetricHandler 挂到 Metrics 监听器暴露的集合中。这里的路径、对象名和类型都来自同一个样例插件；启动顺序与监听地址见 [OTLP 接收](/framework/diagnostics/otlp)。

来源：[framework/Zongsoft.Diagnostics/protocols/server/samples/Zongsoft.Diagnostics.Protocols.Server.Samples.plugin](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Diagnostics/protocols/server/samples/Zongsoft.Diagnostics.Protocols.Server.Samples.plugin#L19)（节选；上下文见源文件）。

{% code title="Zongsoft.Diagnostics.Protocols.Server.Samples.plugin" %}

```xml
<extension path="/Workbench/Diagnostics/Telemetry/Listener/Metrics">
	<object name="MetricHandler" type="Zongsoft.Diagnostics.Protocols.Server.Samples.MetricHandler, Zongsoft.Diagnostics.Protocols.Server.Samples" />
</extension>
```

{% endcode %}

这种模式的重点不是“某个接口有多少方法”，而是“某个扩展点可以接收哪些处理器”。业务模块只要实现处理器并挂载到集合，就能参与消息处理流程。

## 与命令和事件的区别

| 模型  | 关注点                           |
| --- | ----------------------------- |
| 命令  | 调用方提交一段表达式，由命令树解析和执行。         |
| 事件  | 发布方声明“发生了什么”，处理器通过事件节点解耦扩展。   |
| 处理器 | 扩展点维护一个处理器集合，由定位器或选择器挑选合适处理器。 |

处理器适合插件化扩展点、消息响应、事件投递和调度任务。如果调用方明确依赖某个领域服务，并且需要强类型业务契约，直接定义服务接口通常更清楚。`HandlerSelector` 是基于处理器 URL 模板的本地选择器，复杂路由、权限判断或负载均衡通常应交给更专门的机制。

## 参考实现

* [Handler 相关源码](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Components)
* [Zongsoft.Messaging.ZeroMQ.plugin](https://github.com/Zongsoft/framework/blob/main/messaging/zero/src/Zongsoft.Messaging.ZeroMQ.plugin)


# 过滤器

从 Discussions 查询后正文处理理解过滤器时机、生命周期与权限。

过滤器在既有操作前后加入横切逻辑。Discussions 的 PostFilter 属于数据访问过滤器，使用查询上下文处理正文；它不同于通用执行器过滤接口，但展示了同样重要的时机与生命周期约束。

## 查询前与查询后

来源：[src/Data/PostFilter.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Data/PostFilter.cs#L43)（节选；上下文见源文件）。

{% code title="PostFilter.cs" %}

```csharp
public void OnFiltering(DataSelectContextBase context) { }
public void OnFiltered(DataSelectContextBase context)
{
	if(context.Result == null)
		return;

	var identity = context.Principal?.Identity;
	context.Result = FilteredResult.Create(context, item => Filter(item, identity));
}
#endregion
```

{% endcode %}

查询前不包装结果，因为底层操作尚未产生最终集合。查询后以 [FilteredResult](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Data/FilteredResult.cs) 包装，处理仍在枚举阶段发生。这个薄适配层同时满足查询上下文的非泛型集合要求和调用方的泛型异步枚举要求，并转发分页通知；实际枚举、过滤、取消与释放交给 [核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core) 的 [集合扩展](/framework/core/collections/extensions) 和 [Pageable](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Data/Pageable.cs)。

{% hint style="warning" %}
🚨 Discussions 的这一实现要求 [核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core) 7.59.0 中的分页过滤修复。该版本发布前需按[准备环境](/get-started/prerequisites)使用本地 framework 引用；核心类库 7.58.0 的分页过滤会传入错误的当前元素，不能仅凭编译通过判断兼容。
{% endhint %}

## 处理正文而不改变记录数量

来源：[src/Data/PostFilter.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Data/PostFilter.cs#L55)（节选；上下文见源文件）。

{% code title="PostFilter.cs" %}

```csharp
private static bool Filter(object item, System.Security.Principal.IIdentity identity)
{
	var dictionary = DataDictionary.GetDictionary<Models.Post>(item);
	if(!dictionary.TryGetValue(p => p.Content, out var content))
		return true;

	if(!(dictionary.TryGetValue(p => p.Approved, out var approved) && approved) &&
	   !(identity?.IsAuthenticated == true && dictionary.TryGetValue(p => p.CreatorId, out var creatorId) && identity.GetIdentifier<uint>() == creatorId))
	{
		dictionary.TrySetValue(p => p.Content, string.Empty);
		if(dictionary.TryGetValue(p => p.ContentType, out var hiddenType))
			dictionary.TrySetValue(p => p.ContentType, Utility.GetContentType(hiddenType, true));
	}
	else if(dictionary.TryGetValue(p => p.ContentType, out var contentType) && !Utility.IsContentEmbedded(contentType))
	{
		dictionary.SetValue(p => p.Content, string.IsNullOrEmpty(content) ? string.Empty : Utility.ReadTextFile(content));
		dictionary.SetValue(p => p.ContentType, Utility.GetContentType(contentType, true));
	}

	return true;
}
```

{% endcode %}

未审核且非作者的正文被置空，记录仍保留。缺少判定字段时也不能当作已审核。外置文件读出后同步调整内容类型，避免后续服务把正文再次当成文件路径读取。

主题详情还有一个读取顺序问题：ThreadFilter 先脱敏，ThreadService 随后才检查当前用户是否为版主。现在过滤器把原正文与当前模型实例关联保存在内部弱引用表中；同步和异步详情读取只有在版主授权成功后才恢复正文。它不会修改 Approved，也不会让普通列表查询绕过脱敏。外置正文同样在授权完成后读取，相关实现见 [ThreadFilter](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Data/ThreadFilter.cs) 与 [ThreadService](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/ThreadService.cs)。

## 不把过滤器当成全部授权

它处理返回内容，不自动授权编辑、审核、删除或跨站点访问。SiteId 由验证器约束，动作权限由服务与安全机制控制；这些规则需要一起核对。

通用过滤契约和执行器实现参考[组件源码](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Components)，数据访问流程见[数据服务](/framework/data/services)。


# 监视器

采用框架 SuperviserTest 的真实对象与并发测试说明监测生命周期。

Superviser 管理被监测对象，按键查找和取消监测，并通过事件通知状态变化。Discussions 当前没有监测器业务流程，因此本页使用框架 SuperviserTest 的现有用例。

## 注册与键的含义

来源：[framework/Zongsoft.Core/test/Components/SuperviserTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Components/SuperviserTest.cs#L17)（节选；上下文见源文件）。

{% code title="SuperviserTest.cs" %}

```csharp
private void Initialize(int count = 2)
{
	for(int i = 0; i < count; i++)
	{
		var name = $"S{i + 1}";
		_superviser.Supervise(name, new MySupervisable(name));
		_superviser.Supervise(name, new MySupervisable(name));
	}
}
```

{% endcode %}

测试类持有 Superviser\<string>，MySupervisable 是同一测试文件内的被监测对象。这里对同一个键重复注册，用于验证监测器不会按调用次数重复计数。测试对象不是设备协议客户端，不能据此推断网络重连行为。

## 取消监测与对象通知

来源：[framework/Zongsoft.Core/test/Components/SuperviserTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Components/SuperviserTest.cs#L43)（节选；上下文见源文件）。

{% code title="SuperviserTest.cs" %}

```csharp
public void TestUnsupervise()
{
	this.Initialize(2);

	Assert.True(_superviser.Unsupervise("S1", out var observable));
	Assert.NotNull(observable);
	Assert.IsType<MySupervisable>(observable);
	Assert.Equal("S1", ((MySupervisable)observable).Name);
	Assert.True(((MySupervisable)observable).IsUnsupervised(TimeSpan.FromSeconds(10)));
	Assert.False(_superviser.Contains("S1"));

	Assert.True(_superviser.Unsupervise("S2", out observable));
	Assert.NotNull(observable);
	Assert.IsType<MySupervisable>(observable);
	Assert.Equal("S2", ((MySupervisable)observable).Name);
	Assert.True(((MySupervisable)observable).IsUnsupervised(TimeSpan.FromSeconds(10)));
	Assert.False(_superviser.Contains("S2"));

	Assert.Equal(0, _superviser.Count);
}
```

{% endcode %}

取消操作返回关联对象，之后键不再存在。测试等待对象收到取消监测回调，说明回调与调用返回的时间需要分别考虑。

## 并发行为

框架同一测试文件还验证并发注册同一键、并发取消和事件计数。实际业务应特别检查重复注册、取消与重新注册交错时的所有权，不能只在单线程环境验证 Contains。

## 生命周期与适用边界

监测生命周期和失败阈值描述的是对象监测策略，不自动等于网络连接存活、进程健康或业务请求成功。实现被监测对象时，应明确何时报告数据、错误与完成，以及取消监测后如何释放资源。

监测器自身应由明确的服务生命周期管理；不要为每次查询创建共享监测器，也不要在取消后继续把旧对象当作当前连接。参见[服务所有权](/framework/core/services/locating)和[通知](/framework/core/common/notification)。


# 均衡器

Zongsoft.Components Weighter 平滑加权轮询选择器。

`Weighter<T>` 是一个进程内的平滑加权轮询选择器。它适合在多个等价候选项之间按权重分配请求，例如多个数据源、供应商、网关、连接或处理器实例；权重越高，被选中的次数越多，但选择结果会尽量分散到整个调度序列中。

和普通加权轮询相比，`Weighter<T>` 的重点是“平滑”。如果权重为 `4:2:1`，普通轮询很容易得到 `A,A,A,A,B,B,C` 这类集中序列，而平滑加权轮询会更接近 `A,B,A,C,A,B,A`，从而避免高权重节点在短时间内被连续压上太多请求。

{% hint style="info" %}
关于算法背景、随机算法、一致哈希、Nginx 与 LVS 两类加权轮询算法的对比，可阅读《[平滑的加权轮询均衡算法](https://blog.zongsoft.com/ping-hua-de-jia-quan-lun-xun-jun-heng-suan-fa)》。
{% endhint %}

## 设计意图

`Weighter<T>` 解决的是本地选择问题：在一组候选项都可用、都能处理同类请求，但容量、成本或优先级不同的情况下，调用方希望按比例分配流量，同时尽量减少短时间内的请求突刺。

它不保存外部分布式状态，也不负责健康检查、失败熔断或全局配额。多个进程各自持有 `Weighter<T>` 时，只能保证各自进程内的选择序列平滑；如果需要跨进程一致性，通常应在更上层的负载均衡器、调度器或服务治理机制中处理。

## 算法过程

平滑加权轮询会为每个候选项维护当前权重。每次选择时，算法先给所有候选项累加其静态权重，再选择当前权重最高的候选项，最后把该候选项的当前权重减去总权重。经过多个周期后，高权重候选项被选中的次数更多，但结果会尽量均匀地分布在序列中。

设 `A`、`B`、`C` 的权重分别为 `4`、`2`、`1`，总权重为 `7`，一个完整周期的演算如下：

| 轮次 | 累加后当前权重        | 选中项 | 选中后当前权重         |
| -- | -------------- | --- | --------------- |
| 1  | `{ 4, 2, 1 }`  | `A` | `{ -3, 2, 1 }`  |
| 2  | `{ 1, 4, 2 }`  | `B` | `{ 1, -3, 2 }`  |
| 3  | `{ 5, -1, 3 }` | `A` | `{ -2, -1, 3 }` |
| 4  | `{ 2, 1, 4 }`  | `C` | `{ 2, 1, -3 }`  |
| 5  | `{ 6, 3, -2 }` | `A` | `{ -1, 3, -2 }` |
| 6  | `{ 3, 5, -1 }` | `B` | `{ 3, -2, -1 }` |
| 7  | `{ 7, 0, 0 }`  | `A` | `{ 0, 0, 0 }`   |

因此，一个周期内的命中次数符合 `4:2:1`，序列为 `A,B,A,C,A,B,A`，并在周期结束后回到初始状态。

## 常用成员

| 成员                               | 说明                      |
| -------------------------------- | ----------------------- |
| `Weighter(entries, weightThunk)` | 使用候选集合和可选的权重解析函数创建选择器。  |
| `Add(value, weight)`             | 添加一个候选项及其固定权重。          |
| `Add(value, weighter)`           | 添加一个候选项，并通过函数计算该候选项的权重。 |
| `Remove(value)`                  | 移除候选项。                  |
| `Get()`                          | 选择下一个候选项。               |
| `Clear()`                        | 清空全部候选项。                |
| `Count`                          | 获取当前候选项数量。              |

构造函数会忽略初始集合中的 `null` 候选项。未提供权重解析函数时，默认权重为 `100`；最终有效权重不会小于 `1`。如果通过 `Add(value, weight)` 添加候选项并传入非正数权重，则会回退到默认权重。

## 适用场景

* 数据访问框架按读写模式和权重选择数据源。
* 多个短信供应商之间按容量或成本分摊发送量。
* 多个网关、连接、队列消费者之间做平滑选择。
* 同一个功能有多个处理器时，按优先级或权重选择执行者。
* 在本地进程内做轻量负载均衡，不需要引入外部负载均衡器。

来源：[framework/Zongsoft.Core/test/Components/WeighterTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Components/WeighterTest.cs#L13)（节选；上下文见源文件）。

{% code title="WeighterTest.cs" %}

```csharp
var servers = new []
{
	new Server("A", 4),
	new Server("B", 2),
	new Server("C", 1),
};

var weighter = new Weighter<Server>(servers, server => server.Weight);

//Round 1
var server = weighter.Get();
Assert.NotNull(server);
Assert.Equal("A", server.Name);
Assert.Equal(4, server.Weight);
```

{% endcode %}

上面来自 WeighterTest.TestGet1，Server 是同一文件中的测试模型，完整测试依次验证 A、B、A、C、A、B、A。Discussions 没有配置多个等价数据源；上述权重测试用于理解框架算法。

## 使用注意

`Get()` 每调用一次都会推进内部当前权重，所以它不是纯查询方法。调用方应把 `Weighter<T>` 视为有状态选择器，而不是可重复读取同一结果的缓存。

候选项为空时，`Get()` 返回目标类型默认值。对引用类型而言通常是 `null`，调用方应在发送请求、建立连接或执行处理器前处理空集合情况。

候选项可通过 `Add(...)`、`Remove(...)` 和 `Clear()` 动态调整。对于临时不可用的节点，建议先从选择器中移除，或在创建选择器前完成可用性过滤；`Weighter<T>` 本身不会探测节点健康状态，也不会根据调用失败自动降低权重。

## 相关实现

[`Zongsoft.Data`](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Data) 的数据源选择器会分别为可读、可写数据源维护 `Weighter<T>`，并根据数据访问方法选择读库或写库。这个场景体现了 `Weighter<T>` 的典型边界：它只负责在已筛选出的候选集合中按权重选择，下游的数据访问上下文、命令可变性和数据源模式仍由数据框架判断。

## 参考

* [Weighter.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/Weighter.cs)
* [DataSourceSelector.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Data/src/Common/DataSourceSelector.cs)
* [平滑的加权轮询均衡算法](https://blog.zongsoft.com/ping-hua-de-jia-quan-lun-xun-jun-heng-suan-fa)


# 鉴别器

Zongsoft.Components Discriminator 识别器和插件构件装配。

`IDiscriminator` 是一个很小但很有用的接口：它让一个容器对象根据输入参数识别目标类型或目标子集合。插件构件装配时，如果父对象实现了识别器，就可以把子构件追加到正确的位置，而不是只能依赖固定的属性名或集合类型。

它适合解决“同一个节点下允许多种子对象”的装配问题。识别器把类型判断留给领域容器本身，而不是让插件构建器猜测每一种集合属性。

## 关键类型

| 类型               | 说明                                             |
| ---------------- | ---------------------------------------------- |
| `IDiscriminator` | 定义 `Discriminate(object argument)`，根据参数返回识别结果。 |

## 权限分类示例

`PrivilegeCategory` 同时包含子分类集合和权限集合。它实现 `IDiscriminator` 后，可以根据输入内容返回应该追加到哪个集合。

来源：[framework/Zongsoft.Core/src/Security/Privileges/PrivilegeCategory.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Privileges/PrivilegeCategory.cs#L128)（节选；上下文见源文件）。

{% code title="PrivilegeCategory.cs" %}

```csharp
object IDiscriminator.Discriminate(object argument)
{
	switch(argument)
	{
		case string type:
			if(string.IsNullOrEmpty(type) || string.Equals(type, nameof(Category), StringComparison.OrdinalIgnoreCase))
				return this.Categories;

			if(string.Equals(type, nameof(Privilege), StringComparison.OrdinalIgnoreCase))
				return this.Privileges;

			break;
		case Privilege:
			return this.Privileges;
		case PrivilegeCategory:
			return this.Categories;
	}

	return null;
}
```

{% endcode %}

这是核心权限分类的实际实现。空类型名与 Category 指向子分类集合，Privilege 指向权限集合，字符串比较忽略大小写；无法识别时返回空。Discussions 的插件挂载仍遵循同一构件装配规则，但未定义自己的 IDiscriminator。

## 插件装配中的用途

[`Zongsoft.Plugins`](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Plugins) 中的 `BuiltinType` 会先询问拥有者或默认成员是否实现 `IDiscriminator`，如果返回 [`Type`](https://learn.microsoft.com/zh-cn/dotnet/api/system.type) [*源码*](https://source.dot.net/#System.Private.CoreLib/Type.cs) 或集合，就用它判断构件类型。`ObjectBuilder` 在追加子对象时也会先调用容器的识别器，把子对象交给正确的集合。

{% stepper %}
{% step %}

## 读取插件节点

插件系统读取一个内建构件节点，准备构造或追加对象。
{% endstep %}

{% step %}

## 询问识别器

如果父对象实现 `IDiscriminator`，构建器把类型名或子对象传给 `Discriminate(...)`。
{% endstep %}

{% step %}

## 选择目标

识别器返回目标类型、目标集合或目标容器，构建器再完成类型解析或追加操作。
{% endstep %}
{% endstepper %}

这种设计特别适合“一个节点下可能包含多种子对象”的插件结构，例如权限分类下既能放权限项，也能放子分类。

{% hint style="info" %}
识别器返回值可以是目标类型、目标集合或目标容器，具体解释由调用方决定。插件装配中的行为以 [`Zongsoft.Plugins`](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Plugins) 的构件构建流程为准。
{% endhint %}

`IDiscriminator` 适合容器内部有清晰分类规则的场景。如果子对象只有一个固定集合属性，直接公开集合或属性通常更简单。识别规则也应保持可预测，尽量不要依赖外部状态或会频繁变化的运行时条件。

## 参考实现

* [IDiscriminator.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IDiscriminator.cs)
* [PrivilegeCategory.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Privileges/PrivilegeCategory.cs)
* [BuiltinType.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins/src/BuiltinType.cs)
* [ObjectBuilder.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins/src/Builders/ObjectBuilder.cs)


# 尝试器

Zongsoft.Components Attempter 失败尝试统计和锁定窗口。

`Attempter` 用于统计某个键的失败尝试次数，并在达到限制后进入锁定周期。它常用于登录、验证码、敏感操作二次确认等安全场景。

尝试器把“失败次数”和“锁定窗口”放到缓存中维护。调用方只需要在验证前检查、失败后登记、成功后清除即可。

## 关键类型

| 类型                  | 说明                                  |
| ------------------- | ----------------------------------- |
| `IAttempter`        | 尝试器接口，定义检查、成功完成和失败登记。               |
| `Attempter`         | 默认实现，基于分布式缓存和 `ISequence` 递增统计失败次数。 |
| `IAttempterOptions` | 尝试限制选项接口。                           |
| `AttempterOptions`  | 默认尝试限制选项。                           |

## 工作机制

{% stepper %}
{% step %}

## Check

调用 `CheckAsync(key)` 检查当前键是否仍允许尝试。
{% endstep %}

{% step %}

## Fail

失败时调用 `FailAsync(key)`。实现会通过缓存的 `ISequence` 能力递增失败次数。
{% endstep %}

{% step %}

## Window / Period

未达到限制时设置短窗口；第一次达到限制时设置锁定周期。
{% endstep %}

{% step %}

## Done

成功后调用 `DoneAsync(key)` 清除缓存键，避免历史失败次数继续影响用户。
{% endstep %}
{% endstepper %}

来源：[framework/Zongsoft.Core/src/Security/Privileges/Authenticators.Identity.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Privileges/Authenticators.Identity.cs#L86)（节选；上下文见源文件）。

{% code title="Authenticators.Identity.cs" %}

```csharp
public async ValueTask<Ticket> VerifyAsync(string key, Requirement requirement, string scenario, Parameters parameters, CancellationToken cancellation = default)
{
	if(string.IsNullOrWhiteSpace(requirement.Identity))
	{
		if(string.IsNullOrEmpty(key))
			throw new AuthenticationException(SecurityReasons.InvalidIdentity, "Missing identity.");

		requirement.Identity = key;
	}

	//获取验证失败的解决器
	var attempter = this.Attempter;
	var attempterKey = $"{this.GetType().Name}:{requirement.Identity}@{requirement.Namespace}";

	//确认验证失败是否超出限制数，如果超出则返回账号被禁用
	if(attempter != null && !await attempter.CheckAsync(attempterKey, cancellation))
		throw new AuthenticationException(SecurityReasons.AccountSuspended);

	//获取当前用户的密钥信息
	var cipher = await Authentication.Servicer.Users.Passworder.GetAsync(requirement.Identity, requirement.Namespace, cancellation);

	//如果帐户不存在则验证失败
	if(cipher == null)
		throw new AuthenticationException(SecurityReasons.InvalidIdentity);

	//执行密码验证，如果成功则返回验证成功的票证
	if(await Authentication.Servicer.Users.Passworder.VerifyAsync(requirement.Password, cipher, cancellation))
	{
		//通知验证尝试成功，即清空验证失败记录
		if(attempter != null)
			await attempter.DoneAsync(attempterKey, cancellation);

		//返回验证成功的票证
		return this.CreateTicket(cipher.Identifier, requirement);
	}

	//通知验证尝试失败
	if(attempter != null)
		await attempter.FailAsync(attempterKey, cancellation);

	//抛出验证失败异常
	throw new AuthenticationException(SecurityReasons.InvalidPassword);
}
```

{% endcode %}

Discussions 使用框架安全认证入口，模块质询器在其后补充站点身份。上面是核心密码认证器的真实检查、成功清理和失败登记流程，键由认证器类型、用户身份及命名空间构成。不存在的账号在读取密钥失败后直接抛出，不能把这里的 FailAsync 理解为覆盖所有失败类型。

`Attempter` 依赖的缓存需要支持 `ISequence` 递增操作，否则失败登记无法原子计数。安全模块会把认证尝试器作为插件构件暴露，方便通过配置调整限制策略。

{% hint style="warning" %}
尝试器的键会规范化为小写并加上内部前缀。调用方应使用稳定、非敏感的键，例如用户名、手机号或业务主体标识，不要直接把明文密码、验证码或令牌作为键。
{% endhint %}

`Limit` 小于 1 时表示不限制失败次数；`Window` 表示未达到阈值时失败次数保留多久，`Period` 表示达到阈值后的锁定时长。在分布式环境下，建议配置支持原子递增的分布式缓存，否则多个节点上的失败统计可能不可靠。

## 参考实现

* [Attempter.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/Attempter.cs)
* [Zongsoft.Security.plugin](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Security/src/Zongsoft.Security.plugin)


# 转换器

Zongsoft.Components.Converters 常用类型转换器。

`Zongsoft.Components.Converters` 提供组件层常用类型转换器，主要服务于配置绑定、插件构件属性解析、命令选项绑定和序列化协作。

这些转换器的目标不是替代业务解析逻辑，而是让外部文本配置更接近人类书写习惯，同时保持运行时对象仍是明确类型。

## 转换器列表

| 转换器                                      | 说明                                      |
| ---------------------------------------- | --------------------------------------- |
| `ArchitectureConverter`                  | 在文本和处理器架构枚举之间转换。                        |
| `BooleanConverter`                       | 布尔值转换，适合兼容多种文本表示。                       |
| `CollectionConverter`                    | 集合转换器，用于把文本或对象转换为目标集合。                  |
| `CollectionConverter<TElementConverter>` | 为集合元素指定转换器的集合转换器。                       |
| `EncodingConverter`                      | 字符编码转换器，例如从 `utf-8` 转为 `Encoding.UTF8`。 |
| `EndpointConverter`                      | 网络端点转换器。                                |
| `EnumConverter`                          | 枚举转换器。                                  |
| `GuidConverter`                          | GUID 转换器。                               |
| `TimeSpanConverter`                      | 时间间隔转换器，支持命令选项和配置中的时间表达。                |

## 使用位置

* 插件文件中的构件属性。
* 选项配置文件中的复杂值。
* 命令行选项绑定，例如 `--timeout:5s`。
* JSON、类型描述器或配置绑定需要文本与对象互转的地方。

来源：[framework/Zongsoft.Core/src/Terminals/Commands/ShellCommand.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Terminals/Commands/ShellCommand.cs#L42)（节选；上下文见源文件）。

{% code title="ShellCommand.cs" %}

```csharp
[CommandOption(TIMEOUT_OPTION, 't', typeof(TimeSpan), "1s")]
public class ShellCommand : CommandBase<CommandContext>
{
	#region 常量定义
	private const string TIMEOUT_OPTION = "timeout";
```

{% endcode %}

`BooleanConverter` 会兼容 `1`、`0`、`on`、`off`、`yes`、`no`、`enable`、`disable` 等常见写法；`TimeSpanConverter` 则委托通用时间间隔解析工具处理文本。具体格式应以对应转换器源码和调用方配置约定为准。

## 集合转换

`CollectionConverter` 适合把连接字符串、插件属性或命令选项中的单个文本值解析成数组或泛型集合。默认分隔符包括 `|`、`;`、`,` 和换行；解析时会去除空白项并裁剪每个元素两端的空白。写回字符串时使用第一个分隔符连接元素。

如果目标类型是数组，转换器会创建对应元素类型的数组；如果目标类型是抽象集合类型，则会创建 `System.Collections.Generic.List<T>`；如果目标类型是具体集合类型，则会创建该集合并逐项添加元素。

来源：[framework/Zongsoft.Core/test/Configuration/ConnectionSettingsTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Configuration/ConnectionSettingsTest.cs#L462)（节选；上下文见源文件）。

{% code title="ConnectionSettingsTest.cs" %}

```csharp
[TypeConverter(typeof(Components.Converters.CollectionConverter<MappingEntryConverter>))]
public MappingEntry[] Mapping
{
	get => this.GetValue<MappingEntry[]>();
	set => this.SetValue(value);
}
```

{% endcode %}

来源：[framework/Zongsoft.Core/test/Configuration/ConnectionSettingsTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Configuration/ConnectionSettingsTest.cs#L485)（节选；上下文见源文件）。

{% code title="ConnectionSettingsTest.cs" %}

```csharp
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
	if(value is string text)
	{
		if(string.IsNullOrEmpty(text))
			return default(MappingEntry);

		var index = text.IndexOfAny([':', '=']);

		return index < 0 ?
			new MappingEntry(text.Trim()) :
			new MappingEntry(text[..index].Trim(), text[(index + 1)..].Trim());
	}

	return base.ConvertFrom(context, culture, value);
}
```

{% endcode %}

来源：[framework/Zongsoft.Core/test/Configuration/ConnectionSettingsTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Configuration/ConnectionSettingsTest.cs#L130)（节选；上下文见源文件）。

{% code title="ConnectionSettingsTest.cs" %}

```csharp
Assert.NotNull(settings.Mapping);
Assert.NotEmpty(settings.Mapping);
Assert.Equal(3, settings.Mapping.Length);
Assert.Equal("s1", settings.Mapping[0].Source, true);
Assert.Equal("t1", settings.Mapping[0].Target, true);
Assert.Equal("s2", settings.Mapping[1].Source, true);
Assert.Equal("t2", settings.Mapping[1].Target, true);
Assert.Equal("same", settings.Mapping[2].Source, true);
Assert.Equal("same", settings.Mapping[2].Target, true);
```

{% endcode %}

这些片段来自 ConnectionSettingsTest：Mapping 属性属于 MyConnectionSettings，MappingEntry 和 MappingEntryConverter 同在该文件中，settings 来自 MyDriver 对测试连接字符串的解析。上面的声明允许把 `mapping=s1:t1,s2=t2,same` 解析成三个 `MappingEntry` 元素：`s1 -> t1`、`s2 -> t2` 和 `same -> same`。普通 `CollectionConverter` 会使用元素类型本身的转换规则；`CollectionConverter<TElementConverter>` 则显式创建 `TElementConverter` 来解析每个元素，适合元素类型本身没有注册全局转换器，或者某个属性需要专属解析格式的场景。

{% hint style="warning" %}
集合转换器按简单分隔符拆分文本，不处理引号转义或嵌套结构。元素值本身可能包含 `|`、`;`、`,` 或换行时，应改用更明确的配置结构，或为外层类型提供专用转换器。
{% endhint %}

转换器适合处理通用、可复用的文本到对象转换。涉及业务语义、权限、外部资源查找或复杂校验时，建议留在命令、处理器或配置加载流程中完成。给命令选项或插件属性增加新格式前，也可以先确认现有转换器是否已经覆盖，避免同一类型出现多套解析规则。

## 参考实现

* [Converters 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Components/Converters)
* [CollectionConverter.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/Converters/CollectionConverter.cs)


# 状态机

结合 Discussions 的状态动作与框架状态机实现理解迁移、处理器和完成阶段。

`Zongsoft.Components.States` 提供轻量状态机和状态图模型，用于表达状态、状态向量、状态上下文、状态处理器和状态流转。它关注的是“一个对象能否从当前状态迁移到目标状态，以及迁移过程中需要触发哪些处理”，适合把业务对象上的状态判断、副作用和持久化动作拆开管理。

如果你还不熟悉有限状态机（**FSM**）的设计动机，建议先读这两篇文章：

* [代码失控与状态机（上）](https://blog.zongsoft.com/dai-ma-shi-kong-yu-zhuang-tai-ji-shang)：解释状态、状态驱动、迁移判定和状态图为什么能降低复杂流程的认知负担。
* [代码失控与状态机（下）](https://blog.zongsoft.com/dai-ma-shi-kong-yu-zhuang-tai-ji-xia)：通过表达式解析器示例演示如何从语法规则推导状态图并落到代码实现。

## 设计意图

复杂业务流程容易失控，通常不是因为每个分支本身很难，而是因为状态、条件、副作用和持久化更新混在一起后，局部修改会牵动很多隐藏路径。状态机模型把问题拆成几个相对独立的部分：

* **状态** 表示对象当前处于哪个业务阶段，例如订单待审、已审核、已取消。
* **状态向量** 表示一次迁移方向，即从源状态到目标状态。
* **状态图** 描述允许哪些状态向量发生，并负责读取、写入对象当前状态。
* **状态上下文** 保存本次迁移的键、源状态、目标状态、描述和参数。
* **状态处理器** 承载迁移过程中的副作用，例如记录审计日志、发送通知、更新关联数据。
* **状态机** 负责创建迁移上下文、查找处理器并驱动状态图执行迁移。

这种分层让状态规则保持在状态图中，让副作用集中到处理器中，让业务入口只表达“我要把某个对象迁移到某个目标状态”。它不是为了替代所有流程代码，而是让状态迁移的“地图”先清晰起来。

## 类型关系

| 类型                                                               | 说明                                    |
| ---------------------------------------------------------------- | ------------------------------------- |
| `State<TKey, TValue>`                                            | 表示某个对象键对应的状态值，持有所属状态图、对象键和值。          |
| `StateVector<T>`                                                 | 表示从源状态到目标状态的迁移向量。                     |
| `IStateMachine` / `StateMachine`                                 | 状态机入口，负责运行状态迁移并调度处理器。                 |
| `IStateDiagram<TKey, TValue>` / `StateDiagramBase<TKey, TValue>` | 状态图，负责判断状态是否可迁移，并提供当前状态读取和状态写入能力。     |
| `IStateContext<TKey, TValue>` / `StateContext<TKey, TValue>`     | 状态迁移上下文，保存对象键、迁移向量、描述和参数。             |
| `IStateHandler<TKey, TValue>` / `StateHandlerBase<TKey, TValue>` | 状态迁移处理器，负责处理迁移副作用；默认完成阶段会调用上下文写入目标状态。 |
| `IStateHandlerProvider`                                          | 状态处理器提供器，通常从依赖注入容器中获取处理器集合。           |

## Discussions 目前怎样处理状态

Discussions 定义了 ThreadStatus，但主题审核、锁定、置顶等操作主要由独立布尔字段与服务方法表达。当前没有实现 StateDiagramBase 派生类，不能把论坛描述成已经接入这套状态机。

来源：[src/Services/ThreadService.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/ThreadService.cs#L103)（节选；上下文见源文件）。

{% code title="ThreadService.cs" %}

```csharp
public bool SetLocked(ulong threadId, bool value)
{
	return this.DataAccess.Update<Models.Thread>(new
	{
		IsLocked = value,
	}, Condition.Equal(nameof(Models.Thread.ThreadId), threadId) & GetIsModeratorCriteria()) > 0;
}
```

{% endcode %}

这个动作依赖主题编号和版主资格。对于这样的单字段操作，明确的服务方法很直观；当状态之间出现复杂迁移关系、多个协作处理器和统一完成阶段时，再评估状态图抽象。

## 框架实际怎样驱动迁移

下面来自框架状态机本身，是实现参考，并非另建的业务范例：

来源：[framework/Zongsoft.Core/src/Components/States/StateMachine.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/States/StateMachine.cs#L87)（节选；上下文见源文件）。

{% code title="StateMachine.cs" %}

```csharp
public void Run<TKey, TValue>(State<TKey, TValue> state, string description, IEnumerable<KeyValuePair<object, object>> parameters = null) where TKey : struct, IEquatable<TKey> where TValue : struct
{
	ArgumentNullException.ThrowIfNull(state);
	var context = this.GetContext(state, description);

	if(context == null)
		return;

	if(parameters != null)
	{
		foreach(var parameter in parameters)
			context.Parameters.TryAdd(parameter.Key, parameter.Value);
	}

	var count = 0;
	var handlers = this.GetHandlers<TKey, TValue>();

	foreach(var handler in handlers)
	{
		if(count++ == 0)
			_stack.Push(context);

		state.Diagram.Transfer(context, handler);
	}
}
```

{% endcode %}

Run 取得迁移上下文，把参数合入上下文，查找处理器，再调用状态图执行迁移。上下文只在找到首个处理器时入栈，因此没有处理器时不能假定仍会完整执行后续持久化。

## 完成阶段与事务

来源：[framework/Zongsoft.Core/src/Components/States/StateMachine.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/States/StateMachine.cs#L115)（节选；上下文见源文件）。

{% code title="StateMachine.cs" %}

```csharp
private void Stop()
{
	if(_stack.Count == 0)
		return;

	var frames = _stack.Reverse().ToArray();

	using(var transaction = new Data.Transaction())
	{
		for(int i = 0; i < frames.Length; i++)
		{
			this.OnStop(frames[i]);
		}

		//提交事务
		transaction.Commit();
	}
}
```

{% endcode %}

完成阶段遍历已保存的上下文，在环境事务中调用处理器完成逻辑并提交。状态图负责表达允许的迁移和状态读写，处理器负责迁移中的业务动作。调用者必须管理状态机生命周期，不能只调用 Run 后丢弃实例。

## 选择前要回答的问题

* 状态是互斥枚举，还是相互独立的属性？锁定与置顶可以同时成立，未必适合硬塞到同一枚举。
* 哪些迁移允许发生，谁有权限发起，数据库并发变更怎样检查？
* 副作用发生在迁移阶段还是完成阶段，失败时如何回滚或补偿？
* 状态机实例的生命周期由谁控制，是否需要跨请求持久化？

这套状态机不是持久化工作流引擎，也不会自动提供分布式事务、审批历史或所有资源的权限验证。有关业务事务见[Discussions 发帖事务](/framework/data/transactions)。

## 参考实现

[状态机目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Components/States)包含状态、向量、上下文、状态图和处理器契约。本文不再保留仓库中不存在的资产仓储、资产状态图和资产服务。


# 执行管线

Zongsoft.Components Executor 执行器、上下文、Feature 特性和执行管线。

执行管线把一段处理逻辑包装成统一的执行单元，并通过上下文和 Feature 特性扩展重试、回退、熔断、限流、超时等横切能力。它适合把“调用某个处理器”这件事抽象成可组合、可配置、可测试的运行时对象。

`Executor` 负责把委托、处理器或上下文处理逻辑包装成 `IExecutor`；`Feature` 负责声明执行时需要的策略能力；`IFeaturePipelineBuilder` 负责把这些特性构建成实际管线。业务逻辑仍然保持为普通委托或处理器，策略逻辑由管线统一承担。

这个模型适合需要把横切策略放到统一入口的场景。调用方面对的是 `IExecutor<TArgument>` 或 `IExecutor<TArgument, TResult>`，不需要知道背后是委托、处理器还是插件加载出来的对象。

## 关键类型

| 类型                                                                 | 说明                                                      |
| ------------------------------------------------------------------ | ------------------------------------------------------- |
| `IExecutor`                                                        | 执行器接口，定义基于上下文的执行入口。                                     |
| `IExecutor<TArgument>`                                             | 带参数类型的执行器接口。                                            |
| `IExecutor<TArgument, TResult>`                                    | 带参数和返回值类型的执行器接口。                                        |
| `IExecutorContext`                                                 | 执行上下文接口，承载输入、输出、服务和执行状态。                                |
| `Executor`                                                         | 执行器静态工厂，提供 `Build(...)` 和全局 `Features`、`Pipelines` 扩展点。 |
| `ExecutorContext<TArgument>`、`ExecutorContext<TArgument, TResult>` | 默认执行上下文实现。                                              |
| `ExecutorBase<TArgument>`、`ExecutorBase<TArgument, TResult>`       | 执行器基类，支持过滤器和模板方法。                                       |
| `IFeature`                                                         | 执行特性的标记接口。                                              |
| `IFeatureBuilder`                                                  | 特性构建器接口，用于链式组合多个执行策略。                                   |
| `IFeaturePipeline`                                                 | 执行特性管线接口。                                               |
| `IFeaturePipelineBuilder`                                          | 特性管线构建器接口。                                              |
| `FeatureBuilder`                                                   | 默认特性构建器，提供链式组合入口。                                       |
| `Features.BreakerFeature`                                          | 熔断特性。                                                   |
| `Features.FallbackFeature`                                         | 回退特性。                                                   |
| `Features.RetryFeature`                                            | 重试特性。                                                   |
| `Features.ThrottleFeature`                                         | 限流特性。                                                   |
| `Features.TimeoutFeature`                                          | 超时特性。                                                   |

## 执行模型

`Executor.Build(...)` 可以把委托、处理器或上下文处理逻辑包装成 `IExecutor`。如果指定了 Feature，则会通过 `Executor.Pipelines` 构建特性管线，再把原始执行逻辑包在管线中执行。

来源：[framework/externals/polly/samples/Program.cs](https://github.com/Zongsoft/framework/blob/main/externals/polly/samples/Program.cs#L207)（节选；上下文见源文件）。

{% code title="Program.cs" %}

```csharp
var executor = _features.Build<int>(OnExecuteAsync);
```

{% endcode %}

这是 framework 的 Polly 交互样例：retry、timeout 等命令修改 \_features，执行命令再据此构建处理整数输入的执行器；OnExecuteAsync 在同一文件中定义。完整样例及其策略配置见[任务调度与弹性执行](/framework/externals/execution)。Discussions 当前没有在帖子写入外层配置自动重试，不能直接重放包含文件与统计更新的写入流程。

在这种结构里，业务代码只表达“要执行什么”，重试、回退、熔断、限流等策略由 Feature 管线提供，不需要散落在业务代码中。

## 与 Polly 的配合

核心框架只定义 Feature 特性模型和管线构建扩展点，不直接绑定某个弹性处理库。`Zongsoft.Externals.Polly` 会把核心框架中的 Feature 转换为 Polly 策略管线，让应用在保留统一抽象的同时获得具体的重试、超时、熔断等执行能力。

{% hint style="info" %}
如果没有注册对应的 `IFeaturePipelineBuilder` 实现，Feature 只是一组策略描述，不能自动产生重试、限流或熔断行为。实际效果取决于宿主程序加载的扩展和配置。
{% endhint %}

## 使用场景

* 调用外部接口时给处理逻辑增加超时、重试和熔断。
* 把消息处理器包装成统一执行器，便于统一记录日志和异常。
* 在设备采集、事件处理、调度任务中复用同一套执行上下文。
* 为插件加载出来的处理器增加一致的执行策略。
* 把策略配置和业务逻辑分开，让调用方只关心执行入口。

如果只是一次本地方法调用，没有复用策略或统一上下文需求，直接调用业务方法通常更简单。Feature 的执行顺序和具体行为取决于管线构建器实现，应在实际样例中观察每项策略对执行次数、返回值和异常的影响。

## 参考实现

* [Executor.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/Executor.cs)
* [Features 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Components/Features)
* [Zongsoft.Externals.Polly](https://github.com/Zongsoft/framework/tree/main/externals/polly)


# Zongsoft.Configuration

从 Discussions 选项读取出发，理解配置源、对象绑定、连接设置、Profile、Models 和 Options。

Zongsoft.Configuration 在标准 .NET 配置系统上增加插件选项、对象绑定、连接设置和 Profile 支持。配置先由不同来源形成键值树，再被绑定为业务需要的类型；读取配置和执行配置描述的动作是两个阶段。连接字符串被解析成功，不表示数据库已经连通；存储路径存在于选项中，也不表示对应文件系统插件已经部署。

标准配置入口是 [Microsoft.Extensions.Configuration.IConfiguration](https://learn.microsoft.com/zh-cn/dotnet/api/microsoft.extensions.configuration.iconfiguration) [*源码*](https://source.dot.net/#Microsoft.Extensions.Configuration.Abstractions/IConfiguration.cs)。Discussions.Configuration.IConfiguration 是论坛自己声明的选项模型，两者用途不同。

## 设计定位

| 部分      | 解决的问题              | 典型来源                   |
| ------- | ------------------ | ---------------------- |
| 配置源     | 配置值从哪里来，如何覆盖与更新    | 宿主配置、环境参数、插件 option 文件 |
| 对象绑定    | 字符串和节点怎样变成属性、集合与模型 | ConfigurationBinder    |
| 连接设置    | 名称、驱动和连接参数怎样组合     | 数据驱动及外部服务              |
| XML 选项  | XML 结构怎样映射为配置树     | \*.option              |
| Profile | 有层级的 INI 条目及导入     | \*.deploy              |
| Models  | 模型记录怎样成为配置源        | 框架模型配置测试               |
| Options | 如何把选项绑定接入依赖注入与变更监视 | 安全服务和调度服务              |

同一应用可以叠加多种配置源。判断最终值时需要沿宿主加载顺序检查覆盖关系，而不是只看某一个文件。文件选择、环境与站点组合见[选项文件参考](/references/option-files)。

## Discussions 的实际读取

来源：[src/Zongsoft.Discussions.option](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.option#L3)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.option" %}

```xml
<options>
	<option path="/Discussions">
		<general siteId="1" basePath="zfs.s3:/zongsoft-discussions/" />
	</option>
</options>
```

{% endcode %}

General 保存默认站点号和文件基路径。这里的 zfs.s3 是 Discussions 当前选项使用的文件系统方案，依赖相应存储扩展和环境配置；本地调试需要替换为自己可访问的存储位置。

Utility.GetFilePath 直接读取路径值：

来源：[src/Utility.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Utility.cs#L283)（节选；上下文见源文件）。

{% code title="Utility.cs" %}

```csharp
var basePath = ApplicationContext.Current.Configuration.GetOptionValue<string>("/Discussions/General.BasePath");

if(string.IsNullOrWhiteSpace(basePath))
	return string.Empty;
```

{% endcode %}

/Discussions 定位模块配置，General.BasePath 对应 general 元素上的 basePath 属性。取值为空时路径计算返回空字符串，不能继续把这个结果当成可写位置。后续按站点和用户分层的过程见[文件与内容存储](/framework/core/io)。

## 基础绑定

读取单个值适合少量独立设置；配置项较多且一起使用时，GetOption 或 Bind 可把一段配置绑定为模型。Zongsoft 绑定器支持属性别名、集合、未识别属性容器和动态模型，具体转换仍受目标类型约束。

来源：[src/Configuration/IConfiguration.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Configuration/IConfiguration.cs#L35)（节选；上下文见源文件）。

{% code title="IConfiguration.cs" %}

```csharp
public interface IConfiguration
{
	/// <summary>获取或设置默认的站点编号。</summary>
	uint SiteId { get; set; }

	/// <summary>获取或设置文件存储的基路径。</summary>
	string BasePath { get; set; }
}
```

{% endcode %}

这个接口给出 Discussions 已定义的选项形状。当前 Utility 的实际路径是单值读取，不应把接口的存在解释为所有消费方都已经通过依赖注入取得该模型。

框架测试中的 QueueOptions 展示未识别属性与集合命名：

来源：[framework/Zongsoft.Core/test/Configuration/Xml/OptionConfigurationTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Configuration/Xml/OptionConfigurationTest.cs#L186)（节选；上下文见源文件）。

{% code title="OptionConfigurationTest.cs" %}

```csharp
[Configuration(nameof(Properties))]
public class QueueOptions
{
	public string Name { get; set; }
	public SubscriptionOptions Subscription { get; set; }
	public IDictionary<string, string> Properties { get; set; }

	public class SubscriptionOptions
	{
		public Messaging.MessageReliability Reliability { get; set; }

		[ConfigurationProperty]
		public TopicOptionsCollection Topics { get; set; }
	}

	public class TopicOptions
	{
		public string Name { get; set; }

		[ConfigurationProperty("tag")]
		public IList<string> Tags { get; set; }

		public override string ToString() => this.Name;
	}

	public class TopicOptionsCollection() : KeyedCollection<string, TopicOptions>(StringComparer.OrdinalIgnoreCase)
	{
		protected override string GetKeyForItem(TopicOptions topic) => topic.Name;
	}
}

```

{% endcode %}

上面的 QueueOptions 属于 XML 配置测试。Configuration 指定 Properties 接收未识别项，ConfigurationProperty 为嵌套集合和 tag 属性提供绑定约定。复制某一属性声明时还需要对应的元素类型和集合键规则，不能只复制属性名。

ConfigurationBinderOptions 的 BindNonPublicProperties 控制非公共成员绑定，UnrecognizedError 控制未识别配置是否报错。给已有集合重复绑定可能追加元素；重载配置时应明确重建还是更新对象，避免无意累积。

## 连接设置

连接设置把连接名、驱动名和参数值分开。连接名用于应用查找，驱动名决定参数模型，Value 再由该模型解析为端口、超时、端点、集合等类型。

来源：[framework/Zongsoft.Core/test/Configuration/Xml/OptionConfigurationTest-1.option](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Configuration/Xml/OptionConfigurationTest-1.option#L30)（节选；上下文见源文件）。

{% code title="OptionConfigurationTest-1.option" %}

```xml
<option path="/Data">
	<connectionSettings default="db1">
		<connectionSetting connectionSetting.name="db1" driver="mysql" mode="all" value="server=localhost" />
	</connectionSettings>
</option>
```

{% endcode %}

这是框架 XML 测试中的 db1 配置，仅使用 localhost 验证键值绑定。Discussions 按模块名请求访问器，实际部署还需要相应名称或默认连接；完整流程见[连接与数据源](/framework/data/connections)，驱动差异见[按数据驱动阅读](/framework/data/drivers)。

### 复合属性

来源：[framework/Zongsoft.Core/test/Configuration/ConnectionSettingsTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Configuration/ConnectionSettingsTest.cs#L178)（节选；上下文见源文件）。

{% code title="ConnectionSettingsTest.cs" %}

```csharp
public void TestConnectionSettingsCompositeProperty()
{
	var settings = MyDriver.Instance.GetSettings("a.b.c=none;cluster.address=192.168.0.100;nothing.property=none;cluster.heartbeat=30s");
	Assert.NotNull(settings);

	Assert.False(settings.Cluster.IsEmpty);
	Assert.Equal("192.168.0.100", settings.Cluster.Address);
	Assert.Equal(TimeSpan.FromSeconds(30), settings.Cluster.Heartbeat);
	Assert.Equal("none", settings.Properties["a.b.c"]);
	Assert.Equal("none", settings.Properties["nothing.property"]);
}
```

{% endcode %}

MyDriver、ClusterSettings 和 MyConnectionSettings 都是同一测试文件中的夹具。cluster.address 与 cluster.heartbeat 被绑定为 Cluster 的成员，其它未识别键留在 Properties。只有驱动设置模型定义了相应复合属性时，这种语法才会产生结构化对象。集合元素的专用转换见[转换器](/framework/core/components/converters)。

## XML 选项文件

来源：[framework/Zongsoft.Core/test/Configuration/Xml/OptionConfigurationTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Configuration/Xml/OptionConfigurationTest.cs#L14)（节选；上下文见源文件）。

{% code title="OptionConfigurationTest.cs" %}

```csharp
public static IConfigurationRoot GetConfiguration1()
{
	return new ConfigurationBuilder()
		.AddOptionFile("Configuration/Xml/OptionConfigurationTest-1.option")
		.Build();
}
```

{% endcode %}

测试从自己的输出目录载入 option 文件，再读取配置节点。在插件宿主中通常由选项加载流程完成这一步，不需要每个业务服务再次创建配置根。

option 的 path 指定挂载位置；元素形成子节点，属性形成配置值。重复元素需要稳定的键，例如 connectionSetting.name；不能仅靠文档中的排列顺序推断配置集合索引。详细规则和 Discussions 清单的配合见[选项文件参考](/references/option-files)。

## Profile 与 .deploy

Profile 保留 INI 的条目、节和注释，同时支持以空格或 Tab 分隔的层级节。下面是框架现有 Profile 测试输入：

来源：[framework/Zongsoft.Core/test/Configuration/Profiles/Profile-1.ini](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Configuration/Profiles/Profile-1.ini#L1)（节选；上下文见源文件）。

{% code title="Profile-1.ini" %}

```ini
﻿[plugins zongsoft data]
nuget:Zongsoft.Data
```

{% endcode %}

它会得到 plugins / zongsoft / data 三层节，nuget:[Zongsoft.Data](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Data) 是该节下没有值的条目。Profile 只负责解析这个结构，NuGet 定位、复制和版本选择由部署工具解释。

Discussions 的包内清单使用相对产物路径和 Framework 变量：

来源：[src/Zongsoft.Discussions.deploy](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.deploy#L1)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.deploy" %}

```ini
artifacts/Zongsoft.Discussions.plugin
artifacts/Zongsoft.Discussions.option
artifacts/Zongsoft.Discussions.mapping
lib/$(Framework)/Zongsoft.Discussions.*
```

{% endcode %}

这些路径相对于包的文件布局。Framework 变量由部署上下文提供，用来选择目标程序集。段落目标目录、来源路径、重命名、条件表达式、导入及变量作用域都由[部署清单参考](/references/deploy-files)统一说明。

内置 ImportDirective 处理导入，指令以注释形式出现。导入文件中的相对路径、重复文件和循环引用都应按当前 Profile.Load 与部署工具行为核对；不要根据普通 INI 解析器推断它们的语义。

## Models 配置源

Models 把一组带键值的模型记录提供为配置树。Discussions 没有完整的模型配置持久化流程，因此这里采用 ModelConfigurationTest：

来源：[framework/Zongsoft.Core/test/Configuration/Models/ModelConfigurationTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Configuration/Models/ModelConfigurationTest.cs#L163)（节选；上下文见源文件）。

{% code title="ModelConfigurationTest.cs" %}

```csharp
public abstract class ConfigurationEntity
{
	public abstract int TenantId { get; set; }
	public abstract int BranchId { get; set; }
	public abstract string Module { get; set; }
	public abstract string Key { get; set; }
	public abstract string Value { get; set; }
}

public static class ConfigurationEntityExtension
{
	public static string GetInfo(this ConfigurationEntity entity)
	{
		if(entity == null)
			return string.Empty;

		return $"[{entity.TenantId}-{entity.BranchId}:{entity.Module}]" + Environment.NewLine + $"{entity.Key}={entity.Value}";
	}
}
```

{% endcode %}

来源：[framework/Zongsoft.Core/test/Configuration/Models/ModelConfigurationTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Configuration/Models/ModelConfigurationTest.cs#L71)（节选；上下文见源文件）。

{% code title="ModelConfigurationTest.cs" %}

```csharp
var models = GetModels(dictionary);

return new ConfigurationBuilder()
	.AddModels(models, source =>
	{
		source.OnChange(model => System.Diagnostics.Debug.WriteLine(model.GetInfo()));

		if(persistent != null)
			source.OnChange(persistent);
	})
	.Build();
```

{% endcode %}

GetModels 在同一测试里用内存字典构造 ConfigurationEntity，并填充租户、分支和模块字段。OnChange 收到被更新或新建的模型；示例回调只记录或检查变更，没有真实数据库事务。需要持久化时，调用方必须实现回调并承担写入失败、权限、并发和重载策略。

配置源可读取动态模型、数据字典及普通对象。默认键值成员是 Key / Value，字段名不同则通过 Mapping 指定。与[数据服务](/framework/data/services)配合时，应先确定配置读取所处身份与租户范围，不能把配置表天然视为全局可见数据。

## Options 集成

OptionsConfigurationExtension 把 Zongsoft 绑定器接入服务集合，并提供命名选项和配置变更令牌。安全凭证提供者中已有实际的 Options 属性注入：

来源：[framework/Zongsoft.Security/src/CredentialProvider.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Security/src/CredentialProvider.cs#L65)（节选；上下文见源文件）。

{% code title="CredentialProvider.cs" %}

```csharp
[Options("Security/Authority")]
public Configuration.AuthenticationOptions Options { get; set; }
```

{% endcode %}

框架扫描和注入流程识别该属性，从指定配置路径取得 AuthenticationOptions。标记属性本身不会启动配置源，也不会让用 new 手工创建的任意对象自动完成注入；应通过支持该注入约定的服务构建流程取得对象，参阅[服务注册与注入](/framework/core/services)。

| 对象                                    | 作用                      |
| ------------------------------------- | ----------------------- |
| OptionsConfigurator                   | 按选项名称选取配置并调用绑定器         |
| OptionsConfigurationChangeTokenSource | 把配置源变更通知交给 Options 监视机制 |
| OptionsFactory                        | 创建选项实例，支持框架动态模型         |

配置变化能否被当前消费者观察到，还取决于消费方式和对象生命周期。一次取值、缓存一个选项对象、使用 [IOptionsMonitor](https://learn.microsoft.com/zh-cn/dotnet/api/microsoft.extensions.options.ioptionsmonitor-1) [*源码*](https://source.dot.net/#Microsoft.Extensions.Options/IOptionsMonitor.cs) 监听更新，不是相同语义。

## 实践建议

从 Discussions 开始时，先确认选项文件进入宿主，再核对最终键和值、驱动与服务注册，最后执行受影响业务。独立解析测试只能说明格式和绑定正确；数据库连接、文件写入、消息订阅仍需各自验证。

保持配置键稳定，把敏感连接值放在环境配置中。无法识别的键应显式报错或进入明确的扩展容器，不能因为解析器没有抛异常就认为设置已经生效。框架测试入口见 [Configuration 测试目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/test/Configuration)。


# Zongsoft.Data

Zongsoft.Data 命名空间及其子命名空间在核心类库中的职责。

[`Zongsoft.Data`](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Data) 在[核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core)中定义数据访问的基础抽象、数据服务模型、条件、操作元、模式、分页、排序、事务、数据字典和数据元数据。完整的数据引擎实现由 `Zongsoft.Data` 模块继续扩展。

## 主要职责

* 定义 `IDataAccess`、`IDataService`、`IDataSearcher` 等数据访问和数据服务抽象。
* 表达查询条件、操作元、分页、排序、返回值、数据模式和数据类型。
* 提供 [`Transaction`](/framework/core/data/transactions) 环境事务对象和事务参与登记模型。
* 提供数据服务事件、数据操作选项、模型描述和数据字典。
* 提供数据元数据抽象，供映射文件和数据引擎实现使用。

## 子命名空间

| 命名空间                                                              | 说明                       |
| ----------------------------------------------------------------- | ------------------------ |
| `Zongsoft.Data.Archiving`                                         | 数据归档相关抽象。                |
| `Zongsoft.Data.Metadata`                                          | 实体、属性、关联、命令和参数等数据元数据模型。  |
| [`Zongsoft.Data.Transactions`](/framework/core/data/transactions) | 事务状态、事务信息、登记上下文和事务参与者抽象。 |

## 相关资源

* [Data 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Data)
* [数据引擎](/framework/data)
* [Zongsoft.Data NuGet 包](https://www.nuget.org/packages/Zongsoft.Data)


# Transactions

Zongsoft.Data.Transaction 类与 Zongsoft.Data.Transactions 命名空间的职责和主要类型。

`Zongsoft.Data.Transaction` 提供[核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core)中的轻量环境事务对象，事务状态、事务信息、事务阶段和事务参与登记类型位于 `Zongsoft.Data.Transactions` 命名空间。它用于在数据服务、批处理或跨组件操作中表达一段可提交或回滚的应用层事务。

## 主要职责

* 通过 `Transaction.Current` 维护当前异步上下文中的环境事务。
* 支持以 `ReadCommitted()`、`RepeatableRead()`、`Serializable()` 等工厂方法创建常用隔离级别的事务。
* 提供同步与异步提交/回滚，并在释放未提交事务时回滚。
* 通过 `IEnlistment`、`EnlistmentContext` 和 `EnlistmentPhase` 支持事务参与者登记与阶段通知。
* 为数据服务、批处理和跨组件操作提供统一事务抽象。

## 典型用法

Discussions 创建主题时把主记录、主题内容贴和作者统计放入事务。同步与异步完整用例见[事务与一致性](/framework/data/transactions)。

如果需要让组件感知事务提交或回滚，可实现 `Zongsoft.Data.Transactions.IEnlistment` 并登记到当前事务。登记对象会在事务完成时收到 `Commit` 或 `Rollback` 阶段通知。

## 相关资源

* [Zongsoft.Data](/framework/core/data)
* [数据引擎](/framework/data)
* [Transaction 源码](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Data/Transaction.cs)
* [Transactions 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Data/Transactions)

## 异步完成与环境作用域

下面是 Discussions 的真实异步插入钩子；Posting、Utility 和统计方法都来自模块本身。正文文件由 MutateContentAsync 在数据库操作失败时清理，它不是数据库事务自动回滚的资源。

异步数据操作可用 `await using` 管理事务，再等待 `CommitAsync`，让调用方观察真实完成或失败：

来源：[src/Services/ThreadService.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/ThreadService.cs#L336)（节选；上下文见源文件）。

{% code title="ThreadService.cs" %}

```csharp
protected override async ValueTask<int> OnInsertAsync(IDataDictionary<Models.Thread> data, ISchema schema, DataInsertOptions options, CancellationToken cancellation)
{
	cancellation.ThrowIfCancellationRequested();
	if(!data.TryGetValue(p => p.Post, out var post) || post == null || string.IsNullOrEmpty(post.Content))
		throw new InvalidOperationException("Missing content of the thread.");

	//确保数据模式含有“主题内容贴”复合属性
	schema.Include("Post{*}");

	//更新主题内容贴的相关属性
	post.Visible = false;
	post.Approved = await this.ServiceProvider.ResolveRequired<ForumService>().CanPublishAsync(data, cancellation);
	data.SetValue(p => p.Approved, post.Approved);
	schema.Include(nameof(Models.Thread.Approved));

	var content = DataDictionary.GetDictionary<Post>(post);
	return await Utility.MutateContentAsync(content, () => this.Posting.GetContentFilePath(content), async () =>
	{
		await using(var transaction = new Transaction())
		{
			//调用基类同名方法，插入主题数据
			var count = await base.OnInsertAsync(data, schema, options, cancellation);

			if(count < 1)
				return count;

			//更新发帖人关联的主题统计信息
			await this.SetMostRecentThreadAsync(data, cancellation: cancellation);

			//提交事务
			await transaction.CommitAsync(cancellation);

			return count;
		}
	}, cancellation);
}
```

{% endcode %}

异步参与者通过 `IEnlistment.OnEnlistAsync` 接收完成通知。事务开始终结后，后续取消不会中止已经进行的提交/回滚；取消请求不能作为数据库未提交的证明。

`DisposeAsync` 会先退出当前环境事务作用域，再异步回滚未完成事务，防止旧作用域影响后续代码。它不会自动将任意 HTTP 请求、消息确认或外部系统操作纳入原子事务。

数据引擎中的连接、隔离和跨系统边界见[事务与一致性](/framework/data/transactions)。


# Zongsoft.Diagnostics

采用 Hangfire 处理器和框架日志测试说明日志入口、上下文与落盘。

诊断日志记录发生了什么、发生在哪里以及必要上下文。Discussions 没有独立的日志处理器范例，因此采用框架 Hangfire 的现有处理器与文本日志测试。遥测指标和追踪另见[Telemetry](/framework/core/diagnostics/telemetry)。

## 业务处理器怎样写日志

来源：[framework/externals/hangfire/samples/MyHandler.cs](https://github.com/Zongsoft/framework/blob/main/externals/hangfire/samples/MyHandler.cs#L11)（节选；上下文见源文件）。

{% code title="MyHandler.cs" %}

```csharp
public class MyHandler : HandlerBase<object>
{
	private long _count = 0;

	protected override ValueTask OnHandleAsync(object argument, Parameters parameters, CancellationToken cancellation) =>
		Logging.GetLogging(this).DebugAsync(
			"MyHandler handles the scheduling of the Hangfire.",
			new
			{
				Count = Interlocked.Increment(ref _count),
				Argument = argument,
				Parameters = parameters,
			},
			cancellation);
}
```

{% endcode %}

Logging.GetLogging(this) 根据当前对象取得日志入口，异步记录消息以及 Count、Argument、Parameters。计数器是进程内状态，不是持久化执行次数；同一任务重试也可能再次计数。

这里的上下文适合排障，但实际任务参数可能包含敏感信息。扩展这个样例时只记录必要字段，不应照搬“把整个参数对象写入日志”的做法处理用户数据。

## 日志入口与输出实现

日志级别表达严重程度，来源用于定位模块，异常对象保留调用链，附加数据承载结构化上下文。Logger 负责输出，Predication 负责筛选，Logging 组织入口与分发；注册成功不意味着目标文件、数据库或远端接收器已经可用。

## 文本日志的真实并发测试

来源：[framework/Zongsoft.Core/test/Diagnostics/TextFileLoggerTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Diagnostics/TextFileLoggerTest.cs#L13)（节选；上下文见源文件）。

{% code title="TextFileLoggerTest.cs" %}

```csharp
public async Task TestInfoAsync()
{
	const int COUNT = 512;

	using var context = new LoggerContext();
	using var logger = context.CreateLogger(16);

	await Parallel.ForAsync(0, COUNT, TestContext.Current.CancellationToken, async (index, cancellation) =>
	{
		await logger.LogAsync(new LogEntry(LogLevel.Info, "ConcurrentInfo", GetMessage(index)), cancellation);
	});

	await logger.FlushAsync(TestContext.Current.CancellationToken);

	var content = context.ReadAllText();
	AssertMessages(content, COUNT);
}
```

{% endcode %}

LoggerContext、GetMessage 与 AssertMessages 都来自同一测试文件。测试并发写入后显式 Flush，再检查每条消息恰好出现一次。它验证这个文本日志实现的局部行为，不是分布式日志投递承诺。

## 筛选、刷新与退出

日志过滤应先控制来源和级别，再考虑输出成本。缓冲日志可能在刷新前仍未落盘，进程退出和异常路径需要检查 Flush 与释放时机。错误日志和普通信息日志是否使用同样缓冲策略，应以实现和测试为准。

文件路径、滚动、资源文本和异常序列化也要与实际输出器一起检查。不要为了演示扩展接口凭空定义一个数据库日志类；现有实现可从[诊断源码](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Diagnostics)与[日志断言](/framework/core/common/predication)继续阅读。

相关页面：[缓冲器](/framework/core/caching/spooler)、[诊断配置](/framework/diagnostics)、[运行排障](/get-started/run-and-debug)。


# Telemetry

从框架安全模块的真实认证指标理解计量器、仪表、标签和导出。

遥测指标把多次操作汇总为可观察的数量或分布，追踪则记录一次调用经过的边界。Discussions 尚未定义自己的指标组；其依赖的框架安全模块已为认证过程记录计数，本页以该实现为例。

## 创建模块计量器

来源：[framework/Zongsoft.Security/src/Module.Meter.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Security/src/Module.Meter.cs#L61)（节选；上下文见源文件）。

{% code title="Module.Meter.cs" %}

```csharp
internal Diagnostor()
{
	#if NET8_0_OR_GREATER
	_meter = Current.Services.ResolveRequired<IMeterFactory>().Create(NAME);
	#else
	_meter = new Meter(NAME, Current.Version.ToString());
	#endif

	this.Authentication = new(_meter);
}
```

{% endcode %}

.NET 8 及以上从模块容器取得 IMeterFactory；较早目标直接创建 Meter。计量器名来自安全模块常量，生命周期跟随模块诊断器，不是每次登录新建一个。

## 定义认证仪表

来源：[framework/Zongsoft.Security/src/Module.Meter.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Security/src/Module.Meter.cs#L74)（节选；上下文见源文件）。

{% code title="Module.Meter.cs" %}

```csharp
	public sealed class AuthenticationMeter(Meter meter)
	{
		#region 常量定义
		private const string METER = nameof(Diagnostor.Authentication);
		#endregion

		#region 公共字段
		public readonly Counter<long> Authenticated = meter.CreateCounter<long>($"{METER}.{nameof(Authenticated)}");
		public readonly Counter<long> Authenticating = meter.CreateCounter<long>($"{METER}.{nameof(Authenticating)}");
		#endregion
	}
}
```

{% endcode %}

Authenticated 与 Authenticating 是两个 Counter，用于不同阶段。仪表名称稳定之后才能配置过滤、仪表盘和告警；不能仅因中文显示名称相同就更改协议中的名称。

## 在真实事件中记录测量值

来源：[framework/Zongsoft.Security/src/Module.Events.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Security/src/Module.Events.cs#L114)（节选；上下文见源文件）。

{% code title="Module.Events.cs" %}

```csharp
private static bool OnAuthenticating(Privileges.AuthenticatingEventArgs args)
{
	Current.Meter.Authentication.Authenticating.Add(1,
		new KeyValuePair<string, object>(nameof(args.Scheme), args.Scheme),
		new KeyValuePair<string, object>(nameof(args.Scenario), args.Scenario));

	return true;
}
```

{% endcode %}

测量值是一次认证尝试，标签区分 Scheme 与 Scenario。标签应具有受控范围；把每个用户编号或请求编号用作指标标签会造成高基数，个体追踪更适合日志或 tracing。

## 采集与导出是后续步骤

创建 Meter 和记录 Counter 不代表外部后端已经收到数据。还需要选中来源、配置导出器与传输端点，并实际产生测量值。真实配置见[诊断扩展](/framework/diagnostics)。

| 信号 | 适合回答的问题              |
| -- | -------------------- |
| 日志 | 某次操作发生了什么，异常调用链是什么   |
| 指标 | 一段时间内多少次、分布如何、是否异常增长 |
| 追踪 | 一次请求经过哪些组件，每段耗时如何    |

三者可以关联，但不能仅凭一个指标计数反推出完整业务过程。对发帖或文件存储的监控若要加入 Discussions，应先确定业务成功点、失败分类和事务边界，不能把安全模块的计数直接改名成论坛统计。

## 验证范围

确认计量器创建、事件触发、标签值、过滤匹配与导出结果；再检查取消、后端不可用和进程退出。采用自建 OTLP 服务端时还需核对转换限制，见[协议接入](/framework/diagnostics/otlp)。


# Zongsoft.Expressions

Zongsoft.Expressions 的表达式求值抽象、词法分析器、token 模型和 tokenizer 扩展方式。

`Zongsoft.Expressions` 提供表达式求值抽象和词法分析基础设施。它把一段表达式文本先切分成常量、标识符、关键字和符号 token，再交给具体表达式求值器或上层解析器解释语义。

这个命名空间更像“表达式基础层”，而不是某一种固定语法的完整脚本引擎。框架中的条件表达式、配置表达式、命令参数表达式和部分数据查询表达式，都可以在这套基础层之上定义自己的关键字、操作符和求值规则。

{% hint style="info" %}
`Lexer` 只负责词法切分，不负责判断 `1 + 2` 应该如何计算，也不负责处理操作符优先级。算术、条件、成员访问或数据库表达式语义，需要由调用方或具体 `IExpressionEvaluator` 实现完成。
{% endhint %}

## 主要职责

* 定义表达式求值器接口、基础类和运行选项。
* 提供可复用的词法分析器 `Lexer` 和扫描器 `TokenScanner`。
* 把表达式文本切分为 `TokenType.Constant`、`TokenType.Identifier`、`TokenType.Symbol` 和 `TokenType.Keyword`。
* 内置空值、布尔值、数字、字符串、标识符和符号 tokenizer。
* 支持调用方追加或调整 tokenizer，以识别领域关键字和自定义符号。
* 为插件配置、命令解释、条件解析、数据表达式和轻量脚本求值提供底层能力。

## 核心类型

| 类型                                                                                                                                          | 作用         | 常见用法                           |
| ------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------------------------------ |
| [`IExpressionEvaluator`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Expressions/IExpressionEvaluator.cs)             | 表达式求值器契约。  | 按表达式文本、选项和变量集合返回求值结果。          |
| [`ExpressionEvaluatorBase`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Expressions/ExpressionEvaluatorBase.cs)       | 求值器基类。     | 统一名称匹配、默认选项、全局变量和释放逻辑。         |
| [`ExpressionEvaluatorOptions`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Expressions/ExpressionEvaluatorOptions.cs) | 求值运行选项。    | 指定输入、输出、错误输出和扩展属性。             |
| [`Lexer`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Expressions/Lexer.cs)                                           | 词法分析入口。    | 从字符串或流创建 `TokenScanner`。       |
| [`TokenScanner`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Expressions/TokenScanner.cs)                             | token 扫描器。 | 逐个扫描 token，或作为集合枚举所有 token。    |
| [`ITokenizer`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Expressions/ITokenizer.cs)                                 | 分词器接口。     | 为新的字面量、关键字或领域符号提供识别逻辑。         |
| [`Token`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Expressions/Token.cs)                                           | token 对象。  | 携带 token 类型和值。                 |
| [`SymbolToken`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Expressions/SymbolToken.cs)                               | 符号 token。  | 表示 `+`、`==`、`&&`、`??`、括号等内置符号。 |
| [`SyntaxException`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Expressions/SyntaxException.cs)                       | 语法异常。      | 分词过程中遇到非法字符或不完整字面量时抛出。         |

## 词法分析流程

`Lexer` 维护一个 tokenizer 列表。`TokenScanner.Scan()` 每次扫描时会先跳过空白字符，然后按列表顺序尝试每个 tokenizer；第一个返回有效 token 的 tokenizer 决定当前位置的识别结果。如果所有 tokenizer 都无法识别当前字符，而输入尚未结束，则抛出 `SyntaxException`。

默认 `Lexer` 的 tokenizer 顺序如下：

| 顺序 | tokenizer             | 识别内容                            |
| -- | --------------------- | ------------------------------- |
| 1  | `NullTokenizer`       | `null`，忽略大小写。                   |
| 2  | `NumberTokenizer`     | 整数、小数，以及 `L`、`F`、`D`、`M` 数字后缀。  |
| 3  | `StringTokenizer`     | 单引号或双引号字符串。                     |
| 4  | `BooleanTokenizer`    | `true`、`false`，忽略大小写。           |
| 5  | `IdentifierTokenizer` | 以字母或 `_` 开头，后续为字母、数字或 `_` 的标识符。 |
| 6  | `SymbolTokenizer`     | 内置符号 token。                     |

来源：[framework/Zongsoft.Core/test/Expressions/LexerTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Expressions/LexerTest.cs#L15)（节选；上下文见源文件）。

{% code title="LexerTest.cs" %}

```csharp
const string EXPRESSION = @"1+2f	_abc123'text\'suffix'	-30L*4.5 / 5.5m (true || FALSE?yes:no)null??nothing";

var scanner = Lexer.Instance.GetScanner(EXPRESSION);
Assert.NotNull(scanner);

var token = scanner.Scan();
Assert.NotNull(token);
Assert.Equal(TokenType.Constant, token.Type);
Assert.IsType<int>(token.Value);
Assert.Equal(1, (int)token.Value);

token = scanner.Scan();
Assert.NotNull(token);
Assert.Equal(SymbolToken.Plus, token);

token = scanner.Scan();
Assert.NotNull(token);
Assert.Equal(TokenType.Constant, token.Type);
Assert.IsType<float>(token.Value);
Assert.Equal(2.0f, (float)token.Value);
```

{% endcode %}

Discussions 没有自行构造词法器；上面的 [核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core) 测试依次断言整数常量 1、加号和单精度常量 2。完整测试继续覆盖标识符、字符串、负号和其他运算符。后续它是普通条件、数据查询条件还是命令参数条件，取决于调用方如何解释这些 token。

## Token 规则

内置 token 大致分为四类：

| token 类型     | 示例                                             | 说明                                  |
| ------------ | ---------------------------------------------- | ----------------------------------- |
| `Constant`   | `null`、`true`、`30L`、`4.5`、`5.5m`、`'text'`      | 字面量常量，`Token.Value` 会保存解析后的 .NET 值。 |
| `Identifier` | `Name`、`_abc123`、`Field1`                      | 变量名、字段名、参数名、方法名或调用方定义的标识。           |
| `Symbol`     | `+`、`-`、`*`、`/`、`==`、`&&`、`??`、`(`、`)`、`[`、`]` | 内置操作符或分隔符。                          |
| `Keyword`    | `in`、`between`                                 | 由调用方显式加入 `KeywordTokenizer` 后才会出现。  |

数字字面量从数字开始：不带小数点时默认解析为 `int`，包含小数点时默认解析为 `double`；后缀 `L` 表示 `long`，`F` 表示 `float`，`D` 表示 `double`，`M` 表示 `decimal`。数字不能以小数点结尾，也不能包含多个小数点。

字符串可以使用单引号或双引号包裹，并支持常见转义：`\\`、`\'`、`\"`、`\s`、`\t`、`\n`、`\r`。字符串字面量不能跨行，缺少结束引号时会抛出 `SyntaxException`。

内置符号覆盖常见运算和分隔场景：

| 类别    | 符号                      |
| ----- | ----------------------- |
| 算术    | `+`、`-`、`*`、`/`、`%`     |
| 逻辑与比较 | `!`、`&&`、\`             |
| 空值与条件 | `??`、`?`、`:`            |
| 访问与分隔 | `.`、`,`、`;`、\`          |
| 括号    | `(`、`)`、`[`、`]`、`{`、`}` |

{% hint style="warning" %}
数字负号不是数字字面量的一部分。表达式 `-30L` 会被扫描为符号 `-` 和常量 `30L`，是否把它解释为负数由后续语法解析或求值阶段决定。
{% endhint %}

## 关键字与自定义 tokenizer

默认词法器没有启用业务关键字，因此 `in`、`between` 这类文本会先被识别为标识符。需要领域关键字时，可以创建新的 `Lexer` 实例，并把 `KeywordTokenizer` 放在 `IdentifierTokenizer` 之前。

来源：[framework/Zongsoft.Core/test/Expressions/LexerTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Expressions/LexerTest.cs#L139)（节选；上下文见源文件）。

{% code title="LexerTest.cs" %}

```csharp
const string EXPRESSION = @"Field1 == 100 && Field2<1.23f && Field3 >=10.5m && (PI between ""3.1415926~3.1415927"" || Number IN [10,20,30] )";

var lexer = new Lexer();
lexer.Tokenizers.Insert(0, new KeywordTokenizer(true, "in", "Between"));

var scanner = lexer.GetScanner(EXPRESSION);
Assert.NotNull(scanner);

var token = scanner.Scan();
Assert.NotNull(token);
Assert.Equal(TokenType.Identifier, token.Type);
Assert.Equal("Field1", token.Value);
```

{% endcode %}

`KeywordTokenizer(true, ...)` 表示忽略大小写，因此 `IN`、`in` 和 `In` 都可以被识别为同一个关键字 token。关键字识别后仍只产生 token，不会自动实现集合包含、范围匹配或 SQL 翻译；这些语义应由后续解析器或求值器处理。

自定义 tokenizer 适合这些场景：

* 需要识别领域关键字，例如 `between`、`like`、`contains`。
* 需要把某类字面量直接转换为特定值，例如日期、时间段、枚举名或变量占位符。
* 需要限制可用符号，或为某个 DSL 增加专用符号。

实现自定义 tokenizer 时应遵守一个原则：只在当前位置能完整识别时返回 token；不能识别时返回失败结果，并把读取器偏移量恢复到尝试前的位置。这样后续 tokenizer 才能继续判断同一段文本。

## 表达式求值器

`IExpressionEvaluator` 定义了表达式求值的公共形状。调用方传入表达式文本、可选变量集合和可选运行选项，求值器返回一个对象结果。

来源：[framework/externals/python/test/PythonExpressionEvaluatorTest.cs](https://github.com/Zongsoft/framework/blob/main/externals/python/test/PythonExpressionEvaluatorTest.cs#L14)（节选；上下文见源文件）。

{% code title="PythonExpressionEvaluatorTest.cs" %}

```csharp
public void TestEvaluate1()
{
	var evaluator = new PythonExpressionEvaluator();
	var variables = new Dictionary<string, object>();

	var result = evaluator.Evaluate("1+2", null);
	Assert.NotNull(result);
	Assert.Equal(3, Zongsoft.Common.Convert.ConvertValue<int>(result));

	variables["subtract"] = (Delegate)Subtract;
	result = evaluator.Evaluate("subtract(100, 20)", variables);
	Assert.NotNull(result);
	Assert.Equal(80, Zongsoft.Common.Convert.ConvertValue<int>(result));

	evaluator.Evaluate("a=1;b=2;result=a+b;", variables);
	Assert.NotEmpty(variables);
	Assert.True(variables.TryGetValue("result", out result));
	Assert.Equal(3, Zongsoft.Common.Convert.ConvertValue<int>(result));
}
```

{% endcode %}

上面是 Python 插件的实际测试，Subtract 为同一测试类中的静态方法，变量字典还接收脚本写回的 result。求值器通常由具体模块实现自己的语法和语义。`ExpressionEvaluatorBase` 提供这些通用能力：

* `Name` 表示求值器名称。
* 实现服务匹配接口，可以按名称忽略大小写查找求值器。
* `Global` 保存求值器级全局变量。
* `Options` 保存默认输入、输出、错误输出和扩展属性。
* `Dispose()` 释放时会清理全局变量。

`ExpressionEvaluatorOptions` 默认使用 `Console.In`、`Console.Out` 和 `Console.Error`，也可以通过静态方法或扩展方法替换输入输出。

来源：[framework/externals/python/test/PythonExpressionEvaluatorTest.cs](https://github.com/Zongsoft/framework/blob/main/externals/python/test/PythonExpressionEvaluatorTest.cs#L98)（节选；上下文见源文件）。

{% code title="PythonExpressionEvaluatorTest.cs" %}

```csharp
public void TestEvaluateOutputDoesNotLeakAcrossCallsOrEvaluators()
{
	const string PRINT_MESSAGE = "First evaluator output";
	using var evaluator = new PythonExpressionEvaluator();

	using(var output = new StringWriter())
	{
		evaluator.Evaluate($"print('{PRINT_MESSAGE}')", ExpressionEvaluatorOptions.Out(output));
		Assert.Equal(PRINT_MESSAGE, output.ToString());
	}

	var result = evaluator.Evaluate("print('Subsequent call output'); result=41");
	Assert.Equal(41, Zongsoft.Common.Convert.ConvertValue<int>(result));

	using var subsequentEvaluator = new PythonExpressionEvaluator();
	result = subsequentEvaluator.Evaluate("print('Second evaluator output'); result=42");

	Assert.Equal(42, Zongsoft.Common.Convert.ConvertValue<int>(result));
}
```

{% endcode %}

输出隔离测试先释放首轮输出对象，再执行下一次调用和另一个求值器调用，用于验证输出流恢复。它不证明多个调用并发时共享运行时完全隔离；更多边界见脚本专题。

如果应用中注册了多个 `IExpressionEvaluator` 实现，可以结合服务模型按名称选择。例如 `ExpressionEvaluatorBase` 已经支持忽略大小写匹配，因此调用方可以通过服务发现机制查找某个命名求值器。

现有语言实现的注册名是 Lua、Python 和 Scriban。选择方式、部署和语法差异见[脚本与表达式](/framework/externals/scripting)。Discussions 未注册 JavaScript 求值器，不能把名称查找成功视为任意语言都已经可用。

## 使用建议

把 `Lexer` 用在“需要先理解文本结构”的边界层：例如解析配置值、命令参数、查询条件、筛选规则或轻量表达式。对于固定格式并且语义很简单的文本，普通字符串处理可能更直接；对于需要变量、括号、操作符和字面量组合的场景，词法分析能让后续解析更清晰。

为领域语法添加关键字时，尽量用新的 `Lexer` 实例，不要直接修改全局 `Lexer.Instance`。全局实例适合作为默认通用词法器；领域语法通常应该拥有自己的 tokenizer 顺序，避免影响其他模块。

来自用户输入的表达式应限制语法范围，并在求值前做白名单校验。词法器可以告诉你文本被切成了哪些 token，但不会自动判断某个标识符、方法名、字段名或操作符是否允许暴露给外部用户。

{% hint style="warning" %}
不要把 tokenization 结果直接拼接成 SQL、脚本或命令文本。需要生成数据库查询或命令时，应在解析阶段构建明确的语法树或操作模型，再由对应驱动处理参数化和转义。
{% endhint %}

## 子命名空间

| 命名空间                                | 说明                                                        |
| ----------------------------------- | --------------------------------------------------------- |
| `Zongsoft.Expressions.Tokenization` | 内置 tokenizer、字面量 tokenizer 基类和关键字、字符串、数字、布尔、空值、标识符、符号分词器。 |

## 相关资源

* [Expressions 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Expressions)
* [Tokenization 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Expressions/Tokenization)
* [Lexer 测试](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Expressions/LexerTest.cs)
* [服务发现](https://docs.zongsoft.com/framework/core/pages/xdStvhSWKaWHymkFjbza#根据参数查找)
* [数据引擎条件与操作元](/framework/data/conditions-and-operands)


# Zongsoft.IO

以 Discussions 长正文与附件上传说明虚拟文件系统和路径。

Discussions 将短正文放在数据库，将较长正文保存为文件，并把文件路径存回数据模型。Zongsoft.IO 提供统一文件接口，实际本地磁盘或对象存储实现由部署决定。

## 基础路径来自配置

来源：[src/Zongsoft.Discussions.option](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.option#L1)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.option" %}

```xml
<?xml version="1.0" encoding="utf-8" ?>

<options>
	<option path="/Discussions">
		<general siteId="1" basePath="zfs.s3:/zongsoft-discussions/" />
	</option>
</options>
```

{% endcode %}

真实配置使用 zfs.s3 方案。运行需要相应文件系统实现和自己的存储配置；该路径不是文档读者可直接使用的公共空间。Utility.GetFilePath 再根据站点、用户和相对路径组织目录。

## 用同一入口写入文本

来源：[src/Utility.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Utility.cs#L241)（节选；上下文见源文件）。

{% code title="Utility.cs" %}

```csharp
public static bool WriteTextFile(string path, string content)
{
	if(string.IsNullOrWhiteSpace(path))
		throw new ArgumentNullException(nameof(path));

	if(string.IsNullOrWhiteSpace(content))
		return false;

	using(var stream = FileSystem.File.Open(path, FileMode.Create, FileAccess.Write))
	{
		using(var writer = new StreamWriter(stream, System.Text.Encoding.UTF8))
		{
			writer.Write(content);
		}
	}

	return true;
}
```

{% endcode %}

FileSystem.File 根据路径选择实现；流和文本写入器按作用域释放。数据库事务无法回滚这里已经产生的文件，调用业务需要处理失败补偿。

## 内容与路径的区别

来源：[src/Utility.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Utility.cs#L48)（节选；上下文见源文件）。

{% code title="Utility.cs" %}

```csharp
public static bool IsContentEmbedded(string contentType)
{
	if(string.IsNullOrEmpty(contentType))
		return true;

	return contentType.TrimEnd().EndsWith(CONTENT_TYPE_EMBEDDED_SUFFIX, StringComparison.OrdinalIgnoreCase);
}
```

{% endcode %}

空类型或带 +embedded 后缀表示内容字段保存正文；其他类型表示外置文件。这个标记是 Discussions 的业务约定，不是通用 MIME 传输规则。读取外置文件后应同步标记为内嵌，避免下游再次把正文当路径。

## HTTP 附件上传

来源：[src/api/Controllers/FileController.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/api/Controllers/FileController.cs#L79)（节选；上下文见源文件）。

{% code title="FileController.cs" %}

```csharp
var infos = this.Accessor.Write(this.Request,
							  this.DataService.GetDirectory(id),
							  args => args.FileName = $"{Timestamp.Millennium.Epoch.GetElapsed().Days}-{Randomizer.GenerateString()}", cancellation);
```

{% endcode %}

上传目录由 FileService 计算，文件名使用纪元天数和随机字符串。控制器随后把文件信息转换成业务 File 模型，写入失败时清理刚保存的文件。原始客户端文件名与服务端存储名不同，展示与下载应按业务模型解释。

## 运行与故障检查

文件不存在或读取失败时，当前 Utility.ReadTextFile 返回空字符串。排障时要分别检查数据中保存的路径、文件系统插件、存储访问权限与文件是否存在，不能把空正文一律归因于数据库。

为每个隔离环境设置独立目录；站点目录是存储组织方式，不能替代接口权限。有关文件模型、内容标记和外部存储来源，见[真实案例](/cases)及 [Amazon 扩展](/framework/externals/projects/amazon)。


# Zongsoft.Messaging

Zongsoft.Messaging 的消息队列抽象、确认模型、配置发现和守护订阅机制。

`Zongsoft.Messaging` 是核心库中的消息队列抽象层。它不绑定 Kafka、RabbitMQ、MQTT 或 ZeroMQ 的某一种协议，而是把应用侧真正需要稳定依赖的概念抽出来：生产消息、订阅主题、处理消息、确认消费、根据配置创建队列，以及在宿主中守护一组订阅。

具体消息队列由 [消息队列插件](/framework/messaging) 提供。业务模块应优先依赖核心抽象，只有在需要配置驱动专属参数、直接构造驱动队列或使用 ZeroMQ 附加通信能力时，才引用具体实现包。

## 设计定位

消息队列抽象的核心理念是“统一入口，保留语义边界”：

* 统一入口：发布方通过 `IMessageProducer` 发送字节或文本消息；订阅方通过 `IMessageQueue` 订阅主题并获得 `IMessageConsumer`。
* 统一消息：所有实现最终交给处理器的是 `Message`，包含主题、标签、消息体、标识符、发送者身份、时间戳和确认回调。
* 统一配置：插件把连接设置驱动注册到 `/Workbench/Configuration/ConnectionSettings/Drivers`，队列提供器再从 `/Messaging/ConnectionSettings` 读取连接项。
* 保留边界：`MessageReliability`、标签、延迟、过期、优先级等选项是框架意图，不代表每个底层产品都支持同样语义。核心层会检查可靠性上限、正数延迟和非空压缩设置；不支持时拒绝调用。其他选项的具体映射仍需核对驱动。

```mermaid
flowchart LR
	Producer["IMessageProducer"] --> Queue["IMessageQueue"]
	Queue --> Consumer["IMessageConsumer"]
	Consumer --> Handler["IHandler<Message>"]
	Queue --> Provider["IMessageQueueProvider"]
	Factory["IMessageQueueFactory"] --> Queue
	Guarder["MessageQueueGuarder"] --> Queue
	Provider --> Settings["/Messaging/ConnectionSettings"]
	Guarder --> Options["/Messaging/Queues"]
```

## 主要类型

| 类型                              | 说明                                                                                                                     |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `Message`                       | 消息结构，承载主题、标签、数据、标识符、身份、时间戳和确认回调。                                                                                       |
| `IMessageProducer`              | 生产者接口，提供按默认主题、指定主题和指定标签发送字节或文本消息的方法。                                                                                   |
| `IMessageQueue`                 | 队列接口，继承 `IMessageProducer`，增加队列名称、释放状态和订阅能力。                                                                           |
| `IMessageConsumer`              | 消费者接口，表示一次订阅；继承 [通讯通道](/framework/core/communication/general) 的关闭模型，可取消订阅。                                             |
| `IMessageQueueFactory`          | 队列工厂，根据连接设置或连接字符串创建队列实例。                                                                                               |
| `IMessageQueueProvider`         | 队列提供器，按名称从应用配置中发现并复用队列实例。                                                                                              |
| `MessageQueueBase<TSubscriber>` | 队列基类，实现发送和订阅重载、默认主题解析、订阅集合和释放流程。                                                                                       |
| `MessageConsumerBase<TQueue>`   | 消费者基类，把订阅生命周期纳入 [Channel](/framework/core/communication/general) 关闭模型。                                                 |
| `MessageQueueGuarder`           | 宿主[工作器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)，根据配置启动一组订阅，并在停止时取消订阅。 |

处理器使用 [`IHandler<T>`](/framework/core/components/handler) 承接消息处理逻辑。这样订阅回调可以是简单委托，也可以是可复用、可注入、可组合的处理器对象。

## Message 模型

`Message` 是一个轻量结构，重点不是描述所有中间件字段，而是提供跨实现都能表达的最小消息单元：

| 属性           | 说明                                                           |
| ------------ | ------------------------------------------------------------ |
| `Topic`      | 主题名。实现可能会对主题做转换，例如 RabbitMQ 把 `/` 转成 `.`，ZeroMQ 会把分组前缀拼到主题前。 |
| `Tags`       | 标签文本。核心层支持传递标签，但具体实现是否用于过滤取决于驱动。                             |
| `Data`       | 消息体字节数组。文本发送重载会使用指定编码，默认使用 UTF-8。                            |
| `Identifier` | 消息标识符，通常由底层实现返回或填充。                                          |
| `Identity`   | 消息发送者或客户端身份，只有部分实现会设置。                                       |
| `Timestamp`  | 消息时间戳；新建消息默认使用 UTC 时间，部分实现会使用底层消息时间。                         |
| `IsEmpty`    | 数据为空时为真；轮询器用它表示未取到有效消息。                                      |

`Message.Acknowledge()` 和 `Message.AcknowledgeAsync()` 调用消息内携带的确认回调。确认的具体效果由实现决定：Kafka 提交 offset，RabbitMQ 发送 `BasicAck`，MQTT 调用 MQTTnet 的确认方法，ZeroMQ 的 `LeastOnce` 通过 Control 通道确认，`MostOnce` 没有可靠确认。Kafka 当前配置未关闭客户端自动提交，显式 Commit 不保证未确认消息必然重投。

{% hint style="warning" %}
不要把“收到消息”和“消息已经被中间件确认”混为一谈。订阅处理器如果需要至少一次处理语义，应在业务处理成功后再调用 `AcknowledgeAsync()`；如果处理器从不确认，Kafka、RabbitMQ、MQTT 等实现可能按各自协议保留未确认状态或重新投递。
{% endhint %}

## 生产消息

生产接口提供字节和文本两组重载。调用方可以只传消息体，让队列从连接设置中的 `Topic` 取默认主题；也可以显式传入主题和标签。

Discussions 的站内信保存在数据库中，没有直接使用中间件队列。本节使用框架现有的能力边界测试；其中 TestQueue 是同一测试文件里的内存夹具，不会连接 Broker。发送消息的完整客户端见 [Kafka 项目](/framework/messaging/projects/kafka)。

来源：[framework/Zongsoft.Core/test/Messaging/MessageQueueBaseTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Messaging/MessageQueueBaseTest.cs#L59)（节选；上下文见源文件）。

{% code title="MessageQueueBaseTest.cs" %}

```csharp
public async Task UnsupportedDelayFailsBeforeDriverOperation()
{
	using var queue = new TestQueue();
	var options = new MessageEnqueueOptions(TimeSpan.FromSeconds(1));

	var exception = await Assert.ThrowsAsync<OperationException>(() => queue.ProduceAsync("tests/delay", ReadOnlyMemory<byte>.Empty, options).AsTask());

	Assert.Equal(nameof(OperationException.Unsupported), exception.Reason);
	Assert.Contains(MessageQueueFeature.Delay.Name, exception.Message);
	Assert.Equal(0, queue.ProduceCount);
}
```

{% endcode %}

该测试请求一秒延迟，但队列没有声明 Delay 能力，因此在调用驱动前失败，ProduceCount 保持零。支持能力的队列才会收到相应选项。

`MessageEnqueueOptions` 表达发布意图：

| 选项            | 说明                                                  |
| ------------- | --------------------------------------------------- |
| `Delay`       | 延迟投递。正值要求队列声明 Delay 能力，否则在进入驱动前拒绝。                  |
| `Expiration`  | 消息有效期。RabbitMQ、MQTT 5、ZeroMQ 可靠通道等分别实现自己的过期语义。      |
| `Priority`    | 优先级。RabbitMQ 会写入消息优先级。                              |
| `Reliability` | 不得超过驱动声明的上限；MQTT 映射为 QoS，ZeroMQ 区分广播与可靠通道。          |
| Properties    | 扩展属性。RabbitMQ 写入 headers，MQTT 5 写入 user properties。 |
| Compression   | 算法与字节阈值；非空设置要求队列声明 Compression 能力。                  |

## 订阅消息

`IMessageQueue.SubscribeAsync(...)` 支持三类订阅方式：

* 不指定主题：使用连接设置的默认 `Topic`；如果也没有默认主题，则由具体实现决定空主题含义。
* 指定主题：订阅一个明确主题或模式。
* 指定主题和标签：传入标签过滤意图，是否生效取决于实现。

来源：[framework/Zongsoft.Core/test/Messaging/MessageQueueBaseTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Messaging/MessageQueueBaseTest.cs#L283)（节选；上下文见源文件）。

{% code title="MessageQueueBaseTest.cs" %}

```csharp
public async Task ConflictingSubscriptionDoesNotReplaceExistingConsumer()
{
	using var queue = new TestQueue();
	var handler = new TestHandler();
	var first = await queue.SubscribeAsync("tests/conflict", "alpha,beta", handler, new MessageSubscribeOptions(MessageReliability.MostOnce));

	await Assert.ThrowsAsync<InvalidOperationException>(() => queue.SubscribeAsync("tests/conflict", "alpha,beta", new TestHandler(), new MessageSubscribeOptions(MessageReliability.MostOnce)).AsTask());
	await Assert.ThrowsAsync<InvalidOperationException>(() => queue.SubscribeAsync("tests/conflict", "alpha", handler, new MessageSubscribeOptions(MessageReliability.MostOnce)).AsTask());
	await Assert.ThrowsAsync<InvalidOperationException>(() => queue.SubscribeAsync("tests/conflict", "alpha,beta", handler, new MessageSubscribeOptions(MessageReliability.LeastOnce)).AsTask());

	Assert.Same(first, queue.Subscribers["tests/conflict"]);
	Assert.Equal(1, queue.CreateCount);
	Assert.Equal(1, queue.SubscribeCount);
}
```

{% endcode %}

上面的测试固定主题 tests/conflict，然后分别改变处理器、标签与可靠性；这些变化都会被视为不兼容订阅。实际异步处理器见[消息处理器示例](/framework/messaging)。异步处理必须使用 `IHandler<Message>`，不要把异步 lambda 传给同步委托重载。

`IMessageConsumer` 是一次订阅的句柄。下面的测试释放首个消费者后重新订阅，确认旧项被移除，并产生新的订阅者：

来源：[framework/Zongsoft.Core/test/Messaging/MessageQueueBaseTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Messaging/MessageQueueBaseTest.cs#L260)（节选；上下文见源文件）。

{% code title="MessageQueueBaseTest.cs" %}

```csharp
public async Task ActiveConsumerCloseRemovesEntryAndAllowsResubscribe()
{
	using var queue = new TestQueue();
	var first = await queue.SubscribeAsync("tests/resubscribe", new TestHandler());

	Assert.NotNull(first);
	Assert.Single(queue.Subscribers);

	await first.DisposeAsync();

	Assert.Empty(queue.Subscribers);
	Assert.Equal(1, queue.UnsubscribedCount);

	var second = await queue.SubscribeAsync("tests/resubscribe", new TestHandler());

	Assert.NotNull(second);
	Assert.NotSame(first, second);
	Assert.Single(queue.Subscribers);
	Assert.Equal(2, queue.CreateCount);
	Assert.Equal(2, queue.SubscribeCount);
}
```

{% endcode %}

`MessageQueueBase<TSubscriber>` 以主题为键保存订阅者。只有同一主题的处理器、标签及规范化选项一致时才会复用初始化任务和订阅者；不兼容的重复订阅会抛出冲突异常。初始化失败的项不会作为活动消费者暴露。需要多个处理器时，应明确设计分发或使用独立订阅范围。

## 可靠性和失败退避

`MessageReliability` 有三个值：

| 值             | 含义                                  |
| ------------- | ----------------------------------- |
| `MostOnce`    | 最多一次。倾向于减少重复，允许消息丢失。                |
| `LeastOnce`   | 至少一次。倾向于不丢消息，但可能重复投递。               |
| `ExactlyOnce` | 精确一次。表达最强语义，但是否可达取决于底层产品、配置和业务幂等设计。 |

`MessageFallbackBehavior` 用于描述订阅回调失败后的退避策略，目前核心层只是把该意图保存在 `MessageSubscribeOptions` 中，具体实现尚未统一执行重试流程。编写处理器时仍应把幂等、异常记录、死信或补偿逻辑放在业务侧或底层中间件配置中。

{% hint style="info" %}
“精确一次”不能只靠框架枚举保证。生产端事务、消费端确认、业务幂等键、去重存储和中间件能力都要同时成立，才能接近精确一次处理效果。
{% endhint %}

## 配置和发现

消息队列连接配置位于 `/Messaging/ConnectionSettings`。每个连接项通过 `driver` 选择具体实现，通过 `connectionSetting.name` 作为队列名称。

来源：[framework/messaging/kafka/src/Zongsoft.Messaging.Kafka.option](https://github.com/Zongsoft/framework/blob/main/messaging/kafka/src/Zongsoft.Messaging.Kafka.option#L3)（节选；上下文见源文件）。

{% code title="Zongsoft.Messaging.Kafka.option" %}

```xml
<options>
	<option path="/Messaging">
		<connectionSettings>
			<connectionSetting connectionSetting.name="kafka" driver="kafka"
			                   value="server=127.0.0.1;username=program;password=xxxxxx;client=client1;group=group1" />
		</connectionSettings>
	</option>
</options>
```

{% endcode %}

上面的配置是 Kafka 插件附带的本地范例，xxxxxx 是占位密码，运行前应按测试 Broker 修改。`IMessageQueueProvider` 负责读取该路径。提供器按驱动名称匹配连接项，创建出的队列以弱引用缓存；当队列已释放或被回收后，下次访问会重新创建。

来源：[framework/Zongsoft.Core/src/Messaging/MessageQueueUtility.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Messaging/MessageQueueUtility.cs#L40)（节选；上下文见源文件）。

{% code title="MessageQueueUtility.cs" %}

```csharp
public static IMessageQueue Queue(IServiceProvider services, string name, IEnumerable<KeyValuePair<string, string>> settings = null)
{
	name ??= string.Empty;
	services ??= ApplicationContext.Current?.Services ?? throw new ArgumentNullException(nameof(services));

	foreach(var provider in services.ResolveAll<IMessageQueueProvider>())
	{
		if(provider.Exists(name))
			return provider.Queue(name, settings);
	}

	return null;
}
```

{% endcode %}

如果同名连接项可能被多个提供器识别，或希望在配置中明确指定驱动，可使用 `MessageQueueConverter` 支持的 `队列名@提供器名` 形式：

转换语法为“队列名@提供器名”。队列名必须来自真实连接项，提供器名来自驱动注册；可对照上面 Kafka.option 的 kafka 连接及 [MessageQueueConverter](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Messaging/MessageQueueConverter.cs) 核对。

## 守护订阅

`MessageQueueGuarder` 是一个 [工作器](/framework/core/components/worker)，适合在宿主启动时自动订阅一组主题。它有三个关键属性：

| 属性        | 说明                                 |
| --------- | ---------------------------------- |
| `Queue`   | 要守护的消息队列，可通过字符串转换器按队列名和提供器名解析。     |
| `Handler` | 消息处理器。                             |
| `Options` | 队列订阅配置；为空时从 `Messaging/Queues` 读取。 |

订阅配置位于 `Messaging/Queues`，用于声明队列名称、订阅可靠性、失败退避和主题过滤器：

Discussions 当前没有守护订阅配置。可从 [QueueOptions](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Messaging/Options/QueueOptions.cs)、[QueueSubscriptionOptions](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Messaging/Options/QueueSubscriptionOptions.cs) 阅读实际配置模型；守护器按 Queue.Name 匹配集合中的项，再逐条订阅 Filters。只安装驱动不会自动产生业务处理器。

过滤器也可以通过启动参数补充，格式由 `QueueSubscriptionFilter.Parse(...)` 解析，支持 `Topic`、`Topic:TagA,TagB`、`Topic?TagA,TagB` 等写法。

## 实现扩展

新增一个消息队列实现通常包括四部分：

1. 连接设置类型：实现 `IMessageQueueSettings`，继承连接设置基类，并声明 `Server`、`Client`、`Group`、`Topic`、`Timeout` 等通用属性及驱动专属属性。
2. 队列类型：继承 `MessageQueueBase<TSubscriber, TSettings>`，实现 `OnProduceAsync(...)`、`CreateSubscriberAsync(...)` 和 `OnSubscribeAsync(...)`。
3. 消费者类型：继承 `MessageConsumerBase<TQueue>`，把底层消息转换成 `Message`，并在确认回调中执行底层 ACK、commit 或确认操作。
4. 工厂和提供器：实现或继承 `MessageQueueFactoryBase`、`MessageQueueProviderBase<TQueue, TSettings>`，并通过服务特性注册。

{% hint style="warning" %}
实现者应明确写出哪些 `MessageEnqueueOptions`、`MessageSubscribeOptions` 和标签语义被支持，哪些会被忽略。统一接口是为了降低业务依赖成本，不是为了掩盖底层消息系统的真实能力。
{% endhint %}

## 相关资源

* [事件通道](/framework/core/components/events)
* [消息队列插件](/framework/messaging)
* [连接配置](/framework/data/connections)
* [插件文件与加载](/framework/plugins/plugin-file)
* [Messaging 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Messaging)

## 消息存储与负载所有权

`IMessageStorage` 是独立于传输驱动的持久消息契约。存储保存消息元数据和负载快照，不持久化确认委托；恢复后的确认行为由 Broker 重新建立。配置及身份迁移见[可靠消息存储](/framework/messaging/reliability)。

`Message.Data` 当前仍是字节数组。框架 `IsEmpty` 将空数组也视为空消息，但部分协议允许合法空业务负载；驱动适配不能简单把所有空载荷丢弃为无消息。延迟执行或持久化时应按实现契约快照借用数据，避免调用者修改数组影响已接受消息。


# Zongsoft.Reflection

Zongsoft.Reflection 的反射访问、动态访问器和成员表达式求值能力。

`Zongsoft.Reflection` 为运行时对象访问提供一组轻量反射工具。它把常见的字段、属性、索引器和成员路径访问封装成可复用的动态访问器模型，适合配置绑定、数据映射、模板求值、命令参数解析、插件扩展和其他“成员名称在运行时才确定”的场景。

反射访问本身不应替代普通的强类型代码：当成员在编译期已经确定时，直接调用仍然是最清晰的写法；当字段名、属性名或访问路径来自配置、脚本、用户输入或元数据时，再使用本命名空间提供的能力。

## 主要职责

* 通过 `Reflector` 统一读取和写入字段、属性以及索引器。
* 通过 [`System.Reflection.Emit`](https://learn.microsoft.com/zh-cn/dotnet/api/system.reflection.emit) 为 System.Reflection.FieldInfo 和 System.Reflection.PropertyInfo 动态编译并缓存 getter、setter 访问器，减少重复反射调用的成本。
* 支持按字符串成员名读取或写入对象成员，名称查找忽略大小写，并可访问默认成员。
* 解析成员表达式，将 `Address.City`、`Items[0].Name`、`Get("key")` 这类路径转换为表达式节点链。
* 通过 `MemberExpressionEvaluator` 对表达式进行求值或设置最终成员。
* 为模型映射、数据绑定、配置绑定、序列化、报表字段选择等模块提供底层成员访问能力。

## 核心类型

| 类型                          | 作用            | 常见用法                                                           |
| --------------------------- | ------------- | -------------------------------------------------------------- |
| `Reflector`                 | 高层反射访问入口。     | 按 System.Reflection.MemberInfo 或字符串成员名读取、写入字段和属性。              |
| `FieldInfoExtension`        | 字段访问器扩展。      | 从 System.Reflection.FieldInfo 获取缓存的 getter、setter。             |
| `PropertyInfoExtension`     | 属性访问器扩展。      | 从 System.Reflection.PropertyInfo 获取缓存的 getter、setter，并支持索引器参数。 |
| `MemberExpression`          | 成员表达式基类和解析入口。 | 解析或手动构造成员路径表达式。                                                |
| `IMemberExpression`         | 表达式节点接口。      | 表示表达式链中的一个节点，并通过 `Previous`、`Next` 串联。                         |
| `IdentifierExpression`      | 标识符节点。        | 表示普通字段或属性名。                                                    |
| `IndexerExpression`         | 索引器节点。        | 表示 `[]` 访问，并携带索引参数。                                            |
| `MethodExpression`          | 方法节点。         | 表示方法调用和参数列表。                                                   |
| `ConstantExpression`        | 常量节点。         | 表示字符串、数字和空值等参数值。                                               |
| `MemberExpressionEvaluator` | 表达式求值器。       | 对目标对象读取路径值，或写入路径末端成员。                                          |

## 成员访问

Discussions 没有直接调用 Reflector；它通过映射与配置间接使用这些机制。本页从 [核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core) 的现有测试读取 API 形状，其中 MyValue、ClassEntity 都是测试夹具类型。

`Reflector` 是最常用的入口。它既可以接收已经解析好的 System.Reflection.MemberInfo，也可以直接按成员名称查找对象的公开字段或属性。

来源：[framework/Zongsoft.Core/test/Reflection/ReflectorTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Reflection/ReflectorTest.cs#L14)（节选；上下文见源文件）。

{% code title="ReflectorTest.cs" %}

```csharp
var target = new MyValue(5);

Assert.Equal(100, (int)Reflector.GetValue(ref target, nameof(MyValue.IntegerField)));
Assert.Equal(123.05m, (decimal)Reflector.GetValue(ref target, nameof(MyValue.DecimalProperty)));
Assert.Equal("StringField", (string)Reflector.GetValue(ref target, nameof(MyValue.StringField)));
Assert.Equal("StringProperty", (string)Reflector.GetValue(ref target, nameof(MyValue.StringProperty)));
Assert.Null(Reflector.GetValue(ref target, nameof(MyValue.NullableField)));
Assert.Null(Reflector.GetValue(ref target, nameof(MyValue.NullableProperty)));

Assert.Equal("#0", Reflector.GetValue(ref target, "Item", 0));
Assert.Equal("#1", Reflector.GetValue(ref target, "Item", 1));
Assert.Equal("#2", Reflector.GetValue(ref target, "Item", 2));
Assert.Equal("#3", Reflector.GetValue(ref target, "Item", 3));
Assert.Equal("#4", Reflector.GetValue(ref target, "Item", 4));

Reflector.SetValue(ref target, nameof(MyValue.IntegerField), 200);
Reflector.SetValue(ref target, nameof(MyValue.DecimalProperty), 456.78);
Reflector.SetValue(ref target, nameof(MyValue.StringField), "NewStringField");
Reflector.SetValue(ref target, nameof(MyValue.StringProperty), "NewStringProperty");

Assert.Equal(200, (int)Reflector.GetValue(ref target, nameof(MyValue.IntegerField)));
Assert.Equal(456.78m, (decimal)Reflector.GetValue(ref target, nameof(MyValue.DecimalProperty)));
Assert.Equal("NewStringField", (string)Reflector.GetValue(ref target, nameof(MyValue.StringField)));
Assert.Equal("NewStringProperty", (string)Reflector.GetValue(ref target, nameof(MyValue.StringProperty)));
```

{% endcode %}

`GetValue`、`SetValue` 会在目标为空、成员不存在、属性不可读等情况下抛出异常；写入只会对可写属性和非只读字段生效，遇到只读字段或不可写属性时会返回 `false`。`TryGetValue`、`TrySetValue` 更适合处理外部输入、可选字段或兼容旧模型的场景。

如果调用方已经持有 System.Reflection.FieldInfo、System.Reflection.PropertyInfo 或 System.Reflection.MemberInfo，可以直接使用扩展方法。GetGetter/GetSetter 会复用缓存。下面的测试则显式调用 GenerateGetter/GenerateSetter，以验证动态访问器生成与读写结果；不要在热循环中反复 Generate。

来源：[framework/Zongsoft.Core/test/Reflection/PropertyInfoTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Reflection/PropertyInfoTest.cs#L13)（节选；上下文见源文件）。

{% code title="PropertyInfoTest.cs" %}

```csharp
var entity = new ClassEntity()
{
	Id = 100,
	Name = "Popeye",
};

//init the target variable
var target = (object)entity;

//test the instance property(get and set)
var property = typeof(ClassEntity).GetProperty(nameof(ClassEntity.Id));
var getter = property.GenerateGetter();
var setter = property.GenerateSetter();

Assert.NotNull(getter);
Assert.NotNull(setter);

var value = getter(ref target);
Assert.Equal(100, value);

setter(ref target, 200);
value = getter(ref target);
Assert.Equal(200, value);
```

{% endcode %}

## 动态访问器与性能

字段和属性扩展方法内部会基于 [`System.Reflection.Emit`](https://learn.microsoft.com/zh-cn/dotnet/api/system.reflection.emit) 生成动态方法，再把动态方法编译成 getter 或 setter 委托。第一次访问某个成员时需要完成动态方法生成、IL 发射和委托创建；生成完成后，访问器会缓存在成员信息对应的缓存项中，后续读取或写入同一成员时直接调用委托，避免反复走传统反射的 `GetValue`、`SetValue` 路径。

这种设计适合高频、重复的运行时成员访问：例如数据映射持续填充模型、配置绑定反复写入属性、模板或报表按字段名读取对象。它不会让一次性的成员查找变成强类型调用，也不能消除按名称查找成员、解析表达式和参数转换的成本；因此在循环或批处理场景中，应尽量复用已经取得的 System.Reflection.FieldInfo、System.Reflection.PropertyInfo 或解析后的表达式对象。

源码仓库的 `Zongsoft.Core/benchmark/Reflection` 目录提供了属性读写的 BenchmarkDotNet 基准测试。测试以普通反射的 `PropertyInfo.GetValue`、`PropertyInfo.SetValue` 为基线，分别比较 `Reflector` 封装访问和直接复用 `GetGetter<T>`、`GetSetter<T>` 委托的路径；实际收益取决于运行时、成员形状和调用次数，应在目标环境测量，不能仅凭存在基准项目就推断具体倍数。

{% hint style="info" %}
动态访问器的收益来自“生成一次，多次调用”。如果某个成员只访问一次，生成访问器本身也会产生少量开销；如果成员会被反复访问，缓存委托通常比每次使用反射调用更稳定。
{% endhint %}

{% hint style="info" %}
字符串成员名查找默认面向公开实例成员和公开静态成员，并忽略大小写。若成员名称为空，则尝试访问目标类型的默认成员，常用于索引器访问。
{% endhint %}

## 索引器访问

属性访问器支持索引参数，因此既可以通过具体的 System.Reflection.PropertyInfo 调用，也可以通过默认成员访问索引器。

ReflectorTest.TestValueType 已在上面的片段中读取 Item 索引器的 0 至 4 项。MyValue 的索引器与字段定义保存在同一测试文件中，复用时应保留该夹具；索引越界和参数类型错误仍由目标成员决定。

这类写法适合处理集合、字典、动态模型或带默认成员的对象。对于字典、集合这样的强类型代码路径，如果键和索引在编译期已知，直接访问集合通常更直观。

## 成员表达式

`Zongsoft.Reflection.Expressions` 子命名空间提供成员路径解析和求值能力。表达式会被解析为一条双向节点链，每个节点代表一次成员、方法或索引器访问。

来源：[framework/Zongsoft.Plugins/src/Configuration/OptionParser.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins/src/Configuration/OptionParser.cs#L40)（节选；上下文见源文件）。

{% code title="OptionParser.cs" %}

```csharp
public override object Parse(ParserContext context)
{
	if(string.IsNullOrWhiteSpace(context.Text))
		return null;

	var expression = Collections.HierarchicalExpression.Parse(context.Text);

	if(expression != null)
	{
		object target = ApplicationContext.Current.Configuration.GetOption(context.MemberType, expression.Path);

		if(target != null && expression.Accessor != null)
			return Reflection.Expressions.MemberExpressionEvaluator.Default.GetValue(expression.Accessor, target);
		else
			return target;
	}

	return null;
}
```

{% endcode %}

上面是插件 option 表达式的实际求值入口：先按配置路径取得对象，有附加成员路径时才交给 MemberExpressionEvaluator。后面的表格是语法形式参考，不表示 Discussions 模型具有这些属性。

常见表达式形态如下：

| 表达式                   | 含义                    |
| --------------------- | --------------------- |
| `Name`                | 读取目标对象的 `Name` 字段或属性。 |
| `Address.City`        | 逐级读取嵌套成员。             |
| `Items[0]`            | 访问默认索引器的第一个元素。        |
| `Items[0].Name`       | 先取集合元素，再读取元素成员。       |
| `Options["theme"]`    | 使用字符串常量作为索引器参数。       |
| `GetValue("name")`    | 调用公开方法，并传入字符串常量参数。    |
| `GetItem(Index).Name` | 方法或索引器参数也可以是另一个成员表达式。 |

表达式参数支持单引号或双引号字符串、数字常量和成员表达式。数字常量从数字开始，可使用 `L`、`F`、`M` 后缀表示长整型、单精度或十进制数；包含小数点的数字会按双精度解析。

{% hint style="warning" %}
成员表达式不是空值传播表达式。求值过程中如果中间对象为 `null`，后续成员查找通常会失败；需要容错时，应在调用前准备默认对象，或通过 `MemberExpressionEvaluator` 的求值回调接管某些节点的值。
{% endhint %}

## 自定义求值

`MemberExpressionEvaluator` 的读取和写入方法都接受求值回调。回调会在每个节点解析到成员后执行，调用方可以检查当前节点、目标对象、成员信息和参数，也可以提前设置节点值，从而覆盖默认反射求值逻辑。

框架 [Criteria.Transform](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Data/Criteria.cs) 是实际的自定义写入调用点：它将查询成员名解析为路径，再按目标成员类型把文本转成布尔值、数组、集合或标量，通过 valueFactory 返回最终写入值。处理嵌套成员时，还用 evaluate 回调准备中间对象。完整流程见[条件与操作元](/framework/data/conditions-and-operands)。

这适合实现默认值补齐、权限过滤、外部字典取值、惰性加载或对某些路径节点进行特殊映射。回调只应处理确实需要介入的节点；普通成员访问交给默认求值器即可。

## 使用建议

优先把反射访问限制在系统边界，例如配置、插件、映射、绑定、序列化、报表字段和命令参数处理。业务核心流程如果可以用接口、泛型或普通属性访问表达，就不必引入成员名字符串。

重复执行的路径建议先解析并缓存 `IMemberExpression`，不要在循环中反复调用 `MemberExpression.Parse`。同样地，已经拿到 System.Reflection.FieldInfo 或 System.Reflection.PropertyInfo 时，优先复用它们的动态访问器，而不是每次都按字符串名称查找。

来自用户输入或外部配置的成员路径应先做白名单校验。反射工具可以降低调用成本，但不会自动判断某个成员是否适合暴露给外部使用；暴露字段路径、方法调用或索引器访问时，应由调用方定义清晰的可访问范围。

{% hint style="warning" %}
按名称查找的成员访问只覆盖公开字段、公开属性和公开方法。若需要访问非公开成员，应由调用方显式取得对应的 System.Reflection.MemberInfo，并确认这不会破坏封装或安全边界。
{% endhint %}

## 子命名空间

| 命名空间                              | 说明                        |
| --------------------------------- | ------------------------- |
| `Zongsoft.Reflection.Expressions` | 成员路径表达式解析、表达式节点模型和表达式求值器。 |

## 相关资源

* [Reflection 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Reflection)
* [Expressions 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Reflection/Expressions)
* [属性读取性能基准](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/benchmark/Reflection/PropertyGetterBenchmark.cs)
* [属性写入性能基准](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/benchmark/Reflection/PropertySetterBenchmark.cs)


# Zongsoft.Resources

以 Discussions 的资源键和框架现有测试说明本地化资源、定位回退与组件标题。

Resources 为程序集资源提供统一访问入口。一个资源键可以保持稳定，而展示文本随文化设置变化；类型和成员的位置则帮助框架确定去哪个资源集查找。它常用于组件标题、枚举描述、命令帮助和错误提示。

## Discussions 中的资源组织

Discussions 的 [Properties/Resources.resx](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Properties/Resources.resx) 已经保存论坛领域文本，无需再创建假想的 UserCategory 或安全模块资源集。

| 真实资源键                   | 中文文本 | 用途        |
| ----------------------- | ---- | --------- |
| Accessibility           | 可访问性 | 访问范围的显示名称 |
| Accessibility.Internal  | 内部人员 | 枚举值文本     |
| Accessibility.Moderator | 版主   | 枚举值文本     |
| Accessibility.Specified | 限定人员 | 枚举值文本     |
| Approved                | 已审核  | 审核状态的显示名称 |
| Forum                   | 论坛   | 领域对象名称    |
| ForumId                 | 论坛编号 | 字段名称      |

Resources.Designer.cs 是对应的生成访问器。调整文本应编辑 resx，再使用项目的资源生成流程更新访问器；直接修改生成文件不能稳定保留变更。资源键存在只说明文本可供查找，并不表示每个前端都已使用它。

## 主要对象怎样协作

| 对象                            | 职责                     |
| ----------------------------- | ---------------------- |
| IResource                     | 按键读取字符串、对象，以及尝试读取的结果   |
| Resource                      | 扫描程序集内的资源集并组织读取        |
| IResourceLocator              | 把调用位置转换为资源集候选名称        |
| ResourceLocator               | 提供默认的逐级定位规则            |
| ResourceUtility 与 Resource 扩展 | 接收类型、成员和候选键，减少调用方的重复转换 |

资源集定位与资源键查找是两个步骤：先找到可能包含文本的资源集，再在其中查找键。位置通常来自类型的命名空间，不能把位置字符串当作资源键。

## 完整读取范例：枚举描述测试

Discussions 没有独立的资源读取测试，因此使用框架 ResourceTest。这里的 Gender 来自该测试项目，不是 Discussions.Models.Gender；两个项目各自拥有资源文件。

来源：[framework/Zongsoft.Core/test/Resources/ResourceTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Resources/ResourceTest.cs#L14)（节选；上下文见源文件）。

{% code title="ResourceTest.cs" %}

```csharp
public void Test()
{
	var resource = Resource.GetResource<Gender>();
	Assert.NotNull(resource);

	var text = resource.GetString("Gender.Male");
	Assert.NotNull(text);
	Assert.Equal("男士", text);

	text = resource.GetString("Gender.Female");
	Assert.NotNull(text);
	Assert.Equal("女士", text);

	text = EnumUtility.GetEnumDescription(Gender.Male);
	Assert.NotNull(text);
	Assert.Equal("男士", text);

	text = EnumUtility.GetEnumDescription(Gender.Female);
	Assert.NotNull(text);
	Assert.Equal("女士", text);
}
```

{% endcode %}

测试先直接读取 Gender.Male / Gender.Female，再经 EnumUtility 取得枚举说明。两条路径应得到同样文本。这说明界面可展示资源文本，业务仍保留稳定的枚举值。进一步用法见[枚举工具](/framework/core/common/enum-utility)。

## 定位器的真实回退规则

来源：[framework/Zongsoft.Core/src/Resources/ResourceLocator.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Resources/ResourceLocator.cs#L44)（节选；上下文见源文件）。

{% code title="ResourceLocator.cs" %}

```csharp
public IEnumerable<string> Locate(string origin)
{
	//如果资源管理器中没有资源集，则无需定位
	if(_resource.Count == 0)
		yield break;

	//如果资源管理器中只有一个资源集，则只能定位它
	if(_resource.Count == 1)
		yield return _resource.Resources.First().BaseName;

	if(!string.IsNullOrEmpty(origin))
	{
		foreach(var location in GetLocations(origin))
			yield return location;
	}

	foreach(var location in GetLocations($"{_resource.Assembly.GetName().Name}"))
		yield return location;
}
```

{% endcode %}

默认定位器首先处理没有资源集、只有一个资源集的情况，然后尝试来源位置以及程序集名称。GetLocations 会从完整位置逐级回退，每一级依次尝试 Properties.Resources、Resources 和位置本身。实际候选名称由程序集与调用类型决定，不需要手写另一个模块的资源树。

## 组件怎样选择候选键

来源：[framework/Zongsoft.Core/src/Collections/CategoryBase.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Collections/CategoryBase.cs#L130)（节选；上下文见源文件）。

{% code title="CategoryBase.cs" %}

```csharp
protected virtual string GetTitle() => Resources.ResourceUtility.GetString(_resource,
[
	$"{this.FullPath.Trim(PathSeparator).Replace(PathSeparator, '.')}.{nameof(Category)}.{nameof(this.Title)}",
	$"{this.FullPath.Trim(PathSeparator).Replace(PathSeparator, '.')}.{nameof(Category)}",
	$"{this.FullPath.Trim(PathSeparator).Replace(PathSeparator, '.')}.{nameof(this.Title)}",
	this.FullPath.Trim(PathSeparator).Replace(PathSeparator, '.'),
	$"{this.Name}.{nameof(Category)}.{nameof(this.Title)}",
	$"{this.Name}.{nameof(Category)}",
	$"{this.Name}.{nameof(this.Title)}",
	this.Name,
]);
```

{% endcode %}

Category 优先按完整分类路径查标题，再退回节点名称。这个规则允许同名节点在不同位置显示不同文本；也允许小型分类树只提供通用名称。完整分类树范例见[Category](/framework/core/collections/category)。

## 对象资源与自定义定位

GetObject 用于读取非字符串资源，实际类型由资源文件决定。当前 Discussions 没有通过此接口读取图标并渲染的完整用例，因此不提供依赖未实现 RenderIconAsync 的代码。需要展示资源对象时，调用者应先检查类型，再交给相应呈现组件。

项目若采用不同的资源集命名方式，可以实现 IResourceLocator，并把实例交给 Resource 构造函数。默认实现已经适用于 Discussions 的 Properties.Resources 组织，无需为本案例添加新定位器。

{% hint style="info" %}
💡 Resource.GetResource 按程序集缓存实例。需要专用定位器时，显式创建拥有该定位器的 Resource，或者确保首次缓存时使用正确配置；后续取得缓存对象不会自动替换定位规则。
{% endhint %}

## 缺失文本时怎样排查

先检查 resx 是否包含目标键，再检查编译产物是否带有对应资源集，最后核对当前文化和来源位置。区分“键未命中”与“命中的文本为空”；主流程可以接受缺失时保留回退文本，配置错误则应明确报告。框架实现见 [Resource.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Resources/Resource.cs)。


# Zongsoft.Scheduling

以框架 Hangfire 样例串联调度契约、处理器和宿主生命周期。

调度把“什么时候执行”与“执行什么”分开。[核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core) 提供调度公共契约，具体实现决定延迟、周期、持久化和失败处理方式。Discussions 没有现成调度业务，本页沿用框架的 MyHandler 范例。

## 从处理器开始

来源：[framework/externals/hangfire/samples/MyHandler.cs](https://github.com/Zongsoft/framework/blob/main/externals/hangfire/samples/MyHandler.cs#L11)（节选；上下文见源文件）。

{% code title="MyHandler.cs" %}

```csharp
public class MyHandler : HandlerBase<object>
{
	private long _count = 0;

	protected override ValueTask OnHandleAsync(object argument, Parameters parameters, CancellationToken cancellation) =>
		Logging.GetLogging(this).DebugAsync(
			"MyHandler handles the scheduling of the Hangfire.",
			new
			{
				Count = Interlocked.Increment(ref _count),
				Argument = argument,
				Parameters = parameters,
			},
			cancellation);
}
```

{% endcode %}

处理器接收业务参数、扩展 Parameters 和取消令牌。异步接口不意味着当前工作一定耗时或需要后台线程；这里主要是日志写入。

## 挂到调度器可发现的位置

来源：[framework/externals/hangfire/samples/Zongsoft.Externals.Hangfire.Samples.plugin](https://github.com/Zongsoft/framework/blob/main/externals/hangfire/samples/Zongsoft.Externals.Hangfire.Samples.plugin#L19)（节选；上下文见源文件）。

{% code title="Zongsoft.Externals.Hangfire.Samples.plugin" %}

```xml
<extension path="/Workbench/Scheduler/Handlers">
	<object name="MyHandler" type="Zongsoft.Externals.Hangfire.Samples.MyHandler, Zongsoft.Externals.Hangfire.Samples" />
</extension>
```

{% endcode %}

调度作业通过稳定名称关联处理器。重命名类型、处理器名或移动插件时，要考虑存储中已存在作业的引用，否则部署成功后旧作业仍可能无法执行。

## 调度方式与资源

| 方式    | 应核对的内容              |
| ----- | ------------------- |
| 延迟执行  | 时间基准、取消、到期时宿主是否运行   |
| 周期执行  | cron 语法、时区、错过触发时的策略 |
| 持久作业  | 存储可用性、重启恢复、参数兼容性    |
| 多实例执行 | 分配机制、重复执行与幂等        |

具体能力以实现为准。Hangfire 集成及存储插件见[项目说明](/framework/externals/projects/hangfire)，不能把 [核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core) 契约理解为自动具备全部持久化能力。

## 验证与停止

先构建并部署真实样例，再通过所选调度器注册 MyHandler，确认日志包含 Count、Argument 与 Parameters。这个计数在重启后归零，多 Worker 之间不共享；它不是作业完成的持久凭据。

停止时应停止新调度、处理取消并释放服务资源。失败作业可能重试，样例之外的业务动作需要自己的幂等与补偿策略。有关后台宿主见[daemon](/hosting/daemon)。


# Zongsoft.Security

Zongsoft.Core 中 Zongsoft.Security 命名空间的声明身份、凭证、密钥、验证码与安全基础模型。

`Zongsoft.Security` 是核心库中的安全基础层。它不直接等同于完整的安全业务模块，而是为上层的 `Zongsoft.Security` 插件、Web 认证、业务身份模型和权限系统提供可复用的抽象、工具和运行时对象。

核心库把安全能力拆成三类：

| 领域   | 主要类型                                                                                                                                                                                                                                                                                                                                                                                                                      | 用途                                      |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| 声明身份 | [`CredentialIdentity`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/CredentialIdentity.cs)、[`CredentialPrincipal`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/CredentialPrincipal.cs)、[`ClaimsIdentityModeling`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/ClaimsIdentityModeling.cs)                                      | 表示登录后的身份、凭证编号、续约令牌、场景和身份模型转换。           |
| 声明工具 | [`ClaimNames`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/ClaimNames.cs)、[`ClaimUtility`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/ClaimUtility.cs)、[`ClaimsIdentityExtension`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/ClaimsIdentityExtension.cs)                                                                  | 为标准 claims 增加命名空间、描述、授权、类型转换、角色和模型映射能力。 |
| 安全辅助 | [`Password`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Password.cs)、[`Secretor`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Secretor.cs)、[`Certificate`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Certificate.cs)、[`ICaptcha`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/ICaptcha.cs) | 处理密码摘要、一次性秘密、证书、签名、人机识别和挑战扩展。           |

权限、认证器、授权器、用户、角色和成员模型位于 [`Zongsoft.Security.Privileges`](/framework/core/security/privileges)。如果只想了解权限树、角色继承和授权计算，请直接阅读该页。

## 设计边界

核心安全命名空间只定义“如何表达身份与安全材料”，不规定“用户表怎么设计”或“登录接口怎么写”。默认数据库、服务实现和插件挂载位于 [`Zongsoft.Security`](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Security) 项目；业务系统可以继承核心抽象，替换模型、查询条件、claims 补充逻辑和权限存储字段。

这种分层的好处是：

* 身份载体基于标准 [`ClaimsIdentity`](https://learn.microsoft.com/zh-cn/dotnet/api/system.security.claims.claimsidentity) [*(源码)*](https://source.dot.net/#System.Security.Claims/ClaimsIdentity.cs) 和 [`ClaimsPrincipal`](https://learn.microsoft.com/zh-cn/dotnet/api/system.security.claims.claimsprincipal) [*(源码)*](https://source.dot.net/#System.Security.Claims/ClaimsPrincipal.cs)，可以接入 ASP.NET Core、令牌认证和宿主上下文。
* 业务身份可以通过 *转换器* 从 Claims 还原成强类型模型，调用方不必到处解析字符串。
* 凭证、验证码、密码、证书等安全材料以接口暴露，默认实现可替换，业务模块只依赖稳定契约。

## 声明模型

声明是登录后在进程内传递身份信息的最小单元。一个 [`Claim`](https://learn.microsoft.com/zh-cn/dotnet/api/system.security.claims.claim) [*源码*](https://source.dot.net/#System.Security.Claims/Claim.cs) 保存一项事实，例如用户编号、用户名、角色、租户编号、语言或授权信息。

`ClaimNames` 定义了 Zongsoft 额外约定的声明名：

| 名称                        | 含义                        |
| ------------------------- | ------------------------- |
| `Namespace`               | 身份所属命名空间，常用于多租户、组织或业务域隔离。 |
| `Description`             | 身份或声明对象的说明文本。             |
| `Creation`、`Modification` | 创建和修改时间。                  |
| `Authorization`           | 授权相关声明，通常作为可重复声明处理。       |

`ClaimUtility` 负责 Claim 值与 .NET 类型之间的转换。它根据 Claim 的 `ValueType` 识别 `bool`、[`DateTime`](https://learn.microsoft.com/zh-cn/dotnet/api/system.datetime) [*源码*](https://source.dot.net/#System.Private.CoreLib/DateTime.cs)、[`DateOnly`](https://learn.microsoft.com/zh-cn/dotnet/api/system.dateonly) [*源码*](https://source.dot.net/#System.Private.CoreLib/DateOnly.cs)、[`TimeOnly`](https://learn.microsoft.com/zh-cn/dotnet/api/system.timeonly) [*源码*](https://source.dot.net/#System.Private.CoreLib/TimeOnly.cs)、整数、浮点数、[`TimeSpan`](https://learn.microsoft.com/zh-cn/dotnet/api/system.timespan) [*源码*](https://source.dot.net/#System.Private.CoreLib/TimeSpan.cs) 或 Zongsoft 类型别名。设置 Claim 时，`ClaimsIdentityExtension.SetClaim(...)` 会反向生成合适的 `ValueType`。

来源：[src/Security/UserChallenger.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Security/UserChallenger.cs#L126)（节选；上下文见源文件）。

{% code title="UserChallenger.cs" %}

```csharp
private ClaimsIdentity Identity(UserProfile user)
{
	var identity = user.Identity(UserIdentity.Scheme, "Zongsoft");

	identity.SetClaim(nameof(UserProfile.SiteId), user.SiteId);
	identity.SetClaim(nameof(UserProfile.Gender), user.Gender);
	identity.SetClaim(nameof(UserProfile.Avatar), user.Avatar);
	identity.SetClaim(nameof(UserProfile.Grade), user.Grade);
	identity.SetClaim(nameof(UserProfile.TotalPosts), user.TotalPosts);
	identity.SetClaim(nameof(UserProfile.TotalThreads), user.TotalThreads);

	//进行其他声明定义
	this.OnClaims(identity, user);

	//返回新构建的身份
	return identity;
}
```

{% endcode %}

`ClaimsIdentityExtension` 还提供了常用读取和判断：

* `GetIdentifier<T>()` 读取 `ClaimTypes.NameIdentifier` 并转换成指定类型。
* `GetNamespace()` / `SetNamespace(...)` 处理 Zongsoft 命名空间声明。
* `InRole(...)`、`InRoles(...)` 和 `IsAdministrator()` 按身份中的角色声明判断。
* `GetQualifiedName()` 组合命名空间与名称，形成 `namespace:name` 风格的限定名。
* `AsModel<T>()` 把 Claims 写入 `IUser`、`IModel` 或普通对象。

{% hint style="info" %}
Claims 应保存“认证后需要频繁读取的小事实”，例如用户编号、名称、租户编号、分支机构、语言、角色名和许可模块。不要把大对象、动态权限列表或需要实时一致的数据全部塞进 Claims；这些内容通常应通过服务查询或按需缓存。
{% endhint %}

## 凭证主体

`CredentialIdentity` 是登录身份，构造时会写入名称声明和签发者声明。签发者使用 `ClaimTypes.System` 保存，因此同一个 [`ClaimsPrincipal`](https://learn.microsoft.com/zh-cn/dotnet/api/system.security.claims.claimsprincipal) [*源码*](https://source.dot.net/#System.Security.Claims/ClaimsPrincipal.cs) 可以按认证方案或模块查找对应身份。

`CredentialPrincipal` 是登录后的凭证主体，扩展了标准 [`ClaimsPrincipal`](https://learn.microsoft.com/zh-cn/dotnet/api/system.security.claims.claimsprincipal) [*源码*](https://source.dot.net/#System.Security.Claims/ClaimsPrincipal.cs)：

| 属性             | 说明                            |
| -------------- | ----------------------------- |
| `CredentialId` | 凭证编号，用于注册、查找、注销和模型缓存键。        |
| `RenewalToken` | 续约令牌，用于刷新凭证或延长会话。             |
| `Scenario`     | 登录场景，例如 `web`、`api`、`mobile`。 |
| `Validity`     | 凭证有效时长。                       |
| `Disposed`     | 凭证失效通知，身份模型缓存会随之失效。           |

`CredentialPrincipal` 可以序列化和反序列化，因此默认凭证提供器可以把它写入缓存、Cookie 或其他存储。认证成功后，`Authentication.AuthenticateAsync(...)` 会创建凭证主体，执行 *质询器(**C**hallengers)*，最后通过 `ICredentialProvider.RegisterAsync(...)` 注册。

## 身份模型转换

`ClaimsIdentityModeling` 是当前推荐的身份模型入口。它从 `ApplicationContext.Current.Principal` 或传入的 *主体* 中获取 `CredentialPrincipal`，再按认证方案取出对应身份并执行转换。

转换器有两种来源：

* `Authentication.Transformer` 中的 `ClaimsPrincipalTransformer.Transformers`。
* 当前应用服务容器中的 `IClaimsIdentityTransformer`。

转换结果会以 `CredentialId` 和 *scheme* 为键缓存；凭证主体释放时缓存随之失效。Discussions 的 UserIdentity.Current 按 [Zongsoft.Discussions](https://github.com/Zongsoft/discussions/tree/main/src) 方案读取模型：

来源：[src/Security/UserIdentity.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Security/UserIdentity.cs#L110)（节选；上下文见源文件）。

{% code title="UserIdentity.cs" %}

```csharp
public static UserIdentity Current => ClaimsIdentityModeling.GetModel<UserIdentity>(Scheme);
```

{% endcode %}

业务 *转换器* 通常只处理自己认识的认证方案，把 *Claim* 转成业务身份属性：

来源：[src/Security/UserIdentity.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Security/UserIdentity.cs#L130)（节选；上下文见源文件）。

{% code title="UserIdentity.cs" %}

```csharp
private bool OnTransform(UserIdentity user, Claim claim)
{
	switch(claim.Type)
	{
		case nameof(UserIdentity.SiteId):
			user.SiteId = (claim.Value != null && uint.TryParse(claim.Value, out var siteId)) ? siteId : 0;
			return true;
		case nameof(UserIdentity.Gender):
			user.Gender = (claim.Value != null && Enum.TryParse<Models.Gender>(claim.Value, out var gender)) ? gender : Models.Gender.None;
			return true;
		case nameof(UserIdentity.Avatar):
			user.Avatar = claim.Value;
			return true;
		case nameof(UserIdentity.Grade):
			user.Grade = (claim.Value != null && byte.TryParse(claim.Value, out var grade)) ? grade : (byte)0;
			return true;
		case nameof(UserIdentity.TotalPosts):
			user.TotalPosts = (claim.Value != null && uint.TryParse(claim.Value, out var totalPosts)) ? totalPosts : 0;
			return true;
		case nameof(UserIdentity.TotalThreads):
			user.TotalThreads = (claim.Value != null && uint.TryParse(claim.Value, out var totalThreads)) ? totalThreads : 0;
			return true;
		default:
			return false;
	}
}
```

{% endcode %}

## 密码与秘密

`Password` 是核心库推荐的密码摘要结构。它把算法、指数、随机数和派生值打包在同一个值中，文本格式类似：

算法#指数:随机数|派生值。这是格式说明，真实序列化与验证输入见 [PasswordTest](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Security/PasswordTest.cs)。

`Password.Generate(...)` 使用 PBKDF2 生成摘要；省略算法参数时默认使用 SHA256。`Verify(...)` 根据摘要中保存的算法、随机数和指数重新计算，因此已有 SHA1 摘要仍可继续验证。旧的 `PasswordUtility` 已标记为过时，新的实现应优先使用 `Password`；业务服务中的 `Passworder` 目前仍保留旧格式兼容路径，后续需要配合重哈希迁移。

`ISecretor` 和 `Secretor` 用于一次性秘密，例如短信验证码、邮箱验证码、找回密码令牌或敏感操作二次确认。默认实现把秘密保存到分布式缓存，支持：

| 能力                   | 说明                       |
| -------------------- | ------------------------ |
| `GenerateAsync(...)` | 按名称生成秘密，并保存附加文本。         |
| `VerifyAsync(...)`   | 校验秘密但不删除，适合有效期内可重复确认的场景。 |
| `RemoveAsync(...)`   | 校验成功后删除，适合一次性消费。         |
| `Period`             | 限制同一名称重复生成的最小间隔。         |
| `Transmitter`        | 发送秘密，可接入短信、邮件、站内信和验证码校验。 |

{% hint style="warning" %}
验证码名称应包含业务场景和目标标识，可参照 Secretor.Transmitter.GetKey 对方案、目的地、模板、场景和通道的组合，并确保全局唯一。过宽的名称会导致不同用户或不同场景互相覆盖。
{% endhint %}

## 证书、签名与挑战

`ICertificate`、`ICertificateProvider<TCertificate>`、`ICertificateResolver` 和 `Certificate` 封装证书标识、颁发者、主体、有效期以及 RSA/X509 证书适配。`ISignaturer` 和 `ISecretor` 分别覆盖签名与秘密校验场景。

`IChallenger` 是认证后的补充质询点。认证器只负责“凭据是否正确”和“签发基础身份”，challenger 负责登录后的业务检查与 claims 增强。Discussions 的 UserChallenger 完成以下工作：

* 从主身份读取用户编号，并查询或创建 Discussions 用户资料。
* 创建资料时从身份命名空间确定 SiteId；已有资料保留自己的站点信息。
* 调用 OnVerify 扩展点，再创建 [Zongsoft.Discussions](https://github.com/Zongsoft/discussions/tree/main/src) 身份加入主体。
* 写入 SiteId、Gender、Avatar、Grade、TotalPosts 和 TotalThreads 声明。

当前 OnVerify 是空实现，不能据此宣称已校验账号启用状态、站点状态或许可范围；这些业务准入规则需要由实际模块补充。资料统计声明也是签发时的快照，不会随每次发帖自动刷新。

这种拆分能让密码登录、验证码登录等不同认证方式共享同一组业务质询规则。

## 异常与原因

所有安全相关异常都继承自 `SecurityException`。`SecurityReasons` 提供稳定原因码，例如：

| 原因                                   | 常见场景               |
| ------------------------------------ | ------------------ |
| `InvalidIdentity`                    | 身份不存在、编号无效或无法签发身份。 |
| `InvalidPassword`                    | 密码错误。              |
| `AccountDisabled`、`AccountSuspended` | 账号不可用或尝试次数过多。      |
| `Forbidden`                          | 认证通过后不满足业务准入条件。    |
| `VerifyFaild`                        | 验证码或秘密校验失败。        |

业务代码应优先抛出带原因码的 `AuthenticationException` 或 `AuthorizationException`，便于 Web 层、日志、监控和前端统一处理。

## 相关页面

* [Zongsoft.Security.Privileges](/framework/core/security/privileges)
* [安全模块概览](/framework/security)
* [插件框架](/framework/plugins)
* [数据访问接口](/framework/data/data-access)


# Privileges

Zongsoft.Core 中 Zongsoft.Security.Privileges 的权限模型、认证器、授权器和默认实现。

`Zongsoft.Security.Privileges` 是核心库中的身份认证与权限模型命名空间。它定义用户、角色、成员关系、权限定义、认证器、授权器、权限服务和权限计算规则；默认数据库实现位于 [Zongsoft.Security 项目](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Security)，业务系统可以继承这些基类并替换存储模型。

## 核心理念

Privileges 的设计不是把权限写死在接口、控制器或页面上，而是把“权限定义”和“授权结果”分开：

| 概念   | 类型                                                                                                                                                                                                                                                                                                                              | 说明                                                              |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| 权限定义 | [`Privilege`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Privileges/Privilege.cs)、`Privilege.Permission`                                                                                                                                                                                        | 插件声明出来的权限树，说明系统有哪些可授权能力。                                        |
| 授权记录 | [`IPrivilege`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Privileges/IPrivilege.cs)、[`IPrivilegable`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Privileges/IPrivilegable.cs)                                                                                   | 数据库存储的“某个用户或角色被授予/拒绝了哪些权限”。                                     |
| 授权目标 | `Privilege.Permission.Target`                                                                                                                                                                                                                                                                                                   | 业务资源或功能点，由具体模块声明，不能仅凭数据表名推断。                                    |
| 授权操作 | `Privilege.Permission.Action`                                                                                                                                                                                                                                                                                                   | 对目标的动作，例如 `Get`、`Query`、`Create`、`Update`、`Delete`，空操作按 `*` 处理。 |
| 权限主体 | [`IUser`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Privileges/IUser.cs)、[`IRole`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Privileges/IRole.cs)、[`Member`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Privileges/Member.cs) | 用户、角色，以及用户或角色加入角色后的成员关系。                                        |

一句话概括：插件负责声明“系统有什么权限”，数据库负责记录“谁被授予或拒绝了什么”，运行时负责根据角色继承和拒绝规则算出最终结果。

## 权限树

权限树由 `PrivilegeCategory` 和 `Privilege` 组成。分类用于导航和组织，权限用于表达一个可授权能力；一个权限可以包含多个 `Permission`，也就是多个目标与动作组合。

Discussions 当前没有声明独立的权限定义树。它在主题读取中直接判断审核状态、作者和版主关系；不能假定添加一个权限名称就自动改变这些业务规则。权限树本身的装配机制可阅读核心 [PrivilegeCategory](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Privileges/PrivilegeCategory.cs) 与[识别器](/framework/core/components/discriminator)。

权限定义与权限检查是两个步骤：定义告诉配置界面有哪些可授权能力，检查负责在真正执行操作前计算主体是否被授予权限。PermissionCollection 支持目标与动作匹配，空动作按通配动作处理；命名权限与数据记录的授权状态仍需通过服务关联。

## 权限本地化

`Privilege` 和 `PrivilegeCategory` 会按约定从资源中读取标题与说明。资源键按权限名称、分类路径、Category 和 Description 等约定组合。具体查找顺序以 [Privilege](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Privileges/Privilege.cs) 和 [PrivilegeCategory](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Privileges/PrivilegeCategory.cs) 为准。名称用于稳定匹配，资源文本负责展示；Discussions 现有业务资源的组织见[资源管理](/framework/core/resources)。

## 用户、角色与成员

`IUser` 与 `IRole` 都继承 Zongsoft 的可标识模型，核心属性包括名称、启用状态、头像、昵称、命名空间和描述。`IUser.Administrator` 与 `IRole.Administrators` 是框架约定的管理员名称。

`Member` 表示角色成员，成员可以是用户，也可以是角色：

| 成员类型              | 含义             |
| ----------------- | -------------- |
| `MemberType.User` | 用户加入角色。        |
| `MemberType.Role` | 角色加入角色，形成角色继承。 |

默认数据库中的 `Member` 表用 `RoleId + MemberId + MemberType` 保存成员关系。Discussions 的 UserIdentity 实现 IUser 并增加 SiteId 等讨论业务信息，其论坛成员关系仍由模块自己的模型维护。

## 认证链路

认证入口是 `Authentication.AuthenticateAsync(...)`：

1. 触发 Authenticating 事件，根据 scheme 从 `Authentication.Authenticators` 找到 `IAuthenticator`。
2. 调用 `VerifyAsync(...)` 校验输入凭据并返回票证。
3. 调用 `IssueAsync(...)` 把票证签发成 [`ClaimsIdentity`](https://learn.microsoft.com/zh-cn/dotnet/api/system.security.claims.claimsidentity) [*源码*](https://source.dot.net/#System.Security.Claims/ClaimsIdentity.cs)。
4. 创建 `CredentialPrincipal`，携带 `CredentialId`、`RenewalToken`、`Scenario` 和 `Validity`。
5. 依次执行 `Authentication.Challengers`。
6. 通过 `Authentication.Authority` 注册凭证。
7. 触发 Authenticated 事件；异常分支也触发该事件，并携带错误。

核心库提供两个认证器基类：

| 基类                                         | 默认 scheme | 输入         | 适用场景                    |
| ------------------------------------------ | --------- | ---------- | ----------------------- |
| `Authentication.IdentityAuthenticatorBase` | 空字符串      | 命名空间、身份、密码 | 用户名、邮箱、手机号加密码登录。        |
| `Authentication.SecretorAuthenticatorBase` | `Secret`  | 验证码或一次性秘密  | 短信验证码登录、邮箱验证码登录、找回密码确认。 |

默认安全插件把实现挂载到 `/Workbench/Security/Authentication`：

来源：[framework/Zongsoft.Security/src/Zongsoft.Security.plugin](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Security/src/Zongsoft.Security.plugin#L55)（节选；上下文见源文件）。

{% code title="Zongsoft.Security.plugin" %}

```xml
<!-- 挂载身份验证器 -->
<extension path="/Workbench/Security/Authentication">
	<object name="Identity" value="{static:Zongsoft.Security.Privileges.Authenticators.Identity, Zongsoft.Security}" />
	<object name="Secretor" value="{static:Zongsoft.Security.Privileges.Authenticators.Secretor, Zongsoft.Security}" />
</extension>
```

{% endcode %}

## 业务质询

`IChallenger` 在认证器签发身份之后执行，适合放业务准入规则和 claims 增强。业务系统把站点差异放在各站点的 `UserChallenger` 中，并通过插件挂载到 `Authentication.Challengers`：

来源：[src/Zongsoft.Discussions.plugin](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.plugin#L40)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.plugin" %}

```xml
	<!-- 挂载身份质询器 -->
	<extension path="/Workbench/Security/Authentication/Challengers">
		<object value="{static:Zongsoft.Discussions.Security.UserChallenger.Instance, Zongsoft.Discussions}" />
	</extension>

	<!-- 挂载身份转换器 -->
	<extension path="/Workbench/Security/Authentication/Transformers">
		<object value="{static:Zongsoft.Discussions.Security.UserIdentity+Transformer.Instance, Zongsoft.Discussions}" />
	</extension>
```

{% endcode %}

Discussions 的 UserChallenger 追加站点身份，并写入 SiteId、头像、等级和发帖统计等声明。UserIdentity.Transformer 仅识别 [Zongsoft.Discussions](https://github.com/Zongsoft/discussions/tree/main/src) 认证方案，把这些声明转换为业务身份；业务代码通过 UserIdentity.Current 读取。完整源码与字段含义见[安全基础](/framework/core/security)。

## 授权记录

授权记录由 `IPrivilegeService` 读写。默认数据库有两张授权表：

| 表                    | 作用                  |
| -------------------- | ------------------- |
| `Privilege`          | 保存用户或角色对某个权限名的授权方式。 |
| `PrivilegeFiltering` | 保存某个权限下的字段或数据过滤表达式。 |

授权方式由 `PrivilegeMode` 表示：

| 值         | 含义                       |
| --------- | ------------------------ |
| `Granted` | 明确授予。                    |
| `Denied`  | 明确拒绝，计算时会压过同层级授予。        |
| `Revoked` | 撤回或空状态，默认实现通常把它视为没有有效授权。 |

默认安全插件提供通用权限存储。Discussions 没有增加 TenantId、BranchId 形式的权限表覆写；其站点查询边界通过 DataValidator 实现，不能将两种机制混为一谈。

Discussions 未实现独立的角色权限写入范例；默认数据库的写入路径见 [PrivilegeService](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Security/src/Privileges/PrivilegeService.cs) 及 [PrivilegeServiceBase](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Privileges/PrivilegeServiceBase.cs)。调用时使用实际用户或角色标识，并由业务组合层明确授权记录的更新范围。

## 权限计算

授权判断入口是 `IAuthorizer.AuthorizeAsync(identity, privilege, parameters, cancellation)`。默认 `AuthorizerBase` 会先按用户标识缓存计算结果，再检查指定权限名是否在最终集合中。

默认 `PrivilegeEvaluator` 的计算流程是：

1. 把当前用户或角色标识转换成成员标识。
2. 通过 `IMemberService.GetAncestorsAsync(...)` 取出所有祖先角色，按层级由远及近返回。
3. 按层级读取每组角色的授权记录。
4. 读取当前用户或角色自己的授权记录。
5. 调用 `PrivilegeEvaluatorBase` 折叠所有声明。

折叠规则是“就近优先、同级拒绝优先”：

* 祖先层级先进入上下文，当前用户或当前角色最后进入上下文，所以越靠近当前主体的授权越晚处理。
* 同一层级中，`Denied` 会移除同名权限，并阻止同层级的 `Granted` 重新加入。
* 下一层级如果再次 `Granted`，可以覆盖更远层级的拒绝；这就是“就近优先”。

来源：[framework/Zongsoft.Core/src/Security/Privileges/AuthorizerBase.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Security/Privileges/AuthorizerBase.cs#L67)（节选；上下文见源文件）。

{% code title="AuthorizerBase.cs" %}

```csharp
public virtual async ValueTask<bool> AuthorizeAsync(ClaimsIdentity user, string privilege, Parameters parameters, CancellationToken cancellation = default)
{
	if(user == null)
		return false;

	if(privilege == null)
		return false;

	var privileges = await _cache.GetOrCreateAsync(user.Identify(),
		key => (GetPrivilegesAsync((Identifier)key, cancellation), TimeSpan.FromMinutes(60)));

	return privileges.Contains(privilege);

	async Task<HashSet<string>> GetPrivilegesAsync(Identifier identifier, CancellationToken cancellation)
	{
		var privileges = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

		var results = this.Evaluator.EvaluateAsync(identifier, parameters, cancellation);
		await foreach(var result in results)
			privileges.Add(result.Privilege);

		return privileges;
	}
}
```

{% endcode %}

{% hint style="warning" %}
默认 `AuthorizerBase` 以用户标识缓存最终权限集，使用 60 分钟滑动过期。修改角色成员或授权记录后，业务实现需要考虑缓存刷新、应用拥有的缓存失效机制，否则短时间内可能看到旧授权结果。
{% endhint %}

## 权限过滤

`IPrivilegeService.Filtering` 提供权限过滤服务。它不决定“有没有某权限”，而是描述“有该权限时，还应隐藏哪些字段或限制哪些范围”。默认数据库的 `PrivilegeFiltering.PrivilegeFilter` 是字符串表达式，。Discussions 没有接入这套授权过滤表达式；它的 PostFilter 和 ThreadFilter 处理正文可见性，属于数据过滤器机制。真实实现见[过滤器](/framework/core/components/filter)。

这种能力适合数据查询、导出或详情接口的字段裁剪。调用方应先完成普通授权，再读取过滤服务并把过滤表达式交给数据层或业务层解释。

扩展默认实现时，应分别核对模型、授权记录存储、主体身份和缓存失效；只替换用户模型不会自动改变所有权限查询条件。

## 使用建议

* 新增功能权限时，先在对应业务插件的 `*-privileges.plugin` 中声明分类、权限和目标动作，再补资源文本。
* 授权记录只保存权限名和授权方式，不要复制权限树结构。
* 多租户系统应在权限服务的数据条件中带入租户边界，避免跨租户读取授权记录。
* Claims 中保存租户、机构、角色名和许可摘要；实时敏感的授权结果仍通过 `IAuthorizer` 或 `IPrivilegeService` 查询。
* 角色继承不要过深。层级越深，授权解释越难，缓存失效也越容易被忽略。
* `Denied` 用于明确阻断继承权限；普通移除应使用 `Revoked` 或删除授权记录。

## 相关页面

* [Zongsoft.Security](/framework/core/security)
* [插件文件与加载](/framework/plugins/plugin-file)
* [数据访问接口](/framework/data/data-access)
* [分层类](/framework/core/collections/category)


# Zongsoft.Serialization

从 Discussions 用户归档筛选与模型属性理解序列化边界。

Serializer.Json 在 System.Text.Json 基础上支持 Zongsoft 模型与数据字典等框架类型。Discussions 的用户归档提供者使用它将输入流还原为条件模型，然后交给数据服务。

## 真实输入不是任意对象图

来源：[src/Features/Archiving/UserDataTemplateModelProvider.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Features/Archiving/UserDataTemplateModelProvider.cs#L54)（节选；上下文见源文件）。

{% code title="UserDataTemplateModelProvider.cs" %}

```csharp
public override IDataTemplateModel GetModel(IDataTemplate template, object argument)
{
	var schema = $"*, {nameof(UserProfile.Site)}" + "{*}";

	if(argument is Stream stream)
		argument = Serializer.Json.Deserialize<UserProfileCriteria>(stream);

	var data = argument switch
	{
		string key => this.Services.ResolveRequired<UserService>().Get(key, schema),
		IModel model => this.Services.ResolveRequired<UserService>().Select(Criteria.Transform(model), schema),
		_ => this.Services.ResolveRequired<UserService>().Select(null, schema),
	};

	return new DataTemplateModel(new { Users = data });
}
```

{% endcode %}

流输入明确指定 UserProfileCriteria，随后通过 Criteria.Transform 转为数据条件。字符串输入则按用户键查询；这两条路径不同，不应随意把输入反序列化成 Dictionary 再拼接 SQL。

## 模型字段与输出范围

来源：[src/Models/Post.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Models/Post.cs#L110)（节选；上下文见源文件）。

{% code title="Post.cs" %}

```csharp
[Serialization.SerializationMember(Ignored = true)]
public IEnumerable<PostVoting> Upvotes => this.Votes == null ? Array.Empty<PostVoting>() : this.Votes.Where(vote => vote.Value > 0);

/// <summary>获取帖子的被踩记录集。</summary>
[Serialization.SerializationMember(Ignored = true)]
public IEnumerable<PostVoting> Downvotes => this.Votes == null ? Array.Empty<PostVoting>() : this.Votes.Where(vote => vote.Value < 0);
#endregion
```

{% endcode %}

忽略成员特性控制序列化可见性，不代表这些成员不存在于数据库，也不替代 schema 或授权。模型返回正文之前，还会经过审核过滤和外置内容读取。

## 框架选项参考

| 选项                    | 用途         |
| --------------------- | ---------- |
| Indented              | 可读缩进输出     |
| NamingConvention      | 成员命名约定     |
| IgnoreNull、IgnoreZero | 控制空值或默认值输出 |
| IncludeFields         | 是否包含字段     |
| MaximumDepth          | 对象图深度限制    |
| Typified              | 为松散值保留类型信息 |

Discussions 当前未使用 Typified 或自定义 JSON 转换器作为归档入口。相关真实测试可查阅 [JsonSerializerTest](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Serialization/JsonSerializerTest.cs) 和 [TextSerializationOptionsTest](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Serialization/TextSerializationOptionsTest.cs)。

## 安全与生命周期

反序列化成功只说明输入能被解析，不说明调用者有权查询或导出相应用户。应限制输入大小与对象深度，传播取消信号，并让服务层继续验证业务范围。归档输出的工作簿结构见[表格与模板](/framework/externals/documents)。


# Zongsoft.Services

理解 Zongsoft.Services 的应用上下文、服务注册、服务解析和插件服务发现。

`Zongsoft.Services` 是 Zongsoft 运行时的服务模型。它连接标准 .NET 依赖注入、应用上下文、[应用模块](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationModule.cs)和插件树，让宿主程序、插件程序集和声明式构件可以把能力注册到同一个运行时，并按应用、模块或插件树位置解析出来。

它不是要替代 .NET DI，而是在 [`IServiceCollection`](https://learn.microsoft.com/zh-cn/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection) [*源码*](https://source.dot.net/#Microsoft.Extensions.DependencyInjection.Abstractions/IServiceCollection.cs)、`System.IServiceProvider` 和 [`IServiceProviderFactory<TContainerBuilder>`](https://learn.microsoft.com/zh-cn/dotnet/api/microsoft.extensions.dependencyinjection.iserviceproviderfactory-1) [*源码*](https://source.dot.net/#Microsoft.Extensions.DependencyInjection.Abstractions/IServiceProviderFactory.cs) 之上补充这些能力：

* 应用上下文：用 [`IApplicationContext`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationContext.cs) 表示当前应用实例，统一暴露配置、环境、模块、服务、事件、[工作器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)和生命周期。
* 应用模块：用 [`IApplicationModule`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationModule.cs) 表示一个子系统或插件模块，并为模块提供自己的服务解析域。
* 服务发现：通过服务名称、标签、匹配参数、模块名和插件树表达式寻找服务，而不只按类型解析。
* 服务构建：通过 [`ServiceAttribute`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/ServiceAttribute.cs)、[`IServiceRegistration`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IServiceRegistration.cs)、[`ServiceDependencyAttribute`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/ServiceDependencyAttribute.cs) 和 [`ServiceProviderFactory`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/ServiceProviderFactory.cs) 把声明式注册、代码注册和属性注入串起来。
* 分布式协作：在 `Zongsoft.Services.Distributing` 中提供分布式锁和锁令牌等基础抽象。

## 运行时结构

在插件式应用中，服务模型通常形成三层：

| 层级     | 入口                                                   | 作用                                              |
| ------ | ---------------------------------------------------- | ----------------------------------------------- |
| 应用服务容器 | `ApplicationContext.Current.Services`                | 全局默认容器，承载宿主、插件和框架注册的服务。                         |
| 模块服务容器 | `ApplicationContext.Current.Modules["模块名"].Services` | 以模块名为边界解析服务；模块内找不到时会回退到应用服务容器。                  |
| 插件树构件  | `/Workspace/Environment/Services`、`/Workbench/...`   | 声明式对象和扩展点；其中服务节点会被注册到应用服务容器，其他节点通常通过路径或构件解析器发现。 |

这种结构的设计意图是把“进程级基础设施”和“插件提供的业务能力”分开：宿主负责建立 Host 和服务容器，插件负责声明程序集、构件和扩展点，应用上下文负责把两者组织成可查询的运行时。

{% hint style="info" %}
插件树中的对象不一定都是 DI 服务。只有被程序集扫描注册、代码显式注册，或挂在 `/Workspace/Environment/Services` 下并由宿主构建流程加入服务集合的对象，才会进入服务容器。
{% endhint %}

## 注册来源

服务进入应用通常有四个来源。

### 代码注册

宿主或插件可以在 Host 构建阶段直接向服务集合添加服务。下面是 framework 中 gRPC 扩展的真实注册器，配置协议服务及反射支持。普通 .NET 服务生命周期仍然按 DI 规则生效，例如单例、作用域和瞬态。

来源：[framework/Zongsoft.Web/grpc/GrpcInitializer.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Web/grpc/GrpcInitializer.cs#L86)（节选；上下文见源文件）。

{% code title="GrpcInitializer.cs" %}

```csharp
public void Register(IServiceCollection services, IConfiguration configuration)
{
	services.AddGrpc();
	services.AddGrpcReflection();
}
```

{% endcode %}

代码注册适合宿主私有服务、启动入口相关服务，或需要明确生命周期的基础设施服务。

### 程序集扫描

插件宿主在构建应用时，会调用 [`ServiceCollectionExtension.Register`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/ServiceCollectionExtension.cs) 扫描程序集中的公共类型：

1. 先扫描入口程序集引用的程序集。
2. 再扫描入口程序集本身。
3. 再递归扫描已加载插件 `manifest` 中声明的程序集；同一个程序集只注册一次。

扫描时有两种注册模式：

| 模式                        | 适用场景                         | 行为                                                              |
| ------------------------- | ---------------------------- | --------------------------------------------------------------- |
| 实现 `IServiceRegistration` | 一个程序集需要集中注册多项服务、复杂生命周期或条件注册。 | 框架创建注册器实例，调用 `Register(services, configuration)`；注册器接管该类型的服务注册。 |
| 标注 `ServiceAttribute`     | 类型本身就是服务，适合简单、约定化注册。         | 框架把实现类型注册为单例，并按契约、名称、标签和静态成员规则追加注册。                             |

来源：[src/Services/ForumService.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/ForumService.cs#L41)（节选；上下文见源文件）。

{% code title="ForumService.cs" %}

```csharp
[Service(nameof(ForumService))]
[DataService(typeof(ForumCriteria))]
public class ForumService : DataServiceBase<Forum>
{
	#region 构造函数
	public ForumService(IServiceProvider serviceProvider) : base(serviceProvider) { }
```

{% endcode %}

当 `ServiceAttribute` 指定名称时，框架会登记该名称，名称以 `Service` 结尾时还会登记去掉后缀后的短名。因此 Discussions 的 ForumService 会登记 ForumService 名称以及 Forum 短名。

### 静态成员注册

如果服务对象本来就是静态属性或字段，可以通过 `ServiceAttribute.Members` 暴露成员值。框架会读取指定的公开静态成员，并按成员类型和显式契约注册为单例实例。

来源：[framework/Zongsoft.Diagnostics/protocols/server/src/Listener.Metrics.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Diagnostics/protocols/server/src/Listener.Metrics.cs#L51)（节选；上下文见源文件）。

{% code title="Listener.Metrics.cs" %}

```csharp
[Service(Tags = "gRPC", Members = nameof(Metrics))]
partial class Listener
{
	#region 单例字段
	public static readonly MetricsProcessor Metrics = new();
```

{% endcode %}

这种方式适合无状态、全局唯一、已经由框架或第三方库提供的对象。需要依赖注入构造参数的服务，优先使用普通类型注册。

### 插件树服务节点

插件宿主还会查找 `/Workspace/Environment/Services` 节点，并把该节点下的每个子构件作为单例服务注册到应用服务集合中。服务类型来自构件的 `ValueType`，服务实例由构件在解析时创建或解包。

Discussions 的插件声明把模块挂到 /Workbench/Modules，再暴露数据访问器、事件与属性。它没有声明 /Workspace/Environment/Services 节点；完整清单见[插件文件](/framework/plugins/plugin-file)，不要把任意模块节点当成此专用服务注册节点。

这种方式适合需要从插件文件声明、用 `{option:...}`、`{path:...}` 或 `{service:...}` 组装的服务。它的生命周期在当前宿主实现中按单例注册，因此不要把请求级状态、会话状态或必须频繁重建的对象放在这里。

## 服务解析

服务解析分为按类型、按名称、按标签和按匹配参数四类。

| 方式   | API                                                     | 说明                                                                                                                                                                                                                                                                    |
| ---- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 类型解析 | `Resolve<T>()`、`ResolveRequired<T>()`、`ResolveAll<T>()` | 对标准 `GetService`、`GetRequiredService`、`GetServices` 的语义封装。                                                                                                                                                                                                            |
| 名称解析 | `Resolve("name")`、`ResolveRequired("name")`             | 根据 `ServiceAttribute.Name` 登记的名称解析服务。                                                                                                                                                                                                                                 |
| 匹配解析 | `Find<T>(argument)`、`FindAll<T>(argument)`              | 在同一契约的多个实现中查找匹配者。服务可实现 [`IMatchable`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IMatchable.cs) 或 [`IMatcher<T>`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IMatcher.cs)，也可以通过 `Name` 属性与参数做忽略大小写匹配。 |
| 标签解析 | `Resolves(tag)`、`GetTags(tag)`                          | 按 `ServiceAttribute.Tags` 组织服务集合，适合把一组同类扩展归入同一用途。                                                                                                                                                                                                                     |

{% hint style="warning" %}
按名称解析依赖注册阶段记录的名称映射，主要来自 `ServiceAttribute.Name`。如果只用 `services.AddSingleton<T>()` 手工注册，而没有额外登记名称，就不能直接通过 `Resolve("name")` 找到它。
{% endhint %}

### 按类型解析

按类型解析适合普通依赖：调用方知道需要哪个契约，不关心具体实现是谁。`Resolve<T>()` 适合可选依赖，`ResolveRequired<T>()` 适合缺失即失败的强依赖，`ResolveAll<T>()` 适合[初始化器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationInitializer.cs)、处理器、过滤器这类多实现集合。

[`ApplicationContext`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/ApplicationContext.cs) 中有两个典型用法：

* `Exit(...)` 通过 `Resolve<IHost>()` 查找当前 [`IHost`](https://learn.microsoft.com/zh-cn/dotnet/api/microsoft.extensions.hosting.ihost) [*源码*](https://source.dot.net/#Microsoft.Extensions.Hosting.Abstractions/IHost.cs)。只有宿主与生命周期服务都存在、且宿主尚未停止时，才等待停止流程；随后仍会调用 System.Environment.Exit 退出进程。因此不能把此方法当作“没有宿主就什么也不做”的查询。
* `Initialize()` 通过 `ResolveAll<IApplicationInitializer>()` 收集所有应用[初始化器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationInitializer.cs)，然后逐个执行。

来源：[framework/Zongsoft.Core/src/Services/ApplicationContext.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/ApplicationContext.cs#L150)（节选；上下文见源文件）。

{% code title="ApplicationContext.cs" %}

```csharp
public void Exit(int exitCode, TimeSpan timeout = default)
{
	if(_disposed != 0)
		return;

	var host = _services.Resolve<IHost>();
	var lifetime = _services.GetService<IHostApplicationLifetime>();

	if(host != null && lifetime != null)
	{
		//如果应用程序正在停止或已经停止，则不再执行停止操作，否则会导致死锁
		var exiting = lifetime.ApplicationStopping.IsCancellationRequested || lifetime.ApplicationStopped.IsCancellationRequested;

		if(!exiting)
		{
			if(timeout > TimeSpan.Zero)
				host.StopAsync(timeout).GetAwaiter().GetResult();
			else
				host.StopAsync().GetAwaiter().GetResult();

			host.WaitForShutdown();
		}
	}

	System.Environment.Exit(exitCode);
}
```

{% endcode %}

来源：[framework/Zongsoft.Core/src/Services/ApplicationContext.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/ApplicationContext.cs#L188)（节选；上下文见源文件）。

{% code title="ApplicationContext.cs" %}

```csharp
public virtual bool Initialize()
{
	ObjectDisposedException.ThrowIf(_disposed != 0 || _initializers == null, this);

	var initialized = Interlocked.Exchange(ref _initialized, 1);
	if(initialized != 0)
		return false;

	var services = this.Services;

	if(services != null)
		_initializers.AddRange(services.ResolveAll<IApplicationInitializer>());

	foreach(var initializer in _initializers)
		initializer?.Initialize(this);

	return true;
}
```

{% endcode %}

这两个例子体现了类型解析的边界：单个可选对象用 `Resolve<T>()`，多扩展点集合用 `ResolveAll<T>()`。如果调用方必须拿到服务才能继续，则改用 `ResolveRequired<T>()`，把注册缺失暴露为明确异常。

### 按名称解析

按名称解析适合“文本配置最终指向一个服务实例”的场景。核心库中的 [`MessageQueueConverter`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Messaging/MessageQueueConverter.cs) 就是这样的例子：它把字符串转换为 `IMessageQueue`。

它的解析顺序是：

1. 如果文本形如 `queue@provider`，先用 `Find<IMessageQueueProvider>(provider)` 找到对应队列提供器，再从该提供器取队列。
2. 如果没有指定提供器，就遍历 `ResolveAll<IMessageQueueProvider>()`，找到第一个包含该队列名的提供器。
3. 如果所有提供器都找不到，最后才用 `Resolve(text)` 按服务名称解析队列实例。

来源：[framework/Zongsoft.Core/src/Messaging/MessageQueueConverter.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Messaging/MessageQueueConverter.cs#L47)（节选；上下文见源文件）。

{% code title="MessageQueueConverter.cs" %}

```csharp
public static IMessageQueue Resolve(IServiceProvider services, string text)
{
	if(services == null || string.IsNullOrEmpty(text))
		return null;

	var index = text.IndexOf('@');

	if(index > 0 && index < text.Length - 1)
	{
		var provider = services.Find<IMessageQueueProvider>(text[(index + 1)..]);
		if(provider == null)
			return null;

		var name = text[..index];
		return provider.Exists(name) ? provider.Queue(name) : null;
	}

	foreach(var provider in services.ResolveAll<IMessageQueueProvider>())
	{
		if(provider.Exists(text))
			return provider.Queue(text);
	}

	return services.Resolve(text) as IMessageQueue;
}
```

{% endcode %}

这里的 `Resolve(text)` 是兜底方案，适合某个队列对象本身已经通过 `ServiceAttribute.Name` 登记为命名服务的情况。若配置必须命中命名服务，可以使用 `ResolveRequired(name)`，让配置错误在解析阶段直接失败。

### 根据参数查找

`Find<T>(argument)` 和 `FindAll<T>(argument)` 适合同一契约存在多个实现，并且调用方只知道一个“选择参数”的场景。核心库里大量基础类型已经为这种模式实现了 `IMatchable` 或 `IMatchable<string>`。

例如 [`ExpressionEvaluatorBase`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Expressions/ExpressionEvaluatorBase.cs) 以 `Name` 表示表达式求值器名称，并用 `IMatchable` 支持忽略大小写匹配。调用方只需要传入名称，就能在多个 `IExpressionEvaluator` 实现中找到目标求值器。

来源：[framework/Zongsoft.Core/src/Expressions/ExpressionEvaluatorBase.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Expressions/ExpressionEvaluatorBase.cs#L71)（节选；上下文见源文件）。

{% code title="ExpressionEvaluatorBase.cs" %}

```csharp
bool Services.IMatchable.Match(object argument) => argument is string name && string.Equals(name, this.Name, StringComparison.OrdinalIgnoreCase);
bool Services.IMatchable<string>.Match(string name) => string.Equals(name, this.Name, StringComparison.OrdinalIgnoreCase);
```

{% endcode %}

实际按名称匹配并取得队列的调用已经展示在上面的 MessageQueueConverter 中。表达式求值器的执行范例见[脚本与表达式](/framework/externals/scripting)，名称是否可用取决于对应扩展包是否注册。

[`TextRegular`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Text/TextRegular.cs) 则把匹配参数当作待验证文本。也就是说，`Find<ITextRegular>(text)` 不是按名称找正则，而是在一组文本规则服务中找到能够匹配该文本的规则。

来源：[framework/Zongsoft.Core/src/Text/TextRegular.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Text/TextRegular.cs#L108)（节选；上下文见源文件）。

{% code title="TextRegular.cs" %}

```csharp
bool Services.IMatchable.Match(object parameter) => parameter != null && this.Match(parameter.ToString());
```

{% endcode %}

消息队列提供器也是典型场景。[`MessageQueueFactoryBase`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Messaging/MessageQueueFactoryBase.cs) 通过名称匹配队列工厂，`MessageQueueConverter` 解析 `queue@provider` 时就调用了 `Find<IMessageQueueProvider>(provider)`。

来源：[framework/Zongsoft.Core/src/Messaging/MessageQueueFactoryBase.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Messaging/MessageQueueFactoryBase.cs#L48)（节选；上下文见源文件）。

{% code title="MessageQueueFactoryBase.cs" %}

```csharp
protected virtual bool OnMatch(string name) => string.Equals(this.Name, name, StringComparison.OrdinalIgnoreCase);
bool IMatchable.Match(object argument) => this.OnMatch(argument as string);
bool IMatchable<string>.Match(string argument) => this.OnMatch(argument);
```

{% endcode %}

因此，`Find` 的关键不是“按类型拿第一个服务”，而是让每个候选服务自己判断“我是否适合这个参数”。当没有实现 `IMatchable` 或 `IMatcher<T>` 时，框架才会尝试用服务的 `Name` 属性做默认匹配。

### 获取标签集

标签适合把服务按“用途”组织起来，而不是按类型或名称选择单个实现。`Zongsoft.Diagnostics.Protocols.Server` 和 `Zongsoft.Web.Grpc` 就用标签把 gRPC 服务注册与端点映射解耦。

在 [`Listener.Metrics.cs`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Diagnostics/protocols/server/src/Listener.Metrics.cs) 中，`Metrics` 静态成员被标注为服务，并打上 `gRPC` 标签：

静态成员及其 Service 注解见上文“静态成员注册”的真实源码片段。

`ServiceCollectionExtension` 扫描到这个注解时，会把 `Metrics` 成员值注册为服务，并把该服务类型归入 `gRPC` 标签。到了 [`GrpcInitializer`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Web/grpc/GrpcInitializer.cs)，[初始化器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationInitializer.cs)不需要知道有哪些诊断或业务 gRPC 服务，只要读取标签下的服务类型即可：

来源：[framework/Zongsoft.Web/grpc/GrpcInitializer.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Web/grpc/GrpcInitializer.cs#L57)（节选；上下文见源文件）。

{% code title="GrpcInitializer.cs" %}

```csharp
public void Initialize(IApplicationBuilder builder)
{
	if(builder is IEndpointRouteBuilder app)
	{
		foreach(var service in app.ServiceProvider.GetTags("gRPC"))
		{
			MapGrpcService(app, service);
		}

		app.MapGrpcReflectionService();
	}
}
```

{% endcode %}

这个例子的适用场景很明确：插件或模块负责声明“我是一个 gRPC 服务”，Web gRPC [初始化器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationInitializer.cs)负责统一映射所有带 `gRPC` 标签的服务。双方不需要互相引用具体实现。

### 按标签解析

`GetTags(tag)` 返回标签下的服务类型，适合 `GrpcInitializer` 这类“只需要类型，不需要实例”的场景。`Resolves(tag)` 和 `Resolves(Type, tag)` 则会进一步从服务容器中解析实例，适合需要直接调用标签下服务对象的场景。

框架 TaggedServiceTest 提供了真实的实例解析测试。ITaggedContractA、ITaggedContractB、TaggedService 与 ProviderScope 均定义在同一测试文件；两个契约应解析到同一个对象。

来源：[framework/Zongsoft.Core/test/Services/TaggedServiceTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Services/TaggedServiceTest.cs#L15)（节选；上下文见源文件）。

{% code title="TaggedServiceTest.cs" %}

```csharp
public void Register_SameServiceAndTagAcrossAttributes_MergesAllContracts()
{
	using var provider = CreateProvider();

	var first = Assert.Single(provider.Provider.Resolves<ITaggedContractA>(TaggedService.Tag.ToLowerInvariant()));
	var second = Assert.Single(provider.Provider.Resolves<ITaggedContractB>(TaggedService.Tag.ToUpperInvariant()));

	Assert.Same(first, second);
}
```

{% endcode %}

测试通过下列工厂扫描测试程序集，再创建框架服务提供器。解析前必须完成登记；标签不依靠调用方临时附加到对象上。

来源：[framework/Zongsoft.Core/test/Services/TaggedServiceTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Services/TaggedServiceTest.cs#L57)（节选；上下文见源文件）。

{% code title="TaggedServiceTest.cs" %}

```csharp
private static ProviderScope CreateProvider()
{
	var services = new ServiceCollection();
	services.Register(typeof(TaggedServiceTest).Assembly, null);
	return new ProviderScope(new ServiceProviderFactory().CreateServiceProvider(services));
}
```

{% endcode %}

实际映射 gRPC 端点时选择 `GetTags("gRPC")` 更合适，因为 `MapGrpcService<TService>()` 需要的是服务类型；需要执行服务对象行为时，再使用 `Resolves(...)` 解析实例。

## 模块服务域

模块服务域用于解决“同一个契约在不同模块里可能有不同实现”的问题。模块的 `Services` 属性会基于应用服务容器创建一个带模块名的服务提供器；解析时会先尝试模块化服务，再回退到应用服务容器。

模块化服务的来源通常是带 [`ApplicationModuleAttribute`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/ApplicationModuleAttribute.cs) 的服务类型。框架扫描到服务契约时，会额外登记带模块名的包装服务，使模块容器能优先取到本模块实现。

来源：[src/Module.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Module.cs#L31)（节选；上下文见源文件）。

{% code title="Module.cs" %}

```csharp
[assembly: ApplicationModule(Zongsoft.Discussions.Module.NAME)]
```

{% endcode %}

Discussions 在程序集上声明 [ApplicationModule](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/ApplicationModule.cs)，并把 Module.NAME 定义为 Discussions。MessageSendCommand 的属性注入明确选择同一模块服务域。

当对象位于某个模块或插件树节点下时，框架会尽量根据对象所属模块选择服务容器。这样业务插件可以声明自己的模块服务，同时仍能复用应用级公共服务。

## 属性注入

构造函数注入仍然是首选。属性或字段注入主要用于插件构件、运行时反射构建对象、可选依赖或需要按模块名选择服务的场景。

[`ServiceDependencyAttribute`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/ServiceDependencyAttribute.cs) 支持三类选择：

| 设置                                  | 含义                             |
| ----------------------------------- | ------------------------------ |
| 不指定 `Provider`                      | 使用注入目标所在模块的服务容器；找不到时回退到应用服务容器。 |
| `Provider = "/"` 或 `Provider = "*"` | 直接使用应用服务容器。                    |
| `Provider = "模块名"`                  | 使用指定模块的服务容器；找不到时回退到应用服务容器。     |

来源：[src/Services/Commands/MessageSendCommand.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/Commands/MessageSendCommand.cs#L58)（节选；上下文见源文件）。

{% code title="MessageSendCommand.cs" %}

```csharp
[ServiceDependency(Provider = Module.NAME)]
public MessageService Service { get; set; }
```

{% endcode %}

如果设置 `ServiceName`，注入器会改用 [`IServiceProvider<T>`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IServiceProvider%601.cs) 的 `GetService(string)` 查找命名服务。`ServiceName = "~"` 或 `ServiceName = "."` 表示把注入目标所在模块名作为服务名。

## 插件中的服务发现

插件文件里的 `{service:...}` 表达式由 [`Zongsoft.Plugins`](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Plugins) 中的 [`ServicesParser`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins/src/Services/ServicesParser.cs) 处理。它不是单纯从全局容器取对象，而会结合当前构件位置和显式容器名选择服务域。

| 表达式                                 | 结果                              |
| ----------------------------------- | ------------------------------- |
| `{service:@}`                       | 返回应用默认服务容器。                     |
| `{service:@模块名}`                    | 返回名为 `模块名` 的模块服务容器；模块不存在则返回空。   |
| `{service:服务名}`                     | 从当前构件所属模块容器或应用容器解析名为 `服务名` 的服务。 |
| `{service:服务名@模块名}`                 | 从 `模块名` 模块服务容器解析名为 `服务名` 的服务。   |
| `{service:~}`                       | 按当前目标成员类型解析一个服务。                |
| `{service:*}`                       | 按当前目标成员类型解析所有服务。                |
| `{service:~@}`、`{service:*@}`       | 强制从应用默认服务容器按目标成员类型解析。           |
| `{service:~@模块名}`、`{service:*@模块名}` | 从指定模块服务容器按目标成员类型解析。             |

上表的“模块名”“服务名”是语法占位符，并非已注册的服务。Discussions.plugin 实际使用 static 与 path 表达式挂载模块、访问器和过滤器，可对照[构件与服务](/framework/plugins/builtins-and-services)阅读。

所有格式还可以在服务对象后继续访问属性或字段，例如 `{service:服务名.属性名@模块名}`。这适合在插件构件属性中引用已注册服务的某个配置对象或子对象。

{% hint style="info" %}
未显式写 `@模块名` 时，服务解析器会尝试使用当前构件父节点名称匹配模块名；匹配失败才使用应用默认服务容器。插件路径命名如果能和模块名保持一致，服务表达式会更自然。
{% endhint %}

## 插件宿主的构建顺序

`Zongsoft.Plugins.Hosting` 中的应用构建器把服务注册和插件加载编排在一起。以 `Application.Daemon(...)`、`Application.Terminal(...)` 为入口时，构建过程大致如下：

1. 创建 .NET Host 构建器，并把容器工厂设置为 `Zongsoft.Services.ServiceProviderFactory`。
2. 按应用名加载宿主 `.option` 配置。
3. 创建 [`PluginOptions`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins/src/PluginOptions.cs)，把插件配置源加入应用配置。
4. 调用 [`PluginTree.Get(options).Load()`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins/src/PluginTree.cs) 加载插件树。
5. 扫描宿主引用程序集、宿主程序集和插件清单程序集，执行服务注册。
6. 注册默认 `System.Net.Http.HttpClient` 服务。
7. 把 `/Workspace/Environment/Services` 下的构件注册为单例服务。
8. 构建 Host，并通过 `Initialize()` 初始化 [`PluginApplicationContext`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins/src/PluginApplicationContext.cs)。
9. 应用启动时打开[工作台](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins/src/IWorkbenchBase.cs)，加载 `/Workbench/Startup` 下的[工作器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)。

`Daemon` 和 `Terminal` 会先注册各自的应用上下文实现，再映射为 [`PluginApplicationContext`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins/src/PluginApplicationContext.cs) 和 [`IApplicationContext`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationContext.cs)。这意味着应用代码通常只依赖 `IApplicationContext`，而插件宿主内部仍能使用更具体的插件上下文访问插件树和[工作台](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins/src/IWorkbenchBase.cs)。

## 使用建议

* 宿主专属、生命周期敏感、需要立即配置的服务，用代码注册。
* 插件程序集中的普通业务服务，用 `ServiceAttribute` 或 `IServiceRegistration` 注册。
* 需要在插件文件中声明、可由配置或路径表达式组装的对象，放到 `/Workspace/Environment/Services`。
* 需要被其他插件按扩展点发现的对象，优先挂到约定插件树路径，而不是强行放进 DI 容器。
* 同一契约有多个实现时，用 `Find<T>(argument)`、`IMatchable`、`IMatcher<T>` 或标签组织，不要把实现选择逻辑写死在调用方。
* 模块内部服务尽量标注 [`ApplicationModuleAttribute`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/ApplicationModuleAttribute.cs)，让模块容器可以优先解析本模块实现。

{% hint style="warning" %}
`ServiceAttribute` 扫描注册的普通类型默认按单例注册。包含可变状态、请求状态或需要释放的短生命周期对象，应使用代码注册明确生命周期，或通过工厂服务创建。
{% endhint %}

## 子命名空间

| 命名空间                             | 说明                  |
| -------------------------------- | ------------------- |
| `Zongsoft.Services.Distributing` | 分布式锁、锁令牌和分布式协作基础类型。 |

## 相关资源

* [分布式锁](/framework/core/services/distributed-lock)
* [插件应用模型](/framework/plugins/application-model)
* [宿主集成](/framework/plugins/hosting)
* [构件与服务](/framework/plugins/builtins-and-services)
* [Services 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Services)
* [Plugins Hosting 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Plugins/src/Hosting)
* [Plugins Services 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Plugins/src/Services)

关于按契约解析、按名称匹配、提供者定位以及共享实例所有权，参见[服务定位与所有权](/framework/core/services/locating)。表达式实现的语言与并发差异见[脚本与表达式](/framework/externals/scripting)。


# 服务定位与所有权

从 Discussions 服务之间的依赖解释定位范围与生命周期。

服务定位应先明确由哪个容器提供、按哪个契约查找，以及谁负责释放。Discussions 的服务通常从自身 ServiceProvider 或模板提供者的 Services 解析依赖。

## 解析关联业务服务

来源：[src/Services/ThreadService.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/ThreadService.cs#L54)（节选；上下文见源文件）。

{% code title="ThreadService.cs" %}

```csharp
public PostService Posting
{
	get
	{
		if(_posting == null)
			_posting = this.ServiceProvider.ResolveRequired<PostService>();

		return _posting;
	}
}
#endregion
```

{% endcode %}

ThreadService 缓存容器返回的 PostService 引用。这里没有创建新的服务范围，也不在每次业务操作后释放它。若把请求可变状态保存在被共享的服务上，单纯使用容器并不能避免串请求。

## 模板提供者使用同一业务入口

来源：[src/Features/Archiving/UserDataTemplateModelProvider.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Features/Archiving/UserDataTemplateModelProvider.cs#L61)（节选；上下文见源文件）。

{% code title="UserDataTemplateModelProvider.cs" %}

```csharp
var data = argument switch
{
	string key => this.Services.ResolveRequired<UserService>().Get(key, schema),
	IModel model => this.Services.ResolveRequired<UserService>().Select(Criteria.Transform(model), schema),
	_ => this.Services.ResolveRequired<UserService>().Select(null, schema),
};
```

{% endcode %}

按键查询与按条件选择都通过 UserService。模板装配不自行连接数据库，从而能够复用服务层约束。调用者仍应检查用户筛选范围和导出权限。

## 模块依赖与缺失服务

MessageSendCommand 的 ServiceDependency 指定 Provider 为 Discussions；访问器则由模块按名称取得。名称、特性注册、程序集扫描和插件挂载是不同环节，缺少任一步都可能导致定位失败。

框架服务解析还有按名称、匹配参数与标签等形式；Discussions 没有覆盖这些全部形式。完整来源见[服务](/framework/core/services)，不要为展示每个重载额外创建一套论坛服务。


# 分布式锁

通过 Redis 多进程范例理解锁竞争、租约、续期、栅栏令牌与失去所有权后的处理。

Zongsoft.Services.Distributing 用来协调多个进程对同一资源的短时访问。Discussions 当前主要通过[数据库事务](/framework/data/transactions)维护主题、帖子和统计的一致性，没有直接使用分布式锁；本页采用 framework 中已有的 Redis 多进程范例和测试。

## 先区分三个概念

* **互斥**：同一锁键在有效期内只有一个持有者。锁键必须对应真实共享资源，不同进程使用不同键就无法互斥。
* **租约**：锁在服务端的有效时长。进程退出后服务端可以让锁自动过期，但租约结束不会中断旧进程里已经运行的代码。
* **栅栏令牌**：每次成功获取锁产生的递增编号。受保护的存储记录已经接受的最大编号，拒绝编号更小的迟到写入，从而限制失去锁的旧进程继续修改资源。

💡 锁协调执行者，事务保护数据库内的一组写入。需要同时保证多个表更新成功时，仍应使用事务；需要避免重复提交时，仍需幂等约束。

## 核心类型

| 类型                                                                                                                                                | 使用时关心的内容                                                       |
| ------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| [IDistributedLockManager](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/Distributing/IDistributedLockManager.cs)     | AcquireAsync 获取句柄、GetExpiryAsync 查询剩余租约、ReleaseAsync 按所有权令牌释放。 |
| [IDistributedLock](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/Distributing/IDistributedLock.cs)                   | EnterAsync 等待进入、RenewAsync 手动续期，释放句柄结束使用。                      |
| [DistributedLockOptions](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/Distributing/DistributedLockOptions.cs)       | Expiry 必须为正；RenewalInterval 指定自动续期间隔。                          |
| [DistributedLockBase](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/Distributing/DistributedLockBase.cs)             | 封装等待、持有时间、续期与释放；具体原子操作由实现负责。                                   |
| [IDistributedLockTokenizer](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/Distributing/IDistributedLockTokenizer.cs) | 生成字节所有权令牌 Token；这个随机令牌与递增的 FencingToken 用途不同。                  |

## 锁对象状态

| 属性                    | 含义                                          |
| --------------------- | ------------------------------------------- |
| Key / Token           | 被保护的资源键，以及本次获取的所有权令牌。                       |
| IsHeld / IsUnheld     | 句柄是否记录为持有状态；IsHeld 本身不检查是否超过租约。             |
| IsExpired             | 根据本地记录的持有时间判断是否过期。                          |
| IsLocked / IsUnlocked | 是否持有且未过期，以及相反状态。                            |
| FencingToken          | 本次成功获取的栅栏令牌；Redis 获取失败为零，不支持该能力的其他实现也可以返回零。 |

这些状态不是对服务端所有权的实时强一致查询。进程暂停、网络延迟或服务端状态变化之后，本地判断不能替代受保护存储的校验。

## 基本用法

Redis 的 slaver 范例循环竞争同一锁，按配置选择普通租约或自动续期，并把命令取消与总超时合并。下面是循环内取得锁和等待进入的原始片段：

来源：[framework/externals/redis/samples/distributedlock/slaver/RunCommand.cs](https://github.com/Zongsoft/framework/blob/main/externals/redis/samples/distributedlock/slaver/RunCommand.cs#L24)（节选；上下文见源文件）。

{% code title="RunCommand.cs" %}

```csharp
await using var locker = settings.RenewalInterval.HasValue ?
	await redis.AcquireAsync(Utility.Keys.Lock, new DistributedLockOptions(settings.Expiry) { RenewalInterval = settings.RenewalInterval }, linked.Token) :
	await redis.AcquireAsync(Utility.Keys.Lock, settings.Expiry, linked.Token);
await locker.EnterAsync(linked.Token);
```

{% endcode %}

settings、redis 和 linked 由同一 OnExecuteAsync 方法建立。await using 的作用域是一轮循环；该轮完成或抛出异常后会释放锁。EnterAsync 在尚未持有时查询剩余租约并等待，再尝试获取；调用方应传入可取消的等待凭证。

如果不准备等待，应检查刚取得的句柄是否 IsLocked，再决定是否开始工作。Redis 当前获取失败会返回未持有句柄；只检查非空不足以证明抢到锁。框架测试明确验证了这个区别：

来源：[framework/externals/redis/test/RedisDistributedLockTests.cs](https://github.com/Zongsoft/framework/blob/main/externals/redis/test/RedisDistributedLockTests.cs#L40)（节选；上下文见源文件）。

{% code title="RedisDistributedLockTests.cs" %}

```csharp
await using var first = await cache.AcquireAsync(key, TimeSpan.FromSeconds(2));
Assert.True(first.IsLocked);
Assert.True(first.FencingToken > 0);

await using var rejected = await cache.AcquireAsync(key, TimeSpan.FromSeconds(2));
Assert.True(rejected.IsUnheld);
Assert.Equal(0, rejected.FencingToken);
```

{% endcode %}

该片段位于 SuccessfulAcquisitions\_ReturnStrictlyIncreasingFencingTokens；完整测试还释放 first 后重新获取，并检查编号递增。cache 与 key 来自测试夹具，不是 Discussions 的配置。

## 续期与失去所有权

Redis 默认不自动续期。启用时，RenewalInterval 必须大于零且小于 Expiry；空值或零表示关闭，负值会被拒绝。实际测试以 300 毫秒租约、75 毫秒间隔演示续期：

来源：[framework/externals/redis/test/RedisDistributedLockTests.cs](https://github.com/Zongsoft/framework/blob/main/externals/redis/test/RedisDistributedLockTests.cs#L111)（节选；上下文见源文件）。

{% code title="RedisDistributedLockTests.cs" %}

```csharp
var options = new DistributedLockOptions(TimeSpan.FromMilliseconds(300))
{
	RenewalInterval = TimeSpan.FromMilliseconds(75),
};
await using var renewed = await cache.AcquireAsync("automatic", options);
```

{% endcode %}

这些较短时长用于测试，生产环境应根据调度延迟和网络往返设置。Redis 在首次获取成功或 EnterAsync 竞争成功后启动续期循环；释放句柄时停止并等待循环结束。

手动 RenewAsync 成功会重置本地持有时间；返回 false 时调用者应停止受保护操作。自动续期循环遇到失败或异常会结束，异常路径记录日志并清除持有状态。手动调用抛出异常时，同样应按所有权不确定处理，不能因为本地状态暂时仍为持有就继续写入。

{% hint style="warning" %}
🚨 当前基类的 EnterAsync 仅在 IsUnheld 时尝试获取。一个已经过期、但仍记录为 IsHeld 的旧句柄，不会通过再次 EnterAsync 自动获得新租约。应结束旧作用域，重新获取句柄。续期也不会自动取消已经开始的业务工作。
{% endhint %}

## Redis 实现

[RedisService.DistributedLock.cs](https://github.com/Zongsoft/framework/blob/main/externals/redis/src/RedisService.DistributedLock.cs) 先用带有效期的 SET NX 竞争锁键，成功后递增同一键后缀 :FENCE 对应的计数器。失败者获得零编号；分配编号发生异常时尝试按所有权令牌释放锁。

释放和续期分别执行比较 Token 后删除、比较 Token 后延长 TTL 的 Lua 脚本。因此旧进程不能仅凭相同 Key 删除新进程持有的锁。所有键还经过 RedisService.Namespace 前缀处理。

{% hint style="warning" %}
🚨 栅栏编号依赖对应 Redis 计数器的连续性。不要在仍有执行者工作时清理锁前缀或 :FENCE 计数器，也不要把这个单 Redis 实现理解为跨集群故障下的共识服务。
{% endhint %}

## 栅栏令牌怎样保护存储

多进程范例在进入临界区和模拟工作结束后各提交一次 FencingToken。expiry 场景故意让工作时间超过租约，使读者观察旧持有者的迟到写入；mutex 和 renew 则用于对照。

| 范例场景   | 观察目标                              |
| ------ | --------------------------------- |
| mutex  | 临界区短于租约，查看是否出现重叠进入。               |
| expiry | 临界区超过租约，观察 Violations 与 Stale 计数。 |
| renew  | 较长临界区配合自动续期，观察续期是否维持持有状态。         |

完整操作见 [Redis 分布式锁范例](https://github.com/Zongsoft/framework/blob/main/externals/redis/samples/distributedlock/README.zh-Hans.md)。它包含 master 与 slaver 两个真实项目，使用独立运行标识，并支持专用 Redis 连接。

范例 WriteFenceAsync 使用先读取再写入来演示令牌比较。实际受保护存储必须把“检查最大令牌”和“提交数据、更新最大令牌”做成一个原子操作，例如数据库条件更新或服务端脚本；直接复制分开的读写会留下并发窗口。一次范例结果不能证明任意暂停和网络故障下都安全。

## 获取 Redis 锁管理器

插件通过 RedisServiceProvider 注册具名的锁管理器提供器。真实调用方是微信 CredentialManager，它从应用服务中解析提供器，再按缓存配置名取得服务：

来源：[framework/externals/wechat/src/CredentialManager.cs](https://github.com/Zongsoft/framework/blob/main/externals/wechat/src/CredentialManager.cs#L76)（节选；上下文见源文件）。

{% code title="CredentialManager.cs" %}

```csharp
public static Services.Distributing.IDistributedLockManager Locker
{
	get => _locker ??= ApplicationContext.Current.Services.Resolve<IServiceProvider<Services.Distributing.IDistributedLockManager>>()?.GetService(GetCacheName());
	set => _locker = value;
}
```

{% endcode %}

配置入口是 /Externals/Redis/ConnectionSettings，命名服务的所有权和复用见[服务定位](/framework/core/services/locating)，插件部署见 [Redis](/framework/externals/projects/redis)。Discussions 没有现成的 Redis 锁连接，因此不能直接照抄其数据库名称作为 Redis 连接名。

## 真实场景

CredentialManager.GetCredentialAsync 在本地缓存、共享缓存都没有有效凭证后，为凭证刷新请求取得短租约。其当前实现只判断 locker 非空，没有检查 IsLocked 或调用 EnterAsync。结合 Redis 返回未持有句柄的行为，这段现有业务实现不能作为已经保证互斥的完整范例；接入时应核对并补齐持有状态处理。本文以 Redis slaver 和测试作为获取、等待、续期的主要示例。

## 使用建议

* 临界区尽量短，把用户交互、无界等待和批量任务拆出锁作用域。
* 传入取消凭证，并把等待上限与租约时间分别设置。
* 获取、续期或业务写入发生不确定结果时，按可能重复执行设计补偿和幂等处理。
* 使用作用域释放句柄；异步调用链优先使用 await using。
* 需要拒绝迟到写入时，把栅栏令牌校验落实到最终存储，不能只在应用进程中比较。

## 相关资源

* [服务定位与所有权](/framework/core/services/locating)
* [Redis 项目](/framework/externals/projects/redis)
* [事务与一致性](/framework/data/transactions)
* [Redis 锁集成测试](https://github.com/Zongsoft/framework/blob/main/externals/redis/test/RedisDistributedLockTests.cs)


# Zongsoft.Terminals

Zongsoft.Terminals 终端抽象、命令执行器和交互式控制台程序。

`Zongsoft.Terminals` 提供终端抽象、控制台终端实现和终端命令执行器，用于把 `Zongsoft.Components` 的命令模型运行在交互式控制台中。它让一个程序既可以像普通后台服务一样启动组件，也可以在控制台里输入命令、查看状态、执行维护动作和进入持续监听模式。

终端模型的核心关系是：`ITerminal` 负责输入、输出、样式、清屏和 Ctrl+C 中断事件；`ITerminalExecutor` 继承命令执行器，负责读取命令文本、解析命令表达式、维护当前命令节点并触发退出事件。

## 主要职责

* 定义 `ITerminal`、`ITerminalExecutor` 等终端抽象。
* 提供 `Terminal.Console` 控制台终端和 `Terminal.Default` 默认终端入口。
* 把命令树、命令表达式和终端输入循环组合成可交互的命令行程序。
* 支持彩色输出、ANSI 样式、清屏、重置和 Ctrl+C 中断处理。
* 提供 `Clear`、`Exit`、`Shell` 等控制台内置命令。
* 支持插件宿主把默认命令执行器替换为终端执行器，让插件命令可以在终端里运行。

## 关键类型

| 类型                       | 说明                                                                            |
| ------------------------ | ----------------------------------------------------------------------------- |
| `ITerminal`              | 终端接口，提供输入输出、错误输出、样式、清屏、重置和中断事件。                                               |
| `ITerminalExecutor`      | 终端命令执行器接口，继承命令执行器并提供 `Run(...)` / `RunAsync(...)`。                            |
| `Terminal`               | 静态入口，暴露 `Console`、`Default`、`Write(...)`、`WriteLine(...)`、`Clear(...)` 等快捷方法。 |
| `TerminalStyles`         | 终端样式重置选项，例如前景色、背景色和字体样式。                                                      |
| `Terminal.ExitException` | 用于终止终端命令循环的专用异常。                                                              |
| `Terminal.ExitEventArgs` | 终端退出事件参数，包含退出码。                                                               |

## 适用场景

* 为本地调试、运维工具、示例程序提供交互式命令入口。
* 在后台服务旁边提供 `start`、`stop`、`info`、`subscribe` 等维护命令。
* 把插件树中的命令挂到终端，形成可扩展的管理控制台。
* 为消息订阅、事件监听、设备采集观察等场景提供“运行直到 Ctrl+C 退出”的响应模式。

## 快速启动

Discussions 的实际命令是 MessageSendCommand：从命令选项构建站内信，解析收件人后交给 MessageService。它不创建自己的控制台循环，宿主通过命令树和插件装配提供执行入口。

来源：[src/Services/Commands/MessageSendCommand.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/Commands/MessageSendCommand.cs#L63)（节选；上下文见源文件）。

{% code title="MessageSendCommand.cs" %}

```csharp
protected override ValueTask<object> OnExecuteAsync(CommandContext context, CancellationToken cancellation)
{
	if(context.Arguments == null || context.Arguments.IsEmpty)
		throw new CommandException("Missing arguments of the command.");

	var content = context.Options.GetValue<string>(CONTENT_OPTION);
	var contentType = context.Options.GetValue<string>(CONTENTTYPE_OPTION);

	//根据内容类型解析得到真实内容
	content = GetContent(content, ref contentType);

	var message = Zongsoft.Data.Model.Build<Models.Message>(entity =>
	{
		entity.Content = content;
		entity.ContentType = contentType;
		entity.Referer = context.Options.GetValue<string>(SOURCE_OPTION);
		entity.Subject = context.Options.GetValue<string>(SUBJECT_OPTION);
		entity.MessageType = context.Options.GetValue<string>(MESSAGETYPE_OPTION);
	});

	if(this.Service.Send(message, GetUsers(context.Arguments)) > 0)
		return ValueTask.FromResult<object>(message);

	return ValueTask.FromResult<object>(null);
}
```

{% endcode %}

终端执行器默认带有 `Exit`、`Clear` 和 `Shell` 命令。业务命令可以通过命令树、插件加载器或 `CommandExecutorUtility.Command(...)` 这类辅助方法挂载。完整命令模型见：

{% content-ref url="/pages/NrKxcv3CuzLXR9b1lXTf" %}
[命令](/framework/core/components/commands)
{% endcontent-ref %}

## 插件宿主集成

在插件宿主中，终端插件会把 `/Workbench/Executor` 指向 `Terminal.Console.Executor`，并把它设为 `CommandExecutor.Default`。这样其它插件挂载到命令树上的命令，可以直接在终端宿主里输入执行。

来源：[framework/Zongsoft.Plugins/plugins/Terminal.plugin](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins/plugins/Terminal.plugin#L15)（节选；上下文见源文件）。

{% code title="Terminal.plugin" %}

```xml
<extension path="/Workbench">
	<!-- 将默认的命令执行器替换成控制台终端命令执行器 -->
	<object name="Executor" value="{static:Zongsoft.Terminals.Terminal.Console.Executor, Zongsoft.Core}">
		<object.property name="Default" target="{type:Zongsoft.Components.CommandExecutor, Zongsoft.Core}" value="{path:.}" />
	</object>
</extension>
```

{% endcode %}

这种方式适合把插件应用变成可交互的管理控制台。应用启动后，终端[工作台](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins/src/IWorkbenchBase.cs)会运行执行器；终端退出后，工作台随之关闭。

## 子命名空间

| 命名空间                          | 说明      |
| ----------------------------- | ------- |
| `Zongsoft.Terminals.Commands` | 终端内置命令。 |

## 类型

<table data-view="cards"><thead><tr><th></th><th data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td>Terminal：终端抽象、控制台终端、终端执行器、输出样式和响应式命令。</td><td><a href="/framework/core/terminals/terminal">终端</a></td></tr><tr><td>Commands：清屏、退出和 Shell 命令。</td><td><a href="/framework/core/terminals/commands">命令</a></td></tr></tbody></table>

## 相关资源

* [Terminals 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Terminals)
* [Terminal 插件](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins/plugins/Terminal.plugin)


# 终端

Zongsoft.Terminals 终端抽象、控制台终端和交互式命令模式。

`Zongsoft.Terminals` 把控制台程序抽象为终端和终端命令执行器。终端负责输入、输出、样式和中断事件；执行器继承命令执行模型，负责读取命令、执行命令和维护当前命令节点。

`Terminal.Console` 是核心库提供的默认控制台终端，内部封装 `System.Console`。`Terminal.Default` 默认指向控制台终端，也可以被宿主或测试代码替换成其它实现。

## 关键类型

| 类型                       | 说明                                                         |
| ------------------------ | ---------------------------------------------------------- |
| `ITerminal`              | 终端接口，继承 `ICommandOutlet`，提供输入、输出、错误输出、样式、清屏和重置。            |
| `ITerminalExecutor`      | 终端命令执行器接口，继承 `ICommandExecutor`，提供 `Run`/`RunAsync` 和退出事件。 |
| `Terminal`               | 静态入口，提供 `Default`、`Console`、输出和样式快捷方法。                     |
| `ConsoleTerminal`        | 控制台终端实现，封装 `System.Console`、ANSI 样式和 Ctrl+C 中断。            |
| `ConsoleExecutor`        | 控制台命令执行器，读取终端输入并执行命令。                                      |
| `TerminalStyles`         | 终端样式重置选项。                                                  |
| `Terminal.ExitException` | 终端命令用于退出命令循环的专用异常。                                         |

## 启动终端

终端执行器运行时会打印闪屏、进入命令循环并等待用户输入。可以传入纯文本闪屏，也可以传入 `CommandOutletContent` 组合多色输出。

来源：[framework/Zongsoft.Net/samples/server/Program.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Net/samples/server/Program.cs#L46)（节选；上下文见源文件）。

{% code title="Program.cs" %}

```csharp
var splash = CommandOutletContent.Create()
	.AppendLine(CommandOutletColor.Yellow, new string('·', 50))
	.AppendLine(CommandOutletColor.Blue, "Welcome to the TCP Server.".Justify(50))
	.AppendLine(CommandOutletColor.Yellow, new string('·', 50));

await executor.RunAsync(splash);
```

{% endcode %}

上面是 framework TCP 服务端样例的启动片段，executor 及 start、stop、info 命令在同一 Program.cs 前部创建；完整入口见[常规通讯](/framework/core/communication/general)。Discussions 自己的站内信命令复用宿主执行器，不重复创建终端。

命令循环会把输出编码设置为 UTF-8。每次读取命令前会重置终端样式，并显示提示符：根节点下显示 `$>`，进入某个命令节点后显示该节点完整路径。

## 命令循环

{% stepper %}
{% step %}

## 初始化终端

`ConsoleExecutor.RunAsync(...)` 设置输出编码，打印默认或自定义闪屏。
{% endstep %}

{% step %}

## 读取输入

终端重置后显示当前命令节点提示符，然后从 `Input.ReadLine()` 读取命令文本。
{% endstep %}

{% step %}

## 执行命令

输入非空时调用继承自 `CommandExecutor` 的 `ExecuteAsync(...)`。如果命令节点还有子节点，终端执行器会把当前节点切换到该命令节点，后续相对路径会以该节点为锚点查找。
{% endstep %}

{% step %}

## 处理退出和异常

`ExitCommand` 抛出终端退出异常后触发退出事件；普通异常会以红色错误信息输出到终端。
{% endstep %}
{% endstepper %}

## 交互式响应模式

`Terminal.ReactiveAsync(...)` 让命令进入“运行中等待中断”的模式。它会检查当前执行器是否是终端执行器，挂载终端的 `Aborting` 事件，然后等待 Ctrl+C 释放信号量，最后执行退出回调。

来源：[framework/Zongsoft.Commands/src/Messaging/QueueSubscribeCommand.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Commands/src/Messaging/QueueSubscribeCommand.cs#L52)（节选；上下文见源文件）。

{% code title="QueueSubscribeCommand.cs" %}

```csharp
protected override ValueTask<object> OnExecuteAsync(CommandContext context, CancellationToken cancellation) =>
	context.ReactiveAsync(this.OnEnterAsync, this.OnExitAsync, cancellation);
```

{% endcode %}

`Zongsoft.Commands` 中的消息队列订阅命令就是这种结构：进入时订阅队列并持续输出消息，用户按 Ctrl+C 后退出并注销订阅。

{% hint style="info" %}
`ReactiveAsync(...)` 只支持终端执行器环境。普通命令执行器没有终端中断事件，调用该方法会抛出不支持异常。
{% endhint %}

## 输出样式

`ITerminal` 继承 `ICommandOutlet`，因此命令可以使用 `CommandOutletContent` 组合颜色、样式和文本片段。控制台终端会把这些样式转换为 ANSI 转义序列输出。

来源：[framework/Zongsoft.Core/samples/memorycache/Program.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/samples/memorycache/Program.cs#L78)（节选；上下文见源文件）。

{% code title="Program.cs" %}

```csharp
private static void Cache_Evicted(object sender, CacheEvictedEventArgs e)
{
	var content = CommandOutletContent.Create(CommandOutletColor.Magenta, "** Evicted **\t")
		.Append(CommandOutletColor.DarkGreen, Now + ' ')
		.Append(CommandOutletColor.Blue, $"[{e.Reason}] ")
		.Append(CommandOutletColor.DarkYellow, e.Key.ToString())
		.Append(CommandOutletColor.DarkGray, "=")
		.Append(CommandOutletColor.DarkYellow, e.Value?.ToString());

	if(e.State != null)
		content
			.Append(CommandOutletColor.DarkGray, " (")
			.Append(CommandOutletColor.Cyan, e.State.ToString())
			.Append(CommandOutletColor.DarkGray, ")");

	Terminal.WriteLine(content);
}
```

{% endcode %}

这段输出来自 [核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core) 的 memorycache 交互样例，Now 是同一类的时间文本属性，e 来自缓存淘汰事件。命令内部通常优先使用 `context.Output`，这样同一命令既可以在终端中输出，也可以被其它命令执行器复用。只有确定输出目标就是当前默认终端时，才直接使用 `Terminal.WriteLine(...)`。

## 终端扩展方法

`Terminal.Utility` 提供了从命令执行器或命令上下文取得终端的扩展方法：

| 方法                                             | 说明                       |
| ---------------------------------------------- | ------------------------ |
| `GetTerminal(this ICommandExecutor executor)`  | 如果执行器是终端执行器，返回所属终端。      |
| `GetTerminal(this CommandContextBase context)` | 从命令上下文取得所属终端。            |
| `TryGetTerminal(...)`                          | 尝试取得终端，适合命令兼容终端和非终端两种环境。 |
| `ReactiveAsync(...)`                           | 让命令进入 Ctrl+C 退出的响应式模式。   |

## 使用建议

终端程序适合本地调试、插件管理、样例程序和运维工具。面向多用户或远程管理的场景，仍需要额外考虑权限、审计、命令白名单和外部命令限制。

如果命令需要长期运行并等待用户中断，优先使用 `ReactiveAsync(...)` 包住进入和退出逻辑；如果命令只执行一次动作，直接实现普通 `CommandBase<CommandContext>` 即可。

## 参考实现

* [Terminals 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Terminals)
* [QueueSubscribeCommand.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Commands/src/Messaging/QueueSubscribeCommand.cs)
* [ZeroMQ 终端示例](https://github.com/Zongsoft/framework/blob/main/messaging/zero/samples/server/Program.cs)


# 命令

Zongsoft.Terminals.Commands 终端内置命令。

`Zongsoft.Terminals.Commands` 提供控制台终端默认装载的内置命令。`ConsoleExecutor` 构造时会把这些命令加入根命令节点，因此终端程序启动后即可使用。

这些命令都是围绕终端环境设计的。`Exit` 和 `Shell` 需要从命令上下文取得当前终端；如果在普通命令执行器中直接运行，通常会抛出不支持异常或没有实际效果。

## 命令列表

| 命令      | 类型             | 说明                                            |
| ------- | -------------- | --------------------------------------------- |
| `Clear` | `ClearCommand` | 调用当前终端的 `Clear()` 清屏。                         |
| `Exit`  | `ExitCommand`  | 退出终端命令循环，支持 `--yes` 或 `-y` 跳过确认。              |
| `Shell` | `ShellCommand` | 在 Windows 上通过 `cmd.exe /C` 执行外部命令，并把标准输出写回终端。 |

## ClearCommand

`ClearCommand` 会尝试使用当前终端的清屏能力清理控制台输出。控制台终端会先写入 ANSI 清屏序列，再调用 `System.Console.Clear()`。

{% code title="清屏" %}

```bash
Clear
```

{% endcode %}

如果命令不是在终端执行器中运行，`ClearCommand` 取不到终端时不会清屏，也不会输出额外结果。

## ExitCommand

`ExitCommand` 只支持在终端执行器中运行。未指定 `--yes` 时会向终端输出确认提示，用户输入 `yes` 后抛出终端退出异常，由 `ConsoleExecutor` 捕获并触发退出事件。

{% code title="退出终端" %}

```bash
Exit --yes
```

{% endcode %}

也可以使用短选项：

{% code title="使用短选项退出" %}

```bash
Exit -y
```

{% endcode %}

如果没有指定 `--yes`，命令会读取终端输入；只有输入 `yes` 才会退出。其它输入会返回命令循环。

## ShellCommand

`ShellCommand` 用于在终端内临时执行系统命令。它支持 `timeout` 选项，默认值为 `1s`。当前实现面向 Windows 控制台，会通过 `cmd.exe /C` 执行第一个命令参数；非 Windows 平台会抛出不支持异常。

{% code title="执行 Shell 命令" %}

```bash
Shell "dotnet --info" --timeout:10s
```

{% endcode %}

如果进程在超时时间内没有退出，命令返回 `-1`；否则返回外部进程退出码。`timeout` 小于或等于零时，当前实现会使用 30 秒等待时间。

{% hint style="warning" %}
`ShellCommand` 会执行外部系统命令，生产环境或多用户终端中应谨慎开放该命令。更稳妥的做法是只挂载明确的业务命令，而不是把任意 Shell 能力暴露给终端用户。
{% endhint %}

## 命令资源

内置命令的显示名称、描述和选项说明来自资源文件，例如 `ExitCommand.Name`、`ExitCommand.Description`、`ShellCommand.Options.Timeout`。如果终端程序需要多语言展示，建议优先维护资源，而不是在命令逻辑里硬编码展示文本。

## 使用建议

内置命令适合本地终端和开发工具。发布给生产环境或远程用户的终端，应根据权限和运维策略决定是否保留 `Shell`，也可以在启动时调整命令树，只暴露业务允许的命令集合。

## 参考实现

* [ClearCommand.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Terminals/Commands/ClearCommand.cs)
* [ExitCommand.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Terminals/Commands/ExitCommand.cs)
* [ShellCommand.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Terminals/Commands/ShellCommand.cs)


# Zongsoft.Text

Zongsoft.Text 文本模板接口、正则验证器和常用文本匹配用法。

`Zongsoft.Text` 提供轻量的文本模板抽象和正则验证器，用于把“文本如何生成、如何格式化、如何验证”从具体业务代码中拆出来。它适合处理命令参数、配置值、短信模板参数、URI 判断、邮箱和常见中文文本格式验证等基础场景。

当前核心类型集中在两个方向：一类是 `ITemplate` / `ITemplateFormatter`，用于定义模板和模板数据格式化扩展点；另一类是 `ITextRegular` / `TextRegular`，用于封装可复用的正则匹配器，并在匹配成功时提取规范化结果。

## 主要职责

* 定义文本模板接口，让业务模块可以按模板名称把数据求值为文本。
* 定义模板格式化器接口，让外部服务模板参数可以在发送前转换成供应商需要的结构。
* 提供 `TextRegular` 正则封装，统一处理匹配、失败容错和结果提取。
* 提供邮箱、URL、HTTP/FTP URL、中国手机号、固定电话、身份证号、邮政编码等常用验证器。
* 让命令、配置、消息、资源输出和外部通讯可以复用同一套轻量文本处理约定。

## 关键类型

| 类型                    | 说明                                        |
| --------------------- | ----------------------------------------- |
| `ITemplate`           | 文本模板接口，提供模板名称和 `Evaluate(...)` 求值入口。      |
| `ITemplateFormatter`  | 模板数据格式化器接口，按模板名称、数据和附加参数返回格式化后的模板数据。      |
| `ITextRegular`        | 文本匹配接口，继承可匹配对象约定，并支持输出匹配结果。               |
| `TextRegular`         | 正则文本匹配器，提供 `Match(...)` 和 `IsMatch(...)`。 |
| `TextRegular.Web`     | Web 常用验证器，目前提供邮箱验证器。                      |
| `TextRegular.Uri`     | URL 验证器，包含任意协议、HTTP、FTP 和按协议创建验证器。        |
| `TextRegular.Chinese` | 中国手机号、固定电话、身份证号和邮政编码验证器。                  |

## 正则验证器

`TextRegular` 用一个正则表达式构造可复用的文本验证器。它内部启用编译、忽略大小写、忽略模式空白和显式捕获，并设置匹配超时时间。调用 `Match(...)` 只返回是否匹配，调用 `IsMatch(...)` 则会在匹配成功时返回结果文本。

来源：[framework/Zongsoft.Core/src/Text/TextRegular.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Text/TextRegular.cs#L115)（节选；上下文见源文件）。

{% code title="TextRegular.cs" %}

```csharp
public static readonly TextRegular Email = new(@"^\s*(?<value>[A-Za-z0-9]([-_\.]?[A-Za-z0-9]+)*@([A-Za-z0-9]+([-_]?[A-Za-z0-9]+)*)(\.[A-Za-z0-9]+([-_]?[A-Za-z0-9]+)*)*\.[A-Za-z]+)\s*$");
```

{% endcode %}

`IsMatch(...)` 的结果提取有一个重要约定：如果正则包含名为 `value` 的捕获组，则会把该组所有捕获值连接成结果；使用结果提取时，应显式定义 value 组；当前实现对缺少该组的处理不能作为返回完整匹配文本的保证。这个约定适合把用户输入中的空格、分隔符、国家码等非核心内容去掉，返回更适合保存或后续处理的规范化值。

来源：[framework/Zongsoft.Core/src/Text/TextRegular.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Text/TextRegular.cs#L183)（节选；上下文见源文件）。

{% code title="TextRegular.cs" %}

```csharp
public static readonly TextRegular Cellphone = new(@"^\s*((\+|00)86\s*[-\.]?)?\s*(?<value>1\d{2})(?<separator>(\s*)|(-?))(?<value>\d{4})(?<separator>(\s*)|(-?))(?<value>\d{4})\s*$");
```

{% endcode %}

如果只需要判断某个字符串是不是 URL，可以使用 `Match(...)`：

来源：[framework/Zongsoft.Core/src/IO/FileSystem.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/IO/FileSystem.cs#L78)（节选；上下文见源文件）。

{% code title="FileSystem.cs" %}

```csharp
public static string GetUrl(string virtualPath)
{
	if(string.IsNullOrWhiteSpace(virtualPath))
		return virtualPath;

	//如果传入的虚拟路径参数是一个URI格式，则直接返回它作为结果
	if(Zongsoft.Text.TextRegular.Uri.Url.Match(virtualPath))
		return virtualPath;

	return GetFileSystem(virtualPath, false, out Path path)?.GetUrl(path);
}
```

{% endcode %}

框架中的文件系统工具就使用 `TextRegular.Uri.Url.Match(...)` 判断传入的虚拟路径是否已经是 URI；如果是 URI，就直接返回原始地址，而不是继续按本地文件系统路径解析。

## 预置验证器

| 验证器                                  | 用途                       |
| ------------------------------------ | ------------------------ |
| `TextRegular.Web.Email`              | 验证并提取邮箱地址。               |
| `TextRegular.Uri.Url`                | 验证任意协议 URL。              |
| `TextRegular.Uri.Http`               | 验证 `http` 或 `https` URL。 |
| `TextRegular.Uri.Ftp`                | 验证 `ftp` 或 `ftps` URL。   |
| `TextRegular.Uri.GetRegular(scheme)` | 按指定协议创建并缓存 URL 验证器。      |
| `TextRegular.Chinese.Cellphone`      | 验证并规范化中国手机号。             |
| `TextRegular.Chinese.Telephone`      | 验证并规范化中国固定电话。            |
| `TextRegular.Chinese.IdentityNo`     | 验证并规范化中国身份证号码。           |
| `TextRegular.Chinese.PostalCode`     | 验证并提取中国邮政编码。             |

这些验证器更适合做输入格式确认和基础规范化，不应当被当成完整业务校验。例如身份证号码是否真实存在、手机号是否已实名、邮箱域名是否可投递，都需要交给更具体的业务流程或外部服务确认。

## 自定义验证器

下面是框架实际构造入口。业务模块可以用自己的正则创建 TextRegular。只要在正则中使用 `(?<value>...)` 捕获真正需要的片段，调用方就能拿到规范化后的结果。

来源：[framework/Zongsoft.Core/src/Text/TextRegular.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Text/TextRegular.cs#L43)（节选；上下文见源文件）。

{% code title="TextRegular.cs" %}

```csharp
public TextRegular(string pattern)
{
	if(string.IsNullOrWhiteSpace(pattern))
		throw new ArgumentNullException(nameof(pattern));

	_regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase, TimeSpan.FromSeconds(3));
}
```

{% endcode %}

{% hint style="info" %}
`TextRegular` 的匹配失败会返回 `false`，包括空文本、正则匹配失败和匹配异常。它适合做宽容的输入判断；如果调用方需要暴露具体错误原因，应在外层补充更明确的提示。
{% endhint %}

## 模板接口

当前仓库没有现成的 ITemplate 求值实现，下面保留真实接口契约，不把未实现的通知类作为可运行范例。ITemplate 表示一个可求值的文本模板。它只规定模板名称和求值入口，不限定模板语法，因此实现可以来自字符串模板、脚本模板、资源文件、外部服务模板或自定义模板引擎。

来源：[framework/Zongsoft.Core/src/Text/ITemplate.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Text/ITemplate.cs#L37)（节选；上下文见源文件）。

{% code title="ITemplate.cs" %}

```csharp
public interface ITemplate
{
	/// <summary>获取模板名称。</summary>
	string Name { get; }

	/// <summary>应用模板。</summary>
	/// <param name="data">待应用的模板数据。</param>
	/// <param name="arguments">附加参数集。</param>
	/// <returns>返回应用后的文本。</returns>
	string Evaluate(object data, params object[] arguments);
}
```

{% endcode %}

`ITemplateFormatter` 更偏向“发送前格式化”。它接收模板名称、原始数据和附加参数，返回格式化后的数据对象。这个返回值可以继续被序列化为 JSON、查询字符串或供应商 API 需要的参数结构。

来源：[framework/externals/aliyun/src/Telecom/Phone.cs](https://github.com/Zongsoft/framework/blob/main/externals/aliyun/src/Telecom/Phone.cs#L126)（节选；上下文见源文件）。

{% code title="Phone.cs" %}

```csharp
//尝试进行模板数据格式化
if(!string.IsNullOrEmpty(template.Formatter) && this.ServiceProvider.Resolve(template.Formatter) is ITemplateFormatter formatter)
{
	argument.Parameter = formatter.Format(template.Name, argument.Parameter, argument.Extra);
```

{% endcode %}

在外部通讯场景中，模板配置可以指定一个格式化器名称。发送前，服务按名称解析 `ITemplateFormatter`，再把模板参数交给格式化器处理。例如阿里云语音和短信发送流程会在提交请求前调用格式化器，把业务参数转换成供应商模板参数。

## 典型用例

{% tabs %}
{% tab title="输入校验" %}
命令、配置和 Web 表单可以用 `TextRegular` 做第一层格式判断。例如命令参数输入手机号时，先用 `TextRegular.Chinese.Cellphone.IsMatch(...)` 提取规范化号码，再进入认证、发送或业务查询流程。
{% endtab %}

{% tab title="路径判断" %}
文件系统、资源引用和下载地址处理时，可以先用 `TextRegular.Uri.Url.Match(...)` 判断文本是否已经是 URL。如果是 URL，就按远程地址处理；否则继续按本地路径或虚拟路径解析。
{% endtab %}

{% tab title="模板参数" %}
短信、语音、通知和消息队列等外部服务常常要求模板参数符合特定格式。可以把格式转换放到 `ITemplateFormatter`，让业务代码只提交原始数据，格式化器负责适配不同模板或供应商。
{% endtab %}
{% endtabs %}

## 使用建议

`TextRegular` 适合做轻量格式判断和结果提取。对于需要强安全保证的场景，例如身份核验、支付参数、权限表达式或外部回调签名，应在正则验证后继续做业务校验和安全校验。

模板接口适合做框架级扩展点，而不是规定某一种模板语法。项目可以根据实际需要选择字符串插值、资源文件、脚本模板或第三方模板引擎，只要最终实现 `ITemplate` 或 `ITemplateFormatter` 即可。

如果正则需要复用，建议定义成静态只读验证器，避免在热路径上重复构造；如果协议或格式由配置决定，可以使用 `TextRegular.Uri.GetRegular(...)` 这类带缓存的入口。

## 相关资源

* [Text 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core/src/Text)
* [FileSystem.cs URL 判断用例](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/IO/FileSystem.cs)
* [阿里云电信模板格式化用例](https://github.com/Zongsoft/framework/blob/main/externals/aliyun/src/Telecom/Phone.cs)


# Zongsoft.Versioning

Zongsoft.Versioning 命名空间中的语义化版本与数值版本号。

`Zongsoft.Versioning` 提供 [Zongsoft.Core](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core) 中的版本表达模型。它把“用于发布、展示和兼容性判断的语义化版本”和“用于排序、范围查询和持久化的数值版本号”分开，避免同一个类型同时承担文本语义和整数编码两种职责。

## 何时使用

`Version` 表示语义化版本，格式为 `major.minor.patch-label+extra`。它适合描述应用、模块、协议或包的公开版本，例如 `1.2.3`、`1.2.3-alpha.1`、`1.2.3-alpha.1+build.5`。

`Version.Number` 表示四段式数值版本号，包含 `Major`、`Minor`、`Patch`、`Revision` 四个 `ushort` 字段。它适合保存到数据库、配置项或需要稳定排序的持久化字段中，也可以与旧的整数版本值协作。

Discussions 没有直接操作此版本类型；下面采用 [核心类库](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Core) 的 VersionTest 和 VersionNumberTest，保留测试输入及输出断言。它们验证版本规则，不表示 Discussions 当前包版本。

## 语义化版本

`Version` 是引用类型，构造时必须提供非负的 `Major`、`Minor` 和 `Patch`。`Label` 和 `Extra` 会去掉首尾空白；空白值会视为没有标签或额外信息。

来源：[framework/Zongsoft.Core/test/Versioning/VersionTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Versioning/VersionTest.cs#L94)（节选；上下文见源文件）。

{% code title="VersionTest.cs" %}

```csharp
public void TestFormat()
{
	var version = new Version(1, 2, 3, "alpha.1", "build.5");

	Assert.Equal("1.2.3-alpha.1", version.ToString());
	Assert.Equal("1.2.3-alpha.1", version.ToString("N"));
	Assert.Equal("1.2.3-alpha.1+build.5", version.ToString("F"));
	Assert.Equal("1.2.3", version.ToString("V"));
	Assert.Equal("alpha.1", version.ToString("R"));
	Assert.Equal("build.5", version.ToString("M"));
	Assert.Equal("1.2.3.0", version.ToString("x.y.z.r"));
}
```

{% endcode %}

解析要求数字部分正好三段，并遵循语义化版本的基本约束：数字段不能带前导零；标签和额外信息由点分隔的标识符组成，标识符只能包含字母、数字和连字符；标签中的纯数字标识符不能带前导零。额外信息允许类似 `build.01` 这样的内容。

比较时先比较 `Major`、`Minor`、`Patch`。数字相同时，没有 `Label` 的稳定版本高于有 `Label` 的预发布版本；标签按点分段比较，数字标识符按数值比较，数字标识符低于非数字标识符，文本比较忽略大小写。`Extra` 只作为构建或附加信息输出，不参与相等性和大小比较。

| 成员                             | 说明                          |
| ------------------------------ | --------------------------- |
| `Major`、`Minor`、`Patch`        | 语义化版本的三段数字。                 |
| `Label`                        | 预发布标签，对应语义化版本中的 `-alpha.1`。 |
| `Extra`                        | 额外信息，对应语义化版本中的 `+build.5`。  |
| `HasLabel()` / `HasExtra()`    | 判断并读取可选的标签或额外信息。            |
| `Parse(...)` / `TryParse(...)` | 从文本解析 `Version`。            |
| `CompareTo(...)`               | 按语义化版本优先级比较。                |
| `ToString(format)`             | 按格式字符输出不同形态的版本文本。           |

### 格式化

`Version.ToString()` 默认等同于 `ToString("N")`，会输出数字版本和预发布标签，但不输出 `Extra`。如果需要保留构建信息，应使用 `ToString("F")`。

| 格式          | 输出                               |
| ----------- | -------------------------------- |
| `N`         | 规范化版本：`1.2.3` 或 `1.2.3-alpha.1`。 |
| `V`         | 仅数字版本：`1.2.3`。                   |
| `F`         | 完整版本：`1.2.3-alpha.1+build.5`。    |
| `R` / `L`   | 仅标签。                             |
| `M` / `E`   | 仅额外信息。                           |
| `x`、`y`、`z` | 分别输出主版本、次版本和修订号。                 |
| `r`         | 保留的第四段，当前恒为 `0`。                 |

{% hint style="warning" %}
`Version` 的 JSON 转换器会写出 `ToString("F")`，因此会保留 `Extra`；类型转换器转换为字符串时使用默认 `ToString()`，不会输出 `Extra`。
{% endhint %}

## 数值版本号

`Version.Number` 是值类型，保留旧四段式版本值的使用场景。它可以从 `1.2`、`1.2.3`、`1.2.3.4` 解析；缺失的 `Patch` 或 `Revision` 会补为 `0`，但不支持单段版本和超过 `ushort` 范围的版本段。

来源：[framework/Zongsoft.Core/test/Versioning/VersionNumberTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Versioning/VersionNumberTest.cs#L172)（节选；上下文见源文件）。

{% code title="VersionNumberTest.cs" %}

```csharp
public void TestNumericConversion()
{
	var version = new Version.Number(1, 2, 3, 4);
	const ulong Packed = 0x0001_0002_0003_0004UL;

	Assert.Equal(Packed, (ulong)version);
	Assert.Equal((long)Packed, (long)version);
	Assert.Equal(version, (Version.Number)Packed);
	Assert.Equal(version, (Version.Number)(long)Packed);

	version = new Version.Number(ushort.MaxValue, ushort.MaxValue, ushort.MaxValue, ushort.MaxValue);
	Assert.Equal(ulong.MaxValue, (ulong)version);
	Assert.Equal(-1L, (long)version);
	Assert.Equal(version, (Version.Number)(-1L));
}
```

{% endcode %}

整数形式按 `Major`、`Minor`、`Patch`、`Revision` 四个 `ushort` 依次打包到 64 位无符号整数中，因此数值排序、版本比较和持久化结果可以保持一致。`Version.Number` 也支持与 `long`、`ulong`、`System.Version` 隐式转换，便于兼容旧数据和 .NET 标准版本类型。

| 能力                             | 说明                                                  |
| ------------------------------ | --------------------------------------------------- |
| `IsZero`                       | 判断是否为 `0.0.0.0`。                                    |
| `Parse(...)` / `TryParse(...)` | 从二段、三段或四段数字文本解析。                                    |
| 比较运算符                          | 支持 `==`、`!=`、`>`、`>=`、`<`、`<=`。                     |
| 整数转换                           | 可隐式转换为 `ulong`、`long`，也可从这两种整数恢复。                   |
| `System.Version` 转换            | 可与 `System.Version` 互转；没有修订号时输出三段 `System.Version`。 |
| JSON 转换                        | 写出字符串形式；读取时支持字符串版本号，也支持 64 位整数。                     |

{% hint style="warning" %}
`Version.Number` 每段最大值为 `65535`。如果版本需要预发布标签、构建元数据或超过该范围的数字段，应使用 `Version` 或专门的版本模型，而不是把这些信息塞进四段数字中。
{% endhint %}

## 迁移建议

旧代码如果只是比较 `1.2.3.4` 这类四段数字，或者依赖整数持久化，通常只需要把命名空间和类型改为 `Zongsoft.Versioning.Version.Number`。

上面的 TestNumericConversion 就是整数往返兼容用例；跨旧版本迁移时，使用现有持久化值验证每一段恢复结果，再决定是否调整字段类型。

如果旧字段原本保存的是面向用户的发布版本，且需要表达 `alpha`、`beta`, `preview`、`rc` 或构建信息，建议改用 `Version`，并明确是否需要把 `Extra` 写入序列化结果。

## 参考实现

* [Version.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Versioning/Version.cs)
* [Version.Number.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Versioning/Version.Number.cs)
* [VersionTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Versioning/VersionTest.cs)
* [VersionNumberTest.cs](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Versioning/VersionNumberTest.cs)


# 插件框架

Zongsoft.Plugins 插件框架的职责和核心概念。

![插件框架](https://847714710-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FdTV8LtunzSisrDXg3zNv%2Fuploads%2Fgit-blob-9196bec8b8df6c12712d5b0dd515deeb0d57f7cf%2Fzongsoft-plugins-cover.png?alt=media)

[`Zongsoft.Plugins`](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Plugins) 是 Zongsoft 插件化应用的核心库。它把应用拆成可独立部署、可声明依赖、可挂载能力的插件模块，让终端程序、后台服务、Web 应用和富客户端共享一套扩展模型。

## 核心概念

* 插件文件：使用 `*.plugin` 描述程序集、依赖、构件、解析器和扩展点。
* 插件目录：宿主启动时默认扫描应用目录下的 `plugins/` 目录。
* 插件树：所有扩展点会被组织为一棵路径树，例如 `/Workbench/Data/Drivers`。
* 构件：插件树上的可构建对象，由 `object`、`lazy`、`expose` 等构建器创建或暴露。
* 应用上下文：运行时中表示应用、环境、模块、服务、事件和[工作器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)的上下文。
* [应用模块](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationModule.cs)：由应用显式定义并挂载的业务或基础设施边界，可以拥有自己的服务域和事件注册表；不与插件自动一一对应。

## 与宿主程序的关系

宿主程序不是业务模块。它负责建立 .NET Host、配置源和服务容器；插件框架负责加载 `plugins/` 目录中的插件文件，再由插件向应用注册业务能力。

```mermaid
flowchart LR
	A["宿主程序"] --> B["插件框架"]
	B --> C["plugins 目录"]
	C --> D["*.plugin"]
	D --> E["插件树"]
	E --> F["模块 / 服务 / 命令 / 事件 / API"]
```

## 加载结果

插件加载完成后，运行时会形成三层结果：

* 插件集合：记录已加载的主插件、从插件和子插件。
* 插件树：把扩展点、构件和自定义对象挂载到统一路径。
* 服务容器：宿主程序集和插件程序集中的服务会被注册到应用服务容器。

默认的基础插件会挂载 `/Workbench`、`/Workbench/Configuration/ConnectionSettings`、`/Workbench/Diagnostics` 等节点；数据、Web、安全等插件会继续向这些节点添加驱动、过滤器、命令、事件处理器或服务。

## 典型视角

{% tabs %}
{% tab title="应用开发者" %}
关注插件能提供什么能力：服务、命令、Web API、后台[工作器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)、数据驱动或业务模块。通常只需要理解插件目录和部署结果。
{% endtab %}

{% tab title="插件作者" %}
关注 `*.plugin` 文件如何声明程序集、依赖、构件和扩展点。插件作者需要理解插件树路径和构件解析器。
{% endtab %}

{% tab title="宿主维护者" %}
关注 Host 如何启动、配置如何加载、插件目录在哪里、服务如何注册，以及插件加载失败时如何诊断。
{% endtab %}
{% endtabs %}

## 继续阅读

开始编写清单前，可以先阅读[插件化设计专题](https://docs.zongsoft.com/framework/pages/W18xChMa7LOngtHuNewi#插件化设计专题)，明确模块归属、扩展契约与交付责任。

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref">页面</th></tr></thead><tbody><tr><td><strong>插件应用模型</strong></td><td>宿主、应用上下文、模块和服务之间的关系。</td><td><a href="/framework/plugins/application-model">插件应用模型</a></td></tr><tr><td><strong>插件文件与加载</strong></td><td>插件文件、依赖、扩展点和加载策略。</td><td><a href="/framework/plugins/plugin-file">插件文件与加载</a></td></tr><tr><td><strong>宿主集成</strong></td><td>在 .NET Host 中启动插件式应用。</td><td><a href="/framework/plugins/hosting">宿主集成</a></td></tr><tr><td><strong>构件与服务</strong></td><td>构建器、解析器、插件树路径和服务发现。</td><td><a href="/framework/plugins/builtins-and-services">构件与服务</a></td></tr></tbody></table>

## 相关资源

* [Zongsoft.Plugins 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Plugins)
* [Zongsoft.Plugins README](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins/README.md)
* [Zongsoft.Plugins NuGet 包](https://www.nuget.org/packages/Zongsoft.Plugins)
* [framework 仓库](https://github.com/Zongsoft/framework)


# 插件应用模型

理解插件式应用中的宿主、应用上下文、模块和服务。

插件式应用由宿主程序、插件框架、插件树、应用上下文和[应用模块](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationModule.cs)组成。宿主负责“进程如何启动”，插件负责“能力如何进入应用”。

## 宿主程序

宿主程序负责启动进程并建立运行环境。它通常只包含 `Program.cs`、项目文件、部署脚本和基础配置。业务能力应尽量放入插件，而不是固化在宿主中。

框架提供两类常用启动入口：

* `Application.Daemon(...)`：用于后台服务、Worker 和常驻进程。
* `Application.Terminal(...)`：用于命令行、终端工具和交互式程序。

这些入口会创建 .NET Host，加载宿主 `.option` 配置，注册插件配置源，加载插件树并初始化应用上下文。

## 应用上下文

应用上下文表示当前运行中的应用实例，对应核心库中的 [`IApplicationContext`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationContext.cs)。它包含应用名称、版本、环境、配置、模块集合、服务提供器、事件管理器、[工作器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)和运行时属性。

Web 宿主中的 `/Application` 接口会返回当前应用上下文的基本信息。

## 模块

模块对应核心库中的 [`IApplicationModule`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationModule.cs)。模块有名称、版本、程序集、服务容器和属性集合；继承 `ApplicationModule<TEvents>` 的模块还可以拥有自己的事件注册表。

模块名称很重要：数据访问、服务注册、日志命名和模块隔离都会使用模块名作为边界。例如数据服务默认会根据类型所在程序集的 [`ApplicationModuleAttribute`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/ApplicationModuleAttribute.cs) 查找模块名，再获得对应的数据访问器。

Web 宿主中的 `/Modules` 接口可用于查看当前加载的模块信息。

## 服务

插件可以向应用注册服务。宿主启动时会扫描宿主程序集和插件清单中的程序集，把符合服务注册约定的类型加入服务容器；插件树中的 `/Workspace/Environment/Services` 节点也可以把声明式构件注册为单例服务。

模块也可以拥有自己的服务域。[`ApplicationModule`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/ApplicationModule.cs) 会基于应用服务容器创建模块级服务提供器，用于隔离或命名模块内部服务。

## 工作台与启动节点

默认插件会把常用运行时对象挂载到 `/Workbench` 下，例如：

* `/Workbench/Modules`：当前[应用模块](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationModule.cs)集合。
* `/Workbench/Services`：当前应用服务容器。
* `/Workbench/Events`：全局事件管理器。
* `/Workbench/Configuration/ConnectionSettings/Drivers`：连接设置驱动集合。
* `/Workbench/Startup`：启动时需要加载或运行的[工作器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)集合。

这使插件之间可以通过稳定路径发现能力，而不是直接引用彼此的实现类型。

## 从业务边界到运行实例

插件和模块没有自动的一一对应关系。模块对象由应用定义，并通过 `/Workbench/Modules` 加入应用；程序集上的 [`ApplicationModuleAttribute`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/ApplicationModuleAttribute.cs) 则参与类型的模块归属。两者应使用一致的模块名。完整声明和调用见[第一个业务插件](/get-started/first-business-plugin)。

模块容器优先解析模块服务，再回退应用共享服务，适合业务局部实现与公共基础设施协作。它不会自动建立独立进程、数据库或请求作用域，详见[基础概念](/overview/concepts#module-service-provider)。

## 启动与停止的职责

构建宿主期间先加载插件树并注册服务，随后初始化应用上下文。[工作台](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins/src/IWorkbenchBase.cs)首次构建时组织其子节点，并将 Startup 子树后置；宿主进入启动、停止阶段时打开和关闭工作台。

对象构造、应用初始化和持续工作是不同阶段。需要保持消息订阅或周期任务的组件应使用[工作器](/framework/core/components/worker)管理启动、取消和释放；不要依靠构造函数启动无法停止的后台任务。

{% hint style="warning" %}
🚨 插件节点构建失败不代表所有已发生的外部操作都能回滚。组件应控制构造副作用，并实现明确的停止和清理路径。
{% endhint %}

源码定位：[应用上下文](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Plugins/src/PluginApplicationContext.cs)、[模块容器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/ApplicationModule.cs)。


# 插件文件与加载

以 Discussions 清单解释程序集、依赖、扩展路径与构件。

Discussions 通过 .plugin 清单声明模块如何接入宿主。清单负责装配，NuGet 或 .deploy 负责交付，二者不能互相替代。

## 先加载程序集和依赖

来源：[src/Zongsoft.Discussions.plugin](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.plugin#L9)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.plugin" %}

```xml
<manifest>
	<assemblies>
		<assembly name="Zongsoft.Discussions" />
	</assemblies>

	<dependencies>
		<dependency name="Zongsoft.Data" />
		<dependency name="Zongsoft.Security" />
	</dependencies>
</manifest>
```

{% endcode %}

程序集包含模型、服务与模块入口；依赖项要求先装配数据引擎与安全模块。依赖名称是插件名称，不等同于自动下载对应 NuGet 包；宿主部署必须使它们实际存在。

## 暴露共享模块实例

来源：[src/Zongsoft.Discussions.plugin](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.plugin#L20)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.plugin" %}

```xml
<extension path="/Workbench/Modules">
	<object name="Discussions" value="{static:Zongsoft.Discussions.Module.Current, Zongsoft.Discussions}">
		<expose name="Accessor" value="{path:../@Accessor}">
			<expose name="Filters" value="{path:../@Filters}" />
		</expose>

		<expose name="Events" value="{path:../@Events}" />
		<expose name="Properties" value="{path:../@Properties}" />
	</object>
</extension>
```

{% endcode %}

static 表达式引用 Module.Current。expose 把已有对象的成员接入插件树；这里的相对 path 从构件上下文解析，便于后续继续挂载过滤器、事件和属性。

## 向访问器挂载查询过滤器

来源：[src/Zongsoft.Discussions.plugin](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.plugin#L35)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.plugin" %}

```xml
<extension path="/Workbench/Modules/Discussions/Accessor/Filters">
	<object name="PostFilter" type="Zongsoft.Discussions.Data.PostFilter, Zongsoft.Discussions" />
	<object name="ThreadFilter" type="Zongsoft.Discussions.Data.ThreadFilter, Zongsoft.Discussions" />
</extension>
```

{% endcode %}

过滤器运行时还受模型匹配特性控制；挂载成功不意味着任何查询都会执行同一过滤器。PostFilter 处理帖子，ThreadFilter 处理主题及其正文，见[数据服务](/framework/data/services)。

## 验证器与安全扩展

来源：[src/Zongsoft.Discussions.plugin](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.plugin#L31)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.plugin" %}

```xml
<extension path="/Workbench/Data/Validators">
	<object name="Discussions" type="Zongsoft.Discussions.Data.DataValidator, Zongsoft.Discussions" />
</extension>
```

{% endcode %}

验证器以 Discussions 命名，与模块访问器和映射容器的业务范围对应。身份质询器、转换器使用另外两个扩展路径，见[认证](/framework/security/authentication)。

## 排查加载失败

按文件是否部署、XML 是否有效、依赖是否存在、程序集是否可加载、类型和表达式是否可解析、目标扩展点是否存在的顺序检查。特别要区分插件清单文件名、插件 name 和模块名：它们在 Discussions 中相关联，但分别参与配置匹配、依赖排序和模块服务范围。


# 宿主集成

在 .NET Host 中启动插件式应用并加载插件目录。

宿主集成的目标是把标准 .NET Host 与 Zongsoft 插件框架连接起来。宿主仍然使用 [`IHost`](https://learn.microsoft.com/zh-cn/dotnet/api/microsoft.extensions.hosting.ihost) [*源码*](https://source.dot.net/#Microsoft.Extensions.Hosting.Abstractions/IHost.cs)、配置、依赖注入和生命周期事件；插件框架负责在 Host 构建过程中加载插件树、注册插件程序集服务，并创建应用上下文。

## 启动入口

Discussions 的业务代码放在插件中，进程由 hosting 项目承载。下面是现有守护宿主的实际入口，平台分支由项目编译符号选择。

插件框架提供 `Application.Daemon(...)` 和 `Application.Terminal(...)` 两组入口：

来源：[hosting/daemon/Program.cs](https://github.com/Zongsoft/hosting/blob/main/daemon/Program.cs#L9)（节选；上下文见源文件）。

{% code title="Program.cs" %}

```csharp
static void Main(string[] args)
{
	#if WINDOWS
	Zongsoft.Plugins.Hosting.Application
		.Daemon("zongsoft.daemon", [.. args, "host=daemon", "site=daemon"], builder =>
		{
			builder.Services.AddWindowsService(options => options.ServiceName = builder.Environment.ApplicationName);
		}).Run();
	#elif LINUX
	Zongsoft.Plugins.Hosting.Application
		.Daemon("zongsoft.daemon", [.. args, "host=daemon", "site=daemon"], builder =>
		{
			builder.Services.AddSystemd();
		}).Run();
	#else
	Zongsoft.Plugins.Hosting.Application
		.Daemon("zongsoft.daemon", [.. args, "host=daemon", "site=daemon"])
		.Run();
	#endif
}
```

{% endcode %}

`Daemon` 适合后台服务和常驻进程，`Terminal` 适合终端程序。两者都会加载宿主配置文件，再构建和初始化 Host。

## 配置加载顺序

宿主启动时会按应用名加载 `.option` 文件：

* `{ApplicationName}.option`
* `{ApplicationName}.{Environment}.option`
* `{ApplicationName}.{Host}.option`
* `{ApplicationName}.{Host}.{Environment}.option`
* `{ApplicationName}.{Site}.option`
* `{ApplicationName}.{Site}.{Environment}.option`

其中 `Environment` 来自 Host 环境名，`host` 和 `site` 来自配置节。应用名可由 Host 设置、`appsettings.json` 或入口程序集名确定。

{% hint style="info" %}
`.option` 是 Zongsoft 配置体系使用的 XML 配置文件。插件目录中的插件配置也会作为配置源加入应用配置。
{% endhint %}

## 插件加载过程

构建 Host 时，框架会执行以下动作：

1. 创建 `PluginOptions`，确定内容根、环境名和插件目录，并将插件配置源加入应用配置。
2. 通过 `PluginTree.Get(options).Load()` 加载插件树。
3. 注册宿主程序集、宿主引用程序集和插件清单程序集中的服务类型。
4. 添加默认 `System.Net.Http.HttpClient` 服务。
5. 注册 `/Workspace/Environment/Services` 下的声明式服务。
6. 构建 Host 并调用 `Initialize()` 初始化应用上下文。

如果 `plugins` 目录不存在，插件加载会失败并抛出目录不存在异常。部署宿主时应确保插件目录与宿主应用目录匹配。

{% content-ref url="/pages/f9kQtLlyX77jazHogmpe" %}
[插件文件与加载](/framework/plugins/plugin-file)
{% endcontent-ref %}

## 服务注册

插件程序集服务注册来自两个来源：

* 程序集扫描：宿主程序集、宿主引用程序集和插件 `manifest` 中声明的程序集。
* 插件树服务节点：`/Workspace/Environment/Services` 下的构件会作为单例注册到服务集合。

这意味着普通服务可以用代码特性和约定注册；需要声明式配置的服务可以放进插件树。

## 初始化与生命周期

Host 构建完成后会初始化 `ApplicationContext`。应用上下文会解析所有 [`IApplicationInitializer`](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationInitializer.cs) 并执行初始化；当 Host 生命周期进入 Started、Stopping、Stopped 时，应用上下文会启动或停止已注册的[工作器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)，并触发对应事件。

插件式应用的推荐边界是：

* 宿主负责进程、环境和少量基础配置。
* 插件负责模块、服务、命令、驱动、控制器和业务能力。
* 应用上下文负责在运行时统一暴露模块、服务、事件和生命周期。

<details>

<summary>什么时候应该改宿主，什么时候应该写插件？</summary>

如果改动影响进程启动、站点选择、Host 环境、服务托管方式或部署入口，通常属于宿主职责。如果改动是业务能力、数据库驱动、命令、Web API、后台[工作器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Components/IWorker.cs)或模块内部服务，优先放入插件。

</details>

{% content-ref url="/pages/FZiGCjee6QoD0fJgfHdH" %}
[部署第一个插件](/get-started/deploy-first-plugin)
{% endcontent-ref %}

## 内容根与配置匹配

默认插件目录由内容根与 `plugins` 组合。启动 DLL 的绝对路径不会自动更改工作目录，因此应从已部署目录启动，并确认其中包含基础清单。宿主配置与插件配置使用不同的后缀匹配规则，详见[选项配置文件](/references/option-files)。

程序集引用扫描只处理当时已加载的入口引用，再处理入口程序集及已加载插件清单声明的程序集。不要以为放进目录的每个 DLL 都会自动参与服务扫描。

Web 应用使用配套的 `Zongsoft.Web.Application.Web(...)` 入口，并额外完成控制器、[初始化器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/src/Services/IApplicationInitializer.cs)和中间件装配，见[Web 基础](/framework/web)。


# 构件与服务

从 Discussions 模块实例、验证器和命令依赖理解插件构件与容器服务。

构件是插件树中的对象及其装配描述，服务是通过容器解析的能力。一个模块实例可以同时被插件树引用，并通过自己的服务容器组织业务对象。

## 复用已有对象

来源：[src/Zongsoft.Discussions.plugin](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.plugin#L20)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.plugin" %}

```xml
<extension path="/Workbench/Modules">
	<object name="Discussions" value="{static:Zongsoft.Discussions.Module.Current, Zongsoft.Discussions}">
		<expose name="Accessor" value="{path:../@Accessor}">
			<expose name="Filters" value="{path:../@Filters}" />
		</expose>

		<expose name="Events" value="{path:../@Events}" />
		<expose name="Properties" value="{path:../@Properties}" />
	</object>
</extension>
```

{% endcode %}

这里使用 value 引用已有 Module.Current，避免创建第二个模块。type 形式则用于让构建器创建新对象；验证器与过滤器使用这一方式。

## 服务从程序集发现

来源：[src/Services/ForumService.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/ForumService.cs#L41)（节选；上下文见源文件）。

{% code title="ForumService.cs" %}

```csharp
[Service(nameof(ForumService))]
[DataService(typeof(ForumCriteria))]
public class ForumService : DataServiceBase<Forum>
{
	#region 构造函数
	public ForumService(IServiceProvider serviceProvider) : base(serviceProvider) { }
```

{% endcode %}

程序集被加载和扫描后，Service 特性描述注册。ForumService 的构造函数接收服务提供者，后续访问器与关联服务使用该范围。

## 命令依赖业务服务

来源：[src/Services/Commands/MessageSendCommand.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/Commands/MessageSendCommand.cs#L58)（节选；上下文见源文件）。

{% code title="MessageSendCommand.cs" %}

```csharp
[ServiceDependency(Provider = Module.NAME)]
public MessageService Service { get; set; }
```

{% endcode %}

属性注入指定 Discussions 模块提供者。命令类存在并不表示它已经进入终端命令树；当前 Discussions.plugin 没有挂载 MessageSendCommand，因此不能直接编造一个可运行的 send 命令教程。命令本身的真实逻辑见[命令](/framework/core/components/commands)。

## 生命周期与所有权

不要把请求身份放入共享构件，也不要逐次释放容器拥有的服务。需要有状态操作时应使用明确的调用上下文。服务找不到时先判断失败发生在程序集扫描、模块选择还是构件注入阶段，详见[服务定位](/framework/core/services/locating)。


# 数据引擎

Zongsoft.Data 数据引擎的设计目标、能力和阅读入口。

![数据引擎](https://847714710-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FdTV8LtunzSisrDXg3zNv%2Fuploads%2Fgit-blob-4bba74b4b746d0421774fe57bfbad6ef4524fa51%2Fzongsoft-data-cover.png?alt=media)

[`Zongsoft.Data`](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Data) 是一个类 GraphQL 风格的 ORM 数据访问框架。它通过数据模式、映射文件、条件表达式和数据库驱动描述数据访问结构，目标是在不手写 SQL 的情况下完成复杂查询、导航、过滤、分页、分组、聚合和写入操作。

它不是把 SQL 换成另一种字符串 SQL，而是把数据访问拆成四个稳定层次：

* 模型层：业务代码使用 POCO 类型或匿名对象。
* 元数据层：`.mapping` 文件描述实体、字段、继承和导航关系。
* 访问层：`IDataAccess` 暴露统一的查询、写入、聚合、导入和执行接口。
* 驱动层：各数据库驱动把统一表达式转换为对应数据库语法并执行。

先理解[对象关系与数据访问](/framework/data/concepts)，再按[首次查询](/framework/data/quickstart)按 Discussions 的建表、连接和论坛查询用例验证。业务规则组织见[数据服务](/framework/data/services)。

## 特性

* 支持严格 POCO 对象。
* 支持读写分离。
* 支持表继承相关操作。
* 支持按业务模块隔离映射文件。
* 使用数据模式描述查询和写入形状。
* 提供多数据库驱动。

## 核心概念

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref">页面</th></tr></thead><tbody><tr><td><strong>数据模式</strong></td><td>描述查询或写入的数据形状。</td><td><a href="/framework/data/schema">数据模式</a></td></tr><tr><td><strong>映射文件</strong></td><td>描述实体、表、字段和关系。</td><td><a href="/framework/data/mapping">映射文件</a></td></tr><tr><td><strong>连接配置</strong></td><td>配置数据源、读写分离和驱动。</td><td><a href="/framework/data/connections">连接配置</a></td></tr><tr><td><strong>数据访问接口</strong></td><td>执行查询、写入、聚合和命令。</td><td><a href="/framework/data/data-access">数据访问接口</a></td></tr><tr><td><strong>条件与操作元</strong></td><td>表达过滤条件和字段运算。</td><td><a href="/framework/data/conditions-and-operands">条件与操作元</a></td></tr><tr><td><strong>查询与导航</strong></td><td>使用 `schema`、分页和排序读取对象图。</td><td><a href="/framework/data/querying">查询与导航</a></td></tr><tr><td><strong>写入操作</strong></td><td>新增、更新、删除和增改。</td><td><a href="/framework/data/writing">写入操作</a></td></tr><tr><td><strong>驱动</strong></td><td>选择、部署和扩展数据库驱动。</td><td><a href="/framework/data/drivers">按驱动阅读</a></td></tr></tbody></table>

## 选择入口

{% tabs %}
{% tab title="我要查询数据" %}
先看 [数据模式](/framework/data/schema) 和 [查询与导航](/framework/data/querying)。这两页解释如何表达字段、导航属性、分页和排序。
{% endtab %}

{% tab title="我要写入数据" %}
先看 [写入操作](/framework/data/writing) 和 [条件与操作元](/framework/data/conditions-and-operands)。这两页解释新增、更新、删除、增改和字段运算。
{% endtab %}

{% tab title="我要接数据库" %}
先看 [连接配置](/framework/data/connections) 和 [驱动](/framework/data/drivers)。这两页解释连接名、读写分离、驱动插件和部署检查。
{% endtab %}
{% endtabs %}

## 按驱动阅读

常见驱动包括 SQL Server、MySQL、SQLite、DuckDB、PostgreSQL、TDengine、ClickHouse 和 InfluxDB。驱动通常以独立插件方式部署，并把数据驱动与连接设置驱动挂载到插件树。

已选定数据库时，可以直接进入 [MySQL](/framework/data/drivers/mysql)、[SQL Server](/framework/data/drivers/mssql)、[PostgreSQL](/framework/data/drivers/postgres)、[SQLite](/framework/data/drivers/sqlite)、[DuckDB](/framework/data/drivers/duckdb)、[ClickHouse](/framework/data/drivers/clickhouse)、[TDengine](/framework/data/drivers/tdengine) 或 [InfluxDB](/framework/data/drivers/influx) 项目页。

完整包名见 [包与模块索引](/references/packages)，部署方式见 [插件文件与加载](/framework/plugins/plugin-file)。

{% content-ref url="/pages/UWZab2cpmLjNJd0dDlb2" %}
[按驱动阅读](/framework/data/drivers)
{% endcontent-ref %}

## 典型调用

来源：[src/Module.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Module.cs#L52)（节选；上下文见源文件）。

{% code title="Module.cs" %}

```csharp
public IDataAccess Accessor => _accessor ??= this.Services.ResolveRequired<IDataAccessProvider>().GetAccessor(this.Name);
```

{% endcode %}

在这个调用中：

* `this.Services.ResolveRequired<IDataAccessProvider>()` 从模块服务中解析数据访问提供者。
* `GetAccessor(this.Name)` 按当前模块名称获取访问器；所示源码属于 Discussions 模块。
* `??=` 缓存访问器，供后续调用复用。

查询条件、返回字段、导航、排序和分页的用法，见[数据访问接口](/framework/data/data-access)和[查询与导航](/framework/data/querying)。

## 下一步

{% content-ref url="/pages/vBMP7xLOrE93myEE3n4W" %}
[数据模式](/framework/data/schema)
{% endcontent-ref %}

{% content-ref url="/pages/fxkoGuzFwJpwdco6BTrD" %}
[数据访问接口](/framework/data/data-access)
{% endcontent-ref %}

{% content-ref url="/pages/JYpH4ruWnN3DL7Dg9MUx" %}
[写入操作](/framework/data/writing)
{% endcontent-ref %}

## 相关资源

* [Zongsoft.Data 源码目录](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Data)
* [Zongsoft.Data 中文 README](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Data/README.zh-Hans.md)
* [Zongsoft.Data 英文 README](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Data/README.md)
* [Zongsoft.Data NuGet 包](https://www.nuget.org/packages/Zongsoft.Data)


# 对象关系与数据访问

理解对象关系映射、实体、导航、数据形状、访问器与数据库驱动的协作。

业务代码操作订单、客户和明细对象；关系数据库保存表、行、列和关联键。对象关系映射（ORM）负责连接这两种表达。[Zongsoft.Data](https://github.com/Zongsoft/framework/tree/main/Zongsoft.Data) 使用显式映射和数据模式，让业务描述“访问哪些实体、哪些字段和关系”，再由引擎与驱动执行。

## 四个层次

**模型**是代码中的对象类型，可以是普通属性类，也可以采用框架支持的模型机制。**映射**描述实体与表、属性与列、导航与关联键。**数据模式**描述某一次操作的成员范围。**驱动**将表达转换为目标数据库支持的命令。

以订单详情为例：映射声明订单的 `CustomerId` 关联客户的 `CustomerId`；一次详情查询使用 `OrderId, Amount, Customer{Name}, Lines:20{ProductId, Quantity}`；另一次列表查询只需要 `OrderId, Amount`。同一份映射可以支持不同形状，无需为每次查询重写关系。

## 实体与名称 <a href="#entity" id="entity"></a>

映射中的 `container` 为实体和命令提供命名空间。例如 Discussions 映射中的 Discussions.Forum 是实体限定名，实体 alias 指向 Discussions\_Forum 物理表。访问器名、映射容器名和数据库名不是同一概念，即使项目常把它们命名为相同文本。

泛型查询需要根据模型定位实体；动态或跨模块场景可显式传实体限定名。先核对[映射](/framework/data/mapping)，再确认代码使用的模型和实体名称。

## 导航与关系基数 <a href="#navigation" id="navigation"></a>

导航属性表达“沿关系访问另一个对象”。关系基数说明能关联多少对象：`?` 表示零或一个，`!` 表示必须有一个，`*` 表示集合。它们影响连接与对象装配方式，不能只根据 C# 属性名称猜测。

单值导航可通过连接读取，集合导航会形成独立的从查询，并在结果装配过程中填充。请求一个嵌套对象图不保证只执行一条 SQL；层次越深、集合越大，数据库往返和返回数据可能越多。应按页面实际需要选择字段与限量。

## 数据模式与数据库结构 <a href="#schema" id="schema"></a>

这里的 Schema 指操作的数据形状，不是数据库建表脚本，也不是 GraphQL 服务端协议。它在语法上类似选择集，但有自己的解析规则。`.mapping` 则是稳定结构元数据，两者职责不同。

{% hint style="info" %}
💡 部署 `.mapping` 不会自动创建表、迁移数据库或灌入初始化数据。首次接入必须分别准备映射、连接、驱动和真实数据库结构；只验证装配时可使用[无表的常量查询](/framework/data/quickstart)。
{% endhint %}

## 条件与操作元 <a href="#condition" id="condition"></a>

条件回答“哪些行参与操作”，操作元回答“表达式中的值如何计算”。例如 `Condition.Equal("OrderId", orderId)` 限定订单，`Operand.Field("Quantity") + 1` 表示数据库内的字段运算。

将条件值交给引擎绑定，有助于避免手工拼 SQL。字段名和可访问成员仍须由应用控制；参数化不等于自动实现字段权限或租户隔离。

## 访问器与数据服务 <a href="#service" id="service"></a>

[访问器](/framework/data/data-access)提供通用数据操作。[数据服务](/framework/data/services)在其上组织模型相关的业务入口、可写能力、验证、授权与过滤。Web 控制器可以使用数据服务，而不必在每个端点重复定义相同的访问流程。

数据库事务处理同一事务范围内的提交与回滚，业务授权处理谁能操作哪些数据。这些职责不会因为引入 ORM 自动完成。特别是数据库写入与消息发送，不能仅用一个本地事务假定两者原子完成。

## 推荐学习顺序

先完成[首次查询](/framework/data/quickstart)，再学习[映射](/framework/data/mapping)和[数据模式](/framework/data/schema)，随后进入[查询](/framework/data/querying)、[写入](/framework/data/writing)、[事务](/framework/data/transactions)及[数据服务](/framework/data/services)。驱动差异集中见[驱动](/framework/data/drivers)。


# 完成首次数据查询

沿 Discussions 论坛查询核对映射、身份、连接和结果。

第一次查询使用 Discussions 已有论坛接口。前提是[业务插件已部署](/get-started/deploy-first-plugin)，宿主连接的是自己的隔离环境。

## 1. 核对真实数据契约

同时阅读 Discussions 的 Models/Forum.cs、[Zongsoft.Discussions.mapping](https://github.com/Zongsoft/discussions/blob/main/src/Zongsoft.Discussions.mapping) 和 database 下所选数据库脚本。Forum 使用 SiteId 与 ForumId 复合键；外部序号需要相应服务；Message 还有 ClickHouse 驱动标记。这些约束不能靠只修改数据库连接字符串解决。

数据库脚本可能重建对象，应先审阅并只在临时数据库执行。映射文件不会自动创建这些表。

## 2. 核对访问器、连接与身份

来源：[src/Module.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Module.cs#L52)（节选；上下文见源文件）。

{% code title="Module.cs" %}

```csharp
public IDataAccess Accessor => _accessor ??= this.Services.ResolveRequired<IDataAccessProvider>().GetAccessor(this.Name);
```

{% endcode %}

访问器名是 Discussions。连接配置与驱动必须允许该访问器选到正确数据源；身份需要包含 Discussions 方案及 SiteId，参见[连接配置](/framework/data/connections)和[认证](/framework/security/authentication)。

## 3. 使用仓库中的论坛请求

以下仅摘录请求行，省略 Host 和 Authorization；page 是原请求中的环境变量，由你的请求工具提供：

来源：[docs/http/forum.http](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/docs/http/forum.http#L2)（节选；上下文见源文件）。

{% code title="forum.http" %}

```http
GET /Discussions/Forums?page={{page}} HTTP/1.1
```

{% endcode %}

这条请求读取论坛集合。可见性规则由 ForumService 处理，站点条件由数据验证器补充；空集合可能表示当前范围没有数据，不一定是查询失败。

## 4. 顺着服务检查结果

来源：[src/Services/ForumService.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/ForumService.cs#L83)（节选；上下文见源文件）。

{% code title="ForumService.cs" %}

```csharp
public IEnumerable<Models.Thread> GetPinnedThreads(ushort forumId, string schema, Paging paging = null)
{
	return this.DataAccess.Select<Models.Thread>(
		Condition.Equal(nameof(Models.Thread.ForumId), forumId) &
		Condition.Equal(nameof(Models.Thread.IsPinned), true) &
		Condition.Equal(nameof(Models.Thread.Visible), true),
		schema, paging, Sorting.Descending(nameof(Models.Thread.ThreadId)));
}
```

{% endcode %}

这是同一模块里读取置顶主题的真实调用：条件、数据模式、分页和排序都有明确来源。理解接口集合查询后，可以在这个方法及数据过滤器设置断点，观察访问器如何工作。

## 成功标准与排障

确认请求进入正确控制器、访问器选中预期连接、数据只属于当前站点、字段形状符合 schema、未审核正文没有暴露。进一步的写入和事务验证使用独立测试数据；不要把一次 SELECT 成功当作整个论坛业务已经验收。

找不到实体时检查映射部署；找不到驱动时检查插件；无法连接时检查数据源；缺少序号服务时检查依赖装配。更多阅读：[映射](/framework/data/mapping)、[查询](/framework/data/querying)、[服务](/framework/data/services)。


# 数据模式

从用户归档、浏览记录和主题审核理解 schema 对字段与导航的控制。

数据模式是传给数据访问或数据服务的 schema 文本，描述本次操作包含哪些成员。关系定义来自[映射](/framework/data/mapping)，模式只选择这次使用的形状；它不是另一份数据库结构。

## 用户归档需要站点信息

来源：[src/Features/Archiving/UserDataTemplateModelProvider.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Features/Archiving/UserDataTemplateModelProvider.cs#L56)（节选；上下文见源文件）。

{% code title="UserDataTemplateModelProvider.cs" %}

```csharp
var schema = $"*, {nameof(UserProfile.Site)}" + "{*}";
```

{% endcode %}

这条真实模式选择用户的简单字段，并展开 Site 的简单字段。单独的星号不会自动展开所有导航，也不会自动补齐模型计算属性的依赖字段。归档提供者随后把结果包装为 Users，交给 user-list.xlsx，见[表格与模板](/framework/externals/documents)。

## 浏览记录展开主题

来源：[src/Services/UserService.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/UserService.cs#L67)（节选；上下文见源文件）。

{% code title="UserService.cs" %}

```csharp
public IEnumerable<History> GetHistories(uint userId, Paging paging = null)
{
	if(userId == 0)
		userId = this.Principal.Identity.GetIdentifier<uint>();

	return this.DataAccess.Select<History>(Condition.Equal(nameof(History.UserId), userId), $"*, {nameof(History.Thread)}" + "{*}", paging);
}
```

{% endcode %}

History.Thread 关系由映射连接。只在列表确实需要主题详情时才展开；集合或深层关系可能增加查询次数与结果体积。

## 写入模式包含关联成员

来源：[src/Services/ThreadService.cs](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Services/ThreadService.cs#L205)（节选；上下文见源文件）。

{% code title="ThreadService.cs" %}

```csharp
//确保数据模式含有“主题内容贴”复合属性
schema.Include("Post{*}");
```

{% endcode %}

ThreadService 插入主题时，先确认正文存在，再把 Post 加入写入模式。审核动作则限定 Post 的 Approved 字段，防止一个状态动作顺便更新无关正文。

## 语法参考与框架用例

| 语法      | 含义                  |
| ------- | ------------------- |
| 逗号分隔成员  | 选择多个字段或导航           |
| 星号      | 当前层可匹配的简单成员         |
| 感叹号加成员  | 从当前选择中排除成员          |
| 点路径或花括号 | 沿映射关系选择成员           |
| 冒号加数量   | 集合导航限量，不是页码         |
| 括号内排序成员 | 指定导航结果排序，负号或波浪号表示倒序 |

Discussions 没有使用导航限量的业务片段，相关边界采用框架 [SchemaTest](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Core/test/Data/SchemaTest.cs) 和 [SchemaParserTest](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Data/test/SchemaParserTest.cs) 作为参考。根结果集分页使用 Paging，见[查询](/framework/data/querying)。

## 权限与计算字段

模式控制读取形状，不是授权规则。让调用者排除 Approved、CreatorId 等审核判定字段时，不能默认正文可见；Discussions 的结果过滤器对此采用屏蔽处理。计算成员也不会自动推导其所需原始字段，应由服务限定模式。

{% hint style="warning" %}
🚨 写入模式不能赋予调用方修改站点、创建人或其他不可变字段的权限。仍需同时检查映射、验证器、服务动作和调用者身份。
{% endhint %}


# 映射文件

以 Discussions 的论坛复合键、主题正文关系和序号配置解释映射。

.mapping 把领域模型映射到数据库结构。它声明表、键、字段与导航关系，可以随业务插件交付；它本身不创建数据库，也不执行迁移。Discussions 的映射放在单个 [Zongsoft.Discussions.mapping](https://github.com/Zongsoft/discussions/blob/main/src/Zongsoft.Discussions.mapping) 中，容器名是 Discussions。

## 论坛编号属于站点

下面是 Forum 实体内部的键与编号属性定义，外围 entity 声明及其他属性见源文件。

下面是 Forum 实体内部的键与编号属性定义，外围 entity 声明及其他属性见源文件。

来源：[src/Zongsoft.Discussions.mapping](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.mapping#L197)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.mapping" %}

```xml
<key>
	<member name="SiteId" />
	<member name="ForumId" />
</key>

<property name="SiteId" type="uint" nullable="false" />
<property name="ForumId" type="ushort" nullable="false" sequence="#(SiteId)" />
```

{% endcode %}

Forum 的键由 SiteId 与 ForumId 组成。ForumId 使用按 SiteId 分域的外部序号，不能把另一个站点相同 ForumId 的论坛视为同一个对象。实体限定名是 Discussions.Forum，物理表名为 Discussions\_Forum。

## 主题怎样引用论坛和正文

来源：[src/Zongsoft.Discussions.mapping](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.mapping#L278)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.mapping" %}

```xml
<complexProperty name="Forum" port="Forum">
	<link port="SiteId" />
	<link port="ForumId" />
</complexProperty>
```

{% endcode %}

关系的 link.port 指向目标属性，anchor 指当前实体属性。上述关系位于 ForumUser 实体中，完整上下文见源文件；相同名字的 Forum 导航也存在于主题中。复合键关系应同时连接站点与论坛编号。

来源：[src/Zongsoft.Discussions.mapping](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.mapping#L330)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.mapping" %}

```xml
<complexProperty name="Post" port="Post" multiplicity="!">
	<link port="PostId" />
</complexProperty>
```

{% endcode %}

主题的 Post 是必需的正文帖。ThreadService.OnInsert 会把 Post 导航包含进写入模式，才能将模型关系交给引擎处理；仅给模型新增一个属性不会自动产生数据库关系。

## 标量、默认值与不可变字段

来源：[src/Zongsoft.Discussions.mapping](https://github.com/Zongsoft/Zongsoft.Discussions/blob/main/src/Zongsoft.Discussions.mapping#L293)（节选；上下文见源文件）。

{% code title="Zongsoft.Discussions.mapping" %}

```xml
<property name="ThreadId" type="ulong" nullable="false" sequence="#" />
<property name="SiteId" type="uint" nullable="false" immutable="true" />
<property name="ForumId" type="ushort" nullable="false" />
<property name="Title" type="nvarchar" length="50" nullable="false" />
<property name="Acronym" type="varchar" length="50" nullable="true" />
<property name="Summary" type="nvarchar" length="500" nullable="true" />
<property name="Tags" type="nvarchar" length="100" nullable="true" />
<property name="PostId" type="ulong" nullable="false" />
```

{% endcode %}

字符串长度、可空性和数据库类型要同时与模型、SQL 核对。immutable 用来约束写入阶段，不能代替用户授权。序号标记 # 需要外部序号服务；按站点分域的序号还依赖 SiteId 先被正确赋值，见[序号器](/framework/core/common/sequence)。

## 四种 SQL 脚本不是自动兼容承诺

Discussions 的 database 目录包含 MySQL、SQL Server、PostgreSQL、ClickHouse 脚本。当前映射中的 Message 还显式指定 ClickHouse 驱动，因此选择其他数据库时必须同时核对实体驱动标记、数据源选择和 SQL 结构。不能仅换连接字符串就宣布切换完成。

## 扩展与校验

复杂属性的 multiplicity 区分可选单值、必需单值和集合。需要级联写入时应明确审核 immutable 与 behaviors；默认值必须以[映射加载器](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Data/src/Metadata/Profiles/MetadataFileResolver.cs)为准。XSD 验证检查格式，实际驱动验证检查数据库语义，两者不能互相替代。

Discussions 当前没有命名 SQL 命令。该能力可继续查阅框架 [MetadataCommandScriptorTest](https://github.com/Zongsoft/framework/blob/main/Zongsoft.Data/test/MetadataCommandScriptorTest.cs)，不要为论坛另造 Answer 或订单统计命令。

继续阅读：[数据模式](/framework/data/schema)、[连接配置](/framework/data/connections)、[写入](/framework/data/writing)。




---

[Next Page](https://docs.zongsoft.com/llms-full.txt/1)

