Merge branch 'main' into fix/analyzer-warning-reduction-batch

This commit is contained in:
gewuyou 2026-04-21 12:44:28 +08:00 committed by GitHub
commit c61ee140a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 975 additions and 5919 deletions

View File

@ -538,6 +538,27 @@ def build_latest_commit_review_threads(comments: list[dict[str, Any]]) -> list[d
return sorted(threads, key=lambda item: (item["path"], item["line"] or 0, item["thread_id"]))
def select_latest_submitted_review(
reviews: list[dict[str, Any]],
*,
required_user: str | None = None,
prefer_non_empty_body: bool = False,
) -> dict[str, Any] | None:
filtered_reviews = [review for review in reviews if review.get("submitted_at")]
if required_user is not None:
filtered_reviews = [review for review in filtered_reviews if review.get("user", {}).get("login") == required_user]
if not filtered_reviews:
return None
if prefer_non_empty_body:
non_empty_body_reviews = [review for review in filtered_reviews if str(review.get("body") or "").strip()]
if non_empty_body_reviews:
filtered_reviews = non_empty_body_reviews
return max(filtered_reviews, key=lambda review: review.get("submitted_at", ""))
def fetch_latest_commit_review(pr_number: int) -> dict[str, Any]:
api_base = f"https://api.github.com/repos/{OWNER}/{REPO}/pulls/{pr_number}"
commits = fetch_paged_json(f"{api_base}/commits?per_page=100")
@ -558,10 +579,11 @@ def fetch_latest_commit_review(pr_number: int) -> dict[str, Any]:
review for review in reviews if review.get("commit_id") == latest_commit_sha and review.get("submitted_at")
]
candidate_reviews = latest_commit_reviews or [review for review in reviews if review.get("submitted_at")]
latest_review = (
max(candidate_reviews, key=lambda review: review.get("submitted_at", ""))
if candidate_reviews
else None
latest_review = select_latest_submitted_review(candidate_reviews)
latest_coderabbit_review_with_body = select_latest_submitted_review(
candidate_reviews,
required_user=CODERABBIT_LOGIN,
prefer_non_empty_body=True,
)
latest_commit_comments = [comment for comment in comments if comment.get("commit_id") == latest_commit_sha]
@ -581,6 +603,18 @@ def fetch_latest_commit_review(pr_number: int) -> dict[str, Any]:
"user": latest_review.get("user", {}).get("login") if latest_review else "",
"body": latest_review.get("body") if latest_review else "",
},
"latest_coderabbit_review_with_body": {
"id": latest_coderabbit_review_with_body.get("id") if latest_coderabbit_review_with_body else None,
"state": latest_coderabbit_review_with_body.get("state") if latest_coderabbit_review_with_body else "",
"submitted_at": (
latest_coderabbit_review_with_body.get("submitted_at") if latest_coderabbit_review_with_body else ""
),
"commit_id": latest_coderabbit_review_with_body.get("commit_id") if latest_coderabbit_review_with_body else "",
"user": latest_coderabbit_review_with_body.get("user", {}).get("login")
if latest_coderabbit_review_with_body
else "",
"body": latest_coderabbit_review_with_body.get("body") if latest_coderabbit_review_with_body else "",
},
"threads": threads,
"open_threads": open_threads,
}
@ -621,7 +655,7 @@ def build_result(pr_number: int, branch: str) -> dict[str, Any]:
coderabbit_review: dict[str, Any] = {}
try:
latest_commit_review = fetch_latest_commit_review(pr_number)
latest_review = latest_commit_review.get("latest_review", {})
latest_review = latest_commit_review.get("latest_coderabbit_review_with_body", {})
latest_review_body = str(latest_review.get("body") or "")
if latest_review.get("user") == CODERABBIT_LOGIN and latest_review_body:
coderabbit_review = parse_latest_review_body(latest_review_body)

View File

@ -269,8 +269,8 @@ bash scripts/validate-csharp-naming.sh
- Treat source code, `*.csproj`, tests, generated snapshots, and packaging metadata as the primary evidence for
documentation updates.
- Treat `CoreGrid` as a secondary evidence source for real project adoption patterns, directory layouts, and end-to-end
usage examples.
- Treat verified reference implementations under `ai-libs/` as a secondary evidence source for real project adoption
patterns, directory layouts, and end-to-end usage examples.
- Treat existing `README.md` files and `docs/zh-CN/` pages as editable outputs, not authoritative truth.
- If existing documentation conflicts with code or tests, update the documentation to match the implementation instead
of preserving outdated wording.

View File

@ -25,7 +25,8 @@
- `FileStorage``ScopedStorage``JsonSerializer``SettingsModel<TRepository>``SaveRepository<TSaveData>``SceneRouterBase``UiRouterBase``YamlConfigLoader` 等都在实现这里的契约。
- 引擎适配包或项目代码
- `IUiFactory``ISceneFactory``IUiRoot``ISceneRoot`、资源注册表等通常由引擎适配层或游戏项目自己实现。
- CoreGrid 的真实结构也是这样:页面/场景 factory、root、registry 在项目层,运行时基类和契约来自 `GFramework.Game` 与本包。
- 仓库内 `ai-libs/` 下的只读参考实现通常也是这样组织:页面 / 场景 factory、root、registry 在项目层,
运行时基类和契约来自 `GFramework.Game` 与本包。
## 子系统地图
@ -195,9 +196,9 @@ public sealed class ContinueGameCommandHandler
也就是说,本包回答的是“项目各层如何约定”,`GFramework.Game` 回答的是“这些约定默认怎么跑起来”。
## CoreGrid 里的真实用法线索
## `ai-libs/` 里的参考接入线索
CoreGrid 对本包的使用方式,能比较清楚地说明它的职责边界:
`ai-libs/` 下的只读参考实现对本包的使用方式,能比较清楚地说明它的职责边界:
- 公共脚本广泛引用:
- `IUiRouter`
@ -213,7 +214,7 @@ CoreGrid 对本包的使用方式,能比较清楚地说明它的职责边界
- 真正的实现和装配则放在:
- `GFramework.Game`
- `GFramework.Godot.*`
- CoreGrid 自己的模块、factory、root、registry
- 项目自己的模块、factory、root、registry
这正是本包的设计目标:让业务层依赖稳定契约,而不是依赖具体运行时细节。

View File

@ -31,7 +31,8 @@
- 引擎适配包或项目内适配层
- 本包提供的是“引擎无关”的核心逻辑和基类。
- 真正和 Godot、Unity、MonoGame 等引擎对象打交道的工厂、根节点、资源注册表,通常在相邻引擎包或游戏项目内实现。
- CoreGrid 的真实接法就是这样:配置文件 IO 由 `GFramework.Godot.Config` 适配UI/Scene factory 与 root 由项目自己提供。
- 仓库内 `ai-libs/` 下的只读参考实现通常也是这样接入:配置文件 IO 由 `GFramework.Godot.Config` 适配,
UI / Scene factory 与 root 由项目自己提供。
## 子系统地图
@ -72,7 +73,7 @@
- `SaveConfiguration`
- 槽位目录、文件名、前缀等约定
CoreGrid 的真实用法:
`ai-libs/` 下已验证参考实现的常见接法:
- 设置持久化使用 `UnifiedSettingsDataRepository`
- 存档使用 `SaveRepository<GameSaveData>`
@ -95,7 +96,7 @@ CoreGrid 的真实用法:
- `Setting/Events/*`
- 设置初始化、应用、保存、重置相关事件
CoreGrid 的真实用法:
`ai-libs/` 下已验证参考实现的常见接法:
- 在模型模块中创建 `SettingsModel<ISettingsDataRepository>`
- 注册多个 applicator
@ -148,7 +149,7 @@ CoreGrid 的真实用法:
- `Scene/Handler/*``UI/Handler/*`
- 默认转换处理器基类与日志处理器
CoreGrid 的真实用法:
`ai-libs/` 下已验证参考实现的常见接法:
- 项目自定义 `SceneRouter : SceneRouterBase`
- 项目自定义 `UiRouter : UiRouterBase`
@ -253,7 +254,7 @@ await settingsSystem.ApplyAll();
await settingsSystem.SaveAll();
```
CoreGrid 目前就是按这个思路接入,只是底层存储换成了 Godot 适配实现。
`ai-libs/` 下的只读参考实现目前也是按这个思路接入,只是底层存储换成了 Godot 适配实现。
### 3. 接入静态 YAML 配置
@ -311,18 +312,18 @@ public sealed class MyUiRouter : UiRouterBase
这类 router 适合作为你的项目层或引擎适配层代码,而不是直接修改本包。
## CoreGrid 里的真实用法线索
## `ai-libs/` 里的参考接入线索
当前仓库内,CoreGrid 对本包的使用大致分成三层:
当前仓库内的只读参考实现,对本包的使用大致分成三层:
- 配置
- `CoreGridConfigHost` 使用生成表元数据与 YAML loader 完成配置注册
- 项目级配置宿主类型使用生成表元数据与 YAML loader 完成配置注册
- 设置与存档
- `UtilityModule` 注册序列化器、底层存储、`UnifiedSettingsDataRepository``SaveRepository<GameSaveData>`
- `ModelModule` 创建 `SettingsModel<ISettingsDataRepository>` 并注册 applicator
- 项目层 utility 模块注册序列化器、底层存储、`UnifiedSettingsDataRepository`
`SaveRepository<GameSaveData>`
- 项目层 model 模块创建 `SettingsModel<ISettingsDataRepository>` 并注册 applicator
- 路由
- `SceneRouter` 继承 `SceneRouterBase`
- `UiRouter` 继承 `UiRouterBase`
- 项目自定义 `SceneRouterBase` / `UiRouterBase` 的派生类型
这说明本包更适合做“游戏基础设施层”,而不是把所有引擎对象耦死在包内部。

View File

@ -7,35 +7,48 @@
## 当前恢复点
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-001`
- 当前阶段:`Phase 1`
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-004`
- 当前阶段:`Phase 3`
- 当前焦点:
- 已将当前工作树根目录的 legacy `local-plan/` 迁入 `ai-plan/public/documentation-governance-and-refresh/`
- 第一轮治理已完成 `AGENTS.md`、根 `README.md``getting-started` 与第一批高优先级模块 `README.md`
- 下一轮需要继续按栏目核对并重写 `docs/zh-CN/core/*``docs/zh-CN/game/*`
`docs/zh-CN/source-generators/*`
- 已完成 `docs/zh-CN/core/architecture.md``context.md``lifecycle.md``command.md``query.md`
`cqrs.md` 的专题页重写
- `core` 关键专题页已改回当前 `Architecture``ArchitectureContext`、旧 Command/Query 兼容层与新 CQRS
runtime 的真实入口语义
- 下一轮需要继续推进 `docs/zh-CN/core/*` 余下专题页,以及 `docs/zh-CN/game/*`
`docs/zh-CN/source-generators/*` 的专题页核对
## 当前状态摘要
- 文档治理规则已收口到仓库规范README、站点入口与采用链路不再依赖旧文档自证
- 高优先级模块入口已补齐,首轮文档站构建校验已经通过
- 当前主题仍是 active topic因为核心栏目专题页仍可能包含与实现漂移的旧内容
- 高优先级模块入口`core` 关键专题页已回到可作为默认导航入口的状态
- 当前主题仍是 active topic因为 `core` 其余专题页及 `game``source-generators` 栏目下仍可能包含与实现漂移的旧内容
## 当前活跃事实
- 旧 `local-plan/` 的详细 todo 与 trace 已迁入主题内 `archive/`
- 当前分支 `docs/sdk-update-documentation` 已在 `ai-plan/public/README.md` 建立 topic 映射
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
- `core``game``source-generators` 三个栏目入口页现在都以模块 README 与当前包拆分为准
- `docs` 站点构建已验证通过,修正了 VitePress 对 `docs/` 目录外相对链接的 dead-link 检查问题
- `core` 关键专题页已移除 `Init()`、属性式 `CommandBus` / `QueryBus`、旧 `Input` 赋值式示例和已移除的
`RegisterMediatorBehavior` 等过时说明
- `core/index.md` 已把 `Godot``Source Generators` 栏目入口改成可点击链接,补齐 landing page 导航一致性
- `documentation-governance-and-refresh` active trace 已把重复的 `### 下一步` 标题改成带恢复点标识的唯一标题,消除
`MD024/no-duplicate-heading` 告警
- `gframework-pr-review` 脚本已修复“空 `APPROVED` review 覆盖非空 CodeRabbit review body”的解析路径当前分支可重新提取 Nitpick comments
## 当前风险
- 旧专题页示例失真风险:`docs/zh-CN/core/*``game/*``source-generators/*` 中仍可能保留看似合理但与
真实实现不一致的示例
- 缓解措施:继续按源码、测试、`*.csproj``CoreGrid` 真实接法核对,不把旧文档当事实来源
- 缓解措施:继续按源码、测试、`*.csproj``ai-libs/` 下已验证参考实现核对,不把旧文档当事实来源
- 采用路径误导风险:根聚合包与模块边界若再次被写错,会继续误导消费者的包选择
- 缓解措施:保持“源码与包关系优先”的证据顺序,改动采用说明时同步核对包依赖与生成器 wiring
- Active 入口回膨胀风险:后续若把栏目级重写过程直接追加到 active 文档,会再次拖慢恢复
- 缓解措施:阶段完成并验证后,继续把细节迁入本 topic 的 `archive/`
- review 跟进遗漏风险:如果 PR review 抓取继续优先选中空 review body会漏掉 CodeRabbit 的 Nitpick 和
linter 跟进项
- 缓解措施:保持当前“最新提交 + 最新非空 CodeRabbit review body”解析策略并在有疑点时以 API 实抓结果复核
## 活跃文档
@ -46,9 +59,12 @@
- 旧 `local-plan/` 的详细实施历史与文档站构建结果已迁入主题内归档
- active 跟踪文件已按 `ai-plan` 治理规则精简为当前恢复入口
- `cd docs && bun run build`
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json`
## 下一步
1. 继续按栏目核对 `docs/zh-CN/core/*`,列出仍失真的页面与示例
2. 再推进 `docs/zh-CN/game/*``docs/zh-CN/source-generators/*` 的专题页重写
3. 若下一轮重写完成且验证通过,将栏目级详细过程迁入本 topic 的 `archive/`
1. 继续核对 `docs/zh-CN/core/*` 余下专题页,优先处理 `events``property``state-management``coroutine`
`logging`
2. 再推进 `docs/zh-CN/game/*``docs/zh-CN/source-generators/*` 的专题页重写,优先处理仍引用旧安装方式或旧 API 的页面
3. 若 active trace 再积累新的已完成阶段,按恢复点粒度迁入 `archive/traces/`,避免默认启动入口再次膨胀

View File

@ -25,7 +25,83 @@
- 历史 trace 归档:
- `ai-plan/public/documentation-governance-and-refresh/archive/traces/documentation-governance-and-refresh-history-through-2026-04-18.md`
### 下一步
### 下一步RP-001
1. 后续继续该主题时,只从 `ai-plan/public/documentation-governance-and-refresh/` 进入,不再恢复 `local-plan/`
2. 若 active 入口再次积累多轮已完成且已验证阶段,继续按同一模式迁入该主题自己的 `archive/`
## 2026-04-21
### 阶段:栏目 landing page 收口RP-002
- 依据 `ai-plan/public/README.md` 的 worktree 映射恢复 `documentation-governance-and-refresh` 主题,并确认该分支下一步应优先处理 `docs/zh-CN/core/*``game/*``source-generators/*`
- 复核 `docs/zh-CN/core/index.md``docs/zh-CN/game/index.md``docs/zh-CN/source-generators/index.md` 后确认:这三页仍保留旧版“大而全教程”结构,与当前模块 README、包拆分关系和推荐接入路径明显漂移
- 对照 `GFramework.Core/README.md``GFramework.Game/README.md``GFramework.Core.SourceGenerators/README.md`
`GFramework.Game.SourceGenerators/README.md``GFramework.Cqrs.SourceGenerators/README.md`
`GFramework.Godot.SourceGenerators/README.md`,重写三个栏目 landing page使其回到“模块定位、包关系、最小接入路径、继续阅读”的可信入口形态
- 首次执行 `cd docs && bun run build` 时发现 VitePress 会把跳到 `docs/` 目录外的相对链接判定为 dead link因此将 landing page 末尾的模块 README 入口改为纯文本路径提示而非站内链接
- 第二次执行 `cd docs && bun run build` 通过,说明当前 landing page 重写没有破坏站点构建
### 当前结论
- 当前默认导航入口已显著收敛,但专题页仍需逐页按源码与测试继续核对
- 后续优先级应从 `core` 专题页开始,再向 `game``source-generators` 扩展
### 下一步RP-002
1. 审核 `docs/zh-CN/core/architecture.md``context.md``lifecycle.md``command.md``query.md``cqrs.md`
2. 记录每页的失真点、真实 API 名称与应保留的最小示例
3. 完成一轮专题页重写后再次执行 `cd docs && bun run build`
### 补充2026-04-21 内容引用迁移
- 按当前文档治理主题,继续清理活跃规范与面向读者的内容入口中的旧参考仓库命名
- `AGENTS.md` 已把“secondary evidence source”从特定项目名收口为 `ai-libs/` 下的已验证只读参考实现
- `GFramework.Game/README.md``GFramework.Game.Abstractions/README.md`
`docs/zh-CN/game/index.md` 已同步改为 `ai-libs/` 参考表述,并去掉特定参考项目名称与项目内类型名线索
- `documentation-governance-and-refresh` active tracking 已同步把风险缓解中的参考来源更新为
`ai-libs/` 下已验证参考实现
- 下一次专题页重写时,继续沿用同一表述,不再把特定参考项目名写入新的活跃文档入口
### 补充2026-04-21 Core 专题页收口RP-003
- 复核 `docs/zh-CN/core/architecture.md``context.md``lifecycle.md``command.md``query.md``cqrs.md`
后确认:这些页面仍大量保留旧 API 叙述,例如 `Init()`、属性式 `CommandBus` / `QueryBus`、旧 `Input`
赋值式命令/查询示例,以及已移除的 `RegisterMediatorBehavior`
- 对照 `Architecture``ArchitectureContext``IArchitectureContext``ContextAwareBase`、旧
`AbstractCommand` / `AbstractQuery` 基类和 `GFramework.Cqrs/README.md` 后,重写上述六个页面
- 新版专题页将结构统一为“当前角色、真实公开入口、最小示例、兼容边界、迁移方向”,避免继续复刻旧版大而全教程
- `core/context.md` 已明确把 `GameContext` 收束为兼容回退路径,而不是新代码的推荐接法
- `core/command.md``core/query.md` 已明确旧体系仍可用,但新功能应优先走 `GFramework.Cqrs`
- `core/cqrs.md` 已与当前 runtime / generator / handler 注册语义对齐,并明确 `RegisterCqrsPipelineBehavior<TBehavior>()`
是公开入口
- 执行 `cd docs && bun run build` 通过,说明本轮 `core` 专题页重写没有破坏文档站构建
### 下一步RP-003
### 补充2026-04-21 PR review 跟进收口RP-004
- 通过 `gframework-pr-review` 复查当前分支 PR 时发现:脚本把同一 head commit 上空 body 的 `APPROVED`
review 误当成“最新 review body”导致 `Nitpick comments` 未被结构化提取
- 对照 GitHub API 的 review 列表后,确认真正包含 `Nitpick comments (2)` 的是更早 3 秒提交的
`COMMENTED` review因此调整脚本为“保持最新 review 元数据输出不变,但解析时优先选择同一提交上的最新非空
CodeRabbit review body”
- 根据重新提取的 Nitpick 内容,补齐 `docs/zh-CN/core/index.md``Godot``Source Generators`
栏目的可点击链接
- 顺手修正 active trace 中重复的 `### 下一步` 标题,消除 `MD024/no-duplicate-heading` 告警,避免后续 PR
review 再次把文档治理入口本身标成噪音
### 验证RP-004
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json`
- `cd docs && bun run build`
### 下一步RP-004
1. 继续处理 `docs/zh-CN/core/events.md``property.md``state-management.md``coroutine.md``logging.md`
2. 若 active trace 继续累计多个已完成恢复点,按 `archive/traces/` 粒度归档旧阶段细节
3. 保持 PR review 跟进时优先验证最新未解决线程、非空 CodeRabbit review body 与 MegaLinter 明确告警
1. 继续处理 `docs/zh-CN/core/events.md``property.md``state-management.md``coroutine.md``logging.md`
2. 保持同样的证据顺序:源码、`*.csproj`、模块 README、`ai-libs/` 参考实现
3. 完成下一批专题页重写后再次执行 `cd docs && bun run build`

View File

@ -1,239 +1,147 @@
# Architecture 架构详解
# Architecture
> 深入了解 GFramework 的核心架构设计和实现
`Architecture``GFramework.Core` 的运行时入口。它负责三件事:
## 目录
- 组织初始化与销毁阶段
- 接入模型、系统、工具和模块
- 暴露 `ArchitectureContext` 作为统一上下文入口
- [概述](#概述)
- [架构设计](#架构设计)
- [生命周期管理](#生命周期管理)
- [组件注册](#组件注册)
- [模块系统](#模块系统)
- [最佳实践](#最佳实践)
- [API 参考](#api-参考)
当前版本的 `Architecture` 已经是协调器外观。对外仍保留稳定的注册与生命周期 API但内部职责已经拆给专门协作者处理。
## 概述
## 你真正会用到的公开入口
Architecture 是 GFramework 的核心类,负责管理整个应用的生命周期、组件注册和模块管理。从 v1.1.0 开始,Architecture
采用模块化设计,将职责分离到专门的协作者中。
最常见的成员只有这些:
> 命名约定:
> - `ArchitectureServices` 是公开的基础服务入口,负责容器、事件总线、命令执行器、查询执行器和服务模块管理
> - `ArchitectureComponentRegistry` 是内部组件注册器,专门负责 System / Model / Utility 的注册与生命周期接入
> - 两者不是同一层职责,不要混用
- `OnInitialize()`
- 子类唯一必须实现的入口,用来注册模型、系统、工具、模块和额外的 CQRS 行为
- `RegisterModel(...)` / `RegisterSystem(...)` / `RegisterUtility(...)`
- 注册运行时组件
- `InstallModule(...)`
- 安装实现了 `IArchitectureModule` 的模块
- `RegisterLifecycleHook(...)`
- 注册阶段钩子
- `RegisterCqrsPipelineBehavior<TBehavior>()`
- 注册 CQRS pipeline 行为
- `RegisterCqrsHandlersFromAssembly(...)` / `RegisterCqrsHandlersFromAssemblies(...)`
- 显式接入其他程序集中的 CQRS handlers
- `InitializeAsync()` / `WaitUntilReadyAsync()`
- 启动架构并等待进入 `Ready`
- `DestroyAsync()`
- 逆序销毁所有已接入组件
### 设计目标
- **单一职责**: 每个管理器只负责一个明确的功能
- **类型安全**: 基于泛型的组件获取和注册
- **生命周期管理**: 自动的初始化和销毁机制
- **可扩展性**: 支持模块和钩子扩展
- **向后兼容**: 保持公共 API 稳定
### 核心组件
```
Architecture (核心协调器)
├── ArchitectureBootstrapper (初始化基础设施编排)
├── ArchitectureLifecycle (生命周期管理)
├── ArchitectureComponentRegistry (组件注册)
└── ArchitectureModules (模块管理)
```
## 架构设计
### 设计模式
Architecture 采用以下设计模式:
1. **组合模式 (Composition)**: Architecture 组合多个内部协作者
2. **委托模式 (Delegation)**: 方法调用委托给专门的管理器
3. **协调器模式 (Coordinator)**: Architecture 作为协调器统一对外接口
### 类图
```
┌─────────────────────────────────────────────────────┐
│ Architecture │
│ - _bootstrapper: ArchitectureBootstrapper │
│ - _lifecycle: ArchitectureLifecycle │
│ - _componentRegistry: ArchitectureComponentRegistry│
│ - _modules: ArchitectureModules │
│ - _logger: ILogger │
│ │
│ + RegisterSystem<T>() │
│ + RegisterModel<T>() │
│ + RegisterUtility<T>() │
│ + InstallModule() │
│ + InitializeAsync() │
│ + DestroyAsync() │
│ + event PhaseChanged │
└─────────────────────────────────────────────────────┘
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ ┌──────────────┐
│ Bootstrapper │ │ Lifecycle │ │ComponentReg. │ │ Modules │
│ │ │ │ │ │ │ │
│ - 环境初始化 │ │ - 阶段管理 │ │ - System 注册│ │ - 模块安装 │
│ - 服务准备 │ │ - 钩子管理 │ │ - Model 注册 │ │ - 行为注册 │
│ - 上下文绑定 │ │ - 组件初始化 │ │ - Utility 注册│ │ │
│ - 容器冻结 │ │ - 就绪/销毁协调 │ │ - 生命周期接入│ │ │
└──────────────┘ └──────────────────┘ └──────────────┘ └──────────────┘
```
### 构造函数初始化
从 v1.1.0 开始,所有管理器在构造函数中初始化:
## 最小示例
```csharp
protected Architecture(
IArchitectureConfiguration? configuration = null,
IEnvironment? environment = null,
IArchitectureServices? services = null,
IArchitectureContext? context = null)
{
var resolvedConfiguration = configuration ?? new ArchitectureConfiguration();
var resolvedEnvironment = environment ?? new DefaultEnvironment();
var resolvedServices = services ?? new ArchitectureServices();
_context = context;
using GFramework.Core.Architectures;
// 初始化 Logger
LoggerFactoryResolver.Provider = resolvedConfiguration.LoggerProperties.LoggerFactoryProvider;
_logger = LoggerFactoryResolver.Provider.CreateLogger(GetType().Name);
// 初始化协作者
_bootstrapper = new ArchitectureBootstrapper(GetType(), resolvedEnvironment, resolvedServices, _logger);
_lifecycle = new ArchitectureLifecycle(this, resolvedConfiguration, resolvedServices, _logger);
_componentRegistry = new ArchitectureComponentRegistry(this, resolvedConfiguration, resolvedServices, _lifecycle, _logger);
_modules = new ArchitectureModules(this, resolvedServices, _logger);
}
```
**优势**:
- 消除 `null!` 断言,提高代码安全性
- 对象在构造后立即可用
- 符合"构造即完整"原则
- 可以在 InitializeAsync 之前访问事件
## 生命周期管理
### 架构阶段
Architecture 定义了 11 个生命周期阶段:
| 阶段 | 说明 | 触发时机 |
|------------------------|--------------|------------------|
| `None` | 初始状态 | 构造函数完成后 |
| `BeforeUtilityInit` | Utility 初始化前 | 开始初始化 Utility |
| `AfterUtilityInit` | Utility 初始化后 | 所有 Utility 初始化完成 |
| `BeforeModelInit` | Model 初始化前 | 开始初始化 Model |
| `AfterModelInit` | Model 初始化后 | 所有 Model 初始化完成 |
| `BeforeSystemInit` | System 初始化前 | 开始初始化 System |
| `AfterSystemInit` | System 初始化后 | 所有 System 初始化完成 |
| `Ready` | 就绪状态 | 所有组件初始化完成 |
| `Destroying` | 销毁中 | 开始销毁 |
| `Destroyed` | 已销毁 | 销毁完成 |
| `FailedInitialization` | 初始化失败 | 初始化过程中发生异常 |
### 阶段转换
```
正常流程:
None → BeforeUtilityInit → AfterUtilityInit → BeforeModelInit → AfterModelInit
→ BeforeSystemInit → AfterSystemInit → Ready → Destroying → Destroyed
异常流程:
Any → FailedInitialization
```
### 阶段事件
可以通过 `PhaseChanged` 事件监听阶段变化:
```csharp
public class MyArchitecture : Architecture
public sealed class GameArchitecture : Architecture
{
protected override void OnInitialize()
{
// 监听阶段变化
PhaseChanged += phase =>
{
Console.WriteLine($"Phase changed to: {phase}");
};
RegisterModel(new PlayerModel());
RegisterSystem(new CombatSystem());
RegisterUtility(new SaveUtility());
}
}
```
### 生命周期钩子
实现 `IArchitectureLifecycleHook` 接口可以在阶段变化时执行自定义逻辑:
启动方式:
```csharp
public class MyLifecycleHook : IArchitectureLifecycleHook
var architecture = new GameArchitecture();
await architecture.InitializeAsync();
await architecture.WaitUntilReadyAsync();
```
## 初始化时机
当前版本不再使用旧文档里的 `Init()` 入口。注册逻辑必须写在:
```csharp
protected override void OnInitialize()
{
}
```
框架会在 `InitializeAsync()` 内完成:
1. 基础设施准备
2. 创建并绑定 `ArchitectureContext`
3. 调用用户的 `OnInitialize()`
4. 按阶段初始化 `Utility -> Model -> System`
5. 进入 `Ready`
如果你还看到旧示例里写 `protected override void Init()`,那就是过时内容。
## 组件注册顺序
`Architecture` 仍然维持清晰的组件边界:
- `Model`
- 承载状态
- `System`
- 承载业务流程
- `Utility`
- 承载无状态或基础设施型能力
初始化顺序固定为:
1. `Utility`
2. `Model`
3. `System`
销毁时会按逆序处理,并优先调用异步销毁接口。
## 模块与 CQRS 接入
如果你的功能以模块形式组织,优先通过 `InstallModule(...)` 接入,而不是把所有注册逻辑都堆进一个超大的 `OnInitialize()`
如果 handlers 不只在当前架构程序集里,需要显式追加程序集:
```csharp
protected override void OnInitialize()
{
RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
RegisterCqrsHandlersFromAssemblies(
[
typeof(InventoryCqrsMarker).Assembly,
typeof(BattleCqrsMarker).Assembly
]);
}
```
默认运行时会优先尝试消费端程序集上的生成注册表;缺失或不适用时回退到反射扫描。
## 阶段与钩子
`Architecture` 公开:
- `CurrentPhase`
- `IsReady`
- `PhaseChanged`
- `RegisterLifecycleHook(...)`
如果你需要在 `Ready``Destroying` 等阶段执行横切逻辑,比起把这类逻辑塞进某个具体 `System`,更适合单独实现
`IArchitectureLifecycleHook`
```csharp
public sealed class MetricsHook : IArchitectureLifecycleHook
{
public void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
{
switch (phase)
if (phase == ArchitecturePhase.Ready)
{
case ArchitecturePhase.Ready:
Console.WriteLine("Architecture is ready!");
break;
case ArchitecturePhase.Destroying:
Console.WriteLine("Architecture is being destroyed!");
break;
Console.WriteLine("Architecture ready.");
}
}
}
// 注册钩子
architecture.RegisterLifecycleHook(new MyLifecycleHook());
```
### 初始化流程
## 什么时候看别的页面
```
1. 创建 Architecture 实例
└─> 构造函数初始化管理器
2. 调用 InitializeAsync() 或 Initialize()
├─> ArchitectureBootstrapper 准备基础设施
│ ├─> 初始化环境 (Environment.Initialize())
│ ├─> 注册内置服务模块
│ ├─> 初始化架构上下文并绑定 GameContext
│ ├─> 执行服务钩子
│ └─> 初始化服务模块
├─> 调用 OnInitialize() (用户注册组件)
├─> 初始化所有组件
│ ├─> BeforeUtilityInit → 初始化 Utility → AfterUtilityInit
│ ├─> BeforeModelInit → 初始化 Model → AfterModelInit
│ └─> BeforeSystemInit → 初始化 System → AfterSystemInit
├─> CompleteInitialization() 冻结 IoC 容器
└─> 进入 Ready 阶段
3. 等待就绪 (可选)
└─> await architecture.WaitUntilReadyAsync()
```
### 销毁流程
```
1. 调用 DestroyAsync() 或 Destroy()
├─> 检查当前阶段 (如果是 None 或已销毁则直接返回)
├─> 进入 Destroying 阶段
├─> 逆序销毁所有组件
│ ├─> 优先调用 IAsyncDestroyable.DestroyAsync()
│ └─> 否则调用 IDestroyable.Destroy()
├─> 销毁服务模块
├─> 进入 Destroyed 阶段
└─> 清空 IoC 容器
```
---
**版本**: 1.1.0
**更新日期**: 2026-03-17
**相关文档**:
- [核心框架概述](./index.md)
- 想看上下文 API转到 [context](./context.md)
- 想看阶段和销毁语义:转到 [lifecycle](./lifecycle.md)
- 想看旧命令 / 查询兼容层:转到 [command](./command.md) 和 [query](./query.md)
- 想看推荐的新请求模型:转到 [cqrs](./cqrs.md)

View File

@ -1,51 +1,29 @@
# Command 包使用说明
# Command
## 概述
本页只说明 `GFramework.Core.Command` 里的旧命令体系。
Command 包实现了命令模式Command Pattern用于封装用户操作和业务逻辑。通过命令模式可以将请求封装为对象实现操作的参数化、队列化、日志记录、撤销等功能
它仍然被保留,用来兼容存量代码;但如果你在写新功能,优先使用 [cqrs](./cqrs.md) 里的新请求模型
命令系统是 GFramework CQRS 架构的重要组成部分,与事件系统和查询系统协同工作,实现完整的业务逻辑处理流程。
## 当前仍然可用的基类
## 核心接口
旧命令体系当前最常见的三个基类是:
### ICommand
- `AbstractCommand`
- 无输入、无返回值
- `AbstractCommand<TInput>`
- 有输入、无返回值
- `AbstractCommand<TInput, TResult>`
- 有输入、有返回值
无返回值命令接口,定义了命令的基本契约。
注意一个和旧文档不同的点:泛型命令现在通过构造函数接收输入,而不是依赖 `Input` 可写属性
**核心方法:**
## 无输入命令
```csharp
void Execute(); // 执行命令
```
using GFramework.Core.Command;
using GFramework.Core.Extensions;
### ICommand`<TResult>`
带返回值的命令接口,用于需要返回执行结果的命令。
**核心方法:**
```csharp
TResult Execute(); // 执行命令并返回结果
```
## 核心类
### AbstractCommand
无返回值命令的抽象基类,提供了命令的基础实现。它继承自 ContextAwareBase具有上下文感知能力。
**核心方法:**
```csharp
void ICommand.Execute(); // 实现 ICommand 接口
protected abstract void OnExecute(); // 抽象执行方法,由子类实现
```
**使用示例:**
```csharp
// 定义一个无返回值的基础命令
public class SimpleCommand : AbstractCommand
public sealed class RestoreHealthCommand : AbstractCommand
{
protected override void OnExecute()
{
@ -54,422 +32,93 @@ public class SimpleCommand : AbstractCommand
this.SendEvent(new PlayerHealthRestoredEvent());
}
}
// 使用命令
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class GameController : IController
{
public void OnRestoreHealthButtonClicked()
{
this.SendCommand(new SimpleCommand());
}
}
```
### AbstractCommand`<TResult>`
无输入参数但带返回值的命令基类。
**核心方法:**
发送方式:
```csharp
TResult ICommand<TResult>.Execute(); // 实现 ICommand<TResult> 接口
protected abstract TResult OnExecute(); // 抽象执行方法,由子类实现
this.SendCommand(new RestoreHealthCommand());
```
**使用示例:**
## 带输入命令
旧命令输入类型现在直接复用 CQRS 抽象层里的 `ICommandInput`
```csharp
// 定义一个无输入但有返回值的命令
public class GetPlayerHealthQuery : AbstractCommand<int>
using GFramework.Core.Command;
using GFramework.Core.Extensions;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
public sealed record DamagePlayerInput(int Amount) : ICommandInput;
public sealed class DamagePlayerCommand(DamagePlayerInput input)
: AbstractCommand<DamagePlayerInput>(input)
{
protected override int OnExecute()
protected override void OnExecute(DamagePlayerInput input)
{
var playerModel = this.GetModel<PlayerModel>();
return playerModel.Health.Value;
}
}
// 使用命令
public class UISystem : AbstractSystem
{
protected override void OnInit()
{
this.RegisterEvent<UpdateUIEvent>(OnUpdateUI);
}
private void OnUpdateUI(UpdateUIEvent e)
{
var health = this.SendCommand(new GetPlayerHealthQuery());
Console.WriteLine($"Player health: {health}");
playerModel.Health.Value -= input.Amount;
}
}
```
## 命令的生命周期
1. **创建命令**:实例化命令对象,传入必要的参数
2. **执行命令**:调用 `Execute()` 方法,内部委托给 `OnExecute()`
3. **返回结果**:对于带返回值的命令,返回执行结果
4. **命令销毁**:命令执行完毕后可以被垃圾回收
**注意事项:**
- 命令应该是无状态的,执行完即可丢弃
- 避免在命令中保存长期引用
- 命令执行应该是原子操作
### 与 Store 配合使用
当某个 Model 内部使用 `Store<TState>` 管理复杂聚合状态时Command 依然是推荐的写入口。
发送方式:
```csharp
public sealed class DamagePlayerCommand(int amount) : AbstractCommand
{
protected override void OnExecute()
{
var model = this.GetModel<PlayerPanelModel>();
model.Store.Dispatch(new DamagePlayerAction(amount));
}
}
this.SendCommand(new DamagePlayerCommand(new DamagePlayerInput(10)));
```
这样可以保持现有职责边界不变:
- Controller 发送命令
- Command 执行操作
- Model 承载状态
- Store 负责统一归约状态变化
完整示例见 [`state-management`](./state-management)。
## CommandBus - 命令总线
### 功能说明
`CommandBus` 是命令执行的核心组件,负责发送和执行命令。
**主要方法:**
## 带返回值命令
```csharp
void Send(ICommand command); // 发送无返回值命令
TResult Send<TResult>(ICommand<TResult> command); // 发送带返回值命令
using GFramework.Core.Command;
using GFramework.Core.Extensions;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
public sealed record GetGoldRewardInput(int EnemyLevel) : ICommandInput;
public sealed class GetGoldRewardCommand(GetGoldRewardInput input)
: AbstractCommand<GetGoldRewardInput, int>(input)
{
protected override int OnExecute(GetGoldRewardInput input)
{
return input.EnemyLevel * 10;
}
}
```
**特点:**
- 统一的命令执行入口
- 支持同步命令执行
- 与架构上下文集成
### 使用示例
```csharp
// 通过架构获取命令总线
var commandBus = architecture.Context.CommandBus;
// 发送无返回值命令
commandBus.Send(new StartGameCommand(1, "Player1"));
// 发送带返回值命令
var damage = commandBus.Send(new CalculateDamageCommand(100, 50));
var reward = this.SendCommand(new GetGoldRewardCommand(new GetGoldRewardInput(3)));
```
## 命令基类变体
## 发送入口
框架提供了多种命令基类以满足不同需求
旧命令由 `IArchitectureContext` 的兼容入口执行:
### AbstractCommand`<TInput>`
- `SendCommand(ICommand)`
- `SendCommand<TResult>(ICommand<TResult>)`
- `SendCommandAsync(IAsyncCommand)`
- `SendCommandAsync<TResult>(IAsyncCommand<TResult>)`
带输入参数的无返回值命令类。通过 `ICommandInput` 接口传递参数。
**核心方法:**
`IContextAware` 对象内,通常直接通过扩展使用:
```csharp
void ICommand.Execute(); // 实现 ICommand 接口
protected abstract void OnExecute(TInput input); // 抽象执行方法,接收输入参数
using GFramework.Core.Extensions;
```
**使用示例:**
## 什么时候还应该用旧命令
```csharp
// 定义输入对象
public class StartGameInput : ICommandInput
{
public int LevelId { get; set; }
public string PlayerName { get; set; }
}
- 你在维护既有 `Core.Command` 代码
- 你的调用链已经依赖旧 `CommandExecutor`
- 当前改动目标是局部修复,不值得同时做 CQRS 迁移
// 定义命令
public class StartGameCommand : AbstractCommand<StartGameInput>
{
protected override void OnExecute(StartGameInput input)
{
var playerModel = this.GetModel<PlayerModel>();
var gameModel = this.GetModel<GameModel>();
## 什么时候该切到 CQRS
playerModel.PlayerName.Value = input.PlayerName;
gameModel.CurrentLevel.Value = input.LevelId;
gameModel.GameState.Value = GameState.Playing;
下面这些场景更适合新 CQRS runtime
this.SendEvent(new GameStartedEvent());
}
}
- 需要 request / notification / stream 的统一模型
- 需要 pipeline behaviors
- 需要 handler registry 生成器
- 你正在写新的业务模块,而不是维护历史命令代码
// 使用命令
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class GameController : IController
{
public void OnStartButtonClicked()
{
var input = new StartGameInput { LevelId = 1, PlayerName = "Player1" };
this.SendCommand(new StartGameCommand { Input = input });
}
}
```
### AbstractCommand`<TInput, TResult>`
既带输入参数又带返回值的命令类。
**核心方法:**
```csharp
TResult ICommand<TResult>.Execute(); // 实现 ICommand<TResult> 接口
protected abstract TResult OnExecute(TInput input); // 抽象执行方法,接收输入参数
```
**使用示例:**
```csharp
// 定义输入对象
public class CalculateDamageInput : ICommandInput
{
public int AttackerAttackPower { get; set; }
public int DefenderDefense { get; set; }
}
// 定义命令
public class CalculateDamageCommand : AbstractCommand<CalculateDamageInput, int>
{
protected override int OnExecute(CalculateDamageInput input)
{
var config = this.GetModel<GameConfigModel>();
var baseDamage = input.AttackerAttackPower - input.DefenderDefense;
var finalDamage = Math.Max(1, baseDamage * config.DamageMultiplier);
return (int)finalDamage;
}
}
// 使用命令
public class CombatSystem : AbstractSystem
{
protected override void OnInit() { }
public void Attack(Character attacker, Character defender)
{
var input = new CalculateDamageInput
{
AttackerAttackPower = attacker.AttackPower,
DefenderDefense = defender.Defense
};
var damage = this.SendCommand(new CalculateDamageCommand { Input = input });
defender.Health -= damage;
this.SendEvent(new DamageDealtEvent(attacker, defender, damage));
}
}
```
### AbstractAsyncCommand`<TInput>`
支持异步执行的带输入参数的无返回值命令基类。
**核心方法:**
```csharp
Task IAsyncCommand.ExecuteAsync(); // 实现异步命令接口
protected abstract Task OnExecuteAsync(TInput input); // 抽象异步执行方法
```
### AbstractAsyncCommand`<TInput, TResult>`
支持异步执行的既带输入参数又带返回值的命令基类。
**核心方法:**
```csharp
Task<TResult> IAsyncCommand<TResult>.ExecuteAsync(); // 实现异步命令接口
protected abstract Task<TResult> OnExecuteAsync(TInput input); // 抽象异步执行方法
```
**使用示例:**
```csharp
// 定义输入对象
public class LoadSaveDataInput : ICommandInput
{
public string SaveSlot { get; set; }
}
// 定义异步命令
public class LoadSaveDataCommand : AbstractAsyncCommand<LoadSaveDataInput, SaveData>
{
protected override async Task<SaveData> OnExecuteAsync(LoadSaveDataInput input)
{
var storage = this.GetUtility<IStorageUtility>();
return await storage.LoadSaveDataAsync(input.SaveSlot);
}
}
// 使用异步命令
public class SaveSystem : AbstractSystem
{
protected override void OnInit()
{
this.RegisterEvent<LoadGameRequestEvent>(OnLoadGameRequest);
}
private async void OnLoadGameRequest(LoadGameRequestEvent e)
{
var input = new LoadSaveDataInput { SaveSlot = e.SaveSlot };
var saveData = await this.SendCommandAsync(new LoadSaveDataCommand { Input = input });
if (saveData != null)
{
this.SendEvent(new GameLoadedEvent { SaveData = saveData });
}
}
}
```
## 命令处理器执行
所有发送给命令总线的命令最终都会通过 `CommandExecutor` 来执行:
```csharp
public class CommandExecutor
{
public static void Execute(ICommand command)
{
command.Execute();
}
public static TResult Execute<TResult>(ICommand<TResult> command)
{
return command.Execute();
}
}
```
**特点:**
- 提供统一的命令执行机制
- 支持同步和异步命令执行
- 可以扩展添加中间件逻辑
## 使用场景
### 1. 用户交互操作
```csharp
public class SaveGameCommand : AbstractCommand
{
private readonly string _saveSlot;
public SaveGameCommand(string saveSlot)
{
_saveSlot = saveSlot;
}
protected override void OnExecute()
{
var saveSystem = this.GetSystem<SaveSystem>();
var playerModel = this.GetModel<PlayerModel>();
saveSystem.SavePlayerData(playerModel, _saveSlot);
this.SendEvent(new GameSavedEvent(_saveSlot));
}
}
```
### 2. 业务流程控制
```csharp
public class LoadLevelCommand : AbstractCommand
{
private readonly int _levelId;
public LoadLevelCommand(int levelId)
{
_levelId = levelId;
}
protected override void OnExecute()
{
var levelSystem = this.GetSystem<LevelSystem>();
var uiSystem = this.GetSystem<UISystem>();
// 显示加载界面
uiSystem.ShowLoadingScreen();
// 加载关卡
levelSystem.LoadLevel(_levelId);
// 发送事件
this.SendEvent(new LevelLoadedEvent(_levelId));
}
}
```
## 最佳实践
1. **保持命令原子性**:一个命令应该完成一个完整的业务操作
2. **命令无状态**:命令不应该保存长期状态,执行完即可丢弃
3. **参数通过构造函数传递**:命令需要的参数应在创建时传入
4. **避免命令嵌套**:命令内部尽量不要发送其他命令,使用事件通信
5. **合理使用返回值**:只在确实需要返回结果时使用带返回值的命令
6. **命令命名规范**:使用动词+名词形式,如 `StartGameCommand``SavePlayerCommand`
7. **单一职责原则**:每个命令只负责一个特定的业务操作
8. **使用异步命令**:对于需要长时间执行的操作,使用异步命令避免阻塞
9. **命令验证**:在命令执行前验证输入参数的有效性
10. **错误处理**:在命令中适当处理异常情况
## 命令模式优势
### 1. 可扩展性
- 命令可以被序列化和存储
- 支持命令队列和批处理
- 便于实现撤销/重做功能
### 2. 可测试性
- 命令逻辑独立,易于单元测试
- 可以模拟命令执行结果
- 支持行为驱动开发
### 3. 可维护性
- 业务逻辑集中管理
- 降低组件间耦合度
- 便于重构和扩展
## 相关包
- [`architecture`](./architecture.md) - 架构核心,负责命令的分发和执行
- [`extensions`](./extensions.md) - 提供 `SendCommand()` 扩展方法
- [`query`](./query.md) - 查询模式,用于数据查询
- [`events`](./events.md) - 事件系统,命令执行后的通知机制
- [`system`](./system.md) - 业务系统,命令的主要执行者
- [`model`](./model.md) - 数据模型,命令操作的数据
---
**许可证**Apache 2.0
迁移后常见写法见:[cqrs](./cqrs.md)

View File

@ -1,490 +1,163 @@
# Context 上下文指南
# Context
## 概述
`IArchitectureContext` 是框架的统一上下文入口。
Context上下文是 GFramework 中的核心概念,提供了对架构服务的统一访问入口。通过 Context组件可以访问事件总线、命令总线、查询总线、IoC
容器等核心服务。
当前版本的上下文不再以“公开属性总线”作为主要模型,而是以一组明确的方法同时承载:
## 核心接口
- 组件获取
- 事件系统
- 旧 Command / Query 兼容入口
- 新 CQRS runtime 入口
### IArchitectureContext
默认实现类型是 `ArchitectureContext`
架构上下文接口,定义了对架构服务的访问契约。
## 先记住一个事实
**核心属性:**
如果你还在找旧文档里的这些属性:
- `CommandBus`
- `QueryBus`
- `EventBus`
- `Container`
那说明你看到的是旧写法。当前推荐入口是方法,不是这些属性式总线。
## 组件访问
`IArchitectureContext` 直接提供按类型获取组件的方法:
```csharp
IEventBus EventBus { get; } // 事件总线
ICommandBus CommandBus { get; } // 命令总线
IQueryBus QueryBus { get; } // 查询总线
IIocContainer Container { get; } // IoC 容器
IEnvironment Environment { get; } // 环境配置
IArchitectureConfiguration Configuration { get; } // 架构配置
ILogger Logger { get; } // 日志系统
```
## 核心类
### ArchitectureContext
架构上下文的完整实现。
**使用示例:**
```csharp
// 通过架构获取上下文
var context = architecture.Context;
// 访问各个服务
var eventBus = context.EventBus;
var commandBus = context.CommandBus;
var queryBus = context.QueryBus;
var container = context.Container;
var environment = context.Environment;
var logger = context.Logger;
var model = context.GetModel<PlayerModel>();
var system = context.GetSystem<CombatSystem>();
var utility = context.GetUtility<SaveUtility>();
var service = context.GetService<IMyService>();
```
### GameContext
也支持批量获取和按优先级获取:
游戏上下文类,管理架构类型与上下文实例的映射关系。
- `GetModels<T>()`
- `GetSystems<T>()`
- `GetUtilities<T>()`
- `GetServices<T>()`
- `GetModelsByPriority<T>()`
- `GetSystemsByPriority<T>()`
- `GetUtilitiesByPriority<T>()`
- `GetServicesByPriority<T>()`
**核心方法:**
## 在 `IContextAware` 对象里怎么用
大多数业务代码不会手动把 `architecture.Context` 传来传去,而是通过 `IContextAware` 扩展方法访问上下文:
```csharp
// 绑定架构类型到上下文
static void Bind<TArchitecture>(IArchitectureContext context)
where TArchitecture : IArchitecture;
using GFramework.Core.Extensions;
// 获取架构类型对应的上下文
static IArchitectureContext GetContext<TArchitecture>()
where TArchitecture : IArchitecture;
// 解绑架构类型
static void Unbind<TArchitecture>()
where TArchitecture : IArchitecture;
```
## 在组件中使用 Context
### 在 Model 中使用
```csharp
public class PlayerModel : AbstractModel
{
public BindableProperty<int> Health { get; } = new(100);
protected override void OnInit()
{
// 通过 Context 访问事件总线
var context = this.GetContext();
var eventBus = context.EventBus;
// 监听生命值变化
Health.Register(hp =>
{
if (hp <= 0)
{
// 发送事件
eventBus.Send(new PlayerDiedEvent());
}
});
}
}
```
### 在 System 中使用
```csharp
public class CombatSystem : AbstractSystem
{
protected override void OnInit()
{
// 通过 Context 访问各个服务
var context = this.GetContext();
var eventBus = context.EventBus;
var commandBus = context.CommandBus;
var container = context.Container;
// 注册事件监听
eventBus.Register<EnemyAttackEvent>(OnEnemyAttack);
}
private void OnEnemyAttack(EnemyAttackEvent e)
{
var context = this.GetContext();
var playerModel = context.Container.Get<PlayerModel>();
// 处理伤害
playerModel.Health.Value -= e.Damage;
}
}
```
### 在 Command 中使用
```csharp
public class StartGameCommand : AbstractCommand
public sealed class DamagePlayerCommand : AbstractCommand
{
protected override void OnExecute()
{
// 通过 Context 访问服务
var context = this.GetContext();
var container = context.Container;
var eventBus = context.EventBus;
var playerModel = container.Get<PlayerModel>();
playerModel.Health.Value = playerModel.MaxHealth.Value;
eventBus.Send(new GameStartedEvent());
var playerModel = this.GetModel<PlayerModel>();
playerModel.Health.Value -= 10;
}
}
```
### 在 Query 中使用
常用扩展包括:
- `GetModel<T>()`
- `GetSystem<T>()`
- `GetUtility<T>()`
- `GetService<T>()`
- `SendEvent(...)`
- `RegisterEvent(...)`
- `SendCommand(...)`
- `SendQuery(...)`
## 事件入口
框架事件系统仍然由上下文统一暴露:
```csharp
public class GetPlayerHealthQuery : AbstractQuery<int>
context.SendEvent(new PlayerDiedEvent());
var unRegister = context.RegisterEvent<PlayerDiedEvent>(static e =>
{
protected override int OnDo()
{
// 通过 Context 访问容器
var context = this.GetContext();
var playerModel = context.Container.Get<PlayerModel>();
return playerModel.Health.Value;
}
}
```
## GameContext 的使用
### 绑定架构到 GameContext
```csharp
public class GameArchitecture : Architecture
{
protected override void Init()
{
// 注册组件
RegisterModel(new PlayerModel());
RegisterSystem(new CombatSystem());
}
}
// 在应用启动时绑定
var architecture = new GameArchitecture();
await architecture.InitializeAsync();
// 绑定架构到 GameContext
GameContext.Bind<GameArchitecture>(architecture.Context);
```
### 从 GameContext 获取上下文
```csharp
// 在任何地方获取架构上下文
var context = GameContext.GetContext<GameArchitecture>();
// 访问服务
var playerModel = context.Container.Get<PlayerModel>();
var eventBus = context.EventBus;
```
### 使用 GameContext 的扩展方法
```csharp
// 通过扩展方法简化访问
public static class GameContextExtensions
{
public static T GetModel<T>(this IArchitectureContext context)
where T : class, IModel
{
return context.Container.Get<T>();
}
public static T GetSystem<T>(this IArchitectureContext context)
where T : class, ISystem
{
return context.Container.Get<T>();
}
}
// 使用
var context = GameContext.GetContext<GameArchitecture>();
var playerModel = context.GetModel<PlayerModel>();
var combatSystem = context.GetSystem<CombatSystem>();
```
## Context 中的服务
### EventBus - 事件总线
```csharp
var context = architecture.Context;
var eventBus = context.EventBus;
// 注册事件
eventBus.Register<PlayerDiedEvent>(e =>
{
Console.WriteLine("Player died!");
Console.WriteLine("Player died.");
});
// 发送事件
eventBus.Send(new PlayerDiedEvent());
```
### CommandBus - 命令总线
`IContextAware` 对象里也可以直接用扩展:
```csharp
var context = architecture.Context;
var commandBus = context.CommandBus;
// 发送命令
commandBus.Send(new StartGameCommand());
// 发送带返回值的命令
var damage = commandBus.Send(new CalculateDamageCommand { Input = input });
this.SendEvent(new PlayerDiedEvent());
```
### QueryBus - 查询总线
## 旧 Command / Query 兼容入口
当前上下文仍保留旧命令 / 查询体系:
- `SendCommand(ICommand)`
- `SendCommand<TResult>(ICommand<TResult>)`
- `SendCommandAsync(IAsyncCommand)`
- `SendCommandAsync<TResult>(IAsyncCommand<TResult>)`
- `SendQuery<TResult>(IQuery<TResult>)`
- `SendQueryAsync<TResult>(IAsyncQuery<TResult>)`
这部分入口主要用于兼容存量代码。新功能优先看 [cqrs](./cqrs.md)。
## 新 CQRS 入口
`IArchitectureContext` 也是当前 CQRS runtime 的主入口。最重要的方法是:
- `SendRequestAsync(...)`
- `SendRequest(...)`
- `SendAsync(...)`
- `PublishAsync(...)`
- `CreateStream(...)`
- `SendCommandAsync(...)` / `SendQueryAsync(...)` 的 CQRS 重载
示例:
```csharp
var context = architecture.Context;
var queryBus = context.QueryBus;
// 发送查询
var health = queryBus.Send(new GetPlayerHealthQuery { Input = new EmptyQueryInput() });
var playerId = await architecture.Context.SendRequestAsync(
new CreatePlayerCommand(new CreatePlayerInput("Alice")));
```
### Container - IoC 容器
如果你在 `IContextAware` 对象内部,通常直接用 `GFramework.Cqrs.Extensions` 里的扩展:
```csharp
var context = architecture.Context;
var container = context.Container;
using GFramework.Cqrs.Extensions;
// 获取已注册的组件
var playerModel = container.Get<PlayerModel>();
var combatSystem = container.Get<CombatSystem>();
// 获取所有实现某接口的组件
var allSystems = container.GetAll<ISystem>();
var playerId = await this.SendAsync(
new CreatePlayerCommand(new CreatePlayerInput("Alice")));
```
### Environment - 环境配置
## `GameContext` 现在是什么角色
```csharp
var context = architecture.Context;
var environment = context.Environment;
`GameContext` 仍然存在,但已经退到兼容和回退路径。
// 获取环境值
var gameMode = environment.Get<string>("GameMode");
var maxPlayers = environment.Get<int>("MaxPlayers");
`ContextAwareBase` 在实例未显式注入上下文时,会回退到 `GameContext.GetFirstArchitectureContext()`。这能保证部分旧代码继续工作,但它不是新代码的首选接法。
// 安全获取值
if (environment.TryGet<string>("ServerAddress", out var address))
{
Console.WriteLine($"Server: {address}");
}
```
新代码更推荐:
### Logger - 日志系统
- 让对象通过框架流程注入 `IArchitectureContext`
- 或使用 `[ContextAware]` 生成路径
- 或显式从 `architecture.Context` 启动调用链
```csharp
var context = architecture.Context;
var logger = context.Logger;
## 什么时候需要手动拿 `architecture.Context`
// 记录日志
logger.Log("Game started");
logger.LogWarning("Low memory");
logger.LogError("Failed to load resource");
```
以下场景适合直接使用 `architecture.Context`
## Context 的生命周期
- 组合根或启动代码
- 非 `IContextAware` 对象
- 测试中显式驱动请求和事件
- 你要清楚地区分“旧 Command / Query 兼容入口”和“新 CQRS 入口”
### 创建
## 继续阅读
Context 在架构初始化时自动创建:
```csharp
var architecture = new GameArchitecture();
// Context 在这里被创建
var context = architecture.Context;
```
### 使用
Context 在架构的整个生命周期中可用:
```csharp
// 初始化期间
await architecture.InitializeAsync();
// Ready 阶段
var context = architecture.Context;
var playerModel = context.Container.Get<PlayerModel>();
// 销毁前
architecture.Destroy();
```
### 销毁
Context 随着架构的销毁而销毁:
```csharp
architecture.Destroy();
// Context 不再可用
```
## 最佳实践
### 1. 通过扩展方法简化访问
```csharp
public static class ContextExtensions
{
public static T GetModel<T>(this IArchitectureContext context)
where T : class, IModel
{
return context.Container.Get<T>();
}
public static T GetSystem<T>(this IArchitectureContext context)
where T : class, ISystem
{
return context.Container.Get<T>();
}
public static void SendCommand(this IArchitectureContext context, ICommand command)
{
context.CommandBus.Send(command);
}
public static TResult SendQuery<TResult>(this IArchitectureContext context, IQuery<TResult> query)
{
return context.QueryBus.Send(query);
}
}
// 使用
var context = architecture.Context;
var playerModel = context.GetModel<PlayerModel>();
context.SendCommand(new StartGameCommand());
```
### 2. 缓存 Context 引用
```csharp
public class GameSystem : AbstractSystem
{
private IArchitectureContext _context;
protected override void OnInit()
{
// 缓存 Context 引用
_context = this.GetContext();
// 后续使用缓存的引用
_context.EventBus.Register<GameStartedEvent>(OnGameStarted);
}
private void OnGameStarted(GameStartedEvent e)
{
var playerModel = _context.Container.Get<PlayerModel>();
}
}
```
### 3. 使用 GameContext 实现全局访问
```csharp
// 在应用启动时绑定
public class GameBootstrapper
{
public async Task StartAsync()
{
var architecture = new GameArchitecture();
await architecture.InitializeAsync();
// 绑定到 GameContext
GameContext.Bind<GameArchitecture>(architecture.Context);
}
}
// 在任何地方访问
public class UIController
{
public void UpdateHealthDisplay()
{
var context = GameContext.GetContext<GameArchitecture>();
var playerModel = context.Container.Get<PlayerModel>();
// 更新 UI
healthText.text = playerModel.Health.Value.ToString();
}
}
```
### 4. 处理 Context 不可用的情况
```csharp
public class SafeGameSystem : AbstractSystem
{
protected override void OnInit()
{
try
{
var context = this.GetContext();
if (context == null)
{
Console.WriteLine("Context not available");
return;
}
var playerModel = context.Container.Get<PlayerModel>();
}
catch (Exception ex)
{
Console.WriteLine($"Error accessing context: {ex.Message}");
}
}
}
```
## Context vs Architecture
### Architecture
- **职责**:管理组件的生命周期
- **作用**:注册、初始化、销毁组件
- **访问**:通过 `GetArchitecture()` 获取
### Context
- **职责**:提供对架构服务的访问
- **作用**:访问事件总线、命令总线、查询总线等
- **访问**:通过 `GetContext()` 获取
```csharp
// Architecture 用于管理
var architecture = GameArchitecture.Interface;
architecture.RegisterModel(new PlayerModel());
// Context 用于访问服务
var context = architecture.Context;
var playerModel = context.Container.Get<PlayerModel>();
```
## 相关包
- [`architecture`](./architecture.md) - 架构核心,创建和管理 Context
- [`ioc`](./ioc.md) - IoC 容器,通过 Context 访问
- [`events`](./events.md) - 事件总线,通过 Context 访问
- [`command`](./command.md) - 命令总线,通过 Context 访问
- [`query`](./query.md) - 查询总线,通过 Context 访问
- [`environment`](./environment.md) - 环境配置,通过 Context 访问
- [`logging`](./logging.md) - 日志系统,通过 Context 访问
---
**许可证**Apache 2.0
- 架构入口:[architecture](./architecture.md)
- 生命周期:[lifecycle](./lifecycle.md)
- 旧命令系统:[command](./command.md)
- 旧查询系统:[query](./query.md)
- 新 CQRS runtime[cqrs](./cqrs.md)

View File

@ -1,656 +1,171 @@
---
title: CQRS
description: GFramework 内建 CQRS runtime用统一请求分发、通知发布和流式处理组织业务逻辑
description: 当前推荐的新请求模型,统一覆盖 command、query、notification、stream request 和 pipeline behaviors
---
# CQRS
## 概述
`GFramework.Cqrs` 是当前推荐的新请求模型 runtime。
CQRSCommand Query Responsibility Segregation命令查询职责分离是一种架构模式将数据的读取Query和修改Command操作分离。GFramework
当前内建自有 CQRS runtime通过统一的请求分发器、通知发布和流式请求管道提供类型安全、解耦的业务逻辑处理方式。
如果你在写新功能,优先使用这套模型,而不是继续扩展 `GFramework.Core.Command` / `Query` 的兼容层。
通过 CQRS你可以将复杂的业务逻辑拆分为独立的命令和查询处理器每个处理器只负责单一职责使代码更易于测试和维护。
**主要特性**
- 命令查询职责分离
- 内建请求分发与解耦设计
- 支持管道行为Behaviors
- 异步处理支持
- 与架构系统深度集成
- 支持流式处理
## 接入包
按模块安装 CQRS runtime如果希望在编译期生成 handler 注册表,再额外安装对应的 source generator
## 安装方式
```bash
dotnet add package GeWuYou.GFramework.Cqrs
dotnet add package GeWuYou.GFramework.Cqrs.Abstractions
```
# 可选:编译期生成 handler registry减少冷启动反射扫描
如果你希望消费端程序集在编译期生成 handler registry再额外安装
```bash
dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
```
## 核心概念
## 先理解分层
### Command命令
- `GFramework.Cqrs.Abstractions`
- 纯契约层,定义请求、处理器、行为等接口
- `GFramework.Cqrs`
- 默认 runtime、dispatcher、处理器基类和上下文扩展
- `GFramework.Cqrs.SourceGenerators`
- 可选生成器,为消费端程序集生成 `ICqrsHandlerRegistry`
命令表示修改系统状态的操作,如创建、更新、删除:
## 最小示例
```csharp
using GFramework.Cqrs.Command;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
消息基类和处理器基类在不同命名空间:
// 定义命令输入
public class CreatePlayerInput : ICommandInput
{
public string Name { get; set; }
public int Level { get; set; }
}
- 消息基类:`GFramework.Cqrs.Command` / `Query` / `Notification`
- 处理器基类:`GFramework.Cqrs.Cqrs.Command` / `Query` / `Notification`
// 定义命令
public class CreatePlayerCommand : CommandBase<CreatePlayerInput, int>
{
public CreatePlayerCommand(CreatePlayerInput input) : base(input) { }
}
```
### Query查询
查询表示读取系统状态的操作,不修改数据:
```csharp
using GFramework.Cqrs.Query;
using GFramework.Cqrs.Abstractions.Cqrs.Query;
// 定义查询输入
public class GetPlayerInput : IQueryInput
{
public int PlayerId { get; set; }
}
// 定义查询
public class GetPlayerQuery : QueryBase<GetPlayerInput, PlayerData>
{
public GetPlayerQuery(GetPlayerInput input) : base(input) { }
}
```
### Handler处理器
处理器负责执行命令或查询的具体逻辑:
示例:
```csharp
using GFramework.Cqrs.Command;
using GFramework.Cqrs.Cqrs.Command;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
// 命令处理器
public class CreatePlayerCommandHandler : AbstractCommandHandler<CreatePlayerCommand, int>
public sealed record CreatePlayerInput(string Name) : ICommandInput;
public sealed class CreatePlayerCommand(CreatePlayerInput input)
: CommandBase<CreatePlayerInput, int>(input)
{
public override async ValueTask<int> Handle(
}
public sealed class CreatePlayerCommandHandler
: AbstractCommandHandler<CreatePlayerCommand, int>
{
public override ValueTask<int> Handle(
CreatePlayerCommand command,
CancellationToken cancellationToken)
{
var input = command.Input;
var playerModel = this.GetModel<PlayerModel>();
// 创建玩家
var playerId = playerModel.CreatePlayer(input.Name, input.Level);
return playerId;
var playerModel = Context.GetModel<PlayerModel>();
var playerId = playerModel.Create(command.Input.Name);
return ValueTask.FromResult(playerId);
}
}
```
> 说明:消息基类位于 `GFramework.Cqrs.Command` / `Query` / `Notification` 命名空间,而处理器基类位于
> `GFramework.Cqrs.Cqrs.*` 命名空间。编写最小示例时需要同时引用对应的消息与 handler 命名空间。
## 发送请求
### Dispatcher请求分发器
架构上下文会负责将命令、查询和通知路由到对应的处理器:
如果你在 `IContextAware` 对象内部:
```csharp
// 通过架构上下文发送命令
var command = new CreatePlayerCommand(new CreatePlayerInput
{
Name = "Player1",
Level = 1
});
using GFramework.Cqrs.Extensions;
var playerId = await this.SendAsync(command);
var playerId = await this.SendAsync(
new CreatePlayerCommand(new CreatePlayerInput("Alice")));
```
## 基本用法
### 定义和发送命令
如果你在组合根或测试里:
```csharp
// 1. 定义命令输入
public class SaveGameInput : ICommandInput
var playerId = await architecture.Context.SendRequestAsync(
new CreatePlayerCommand(new CreatePlayerInput("Alice")));
```
最常用的上下文入口有:
- `SendRequestAsync(...)`
- `SendAsync(...)`
- `SendQueryAsync(...)`
- `PublishAsync(...)`
- `CreateStream(...)`
## 查询、通知和流
这套 runtime 不只处理 command也统一处理
- Query
- 读路径请求
- Notification
- 一对多广播
- Stream Request
- 返回 `IAsyncEnumerable<T>`
也就是说,新代码通常不需要再分别设计“命令总线”“查询总线”和另一套通知分发语义。
## 注册处理器
在标准 `Architecture` 启动路径中CQRS runtime 会自动接入基础设施。你通常只需要在 `OnInitialize()` 里追加行为或额外程序集:
```csharp
protected override void OnInitialize()
{
public int SlotId { get; set; }
public GameData Data { get; set; }
}
RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
RegisterCqrsPipelineBehavior<PerformanceBehavior<,>>();
// 2. 定义命令
public class SaveGameCommand : CommandBase<SaveGameInput, Unit>
{
public SaveGameCommand(SaveGameInput input) : base(input) { }
}
// 3. 实现命令处理器
public class SaveGameCommandHandler : AbstractCommandHandler<SaveGameCommand>
{
public override async ValueTask<Unit> Handle(
SaveGameCommand command,
CancellationToken cancellationToken)
{
var input = command.Input;
var saveSystem = this.GetSystem<SaveSystem>();
// 保存游戏
await saveSystem.SaveAsync(input.SlotId, input.Data);
// 发送事件
this.SendEvent(new GameSavedEvent { SlotId = input.SlotId });
return Unit.Value;
}
}
// 4. 发送命令
public async Task SaveGame()
{
var command = new SaveGameCommand(new SaveGameInput
{
SlotId = 1,
Data = currentGameData
});
await this.SendAsync(command);
RegisterCqrsHandlersFromAssemblies(
[
typeof(InventoryCqrsMarker).Assembly,
typeof(BattleCqrsMarker).Assembly
]);
}
```
### 定义和发送查询
默认逻辑会:
1. 优先使用消费端程序集上的生成注册器
2. 生成注册器不可用时回退到反射扫描
3. 对同一程序集去重,避免重复注册
## Pipeline Behavior
如果你需要围绕请求处理流程插入横切逻辑,使用:
```csharp
// 1. 定义查询输入
public class GetHighScoresInput : IQueryInput
{
public int Count { get; set; } = 10;
}
// 2. 定义查询
public class GetHighScoresQuery : QueryBase<GetHighScoresInput, List<ScoreData>>
{
public GetHighScoresQuery(GetHighScoresInput input) : base(input) { }
}
// 3. 实现查询处理器
public class GetHighScoresQueryHandler : AbstractQueryHandler<GetHighScoresQuery, List<ScoreData>>
{
public override async ValueTask<List<ScoreData>> Handle(
GetHighScoresQuery query,
CancellationToken cancellationToken)
{
var input = query.Input;
var scoreModel = this.GetModel<ScoreModel>();
// 查询高分榜
var scores = await scoreModel.GetTopScoresAsync(input.Count);
return scores;
}
}
// 4. 发送查询
public async Task<List<ScoreData>> GetHighScores()
{
var query = new GetHighScoresQuery(new GetHighScoresInput
{
Count = 10
});
var scores = await this.SendQueryAsync(query);
return scores;
}
```
### 注册处理器
在架构中注册 CQRS 行为;默认会自动接入当前架构所在程序集和 `GFramework.Core` 程序集中的处理器:
```csharp
public class GameArchitecture : Architecture
{
protected override void OnInitialize()
{
// 注册通用开放泛型行为
RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
RegisterCqrsPipelineBehavior<PerformanceBehavior<,>>();
// 默认只自动扫描当前架构程序集和 GFramework.Core 程序集中的处理器
}
}
```
当前版本会优先使用源码生成的程序集级 handler registry 来注册“当前业务程序集”里的处理器;
如果该程序集没有生成注册器,或者包含生成代码无法合法引用的处理器类型,则会自动回退到运行时反射扫描。
`GFramework.Core` 等未挂接该生成器的程序集仍会继续走反射扫描。
如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是只依赖默认接入范围:
```csharp
public class GameArchitecture : Architecture
{
protected override void OnInitialize()
{
RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
RegisterCqrsHandlersFromAssemblies(
[
typeof(InventoryCqrsMarker).Assembly,
typeof(BattleCqrsMarker).Assembly
]);
}
}
```
`RegisterCqrsHandlersFromAssembly(...)` / `RegisterCqrsHandlersFromAssemblies(...)` 会复用与默认启动路径相同的注册逻辑:
优先使用程序集级生成注册器,失败时自动回退到反射扫描;如果同一程序集已经由默认路径或其他模块接入,框架会自动去重,避免重复注册
handler。
`RegisterCqrsPipelineBehavior<TBehavior>()` 是唯一保留的公开入口;旧的 `Mediator` 兼容别名与扩展已移除,不再继续维护。
如果你正在从旧版本迁移,只需要直接改用 `RegisterCqrsPipelineBehavior<TBehavior>()`
`RegisterMediatorBehavior<TBehavior>()` 已移除,不再保留兼容入口。
当前接口支持两种形式:
- 开放泛型行为,例如 `LoggingBehavior<,>`,用于匹配所有请求
- 封闭行为类型,例如某个只服务于单一请求的 `SpecialBehavior`
## 高级用法
### Request请求
Request 是更通用的消息类型,可以用于任何场景:
```csharp
using GFramework.Cqrs.Request;
using GFramework.Cqrs.Abstractions.Cqrs.Request;
// 定义请求输入
public class ValidatePlayerInput : IRequestInput
{
public string PlayerName { get; set; }
}
// 定义请求
public class ValidatePlayerRequest : RequestBase<ValidatePlayerInput, bool>
{
public ValidatePlayerRequest(ValidatePlayerInput input) : base(input) { }
}
// 实现请求处理器
public class ValidatePlayerRequestHandler : AbstractRequestHandler<ValidatePlayerRequest, bool>
{
public override async ValueTask<bool> Handle(
ValidatePlayerRequest request,
CancellationToken cancellationToken)
{
var input = request.Input;
var playerModel = this.GetModel<PlayerModel>();
// 验证玩家名称
var isValid = await playerModel.IsNameValidAsync(input.PlayerName);
return isValid;
}
}
```
### Notification通知
Notification 用于一对多的消息广播:
```csharp
using GFramework.Cqrs.Notification;
using GFramework.Cqrs.Abstractions.Cqrs.Notification;
// 定义通知输入
public class PlayerLevelUpInput : INotificationInput
{
public int PlayerId { get; set; }
public int NewLevel { get; set; }
}
// 定义通知
public class PlayerLevelUpNotification : NotificationBase<PlayerLevelUpInput>
{
public PlayerLevelUpNotification(PlayerLevelUpInput input) : base(input) { }
}
// 实现通知处理器 1
public class AchievementNotificationHandler : AbstractNotificationHandler<PlayerLevelUpNotification>
{
public override async ValueTask Handle(
PlayerLevelUpNotification notification,
CancellationToken cancellationToken)
{
var input = notification.Input;
// 检查成就
CheckLevelAchievements(input.PlayerId, input.NewLevel);
await Task.CompletedTask;
}
}
// 实现通知处理器 2
public class RewardNotificationHandler : AbstractNotificationHandler<PlayerLevelUpNotification>
{
public override async ValueTask Handle(
PlayerLevelUpNotification notification,
CancellationToken cancellationToken)
{
var input = notification.Input;
// 发放奖励
GiveRewards(input.PlayerId, input.NewLevel);
await Task.CompletedTask;
}
}
// 发布通知(所有处理器都会收到)
var notification = new PlayerLevelUpNotification(new PlayerLevelUpInput
{
PlayerId = 1,
NewLevel = 10
});
await this.PublishAsync(notification);
```
### Pipeline Behaviors管道行为
Behaviors 可以在处理器执行前后添加横切关注点:
```csharp
using GFramework.Core.Abstractions.Cqrs;
// 日志行为
public class LoggingBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
where TMessage : IRequest<TResponse>
{
public async ValueTask<TResponse> Handle(
TMessage message,
MessageHandlerDelegate<TMessage, TResponse> next,
CancellationToken cancellationToken)
{
var messageName = message.GetType().Name;
Console.WriteLine($"[开始] {messageName}");
var response = await next(message, cancellationToken);
Console.WriteLine($"[完成] {messageName}");
return response;
}
}
// 性能监控行为
public class PerformanceBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
where TMessage : IRequest<TResponse>
{
public async ValueTask<TResponse> Handle(
TMessage message,
MessageHandlerDelegate<TMessage, TResponse> next,
CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
var response = await next(message, cancellationToken);
stopwatch.Stop();
var elapsed = stopwatch.ElapsedMilliseconds;
if (elapsed > 100)
{
Console.WriteLine($"警告: {message.GetType().Name} 耗时 {elapsed}ms");
}
return response;
}
}
// 注册行为
RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
RegisterCqrsPipelineBehavior<PerformanceBehavior<,>>();
```
### 验证行为
适合的场景包括:
```csharp
public class ValidationBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
where TMessage : IRequest<TResponse>
{
public async ValueTask<TResponse> Handle(
TMessage message,
MessageHandlerDelegate<TMessage, TResponse> next,
CancellationToken cancellationToken)
{
// 验证输入
if (message is IValidatable validatable)
{
var errors = validatable.Validate();
if (errors.Any())
{
throw new ValidationException(errors);
}
}
- 日志
- 性能统计
- 校验
- 审计
- 重试或统一异常封装
return await next(message, cancellationToken);
}
}
```
旧的 `Mediator` 兼容别名入口已经移除;当前公开入口只有 `RegisterCqrsPipelineBehavior<TBehavior>()`
### 流式处理
## 和旧 Command / Query 的关系
处理大量数据时使用流式处理
当前仓库同时存在两套路径:
```csharp
// 流式查询
public class GetAllPlayersStreamQuery : QueryBase<EmptyInput, IAsyncEnumerable<PlayerData>>
{
public GetAllPlayersStreamQuery() : base(new EmptyInput()) { }
}
- 旧路径
- `GFramework.Core.Command`
- `GFramework.Core.Query`
- 新路径
- `GFramework.Cqrs`
// 流式查询处理器
public class GetAllPlayersStreamQueryHandler : AbstractStreamQueryHandler<GetAllPlayersStreamQuery, PlayerData>
{
public override async IAsyncEnumerable<PlayerData> Handle(
GetAllPlayersStreamQuery query,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var playerModel = this.GetModel<PlayerModel>();
`IArchitectureContext` 仍然会兼容旧入口,但新代码应优先使用 CQRS runtime。
await foreach (var player in playerModel.GetAllPlayersAsync(cancellationToken))
{
yield return player;
}
}
}
一个简单判断规则:
// 使用流式查询
var query = new GetAllPlayersStreamQuery();
var stream = this.CreateStream(query);
- 在维护历史代码:允许继续使用旧 Command / Query
- 在写新功能或新模块:优先使用 CQRS
await foreach (var player in stream)
{
Console.WriteLine($"玩家: {player.Name}");
}
```
## 继续阅读
## 最佳实践
1. **命令和查询分离**:严格区分修改和读取操作
```csharp
✓ CreatePlayerCommand, GetPlayerQuery // 职责清晰
✗ PlayerCommand // 职责不明确
```
2. **使用有意义的命名**:命令用动词,查询用 Get
```csharp
✓ CreatePlayerCommand, UpdateScoreCommand, GetHighScoresQuery
✗ PlayerCommand, ScoreCommand, ScoresQuery
```
3. **输入验证**:在处理器中验证输入
```csharp
public override async ValueTask<int> Handle(...)
{
if (string.IsNullOrEmpty(command.Input.Name))
throw new ArgumentException("Name is required");
// 处理逻辑
}
```
4. **使用 Behaviors 处理横切关注点**:日志、性能、验证等
```csharp
RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
RegisterCqrsPipelineBehavior<ValidationBehavior<,>>();
```
5. **保持处理器简单**:一个处理器只做一件事
```csharp
✓ 处理器只负责业务逻辑,通过架构组件访问数据
✗ 处理器中包含复杂的数据访问和业务逻辑
```
6. **使用 CancellationToken**:支持操作取消
```csharp
public override async ValueTask<T> Handle(..., CancellationToken cancellationToken)
{
await someAsyncOperation(cancellationToken);
}
```
## 常见问题
### 问题Command 和 Query 有什么区别?
**解答**
- **Command**:修改系统状态,可能有副作用,通常返回 void 或简单结果
- **Query**:只读取数据,无副作用,返回查询结果
```csharp
// Command: 修改状态
CreatePlayerCommand -> 创建玩家
UpdateScoreCommand -> 更新分数
// Query: 读取数据
GetPlayerQuery -> 获取玩家信息
GetHighScoresQuery -> 获取高分榜
```
### 问题:什么时候使用 Request
**解答**
Request 是更通用的消息类型,当操作既不是纯命令也不是纯查询时使用:
```csharp
// 验证操作:读取数据并返回结果,但不修改状态
ValidatePlayerRequest
// 计算操作:基于输入计算结果
CalculateDamageRequest
```
### 问题Notification 和 Event 有什么区别?
**解答**
- **Notification**:通过内建 CQRS runtime 发送,处理器在同一请求上下文中执行
- **Event**:通过 EventBus 发送,监听器异步执行
```csharp
// Notification: 同步处理
await this.PublishAsync(notification); // 等待所有处理器完成
// Event: 异步处理
this.SendEvent(event); // 立即返回,监听器异步执行
```
### 问题:如何处理命令失败?
**解答**
使用异常或返回 Result 类型:
```csharp
// 方式 1: 抛出异常
public override async ValueTask<Unit> Handle(...)
{
if (!IsValid())
throw new InvalidOperationException("Invalid operation");
return Unit.Value;
}
// 方式 2: 返回 Result
public override async ValueTask<Result> Handle(...)
{
if (!IsValid())
return Result.Failure("Invalid operation");
return Result.Success();
}
```
### 问题:处理器可以调用其他处理器吗?
**解答**
可以,通过架构上下文继续发送新的命令或查询:
```csharp
public override async ValueTask<Unit> Handle(...)
{
// 调用其他命令
await this.SendAsync(new AnotherCommand(...));
return Unit.Value;
}
```
### 问题:如何测试处理器?
**解答**
处理器是独立的类,易于单元测试:
```csharp
[Test]
public async Task CreatePlayer_ShouldReturnPlayerId()
{
// Arrange
var handler = new CreatePlayerCommandHandler();
handler.SetContext(mockContext);
var command = new CreatePlayerCommand(new CreatePlayerInput
{
Name = "Test",
Level = 1
});
// Act
var playerId = await handler.Handle(command, CancellationToken.None);
// Assert
Assert.That(playerId, Is.GreaterThan(0));
}
```
## 相关文档
- [命令系统](/zh-CN/core/command) - 传统命令模式
- [查询系统](/zh-CN/core/query) - 传统查询模式
- [事件系统](/zh-CN/core/events) - 事件驱动架构
- [协程系统](/zh-CN/core/coroutine) - 在协程中使用 CQRS
- 架构入口:[architecture](./architecture.md)
- 上下文入口:[context](./context.md)
- 模块 README`GFramework.Cqrs/README.md`

View File

@ -1,674 +1,107 @@
# GFramework.Core 核心框架
# Core
> 一个基于 CQRS、MVC 和事件驱动的轻量级游戏开发架构框架
`Core` 栏目对应 `GFramework` 的基础运行时层,主要覆盖 `GFramework.Core``GFramework.Core.Abstractions`,以及与之直接相邻的旧版
`Command` / `Query` 执行器和新版 `CQRS` 迁移入口。
## 目录
如果你第一次接入框架,建议先把这里当作“运行时底座说明”,再按需进入 `Game``Godot` 或 Source Generators 栏目。
- [框架概述](#框架概述)
- [核心概念](#核心概念)
- [架构图](#架构图)
- [快速开始](#快速开始)
- [包说明](#包说明)
- [组件联动](#组件联动)
- [最佳实践](#最佳实践)
- [设计理念](#设计理念)
## 先理解包关系
## 框架概述
- `GeWuYou.GFramework.Core`
- 基础运行时实现,包含 `Architecture`、上下文、生命周期、事件、属性、状态、资源、日志、协程、IoC 等能力。
- `GeWuYou.GFramework.Core.Abstractions`
- 对应的契约层,适合只依赖接口、做模块拆分或测试替身。
- `GeWuYou.GFramework.Cqrs`
- 推荐给新功能使用的新请求模型运行时。
- `GeWuYou.GFramework.Game`
- 在 `Core` 之上叠加游戏层配置、数据、设置、场景与 UI。
- `GeWuYou.GFramework.Core.SourceGenerators`
- 在编译期补齐日志、上下文注入、模块自动注册等样板代码。
本框架是一个与平台无关的轻量级架构,它结合了多种经典设计模式:
如果你只想先把架构跑起来,最小安装组合仍是
- **MVC 架构模式** - 清晰的层次划分
- **CQRS 模式** - 命令查询职责分离
- **IoC/DI** - 依赖注入和控制反转
- **事件驱动** - 松耦合的组件通信
- **响应式编程** - 可绑定属性和数据流
- **阶段式生命周期管理** - 精细化的架构状态控制
**重要说明**GFramework.Core 是与平台无关的核心模块,不包含任何 Godot 特定代码。Godot 集成功能在 GFramework.Godot 包中实现。
### 核心特性
- **清晰的分层架构** - Model、View、Controller、System、Utility 各司其职
- **类型安全** - 基于泛型的组件获取和事件系统
- **松耦合** - 通过事件和接口实现组件解耦
- **易于测试** - 依赖注入和纯函数设计
- **可扩展** - 基于接口的规则体系
- **生命周期管理** - 自动的注册和注销机制
- **模块化** - 支持架构模块安装
- **平台无关** - Core 模块可以在任何 .NET 环境中使用
## 核心概念
### 五层架构
```
┌─────────────────────────────────────────┐
│ View / UI │ UI 层:用户界面
├─────────────────────────────────────────┤
│ Controller │ 控制层:连接 UI 和业务逻辑
├─────────────────────────────────────────┤
│ System │ 逻辑层:业务逻辑
├─────────────────────────────────────────┤
│ Model │ 数据层:游戏状态
├─────────────────────────────────────────┤
│ Utility │ 工具层:无状态工具
└─────────────────────────────────────────┘
```bash
dotnet add package GeWuYou.GFramework.Core
dotnet add package GeWuYou.GFramework.Core.Abstractions
```
### 横切关注点
## 这个栏目应该回答什么
```
Command ──┐
Query ──┼──→ 跨层操作(修改/查询数据)
Event ──┘
```
`Core` 栏目不是旧版“完整框架教程”的镜像,而是当前实现的入口导航。这里的页面按能力域组织:
### 架构阶段
- 架构与上下文
- [architecture](./architecture.md)
- [context](./context.md)
- [lifecycle](./lifecycle.md)
- 旧版命令 / 查询执行器与迁移入口
- [command](./command.md)
- [query](./query.md)
- [cqrs](./cqrs.md)
- 核心横切能力
- [events](./events.md)
- [property](./property.md)
- [logging](./logging.md)
- [resource](./resource.md)
- [coroutine](./coroutine.md)
- [ioc](./ioc.md)
- 状态与扩展能力
- [state-machine](./state-machine.md)
- [state-management](./state-management.md)
- [pause](./pause.md)
- [localization](./localization.md)
- [functional](./functional.md)
- [extensions](./extensions.md)
框架提供了精细化的生命周期管理,包含 11 个阶段:
## 最小接入路径
```
初始化流程:
None → BeforeUtilityInit → AfterUtilityInit → BeforeModelInit → AfterModelInit → BeforeSystemInit → AfterSystemInit → Ready
当前版本的最小运行时入口只有三个关键动作:
销毁流程:
Ready → Destroying → Destroyed
1. 继承 `Architecture`
2. 在 `OnInitialize()` 中注册模型、系统、工具或模块
3. 通过 `architecture.Context``ContextAwareBase` 的扩展方法访问上下文
异常流程:
Any → FailedInitialization
```
每个阶段都会触发 `PhaseChanged` 事件,允许组件监听架构状态变化。
## 架构图
### 整体架构
从 v1.1.0 开始,Architecture 类采用模块化设计,将职责分离到专门的管理器中:
```
┌──────────────────┐
│ Architecture │ ← 核心协调器
└────────┬─────────┘
┌────────────────────┼────────────────────┐
│ │ │
┌────▼────────┐ ┌──────▼──────┐ ┌────────▼────────┐
│ Lifecycle │ │ Component │ │ Modules │
│ Manager │ │ Registry │ │ Manager │
└─────────────┘ └─────────────┘ └─────────────────┘
│ │ │
│ │ │
生命周期管理 组件注册管理 模块管理
- 阶段转换 - System 注册 - 模块安装
- 钩子管理 - Model 注册 - 行为注册
- 初始化/销毁 - Utility 注册
```
这种设计遵循单一职责原则,使代码更易维护和测试。
```
┌──────────────────┐
│ Architecture │ ← 管理所有组件
└────────┬─────────┘
┌────────────────────┼────────────────────┐
│ │ │
┌───▼────┐ ┌───▼────┐ ┌───▼─────┐
│ Model │ │ System │ │ Utility │
│ 层 │ │ 层 │ │ 层 │
└───┬────┘ └───┬────┘ └────────┘
│ │
│ ┌─────────────┤
│ │ │
┌───▼────▼───┐ ┌───▼──────┐
│ Controller │ │ Command/ │
│ 层 │ │ Query │
└─────┬──────┘ └──────────┘
┌─────▼─────┐
│ View │
│ UI │
└───────────┘
```
### 数据流向
```
用户输入 → Controller → Command → System → Model → Event → Controller → View 更新
查询流程Controller → Query → Model → 返回数据
```
## 快速开始
本框架采用"约定优于配置"的设计理念,只需 4 步即可搭建完整的架构。
### 为什么需要这个框架?
在传统开发中,我们经常遇到这些问题:
- 代码耦合严重UI 直接访问游戏逻辑,逻辑直接操作 UI
- 难以维护:修改一个功能需要改动多个文件
- 难以测试:业务逻辑和 UI 混在一起无法独立测试
- 难以复用:代码紧密耦合,无法在其他项目中复用
本框架通过清晰的分层解决这些问题。
### 1. 定义架构Architecture
**作用**Architecture 是整个应用的"中央调度器",负责管理所有组件的生命周期。
最小示例:
```csharp
using GFramework.Core.Architecture;
using GFramework.Core.Architectures;
public class GameArchitecture : Architecture
public sealed class CounterArchitecture : Architecture
{
protected override void Init()
protected override void OnInitialize()
{
// 注册 Model - 游戏数据
RegisterModel(new PlayerModel());
// 注册 System - 业务逻辑
RegisterSystem(new CombatSystem());
// 注册 Utility - 工具类
RegisterUtility(new StorageUtility());
RegisterModel(new CounterModel());
RegisterSystem(new CounterSystem());
}
}
```
**优势**
对应的完整起步示例见:
- **依赖注入**:组件通过上下文获取架构引用
- **集中管理**:所有组件注册在一处,一目了然
- **生命周期管理**:自动初始化和销毁
- **平台无关**:可以在任何 .NET 环境中使用
- [快速开始](../getting-started/quick-start.md)
### 2. 定义 Model数据层
## 新项目如何选择能力
**作用**Model 是应用的"数据库",只负责存储和管理状态。
- 只需要基础架构、事件、日志、资源、协程:
- 先停留在 `Core`
- 要写新的请求/通知处理流:
- 优先阅读 [cqrs](./cqrs.md)
- 要接入游戏内容配置、设置、数据仓库、Scene 或 UI
- 转到 [Game](../game/index.md)
- 要接入 Godot 节点、场景和项目元数据生成:
- 转到 [Godot](../godot/index.md) 与 [Source Generators](../source-generators/index.md) 栏目
```csharp
public class PlayerModel : AbstractModel
{
// 使用 BindableProperty 实现响应式数据
public BindableProperty<int> Health { get; } = new(100);
public BindableProperty<int> Gold { get; } = new(0);
protected override void OnInit()
{
// Model 中可以监听自己的数据变化
Health.Register(hp =>
{
if (hp <= 0) this.SendEvent(new PlayerDiedEvent());
});
}
}
## 推荐阅读顺序
// 也可以不使用 BindableProperty
public class PlayerModel : AbstractModel
{
public int Health { get; private set; }
public int Gold { get; private set; }
protected override void OnInit()
{
Health = 100;
Gold = 0;
}
}
```
1. [快速开始](../getting-started/quick-start.md)
2. [architecture](./architecture.md)
3. [context](./context.md)
4. [lifecycle](./lifecycle.md)
5. [cqrs](./cqrs.md)
**优势**
之后再按实际需要进入具体专题页,而不是把 `Core` 当成一次性读完的大杂烩。
- **数据响应式**BindableProperty 让数据变化自动通知监听者
- **职责单一**:只存储数据,不包含复杂业务逻辑
- **易于测试**:可以独立测试数据逻辑
## 对应模块入口
### 3. 定义 System业务逻辑层
**作用**System 是应用的"大脑",处理所有业务逻辑。
```csharp
public class CombatSystem : AbstractSystem
{
protected override void OnInit()
{
// System 通过事件驱动,响应游戏中的各种事件
this.RegisterEvent<EnemyAttackEvent>(OnEnemyAttack);
}
private void OnEnemyAttack(EnemyAttackEvent e)
{
var playerModel = this.GetModel<PlayerModel>();
// 处理业务逻辑:计算伤害、更新数据
playerModel.Health.Value -= e.Damage;
// 发送事件通知其他组件
this.SendEvent(new PlayerTookDamageEvent { Damage = e.Damage });
}
}
```
**优势**
- **事件驱动**:通过事件解耦,不同 System 之间松耦合
- **可组合**:多个 System 协同工作,每个专注自己的领域
- **易于扩展**:新增功能只需添加新的 System 和事件监听
### 4. 定义 Controller控制层
**作用**Controller 是"桥梁",连接 UI 和业务逻辑。
```csharp
public class PlayerController : IController
{
// 通过依赖注入获取架构
private readonly IArchitecture _architecture;
public PlayerController(IArchitecture architecture)
{
_architecture = architecture;
}
// 监听模型变化
public void Initialize()
{
var playerModel = _architecture.GetModel<PlayerModel>();
// 数据绑定Model 数据变化自动更新 UI
playerModel.Health.RegisterWithInitValue(OnHealthChanged);
}
private void OnHealthChanged(int hp)
{
// 更新 UI 显示
UpdateHealthDisplay(hp);
}
private void UpdateHealthDisplay(int hp) { /* UI 更新逻辑 */ }
}
```
**优势**
- **自动更新 UI**:通过 BindableProperty数据变化自动反映到界面
- **分离关注点**UI 逻辑和业务逻辑完全分离
- **易于测试**:可以通过依赖注入模拟架构进行测试
### 完成!现在你有了一个完整的架构
这 4 步完成后,你就拥有了:
- **清晰的数据层**Model
- **独立的业务逻辑**System
- **灵活的控制层**Controller
- **统一的生命周期管理**Architecture
### 下一步该做什么?
1. **添加 Command**:封装用户操作(如购买物品、使用技能)
2. **添加 Query**:封装数据查询(如查询背包物品数量)
3. **添加更多 System**:如任务系统、背包系统、商店系统
4. **使用 Utility**:添加工具类(如存档工具、数学工具)
5. **使用模块**:通过 IArchitectureModule 扩展架构功能
## 包说明
### Architecture 内部结构 (v1.1.0+)
从 v1.1.0 开始,Architecture 类采用模块化设计,将原本 708 行的单一类拆分为多个职责清晰的协作者:
#### 1. Architecture (核心协调器)
**职责**: 提供统一的公共 API,协调各个管理器
**主要方法**:
- `RegisterSystem<T>()` - 注册系统
- `RegisterModel<T>()` - 注册模型
- `RegisterUtility<T>()` - 注册工具
- `InstallModule()` - 安装模块
- `InitializeAsync()` / `Initialize()` - 初始化架构
- `DestroyAsync()` / `Destroy()` - 销毁架构
**事件**:
- `PhaseChanged` - 阶段变更事件
#### 2. ArchitectureBootstrapper (初始化基础设施编排器)
**职责**: 在用户 `OnInitialize()` 执行前准备环境、服务和上下文,并在组件初始化完成后执行初始化收尾
**核心功能**:
- 初始化环境对象
- 注册内置服务模块
- 绑定架构上下文到 `GameContext`
- 执行服务钩子
- 在 `InitializeAllComponentsAsync()` 完成后通过 `CompleteInitialization()` 冻结 IoC 容器
#### 3. ArchitectureLifecycle (生命周期管理器)
**职责**: 管理架构的生命周期和阶段转换
**核心功能**:
- 11 个架构阶段的管理和转换
- 生命周期钩子 (IArchitectureLifecycleHook) 管理
- 组件初始化 (按 Utility → Model → System 顺序)
- 组件销毁 (逆序销毁)
- 就绪状态管理
**关键方法**:
- `EnterPhase()` - 进入指定阶段
- `RegisterLifecycleHook()` - 注册生命周期钩子
- `InitializeAllComponentsAsync()` - 初始化所有组件
- `DestroyAsync()` - 异步销毁
#### 4. ArchitectureComponentRegistry (组件注册管理器)
**职责**: 管理 System、Model、Utility 的注册
**核心功能**:
- 组件注册和验证
- 自动设置组件上下文 (IContextAware)
- 自动注册组件生命周期 (IInitializable、IDestroyable)
- 支持实例注册和类型注册
**关键方法**:
- `RegisterSystem<T>()` - 注册系统
- `RegisterModel<T>()` - 注册模型
- `RegisterUtility<T>()` - 注册工具
> 命名提醒: 公开的 `ArchitectureServices` 负责容器和基础服务,并不承担组件注册职责。
> `ArchitectureComponentRegistry` 才是内部的 System / Model / Utility 注册器。
#### 5. ArchitectureModules (模块管理器)
**职责**: 管理架构模块和 CQRS 管道行为
**核心功能**:
- 模块安装 (IArchitectureModule)
- CQRS 管道行为注册(推荐 API 为 `RegisterCqrsPipelineBehavior`
**关键方法**:
- `InstallModule()` - 安装模块
- `RegisterCqrsPipelineBehavior<T>()` - 注册 CQRS 管道行为
#### 设计优势
这种模块化设计带来以下优势:
1. **单一职责**: 每个类只负责一个明确的功能
2. **易于测试**: 可以独立测试每个管理器
3. **易于维护**: 修改某个功能不影响其他功能
4. **易于扩展**: 添加新功能更容易
5. **代码安全**: 消除了 `null!` 断言,所有字段在构造后立即可用
详细的设计决策已在架构实现重构中落地。
---
## 包说明
| 包名 | 职责 | 文档 |
|----------------------|-----------------|--------------------------|
| **architecture** | 架构核心,管理所有组件生命周期 | [查看](./architecture) |
| **constants** | 框架常量定义 | 本文档 |
| **model** | 数据模型层,存储状态 | [查看](./model) |
| **system** | 业务逻辑层,处理业务规则 | [查看](./system) |
| **controller** | 控制器层,连接视图和逻辑 | (在 Abstractions 中) |
| **utility** | 工具类层,提供无状态工具 | [查看](./utility) |
| **command** | 命令模式,封装写操作 | [查看](./command) |
| **query** | 查询模式,封装读操作 | [查看](./query) |
| **events** | 事件系统,组件间通信 | [查看](./events) |
| **property** | 可绑定属性,响应式编程 | [查看](./property) |
| **state-management** | 集中式状态容器与选择器 | [查看](./state-management) |
| **ioc** | IoC 容器,依赖注入 | [查看](./ioc) |
| **rule** | 规则接口,定义组件约束 | [查看](./rule) |
| **extensions** | 扩展方法,简化 API 调用 | [查看](./extensions) |
| **logging** | 日志系统,记录运行日志 | [查看](./logging) |
| **environment** | 环境接口,提供运行环境信息 | [查看](./environment) |
| **localization** | 本地化系统,多语言支持 | [查看](./localization) |
## 组件联动
### 1. 初始化流程
```
创建 Architecture 实例
└─> 构造函数
├─> 初始化 Logger
├─> 创建 ArchitectureBootstrapper
├─> 创建 ArchitectureLifecycle
├─> 创建 ArchitectureComponentRegistry
└─> 创建 ArchitectureModules
└─> InitializeAsync()
├─> Bootstrapper 准备环境/服务/上下文
├─> OnInitialize() (用户注册组件)
│ ├─> RegisterModel → Model.SetContext()
│ ├─> RegisterSystem → System.SetContext()
│ └─> RegisterUtility → 注册到容器
├─> InitializeAllComponentsAsync()
│ ├─> BeforeUtilityInit → Utility.Initialize()
│ ├─> BeforeModelInit → Model.Initialize()
│ └─> BeforeSystemInit → System.Initialize()
├─> CompleteInitialization() → 冻结 IoC 容器
└─> 进入 Ready
```
**重要变更 (v1.1.0)**: 管理器现在在构造函数中初始化,而不是在 InitializeAsync 中。这消除了 `null!` 断言,提高了代码安全性。
### 2. Command 执行流程
```
Controller.SendCommand(command)
└─> command.Execute()
└─> command.OnDo() // 子类实现
├─> GetModel<T>() // 获取数据
├─> 修改 Model 数据
└─> SendEvent() // 发送事件
```
### 3. Event 传播流程
```
组件.SendEvent(event)
└─> TypeEventSystem.Send(event)
└─> 通知所有订阅者
├─> Controller 响应 → 更新 UI
├─> System 响应 → 执行逻辑
└─> Model 响应 → 更新状态
```
### 4. BindableProperty 数据绑定
```
Model: BindableProperty<int> Health = new(100);
Controller: Health.RegisterWithInitValue(hp => UpdateUI(hp))
修改值: Health.Value = 50 → 触发所有回调 → 更新 UI
```
## 最佳实践
### 1. 分层职责原则
每一层都有明确的职责边界,遵循这些原则能让代码更清晰、更易维护。
**Model 层**
```csharp
// 好:只存储数据
public class PlayerModel : AbstractModel
{
public BindableProperty<int> Health { get; } = new(100);
protected override void OnInit() { }
}
// 坏:包含业务逻辑
public class PlayerModel : AbstractModel
{
public void TakeDamage(int damage) // 业务逻辑应在 System
{
Health.Value -= damage;
if (Health.Value <= 0) Die();
}
}
```
**System 层**
```csharp
// 好:处理业务逻辑
public class CombatSystem : AbstractSystem
{
protected override void OnInit()
{
this.RegisterEvent<AttackEvent>(OnAttack);
}
private void OnAttack(AttackEvent e)
{
var target = this.GetModel<PlayerModel>();
int finalDamage = CalculateDamage(e.BaseDamage, target);
target.Health.Value -= finalDamage;
}
}
```
### 2. 通信方式选择指南
| 通信方式 | 使用场景 | 优势 |
|----------------------|-----------|----------|
| **Command** | 用户操作、修改状态 | 可撤销、可记录 |
| **Query** | 查询数据、检查条件 | 明确只读意图 |
| **Event** | 通知其他组件 | 松耦合、可扩展 |
| **BindableProperty** | 数据变化通知 | 自动化、不会遗漏 |
### 3. 生命周期管理
**为什么需要注销?**
忘记注销监听器会导致:
- **内存泄漏**:对象无法被 GC 回收
- **逻辑错误**:已销毁的对象仍在响应事件
```csharp
// 使用 UnRegisterList 统一管理
private IUnRegisterList _unregisterList = new UnRegisterList();
public void Initialize()
{
this.RegisterEvent<Event1>(OnEvent1)
.AddToUnregisterList(_unregisterList);
model.Property.Register(OnPropertyChanged)
.AddToUnregisterList(_unregisterList);
}
public void Cleanup()
{
_unregisterList.UnRegisterAll();
}
```
### 4. 性能优化技巧
```csharp
// 低效:每帧都查询
var model = _architecture.GetModel<PlayerModel>(); // 频繁调用
// 高效:缓存引用
private PlayerModel _playerModel;
public void Initialize()
{
_playerModel = _architecture.GetModel<PlayerModel>(); // 只查询一次
}
```
## 设计理念
框架的设计遵循 SOLID 原则和经典设计模式。
### 1. 单一职责原则SRP
- **Model**:只负责存储数据
- **System**:只负责处理业务逻辑
- **Controller**:只负责协调和输入处理
- **Utility**:只负责提供工具方法
### 2. 开闭原则OCP
- 通过**事件系统**添加新功能,无需修改现有代码
- 新的 System 可以监听现有事件,插入自己的逻辑
### 3. 依赖倒置原则DIP
- 所有组件通过接口交互
- 通过 IoC 容器注入依赖
- 易于替换实现和编写测试
### 4. 接口隔离原则ISP
```csharp
// 小而专注的接口
public interface ICanGetModel : IBelongToArchitecture { }
public interface ICanSendCommand : IBelongToArchitecture { }
public interface ICanRegisterEvent : IBelongToArchitecture { }
// 组合需要的能力
public interface IController :
ICanGetModel,
ICanSendCommand,
ICanRegisterEvent { }
```
### 5. 组合优于继承
通过接口组合获得能力,而不是通过继承。
### 框架核心设计模式
| 设计模式 | 应用位置 | 解决的问题 | 带来的好处 |
|-----------|------------|----------|--------|
| **工厂模式** | IoC 容器 | 组件的创建和管理 | 解耦创建逻辑 |
| **观察者模式** | Event 系统 | 组件间的通信 | 松耦合通信 |
| **命令模式** | Command | 封装操作请求 | 支持撤销重做 |
| **策略模式** | System | 不同的业务逻辑 | 易于切换策略 |
| **依赖注入** | 整体架构 | 组件间的依赖 | 自动管理依赖 |
| **模板方法** | Abstract 类 | 定义算法骨架 | 统一流程规范 |
### 平台无关性
- **GFramework.Core**:纯 .NET 库,无任何平台特定代码
- **GFramework.Godot**Godot 特定实现,包含 Node 扩展、GodotLogger 等
- 可以轻松将 Core 框架移植到其他平台Unity、.NET MAUI 等)
---
**版本**: 1.1.0
**更新日期**: 2026-03-17
**许可证**: Apache 2.0
## 更新日志
### v1.1.0 (2026-03-17)
**重大重构**:
- 拆分 Architecture 类为 4 个职责清晰的类
- 消除 3 处 `null!` 强制断言,提高代码安全性
- 在构造函数中初始化管理器,符合"构造即完整"原则
- 添加 `PhaseChanged` 事件,支持阶段监听
**向后兼容**: 所有公共 API 保持不变,现有代码无需修改。
- `GFramework.Core/README.md`
- `GFramework.Core.Abstractions/README.md`
- 仓库根 `README.md`

View File

@ -1,461 +1,166 @@
---
title: 生命周期管理
description: 生命周期管理提供了标准化的组件初始化和销毁机制,确保资源的正确管理和释放
description: 当前版本的架构生命周期由阶段模型、初始化顺序、逆序销毁和生命周期钩子共同组成
---
# 生命周期管理
## 概述
`GFramework.Core` 的生命周期由 `Architecture` 统一编排,而不是让每个组件各自决定初始化时机。
生命周期管理是 GFramework 中用于管理组件初始化和销毁的核心机制。通过实现标准的生命周期接口,组件可以在适当的时机执行初始化逻辑和资源清理,确保系统的稳定性和资源的有效管理。
你真正需要关注的是:
GFramework 提供了同步和异步两套生命周期接口,适用于不同的使用场景。架构会自动管理所有注册组件的生命周期,开发者只需实现相应的接口即可。
- 阶段枚举 `ArchitecturePhase`
- 组件初始化顺序
- 逆序销毁语义
- `IArchitectureLifecycleHook`
**主要特性**
## 阶段模型
- 标准化的初始化和销毁流程
- 支持同步和异步操作
- 自动生命周期管理
- 按注册顺序初始化,按逆序销毁
- 与架构系统深度集成
当前公开阶段如下:
## 核心概念
| 阶段 | 含义 |
| --- | --- |
| `None` | 尚未开始初始化 |
| `BeforeUtilityInit` | 即将初始化工具 |
| `AfterUtilityInit` | 工具初始化完成 |
| `BeforeModelInit` | 即将初始化模型 |
| `AfterModelInit` | 模型初始化完成 |
| `BeforeSystemInit` | 即将初始化系统 |
| `AfterSystemInit` | 系统初始化完成 |
| `Ready` | 架构已完成初始化并可供稳定使用 |
| `Destroying` | 正在销毁 |
| `Destroyed` | 已销毁 |
| `FailedInitialization` | 初始化流程失败 |
### 生命周期接口层次
正常路径:
GFramework 提供了一套完整的生命周期接口:
```text
None
-> BeforeUtilityInit
-> AfterUtilityInit
-> BeforeModelInit
-> AfterModelInit
-> BeforeSystemInit
-> AfterSystemInit
-> Ready
-> Destroying
-> Destroyed
```
## 初始化顺序
注册顺序和初始化顺序不是一回事。当前框架会按组件类别统一推进:
1. `Utility`
2. `Model`
3. `System`
这保证了大多数系统在初始化时,可以安全依赖已经就绪的工具与模型。
启动方式:
```csharp
// 同步接口
public interface IInitializable
{
void Initialize();
}
var architecture = new GameArchitecture();
await architecture.InitializeAsync();
await architecture.WaitUntilReadyAsync();
```
public interface IDestroyable
{
void Destroy();
}
注册逻辑仍然写在 `OnInitialize()`
public interface ILifecycle : IInitializable, IDestroyable
{
}
// 异步接口
public interface IAsyncInitializable
{
Task InitializeAsync();
}
public interface IAsyncDestroyable
{
ValueTask DestroyAsync();
}
public interface IAsyncLifecycle : IAsyncInitializable, IAsyncDestroyable
```csharp
protected override void OnInitialize()
{
RegisterUtility(new SaveUtility());
RegisterModel(new PlayerModel());
RegisterSystem(new CombatSystem());
}
```
### 初始化阶段
## 销毁语义
组件在注册到架构后会自动进行初始化:
销毁由 `DestroyAsync()` 统一触发,框架会按逆序回收组件。
如果组件实现了异步销毁接口,框架会优先走异步路径。也就是说,新代码应优先实现:
- `IAsyncDestroyable`
- 或其他已有的异步销毁基类路径
同步 `Destroy()` 主要是兼容入口。
## 组件自己的生命周期
大多数组件不需要手写 `Initialize()`;继承框架基类即可:
```csharp
public class PlayerModel : AbstractModel
public sealed class PlayerModel : AbstractModel
{
protected override void OnInit()
{
// 初始化逻辑
Console.WriteLine("PlayerModel 初始化");
}
}
```
### 销毁阶段
当架构销毁时,所有实现了 `IDestroyable` 的组件会按注册的逆序被销毁:
```csharp
public class GameSystem : AbstractSystem
{
public void Destroy()
{
// 清理资源
Console.WriteLine("GameSystem 销毁");
}
}
```
## 基本用法
### 实现同步生命周期
最常见的方式是继承框架提供的抽象基类:
```csharp
using GFramework.Core.Model;
public class InventoryModel : AbstractModel
{
private List<Item> _items = new();
protected override void OnInit()
{
// 初始化库存
_items = new List<Item>();
Console.WriteLine("库存系统已初始化");
}
}
```
### 实现销毁逻辑
对于需要清理资源的组件,实现 `IDestroyable` 接口:
```csharp
using GFramework.Core.Abstractions.System;
using GFramework.Core.Abstractions.Lifecycle;
public class AudioSystem : ISystem, IDestroyable
{
private AudioEngine _engine;
public void Initialize()
{
_engine = new AudioEngine();
_engine.Start();
}
public void Destroy()
{
// 清理音频资源
_engine?.Stop();
_engine?.Dispose();
_engine = null;
}
}
```
### 在架构中注册
组件注册后,架构会自动管理其生命周期:
```csharp
public class GameArchitecture : Architecture
{
protected override void Init()
{
// 注册顺序Model -> System -> Utility
RegisterModel(new PlayerModel()); // 1. 初始化
RegisterModel(new InventoryModel()); // 2. 初始化
RegisterSystem(new AudioSystem()); // 3. 初始化
// 销毁顺序会自动反转:
// AudioSystem -> InventoryModel -> PlayerModel
}
}
```
## 高级用法
### 异步初始化
对于需要异步操作的组件(如加载配置、连接数据库),使用异步生命周期:
```csharp
using GFramework.Core.Abstractions.Lifecycle;
using GFramework.Core.Abstractions.System;
public class ConfigurationSystem : ISystem, IAsyncInitializable
{
private Configuration _config;
public async Task InitializeAsync()
{
// 异步加载配置文件
_config = await LoadConfigurationAsync();
Console.WriteLine("配置已加载");
}
private async Task<Configuration> LoadConfigurationAsync()
{
await Task.Delay(100); // 模拟异步操作
return new Configuration();
}
}
```
### 异步销毁
对于需要异步清理的资源(如关闭网络连接、保存数据):
```csharp
using GFramework.Core.Abstractions.Lifecycle;
public class NetworkSystem : ISystem, IAsyncDestroyable
{
private NetworkClient _client;
public void Initialize()
{
_client = new NetworkClient();
}
public async ValueTask DestroyAsync()
{
// 异步关闭连接
if (_client != null)
{
await _client.DisconnectAsync();
await _client.DisposeAsync();
}
Console.WriteLine("网络连接已关闭");
}
}
```
### 完整异步生命周期
同时实现异步初始化和销毁:
```csharp
public class DatabaseSystem : ISystem, IAsyncLifecycle
{
private DatabaseConnection _connection;
public async Task InitializeAsync()
{
// 异步连接数据库
_connection = new DatabaseConnection();
await _connection.ConnectAsync("connection-string");
Console.WriteLine("数据库已连接");
}
public async ValueTask DestroyAsync()
{
// 异步关闭数据库连接
if (_connection != null)
{
await _connection.CloseAsync();
await _connection.DisposeAsync();
}
Console.WriteLine("数据库连接已关闭");
}
}
```
### 生命周期钩子
监听架构的生命周期阶段:
```csharp
using GFramework.Core.Abstractions.Enums;
public class AnalyticsSystem : AbstractSystem
public sealed class CombatSystem : AbstractSystem
{
protected override void OnInit()
{
Console.WriteLine("分析系统初始化");
}
public override void OnArchitecturePhase(ArchitecturePhase phase)
{
switch (phase)
{
case ArchitecturePhase.Initializing:
Console.WriteLine("架构正在初始化");
break;
case ArchitecturePhase.Ready:
Console.WriteLine("架构已就绪");
StartTracking();
break;
case ArchitecturePhase.Destroying:
Console.WriteLine("架构正在销毁");
StopTracking();
break;
}
}
private void StartTracking() { }
private void StopTracking() { }
}
```
## 最佳实践
1. **优先使用抽象基类**:继承 `AbstractModel``AbstractSystem` 等基类,它们已经实现了生命周期接口
```csharp
✓ public class MyModel : AbstractModel { }
✗ public class MyModel : IModel, IInitializable { }
```
2. **初始化顺序很重要**:按依赖关系注册组件,被依赖的组件先注册
```csharp
protected override void Init()
{
RegisterModel(new ConfigModel()); // 先注册配置
RegisterModel(new PlayerModel()); // 再注册依赖配置的模型
RegisterSystem(new GameplaySystem()); // 最后注册系统
}
```
3. **销毁时释放资源**:实现 `Destroy()` 方法清理非托管资源
```csharp
public void Destroy()
{
// 释放事件订阅
_eventBus.Unsubscribe<GameEvent>(OnGameEvent);
// 释放非托管资源
_nativeHandle?.Dispose();
// 清空引用
_cache?.Clear();
}
```
4. **异步操作使用异步接口**:避免在同步方法中阻塞异步操作
```csharp
✓ public async Task InitializeAsync() { await LoadDataAsync(); }
✗ public void Initialize() { LoadDataAsync().Wait(); } // 可能死锁
```
5. **避免在初始化中访问其他组件**:初始化顺序可能导致组件尚未就绪
```csharp
✗ protected override void OnInit()
{
var system = this.GetSystem<OtherSystem>(); // 可能尚未初始化
}
✓ public override void OnArchitecturePhase(ArchitecturePhase phase)
{
if (phase == ArchitecturePhase.Ready)
{
var system = this.GetSystem<OtherSystem>(); // 安全
}
}
```
6. **使用 OnArchitecturePhase 处理跨组件依赖**:在 Ready 阶段访问其他组件
```csharp
public override void OnArchitecturePhase(ArchitecturePhase phase)
{
if (phase == ArchitecturePhase.Ready)
{
// 此时所有组件都已初始化完成
var config = this.GetModel<ConfigModel>();
ApplyConfiguration(config);
}
}
```
## 常见问题
### 问题:什么时候使用同步 vs 异步生命周期?
**解答**
- **同步**:简单的初始化逻辑,如创建对象、设置默认值
- **异步**:需要 I/O 操作的场景,如加载文件、网络请求、数据库连接
```csharp
// 同步:简单初始化
public class ScoreModel : AbstractModel
{
protected override void OnInit()
{
Score = 0; // 简单赋值
}
}
// 异步:需要 I/O
public class SaveSystem : ISystem, IAsyncInitializable
{
public async Task InitializeAsync()
{
await LoadSaveDataAsync(); // 文件 I/O
}
}
```
### 问题:组件的初始化和销毁顺序是什么?
如果你的组件需要真正的异步初始化或销毁,再补对应接口。
**解答**
## 生命周期钩子
- **初始化顺序**:按注册顺序(先注册先初始化)
- **销毁顺序**:按注册的逆序(后注册先销毁)
当你需要做横切阶段逻辑时,优先实现 `IArchitectureLifecycleHook`,而不是把这些逻辑分散到某个具体 `System` 里。
```csharp
protected override void Init()
public sealed class MetricsHook : IArchitectureLifecycleHook
{
RegisterModel(new A()); // 1. 初始化3. 销毁
RegisterModel(new B()); // 2. 初始化2. 销毁
RegisterSystem(new C()); // 3. 初始化1. 销毁
}
```
### 问题:如何在初始化时访问其他组件?
**解答**
不要在 `OnInit()` 中访问其他组件,使用 `OnArchitecturePhase()` 在 Ready 阶段访问:
```csharp
public class DependentSystem : AbstractSystem
{
protected override void OnInit()
{
// ✗ 不要在这里访问其他组件
}
public override void OnArchitecturePhase(ArchitecturePhase phase)
public void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
{
if (phase == ArchitecturePhase.Ready)
{
// ✓ 在这里安全访问其他组件
var config = this.GetModel<ConfigModel>();
Console.WriteLine("Architecture ready.");
}
}
}
```
### 问题Destroy() 方法一定会被调用吗?
**解答**
只有在正常销毁架构时才会调用。如果应用程序崩溃或被强制终止,`Destroy()` 可能不会被调用。因此:
- 不要依赖 `Destroy()` 保存关键数据
- 使用自动保存机制保护重要数据
- 非托管资源应该实现 `IDisposable` 模式
### 问题:可以在 Destroy() 中访问其他组件吗?
**解答**
不推荐。销毁时其他组件可能已经被销毁。如果必须访问,确保检查组件是否仍然可用:
注册方式:
```csharp
public void Destroy()
{
// ✗ 不安全
var system = this.GetSystem<OtherSystem>();
system.DoSomething();
// ✓ 安全
try
{
var system = this.GetSystem<OtherSystem>();
system?.DoSomething();
}
catch
{
// 组件可能已销毁
}
}
architecture.RegisterLifecycleHook(new MetricsHook());
```
## 相关文档
## 阶段监听
- [架构组件](/zh-CN/core/architecture) - 架构基础和组件注册
- [Model 层](/zh-CN/core/model) - 数据模型的生命周期
- [System 层](/zh-CN/core/system) - 业务系统的生命周期
- [异步初始化](/zh-CN/core/async-initialization) - 异步架构初始化详解
如果你只需要观察阶段变化,也可以直接订阅:
```csharp
architecture.PhaseChanged += phase =>
{
Console.WriteLine($"Phase changed: {phase}");
};
```
## 什么时候会进入 `FailedInitialization`
如果初始化流程中抛出异常,架构会切到 `FailedInitialization`。这意味着:
- `Ready` 不会被触发
- 后续诊断应先回到启动路径
- 文档示例不应假设“只要 new 了 Architecture 就一定能跑到 Ready”
## 推荐做法
- 新代码优先使用 `InitializeAsync()` / `DestroyAsync()`
- 把注册逻辑放在 `OnInitialize()`,不要沿用旧文档里的 `Init()`
- 让 `Utility` 承载底层能力,让 `Model` 承载状态,再让 `System` 消费两者
- 跨组件阶段逻辑优先写成 `IArchitectureLifecycleHook`
## 继续阅读
- 架构入口:[architecture](./architecture.md)
- 上下文入口:[context](./context.md)

View File

@ -1,532 +1,103 @@
# Query 包使用说明
# Query
## 概述
本页说明 `GFramework.Core.Query` 里的旧查询体系。
Query 包实现了 CQRS命令查询职责分离模式中的查询部分。Query 用于封装数据查询逻辑,与 Command 不同的是Query
有返回值且不应该修改系统状态。
和旧命令系统一样,它仍然保留用于兼容存量代码;新功能优先使用 [cqrs](./cqrs.md) 中的新查询模型。
查询系统是 GFramework CQRS 架构的重要组成部分,专门负责数据读取操作,与命令系统和事件系统协同工作。
## 当前仍然可用的基类
## 核心接口
旧查询体系最常见的两个基类是:
### IQuery`<TResult>`
- `AbstractQuery<TResult>`
- 无输入查询
- `AbstractQuery<TInput, TResult>`
- 带输入查询
查询接口,定义了查询的基本契约。
与旧文档不同,带输入查询现在通过构造函数接收输入,不再依赖 `Input` 属性赋值
**核心成员:**
## 无输入查询
```csharp
TResult Do(); // 执行查询并返回结果
```
using GFramework.Core.Extensions;
using GFramework.Core.Query;
## 核心类
### AbstractQuery`<TInput, TResult>`
抽象查询基类,提供了查询的基础实现。通过 `IQueryInput` 接口传递参数。
**核心方法:**
```csharp
TResult IQuery<TResult>.Do(); // 实现 IQuery 接口
protected abstract TResult OnDo(TInput input); // 抽象查询方法,接收输入参数
```
**使用方式:**
```csharp
public abstract class AbstractQuery<TInput, TResult> : ContextAwareBase, IQuery<TResult>
where TInput : IQueryInput
public sealed class GetPlayerHealthQuery : AbstractQuery<int>
{
public TResult Do() => OnDo(Input); // 执行查询
public TInput Input { get; set; } // 输入参数
protected abstract TResult OnDo(TInput input); // 子类实现查询逻辑
}
```
### AbstractAsyncQuery`<TInput, TResult>`
支持异步执行的查询基类。
**核心方法:**
```csharp
Task<TResult> IAsyncQuery<TResult>.DoAsync(); // 实现异步查询接口
protected abstract Task<TResult> OnDoAsync(TInput input); // 抽象异步查询方法
```
### EmptyQueryInput
空查询输入类,用于表示不需要任何输入参数的查询操作。
**使用方式:**
```csharp
public sealed class EmptyQueryInput : IQueryInput
{
// 作为占位符使用,适用于那些不需要额外输入参数的查询场景
}
```
### QueryBus
查询总线实现,负责执行查询并返回结果。
**核心方法:**
```csharp
TResult Send<TResult>(IQuery<TResult> query); // 发送并执行查询
```
**使用方式:**
```csharp
public sealed class QueryBus : IQueryBus
{
public TResult Send<TResult>(IQuery<TResult> query)
protected override int OnDo()
{
ArgumentNullException.ThrowIfNull(query);
return query.Do();
return this.GetModel<PlayerModel>().Health.Value;
}
}
```
## 基本使用
### 1. 定义查询
发送方式:
```csharp
// 定义查询输入
public class GetPlayerGoldInput : IQueryInput { }
var health = this.SendQuery(new GetPlayerHealthQuery());
```
// 查询玩家金币数量
public class GetPlayerGoldQuery : AbstractQuery<GetPlayerGoldInput, int>
{
protected override int OnDo(GetPlayerGoldInput input)
{
return this.GetModel<PlayerModel>().Gold.Value;
}
}
## 带输入查询
// 定义查询输入
public class GetItemCountInput : IQueryInput
{
public string ItemId { get; set; }
}
旧查询输入类型现在直接复用 CQRS 抽象层里的 `IQueryInput`
// 查询背包中指定物品的数量
public class GetItemCountQuery : AbstractQuery<GetItemCountInput, int>
```csharp
using GFramework.Core.Extensions;
using GFramework.Core.Query;
using GFramework.Cqrs.Abstractions.Cqrs.Query;
public sealed record GetItemCountInput(string ItemId) : IQueryInput;
public sealed class GetItemCountQuery(GetItemCountInput input)
: AbstractQuery<GetItemCountInput, int>(input)
{
protected override int OnDo(GetItemCountInput input)
{
var inventory = this.GetModel<InventoryModel>();
return inventory.GetItemCount(input.ItemId);
}
}
// 定义异步查询输入
public class LoadPlayerDataInput : IQueryInput
{
public string PlayerId { get; set; }
}
// 异步查询玩家数据
public class LoadPlayerDataQuery : AbstractAsyncQuery<LoadPlayerDataInput, PlayerData>
{
protected override async Task<PlayerData> OnDoAsync(LoadPlayerDataInput input)
{
var storage = this.GetUtility<IStorageUtility>();
return await storage.LoadPlayerDataAsync(input.PlayerId);
var inventoryModel = this.GetModel<InventoryModel>();
return inventoryModel.GetItemCount(input.ItemId);
}
}
```
### 2. 发送查询
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class ShopUI : IController
{
[Export] private Button _buyButton;
[Export] private int _itemPrice = 100;
public void OnReady()
{
_buyButton.Pressed += OnBuyButtonPressed;
}
private void OnBuyButtonPressed()
{
// 查询玩家金币
var query = new GetPlayerGoldQuery { Input = new GetPlayerGoldInput() };
int playerGold = this.SendQuery(query);
if (playerGold >= _itemPrice)
{
// 发送购买命令
this.SendCommand(new BuyItemCommand { Input = new BuyItemInput { ItemId = "sword_01" } });
}
else
{
Console.WriteLine("金币不足!");
}
}
}
var count = this.SendQuery(
new GetItemCountQuery(new GetItemCountInput("potion")));
```
### 3. 在 System 中使用
## 异步查询
上下文仍然保留旧异步查询执行入口:
- `SendQueryAsync(IAsyncQuery<TResult>)`
这主要面向兼容旧 `AsyncQueryExecutor` 路径。文档不再推荐围绕旧 `QueryBus` 设计新功能。
## 发送入口
旧查询的执行入口是:
- `SendQuery<TResult>(IQuery<TResult>)`
- `SendQueryAsync<TResult>(IAsyncQuery<TResult>)`
`IContextAware` 对象内部,通常直接使用 `GFramework.Core.Extensions` 里的扩展:
```csharp
public class CombatSystem : AbstractSystem
{
protected override void OnInit()
{
// 注册事件监听
this.RegisterEvent<EnemyAttackEvent>(OnEnemyAttack);
}
private void OnEnemyAttack(EnemyAttackEvent e)
{
// 查询玩家是否已经死亡
var query = new IsPlayerDeadQuery { Input = new EmptyQueryInput() };
bool isDead = this.SendQuery(query);
if (!isDead)
{
// 执行伤害逻辑
this.SendCommand(new TakeDamageCommand { Input = new TakeDamageInput { Damage = e.Damage } });
}
}
}
public class IsPlayerDeadQuery : AbstractQuery<EmptyQueryInput, bool>
{
protected override bool OnDo(EmptyQueryInput input)
{
return this.GetModel<PlayerModel>().Health.Value <= 0;
}
}
```
using GFramework.Core.Extensions;
```
## 高级用法
## 什么时候继续保留旧查询
### 1. 带参数的复杂查询
- 你在维护现有 `Core.Query` 代码
- 当前代码已经建立在旧查询执行器之上
- 你只想修正局部行为,不想顺手迁移整条调用链
```csharp
// 定义查询输入
public class GetEnemiesInRangeInput : IQueryInput
{
public Vector3 Center { get; set; }
public float Radius { get; set; }
}
## 什么时候改用 CQRS 查询
// 查询指定范围内的敌人列表
public class GetEnemiesInRangeQuery : AbstractQuery<GetEnemiesInRangeInput, List<Enemy>>
{
protected override List<Enemy> OnDo(GetEnemiesInRangeInput input)
{
var enemySystem = this.GetSystem<EnemySpawnSystem>();
return enemySystem.GetEnemiesInRange(input.Center, input.Radius);
}
}
如果你正在写新的读取路径,优先考虑:
// 使用
var input = new GetEnemiesInRangeInput { Center = playerPosition, Radius = 10.0f };
var query = new GetEnemiesInRangeQuery { Input = input };
var enemies = this.SendQuery(query);
```
- `GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse>`
- `AbstractQueryHandler<TQuery, TResponse>`
- `architecture.Context.SendQueryAsync(...)`
### 2. 组合查询
原因很简单:新查询路径和命令、通知、流式请求共享同一 dispatcher 与行为管道。
```csharp
// 定义查询输入
public class CanUseSkillInput : IQueryInput
{
public string SkillId { get; set; }
}
// 查询玩家是否可以使用技能
public class CanUseSkillQuery : AbstractQuery<CanUseSkillInput, bool>
{
protected override bool OnDo(CanUseSkillInput input)
{
var playerModel = this.GetModel<PlayerModel>();
// 查询技能消耗
var skillCostQuery = new GetSkillCostQuery { Input = new GetSkillCostInput { SkillId = input.SkillId } };
var skillCost = this.SendQuery(skillCostQuery);
// 检查是否满足条件
return playerModel.Mana.Value >= skillCost.ManaCost
&& !this.SendQuery(new IsSkillOnCooldownQuery { Input = new IsSkillOnCooldownInput { SkillId = input.SkillId } });
}
}
public class GetSkillCostInput : IQueryInput
{
public string SkillId { get; set; }
}
public class GetSkillCostQuery : AbstractQuery<GetSkillCostInput, SkillCost>
{
protected override SkillCost OnDo(GetSkillCostInput input)
{
return this.GetModel<SkillModel>().GetSkillCost(input.SkillId);
}
}
public class IsSkillOnCooldownInput : IQueryInput
{
public string SkillId { get; set; }
}
public class IsSkillOnCooldownQuery : AbstractQuery<IsSkillOnCooldownInput, bool>
{
protected override bool OnDo(IsSkillOnCooldownInput input)
{
return this.GetModel<SkillModel>().IsOnCooldown(input.SkillId);
}
}
```
### 3. 聚合数据查询
```csharp
// 查询玩家战斗力
public class GetPlayerPowerQuery : AbstractQuery<EmptyQueryInput, int>
{
protected override int OnDo(EmptyQueryInput input)
{
var playerModel = this.GetModel<PlayerModel>();
var equipmentModel = this.GetModel<EquipmentModel>();
int basePower = playerModel.Level.Value * 10;
int equipmentPower = equipmentModel.GetTotalPower();
int buffPower = this.SendQuery(new GetBuffPowerQuery { Input = new EmptyQueryInput() });
return basePower + equipmentPower + buffPower;
}
}
// 查询玩家详细信息用于UI显示
public class GetPlayerInfoQuery : AbstractQuery<EmptyQueryInput, PlayerInfo>
{
protected override PlayerInfo OnDo(EmptyQueryInput input)
{
var playerModel = this.GetModel<PlayerModel>();
return new PlayerInfo
{
Name = playerModel.Name.Value,
Level = playerModel.Level.Value,
Health = playerModel.Health.Value,
MaxHealth = playerModel.MaxHealth.Value,
Gold = this.SendQuery(new GetPlayerGoldQuery { Input = new GetPlayerGoldInput() }),
Power = this.SendQuery(new GetPlayerPowerQuery { Input = new EmptyQueryInput() })
};
}
}
```
### 4. 跨 System 查询
```csharp
// 在 AI System 中查询玩家状态
public class EnemyAISystem : AbstractSystem
{
protected override void OnInit() { }
public void UpdateEnemyBehavior(Enemy enemy)
{
// 查询玩家位置
var playerPosQuery = new GetPlayerPositionQuery { Input = new EmptyQueryInput() };
var playerPos = this.SendQuery(playerPosQuery);
// 查询玩家是否在攻击范围内
var inRangeInput = new IsPlayerInRangeInput { Position = enemy.Position, Range = enemy.AttackRange };
bool inRange = this.SendQuery(new IsPlayerInRangeQuery { Input = inRangeInput });
if (inRange)
{
// 查询是否可以攻击
var canAttackInput = new CanEnemyAttackInput { EnemyId = enemy.Id };
bool canAttack = this.SendQuery(new CanEnemyAttackQuery { Input = canAttackInput });
if (canAttack)
{
this.SendCommand(new EnemyAttackCommand { Input = new EnemyAttackInput { EnemyId = enemy.Id } });
}
}
}
}
```
## Query 的执行机制
所有发送给查询总线的查询最终都会通过 `QueryExecutor` 来执行:
```csharp
public class QueryExecutor
{
public static TResult Execute<TResult>(IQuery<TResult> query)
{
return query.Do();
}
}
```
**特点:**
- 提供统一的查询执行机制
- 支持同步查询执行
- 与架构上下文集成
## Command vs Query
### Command命令
- **用途**:修改系统状态
- **返回值**无返回值void或有返回值
- **示例**:购买物品、造成伤害、升级角色
### Query查询
- **用途**:读取数据,不修改状态
- **返回值**:必须有返回值
- **示例**:获取金币数量、检查技能冷却、查询玩家位置
```csharp
// ❌ 错误:在 Query 中修改状态
public class BadQuery : AbstractQuery<int>
{
protected override int OnDo()
{
var model = this.GetModel<PlayerModel>();
model.Gold.Value += 100; // 不应该在 Query 中修改数据!
return model.Gold.Value;
}
}
// ✅ 正确Query 只读取数据
public class GoodQuery : AbstractQuery<int>
{
protected override int OnDo()
{
return this.GetModel<PlayerModel>().Gold.Value;
}
}
// ✅ 修改数据应该使用 Command
public class AddGoldCommand : AbstractCommand
{
private readonly int _amount;
public AddGoldCommand(int amount)
{
_amount = amount;
}
protected override void OnExecute()
{
var model = this.GetModel<PlayerModel>();
model.Gold.Value += _amount;
}
}
```
## 最佳实践
1. **查询只读取,不修改** - 保持 Query 的纯粹性
2. **小而专注** - 每个 Query 只负责一个具体的查询任务
3. **可组合** - 复杂查询可以通过组合简单查询实现
4. **避免过度查询** - 如果需要频繁查询,考虑使用 BindableProperty
5. **命名清晰** - Query 名称应该清楚表达查询意图Get、Is、Can、Has等前缀
6. **参数通过构造函数传递** - 查询需要的参数应在创建时传入
7. **查询无状态** - 查询不应该保存长期状态,执行完即可丢弃
8. **合理使用缓存** - 对于复杂计算,可以在 Model 中缓存结果
## 性能优化
### 1. 缓存查询结果
```csharp
// 在 Model 中缓存复杂计算
public class PlayerModel : AbstractModel
{
private int? _cachedPower;
public int GetPower()
{
if (_cachedPower == null)
{
_cachedPower = CalculatePower();
}
return _cachedPower.Value;
}
private int CalculatePower()
{
// 复杂计算...
return 100;
}
public void InvalidatePowerCache()
{
_cachedPower = null;
}
}
```
### 2. 批量查询
```csharp
// 一次查询多个数据,而不是多次单独查询
public class GetMultipleItemCountsQuery : AbstractQuery<Dictionary<string, int>>
{
private readonly List<string> _itemIds;
public GetMultipleItemCountsQuery(List<string> itemIds)
{
_itemIds = itemIds;
}
protected override Dictionary<string, int> OnDo()
{
var inventory = this.GetModel<InventoryModel>();
return _itemIds.ToDictionary(id => id, id => inventory.GetItemCount(id));
}
}
```
## 查询模式优势
### 1. 职责分离
- 读写操作明确分离
- 便于优化读写性能
- 降低系统复杂度
### 2. 可扩展性
- 读写可以独立扩展
- 支持不同的数据存储策略
- 便于实现读写分离
### 3. 可维护性
- 查询逻辑集中管理
- 便于重构和优化
- 降低组件间耦合
## 相关包
- [`command`](./command.md) - CQRS 的命令部分
- [`model`](./model.md) - Query 主要从 Model 获取数据
- [`system`](./system.md) - System 中可以发送 Query
- **Controller** - Controller 中可以发送 Query接口定义在 Core.Abstractions 中)
- [`extensions`](./extensions.md) - 提供 SendQuery 扩展方法
- [`architecture`](./architecture.md) - 架构核心,负责查询的分发和执行
继续阅读:[cqrs](./cqrs.md)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff