mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-12 13:14:30 +08:00
Compare commits
26 Commits
b2a5555c75
...
9656393fbb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9656393fbb | ||
|
|
b8c2ad42a9 | ||
|
|
ba3a4d4a37 | ||
|
|
2fc8442bd4 | ||
|
|
050f4321c6 | ||
|
|
4ef9406ee9 | ||
|
|
df68cdfd82 | ||
|
|
12f15961af | ||
|
|
6d4f9f2f94 | ||
|
|
0f8bf077e4 | ||
|
|
78a23bf53a | ||
|
|
de782ae179 | ||
|
|
7ec2185ae0 | ||
|
|
97573be2e1 | ||
|
|
3f95843d59 | ||
|
|
df91d3706b | ||
|
|
737dd5d91d | ||
|
|
819f91a7ad | ||
|
|
007c33f772 | ||
|
|
a1dbed3c8d | ||
|
|
da0ae700a3 | ||
|
|
af21f16c09 | ||
|
|
704fdaa2c8 | ||
|
|
cc49b8638f | ||
|
|
45a87c6988 | ||
|
|
310aeafa57 |
@ -12,7 +12,9 @@ Shortcut: `$gframework-pr-review`
|
||||
## Workflow
|
||||
|
||||
1. Read `AGENTS.md` before deciding how to validate or fix anything.
|
||||
2. Resolve the current branch with Windows Git from WSL, following the repository worktree rule.
|
||||
2. Resolve the current branch following the repository worktree rule:
|
||||
- prefer Linux `git` with explicit `--git-dir` / `--work-tree` binding in WSL worktrees
|
||||
- only fall back to `git.exe` when that executable is available and actually runnable in the current session
|
||||
3. Run `scripts/fetch_current_pr_review.py` to:
|
||||
- locate the PR for the current branch through the GitHub PR API
|
||||
- fetch PR metadata, issue comments, reviews, and review comments through the GitHub API
|
||||
@ -31,20 +33,20 @@ Shortcut: `$gframework-pr-review`
|
||||
## Commands
|
||||
|
||||
- Default:
|
||||
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py`
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py`
|
||||
- Recommended machine-readable workflow:
|
||||
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 265 --json-output /tmp/pr265-review.json`
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 265 --json-output /tmp/pr265-review.json`
|
||||
- `jq '.coderabbit_review.outside_diff_comments' /tmp/pr265-review.json`
|
||||
- Force a PR number:
|
||||
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253`
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253`
|
||||
- Machine-readable output:
|
||||
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json`
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json`
|
||||
- Write machine-readable output to a file instead of stdout:
|
||||
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253 --format json --json-output /tmp/pr253-review.json`
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253 --format json --json-output /tmp/pr253-review.json`
|
||||
- Inspect only a high-signal section:
|
||||
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253 --section outside-diff`
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253 --section outside-diff`
|
||||
- Narrow text output to one path fragment:
|
||||
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253 --section outside-diff --path GFramework.Core/Events/Event.cs`
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253 --section outside-diff --path GFramework.Core/Events/Event.cs`
|
||||
|
||||
## Output Expectations
|
||||
|
||||
@ -67,6 +69,7 @@ The script should produce:
|
||||
|
||||
- If the current branch has no matching public PR, report that clearly instead of guessing.
|
||||
- If GitHub access fails because of proxy configuration, rerun the fetch with proxy variables removed.
|
||||
- If the current WSL session resolves `git.exe` but cannot execute it cleanly, keep using the explicit Linux worktree binding instead of retrying Windows Git.
|
||||
- Prefer GitHub API results over PR HTML. The PR HTML page is now a fallback/debugging source, not the primary source of truth.
|
||||
- If the summary block and the latest head review threads disagree, trust the latest unresolved head-review threads and treat older summary findings as stale until re-verified locally.
|
||||
- Do not assume every AI reviewer behaves like CodeRabbit. `greptile-apps[bot]` findings may exist only as latest-head review threads, without CodeRabbit-style issue comments or folded review-body sections.
|
||||
|
||||
@ -21,8 +21,11 @@ from typing import Any
|
||||
|
||||
OWNER = "GeWuYou"
|
||||
REPO = "GFramework"
|
||||
WORKTREE_ROOT_DIRECTORY_NAME = "GFramework-WorkTree"
|
||||
DEFAULT_WINDOWS_GIT = "/mnt/d/Tool/Development Tools/Git/cmd/git.exe"
|
||||
GIT_ENVIRONMENT_KEY = "GFRAMEWORK_WINDOWS_GIT"
|
||||
GIT_DIR_ENVIRONMENT_KEY = "GFRAMEWORK_GIT_DIR"
|
||||
WORK_TREE_ENVIRONMENT_KEY = "GFRAMEWORK_WORK_TREE"
|
||||
USER_AGENT = "codex-gframework-pr-review"
|
||||
CODERABBIT_LOGIN = "coderabbitai[bot]"
|
||||
GREPTILE_LOGIN = "greptile-apps[bot]"
|
||||
@ -83,6 +86,43 @@ def resolve_git_command() -> str:
|
||||
raise RuntimeError(f"No usable git executable found. Set {GIT_ENVIRONMENT_KEY} to override it.")
|
||||
|
||||
|
||||
def find_repository_root(start_path: Path) -> Path | None:
|
||||
"""Locate the repository root by walking parent directories for repo markers."""
|
||||
for candidate in (start_path, *start_path.parents):
|
||||
if (candidate / "AGENTS.md").exists() and (candidate / ".ai/environment/tools.ai.yaml").exists():
|
||||
return candidate
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def resolve_worktree_git_dir(repository_root: Path) -> Path | None:
|
||||
"""Resolve the main-repository worktree gitdir for this WSL worktree layout."""
|
||||
if repository_root.parent.name != WORKTREE_ROOT_DIRECTORY_NAME:
|
||||
return None
|
||||
|
||||
primary_repository_root = repository_root.parent.parent / REPO
|
||||
candidate_git_dir = primary_repository_root / ".git" / "worktrees" / repository_root.name
|
||||
return candidate_git_dir if candidate_git_dir.exists() else None
|
||||
|
||||
|
||||
def resolve_git_invocation() -> list[str]:
|
||||
"""Resolve the git command arguments, preferring explicit WSL worktree binding."""
|
||||
configured_git_dir = os.environ.get(GIT_DIR_ENVIRONMENT_KEY)
|
||||
configured_work_tree = os.environ.get(WORK_TREE_ENVIRONMENT_KEY)
|
||||
linux_git = shutil.which("git")
|
||||
|
||||
if configured_git_dir and configured_work_tree and linux_git:
|
||||
return [linux_git, f"--git-dir={configured_git_dir}", f"--work-tree={configured_work_tree}"]
|
||||
|
||||
repository_root = find_repository_root(Path.cwd())
|
||||
if repository_root is not None and linux_git:
|
||||
worktree_git_dir = resolve_worktree_git_dir(repository_root)
|
||||
if worktree_git_dir is not None:
|
||||
return [linux_git, f"--git-dir={worktree_git_dir}", f"--work-tree={repository_root}"]
|
||||
|
||||
return [resolve_git_command()]
|
||||
|
||||
|
||||
def resolve_request_timeout_seconds() -> int:
|
||||
"""Return the GitHub request timeout in seconds."""
|
||||
configured_timeout = os.environ.get(REQUEST_TIMEOUT_ENVIRONMENT_KEY)
|
||||
@ -113,7 +153,7 @@ def run_command(args: list[str]) -> str:
|
||||
|
||||
def get_current_branch() -> str:
|
||||
"""Return the current git branch name."""
|
||||
return run_command([resolve_git_command(), "rev-parse", "--abbrev-ref", "HEAD"])
|
||||
return run_command([*resolve_git_invocation(), "rev-parse", "--abbrev-ref", "HEAD"])
|
||||
|
||||
|
||||
def open_url(url: str, accept: str) -> tuple[str, Any]:
|
||||
|
||||
29
AGENTS.md
29
AGENTS.md
@ -10,20 +10,33 @@ All AI agents and contributors must follow these rules when writing, reviewing,
|
||||
- Use `@.ai/environment/tools.raw.yaml` only when you need the full collected facts behind the AI-facing hints.
|
||||
- Prefer the project-relevant tools listed there instead of assuming every installed system tool is fair game.
|
||||
- If the real environment differs from the inventory, use the project-relevant installed tool and report the mismatch.
|
||||
- When working in WSL against this repository's Windows-backed worktree, prefer Windows Git from WSL (for example
|
||||
`git.exe`) instead of the Linux `git` binary.
|
||||
- If a Git command in WSL fails with a worktree-style “not a git repository” path translation error, rerun it with the
|
||||
Windows Git executable and treat that as the repository-default Git path for the rest of the task.
|
||||
- If the shell does not currently resolve `git.exe` to the host Windows Git installation, prepend that installation's
|
||||
command directory to `PATH` and reset shell command hashing for the current session before continuing.
|
||||
- After resolving the host Windows Git path, prefer an explicit session-local binding for subsequent commands so the
|
||||
shell does not fall back to Linux `/usr/bin/git` later in the same WSL session.
|
||||
- When working in WSL against this repository's Windows-backed worktree, first prefer Linux `git` with an explicit
|
||||
`--git-dir=<repo>/.git/worktrees/<worktree-name>` and `--work-tree=<worktree-root>` binding for every repository
|
||||
command. Treat that explicit binding as higher priority than `git.exe`, because it avoids WSL worktree path
|
||||
translation mistakes and still works in sessions where Windows `.exe` execution is unavailable.
|
||||
- If a plain Linux `git` command in WSL fails with a worktree-style “not a git repository” path translation error,
|
||||
rerun it with the explicit `--git-dir` / `--work-tree` binding before trying `git.exe`.
|
||||
- Only prefer Windows Git from WSL (for example `git.exe`) when that executable is both resolvable and executable in the
|
||||
current session, and when the explicit Linux `git` binding is unavailable or has already failed.
|
||||
- If the shell resolves `git.exe` but the current WSL session cannot execute it cleanly (for example `Exec format
|
||||
error`), keep using the explicit Linux `git` binding for the rest of the task instead of retrying Windows Git.
|
||||
- If the shell does not currently resolve `git.exe` to the host Windows Git installation and you still need Windows Git
|
||||
as a fallback, prepend that installation's command directory to `PATH` and reset shell command hashing for the
|
||||
current session before continuing.
|
||||
- After resolving either strategy, prefer a session-local binding or command wrapper for subsequent Git commands so the
|
||||
shell does not silently fall back to the wrong repository context later in the same WSL session.
|
||||
|
||||
## Git Workflow Rules
|
||||
|
||||
- Every completed task MUST pass at least one build validation before it is considered done.
|
||||
- If the task changes multiple projects or shared abstractions, prefer a solution-level or affected-project
|
||||
`dotnet build ... -c Release`; otherwise use the smallest build command that still proves the result compiles.
|
||||
- When a task adds a feature or modifies code, contributors MUST run a Release build for every directly affected
|
||||
module/project instead of relying on an unrelated project or solution slice that does not actually compile the touched
|
||||
code.
|
||||
- Warnings reported by those affected-module builds are part of the task scope. Contributors MUST resolve the touched
|
||||
module's build warnings in the same change, or stop and explicitly report the exact warning IDs and blocker instead of
|
||||
deferring them to a separate long-lived cleanup branch by default.
|
||||
- If the required build passes and there are task-related staged or unstaged changes, contributors MUST create a Git
|
||||
commit automatically instead of leaving the task uncommitted, unless the user explicitly says not to commit.
|
||||
- Commit messages MUST use Conventional Commits format: `<type>(<scope>): <summary>`.
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
`GFramework.Core.Abstractions` 承载 `Core` 运行时对应的接口、枚举和值对象,用来定义跨模块协作边界。
|
||||
|
||||
它只描述契约,不提供默认的架构、事件、状态、资源或 IoC 实现;这些实现都在 `GFramework.Core` 中。
|
||||
|
||||
## 什么时候单独依赖它
|
||||
|
||||
- 你在做插件、适配层或扩展包,只想依赖契约,不想把完整运行时拉进来
|
||||
@ -20,23 +22,34 @@
|
||||
|
||||
## 契约地图
|
||||
|
||||
| 目录 | 作用 |
|
||||
| 目录族 | 作用 |
|
||||
| --- | --- |
|
||||
| `Architectures/` | `IArchitecture`、模块、阶段监听与服务管理契约 |
|
||||
| `Command/` / `Query/` | 旧版命令与查询执行器接口 |
|
||||
| `Controller/` | `IController` |
|
||||
| `Events/` | 事件契约、解绑接口与传播上下文 |
|
||||
| `Model/` / `Systems/` / `Utility/` | 核心组件接口 |
|
||||
| `State/` / `StateManagement/` | 状态机、Store、reducer、selector 契约 |
|
||||
| `Property/` | `IBindableProperty` 与只读属性接口 |
|
||||
| `Resource/` | 资源管理与释放策略契约 |
|
||||
| `Localization/` | 本地化表、格式化与异常类型 |
|
||||
| `Logging/` | logger、log entry、factory 相关契约 |
|
||||
| `Ioc/` | `IIocContainer` |
|
||||
| `Lifecycle/` | 初始化 / 销毁生命周期契约 |
|
||||
| `Coroutine/` | 时间源、yield 指令与协程状态枚举 |
|
||||
| `Pause/` | 暂停栈、token 与状态事件 |
|
||||
| `Storage/` / `Serializer/` / `Versioning/` | 通用存储、序列化与版本化契约 |
|
||||
| `Architectures/` `Lifecycle/` `Registries/` | `IArchitecture`、上下文、模块、服务模块、阶段监听、注册表基类与生命周期契约 |
|
||||
| `Bases/` `Controller/` `Model/` `Systems/` `Utility/` `Rule/` | 组件角色接口、优先级 / key 值对象、上下文感知约束与扩展边界 |
|
||||
| `Command/` `Query/` `Cqrs/` | 旧版命令 / 查询执行器接口,以及 `ICqrsRuntime` 这类新请求模型接线契约 |
|
||||
| `Events/` `Property/` `State/` `StateManagement/` | 事件总线、解绑对象、可绑定属性、状态机、Store / reducer / middleware 契约 |
|
||||
| `Coroutine/` `Time/` `Pause/` `Concurrency/` | 协程状态、时间源、暂停栈、键控异步锁和统计对象 |
|
||||
| `Resource/` `Pool/` `Logging/` `Localization/` | 资源句柄、对象池、日志、日志工厂、本地化表与格式化契约 |
|
||||
| `Configuration/` `Environment/` | 配置管理器、环境对象与运行时环境访问契约 |
|
||||
| `Data/` `Serializer/` `Storage/` `Versioning/` | 数据装载、序列化、存储与版本化契约 |
|
||||
| `Enums/` `Properties/` | 架构阶段枚举,以及架构 / logger 相关属性键 |
|
||||
|
||||
## XML 覆盖基线
|
||||
|
||||
截至 `2026-04-22`,已按顶层目录对 `GFramework.Core.Abstractions` 的公开 / 内部类型声明做过一轮轻量盘点;当前契约目录族的类型声明都已带
|
||||
XML 注释。这里记录的是类型族级基线,成员级契约细节仍需要在后续波次继续审计。
|
||||
|
||||
| 类型族 | 基线状态 | 代表类型 |
|
||||
| --- | --- | --- |
|
||||
| `Architectures/` `Lifecycle/` `Registries/` | `20/20` 个类型声明已带 XML 注释 | `IArchitecture`、`IArchitectureContext`、`IServiceModule`、`KeyValueRegistryBase<TKey, TValue>` |
|
||||
| `Command/` `Query/` `Cqrs/` | `10/10` 个类型声明已带 XML 注释 | `ICommandExecutor`、`IAsyncQueryExecutor`、`ICqrsRuntime` |
|
||||
| `Events/` `Property/` `State/` `StateManagement/` | `25/25` 个类型声明已带 XML 注释 | `IEventBus`、`IBindableProperty<T>`、`IStateMachine`、`IStore<TState>` |
|
||||
| `Coroutine/` `Time/` `Pause/` `Concurrency/` | `17/17` 个类型声明已带 XML 注释 | `IYieldInstruction`、`ITimeProvider`、`IPauseStackManager`、`IAsyncKeyLockManager` |
|
||||
| `Resource/` `Pool/` `Logging/` `Localization/` | `27/27` 个类型声明已带 XML 注释 | `IResourceManager`、`IObjectPoolSystem`、`ILogger`、`ILocalizationManager` |
|
||||
| `Configuration/` `Environment/` `Data/` `Serializer/` `Storage/` `Versioning/` | `7/7` 个类型声明已带 XML 注释 | `IConfigurationManager`、`IEnvironment`、`ILoadableFrom<T>`、`ISerializer`、`IStorage` |
|
||||
| `Bases/` `Controller/` `Model/` `Systems/` `Utility/` `Rule/` `Enums/` `Properties/` | `19/19` 个类型声明已带 XML 注释 | `IPrioritized`、`IController`、`IModel`、`ISystem`、`IContextUtility`、`ArchitecturePhase` |
|
||||
|
||||
完整 inventory 与阅读顺序见 `docs/zh-CN/abstractions/core-abstractions.md`。
|
||||
|
||||
## 采用建议
|
||||
|
||||
@ -44,8 +57,18 @@
|
||||
- 若你只需要对接口编程,可以仅引用本包,再在应用层自行提供实现
|
||||
- 若你在写上层模块,优先把公共契约放在 `*.Abstractions`,实现放在对应 runtime 包
|
||||
|
||||
## 重点 XML 关注点
|
||||
|
||||
如果你在做契约审计、模块拆分或测试替身,优先看这些类型族的 XML 文档:
|
||||
|
||||
- 架构与模块入口:`IArchitecture`、`IArchitectureContext`、`IServiceModule`
|
||||
- 运行时基础设施:`IIocContainer`、`ILogger`、`IResourceManager`、`IConfigurationManager`
|
||||
- 状态与并发能力:`IStateMachine`、`IStore`、`IAsyncKeyLockManager`、`ITimeProvider`
|
||||
- 迁移与组合边界:`ICommandExecutor`、`IQueryExecutor`、`ICqrsRuntime`
|
||||
|
||||
## 对应文档
|
||||
|
||||
- 抽象接口栏目:[`../docs/zh-CN/abstractions/index.md`](../docs/zh-CN/abstractions/index.md)
|
||||
- Core 抽象页:[`../docs/zh-CN/abstractions/core-abstractions.md`](../docs/zh-CN/abstractions/core-abstractions.md)
|
||||
- Core 运行时入口:[`../GFramework.Core/README.md`](../GFramework.Core/README.md)
|
||||
- API 参考入口:[`../docs/zh-CN/api-reference/index.md`](../docs/zh-CN/api-reference/index.md)
|
||||
|
||||
@ -96,6 +96,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
|
||||
var interfaceName = iContextAware.ToDisplayString(
|
||||
SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
var memberNames = CreateGeneratedContextMemberNames(symbol);
|
||||
sb.AppendLine("/// <summary>");
|
||||
sb.AppendLine("/// 为当前规则类型补充自动生成的架构上下文访问实现。");
|
||||
sb.AppendLine("/// </summary>");
|
||||
@ -107,15 +108,15 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
sb.AppendLine(
|
||||
"/// 已缓存的实例上下文需要通过 <see cref=\"GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)\" /> 显式覆盖。");
|
||||
sb.AppendLine(
|
||||
"/// 与手动继承 <see cref=\"global::GFramework.Core.Rule.ContextAwareBase\" /> 的路径相比,生成实现会使用 <c>_contextSync</c> 协调惰性初始化、provider 切换和显式上下文注入;");
|
||||
$"/// 与手动继承 <see cref=\"global::GFramework.Core.Rule.ContextAwareBase\" /> 的路径相比,生成实现会使用 <c>{memberNames.SyncFieldName}</c> 协调惰性初始化、provider 切换和显式上下文注入;");
|
||||
sb.AppendLine(
|
||||
"/// <see cref=\"global::GFramework.Core.Rule.ContextAwareBase\" /> 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。");
|
||||
sb.AppendLine("/// </remarks>");
|
||||
sb.AppendLine($"partial class {symbol.Name} : {interfaceName}");
|
||||
sb.AppendLine("{");
|
||||
|
||||
GenerateContextProperty(sb);
|
||||
GenerateInterfaceImplementations(sb, iContextAware);
|
||||
GenerateContextProperty(sb, memberNames);
|
||||
GenerateInterfaceImplementations(sb, iContextAware, memberNames);
|
||||
|
||||
sb.AppendLine("}");
|
||||
return sb.ToString().TrimEnd();
|
||||
@ -138,13 +139,40 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
/// 生成Context属性
|
||||
/// </summary>
|
||||
/// <param name="sb">字符串构建器</param>
|
||||
private static void GenerateContextProperty(StringBuilder sb)
|
||||
/// <param name="memberNames">当前目标类型应使用的上下文字段名。</param>
|
||||
private static void GenerateContextProperty(
|
||||
StringBuilder sb,
|
||||
GeneratedContextMemberNames memberNames)
|
||||
{
|
||||
GenerateContextBackingFields(sb, memberNames);
|
||||
GenerateContextGetter(sb, memberNames);
|
||||
GenerateContextProviderConfiguration(sb, memberNames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成上下文缓存和同步所需的字段。
|
||||
/// </summary>
|
||||
/// <param name="sb">字符串构建器。</param>
|
||||
private static void GenerateContextBackingFields(
|
||||
StringBuilder sb,
|
||||
GeneratedContextMemberNames memberNames)
|
||||
{
|
||||
sb.AppendLine(" private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _context;");
|
||||
sb.AppendLine(
|
||||
" private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _contextProvider;");
|
||||
sb.AppendLine(" private static readonly object _contextSync = new();");
|
||||
$" private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? {memberNames.ContextFieldName};");
|
||||
sb.AppendLine(
|
||||
$" private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? {memberNames.ProviderFieldName};");
|
||||
sb.AppendLine($" private static readonly object {memberNames.SyncFieldName} = new();");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成实例上下文访问器,包含显式注入优先和 provider 惰性回退语义。
|
||||
/// </summary>
|
||||
/// <param name="sb">字符串构建器。</param>
|
||||
private static void GenerateContextGetter(
|
||||
StringBuilder sb,
|
||||
GeneratedContextMemberNames memberNames)
|
||||
{
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// 获取当前实例绑定的架构上下文。");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
@ -158,7 +186,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
sb.AppendLine(
|
||||
" /// 或 <see cref=\"ResetContextProvider\" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。");
|
||||
sb.AppendLine(
|
||||
" /// 当前实现还假设 <see cref=\"GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext\" /> 可在持有 <c>_contextSync</c> 时安全执行;");
|
||||
$" /// 当前实现还假设 <see cref=\"GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext\" /> 可在持有 <c>{memberNames.SyncFieldName}</c> 时安全执行;");
|
||||
sb.AppendLine(
|
||||
" /// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API,且应避免引入与外部全局锁相互等待的锁顺序。");
|
||||
sb.AppendLine(" /// </remarks>");
|
||||
@ -166,29 +194,35 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" get");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" var context = _context;");
|
||||
sb.AppendLine(" if (context is not null)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" return context;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" // 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。");
|
||||
sb.AppendLine(
|
||||
" // provider 的 GetContext() 会在持有 _contextSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。");
|
||||
sb.AppendLine(" lock (_contextSync)");
|
||||
$" // provider 的 GetContext() 会在持有 {memberNames.SyncFieldName} 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。");
|
||||
sb.AppendLine($" lock ({memberNames.SyncFieldName})");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(
|
||||
" _contextProvider ??= new global::GFramework.Core.Architectures.GameContextProvider();");
|
||||
sb.AppendLine(" _context ??= _contextProvider.GetContext();");
|
||||
sb.AppendLine(" return _context;");
|
||||
$" {memberNames.ProviderFieldName} ??= new global::GFramework.Core.Architectures.GameContextProvider();");
|
||||
sb.AppendLine($" {memberNames.ContextFieldName} ??= {memberNames.ProviderFieldName}.GetContext();");
|
||||
sb.AppendLine($" return {memberNames.ContextFieldName};");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成静态 provider 配置 API,供测试和宿主在懒加载前替换默认上下文来源。
|
||||
/// </summary>
|
||||
/// <param name="sb">字符串构建器。</param>
|
||||
private static void GenerateContextProviderConfiguration(
|
||||
StringBuilder sb,
|
||||
GeneratedContextMemberNames memberNames)
|
||||
{
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// 配置当前生成类型共享的上下文提供者。");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" /// <param name=\"provider\">后续懒加载上下文时要使用的提供者实例。</param>");
|
||||
sb.AppendLine(
|
||||
" /// <exception cref=\"global::System.ArgumentNullException\">当 <paramref name=\"provider\" /> 为 null 时抛出。</exception>");
|
||||
sb.AppendLine(" /// <remarks>");
|
||||
sb.AppendLine(" /// 该方法使用与 <see cref=\"Context\" /> 相同的同步锁,避免提供者切换与惰性初始化交错。");
|
||||
sb.AppendLine(
|
||||
@ -198,9 +232,10 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
sb.AppendLine(
|
||||
" public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" lock (_contextSync)");
|
||||
sb.AppendLine(" global::System.ArgumentNullException.ThrowIfNull(provider);");
|
||||
sb.AppendLine($" lock ({memberNames.SyncFieldName})");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" _contextProvider = provider;");
|
||||
sb.AppendLine($" {memberNames.ProviderFieldName} = provider;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
@ -215,9 +250,9 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
sb.AppendLine(" /// </remarks>");
|
||||
sb.AppendLine(" public static void ResetContextProvider()");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" lock (_contextSync)");
|
||||
sb.AppendLine($" lock ({memberNames.SyncFieldName})");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" _contextProvider = null;");
|
||||
sb.AppendLine($" {memberNames.ProviderFieldName} = null;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
@ -234,7 +269,8 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
/// <param name="interfaceSymbol">接口符号</param>
|
||||
private static void GenerateInterfaceImplementations(
|
||||
StringBuilder sb,
|
||||
INamedTypeSymbol interfaceSymbol)
|
||||
INamedTypeSymbol interfaceSymbol,
|
||||
GeneratedContextMemberNames memberNames)
|
||||
{
|
||||
var interfaceName = interfaceSymbol.ToDisplayString(
|
||||
SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
@ -244,7 +280,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
if (method.MethodKind != MethodKind.Ordinary)
|
||||
continue;
|
||||
|
||||
GenerateMethod(sb, interfaceName, method);
|
||||
GenerateMethod(sb, interfaceName, method, memberNames);
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
@ -258,7 +294,8 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
private static void GenerateMethod(
|
||||
StringBuilder sb,
|
||||
string interfaceName,
|
||||
IMethodSymbol method)
|
||||
IMethodSymbol method,
|
||||
GeneratedContextMemberNames memberNames)
|
||||
{
|
||||
var returnType = method.ReturnType.ToDisplayString(
|
||||
SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
@ -271,7 +308,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
$" {returnType} {interfaceName}.{method.Name}({parameters})");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
GenerateMethodBody(sb, method);
|
||||
GenerateMethodBody(sb, method, memberNames);
|
||||
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
@ -283,15 +320,16 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
/// <param name="method">方法符号</param>
|
||||
private static void GenerateMethodBody(
|
||||
StringBuilder sb,
|
||||
IMethodSymbol method)
|
||||
IMethodSymbol method,
|
||||
GeneratedContextMemberNames memberNames)
|
||||
{
|
||||
switch (method.Name)
|
||||
{
|
||||
case "SetContext":
|
||||
sb.AppendLine(" // 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。");
|
||||
sb.AppendLine(" lock (_contextSync)");
|
||||
sb.AppendLine($" lock ({memberNames.SyncFieldName})");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" _context = context;");
|
||||
sb.AppendLine($" {memberNames.ContextFieldName} = context;");
|
||||
sb.AppendLine(" }");
|
||||
break;
|
||||
|
||||
@ -307,4 +345,75 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为生成字段选择不会与目标类型现有成员冲突的稳定名称。
|
||||
/// </summary>
|
||||
/// <param name="symbol">当前需要补充 ContextAware 实现的目标类型。</param>
|
||||
/// <returns>当前生成轮次应使用的上下文字段名集合。</returns>
|
||||
private static GeneratedContextMemberNames CreateGeneratedContextMemberNames(INamedTypeSymbol symbol)
|
||||
{
|
||||
var reservedNames = CollectReservedContextMemberNames(symbol);
|
||||
|
||||
return new GeneratedContextMemberNames(
|
||||
AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareContext"),
|
||||
AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareProvider"),
|
||||
AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareSync"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 收集当前类型及其基类链上所有显式声明的成员名,确保生成字段不会意外隐藏继承成员。
|
||||
/// </summary>
|
||||
/// <param name="symbol">当前需要补充 ContextAware 实现的目标类型。</param>
|
||||
/// <returns>已被当前类型层级占用的成员名集合。</returns>
|
||||
private static HashSet<string> CollectReservedContextMemberNames(INamedTypeSymbol symbol)
|
||||
{
|
||||
var reservedNames = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
// Walk the full inheritance chain so numeric suffix allocation also covers members introduced by base types.
|
||||
for (var currentType = symbol; currentType is not null; currentType = currentType.BaseType)
|
||||
{
|
||||
foreach (var member in currentType.GetMembers())
|
||||
{
|
||||
if (!member.IsImplicitlyDeclared)
|
||||
{
|
||||
reservedNames.Add(member.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return reservedNames;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在固定前缀基础上按顺序追加数字后缀,直到找到可安全写入的成员名。
|
||||
/// </summary>
|
||||
/// <param name="reservedNames">当前类型已占用或已为其他生成字段保留的名称集合。</param>
|
||||
/// <param name="baseName">优先尝试的基础名称。</param>
|
||||
/// <returns>本轮生成可以使用的唯一成员名。</returns>
|
||||
private static string AllocateGeneratedMemberName(
|
||||
ISet<string> reservedNames,
|
||||
string baseName)
|
||||
{
|
||||
if (reservedNames.Add(baseName))
|
||||
return baseName;
|
||||
|
||||
for (var suffix = 1; ; suffix++)
|
||||
{
|
||||
var candidateName = $"{baseName}{suffix}";
|
||||
if (reservedNames.Add(candidateName))
|
||||
return candidateName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 描述一次 ContextAware 代码生成中选定的上下文字段名。
|
||||
/// </summary>
|
||||
/// <param name="ContextFieldName">实例上下文缓存字段名。</param>
|
||||
/// <param name="ProviderFieldName">共享上下文提供者字段名。</param>
|
||||
/// <param name="SyncFieldName">用于串行化访问的同步字段名。</param>
|
||||
private readonly record struct GeneratedContextMemberNames(
|
||||
string ContextFieldName,
|
||||
string ProviderFieldName,
|
||||
string SyncFieldName);
|
||||
}
|
||||
|
||||
@ -99,7 +99,7 @@ public class EasyEventsTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试并发场景下AddEvent的行为
|
||||
/// 测试 AddEvent 对重复事件类型保持兼容的参数异常类型。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void AddEvent_Should_Throw_When_Already_Registered()
|
||||
@ -167,4 +167,4 @@ public class EasyEventsTests
|
||||
Assert.That(_easyEvents.GetEvent<Event<int, string>>(), Is.Not.Null);
|
||||
Assert.That(_easyEvents.GetEvent<Event<double>>(), Is.Not.Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,6 +165,30 @@ public class CollectionExtensionsTests
|
||||
Assert.That(result["c"], Is.EqualTo(3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试ToDictionarySafe保持具体Dictionary返回类型,避免公开API继续收窄。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ToDictionarySafe_Should_Preserve_Concrete_Return_Type()
|
||||
{
|
||||
var method = typeof(GFramework.Core.Extensions.CollectionExtensions)
|
||||
.GetMethods()
|
||||
.Single(static method => method.Name == nameof(GFramework.Core.Extensions.CollectionExtensions.ToDictionarySafe));
|
||||
var methodGenericArguments = method.GetGenericArguments();
|
||||
var returnTypeGenericArguments = method.ReturnType.GetGenericArguments();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(method.IsGenericMethodDefinition, Is.True);
|
||||
Assert.That(method.ReturnType.IsGenericType, Is.True);
|
||||
Assert.That(method.ReturnType.GetGenericTypeDefinition(), Is.EqualTo(typeof(Dictionary<,>)));
|
||||
Assert.That(methodGenericArguments.Select(static argument => argument.Name), Is.EqualTo(new[] { "T", "TKey", "TValue" }));
|
||||
Assert.That(returnTypeGenericArguments, Has.Length.EqualTo(2));
|
||||
Assert.That(returnTypeGenericArguments[0], Is.SameAs(methodGenericArguments[1]));
|
||||
Assert.That(returnTypeGenericArguments[1], Is.SameAs(methodGenericArguments[2]));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试ToDictionarySafe方法在存在重复键时覆盖前面的值
|
||||
/// </summary>
|
||||
@ -224,4 +248,4 @@ public class CollectionExtensionsTests
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
items.ToDictionarySafe<(string, int), string, int>(x => x.Item1, null!));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,6 +39,40 @@ public class LoggingConfigurationTests
|
||||
Assert.That(config.LoggerLevels["GFramework.Core"], Is.EqualTo(LogLevel.Trace));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Configuration_Collections_Should_Preserve_Public_Concrete_Types()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(
|
||||
typeof(LoggingConfiguration).GetProperty(nameof(LoggingConfiguration.Appenders))!.PropertyType,
|
||||
Is.EqualTo(typeof(List<AppenderConfiguration>)));
|
||||
Assert.That(
|
||||
typeof(LoggingConfiguration).GetProperty(nameof(LoggingConfiguration.LoggerLevels))!.PropertyType,
|
||||
Is.EqualTo(typeof(Dictionary<string, LogLevel>)));
|
||||
Assert.That(
|
||||
typeof(FilterConfiguration).GetProperty(nameof(FilterConfiguration.Namespaces))!.PropertyType,
|
||||
Is.EqualTo(typeof(List<string>)));
|
||||
Assert.That(
|
||||
typeof(FilterConfiguration).GetProperty(nameof(FilterConfiguration.Filters))!.PropertyType,
|
||||
Is.EqualTo(typeof(List<FilterConfiguration>)));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LoggerLevels_Should_Remain_Case_Sensitive_By_Default()
|
||||
{
|
||||
var config = new LoggingConfiguration();
|
||||
config.LoggerLevels["GFramework.Core"] = LogLevel.Info;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(config.LoggerLevels.ContainsKey("GFramework.Core"), Is.True);
|
||||
Assert.That(config.LoggerLevels["GFramework.Core"], Is.EqualTo(LogLevel.Info));
|
||||
Assert.That(config.LoggerLevels.ContainsKey("gframework.core"), Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LoadFromJsonString_WithInvalidJson_ShouldThrow()
|
||||
{
|
||||
|
||||
@ -41,7 +41,7 @@ public sealed class CoroutineScheduler(
|
||||
|
||||
private readonly Dictionary<CoroutineHandle, CoroutineCompletionStatus> _completionStatuses = new();
|
||||
private readonly Queue<CoroutineHandle> _completionStatusOrder = new();
|
||||
private readonly Dictionary<string, HashSet<CoroutineHandle>> _grouped = new();
|
||||
private readonly Dictionary<string, HashSet<CoroutineHandle>> _grouped = new(StringComparer.Ordinal);
|
||||
private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CoroutineScheduler));
|
||||
private readonly Dictionary<CoroutineHandle, CoroutineMetadata> _metadata = new();
|
||||
private readonly ConcurrentQueue<CoroutineHandle> _pendingKills = new();
|
||||
@ -50,7 +50,7 @@ public sealed class CoroutineScheduler(
|
||||
throw new ArgumentNullException(nameof(timeSource));
|
||||
|
||||
private readonly CoroutineStatistics? _statistics = enableStatistics ? new CoroutineStatistics() : null;
|
||||
private readonly Dictionary<string, HashSet<CoroutineHandle>> _tagged = new();
|
||||
private readonly Dictionary<string, HashSet<CoroutineHandle>> _tagged = new(StringComparer.Ordinal);
|
||||
private readonly ITimeSource _timeSource = timeSource ?? throw new ArgumentNullException(nameof(timeSource));
|
||||
private readonly Dictionary<CoroutineHandle, HashSet<CoroutineHandle>> _waiting = new();
|
||||
private int _nextSlot;
|
||||
|
||||
@ -53,12 +53,14 @@ public class EasyEvents
|
||||
/// 添加指定类型的事件到事件字典中
|
||||
/// </summary>
|
||||
/// <typeparam name="T">事件类型,必须实现IEasyEvent接口且具有无参构造函数</typeparam>
|
||||
/// <exception cref="ArgumentException">当事件类型已存在时抛出</exception>
|
||||
/// <exception cref="ArgumentException">当事件类型已存在时抛出。</exception>
|
||||
public void AddEvent<T>() where T : IEvent, new()
|
||||
{
|
||||
if (!_mTypeEvents.TryAdd(typeof(T), new T()))
|
||||
{
|
||||
#pragma warning disable MA0015 // Preserve the public ArgumentException contract without inventing a fake parameter name.
|
||||
throw new ArgumentException($"Event type {typeof(T).Name} already registered.");
|
||||
#pragma warning restore MA0015
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,4 +83,4 @@ public class EasyEvents
|
||||
{
|
||||
return (T)_mTypeEvents.GetOrAdd(typeof(T), _ => new T());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,10 +81,12 @@ public static class CollectionExtensions
|
||||
/// // dict["a"] == 3 (最后一个值)
|
||||
/// </code>
|
||||
/// </example>
|
||||
#pragma warning disable MA0016 // Preserve the established concrete return type for public API compatibility.
|
||||
public static Dictionary<TKey, TValue> ToDictionarySafe<T, TKey, TValue>(
|
||||
this IEnumerable<T> source,
|
||||
Func<T, TKey> keySelector,
|
||||
Func<T, TValue> valueSelector) where TKey : notnull
|
||||
#pragma warning restore MA0016
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
ArgumentNullException.ThrowIfNull(keySelector);
|
||||
|
||||
@ -16,8 +16,23 @@ namespace GFramework.Core.Functional;
|
||||
/// <summary>
|
||||
/// 表示可能存在或不存在的值,用于替代 null 引用的函数式编程类型
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="Option{T}" /> 只表示两种显式状态:通过 <see cref="Some(T)" /> 创建的有值状态,以及
|
||||
/// <see cref="None" /> 表示的无值状态;调用方不应把 <see cref="None" /> 当作 <see langword="null" /> 的别名。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="Some(T)" /> 会拒绝 <see langword="null" />,因此引用类型和可空引用类型参数都必须包装真实值;访问方应优先通过
|
||||
/// <see cref="IsSome" />、<see cref="IsNone" />、模式匹配或 <c>Match</c>/<c>Map</c> 等函数式 API 消费结果,而不是假设默认值
|
||||
/// 与无值状态等价。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 该结构体是不可变值类型;一旦创建,其状态与内部值不会再改变。但在 <see cref="IsNone" /> 为 <see langword="true" /> 时,
|
||||
/// 调用需要真实值的方法仍应遵守各成员自身的契约与异常说明。
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">值的类型</typeparam>
|
||||
public readonly struct Option<T>
|
||||
public readonly struct Option<T> : IEquatable<Option<T>>
|
||||
{
|
||||
private readonly T _value;
|
||||
private readonly bool _isSome;
|
||||
@ -313,4 +328,4 @@ public readonly struct Option<T>
|
||||
_isSome ? $"Some({_value})" : "None";
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,10 +20,14 @@ public sealed class FilterConfiguration
|
||||
/// <summary>
|
||||
/// 命名空间前缀列表(用于 Namespace 过滤器)。
|
||||
/// </summary>
|
||||
#pragma warning disable MA0016 // Preserve the established concrete configuration API surface.
|
||||
public List<string>? Namespaces { get; set; }
|
||||
#pragma warning restore MA0016
|
||||
|
||||
/// <summary>
|
||||
/// 子过滤器列表(用于 Composite 过滤器)。
|
||||
/// </summary>
|
||||
#pragma warning disable MA0016 // Preserve the established concrete configuration API surface.
|
||||
public List<FilterConfiguration>? Filters { get; set; }
|
||||
#pragma warning restore MA0016
|
||||
}
|
||||
|
||||
@ -15,10 +15,15 @@ public sealed class LoggingConfiguration
|
||||
/// <summary>
|
||||
/// Appender 配置列表
|
||||
/// </summary>
|
||||
#pragma warning disable MA0016 // Preserve the established concrete configuration API surface.
|
||||
public List<AppenderConfiguration> Appenders { get; set; } = new();
|
||||
#pragma warning restore MA0016
|
||||
|
||||
/// <summary>
|
||||
/// 特定 Logger 的日志级别配置
|
||||
/// </summary>
|
||||
public Dictionary<string, LogLevel> LoggerLevels { get; set; } = new(StringComparer.Ordinal);
|
||||
#pragma warning disable MA0016 // Preserve the established concrete configuration API surface.
|
||||
public Dictionary<string, LogLevel> LoggerLevels { get; set; } =
|
||||
new Dictionary<string, LogLevel>(StringComparer.Ordinal);
|
||||
#pragma warning restore MA0016
|
||||
}
|
||||
|
||||
@ -10,9 +10,10 @@
|
||||
|
||||
- `Architecture` 与 `ArchitectureContext`
|
||||
- `Model` / `System` / `Utility` 运行时
|
||||
- 旧版 `Command` / `Query` 执行器
|
||||
- 事件、属性、状态机、状态管理
|
||||
- 资源、日志、协程、并发、环境与本地化
|
||||
- 旧版 `Command` / `Query` 执行器,以及与新版 `CQRS` runtime 的接线入口
|
||||
- 事件、属性、状态机、状态管理、规则与上下文扩展
|
||||
- 资源、对象池、日志、协程、并发、环境、配置与本地化
|
||||
- 服务模块管理、时间提供器与默认的 IoC 容器适配
|
||||
|
||||
它不负责:
|
||||
|
||||
@ -37,6 +38,7 @@
|
||||
| 目录 | 作用 |
|
||||
| --- | --- |
|
||||
| `Architectures/` | 架构入口、上下文、生命周期、模块安装与组件注册 |
|
||||
| `Services/` | 服务模块注册、生命周期协调与模块管理 |
|
||||
| `Command/` | 旧版命令执行器与同步 / 异步命令基类 |
|
||||
| `Query/` | 旧版查询执行器与同步 / 异步查询基类 |
|
||||
| `Events/` | 事件总线、事件作用域、统计与过滤 |
|
||||
@ -44,15 +46,37 @@
|
||||
| `State/` | 状态机与状态切换事件 |
|
||||
| `StateManagement/` | Store、selector、middleware 与状态诊断 |
|
||||
| `Coroutine/` | 协程调度、快照、统计与优先级 |
|
||||
| `Time/` | 默认时间提供器与协程时间源 |
|
||||
| `Resource/` | 资源缓存、句柄和释放策略 |
|
||||
| `Pool/` | 对象池系统与常用池化辅助实现 |
|
||||
| `Logging/` | logger、factory、配置与组合日志器 |
|
||||
| `Ioc/` | 基于 `Microsoft.Extensions.DependencyInjection` 的容器适配 |
|
||||
| `Concurrency/` | 键控异步锁与统计 |
|
||||
| `Configuration/` | 配置管理器与配置监听解绑对象 |
|
||||
| `Environment/` | 运行环境对象与上下文环境扩展 |
|
||||
| `Pause/` | 暂停栈和暂停范围 |
|
||||
| `Localization/` | 本地化表与格式化入口 |
|
||||
| `Rule/` | `ContextAwareBase` 等上下文感知基类 |
|
||||
| `Functional/` | `Option`、`Result` 等轻量函数式工具 |
|
||||
| `Extensions/` | 上下文与集合等扩展方法 |
|
||||
|
||||
## XML 覆盖基线
|
||||
|
||||
截至 `2026-04-22`,已按顶层目录对 `GFramework.Core` 的公开 / 内部类型声明做过一轮轻量盘点;当前主目录族的类型声明都已带
|
||||
XML 注释。这里先保留阅读基线,成员级 ``<param>`` / ``<returns>`` / 生命周期语义审计仍属于后续治理项。
|
||||
|
||||
| 类型族 | 基线状态 | 代表类型 |
|
||||
| --- | --- | --- |
|
||||
| `Architectures/` `Services/` | `22/22` 个类型声明已带 XML 注释 | `Architecture`、`ArchitectureContext`、`ArchitectureLifecycle`、`ServiceModuleManager` |
|
||||
| `Command/` `Query/` | `15/15` 个类型声明已带 XML 注释 | `CommandExecutor`、`AsyncQueryExecutor`、`AbstractCommand<TInput>`、`AbstractQuery<TResult>` |
|
||||
| `Events/` `Property/` `State/` `StateManagement/` | `29/29` 个类型声明已带 XML 注释 | `EventBus`、`BindableProperty<T>`、`StateMachine`、`Store<TState>` |
|
||||
| `Coroutine/` `Time/` `Pause/` `Concurrency/` | `43/43` 个类型声明已带 XML 注释 | `CoroutineScheduler`、`CoroutineHandle`、`PauseStackManager`、`AsyncKeyLockManager` |
|
||||
| `Resource/` `Pool/` | `8/8` 个类型声明已带 XML 注释 | `ResourceManager`、`AutoReleaseStrategy`、`AbstractObjectPoolSystem<TKey, TObject>` |
|
||||
| `Logging/` `Localization/` `Configuration/` `Environment/` `Ioc/` | `31/31` 个类型声明已带 XML 注释 | `ConsoleLogger`、`LocalizationManager`、`ConfigurationManager`、`DefaultEnvironment`、`MicrosoftDiContainer` |
|
||||
| `Model/` `Systems/` `Utility/` `Rule/` `Extensions/` `Functional/` | `34/34` 个类型声明已带 XML 注释 | `AbstractModel`、`AbstractSystem`、`NumericDisplayFormatter`、`ContextAwareBase`、`Result<T>` |
|
||||
|
||||
完整的模块化阅读顺序和 inventory 说明见 `docs/zh-CN/core/index.md`。
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
```bash
|
||||
@ -80,5 +104,7 @@ dotnet add package GeWuYou.GFramework.Core.Abstractions
|
||||
## 对应文档
|
||||
|
||||
- Core 栏目:[`../docs/zh-CN/core/index.md`](../docs/zh-CN/core/index.md)
|
||||
- Core 抽象层:[`../docs/zh-CN/abstractions/core-abstractions.md`](../docs/zh-CN/abstractions/core-abstractions.md)
|
||||
- API 参考入口:[`../docs/zh-CN/api-reference/index.md`](../docs/zh-CN/api-reference/index.md)
|
||||
- CQRS:[`../docs/zh-CN/core/cqrs.md`](../docs/zh-CN/core/cqrs.md)
|
||||
- 入门指南:[`../docs/zh-CN/getting-started/index.md`](../docs/zh-CN/getting-started/index.md)
|
||||
|
||||
@ -0,0 +1,284 @@
|
||||
namespace GFramework.Cqrs.SourceGenerators.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。
|
||||
/// </summary>
|
||||
public sealed partial class CqrsHandlerRegistryGenerator
|
||||
{
|
||||
private readonly record struct HandlerRegistrationSpec(
|
||||
string HandlerInterfaceDisplayName,
|
||||
string ImplementationTypeDisplayName,
|
||||
string HandlerInterfaceLogName,
|
||||
string ImplementationLogName);
|
||||
|
||||
private readonly record struct ReflectedImplementationRegistrationSpec(
|
||||
string HandlerInterfaceDisplayName,
|
||||
string HandlerInterfaceLogName);
|
||||
|
||||
private readonly record struct OrderedRegistrationSpec(
|
||||
string HandlerInterfaceLogName,
|
||||
OrderedRegistrationKind Kind,
|
||||
int Index);
|
||||
|
||||
private readonly record struct GeneratedRegistrySourceShape(
|
||||
bool HasReflectedImplementationRegistrations,
|
||||
bool HasPreciseReflectedRegistrations,
|
||||
bool HasReflectionTypeLookups,
|
||||
bool HasExternalAssemblyTypeLookups)
|
||||
{
|
||||
public bool RequiresRegistryAssemblyVariable =>
|
||||
HasReflectedImplementationRegistrations ||
|
||||
HasPreciseReflectedRegistrations ||
|
||||
HasReflectionTypeLookups;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记某条 handler 注册语句在生成阶段采用的表达策略。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该枚举只服务于输出排序与代码分支选择,用来保证生成注册器在“直接注册”
|
||||
/// “反射实现类型查找”和“精确运行时类型解析”之间保持稳定顺序。
|
||||
/// </remarks>
|
||||
private enum OrderedRegistrationKind
|
||||
{
|
||||
Direct,
|
||||
ReflectedImplementation,
|
||||
PreciseReflected
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 描述生成注册器中某个运行时类型引用的构造方式。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 某些 handler 服务类型可以直接以 <c>typeof(...)</c> 输出,某些则需要在运行时补充
|
||||
/// 反射查找、数组封装或泛型实参重建。该记录把这些差异收敛为统一的递归结构,
|
||||
/// 供源码输出阶段生成稳定的类型解析语句。
|
||||
/// </remarks>
|
||||
private sealed record RuntimeTypeReferenceSpec(
|
||||
string? TypeDisplayName,
|
||||
string? ReflectionTypeMetadataName,
|
||||
string? ReflectionAssemblyName,
|
||||
RuntimeTypeReferenceSpec? ArrayElementTypeReference,
|
||||
int ArrayRank,
|
||||
RuntimeTypeReferenceSpec? PointerElementTypeReference,
|
||||
RuntimeTypeReferenceSpec? GenericTypeDefinitionReference,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec> GenericTypeArguments)
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建一个可直接通过 <c>typeof(...)</c> 表达的类型引用。
|
||||
/// </summary>
|
||||
public static RuntimeTypeReferenceSpec FromDirectReference(string typeDisplayName)
|
||||
{
|
||||
return new RuntimeTypeReferenceSpec(
|
||||
typeDisplayName,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个需要从当前消费端程序集反射解析的类型引用。
|
||||
/// </summary>
|
||||
public static RuntimeTypeReferenceSpec FromReflectionLookup(string reflectionTypeMetadataName)
|
||||
{
|
||||
return new RuntimeTypeReferenceSpec(
|
||||
null,
|
||||
reflectionTypeMetadataName,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个需要从被引用程序集反射解析的类型引用。
|
||||
/// </summary>
|
||||
public static RuntimeTypeReferenceSpec FromExternalReflectionLookup(
|
||||
string reflectionAssemblyName,
|
||||
string reflectionTypeMetadataName)
|
||||
{
|
||||
return new RuntimeTypeReferenceSpec(
|
||||
null,
|
||||
reflectionTypeMetadataName,
|
||||
reflectionAssemblyName,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个数组类型引用。
|
||||
/// </summary>
|
||||
public static RuntimeTypeReferenceSpec FromArray(RuntimeTypeReferenceSpec elementTypeReference, int arrayRank)
|
||||
{
|
||||
return new RuntimeTypeReferenceSpec(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
elementTypeReference,
|
||||
arrayRank,
|
||||
null,
|
||||
null,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个封闭泛型类型引用。
|
||||
/// </summary>
|
||||
public static RuntimeTypeReferenceSpec FromConstructedGeneric(
|
||||
RuntimeTypeReferenceSpec genericTypeDefinitionReference,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec> genericTypeArguments)
|
||||
{
|
||||
return new RuntimeTypeReferenceSpec(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
genericTypeDefinitionReference,
|
||||
genericTypeArguments);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct PreciseReflectedRegistrationSpec(
|
||||
string OpenHandlerTypeDisplayName,
|
||||
string HandlerInterfaceLogName,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec> ServiceTypeArguments);
|
||||
|
||||
private readonly record struct ImplementationRegistrationSpec(
|
||||
string ImplementationTypeDisplayName,
|
||||
string ImplementationLogName,
|
||||
ImmutableArray<HandlerRegistrationSpec> DirectRegistrations,
|
||||
ImmutableArray<ReflectedImplementationRegistrationSpec> ReflectedImplementationRegistrations,
|
||||
ImmutableArray<PreciseReflectedRegistrationSpec> PreciseReflectedRegistrations,
|
||||
string? ReflectionTypeMetadataName,
|
||||
string? ReflectionFallbackHandlerTypeMetadataName);
|
||||
|
||||
private readonly struct HandlerCandidateAnalysis : IEquatable<HandlerCandidateAnalysis>
|
||||
{
|
||||
public HandlerCandidateAnalysis(
|
||||
string implementationTypeDisplayName,
|
||||
string implementationLogName,
|
||||
ImmutableArray<HandlerRegistrationSpec> registrations,
|
||||
ImmutableArray<ReflectedImplementationRegistrationSpec> reflectedImplementationRegistrations,
|
||||
ImmutableArray<PreciseReflectedRegistrationSpec> preciseReflectedRegistrations,
|
||||
string? reflectionTypeMetadataName,
|
||||
string? reflectionFallbackHandlerTypeMetadataName)
|
||||
{
|
||||
ImplementationTypeDisplayName = implementationTypeDisplayName;
|
||||
ImplementationLogName = implementationLogName;
|
||||
Registrations = registrations;
|
||||
ReflectedImplementationRegistrations = reflectedImplementationRegistrations;
|
||||
PreciseReflectedRegistrations = preciseReflectedRegistrations;
|
||||
ReflectionTypeMetadataName = reflectionTypeMetadataName;
|
||||
ReflectionFallbackHandlerTypeMetadataName = reflectionFallbackHandlerTypeMetadataName;
|
||||
}
|
||||
|
||||
public string ImplementationTypeDisplayName { get; }
|
||||
|
||||
public string ImplementationLogName { get; }
|
||||
|
||||
public ImmutableArray<HandlerRegistrationSpec> Registrations { get; }
|
||||
|
||||
public ImmutableArray<ReflectedImplementationRegistrationSpec> ReflectedImplementationRegistrations { get; }
|
||||
|
||||
public ImmutableArray<PreciseReflectedRegistrationSpec> PreciseReflectedRegistrations { get; }
|
||||
|
||||
public string? ReflectionTypeMetadataName { get; }
|
||||
|
||||
public string? ReflectionFallbackHandlerTypeMetadataName { get; }
|
||||
|
||||
public bool Equals(HandlerCandidateAnalysis other)
|
||||
{
|
||||
if (!string.Equals(ImplementationTypeDisplayName, other.ImplementationTypeDisplayName,
|
||||
StringComparison.Ordinal) ||
|
||||
!string.Equals(ImplementationLogName, other.ImplementationLogName, StringComparison.Ordinal) ||
|
||||
!string.Equals(ReflectionTypeMetadataName, other.ReflectionTypeMetadataName,
|
||||
StringComparison.Ordinal) ||
|
||||
!string.Equals(
|
||||
ReflectionFallbackHandlerTypeMetadataName,
|
||||
other.ReflectionFallbackHandlerTypeMetadataName,
|
||||
StringComparison.Ordinal) ||
|
||||
Registrations.Length != other.Registrations.Length ||
|
||||
ReflectedImplementationRegistrations.Length != other.ReflectedImplementationRegistrations.Length ||
|
||||
PreciseReflectedRegistrations.Length != other.PreciseReflectedRegistrations.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var index = 0; index < Registrations.Length; index++)
|
||||
{
|
||||
if (!Registrations[index].Equals(other.Registrations[index]))
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var index = 0; index < ReflectedImplementationRegistrations.Length; index++)
|
||||
{
|
||||
if (!ReflectedImplementationRegistrations[index].Equals(
|
||||
other.ReflectedImplementationRegistrations[index]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (var index = 0; index < PreciseReflectedRegistrations.Length; index++)
|
||||
{
|
||||
if (!PreciseReflectedRegistrations[index].Equals(other.PreciseReflectedRegistrations[index]))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is HandlerCandidateAnalysis other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var hashCode = StringComparer.Ordinal.GetHashCode(ImplementationTypeDisplayName);
|
||||
hashCode = (hashCode * 397) ^ StringComparer.Ordinal.GetHashCode(ImplementationLogName);
|
||||
hashCode = (hashCode * 397) ^
|
||||
(ReflectionTypeMetadataName is null
|
||||
? 0
|
||||
: StringComparer.Ordinal.GetHashCode(ReflectionTypeMetadataName));
|
||||
hashCode = (hashCode * 397) ^
|
||||
(ReflectionFallbackHandlerTypeMetadataName is null
|
||||
? 0
|
||||
: StringComparer.Ordinal.GetHashCode(ReflectionFallbackHandlerTypeMetadataName));
|
||||
foreach (var registration in Registrations)
|
||||
{
|
||||
hashCode = (hashCode * 397) ^ registration.GetHashCode();
|
||||
}
|
||||
|
||||
foreach (var reflectedImplementationRegistration in ReflectedImplementationRegistrations)
|
||||
{
|
||||
hashCode = (hashCode * 397) ^ reflectedImplementationRegistration.GetHashCode();
|
||||
}
|
||||
|
||||
foreach (var preciseReflectedRegistration in PreciseReflectedRegistrations)
|
||||
{
|
||||
hashCode = (hashCode * 397) ^ preciseReflectedRegistration.GetHashCode();
|
||||
}
|
||||
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct GenerationEnvironment(
|
||||
bool GenerationEnabled,
|
||||
bool SupportsReflectionFallbackAttribute);
|
||||
}
|
||||
@ -0,0 +1,327 @@
|
||||
namespace GFramework.Cqrs.SourceGenerators.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。
|
||||
/// </summary>
|
||||
public sealed partial class CqrsHandlerRegistryGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 为无法直接在生成代码中书写的关闭处理器接口构造精确的运行时注册描述。
|
||||
/// </summary>
|
||||
/// <param name="compilation">
|
||||
/// 当前生成轮次对应的编译上下文,用于判断类型是否属于当前程序集,从而决定是生成直接类型引用还是延迟到运行时反射解析。
|
||||
/// </param>
|
||||
/// <param name="handlerInterface">
|
||||
/// 需要注册的关闭处理器接口。调用方应保证它来自受支持的 CQRS 处理器接口定义,并且其泛型参数顺序与运行时注册约定一致。
|
||||
/// </param>
|
||||
/// <param name="registration">
|
||||
/// 当方法返回 <see langword="true" /> 时,包含开放泛型处理器类型和每个运行时类型实参的精确描述;
|
||||
/// 当方法返回 <see langword="false" /> 时,为默认值,调用方应回退到基于实现类型的宽松反射发现路径。
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// 当接口上的所有运行时类型引用都能在生成阶段被稳定描述时返回 <see langword="true" />;
|
||||
/// 只要任一泛型实参无法安全编码到生成输出中,就返回 <see langword="false" />。
|
||||
/// </returns>
|
||||
private static bool TryCreatePreciseReflectedRegistration(
|
||||
Compilation compilation,
|
||||
INamedTypeSymbol handlerInterface,
|
||||
out PreciseReflectedRegistrationSpec registration)
|
||||
{
|
||||
var openHandlerTypeDisplayName = handlerInterface.OriginalDefinition
|
||||
.ConstructUnboundGenericType()
|
||||
.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
var typeArguments =
|
||||
ImmutableArray.CreateBuilder<RuntimeTypeReferenceSpec>(handlerInterface.TypeArguments.Length);
|
||||
foreach (var typeArgument in handlerInterface.TypeArguments)
|
||||
{
|
||||
if (!TryCreateRuntimeTypeReference(compilation, typeArgument, out var runtimeTypeReference))
|
||||
{
|
||||
registration = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
typeArguments.Add(runtimeTypeReference!);
|
||||
}
|
||||
|
||||
registration = new PreciseReflectedRegistrationSpec(
|
||||
openHandlerTypeDisplayName,
|
||||
GetLogDisplayName(handlerInterface),
|
||||
typeArguments.ToImmutable());
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Roslyn 类型符号转换为生成注册器可消费的运行时类型引用描述。
|
||||
/// </summary>
|
||||
/// <param name="compilation">
|
||||
/// 当前编译上下文,用于区分可直接引用的外部可访问类型与必须通过当前程序集运行时反射查找的内部类型。
|
||||
/// </param>
|
||||
/// <param name="type">
|
||||
/// 需要转换的类型符号。该方法会递归处理数组元素类型和已构造泛型的类型实参,但不会为未绑定泛型或类型参数生成引用。
|
||||
/// </param>
|
||||
/// <param name="runtimeTypeReference">
|
||||
/// 当方法返回 <see langword="true" /> 时,包含可直接引用、数组、已构造泛型或反射查找中的一种运行时表示;
|
||||
/// 当方法返回 <see langword="false" /> 时为 <see langword="null" />,调用方应回退到更宽泛的实现类型反射扫描策略。
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// 当 <paramref name="type" /> 及其递归子结构都能映射为稳定的运行时引用时返回 <see langword="true" />;
|
||||
/// 若遇到类型参数、无法访问的运行时结构,或任一递归分支无法表示,则返回 <see langword="false" />。
|
||||
/// </returns>
|
||||
private static bool TryCreateRuntimeTypeReference(
|
||||
Compilation compilation,
|
||||
ITypeSymbol type,
|
||||
out RuntimeTypeReferenceSpec? runtimeTypeReference)
|
||||
{
|
||||
// CLR forbids pointer and function-pointer types from being used as generic arguments.
|
||||
// CQRS handler contracts are generic interfaces, so emitting runtime reconstruction code for these
|
||||
// shapes would only defer the failure to MakeGenericType(...) at runtime.
|
||||
if (type is IPointerTypeSymbol or IFunctionPointerTypeSymbol)
|
||||
{
|
||||
runtimeTypeReference = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Roslyn models dynamic as a pseudo-type, but generated C# cannot emit typeof(dynamic).
|
||||
// Normalize it to the CLR runtime type so precise reflected registrations stay compilable.
|
||||
if (type.TypeKind == TypeKind.Dynamic)
|
||||
{
|
||||
runtimeTypeReference = RuntimeTypeReferenceSpec.FromDirectReference("global::System.Object");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CanReferenceFromGeneratedRegistry(compilation, type))
|
||||
{
|
||||
runtimeTypeReference = RuntimeTypeReferenceSpec.FromDirectReference(
|
||||
type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type is IArrayTypeSymbol arrayType &&
|
||||
TryCreateRuntimeTypeReference(compilation, arrayType.ElementType, out var elementTypeReference))
|
||||
{
|
||||
runtimeTypeReference = RuntimeTypeReferenceSpec.FromArray(elementTypeReference!, arrayType.Rank);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type is INamedTypeSymbol genericNamedType &&
|
||||
genericNamedType.IsGenericType &&
|
||||
!genericNamedType.IsUnboundGenericType)
|
||||
{
|
||||
return TryCreateConstructedGenericRuntimeTypeReference(
|
||||
compilation,
|
||||
genericNamedType,
|
||||
out runtimeTypeReference);
|
||||
}
|
||||
|
||||
if (type is INamedTypeSymbol namedType &&
|
||||
TryCreateNamedRuntimeTypeReference(compilation, namedType, out var namedTypeReference))
|
||||
{
|
||||
runtimeTypeReference = namedTypeReference;
|
||||
return true;
|
||||
}
|
||||
|
||||
runtimeTypeReference = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为已构造泛型类型构造运行时类型引用,并递归验证每个泛型实参都可以稳定编码到生成输出中。
|
||||
/// </summary>
|
||||
/// <param name="compilation">当前生成轮次的编译上下文。</param>
|
||||
/// <param name="genericNamedType">需要表示的已构造泛型类型。</param>
|
||||
/// <param name="runtimeTypeReference">
|
||||
/// 当方法返回 <see langword="true" /> 时,包含泛型定义和泛型实参的运行时重建描述。
|
||||
/// </param>
|
||||
/// <returns>当泛型定义和全部泛型实参都能表达时返回 <see langword="true" />。</returns>
|
||||
private static bool TryCreateConstructedGenericRuntimeTypeReference(
|
||||
Compilation compilation,
|
||||
INamedTypeSymbol genericNamedType,
|
||||
out RuntimeTypeReferenceSpec? runtimeTypeReference)
|
||||
{
|
||||
if (!TryCreateGenericTypeDefinitionReference(
|
||||
compilation,
|
||||
genericNamedType,
|
||||
out var genericTypeDefinitionReference))
|
||||
{
|
||||
runtimeTypeReference = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var genericTypeArguments =
|
||||
ImmutableArray.CreateBuilder<RuntimeTypeReferenceSpec>(genericNamedType.TypeArguments.Length);
|
||||
foreach (var typeArgument in genericNamedType.TypeArguments)
|
||||
{
|
||||
if (!TryCreateRuntimeTypeReference(compilation, typeArgument, out var genericTypeArgumentReference))
|
||||
{
|
||||
runtimeTypeReference = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
genericTypeArguments.Add(genericTypeArgumentReference!);
|
||||
}
|
||||
|
||||
runtimeTypeReference = RuntimeTypeReferenceSpec.FromConstructedGeneric(
|
||||
genericTypeDefinitionReference!,
|
||||
genericTypeArguments.ToImmutable());
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为无法直接书写的命名类型选择当前程序集反射查找或外部程序集反射查找表示。
|
||||
/// </summary>
|
||||
/// <param name="compilation">当前生成轮次的编译上下文。</param>
|
||||
/// <param name="namedType">需要在运行时解析的命名类型。</param>
|
||||
/// <param name="runtimeTypeReference">
|
||||
/// 当方法返回 <see langword="true" /> 时,包含适合写入生成注册器的命名类型运行时引用;
|
||||
/// 当返回 <see langword="false" /> 时,调用方应回退到更保守的注册路径。
|
||||
/// </param>
|
||||
/// <returns>当命名类型可安全编码为运行时引用时返回 <see langword="true" />。</returns>
|
||||
private static bool TryCreateNamedRuntimeTypeReference(
|
||||
Compilation compilation,
|
||||
INamedTypeSymbol namedType,
|
||||
out RuntimeTypeReferenceSpec? runtimeTypeReference)
|
||||
{
|
||||
return TryCreateReflectionLookupReference(
|
||||
compilation,
|
||||
namedType,
|
||||
GetReflectionTypeMetadataName(namedType),
|
||||
out runtimeTypeReference);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为已构造泛型类型解析其泛型定义的运行时引用描述。
|
||||
/// </summary>
|
||||
/// <param name="compilation">
|
||||
/// 当前编译上下文,用于判断泛型定义是否应以内联类型引用形式生成,或在运行时通过当前程序集反射解析。
|
||||
/// </param>
|
||||
/// <param name="genericNamedType">
|
||||
/// 已构造的泛型类型。该方法只处理其原始泛型定义,不负责递归解析类型实参。
|
||||
/// </param>
|
||||
/// <param name="genericTypeDefinitionReference">
|
||||
/// 当方法返回 <see langword="true" /> 时,包含泛型定义的直接引用或反射查找描述;
|
||||
/// 当方法返回 <see langword="false" /> 时为 <see langword="null" />,调用方应停止精确构造并回退到更保守的注册路径。
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// 当泛型定义能够以稳定方式编码到生成输出中时返回 <see langword="true" />;
|
||||
/// 若泛型定义既不能直接引用,也不属于当前程序集可供反射查找,则返回 <see langword="false" />。
|
||||
/// </returns>
|
||||
private static bool TryCreateGenericTypeDefinitionReference(
|
||||
Compilation compilation,
|
||||
INamedTypeSymbol genericNamedType,
|
||||
out RuntimeTypeReferenceSpec? genericTypeDefinitionReference)
|
||||
{
|
||||
var genericTypeDefinition = genericNamedType.OriginalDefinition;
|
||||
if (CanReferenceFromGeneratedRegistry(compilation, genericTypeDefinition))
|
||||
{
|
||||
genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromDirectReference(
|
||||
genericTypeDefinition
|
||||
.ConstructUnboundGenericType()
|
||||
.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
|
||||
return true;
|
||||
}
|
||||
|
||||
return TryCreateReflectionLookupReference(
|
||||
compilation,
|
||||
genericTypeDefinition,
|
||||
GetReflectionTypeMetadataName(genericTypeDefinition),
|
||||
out genericTypeDefinitionReference);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为当前程序集或外部程序集中的命名类型构造统一的运行时反射查找描述。
|
||||
/// </summary>
|
||||
/// <param name="compilation">当前生成轮次的编译上下文。</param>
|
||||
/// <param name="namedType">需要在运行时解析的命名类型。</param>
|
||||
/// <param name="metadataName">写入生成代码的反射元数据名称。</param>
|
||||
/// <param name="runtimeTypeReference">成功时返回可直接写入注册器的运行时类型引用描述。</param>
|
||||
/// <returns>当命名类型具备可稳定编码的程序集归属信息时返回 <see langword="true" />。</returns>
|
||||
private static bool TryCreateReflectionLookupReference(
|
||||
Compilation compilation,
|
||||
INamedTypeSymbol namedType,
|
||||
string metadataName,
|
||||
out RuntimeTypeReferenceSpec? runtimeTypeReference)
|
||||
{
|
||||
if (SymbolEqualityComparer.Default.Equals(namedType.ContainingAssembly, compilation.Assembly))
|
||||
{
|
||||
runtimeTypeReference = RuntimeTypeReferenceSpec.FromReflectionLookup(metadataName);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (namedType.ContainingAssembly is null)
|
||||
{
|
||||
runtimeTypeReference = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
runtimeTypeReference = RuntimeTypeReferenceSpec.FromExternalReflectionLookup(
|
||||
namedType.ContainingAssembly.Identity.ToString(),
|
||||
metadataName);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool CanReferenceFromGeneratedRegistry(Compilation compilation, ITypeSymbol type)
|
||||
{
|
||||
// Roslyn error symbols stringify to unresolved type names; emitting them via typeof(...) would turn
|
||||
// an existing user-code error into a second generator-produced compile error instead of falling back.
|
||||
if (type.TypeKind is TypeKind.Error or TypeKind.Dynamic)
|
||||
return false;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case IArrayTypeSymbol arrayType:
|
||||
return CanReferenceFromGeneratedRegistry(compilation, arrayType.ElementType);
|
||||
case INamedTypeSymbol namedType:
|
||||
if (!compilation.IsSymbolAccessibleWithin(namedType, compilation.Assembly, throughType: null))
|
||||
return false;
|
||||
|
||||
foreach (var typeArgument in namedType.TypeArguments)
|
||||
{
|
||||
if (!CanReferenceFromGeneratedRegistry(compilation, typeArgument))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
case IPointerTypeSymbol:
|
||||
case IFunctionPointerTypeSymbol:
|
||||
return false;
|
||||
case ITypeParameterSymbol:
|
||||
return false;
|
||||
default:
|
||||
// Remaining Roslyn type kinds that reach this branch have already been normalized by earlier guards
|
||||
// and can continue through the direct-reference path without emitting fallback reflection code.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ContainsExternalAssemblyTypeLookup(RuntimeTypeReferenceSpec runtimeTypeReference)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(runtimeTypeReference.ReflectionAssemblyName))
|
||||
return true;
|
||||
|
||||
if (runtimeTypeReference.ArrayElementTypeReference is not null &&
|
||||
ContainsExternalAssemblyTypeLookup(runtimeTypeReference.ArrayElementTypeReference))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (runtimeTypeReference.PointerElementTypeReference is not null &&
|
||||
ContainsExternalAssemblyTypeLookup(runtimeTypeReference.PointerElementTypeReference))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (runtimeTypeReference.GenericTypeDefinitionReference is not null &&
|
||||
ContainsExternalAssemblyTypeLookup(runtimeTypeReference.GenericTypeDefinitionReference))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var genericTypeArgument in runtimeTypeReference.GenericTypeArguments)
|
||||
{
|
||||
if (ContainsExternalAssemblyTypeLookup(genericTypeArgument))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,846 @@
|
||||
namespace GFramework.Cqrs.SourceGenerators.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。
|
||||
/// </summary>
|
||||
public sealed partial class CqrsHandlerRegistryGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 生成程序集级 CQRS handler 注册器源码。
|
||||
/// </summary>
|
||||
/// <param name="generationEnvironment">
|
||||
/// 当前轮次的生成环境,用于决定 runtime 是否提供 <c>CqrsReflectionFallbackAttribute</c> 契约,以及是否需要在输出中发射对应的程序集级元数据。
|
||||
/// </param>
|
||||
/// <param name="registrations">
|
||||
/// 已整理并排序的 handler 注册描述。方法会据此生成 <c>CqrsHandlerRegistry.g.cs</c>,其中包含直接注册、实现类型反射注册、精确运行时类型查找等分支。
|
||||
/// </param>
|
||||
/// <param name="fallbackHandlerTypeMetadataNames">
|
||||
/// 仍需依赖程序集级 reflection fallback 元数据恢复的 handler 元数据名称集合。
|
||||
/// 调用方必须先确保:若该集合非空,则 <paramref name="generationEnvironment" /> 已声明支持对应的 fallback attribute 契约;
|
||||
/// 否则应在进入本方法前报告诊断并放弃生成,而不是输出会静默漏注册的半成品注册器。
|
||||
/// </param>
|
||||
/// <returns>完整的注册器源代码文本。</returns>
|
||||
/// <remarks>
|
||||
/// 当 <paramref name="fallbackHandlerTypeMetadataNames" /> 为空时,输出只包含程序集级 <c>CqrsHandlerRegistryAttribute</c> 和注册器实现。
|
||||
/// 当其非空且 runtime 合同可用时,输出还会附带程序集级 <c>CqrsReflectionFallbackAttribute</c>,让运行时补齐生成阶段无法精确表达的剩余 handler。
|
||||
/// 该方法本身不报告诊断;“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。
|
||||
/// </remarks>
|
||||
private static string GenerateSource(
|
||||
GenerationEnvironment generationEnvironment,
|
||||
IReadOnlyList<ImplementationRegistrationSpec> registrations,
|
||||
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
|
||||
{
|
||||
var sourceShape = CreateGeneratedRegistrySourceShape(registrations);
|
||||
var builder = new StringBuilder();
|
||||
AppendGeneratedSourcePreamble(builder, generationEnvironment, fallbackHandlerTypeMetadataNames);
|
||||
AppendGeneratedRegistryType(builder, registrations, sourceShape);
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预先计算生成注册器需要的辅助分支,让主源码发射流程保持线性且避免重复扫描注册集合。
|
||||
/// </summary>
|
||||
/// <param name="registrations">已整理并排序的 handler 注册描述。</param>
|
||||
/// <returns>当前生成输出需要启用的结构分支。</returns>
|
||||
private static GeneratedRegistrySourceShape CreateGeneratedRegistrySourceShape(
|
||||
IReadOnlyList<ImplementationRegistrationSpec> registrations)
|
||||
{
|
||||
var hasReflectedImplementationRegistrations = registrations.Any(static registration =>
|
||||
!registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty);
|
||||
var hasPreciseReflectedRegistrations = registrations.Any(static registration =>
|
||||
!registration.PreciseReflectedRegistrations.IsDefaultOrEmpty);
|
||||
var hasReflectionTypeLookups = registrations.Any(static registration =>
|
||||
!string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName));
|
||||
var hasExternalAssemblyTypeLookups = registrations.Any(static registration =>
|
||||
registration.PreciseReflectedRegistrations.Any(static preciseRegistration =>
|
||||
preciseRegistration.ServiceTypeArguments.Any(ContainsExternalAssemblyTypeLookup)));
|
||||
|
||||
return new GeneratedRegistrySourceShape(
|
||||
hasReflectedImplementationRegistrations,
|
||||
hasPreciseReflectedRegistrations,
|
||||
hasReflectionTypeLookups,
|
||||
hasExternalAssemblyTypeLookups);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射生成文件头、nullable 指令以及注册器所需的程序集级元数据特性。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="generationEnvironment">当前轮次的生成环境。</param>
|
||||
/// <param name="fallbackHandlerTypeMetadataNames">需要程序集级 reflection fallback 的 handler 元数据名称。</param>
|
||||
private static void AppendGeneratedSourcePreamble(
|
||||
StringBuilder builder,
|
||||
GenerationEnvironment generationEnvironment,
|
||||
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
|
||||
{
|
||||
builder.AppendLine("// <auto-generated />");
|
||||
builder.AppendLine("#nullable enable");
|
||||
builder.AppendLine();
|
||||
if (generationEnvironment.SupportsReflectionFallbackAttribute && fallbackHandlerTypeMetadataNames.Count > 0)
|
||||
{
|
||||
AppendReflectionFallbackAttribute(builder, fallbackHandlerTypeMetadataNames);
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
builder.Append("[assembly: global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.Append(".CqrsHandlerRegistryAttribute(typeof(global::");
|
||||
builder.Append(GeneratedNamespace);
|
||||
builder.Append('.');
|
||||
builder.Append(GeneratedTypeName);
|
||||
builder.AppendLine("))]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射程序集级 reflection fallback 元数据特性,供运行时补齐生成阶段无法精确表达的 handler。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="fallbackHandlerTypeMetadataNames">需要写入特性的 handler 元数据名称。</param>
|
||||
private static void AppendReflectionFallbackAttribute(
|
||||
StringBuilder builder,
|
||||
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
|
||||
{
|
||||
builder.Append("[assembly: global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.Append(".CqrsReflectionFallbackAttribute(");
|
||||
for (var index = 0; index < fallbackHandlerTypeMetadataNames.Count; index++)
|
||||
{
|
||||
if (index > 0)
|
||||
builder.Append(", ");
|
||||
|
||||
builder.Append('"');
|
||||
builder.Append(EscapeStringLiteral(fallbackHandlerTypeMetadataNames[index]));
|
||||
builder.Append('"');
|
||||
}
|
||||
|
||||
builder.AppendLine(")]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射生成注册器类型本体,包括 <c>Register</c> 方法和运行时反射辅助方法。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="registrations">已排序的 handler 注册描述。</param>
|
||||
/// <param name="sourceShape">当前输出需要启用的结构分支。</param>
|
||||
private static void AppendGeneratedRegistryType(
|
||||
StringBuilder builder,
|
||||
IReadOnlyList<ImplementationRegistrationSpec> registrations,
|
||||
GeneratedRegistrySourceShape sourceShape)
|
||||
{
|
||||
builder.AppendLine();
|
||||
builder.Append("namespace ");
|
||||
builder.Append(GeneratedNamespace);
|
||||
builder.AppendLine(";");
|
||||
builder.AppendLine();
|
||||
builder.Append("internal sealed class ");
|
||||
builder.Append(GeneratedTypeName);
|
||||
builder.Append(" : global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.AppendLine(".ICqrsHandlerRegistry");
|
||||
builder.AppendLine("{");
|
||||
AppendRegisterMethod(builder, registrations, sourceShape);
|
||||
|
||||
if (sourceShape.HasExternalAssemblyTypeLookups)
|
||||
{
|
||||
builder.AppendLine();
|
||||
AppendReflectionHelpers(builder);
|
||||
}
|
||||
|
||||
builder.AppendLine("}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射注册器的 <c>Register</c> 方法,保持直接注册和反射注册之间的原始稳定排序。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="registrations">已排序的 handler 注册描述。</param>
|
||||
/// <param name="sourceShape">当前输出需要启用的结构分支。</param>
|
||||
private static void AppendRegisterMethod(
|
||||
StringBuilder builder,
|
||||
IReadOnlyList<ImplementationRegistrationSpec> registrations,
|
||||
GeneratedRegistrySourceShape sourceShape)
|
||||
{
|
||||
builder.Append(
|
||||
" public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::");
|
||||
builder.Append(LoggingNamespace);
|
||||
builder.AppendLine(".ILogger logger)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" if (services is null)");
|
||||
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(services));");
|
||||
builder.AppendLine(" if (logger is null)");
|
||||
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(logger));");
|
||||
if (sourceShape.RequiresRegistryAssemblyVariable)
|
||||
{
|
||||
builder.AppendLine();
|
||||
builder.Append(" var registryAssembly = typeof(global::");
|
||||
builder.Append(GeneratedNamespace);
|
||||
builder.Append('.');
|
||||
builder.Append(GeneratedTypeName);
|
||||
builder.AppendLine(").Assembly;");
|
||||
}
|
||||
|
||||
if (registrations.Count > 0)
|
||||
builder.AppendLine();
|
||||
|
||||
for (var registrationIndex = 0; registrationIndex < registrations.Count; registrationIndex++)
|
||||
{
|
||||
var registration = registrations[registrationIndex];
|
||||
if (!registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty ||
|
||||
!registration.PreciseReflectedRegistrations.IsDefaultOrEmpty)
|
||||
{
|
||||
AppendOrderedImplementationRegistrations(builder, registration, registrationIndex);
|
||||
}
|
||||
else if (!registration.DirectRegistrations.IsDefaultOrEmpty)
|
||||
{
|
||||
AppendDirectRegistrations(builder, registration);
|
||||
}
|
||||
}
|
||||
|
||||
builder.AppendLine(" }");
|
||||
}
|
||||
|
||||
private static void AppendDirectRegistrations(
|
||||
StringBuilder builder,
|
||||
ImplementationRegistrationSpec registration)
|
||||
{
|
||||
foreach (var directRegistration in registration.DirectRegistrations)
|
||||
{
|
||||
AppendServiceRegistration(
|
||||
builder,
|
||||
$"typeof({directRegistration.HandlerInterfaceDisplayName})",
|
||||
$"typeof({directRegistration.ImplementationTypeDisplayName})",
|
||||
" ");
|
||||
AppendRegistrationLog(
|
||||
builder,
|
||||
directRegistration.ImplementationLogName,
|
||||
directRegistration.HandlerInterfaceLogName,
|
||||
" ");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射 <c>AddTransient</c> 调用,调用方负责传入已经按当前分支解析好的 service 和 implementation 表达式。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="serviceTypeExpression">生成代码中的服务类型表达式。</param>
|
||||
/// <param name="implementationTypeExpression">生成代码中的实现类型表达式。</param>
|
||||
/// <param name="indent">当前生成语句的缩进。</param>
|
||||
private static void AppendServiceRegistration(
|
||||
StringBuilder builder,
|
||||
string serviceTypeExpression,
|
||||
string implementationTypeExpression,
|
||||
string indent)
|
||||
{
|
||||
builder.Append(indent);
|
||||
builder.AppendLine("global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient(");
|
||||
builder.Append(indent);
|
||||
builder.AppendLine(" services,");
|
||||
builder.Append(indent);
|
||||
builder.Append(" ");
|
||||
builder.Append(serviceTypeExpression);
|
||||
builder.AppendLine(",");
|
||||
builder.Append(indent);
|
||||
builder.Append(" ");
|
||||
builder.Append(implementationTypeExpression);
|
||||
builder.AppendLine(");");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射与注册语句配套的调试日志,保持所有生成注册路径的日志文本完全一致。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="implementationLogName">实现类型日志名。</param>
|
||||
/// <param name="handlerInterfaceLogName">handler 接口日志名。</param>
|
||||
/// <param name="indent">当前生成语句的缩进。</param>
|
||||
private static void AppendRegistrationLog(
|
||||
StringBuilder builder,
|
||||
string implementationLogName,
|
||||
string handlerInterfaceLogName,
|
||||
string indent)
|
||||
{
|
||||
builder.Append(indent);
|
||||
builder.Append("logger.Debug(\"Registered CQRS handler ");
|
||||
builder.Append(EscapeStringLiteral(implementationLogName));
|
||||
builder.Append(" as ");
|
||||
builder.Append(EscapeStringLiteral(handlerInterfaceLogName));
|
||||
builder.AppendLine(".\");");
|
||||
}
|
||||
|
||||
private static void AppendOrderedImplementationRegistrations(
|
||||
StringBuilder builder,
|
||||
ImplementationRegistrationSpec registration,
|
||||
int registrationIndex)
|
||||
{
|
||||
var orderedRegistrations = CreateOrderedRegistrations(registration);
|
||||
var implementationVariableName = $"implementationType{registrationIndex}";
|
||||
AppendImplementationTypeVariable(builder, registration, implementationVariableName);
|
||||
|
||||
builder.Append(" if (");
|
||||
builder.Append(implementationVariableName);
|
||||
builder.AppendLine(" is not null)");
|
||||
builder.AppendLine(" {");
|
||||
|
||||
foreach (var orderedRegistration in orderedRegistrations)
|
||||
{
|
||||
AppendOrderedRegistration(
|
||||
builder,
|
||||
registration,
|
||||
orderedRegistration,
|
||||
registrationIndex,
|
||||
implementationVariableName);
|
||||
}
|
||||
|
||||
builder.AppendLine(" }");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合并直接注册、实现类型反射注册和精确反射注册,并按 handler 接口日志名排序以保持生成输出稳定。
|
||||
/// </summary>
|
||||
/// <param name="registration">单个实现类型聚合后的注册描述。</param>
|
||||
/// <returns>带有来源类型和原始索引的有序注册列表。</returns>
|
||||
private static List<OrderedRegistrationSpec> CreateOrderedRegistrations(ImplementationRegistrationSpec registration)
|
||||
{
|
||||
var orderedRegistrations = new List<OrderedRegistrationSpec>(
|
||||
registration.DirectRegistrations.Length +
|
||||
registration.ReflectedImplementationRegistrations.Length +
|
||||
registration.PreciseReflectedRegistrations.Length);
|
||||
for (var directIndex = 0; directIndex < registration.DirectRegistrations.Length; directIndex++)
|
||||
{
|
||||
orderedRegistrations.Add(new OrderedRegistrationSpec(
|
||||
registration.DirectRegistrations[directIndex].HandlerInterfaceLogName,
|
||||
OrderedRegistrationKind.Direct,
|
||||
directIndex));
|
||||
}
|
||||
|
||||
for (var reflectedIndex = 0;
|
||||
reflectedIndex < registration.ReflectedImplementationRegistrations.Length;
|
||||
reflectedIndex++)
|
||||
{
|
||||
orderedRegistrations.Add(new OrderedRegistrationSpec(
|
||||
registration.ReflectedImplementationRegistrations[reflectedIndex].HandlerInterfaceLogName,
|
||||
OrderedRegistrationKind.ReflectedImplementation,
|
||||
reflectedIndex));
|
||||
}
|
||||
|
||||
for (var preciseIndex = 0;
|
||||
preciseIndex < registration.PreciseReflectedRegistrations.Length;
|
||||
preciseIndex++)
|
||||
{
|
||||
orderedRegistrations.Add(new OrderedRegistrationSpec(
|
||||
registration.PreciseReflectedRegistrations[preciseIndex].HandlerInterfaceLogName,
|
||||
OrderedRegistrationKind.PreciseReflected,
|
||||
preciseIndex));
|
||||
}
|
||||
|
||||
orderedRegistrations.Sort(static (left, right) =>
|
||||
StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName));
|
||||
return orderedRegistrations;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射实现类型变量。公开类型直接使用 <c>typeof</c>,不可直接引用的实现类型则从当前程序集反射解析。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="registration">单个实现类型聚合后的注册描述。</param>
|
||||
/// <param name="implementationVariableName">生成代码中的实现类型变量名。</param>
|
||||
private static void AppendImplementationTypeVariable(
|
||||
StringBuilder builder,
|
||||
ImplementationRegistrationSpec registration,
|
||||
string implementationVariableName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName))
|
||||
{
|
||||
builder.Append(" var ");
|
||||
builder.Append(implementationVariableName);
|
||||
builder.Append(" = typeof(");
|
||||
builder.Append(registration.ImplementationTypeDisplayName);
|
||||
builder.AppendLine(");");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(" var ");
|
||||
builder.Append(implementationVariableName);
|
||||
builder.Append(" = registryAssembly.GetType(\"");
|
||||
builder.Append(EscapeStringLiteral(registration.ReflectionTypeMetadataName!));
|
||||
builder.AppendLine("\", throwOnError: false, ignoreCase: false);");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据注册来源发射单条有序注册,确保混合直接和反射路径时仍按 handler 接口名稳定输出。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="registration">单个实现类型聚合后的注册描述。</param>
|
||||
/// <param name="orderedRegistration">带来源类型和原始索引的排序项。</param>
|
||||
/// <param name="registrationIndex">实现类型在整体注册列表中的索引,用于生成稳定变量名。</param>
|
||||
/// <param name="implementationVariableName">生成代码中的实现类型变量名。</param>
|
||||
private static void AppendOrderedRegistration(
|
||||
StringBuilder builder,
|
||||
ImplementationRegistrationSpec registration,
|
||||
OrderedRegistrationSpec orderedRegistration,
|
||||
int registrationIndex,
|
||||
string implementationVariableName)
|
||||
{
|
||||
switch (orderedRegistration.Kind)
|
||||
{
|
||||
case OrderedRegistrationKind.Direct:
|
||||
AppendOrderedDirectRegistration(
|
||||
builder,
|
||||
registration,
|
||||
registration.DirectRegistrations[orderedRegistration.Index],
|
||||
implementationVariableName);
|
||||
break;
|
||||
case OrderedRegistrationKind.ReflectedImplementation:
|
||||
AppendOrderedReflectedImplementationRegistration(
|
||||
builder,
|
||||
registration,
|
||||
registration.ReflectedImplementationRegistrations[orderedRegistration.Index],
|
||||
implementationVariableName);
|
||||
break;
|
||||
case OrderedRegistrationKind.PreciseReflected:
|
||||
AppendOrderedPreciseReflectedRegistration(
|
||||
builder,
|
||||
registration,
|
||||
registration.PreciseReflectedRegistrations[orderedRegistration.Index],
|
||||
registrationIndex,
|
||||
orderedRegistration.Index,
|
||||
implementationVariableName);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Unsupported ordered CQRS registration kind {orderedRegistration.Kind}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射实现类型已通过变量解析、handler 接口可直接引用的直接注册语句。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="registration">单个实现类型聚合后的注册描述。</param>
|
||||
/// <param name="directRegistration">当前直接注册项。</param>
|
||||
/// <param name="implementationVariableName">生成代码中的实现类型变量名。</param>
|
||||
private static void AppendOrderedDirectRegistration(
|
||||
StringBuilder builder,
|
||||
ImplementationRegistrationSpec registration,
|
||||
HandlerRegistrationSpec directRegistration,
|
||||
string implementationVariableName)
|
||||
{
|
||||
AppendServiceRegistration(
|
||||
builder,
|
||||
$"typeof({directRegistration.HandlerInterfaceDisplayName})",
|
||||
implementationVariableName,
|
||||
" ");
|
||||
AppendRegistrationLog(
|
||||
builder,
|
||||
registration.ImplementationLogName,
|
||||
directRegistration.HandlerInterfaceLogName,
|
||||
" ");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射实现类型需要反射解析、handler 接口可直接引用的注册语句。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="registration">单个实现类型聚合后的注册描述。</param>
|
||||
/// <param name="reflectedRegistration">当前实现类型反射注册项。</param>
|
||||
/// <param name="implementationVariableName">生成代码中的实现类型变量名。</param>
|
||||
private static void AppendOrderedReflectedImplementationRegistration(
|
||||
StringBuilder builder,
|
||||
ImplementationRegistrationSpec registration,
|
||||
ReflectedImplementationRegistrationSpec reflectedRegistration,
|
||||
string implementationVariableName)
|
||||
{
|
||||
AppendServiceRegistration(
|
||||
builder,
|
||||
$"typeof({reflectedRegistration.HandlerInterfaceDisplayName})",
|
||||
implementationVariableName,
|
||||
" ");
|
||||
AppendRegistrationLog(
|
||||
builder,
|
||||
registration.ImplementationLogName,
|
||||
reflectedRegistration.HandlerInterfaceLogName,
|
||||
" ");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射 handler 接口需要运行时精确构造的注册语句。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="registration">单个实现类型聚合后的注册描述。</param>
|
||||
/// <param name="preciseRegistration">当前精确反射注册项。</param>
|
||||
/// <param name="registrationIndex">实现类型在整体注册列表中的索引。</param>
|
||||
/// <param name="orderedRegistrationIndex">当前注册项在原始精确反射注册集合中的索引。</param>
|
||||
/// <param name="implementationVariableName">生成代码中的实现类型变量名。</param>
|
||||
private static void AppendOrderedPreciseReflectedRegistration(
|
||||
StringBuilder builder,
|
||||
ImplementationRegistrationSpec registration,
|
||||
PreciseReflectedRegistrationSpec preciseRegistration,
|
||||
int registrationIndex,
|
||||
int orderedRegistrationIndex,
|
||||
string implementationVariableName)
|
||||
{
|
||||
var registrationVariablePrefix = $"serviceType{registrationIndex}_{orderedRegistrationIndex}";
|
||||
AppendPreciseReflectedTypeResolution(
|
||||
builder,
|
||||
preciseRegistration.ServiceTypeArguments,
|
||||
registrationVariablePrefix,
|
||||
implementationVariableName,
|
||||
preciseRegistration.OpenHandlerTypeDisplayName,
|
||||
registration.ImplementationLogName,
|
||||
preciseRegistration.HandlerInterfaceLogName,
|
||||
3);
|
||||
}
|
||||
|
||||
private static void AppendPreciseReflectedTypeResolution(
|
||||
StringBuilder builder,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec> serviceTypeArguments,
|
||||
string registrationVariablePrefix,
|
||||
string implementationVariableName,
|
||||
string openHandlerTypeDisplayName,
|
||||
string implementationLogName,
|
||||
string handlerInterfaceLogName,
|
||||
int indentLevel)
|
||||
{
|
||||
var indent = new string(' ', indentLevel * 4);
|
||||
var reflectedArgumentNames = new List<string>();
|
||||
var resolvedArgumentNames = AppendServiceTypeArgumentResolutions(
|
||||
builder,
|
||||
serviceTypeArguments,
|
||||
registrationVariablePrefix,
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
|
||||
if (reflectedArgumentNames.Count > 0)
|
||||
indent = AppendReflectedArgumentGuardStart(builder, reflectedArgumentNames, indent);
|
||||
|
||||
AppendClosedGenericServiceTypeCreation(
|
||||
builder,
|
||||
registrationVariablePrefix,
|
||||
openHandlerTypeDisplayName,
|
||||
resolvedArgumentNames,
|
||||
indent);
|
||||
AppendServiceRegistration(builder, registrationVariablePrefix, implementationVariableName, indent);
|
||||
AppendRegistrationLog(builder, implementationLogName, handlerInterfaceLogName, indent);
|
||||
|
||||
if (reflectedArgumentNames.Count > 0)
|
||||
{
|
||||
builder.Append(new string(' ', indentLevel * 4));
|
||||
builder.AppendLine("}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归发射每个 handler 泛型实参的运行时类型解析表达式。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="serviceTypeArguments">handler 服务类型的运行时泛型实参描述。</param>
|
||||
/// <param name="registrationVariablePrefix">当前注册项的稳定变量名前缀。</param>
|
||||
/// <param name="reflectedArgumentNames">需要空值检查的反射解析变量集合。</param>
|
||||
/// <param name="indent">当前生成语句的缩进。</param>
|
||||
/// <returns>可传给 <c>MakeGenericType</c> 的实参表达式。</returns>
|
||||
private static string[] AppendServiceTypeArgumentResolutions(
|
||||
StringBuilder builder,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec> serviceTypeArguments,
|
||||
string registrationVariablePrefix,
|
||||
ICollection<string> reflectedArgumentNames,
|
||||
string indent)
|
||||
{
|
||||
var resolvedArgumentNames = new string[serviceTypeArguments.Length];
|
||||
for (var argumentIndex = 0; argumentIndex < serviceTypeArguments.Length; argumentIndex++)
|
||||
{
|
||||
resolvedArgumentNames[argumentIndex] = AppendRuntimeTypeReferenceResolution(
|
||||
builder,
|
||||
serviceTypeArguments[argumentIndex],
|
||||
$"{registrationVariablePrefix}Argument{argumentIndex}",
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
}
|
||||
|
||||
return resolvedArgumentNames;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为运行时反射解析出的泛型实参发射空值保护块,避免生成注册器注册无法完整构造的服务类型。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="reflectedArgumentNames">需要参与空值检查的变量名。</param>
|
||||
/// <param name="indent">保护块开始前的缩进。</param>
|
||||
/// <returns>保护块内部应使用的下一层缩进。</returns>
|
||||
private static string AppendReflectedArgumentGuardStart(
|
||||
StringBuilder builder,
|
||||
IReadOnlyList<string> reflectedArgumentNames,
|
||||
string indent)
|
||||
{
|
||||
builder.Append(indent);
|
||||
builder.Append("if (");
|
||||
for (var index = 0; index < reflectedArgumentNames.Count; index++)
|
||||
{
|
||||
if (index > 0)
|
||||
builder.Append(" && ");
|
||||
|
||||
builder.Append(reflectedArgumentNames[index]);
|
||||
builder.Append(" is not null");
|
||||
}
|
||||
|
||||
builder.AppendLine(")");
|
||||
builder.Append(indent);
|
||||
builder.AppendLine("{");
|
||||
return $"{indent} ";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射关闭 handler 服务类型的 <c>MakeGenericType</c> 构造语句。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="registrationVariablePrefix">生成代码中的服务类型变量名。</param>
|
||||
/// <param name="openHandlerTypeDisplayName">开放 handler 接口类型显示名。</param>
|
||||
/// <param name="resolvedArgumentNames">已解析的泛型实参表达式。</param>
|
||||
/// <param name="indent">当前生成语句的缩进。</param>
|
||||
private static void AppendClosedGenericServiceTypeCreation(
|
||||
StringBuilder builder,
|
||||
string registrationVariablePrefix,
|
||||
string openHandlerTypeDisplayName,
|
||||
IReadOnlyList<string> resolvedArgumentNames,
|
||||
string indent)
|
||||
{
|
||||
builder.Append(indent);
|
||||
builder.Append("var ");
|
||||
builder.Append(registrationVariablePrefix);
|
||||
builder.Append(" = typeof(");
|
||||
builder.Append(openHandlerTypeDisplayName);
|
||||
builder.Append(").MakeGenericType(");
|
||||
for (var index = 0; index < resolvedArgumentNames.Count; index++)
|
||||
{
|
||||
if (index > 0)
|
||||
builder.Append(", ");
|
||||
|
||||
builder.Append(resolvedArgumentNames[index]);
|
||||
}
|
||||
|
||||
builder.AppendLine(");");
|
||||
}
|
||||
|
||||
private static string AppendRuntimeTypeReferenceResolution(
|
||||
StringBuilder builder,
|
||||
RuntimeTypeReferenceSpec runtimeTypeReference,
|
||||
string variableBaseName,
|
||||
ICollection<string> reflectedArgumentNames,
|
||||
string indent)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(runtimeTypeReference.TypeDisplayName))
|
||||
return $"typeof({runtimeTypeReference.TypeDisplayName})";
|
||||
|
||||
if (runtimeTypeReference.ArrayElementTypeReference is not null)
|
||||
return AppendArrayRuntimeTypeReferenceResolution(
|
||||
builder,
|
||||
runtimeTypeReference,
|
||||
variableBaseName,
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
|
||||
if (runtimeTypeReference.PointerElementTypeReference is not null)
|
||||
return AppendPointerRuntimeTypeReferenceResolution(
|
||||
builder,
|
||||
runtimeTypeReference,
|
||||
variableBaseName,
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
|
||||
if (runtimeTypeReference.GenericTypeDefinitionReference is not null)
|
||||
return AppendConstructedGenericRuntimeTypeReferenceResolution(
|
||||
builder,
|
||||
runtimeTypeReference,
|
||||
variableBaseName,
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
|
||||
return AppendReflectionRuntimeTypeReferenceResolution(
|
||||
builder,
|
||||
runtimeTypeReference,
|
||||
variableBaseName,
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射数组类型引用的运行时重建表达式。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="runtimeTypeReference">数组类型引用描述。</param>
|
||||
/// <param name="variableBaseName">用于递归生成变量名的稳定前缀。</param>
|
||||
/// <param name="reflectedArgumentNames">需要空值检查的反射解析变量集合。</param>
|
||||
/// <param name="indent">当前生成语句的缩进。</param>
|
||||
/// <returns>数组类型表达式。</returns>
|
||||
private static string AppendArrayRuntimeTypeReferenceResolution(
|
||||
StringBuilder builder,
|
||||
RuntimeTypeReferenceSpec runtimeTypeReference,
|
||||
string variableBaseName,
|
||||
ICollection<string> reflectedArgumentNames,
|
||||
string indent)
|
||||
{
|
||||
var elementExpression = AppendRuntimeTypeReferenceResolution(
|
||||
builder,
|
||||
runtimeTypeReference.ArrayElementTypeReference!,
|
||||
$"{variableBaseName}Element",
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
|
||||
return runtimeTypeReference.ArrayRank == 1
|
||||
? $"{elementExpression}.MakeArrayType()"
|
||||
: $"{elementExpression}.MakeArrayType({runtimeTypeReference.ArrayRank})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射指针类型引用的运行时重建表达式。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="runtimeTypeReference">指针类型引用描述。</param>
|
||||
/// <param name="variableBaseName">用于递归生成变量名的稳定前缀。</param>
|
||||
/// <param name="reflectedArgumentNames">需要空值检查的反射解析变量集合。</param>
|
||||
/// <param name="indent">当前生成语句的缩进。</param>
|
||||
/// <returns>指针类型表达式。</returns>
|
||||
private static string AppendPointerRuntimeTypeReferenceResolution(
|
||||
StringBuilder builder,
|
||||
RuntimeTypeReferenceSpec runtimeTypeReference,
|
||||
string variableBaseName,
|
||||
ICollection<string> reflectedArgumentNames,
|
||||
string indent)
|
||||
{
|
||||
var pointedAtExpression = AppendRuntimeTypeReferenceResolution(
|
||||
builder,
|
||||
runtimeTypeReference.PointerElementTypeReference!,
|
||||
$"{variableBaseName}PointedAt",
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
|
||||
return $"{pointedAtExpression}.MakePointerType()";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射已构造泛型类型引用的运行时重建表达式。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="runtimeTypeReference">已构造泛型类型引用描述。</param>
|
||||
/// <param name="variableBaseName">用于递归生成变量名的稳定前缀。</param>
|
||||
/// <param name="reflectedArgumentNames">需要空值检查的反射解析变量集合。</param>
|
||||
/// <param name="indent">当前生成语句的缩进。</param>
|
||||
/// <returns>已构造泛型类型表达式。</returns>
|
||||
private static string AppendConstructedGenericRuntimeTypeReferenceResolution(
|
||||
StringBuilder builder,
|
||||
RuntimeTypeReferenceSpec runtimeTypeReference,
|
||||
string variableBaseName,
|
||||
ICollection<string> reflectedArgumentNames,
|
||||
string indent)
|
||||
{
|
||||
var genericTypeDefinitionExpression = AppendRuntimeTypeReferenceResolution(
|
||||
builder,
|
||||
runtimeTypeReference.GenericTypeDefinitionReference!,
|
||||
$"{variableBaseName}GenericDefinition",
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
var genericArgumentExpressions = new string[runtimeTypeReference.GenericTypeArguments.Length];
|
||||
for (var argumentIndex = 0;
|
||||
argumentIndex < runtimeTypeReference.GenericTypeArguments.Length;
|
||||
argumentIndex++)
|
||||
{
|
||||
genericArgumentExpressions[argumentIndex] = AppendRuntimeTypeReferenceResolution(
|
||||
builder,
|
||||
runtimeTypeReference.GenericTypeArguments[argumentIndex],
|
||||
$"{variableBaseName}GenericArgument{argumentIndex}",
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
}
|
||||
|
||||
return $"{genericTypeDefinitionExpression}.MakeGenericType({string.Join(", ", genericArgumentExpressions)})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射命名类型的运行时反射查找语句,并返回后续服务类型构造应引用的变量名。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="runtimeTypeReference">反射查找类型引用描述。</param>
|
||||
/// <param name="variableBaseName">生成代码中的反射变量名。</param>
|
||||
/// <param name="reflectedArgumentNames">需要空值检查的反射解析变量集合。</param>
|
||||
/// <param name="indent">当前生成语句的缩进。</param>
|
||||
/// <returns>生成代码中的反射变量名。</returns>
|
||||
private static string AppendReflectionRuntimeTypeReferenceResolution(
|
||||
StringBuilder builder,
|
||||
RuntimeTypeReferenceSpec runtimeTypeReference,
|
||||
string variableBaseName,
|
||||
ICollection<string> reflectedArgumentNames,
|
||||
string indent)
|
||||
{
|
||||
reflectedArgumentNames.Add(variableBaseName);
|
||||
builder.Append(indent);
|
||||
builder.Append("var ");
|
||||
builder.Append(variableBaseName);
|
||||
if (string.IsNullOrWhiteSpace(runtimeTypeReference.ReflectionAssemblyName))
|
||||
{
|
||||
builder.Append(" = registryAssembly.GetType(\"");
|
||||
builder.Append(EscapeStringLiteral(runtimeTypeReference.ReflectionTypeMetadataName!));
|
||||
builder.AppendLine("\", throwOnError: false, ignoreCase: false);");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(" = ResolveReferencedAssemblyType(\"");
|
||||
builder.Append(EscapeStringLiteral(runtimeTypeReference.ReflectionAssemblyName!));
|
||||
builder.Append("\", \"");
|
||||
builder.Append(EscapeStringLiteral(runtimeTypeReference.ReflectionTypeMetadataName!));
|
||||
builder.AppendLine("\");");
|
||||
}
|
||||
|
||||
return variableBaseName;
|
||||
}
|
||||
|
||||
private static void AppendReflectionHelpers(StringBuilder builder)
|
||||
{
|
||||
builder.AppendLine(
|
||||
" private static global::System.Type? ResolveReferencedAssemblyType(string assemblyIdentity, string typeMetadataName)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" var assembly = ResolveReferencedAssembly(assemblyIdentity);");
|
||||
builder.AppendLine(
|
||||
" return assembly?.GetType(typeMetadataName, throwOnError: false, ignoreCase: false);");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(
|
||||
" private static global::System.Reflection.Assembly? ResolveReferencedAssembly(string assemblyIdentity)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" global::System.Reflection.AssemblyName targetAssemblyName;");
|
||||
builder.AppendLine(" try");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(
|
||||
" targetAssemblyName = new global::System.Reflection.AssemblyName(assemblyIdentity);");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine(" catch");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" return null;");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(
|
||||
" foreach (var assembly in global::System.AppDomain.CurrentDomain.GetAssemblies())");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(
|
||||
" if (global::System.Reflection.AssemblyName.ReferenceMatchesDefinition(targetAssemblyName, assembly.GetName()))");
|
||||
builder.AppendLine(" return assembly;");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(" try");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(
|
||||
" return global::System.Reflection.Assembly.Load(targetAssemblyName);");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine(" catch");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" return null;");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine(" }");
|
||||
}
|
||||
|
||||
private static string EscapeStringLiteral(string value)
|
||||
{
|
||||
return value.Replace("\\", "\\\\")
|
||||
.Replace("\"", "\\\"")
|
||||
.Replace("\n", "\\n")
|
||||
.Replace("\r", "\\r");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -651,32 +651,70 @@ internal static class CqrsHandlerRegistrar
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 描述某个程序集在生成注册器之后仍需运行时补扫的 handler 元数据。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该对象把“是否存在精确 fallback 类型列表”与“是否只能回退到整程序集扫描”收敛为同一份内部状态,
|
||||
/// 供注册流水线后续阶段统一判断。
|
||||
/// </remarks>
|
||||
private sealed class ReflectionFallbackMetadata(IReadOnlyList<Type> types)
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取需要通过运行时反射补充注册的 handler 类型集合。
|
||||
/// </summary>
|
||||
public IReadOnlyList<Type> Types { get; } = types ?? throw new ArgumentNullException(nameof(types));
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否持有精确的 fallback 类型清单。
|
||||
/// </summary>
|
||||
public bool HasExplicitTypes => Types.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 描述单个程序集在注册阶段提取到的 generated registry 与 reflection fallback 元数据。
|
||||
/// </summary>
|
||||
private sealed class AssemblyRegistrationMetadata(
|
||||
IReadOnlyList<Type> registryTypes,
|
||||
ReflectionFallbackMetadata? reflectionFallbackMetadata)
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取程序集上声明的 generated registry 类型集合。
|
||||
/// </summary>
|
||||
public IReadOnlyList<Type> RegistryTypes { get; } =
|
||||
registryTypes ?? throw new ArgumentNullException(nameof(registryTypes));
|
||||
|
||||
/// <summary>
|
||||
/// 获取该程序集是否还要求运行时补充 reflection fallback。
|
||||
/// </summary>
|
||||
public ReflectionFallbackMetadata? ReflectionFallbackMetadata { get; } = reflectionFallbackMetadata;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 缓存 generated registry 激活所需的类型判定结果与工厂委托。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该缓存把“是否实现契约”“是否为抽象类型”“是否已构建激活委托”封装为不可变快照,
|
||||
/// 避免对同一 registry 类型重复执行反射分析。
|
||||
/// </remarks>
|
||||
private sealed class RegistryActivationMetadata(
|
||||
bool implementsRegistryContract,
|
||||
bool isAbstract,
|
||||
Func<ICqrsHandlerRegistry>? factory)
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取目标类型是否实现了 <see cref="ICqrsHandlerRegistry" />。
|
||||
/// </summary>
|
||||
public bool ImplementsRegistryContract { get; } = implementsRegistryContract;
|
||||
|
||||
/// <summary>
|
||||
/// 获取目标类型是否为抽象类型。
|
||||
/// </summary>
|
||||
public bool IsAbstract { get; } = isAbstract;
|
||||
|
||||
/// <summary>
|
||||
/// 获取可用于实例化 registry 的工厂委托。
|
||||
/// </summary>
|
||||
public Func<ICqrsHandlerRegistry>? Factory { get; } = factory;
|
||||
}
|
||||
}
|
||||
|
||||
103
GFramework.Ecs.Arch.Abstractions/README.md
Normal file
103
GFramework.Ecs.Arch.Abstractions/README.md
Normal file
@ -0,0 +1,103 @@
|
||||
# GFramework.Ecs.Arch.Abstractions
|
||||
|
||||
`GFramework.Ecs.Arch.Abstractions` 承载 Arch ECS 集成层的最小契约,用来让共享业务层、宿主循环或扩展模块在不依赖
|
||||
`GFramework.Ecs.Arch` 默认实现的前提下,仍然可以约定 ECS 模块边界。
|
||||
|
||||
如果你需要的是 `UseArch(...)` 扩展、`ArchSystemAdapter<T>` 基类、`World` 注册和默认模块实现,请改为依赖
|
||||
`GFramework.Ecs.Arch`。
|
||||
|
||||
## 包定位
|
||||
|
||||
- 这是 `Ecs.Arch` 的契约层,不是默认实现层。
|
||||
- 适合让上层模块只面向 `IArchEcsModule`、`IArchSystemAdapter<T>` 和 `ArchOptions` 编程。
|
||||
- 常见场景:
|
||||
- 共享宿主循环只依赖更新契约,不直接引用 Arch runtime 实现
|
||||
- 多程序集之间需要共享 ECS 配置对象或接口边界
|
||||
- 测试替身、编辑器工具或外部适配层希望复用契约,但自行决定底层实现
|
||||
|
||||
## 与相邻包的关系
|
||||
|
||||
- `GFramework.Core.Abstractions`
|
||||
- 本包直接依赖它,并复用 `IServiceModule`、`ISystem` 等基础契约。
|
||||
- `GFramework.Ecs.Arch.Abstractions`
|
||||
- 只定义 Arch ECS 集成相关的最小契约和配置对象。
|
||||
- `GFramework.Ecs.Arch`
|
||||
- 本包的默认实现层。
|
||||
- 负责 `UseArch(...)` 扩展、默认模块注册、Arch `World` 装配,以及系统适配器基类。
|
||||
|
||||
## 契约地图
|
||||
|
||||
| 文件 | 作用 |
|
||||
| --- | --- |
|
||||
| `IArchEcsModule.cs` | ECS 模块服务契约,负责统一驱动系统更新 |
|
||||
| `IArchSystemAdapter.cs` | 让 ECS 系统适配到 GFramework `ISystem` 生命周期的接口 |
|
||||
| `ArchOptions.cs` | `WorldCapacity`、`EnableStatistics`、`Priority` 等配置对象 |
|
||||
|
||||
## XML 阅读基线
|
||||
|
||||
下表记录当前契约包的类型声明级 XML 基线,方便把 README、站内抽象页与源码阅读顺序对齐。
|
||||
|
||||
| 类型族 | 代表类型 | XML 状态 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| 模块契约 | `IArchEcsModule` | 已覆盖 | 宿主循环如何统一驱动 ECS 更新 |
|
||||
| 系统桥接契约 | `IArchSystemAdapter<T>` | 已覆盖 | 外部模块怎样只依赖更新接口而不绑定默认实现 |
|
||||
| 配置对象 | `ArchOptions` | 已覆盖 | 跨程序集共享 ECS 配置边界 |
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
### 1. 只想约定宿主循环与 ECS 模块边界
|
||||
|
||||
```csharp
|
||||
using GFramework.Ecs.Arch.Abstractions;
|
||||
|
||||
public sealed class EcsUpdateLoop
|
||||
{
|
||||
private readonly IArchEcsModule _ecsModule;
|
||||
|
||||
public EcsUpdateLoop(IArchEcsModule ecsModule)
|
||||
{
|
||||
_ecsModule = ecsModule;
|
||||
}
|
||||
|
||||
public void Tick(float deltaTime)
|
||||
{
|
||||
_ecsModule.Update(deltaTime);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 只想共享配置对象
|
||||
|
||||
```csharp
|
||||
using GFramework.Ecs.Arch.Abstractions;
|
||||
|
||||
var options = new ArchOptions
|
||||
{
|
||||
WorldCapacity = 2048,
|
||||
EnableStatistics = true,
|
||||
Priority = 40
|
||||
};
|
||||
```
|
||||
|
||||
### 3. 什么时候要升级到 `GFramework.Ecs.Arch`
|
||||
|
||||
一旦你需要下面任一项,就不该只停留在本包:
|
||||
|
||||
- `UseArch(...)` 或其他 runtime 装配入口
|
||||
- `ArchSystemAdapter<T>` 等默认基类
|
||||
- Arch `World` 的创建、注册和查询能力
|
||||
- 与 `GFramework` 架构生命周期绑定的默认模块实现
|
||||
|
||||
## 边界说明
|
||||
|
||||
- 本包不提供 Arch `World` 的默认构造与注册逻辑。
|
||||
- 本包不提供系统基类、扩展方法或默认服务实现。
|
||||
- 它回答的是“外部模块怎样与 Arch ECS 集成层约定边界”,不是“Arch ECS 默认怎么接入到项目里”。
|
||||
|
||||
## 对应文档入口
|
||||
|
||||
- 抽象接口总览:[`../docs/zh-CN/abstractions/index.md`](../docs/zh-CN/abstractions/index.md)
|
||||
- Ecs.Arch 抽象层说明:[`../docs/zh-CN/abstractions/ecs-arch-abstractions.md`](../docs/zh-CN/abstractions/ecs-arch-abstractions.md)
|
||||
- ECS 模块入口:[`../docs/zh-CN/ecs/index.md`](../docs/zh-CN/ecs/index.md)
|
||||
- Arch ECS 集成:[`../docs/zh-CN/ecs/arch.md`](../docs/zh-CN/ecs/arch.md)
|
||||
- 运行时实现入口:[`../GFramework.Ecs.Arch/README.md`](../GFramework.Ecs.Arch/README.md)
|
||||
@ -1,16 +1,30 @@
|
||||
# GFramework.Ecs.Arch
|
||||
|
||||
GFramework 的 Arch ECS 集成包,提供开箱即用的 ECS(Entity Component System)支持。
|
||||
`GFramework.Ecs.Arch` 是 `GFramework` 当前 Arch ECS family 的默认运行时实现包。
|
||||
|
||||
## 特性
|
||||
它负责把 Arch `World`、GFramework 的服务模块生命周期,以及 `ArchSystemAdapter<T>` 系统桥接到同一条采用路径中。
|
||||
如果你需要的只是共享契约,请改为依赖 `GFramework.Ecs.Arch.Abstractions`。
|
||||
|
||||
- 🎯 **显式集成** - 符合 .NET 生态习惯的显式注册方式
|
||||
- 🔌 **零依赖** - 不使用时,Core 包无 Arch 依赖
|
||||
- 🎯 **类型安全** - 完整的类型系统和编译时检查
|
||||
- ⚡ **高性能** - 基于 Arch ECS 的高性能实现
|
||||
- 🔧 **易扩展** - 简单的系统适配器模式
|
||||
## 包定位
|
||||
|
||||
## 快速开始
|
||||
- 这是运行时实现层,不是纯契约层。
|
||||
- 适合需要 `UseArch(...)`、`World` 自动注册、默认模块生命周期和系统桥接基类的项目。
|
||||
- 常见场景:
|
||||
- 在架构实例上显式接入 Arch ECS
|
||||
- 让 `World` 由默认模块创建并放入容器
|
||||
- 让 ECS 系统复用 `ArchSystemAdapter<float>` 生命周期桥接
|
||||
- 通过 `IArchEcsModule.Update(deltaTime)` 统一驱动 ECS 帧更新
|
||||
|
||||
## 与相邻包的关系
|
||||
|
||||
- `GFramework.Core`
|
||||
- 提供架构、容器、生命周期和系统注册基础设施。
|
||||
- `GFramework.Ecs.Arch.Abstractions`
|
||||
- 提供 `IArchEcsModule`、`IArchSystemAdapter<T>` 和契约层 `ArchOptions`。
|
||||
- `GFramework.Ecs.Arch`
|
||||
- 提供 `UseArch(...)`、默认 `ArchEcsModule`、`World` 注册,以及系统适配器基类与示例类型。
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
### 1. 安装包
|
||||
|
||||
@ -18,53 +32,43 @@ GFramework 的 Arch ECS 集成包,提供开箱即用的 ECS(Entity Component
|
||||
dotnet add package GeWuYou.GFramework.Ecs.Arch
|
||||
```
|
||||
|
||||
### 2. 注册 ECS 模块
|
||||
### 2. 在 `Initialize()` 之前显式接入 Arch runtime
|
||||
|
||||
按当前实现,`UseArch(...)` 会把 `ArchEcsModule` 提前登记到 `ArchitectureModuleRegistry`,因此调用时机应早于
|
||||
`Initialize()`。
|
||||
|
||||
```csharp
|
||||
// 在架构初始化时添加 Arch ECS 支持
|
||||
var architecture = new GameArchitecture(config)
|
||||
.UseArch(); // 添加 ECS 支持
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Ecs.Arch.Extensions;
|
||||
|
||||
architecture.Initialize();
|
||||
```
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
public GameArchitecture() : base(new ArchitectureConfiguration())
|
||||
{
|
||||
}
|
||||
|
||||
### 3. 带配置的注册
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterSystem<MovementSystem>();
|
||||
}
|
||||
}
|
||||
|
||||
```csharp
|
||||
var architecture = new GameArchitecture(config)
|
||||
var architecture = new GameArchitecture()
|
||||
.UseArch(options =>
|
||||
{
|
||||
options.WorldCapacity = 2000;
|
||||
options.EnableStatistics = true;
|
||||
options.WorldCapacity = 2048;
|
||||
options.Priority = 50;
|
||||
});
|
||||
|
||||
architecture.Initialize();
|
||||
```
|
||||
|
||||
```csharp
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct Position(float x, float y)
|
||||
{
|
||||
public float X { get; set; } = x;
|
||||
public float Y { get; set; } = y;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct Velocity(float x, float y)
|
||||
{
|
||||
public float X { get; set; } = x;
|
||||
public float Y { get; set; } = y;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 创建系统
|
||||
### 3. 编写并注册系统
|
||||
|
||||
```csharp
|
||||
using Arch.Core;
|
||||
using GFramework.Ecs.Arch;
|
||||
using GFramework.Ecs.Arch.Components;
|
||||
|
||||
public sealed class MovementSystem : ArchSystemAdapter<float>
|
||||
{
|
||||
@ -78,115 +82,57 @@ public sealed class MovementSystem : ArchSystemAdapter<float>
|
||||
|
||||
protected override void OnUpdate(in float deltaTime)
|
||||
{
|
||||
World.Query(in _query, (ref Position pos, ref Velocity vel) =>
|
||||
var frameDelta = deltaTime;
|
||||
|
||||
World.Query(in _query, (ref Position position, ref Velocity velocity) =>
|
||||
{
|
||||
pos.X += vel.X * deltaTime;
|
||||
pos.Y += vel.Y * deltaTime;
|
||||
position.X += velocity.X * frameDelta;
|
||||
position.Y += velocity.Y * frameDelta;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 注册系统
|
||||
### 4. 初始化后获取 `World` 与 ECS 模块
|
||||
|
||||
```csharp
|
||||
public class MyArchitecture : Architecture
|
||||
{
|
||||
protected override void OnRegisterSystem(IIocContainer container)
|
||||
{
|
||||
container.Register<MovementSystem>();
|
||||
}
|
||||
}
|
||||
using Arch.Core;
|
||||
using GFramework.Ecs.Arch.Abstractions;
|
||||
|
||||
var world = architecture.Context.GetService<World>();
|
||||
var ecsModule = architecture.Context.GetService<IArchEcsModule>();
|
||||
```
|
||||
|
||||
### 7. 创建实体
|
||||
### 5. 由宿主循环驱动更新
|
||||
|
||||
```csharp
|
||||
var world = this.GetService<World>();
|
||||
var entity = world.Create(
|
||||
new Position(0, 0),
|
||||
new Velocity(1, 1)
|
||||
);
|
||||
```
|
||||
|
||||
### 8. 更新系统
|
||||
|
||||
```csharp
|
||||
var ecsModule = this.GetService<IArchEcsModule>();
|
||||
ecsModule.Update(deltaTime);
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
## 运行时职责地图
|
||||
|
||||
### 代码配置
|
||||
| 文件 | 作用 |
|
||||
| --- | --- |
|
||||
| `Extensions/ArchExtensions.cs` | 通过 `UseArch(...)` 把默认模块注册到 `ArchitectureModuleRegistry` |
|
||||
| `ArchEcsModule.cs` | 创建并注册 `World`,按优先级收集 `ArchSystemAdapter<float>`,负责初始化、销毁和逐帧更新 |
|
||||
| `ArchSystemAdapter.cs` | 把 GFramework 系统生命周期桥接到 Arch `ISystem<T>` 生命周期 |
|
||||
| `ArchOptions.cs` | 承载 `WorldCapacity`、`EnableStatistics`、`Priority` 这组运行时配置 |
|
||||
| `Components/*.cs`、`Systems/*.cs` | 提供最小组件与系统示例,帮助对照查询写法和更新模式 |
|
||||
|
||||
```csharp
|
||||
var architecture = new GameArchitecture(config)
|
||||
.UseArch(options =>
|
||||
{
|
||||
options.WorldCapacity = 2000;
|
||||
options.EnableStatistics = true;
|
||||
options.Priority = 50;
|
||||
});
|
||||
```
|
||||
## XML 阅读基线
|
||||
|
||||
### 配置说明
|
||||
下表记录当前模块 README 与源码可对照的类型声明级 XML 基线。
|
||||
|
||||
- `WorldCapacity` - World 初始容量(默认:1000)
|
||||
- `EnableStatistics` - 是否启用统计信息(默认:false)
|
||||
- `Priority` - 模块优先级(默认:50)
|
||||
| 类型族 | 代表类型 | XML 状态 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| 装配入口 | `ArchExtensions` | 已覆盖 | `UseArch(...)` 的时机与返回值 |
|
||||
| 运行时模块 | `ArchEcsModule` | 已覆盖 | `World` 注册、系统排序、销毁顺序 |
|
||||
| 系统桥接层 | `ArchSystemAdapter<T>` | 已覆盖 | `OnArchInitialize`、`OnUpdate`、`OnArchDispose` |
|
||||
| 示例类型 | `Position`、`Velocity`、`MovementSystem` | 已覆盖 | 组件布局、查询写法、最小示例 |
|
||||
|
||||
## 架构说明
|
||||
## 对应文档入口
|
||||
|
||||
### 显式注册模式
|
||||
|
||||
本包采用 .NET 生态标准的显式注册模式,基于架构实例:
|
||||
|
||||
**优点:**
|
||||
|
||||
- ✅ 符合 .NET 生态习惯
|
||||
- ✅ 显式、可控
|
||||
- ✅ 易于测试和调试
|
||||
- ✅ 支持配置
|
||||
- ✅ 支持链式调用
|
||||
- ✅ 避免"魔法"行为
|
||||
|
||||
**使用方式:**
|
||||
```csharp
|
||||
// 在架构初始化时添加
|
||||
var architecture = new GameArchitecture(config)
|
||||
.UseArch(); // 显式注册
|
||||
|
||||
architecture.Initialize();
|
||||
```
|
||||
|
||||
详见:[INTEGRATION_PATTERN.md](INTEGRATION_PATTERN.md)
|
||||
|
||||
### 系统适配器
|
||||
|
||||
`ArchSystemAdapter<T>` 桥接 Arch.System.ISystem<T> 到 GFramework 架构:
|
||||
|
||||
- 自动获取 World 实例
|
||||
- 集成到框架生命周期
|
||||
- 支持上下文感知(Context-Aware)
|
||||
|
||||
### 生命周期
|
||||
|
||||
1. **注册阶段** - 模块自动注册到架构
|
||||
2. **初始化阶段** - 创建 World,初始化系统
|
||||
3. **运行阶段** - 每帧调用 Update
|
||||
4. **销毁阶段** - 清理资源,销毁 World
|
||||
|
||||
## 示例
|
||||
|
||||
完整示例请参考 `GFramework.Ecs.Arch.Tests` 项目。
|
||||
|
||||
## 依赖
|
||||
|
||||
- GFramework.Core >= 1.0.0
|
||||
- Arch >= 2.1.0
|
||||
- Arch.System >= 1.1.0
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
- ECS 总览:[`../docs/zh-CN/ecs/index.md`](../docs/zh-CN/ecs/index.md)
|
||||
- Arch ECS 集成:[`../docs/zh-CN/ecs/arch.md`](../docs/zh-CN/ecs/arch.md)
|
||||
- 抽象契约页:[`../docs/zh-CN/abstractions/ecs-arch-abstractions.md`](../docs/zh-CN/abstractions/ecs-arch-abstractions.md)
|
||||
- 统一 API / XML 导航:[`../docs/zh-CN/api-reference/index.md`](../docs/zh-CN/api-reference/index.md)
|
||||
|
||||
@ -131,6 +131,20 @@ Scene 与 UI 路由共享这套基础约定。
|
||||
- `Enums/`
|
||||
- UI/Scene 转场、UI 层级、输入动作、存储类型等公共枚举
|
||||
|
||||
## XML 覆盖基线
|
||||
|
||||
下面这份 inventory 记录的是 `2026-04-23` 对 `GFramework.Game.Abstractions` 做的一轮轻量 XML 盘点结果:只统计公开 /
|
||||
内部类型声明是否带 XML 注释,用来建立契约层阅读入口;成员级参数、返回值、异常和生命周期说明仍需要在后续 API 波次继续细化。
|
||||
|
||||
| 契约族 | 基线状态 | 代表类型 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Config/` | `7/7` 个类型声明已带 XML 注释 | `IConfigLoader`、`IConfigRegistry`、`IConfigTable<TKey, TValue>`、`ConfigLoadException` | 看配置表注册、读取约定和失败诊断模型 |
|
||||
| `Data/` | `14/14` 个类型声明已带 XML 注释 | `IDataRepository`、`ISettingsDataRepository`、`ISaveRepository<TSaveData>`、`DataRepositoryOptions` | 看业务数据、设置持久化、槽位存档和版本迁移契约 |
|
||||
| `Setting/` | `12/12` 个类型声明已带 XML 注释 | `ISettingsData`、`ISettingsModel`、`ISettingsSystem`、`LocalizationSettings` | 看设置数据、应用语义、迁移接口和内置设置对象 |
|
||||
| `Scene/` | `14/14` 个类型声明已带 XML 注释 | `IScene`、`ISceneRouter`、`ISceneFactory`、`SceneTransitionEvent` | 看场景行为、路由、工厂 / root 边界与转场事件模型 |
|
||||
| `UI/` | `19/19` 个类型声明已带 XML 注释 | `IUiPage`、`IUiRouter`、`IUiFactory`、`UiInteractionProfile`、`UiTransitionHandlerOptions` | 看页面栈、层级 UI、输入动作与 UI 转场契约 |
|
||||
| `Routing/` `Storage/` `Asset/` `Enums/` | `13/13` 个类型声明已带 XML 注释 | `IRoute`、`IRouteContext`、`IFileStorage`、`IAssetRegistry<T>`、`UiLayer`、`SceneTransitionType` | 看公共路由上下文、存储角色、资源注册表与跨层共享枚举 |
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
### 1. 只想在公共业务层声明游戏对象
|
||||
|
||||
@ -18,3 +18,4 @@
|
||||
GF_ConfigSchema_011 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
GF_ConfigSchema_012 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
GF_ConfigSchema_013 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
GF_ConfigSchema_014 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -151,4 +151,15 @@ public static class ConfigSchemaDiagnostics
|
||||
SourceGeneratorsConfigCategory,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
|
||||
/// <summary>
|
||||
/// schema 字段名在标识符归一化后发生冲突。
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor DuplicateGeneratedIdentifier = new(
|
||||
"GF_ConfigSchema_014",
|
||||
"Config schema property names collide after C# identifier normalization",
|
||||
"Property '{1}' in schema file '{0}' uses schema key '{2}', which generates duplicate C# identifier '{3}' already produced by schema key '{4}'",
|
||||
SourceGeneratorsConfigCategory,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
}
|
||||
|
||||
@ -44,6 +44,15 @@ GameProject/
|
||||
|
||||
默认情况下,打包产物会通过 `targets` 把 `schemas/**/*.schema.json` 纳入 `AdditionalFiles`。
|
||||
|
||||
## XML 覆盖基线
|
||||
|
||||
下面这份 inventory 记录的是 `2026-04-23` 对 `GFramework.Game.SourceGenerators` 做的一轮轻量 XML 盘点结果:只统计公开类型声明是否带 XML 注释,用来建立生成器入口;具体诊断消息、生成输出和兼容性语义仍需要回到源码与测试继续核对。
|
||||
|
||||
| 类型族 | 基线状态 | 代表类型 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Config/` | `1/1` 个类型声明已带 XML 注释 | `SchemaConfigGenerator` | 看 schema 到配置类型 / 表包装 / 注册辅助代码的生成入口 |
|
||||
| `Diagnostics/` | `1/1` 个类型声明已带 XML 注释 | `ConfigSchemaDiagnostics` | 看生成器会抛出的诊断类别与失败边界 |
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
```xml
|
||||
|
||||
@ -169,6 +169,19 @@
|
||||
|
||||
这两部分一般被上层子系统消费,不是多数项目的第一接入点。
|
||||
|
||||
## XML 覆盖基线
|
||||
|
||||
下面这份 inventory 记录的是 `2026-04-23` 对 `GFramework.Game` 做的一轮轻量 XML 盘点结果:只统计公开 /
|
||||
内部类型声明是否带 XML 注释,用来建立运行时阅读入口;成员级参数、返回值、异常和生命周期说明仍需要在后续 API 波次继续细化。
|
||||
|
||||
| 子系统 | 基线状态 | 代表类型 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Config/` | `26/26` 个类型声明已带 XML 注释 | `YamlConfigLoader`、`ConfigRegistry`、`GameConfigBootstrap`、`YamlConfigSchemaValidator` | 看 YAML 加载、schema 校验、模块接入与热重载边界 |
|
||||
| `Data/` `Storage/` `Serializer/` | `8/8` 个类型声明已带 XML 注释 | `DataRepository`、`SaveRepository<TSaveData>`、`UnifiedSettingsDataRepository`、`FileStorage`、`JsonSerializer` | 看持久化布局、槽位存档、统一设置文件和底层序列化 / 存储实现 |
|
||||
| `Setting/` | `9/9` 个类型声明已带 XML 注释 | `SettingsModel<TRepository>`、`SettingsSystem`、`SettingsAppliedEvent<T>` | 看初始化、应用、保存、重置等设置生命周期编排 |
|
||||
| `Scene/` `UI/` `Routing/` | `10/10` 个类型声明已带 XML 注释 | `SceneRouterBase`、`UiRouterBase`、`SceneTransitionPipeline`、`UiTransitionPipeline`、`RouterBase<TRoute, TContext>` | 看路由基类、转换处理器和项目层需要自己提供的 factory / root 边界 |
|
||||
| `Extensions/` `Internal/` `State/` | `3/3` 个类型声明已带 XML 注释 | `DataLocationExtensions`、`VersionedMigrationRunner`、`GameStateMachineSystem` | 看辅助扩展、内部迁移执行逻辑和游戏态状态机封装 |
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
下面按最常见的四种接入目标给出最短路径。
|
||||
|
||||
@ -6,6 +6,32 @@ namespace GFramework.SourceGenerators.Tests.Config;
|
||||
[TestFixture]
|
||||
public class SchemaConfigGeneratorTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证 AdditionalFiles 读取被取消时会向上传播取消,而不是伪造成 schema 诊断。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Run_Should_Propagate_Cancellation_When_AdditionalText_Read_Is_Cancelled()
|
||||
{
|
||||
var method = typeof(global::GFramework.Game.SourceGenerators.Config.SchemaConfigGenerator)
|
||||
.GetMethod(
|
||||
"TryReadSchemaText",
|
||||
global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static);
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
cancellationTokenSource.Cancel();
|
||||
var invocationArguments = new object?[]
|
||||
{
|
||||
new ThrowingAdditionalText("monster.schema.json"),
|
||||
cancellationTokenSource.Token,
|
||||
null,
|
||||
null
|
||||
};
|
||||
|
||||
var exception = Assert.Throws<global::System.Reflection.TargetInvocationException>(() =>
|
||||
method!.Invoke(null, invocationArguments));
|
||||
|
||||
Assert.That(exception!.InnerException, Is.TypeOf<OperationCanceledException>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证缺失必填 id 字段时会产生命名明确的诊断。
|
||||
/// </summary>
|
||||
@ -46,6 +72,111 @@ public class SchemaConfigGeneratorTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证根节点 <c>type</c> 元数据不是字符串时,会返回根对象约束诊断,而不是抛出 JSON 访问异常。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Run_Should_Report_Diagnostic_When_Root_Type_Metadata_Is_Not_A_String()
|
||||
{
|
||||
const string source = """
|
||||
namespace TestApp
|
||||
{
|
||||
public sealed class Dummy
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
const string schema = """
|
||||
{
|
||||
"type": 123,
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = SchemaGeneratorTestDriver.Run(
|
||||
source,
|
||||
("monster.schema.json", schema));
|
||||
|
||||
var diagnostic = result.Results.Single().Diagnostics.Single();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_002"));
|
||||
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("monster.schema.json"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 schema 文件名若生成无效根类型标识符时,会在生成前产生命名明确的诊断。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Run_Should_Report_Diagnostic_When_Schema_File_Name_Generates_Invalid_Root_Type_Identifier()
|
||||
{
|
||||
const string source = """
|
||||
namespace TestApp
|
||||
{
|
||||
public sealed class Dummy
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
const string schema = """
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": { "type": "string" }
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = SchemaGeneratorTestDriver.Run(
|
||||
source,
|
||||
("123-monster.schema.json", schema));
|
||||
|
||||
var diagnostic = result.Results.Single().Diagnostics.Single();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_006"));
|
||||
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("<root>"));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("123-monster"));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("123MonsterConfig"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于模拟 AdditionalFiles 读取阶段直接收到取消请求的测试桩。
|
||||
/// </summary>
|
||||
private sealed class ThrowingAdditionalText : AdditionalText
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建一个在读取时抛出取消异常的 AdditionalText。
|
||||
/// </summary>
|
||||
/// <param name="path">虚拟 schema 路径。</param>
|
||||
public ThrowingAdditionalText(string path)
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Path { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override SourceText GetText(CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new OperationCanceledException(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证空字符串 <c>const</c> 不会在生成 XML 文档时被当成“缺失约束”跳过。
|
||||
/// </summary>
|
||||
@ -1844,6 +1975,49 @@ public class SchemaConfigGeneratorTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证同一对象内不同 schema key 若归一化后映射到同一属性名,会在生成前直接给出冲突诊断。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Run_Should_Report_Diagnostic_When_Schema_Keys_Collide_After_Identifier_Normalization()
|
||||
{
|
||||
const string source = """
|
||||
namespace TestApp
|
||||
{
|
||||
public sealed class Dummy
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
const string schema = """
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"foo-bar": { "type": "string" },
|
||||
"foo_bar": { "type": "string" }
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = SchemaGeneratorTestDriver.Run(
|
||||
source,
|
||||
("monster.schema.json", schema));
|
||||
|
||||
var diagnostic = result.Results.Single().Diagnostics.Single();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_014"));
|
||||
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("foo_bar"));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("FooBar"));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("foo-bar"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 schema 顶层允许通过元数据覆盖默认配置目录,并会统一路径分隔符。
|
||||
/// </summary>
|
||||
@ -2299,7 +2473,7 @@ public class SchemaConfigGeneratorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证引用元数据成员名在不同路径规范化后发生碰撞时,生成器仍会分配全局唯一的成员名。
|
||||
/// 验证引用元数据成员名在不同合法字段路径规范化后发生碰撞时,生成器仍会分配全局唯一的成员名。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names()
|
||||
@ -2360,12 +2534,21 @@ public class SchemaConfigGeneratorTests
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"drop-items": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"x-gframework-ref-table": "item"
|
||||
"drop": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"x-gframework-ref-table": "item"
|
||||
},
|
||||
"items1": {
|
||||
"type": "string",
|
||||
"x-gframework-ref-table": "item"
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop_items": {
|
||||
"dropItems": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"x-gframework-ref-table": "item"
|
||||
@ -2394,6 +2577,7 @@ public class SchemaConfigGeneratorTests
|
||||
Assert.That(generatedSources.TryGetValue("MonsterConfigBindings.g.cs", out var bindingsSource), Is.True);
|
||||
Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems ="));
|
||||
Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems1 ="));
|
||||
Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems2 ="));
|
||||
Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems11 ="));
|
||||
}
|
||||
|
||||
@ -2637,6 +2821,12 @@ public class SchemaConfigGeneratorTests
|
||||
Assert.That(catalogSource,
|
||||
Does.Contain(
|
||||
"public global::System.Collections.Generic.IEqualityComparer<int>? MonsterComparer { get; init; }"));
|
||||
Assert.That(catalogSource,
|
||||
Does.Contain(
|
||||
"using <c>global::System.Collections.Generic.IEqualityComparer<string>?</c> when aggregate registration runs."));
|
||||
Assert.That(catalogSource,
|
||||
Does.Contain(
|
||||
"using <c>global::System.Collections.Generic.IEqualityComparer<int>?</c> when aggregate registration runs."));
|
||||
Assert.That(catalogSource, Does.Contain("return RegisterAllGeneratedConfigTables(loader, options: null);"));
|
||||
Assert.That(catalogSource, Does.Contain("GeneratedConfigRegistrationOptions? options"));
|
||||
Assert.That(catalogSource, Does.Contain("loader.RegisterItemTable(effectiveOptions.ItemComparer);"));
|
||||
|
||||
@ -20,6 +20,23 @@ public static class SchemaGeneratorTestDriver
|
||||
public static GeneratorDriverRunResult Run(
|
||||
string source,
|
||||
params (string path, string content)[] additionalFiles)
|
||||
{
|
||||
return Run(
|
||||
source,
|
||||
additionalFiles
|
||||
.Select(static item => (AdditionalText)new InMemoryAdditionalText(item.path, item.content))
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行 schema 配置生成器,并允许测试自定义 AdditionalText 行为。
|
||||
/// </summary>
|
||||
/// <param name="source">测试用源码。</param>
|
||||
/// <param name="additionalTexts">自定义 AdditionalText 集合。</param>
|
||||
/// <returns>生成器运行结果。</returns>
|
||||
public static GeneratorDriverRunResult Run(
|
||||
string source,
|
||||
params AdditionalText[] additionalTexts)
|
||||
{
|
||||
var syntaxTree = CSharpSyntaxTree.ParseText(source);
|
||||
var compilation = CSharpCompilation.Create(
|
||||
@ -28,13 +45,9 @@ public static class SchemaGeneratorTestDriver
|
||||
GetMetadataReferences(),
|
||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
||||
|
||||
var additionalTexts = additionalFiles
|
||||
.Select(static item => (AdditionalText)new InMemoryAdditionalText(item.path, item.content))
|
||||
.ToImmutableArray();
|
||||
|
||||
GeneratorDriver driver = CSharpGeneratorDriver.Create(
|
||||
generators: new[] { new SchemaConfigGenerator().AsSourceGenerator() },
|
||||
additionalTexts: additionalTexts,
|
||||
additionalTexts: additionalTexts.ToImmutableArray(),
|
||||
parseOptions: (CSharpParseOptions)syntaxTree.Options);
|
||||
|
||||
driver = driver.RunGenerators(compilation);
|
||||
|
||||
@ -236,7 +236,7 @@ public sealed class GeneratedConfigRegistrationOptions
|
||||
public global::System.Predicate<GeneratedConfigCatalog.TableMetadata>? TableFilter { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the optional key comparer forwarded to MonsterConfigBindings.RegisterMonsterTable(global::GFramework.Game.Config.YamlConfigLoader, global::System.Collections.Generic.IEqualityComparer<int>?) when aggregate registration runs.
|
||||
/// Gets or sets the optional key comparer forwarded to MonsterConfigBindings.RegisterMonsterTable using <c>global::System.Collections.Generic.IEqualityComparer<int>?</c> when aggregate registration runs.
|
||||
/// </summary>
|
||||
public global::System.Collections.Generic.IEqualityComparer<int>? MonsterComparer { get; init; }
|
||||
}
|
||||
|
||||
@ -1375,6 +1375,197 @@ public class CqrsHandlerRegistryGeneratorTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 handler 合同里出现未解析错误类型时,生成器会改为运行时精确查找该类型,
|
||||
/// 而不会把无效类型名直接写进生成代码中的 <c>typeof(...)</c>。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Emits_Runtime_Type_Lookup_When_Handler_Contract_Contains_Unresolved_Error_Types()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
public interface IServiceCollection { }
|
||||
|
||||
public static class ServiceCollectionServiceExtensions
|
||||
{
|
||||
public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Logging
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
void Debug(string msg);
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Cqrs.Abstractions.Cqrs
|
||||
{
|
||||
public interface IRequest<TResponse> { }
|
||||
public interface INotification { }
|
||||
public interface IStreamRequest<TResponse> { }
|
||||
|
||||
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
|
||||
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
|
||||
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
|
||||
}
|
||||
|
||||
namespace GFramework.Cqrs
|
||||
{
|
||||
public interface ICqrsHandlerRegistry
|
||||
{
|
||||
void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public sealed class CqrsHandlerRegistryAttribute : Attribute
|
||||
{
|
||||
public CqrsHandlerRegistryAttribute(Type registryType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
public sealed class CqrsReflectionFallbackAttribute : Attribute
|
||||
{
|
||||
public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
public sealed record BrokenRequest() : IRequest<MissingResponse>;
|
||||
|
||||
public sealed class BrokenHandler : IRequestHandler<BrokenRequest, MissingResponse>
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var execution = ExecuteGenerator(source);
|
||||
var inputCompilationErrors = execution.InputCompilationDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
var generatorErrors = execution.GeneratorDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0246"));
|
||||
Assert.That(generatedCompilationErrors, Is.Empty);
|
||||
Assert.That(generatorErrors, Is.Empty);
|
||||
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
|
||||
Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs"));
|
||||
var generatedSource = execution.GeneratedSources[0].content;
|
||||
Assert.That(
|
||||
generatedSource,
|
||||
Does.Contain("registryAssembly.GetType(\"MissingResponse\", throwOnError: false, ignoreCase: false);"));
|
||||
Assert.That(
|
||||
generatedSource,
|
||||
Does.Contain("internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry"));
|
||||
Assert.That(generatedSource, Does.Not.Contain("typeof(MissingResponse)"));
|
||||
Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute("));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 <see langword="dynamic" /> 响应类型会在生成阶段归一化为 <see cref="System.Object" />,
|
||||
/// 避免注册器发射非法的 <c>typeof(dynamic)</c>。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Emits_Object_Type_Reference_When_Handler_Response_Uses_Dynamic()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
public interface IServiceCollection { }
|
||||
|
||||
public static class ServiceCollectionServiceExtensions
|
||||
{
|
||||
public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Logging
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
void Debug(string msg);
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Cqrs.Abstractions.Cqrs
|
||||
{
|
||||
public interface IRequest<TResponse> { }
|
||||
public interface INotification { }
|
||||
public interface IStreamRequest<TResponse> { }
|
||||
|
||||
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
|
||||
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
|
||||
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
|
||||
}
|
||||
|
||||
namespace GFramework.Cqrs
|
||||
{
|
||||
public interface ICqrsHandlerRegistry
|
||||
{
|
||||
void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public sealed class CqrsHandlerRegistryAttribute : Attribute
|
||||
{
|
||||
public CqrsHandlerRegistryAttribute(Type registryType) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
public sealed record DynamicRequest() : IRequest<dynamic>;
|
||||
|
||||
public sealed class DynamicHandler : IRequestHandler<DynamicRequest, dynamic>
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var execution = ExecuteGenerator(source);
|
||||
var inputCompilationErrors = execution.InputCompilationDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
var generatorErrors = execution.GeneratorDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS1966"));
|
||||
Assert.That(generatedCompilationErrors, Is.Empty);
|
||||
Assert.That(generatorErrors, Is.Empty);
|
||||
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
|
||||
Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs"));
|
||||
var generatedSource = execution.GeneratedSources[0].content;
|
||||
Assert.That(generatedSource, Does.Contain("typeof(global::System.Object)"));
|
||||
Assert.That(generatedSource, Does.Not.Contain("typeof(dynamic)"));
|
||||
Assert.That(generatedSource, Does.Contain("internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 fallback metadata 仍然必需且 runtime 提供了承载契约时,
|
||||
/// 生成器会继续产出注册器并发射程序集级 <c>CqrsReflectionFallbackAttribute</c>。
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
using System.IO;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using GFramework.Core.SourceGenerators.Rule;
|
||||
using GFramework.SourceGenerators.Tests.Core;
|
||||
|
||||
@ -11,6 +13,60 @@ namespace GFramework.SourceGenerators.Tests.Rule;
|
||||
[TestFixture]
|
||||
public class ContextAwareGeneratorSnapshotTests
|
||||
{
|
||||
private const string SharedContextAwareInfrastructure = """
|
||||
using System;
|
||||
|
||||
namespace GFramework.Core.SourceGenerators.Abstractions.Rule
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class ContextAwareAttribute : Attribute { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Rule
|
||||
{
|
||||
public interface IContextAware
|
||||
{
|
||||
void SetContext(
|
||||
GFramework.Core.Abstractions.Architectures.IArchitectureContext context);
|
||||
|
||||
GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext();
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Architectures
|
||||
{
|
||||
public interface IArchitectureContext { }
|
||||
|
||||
public interface IArchitectureContextProvider
|
||||
{
|
||||
IArchitectureContext GetContext();
|
||||
bool TryGetContext<T>(out T? context) where T : class, IArchitectureContext;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Architectures
|
||||
{
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
|
||||
public sealed class GameContextProvider : IArchitectureContextProvider
|
||||
{
|
||||
public IArchitectureContext GetContext() => null;
|
||||
public bool TryGetContext<T>(out T? context) where T : class, IArchitectureContext
|
||||
{
|
||||
context = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string GameContextHelperSource = """
|
||||
|
||||
public static class GameContext
|
||||
{
|
||||
public static IArchitectureContext GetFirstArchitectureContext() => null;
|
||||
}
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// 测试ContextAwareGenerator源代码生成器的快照功能
|
||||
/// 验证生成器对带有ContextAware特性的类的处理结果
|
||||
@ -19,76 +75,119 @@ public class ContextAwareGeneratorSnapshotTests
|
||||
[Test]
|
||||
public async Task Snapshot_ContextAwareGenerator()
|
||||
{
|
||||
// 定义测试用的源代码,包含ContextAware特性和相关接口定义
|
||||
const string source = """
|
||||
using System;
|
||||
|
||||
namespace GFramework.Core.SourceGenerators.Abstractions.Rule
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class ContextAwareAttribute : Attribute { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Rule
|
||||
{
|
||||
public interface IContextAware
|
||||
{
|
||||
void SetContext(
|
||||
GFramework.Core.Abstractions.Architectures.IArchitectureContext context);
|
||||
|
||||
GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext();
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Architectures
|
||||
{
|
||||
public interface IArchitectureContext { }
|
||||
|
||||
public interface IArchitectureContextProvider
|
||||
{
|
||||
IArchitectureContext GetContext();
|
||||
bool TryGetContext<T>(out T? context) where T : class, IArchitectureContext;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Architectures
|
||||
{
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
|
||||
public sealed class GameContextProvider : IArchitectureContextProvider
|
||||
{
|
||||
public IArchitectureContext GetContext() => null;
|
||||
public bool TryGetContext<T>(out T? context) where T : class, IArchitectureContext
|
||||
{
|
||||
context = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class GameContext
|
||||
{
|
||||
public static IArchitectureContext GetFirstArchitectureContext() => null;
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
|
||||
[ContextAware]
|
||||
public partial class MyRule : IContextAware
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
// 执行生成器快照测试,将生成的代码与预期快照进行比较
|
||||
await GeneratorSnapshotTest<ContextAwareGenerator>.RunAsync(
|
||||
source,
|
||||
CreateContextAwareTestSource(
|
||||
"""
|
||||
[ContextAware]
|
||||
public partial class MyRule : IContextAware
|
||||
{
|
||||
}
|
||||
""",
|
||||
includeGameContextHelper: true),
|
||||
GetSnapshotFolder());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证生成器在用户 partial 类型已经声明常见上下文字段名时仍能生成可编译代码。
|
||||
/// </summary>
|
||||
/// <returns>异步任务,无返回值。</returns>
|
||||
[Test]
|
||||
public async Task Snapshot_ContextAwareGenerator_With_User_Field_Name_Collisions()
|
||||
{
|
||||
await GeneratorSnapshotTest<ContextAwareGenerator>.RunAsync(
|
||||
CreateContextAwareTestSource(
|
||||
"""
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
|
||||
[ContextAware]
|
||||
public partial class CollisionProneRule : IContextAware
|
||||
{
|
||||
private readonly string _context = "user-field";
|
||||
private static readonly string _contextProvider = "user-provider";
|
||||
private static readonly object _contextSync = new();
|
||||
private IArchitectureContext? _gFrameworkContextAwareContext;
|
||||
private static IArchitectureContextProvider? _gFrameworkContextAwareProvider;
|
||||
private static readonly object _gFrameworkContextAwareSync = new();
|
||||
}
|
||||
"""),
|
||||
GetSnapshotFolder());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证生成器在基类已经占用自动生成字段名时,也会为派生规则类型分配带后缀的唯一成员名。
|
||||
/// </summary>
|
||||
/// <returns>异步任务,无返回值。</returns>
|
||||
[Test]
|
||||
public async Task Snapshot_ContextAwareGenerator_With_Inherited_Field_Name_Collisions()
|
||||
{
|
||||
await GeneratorSnapshotTest<ContextAwareGenerator>.RunAsync(
|
||||
CreateContextAwareTestSource(
|
||||
"""
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
|
||||
public abstract class ContextAwareRuleBase
|
||||
{
|
||||
protected IArchitectureContext? _gFrameworkContextAwareContext;
|
||||
protected static IArchitectureContextProvider? _gFrameworkContextAwareProvider;
|
||||
protected static readonly object _gFrameworkContextAwareSync = new();
|
||||
}
|
||||
|
||||
[ContextAware]
|
||||
public partial class InheritedCollisionRule : ContextAwareRuleBase, IContextAware
|
||||
{
|
||||
}
|
||||
"""),
|
||||
GetSnapshotFolder());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 组装 ContextAwareGenerator 快照测试共用的最小宿主源码,避免每个用例都重复长块样板代码。
|
||||
/// </summary>
|
||||
/// <param name="testTypeDeclarations">放在 <c>TestApp</c> 命名空间内的测试类型声明。</param>
|
||||
/// <param name="includeGameContextHelper">是否额外包含兼容旧快照输入的 <c>GameContext</c> 帮助类型。</param>
|
||||
/// <returns>可直接交给生成器测试驱动的完整源码文本。</returns>
|
||||
private static string CreateContextAwareTestSource(string testTypeDeclarations, bool includeGameContextHelper = false)
|
||||
{
|
||||
var gameContextHelper = includeGameContextHelper ? GameContextHelperSource : string.Empty;
|
||||
var testAppDeclarations = IndentBlock(testTypeDeclarations, 4);
|
||||
|
||||
return string.Concat(
|
||||
SharedContextAwareInfrastructure,
|
||||
gameContextHelper,
|
||||
"""
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
|
||||
""",
|
||||
testAppDeclarations,
|
||||
"""
|
||||
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为内嵌源码片段补齐缩进,使其能安全插入原始字符串模板中的命名空间块。
|
||||
/// </summary>
|
||||
/// <param name="text">要缩进的源码文本。</param>
|
||||
/// <param name="spaces">每行前要补齐的空格数。</param>
|
||||
/// <returns>已经补齐统一缩进的多行文本。</returns>
|
||||
private static string IndentBlock(string text, int spaces)
|
||||
{
|
||||
var indentation = new string(' ', spaces);
|
||||
return string.Join(
|
||||
Environment.NewLine,
|
||||
text.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||
.Trim()
|
||||
.Split('\n')
|
||||
.Select(line => indentation + line));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将运行时测试目录映射回仓库内已提交的上下文感知生成器快照目录。
|
||||
/// </summary>
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
// <auto-generated/>
|
||||
#nullable enable
|
||||
|
||||
namespace TestApp;
|
||||
|
||||
/// <summary>
|
||||
/// 为当前规则类型补充自动生成的架构上下文访问实现。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
|
||||
/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例,
|
||||
/// 已缓存的实例上下文需要通过 <see cref="GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)" /> 显式覆盖。
|
||||
/// 与手动继承 <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 的路径相比,生成实现会使用 <c>_gFrameworkContextAwareSync1</c> 协调惰性初始化、provider 切换和显式上下文注入;
|
||||
/// <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。
|
||||
/// </remarks>
|
||||
partial class CollisionProneRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
||||
{
|
||||
private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _gFrameworkContextAwareContext1;
|
||||
private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _gFrameworkContextAwareProvider1;
|
||||
private static readonly object _gFrameworkContextAwareSync1 = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前实例绑定的架构上下文。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该属性会先返回通过 <c>IContextAware.SetContext(...)</c> 显式注入的实例上下文;若尚未设置,则在同一个同步域内惰性初始化共享提供者。
|
||||
/// 当静态提供者尚未配置时,生成代码会回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
|
||||
/// 一旦某个实例成功缓存上下文,后续 <see cref="SetContextProvider(GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider)" />
|
||||
/// 或 <see cref="ResetContextProvider" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。
|
||||
/// 当前实现还假设 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext" /> 可在持有 <c>_gFrameworkContextAwareSync1</c> 时安全执行;
|
||||
/// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API,且应避免引入与外部全局锁相互等待的锁顺序。
|
||||
/// </remarks>
|
||||
protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context
|
||||
{
|
||||
get
|
||||
{
|
||||
// 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。
|
||||
// provider 的 GetContext() 会在持有 _gFrameworkContextAwareSync1 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。
|
||||
lock (_gFrameworkContextAwareSync1)
|
||||
{
|
||||
_gFrameworkContextAwareProvider1 ??= new global::GFramework.Core.Architectures.GameContextProvider();
|
||||
_gFrameworkContextAwareContext1 ??= _gFrameworkContextAwareProvider1.GetContext();
|
||||
return _gFrameworkContextAwareContext1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置当前生成类型共享的上下文提供者。
|
||||
/// </summary>
|
||||
/// <param name="provider">后续懒加载上下文时要使用的提供者实例。</param>
|
||||
/// <exception cref="global::System.ArgumentNullException">当 <paramref name="provider" /> 为 null 时抛出。</exception>
|
||||
/// <remarks>
|
||||
/// 该方法使用与 <see cref="Context" /> 相同的同步锁,避免提供者切换与惰性初始化交错。
|
||||
/// 已经缓存上下文的实例不会因为提供者切换而自动失效;该变更仅影响尚未初始化上下文的新实例或未缓存实例。
|
||||
/// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。
|
||||
/// </remarks>
|
||||
public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)
|
||||
{
|
||||
global::System.ArgumentNullException.ThrowIfNull(provider);
|
||||
lock (_gFrameworkContextAwareSync1)
|
||||
{
|
||||
_gFrameworkContextAwareProvider1 = provider;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置共享上下文提供者,使后续懒加载回退到默认提供者。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该方法主要用于测试清理或跨用例恢复默认行为。
|
||||
/// 它不会清除已经缓存到实例字段中的上下文;只有后续尚未初始化上下文的实例会重新回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
|
||||
/// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。
|
||||
/// </remarks>
|
||||
public static void ResetContextProvider()
|
||||
{
|
||||
lock (_gFrameworkContextAwareSync1)
|
||||
{
|
||||
_gFrameworkContextAwareProvider1 = null;
|
||||
}
|
||||
}
|
||||
|
||||
void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
|
||||
{
|
||||
// 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。
|
||||
lock (_gFrameworkContextAwareSync1)
|
||||
{
|
||||
_gFrameworkContextAwareContext1 = context;
|
||||
}
|
||||
}
|
||||
|
||||
global::GFramework.Core.Abstractions.Architectures.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext()
|
||||
{
|
||||
return Context;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
// <auto-generated/>
|
||||
#nullable enable
|
||||
|
||||
namespace TestApp;
|
||||
|
||||
/// <summary>
|
||||
/// 为当前规则类型补充自动生成的架构上下文访问实现。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
|
||||
/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例,
|
||||
/// 已缓存的实例上下文需要通过 <see cref="GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)" /> 显式覆盖。
|
||||
/// 与手动继承 <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 的路径相比,生成实现会使用 <c>_gFrameworkContextAwareSync1</c> 协调惰性初始化、provider 切换和显式上下文注入;
|
||||
/// <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。
|
||||
/// </remarks>
|
||||
partial class InheritedCollisionRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
||||
{
|
||||
private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _gFrameworkContextAwareContext1;
|
||||
private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _gFrameworkContextAwareProvider1;
|
||||
private static readonly object _gFrameworkContextAwareSync1 = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前实例绑定的架构上下文。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该属性会先返回通过 <c>IContextAware.SetContext(...)</c> 显式注入的实例上下文;若尚未设置,则在同一个同步域内惰性初始化共享提供者。
|
||||
/// 当静态提供者尚未配置时,生成代码会回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
|
||||
/// 一旦某个实例成功缓存上下文,后续 <see cref="SetContextProvider(GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider)" />
|
||||
/// 或 <see cref="ResetContextProvider" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。
|
||||
/// 当前实现还假设 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext" /> 可在持有 <c>_gFrameworkContextAwareSync1</c> 时安全执行;
|
||||
/// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API,且应避免引入与外部全局锁相互等待的锁顺序。
|
||||
/// </remarks>
|
||||
protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context
|
||||
{
|
||||
get
|
||||
{
|
||||
// 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。
|
||||
// provider 的 GetContext() 会在持有 _gFrameworkContextAwareSync1 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。
|
||||
lock (_gFrameworkContextAwareSync1)
|
||||
{
|
||||
_gFrameworkContextAwareProvider1 ??= new global::GFramework.Core.Architectures.GameContextProvider();
|
||||
_gFrameworkContextAwareContext1 ??= _gFrameworkContextAwareProvider1.GetContext();
|
||||
return _gFrameworkContextAwareContext1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置当前生成类型共享的上下文提供者。
|
||||
/// </summary>
|
||||
/// <param name="provider">后续懒加载上下文时要使用的提供者实例。</param>
|
||||
/// <exception cref="global::System.ArgumentNullException">当 <paramref name="provider" /> 为 null 时抛出。</exception>
|
||||
/// <remarks>
|
||||
/// 该方法使用与 <see cref="Context" /> 相同的同步锁,避免提供者切换与惰性初始化交错。
|
||||
/// 已经缓存上下文的实例不会因为提供者切换而自动失效;该变更仅影响尚未初始化上下文的新实例或未缓存实例。
|
||||
/// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。
|
||||
/// </remarks>
|
||||
public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)
|
||||
{
|
||||
global::System.ArgumentNullException.ThrowIfNull(provider);
|
||||
lock (_gFrameworkContextAwareSync1)
|
||||
{
|
||||
_gFrameworkContextAwareProvider1 = provider;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置共享上下文提供者,使后续懒加载回退到默认提供者。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该方法主要用于测试清理或跨用例恢复默认行为。
|
||||
/// 它不会清除已经缓存到实例字段中的上下文;只有后续尚未初始化上下文的实例会重新回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
|
||||
/// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。
|
||||
/// </remarks>
|
||||
public static void ResetContextProvider()
|
||||
{
|
||||
lock (_gFrameworkContextAwareSync1)
|
||||
{
|
||||
_gFrameworkContextAwareProvider1 = null;
|
||||
}
|
||||
}
|
||||
|
||||
void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
|
||||
{
|
||||
// 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。
|
||||
lock (_gFrameworkContextAwareSync1)
|
||||
{
|
||||
_gFrameworkContextAwareContext1 = context;
|
||||
}
|
||||
}
|
||||
|
||||
global::GFramework.Core.Abstractions.Architectures.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext()
|
||||
{
|
||||
return Context;
|
||||
}
|
||||
|
||||
}
|
||||
@ -10,14 +10,14 @@ namespace TestApp;
|
||||
/// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
|
||||
/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例,
|
||||
/// 已缓存的实例上下文需要通过 <see cref="GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)" /> 显式覆盖。
|
||||
/// 与手动继承 <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 的路径相比,生成实现会使用 <c>_contextSync</c> 协调惰性初始化、provider 切换和显式上下文注入;
|
||||
/// 与手动继承 <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 的路径相比,生成实现会使用 <c>_gFrameworkContextAwareSync</c> 协调惰性初始化、provider 切换和显式上下文注入;
|
||||
/// <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。
|
||||
/// </remarks>
|
||||
partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
||||
{
|
||||
private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _context;
|
||||
private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _contextProvider;
|
||||
private static readonly object _contextSync = new();
|
||||
private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _gFrameworkContextAwareContext;
|
||||
private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _gFrameworkContextAwareProvider;
|
||||
private static readonly object _gFrameworkContextAwareSync = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前实例绑定的架构上下文。
|
||||
@ -27,26 +27,20 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
||||
/// 当静态提供者尚未配置时,生成代码会回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
|
||||
/// 一旦某个实例成功缓存上下文,后续 <see cref="SetContextProvider(GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider)" />
|
||||
/// 或 <see cref="ResetContextProvider" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。
|
||||
/// 当前实现还假设 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext" /> 可在持有 <c>_contextSync</c> 时安全执行;
|
||||
/// 当前实现还假设 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext" /> 可在持有 <c>_gFrameworkContextAwareSync</c> 时安全执行;
|
||||
/// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API,且应避免引入与外部全局锁相互等待的锁顺序。
|
||||
/// </remarks>
|
||||
protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context
|
||||
{
|
||||
get
|
||||
{
|
||||
var context = _context;
|
||||
if (context is not null)
|
||||
{
|
||||
return context;
|
||||
}
|
||||
|
||||
// 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。
|
||||
// provider 的 GetContext() 会在持有 _contextSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。
|
||||
lock (_contextSync)
|
||||
// provider 的 GetContext() 会在持有 _gFrameworkContextAwareSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。
|
||||
lock (_gFrameworkContextAwareSync)
|
||||
{
|
||||
_contextProvider ??= new global::GFramework.Core.Architectures.GameContextProvider();
|
||||
_context ??= _contextProvider.GetContext();
|
||||
return _context;
|
||||
_gFrameworkContextAwareProvider ??= new global::GFramework.Core.Architectures.GameContextProvider();
|
||||
_gFrameworkContextAwareContext ??= _gFrameworkContextAwareProvider.GetContext();
|
||||
return _gFrameworkContextAwareContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -55,6 +49,7 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
||||
/// 配置当前生成类型共享的上下文提供者。
|
||||
/// </summary>
|
||||
/// <param name="provider">后续懒加载上下文时要使用的提供者实例。</param>
|
||||
/// <exception cref="global::System.ArgumentNullException">当 <paramref name="provider" /> 为 null 时抛出。</exception>
|
||||
/// <remarks>
|
||||
/// 该方法使用与 <see cref="Context" /> 相同的同步锁,避免提供者切换与惰性初始化交错。
|
||||
/// 已经缓存上下文的实例不会因为提供者切换而自动失效;该变更仅影响尚未初始化上下文的新实例或未缓存实例。
|
||||
@ -62,9 +57,10 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
||||
/// </remarks>
|
||||
public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)
|
||||
{
|
||||
lock (_contextSync)
|
||||
global::System.ArgumentNullException.ThrowIfNull(provider);
|
||||
lock (_gFrameworkContextAwareSync)
|
||||
{
|
||||
_contextProvider = provider;
|
||||
_gFrameworkContextAwareProvider = provider;
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,18 +74,18 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
||||
/// </remarks>
|
||||
public static void ResetContextProvider()
|
||||
{
|
||||
lock (_contextSync)
|
||||
lock (_gFrameworkContextAwareSync)
|
||||
{
|
||||
_contextProvider = null;
|
||||
_gFrameworkContextAwareProvider = null;
|
||||
}
|
||||
}
|
||||
|
||||
void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
|
||||
{
|
||||
// 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。
|
||||
lock (_contextSync)
|
||||
lock (_gFrameworkContextAwareSync)
|
||||
{
|
||||
_context = context;
|
||||
_gFrameworkContextAwareContext = context;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
14
README.md
14
README.md
@ -27,11 +27,22 @@
|
||||
| `GFramework.Game.Abstractions` | `Game` 对应的契约层 | [README](GFramework.Game.Abstractions/README.md) |
|
||||
| `GFramework.Godot` | Godot 集成层,负责把框架能力接入节点、场景、UI、设置与存储 | [README](GFramework.Godot/README.md) |
|
||||
| `GFramework.Ecs.Arch` | Arch ECS 集成 | [README](GFramework.Ecs.Arch/README.md) |
|
||||
| `GFramework.Ecs.Arch.Abstractions` | Arch ECS 集成对应的契约层,适合共享宿主循环与 ECS 模块边界 | [README](GFramework.Ecs.Arch.Abstractions/README.md) |
|
||||
| `GFramework.Core.SourceGenerators` | Core 侧通用源码生成器与分析器 | [README](GFramework.Core.SourceGenerators/README.md) |
|
||||
| `GFramework.Game.SourceGenerators` | 游戏内容配置 schema 生成器 | [README](GFramework.Game.SourceGenerators/README.md) |
|
||||
| `GFramework.Cqrs.SourceGenerators` | CQRS handler registry 生成器 | [README](GFramework.Cqrs.SourceGenerators/README.md) |
|
||||
| `GFramework.Godot.SourceGenerators` | Godot 场景专用源码生成器 | [README](GFramework.Godot.SourceGenerators/README.md) |
|
||||
|
||||
## 内部支撑模块
|
||||
|
||||
以下目录目前不是独立采用入口,而是跟随所属模块维护的内部支撑组件:
|
||||
|
||||
| 目录 | 定位 | 跟随入口 |
|
||||
| --- | --- | --- |
|
||||
| `GFramework.Core.SourceGenerators.Abstractions` | `Core.SourceGenerators` 的内部契约层 | [GFramework.Core.SourceGenerators/README.md](GFramework.Core.SourceGenerators/README.md) |
|
||||
| `GFramework.Godot.SourceGenerators.Abstractions` | `Godot.SourceGenerators` 的内部契约层 | [GFramework.Godot.SourceGenerators/README.md](GFramework.Godot.SourceGenerators/README.md) |
|
||||
| `GFramework.SourceGenerators.Common` | 生成器家族共享的公共支撑代码 | [docs/zh-CN/source-generators/index.md](docs/zh-CN/source-generators/index.md) |
|
||||
|
||||
## 文档导航
|
||||
|
||||
仓库根 README 与文档站点保持同一套栏目命名:
|
||||
@ -119,10 +130,13 @@ GFramework.sln
|
||||
├─ GFramework.Game.Abstractions/
|
||||
├─ GFramework.Godot/
|
||||
├─ GFramework.Ecs.Arch/
|
||||
├─ GFramework.Ecs.Arch.Abstractions/
|
||||
├─ GFramework.Core.SourceGenerators/
|
||||
├─ GFramework.Core.SourceGenerators.Abstractions/
|
||||
├─ GFramework.Game.SourceGenerators/
|
||||
├─ GFramework.Cqrs.SourceGenerators/
|
||||
├─ GFramework.Godot.SourceGenerators/
|
||||
├─ GFramework.Godot.SourceGenerators.Abstractions/
|
||||
├─ GFramework.SourceGenerators.Common/
|
||||
└─ docs/
|
||||
```
|
||||
|
||||
@ -25,6 +25,11 @@ help the current worktree land on the right recovery documents without scanning
|
||||
- Purpose: continue the AI-First config runtime, generator, and consumer DX work for `GFramework.Game`.
|
||||
- Tracking: `ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md`
|
||||
- Trace: `ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md`
|
||||
- `documentation-full-coverage-governance`
|
||||
- Purpose: govern full-coverage documentation inventory, module-wave remediation, and the README / docs / XML /
|
||||
API-reference alignment baseline.
|
||||
- Tracking: `ai-plan/public/documentation-full-coverage-governance/todos/documentation-full-coverage-governance-tracking.md`
|
||||
- Trace: `ai-plan/public/documentation-full-coverage-governance/traces/documentation-full-coverage-governance-trace.md`
|
||||
- `coroutine-optimization`
|
||||
- Purpose: continue the coroutine semantics, host integration, observability, regression coverage, and migration-doc
|
||||
follow-up work.
|
||||
@ -38,10 +43,6 @@ help the current worktree land on the right recovery documents without scanning
|
||||
- Purpose: continue the data repository persistence hardening plus the settings / serialization follow-up backlog.
|
||||
- Tracking: `ai-plan/public/data-repository-persistence/todos/data-repository-persistence-tracking.md`
|
||||
- Trace: `ai-plan/public/data-repository-persistence/traces/data-repository-persistence-trace.md`
|
||||
- `documentation-governance-and-refresh`
|
||||
- Purpose: continue the documentation governance, README hardening, and `docs/zh-CN` accuracy refresh work.
|
||||
- Tracking: `ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md`
|
||||
- Trace: `ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md`
|
||||
|
||||
## Worktree To Active Topic Map
|
||||
|
||||
@ -64,10 +65,12 @@ help the current worktree land on the right recovery documents without scanning
|
||||
- Priority 1: `data-repository-persistence`
|
||||
- Branch: `docs/sdk-update-documentation`
|
||||
- Worktree hint: `GFramework-update-documentation`
|
||||
- Priority 1: `documentation-governance-and-refresh`
|
||||
|
||||
- Priority 1: `documentation-full-coverage-governance`
|
||||
## Archived Topics
|
||||
|
||||
- `cqrs-cache-docs-hardening`
|
||||
- Archive root: `ai-plan/public/archive/cqrs-cache-docs-hardening/`
|
||||
- Note: archived topics stay outside the default `boot` context until a user explicitly requests historical review.
|
||||
- `documentation-governance-and-refresh`
|
||||
- Archive root: `ai-plan/public/archive/documentation-governance-and-refresh/`
|
||||
- Note: PR #268 已合并;文档治理与 Godot 栏目刷新阶段已完成,后续仅作为历史恢复材料保留。
|
||||
|
||||
@ -7,17 +7,51 @@
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-015`
|
||||
- 当前阶段:`Phase 15`
|
||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-028`
|
||||
- 当前阶段:`Phase 28`
|
||||
- 当前焦点:
|
||||
- 当前分支 PR #267 的失败测试已通过 `$gframework-pr-review` 与本地整包测试完成复核
|
||||
- 已确认并修复 `AsyncLogAppender.Flush()` 在“后台线程先清空队列”场景下可能超时返回 `false` 的竞态
|
||||
- 已补上稳定回归测试,避免只在整包 `GFramework.Core.Tests` 里偶发暴露的刷新完成信号问题再次回归
|
||||
- 下一轮默认恢复到 `MA0016` 或 `MA0002` 低风险批次;`MA0015` 与 `MA0077` 继续作为尾项顺手吸收
|
||||
- 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次
|
||||
- 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock
|
||||
建议点,属于跨 target 兼容性风险,不在本轮直接批量替换
|
||||
- 已完成 `GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs` 的剩余 `MA0051` 结构拆分,生成输出保持不变
|
||||
- 已完成 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 的 `MA0051` 结构拆分,生成输出保持不变
|
||||
- 已完成 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的 `MA0006` 低风险收口,schema 关键字比较显式使用
|
||||
`StringComparison.Ordinal`
|
||||
- 已完成 `SchemaConfigGenerator.cs` 的第一批 `MA0051` 结构拆分:schema 入口解析、属性解析、schema 遍历、数组属性解析、
|
||||
约束文档生成与若干生成代码发射 helper 已拆出语义阶段
|
||||
- 已完成当前 PR #269 review follow-up:`CqrsHandlerRegistryGenerator` 按职责拆分为 partial 生成器文件,
|
||||
`ContextAwareGenerator` 已补上字段名去冲突与锁内读取修正,`Option<T>` 补齐 `<remarks>` 契约说明
|
||||
- 已完成当前 PR #269 第二轮 follow-up:恢复 `EasyEvents`、`CollectionExtensions`、`LoggingConfiguration` 与
|
||||
`FilterConfiguration` 的公共 API 兼容形状,并将 analyzer 兼容性处理收敛到局部 pragma
|
||||
- 已完成当前 PR #269 第三轮 follow-up:继续收口 `SchemaConfigGenerator` 的根类型标识符校验与 XML 文档转义,
|
||||
并补齐 `LoggingConfigurationTests`、`CollectionExtensionsTests`、`Cqrs` helper 抽取与 `ai-plan` 命令文本修正
|
||||
- 已完成当前 PR #269 第四轮 follow-up:将 `CqrsHandlerRegistryGenerator` 的 Roslyn error type 直接引用改为
|
||||
运行时精确查找路径,并为 `SchemaConfigGenerator` 补上根 `type` 非字符串时的防御与回归测试
|
||||
- 已完成当前 PR #269 第五轮 follow-up:`SchemaConfigGenerator` 补上归一化后属性名冲突诊断并新增
|
||||
`GF_ConfigSchema_014`,`CqrsHandlerRegistryGenerator` 将 `dynamic` 归一化为 `global::System.Object`,
|
||||
同时收紧相关 generator regression tests
|
||||
- 已完成当前 PR #269 failed-test follow-up:修正 `SchemaConfigGeneratorTests`
|
||||
`Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names` 的测试输入,使其继续覆盖
|
||||
reference metadata 成员名全局去冲突,但不再依赖现已被 `GF_ConfigSchema_014` 拦截的非法同层 schema key 冲突
|
||||
- 已完成当前 PR #269 Greptile follow-up:`ContextAwareGenerator` 现在会把基类链显式成员名也纳入
|
||||
`_gFrameworkContextAware*` 字段分配冲突检测,并新增 inherited-field collision 快照回归测试
|
||||
- 已完成当前分支与 `main` 的 `CqrsHandlerRegistryGenerator.cs` 文件级冲突收口:确认 `main` 侧新增的是
|
||||
`OrderedRegistrationKind` / `RuntimeTypeReferenceSpec` 的 XML 文档,现已按当前 partial 拆分结构迁移到
|
||||
`CqrsHandlerRegistryGenerator.Models.cs`,不回退已完成的生成器拆分
|
||||
- 已更新 `AGENTS.md`:变更模块必须运行对应 `dotnet build -c Release`,并处理或显式报告模块构建 warning,
|
||||
不再默认留给长期 warning 清理分支
|
||||
- `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义
|
||||
- `EasyEvents.AddEvent<T>()` 的重复注册路径已恢复为 `ArgumentException`,以保持既有异常契约
|
||||
- `Option<T>` 已声明 `IEquatable<Option<T>>`,与已有强类型 `Equals(Option<T>)` 契约对齐
|
||||
- 当前 `GFramework.Core` `net8.0` warnings-only 基线已降到 `0` 条
|
||||
- 当前 `GFramework.Core.SourceGenerators` warnings-only 基线已降到 `0` 条
|
||||
- 当前 `GFramework.Cqrs.SourceGenerators` warnings-only 基线已降到 `0` 条
|
||||
- 当前 `GFramework.Game.SourceGenerators` warnings-only 基线已从 `46` 条降到 `9` 条,剩余均为
|
||||
`SchemaConfigGenerator.cs` 的 `MA0051`
|
||||
- `GFramework.Godot` 的 `Timing.cs` 已同步适配新事件签名,但当前 worktree 的 Godot restore 资产仍受 Windows fallback package folder 干扰,独立 build 需在修复资产后补跑
|
||||
- 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进
|
||||
- 当某一轮主类型数量不足时,允许顺手合并其他低冲突 warning 类型,`MA0015` 与 `MA0077`
|
||||
只是当前最明显的低数量示例,不构成限定
|
||||
- 下一轮默认继续拆分 `GFramework.Game.SourceGenerators` 的 `MA0051` 热点,或评估跨 target 的 `MA0158`
|
||||
锁替换风险
|
||||
- 单次 `boot` 的工作树改动上限控制在约 `100` 个文件以内,避免 recovery context 与 review 面同时失控
|
||||
- 若任务边界互不冲突,允许使用不同模型的 subagent 并行处理不同 warning 类型或不同目录,但必须遵守显式 ownership
|
||||
|
||||
@ -34,8 +68,24 @@
|
||||
`PhaseChanged` / `CoroutineExceptionEventArgs` XML 文档、`PhaseChanged` 迁移说明和 `ai-plan` 基线注释
|
||||
- 已完成当前 PR #267 failed-test follow-up:修复 `AsyncLogAppender.Flush()` 在队列已被后台线程提前清空时仍可能
|
||||
等待满默认超时并返回 `false` 的竞态,并通过整包 `GFramework.Core.Tests` 重新验证
|
||||
- 当前 `GFramework.Core` `net8.0` warnings-only 基线已降到 `9` 条;剩余 warning 集中在
|
||||
`MA0016` 集合抽象接口、`MA0002` comparer 重载,以及 `MA0015` / `MA0077` 两个低数量尾项
|
||||
- 已完成当前 `GFramework.Core` `net8.0` 剩余低风险 analyzer warning 批次;warnings-only 基线已降到 `0` 条
|
||||
- 已完成 `GFramework.Core.SourceGenerators` 中 `ContextAwareGenerator` 的剩余 `MA0051` 收口;warnings-only 基线已降到 `0` 条
|
||||
- 已完成 `GFramework.Cqrs.SourceGenerators` 中 `CqrsHandlerRegistryGenerator` 的剩余 `MA0051` 收口;warnings-only 基线已降到 `0` 条
|
||||
- 已完成当前 PR #269 的 review follow-up:收口 `ContextAwareGenerator` 的字段命名冲突 / 锁内读取契约、
|
||||
`CqrsHandlerRegistryGenerator` 的运行时类型 null 防御与超大文件拆分、`SchemaConfigGenerator` 的取消语义,
|
||||
并恢复 `EasyEvents` / `CollectionExtensions` / logging 配置模型的公共 API 兼容形状
|
||||
- 已完成当前 PR #269 的第四轮 review follow-up:确认 5 个 latest-head 未解决线程中仅剩 2 个本地仍成立,
|
||||
已分别在 `CqrsHandlerRegistryGenerator` 与 `SchemaConfigGenerator` 中收口,并补齐定向 generator regression tests
|
||||
- 已完成当前 PR #269 的第五轮 review follow-up:收口 `SchemaConfigGenerator` 的归一化字段名冲突诊断、
|
||||
`CqrsHandlerRegistryGenerator` 的 `dynamic` 类型引用风险,并同步更新 `AGENTS.md` 的模块 build / warning 治理规范
|
||||
- 已完成当前 PR #269 的 failed-test follow-up:将 reference metadata 成员名唯一性回归测试改为合法 schema 路径组合,
|
||||
并重新通过定向 generator test
|
||||
- 已完成当前 PR #269 的 Greptile follow-up:修复 `ContextAwareGenerator` 未覆盖基类成员名冲突的问题,并补齐
|
||||
inherited-collision 快照测试
|
||||
- 已完成当前分支与 `main` 的 `CqrsHandlerRegistryGenerator.cs` 冲突化解:保留当前 partial 结构,并把
|
||||
`main` 侧新增的模型文档合并到 `CqrsHandlerRegistryGenerator.Models.cs`
|
||||
- 已完成 `GFramework.Game.SourceGenerators` 中 `SchemaConfigGenerator` 的第一批 `MA0051` 收口;warnings-only 基线剩余 `9` 条
|
||||
`MA0051`
|
||||
|
||||
## 当前活跃事实
|
||||
|
||||
@ -66,16 +116,53 @@
|
||||
nitpick comment 后,确认 8 条高信号项中仍成立的是 1 个行为 bug 与 7 个文档/测试/跟踪缺口,并按最小改动收口
|
||||
- `RP-015` 使用 `$gframework-pr-review` 复核 PR #267 的 CTRF 失败测试评论后,确认 `AsyncLogAppender` 仍存在
|
||||
“队列已空但 Flush 仍超时失败”的竞态;该问题在本地整包 `GFramework.Core.Tests` 中可复现,现已修复并补上稳定回归测试
|
||||
- `RP-016` 将 `GFramework.Core` 当前剩余 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险批次清零,并用
|
||||
warnings-only build 与 focused tests 验证配置反序列化、集合扩展、事件重复注册、`Option<T>` 相等性和协程 tag/group 语义
|
||||
- `RP-017` 复核 `MA0158` 当前仍是跨 target 锁类型迁移问题,因此先收口单点 `ContextAwareGenerator` `MA0051`,
|
||||
并通过 source generator 项目 build 与 `ContextAwareGeneratorSnapshotTests` 验证生成输出未回归
|
||||
- `RP-018` 暂缓跨 target `MA0158`,转入 `GFramework.Cqrs.SourceGenerators` 的单文件结构性 warning;
|
||||
通过拆分 handler 分析、运行时类型引用构造、注册器源码发射与精确反射注册输出阶段,清空该项目当前 `MA0051`
|
||||
- `RP-019` 转入 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs`,先完成低风险 `MA0006` 批次;
|
||||
通过 schema 类型比较 helper 与显式 `StringComparison.Ordinal` 清空当前项目的 `MA0006`
|
||||
- `RP-020` 继续拆分 `SchemaConfigGenerator.cs` 的 `MA0051` 热点,将当前项目 warnings-only 基线从 `19` 条降到 `9` 条,
|
||||
并用 focused schema generator tests 验证 50 个用例通过
|
||||
- `RP-021` 使用 `$gframework-pr-review` 复核当前分支 PR #269 后,修复仍在本地成立的 4 个项:将
|
||||
`CqrsHandlerRegistryGenerator` 拆分为职责清晰的 partial 文件、为 `ContextAwareGenerator` 生成字段增加稳定前缀并补上
|
||||
`SetContextProvider` 的运行时 null 校验、为 `Option<T>` 补齐 `<remarks>`,并新增字段重名场景的生成器快照测试
|
||||
- `RP-022` 继续复核 PR #269 的 latest-head review threads 与 nitpick,确认仍成立的项包括公共 API 兼容回退、
|
||||
`ContextAwareGenerator` 字段名真正去冲突与锁内读取、`SchemaConfigGenerator` 取消传播、`Cqrs` 运行时类型 null 防御;
|
||||
已补齐对应回归测试与 focused build/test 验证
|
||||
- `RP-023` 继续复核 PR #269 剩余 nitpick/outside-diff 项,确认仍成立的项集中在 `SchemaConfigGenerator` 根类型名校验、
|
||||
aggregate registration comparer XML 文档转义、logging / collection 反射测试补强,以及跟踪文档中的
|
||||
`RestoreFallbackFolders=""` 可复制性问题
|
||||
- `RP-024` 使用 `$gframework-pr-review` 继续复核 PR #269 latest-head unresolved threads,确认 `EasyEvents` 异常契约、
|
||||
`SchemaConfigGenerator` 取消传播与 `ContextAwareGenerator` 快照冲突线程均已在本地收口,仅剩 `Cqrs` error type
|
||||
直接引用与根 schema `type` 非字符串防御仍成立;现已补齐实现与回归测试
|
||||
- `RP-025` 继续复核 PR #269 剩余 outside-diff / nitpick 信号后,确认本地仍成立的是 `SchemaConfigGenerator`
|
||||
的归一化字段名冲突与 `Cqrs` 对 `dynamic` 的直接类型引用;已分别补上诊断、运行时类型归一化与回归测试,
|
||||
并把“变更模块必须运行对应 build 且处理 warning”的治理规则写回 `AGENTS.md`
|
||||
- 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射
|
||||
|
||||
## 当前风险
|
||||
|
||||
- 公共契约兼容风险:剩余 `MA0016` 若直接改公开集合类型,可能波及用户代码
|
||||
- 缓解措施:优先选择不改公共 API 的低风险切法;若必须触达公共契约,先补齐 XML 契约说明与定向测试
|
||||
- analyzer 收口回退风险:后续若继续压 `MA0015` / `MA0016`,容易再次把公共 API 收窄成与既有契约不兼容的形状
|
||||
- 缓解措施:优先保留既有公共 API,并将兼容性例外收敛到局部 pragma;继续用反射断言覆盖返回类型、属性类型与异常类型
|
||||
- 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定
|
||||
- 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证
|
||||
- 多目标框架 warning 解释风险:同一源位置会在多个 target framework 下重复计数
|
||||
- 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数
|
||||
- net10 专属 warning 风险:`MA0158` 建议使用 `System.Threading.Lock`,但项目多 target 时需要确认兼容边界
|
||||
- 缓解措施:下一轮先按 target framework 与 API 可用性评估,不直接批量替换共享源码中的 `object` lock
|
||||
- source generator warning 外溢风险:运行 `GFramework.SourceGenerators.Tests` 会构建相邻 generator/test 项目并显示既有
|
||||
`GFramework.Game.SourceGenerators` 与测试项目 warning
|
||||
- 缓解措施:继续以被修改 generator 项目的独立 warnings-only build 作为主验收,并用 focused generator test 验证行为
|
||||
- source generator test warning 治理风险:`GFramework.SourceGenerators.Tests` 当前仍有既有 `MA0051` / `MA0004` / `MA0048`
|
||||
warning,本轮 focused test 已通过,但测试项目整包 warning 尚未进入本轮写集
|
||||
- 缓解措施:本轮已在 failed-test follow-up 的定向 `dotnet test` 中再次确认这些 warning 仍为既有基线;后续若继续修改该测试项目,
|
||||
应按新增 `AGENTS.md` 规则先明确 warning 收口范围,再决定是否进入专门清理切片
|
||||
- ContextAware 基类命名隐藏风险:若生成器只看当前类型声明成员,派生规则会重新占用基类已声明的
|
||||
`_gFrameworkContextAware*` 字段名,导致生成成员隐藏继承状态并让快照无法锁定后缀分配行为
|
||||
- 缓解措施:本轮已改为遍历完整 base-type 链收集保留名,并用 inherited collision 快照用例锁定该行为
|
||||
- Godot 资产文件环境风险:当前 worktree 的 `GFramework.Godot` restore/build 仍会命中 Windows fallback package folder
|
||||
- 缓解措施:后续若继续触达 Godot 模块,先用 Linux 侧 restore 资产或 Windows-hosted 构建链刷新该项目,再补跑定向 build
|
||||
- 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构
|
||||
@ -158,12 +245,80 @@
|
||||
- 结果:`15 Passed`,`0 Failed`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --disable-build-servers`
|
||||
- 结果:`1607 Passed`,`0 Failed`
|
||||
- `RP-016` 的验证结果:
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`;当前 `GFramework.Core` `net8.0` analyzer baseline 已清零
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~EasyEventsTests|FullyQualifiedName~OptionTests|FullyQualifiedName~CoroutineGroupTests|FullyQualifiedName~CoroutineSchedulerTests" -m:1 -nologo`
|
||||
- 结果:`112 Passed`,`0 Failed`;测试构建仍会显示既有 `net10.0` `MA0158` 与 source generator `MA0051` warning
|
||||
- `RP-017` 的验证结果:
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net10.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`16 Warning(s)`,`0 Error(s)`;当前 `MA0158` 跨 `GFramework.Core` / `GFramework.Cqrs`,本轮只记录基线不批量改锁
|
||||
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`;`ContextAwareGenerator.cs` 已不再出现 `MA0051`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`1 Passed`,`0 Failed`;测试构建仍显示相邻 source generator 和测试项目的既有 analyzer warning
|
||||
- `RP-018` 的验证结果:
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`;`CqrsHandlerRegistryGenerator.cs` 当前 `MA0051` 已清零
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~CqrsHandlerRegistryGeneratorTests -m:1 -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:`14 Passed`,`0 Failed`
|
||||
- 说明:该 test project 构建仍显示 `GFramework.Game.SourceGenerators` 与测试项目中的既有 analyzer warning;本轮关注的
|
||||
`GFramework.Cqrs.SourceGenerators` 独立 build 已清零
|
||||
- `RP-019` 的验证结果:
|
||||
- `dotnet restore GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:通过;刷新 Linux 侧资产以清除 stale Windows fallback package folder
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`19 Warning(s)`,`0 Error(s)`;当前项目输出已不再出现 `MA0006`,剩余均为 `SchemaConfigGenerator.cs` 的
|
||||
`MA0051`
|
||||
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:通过;刷新 test project 资产以清除 stale Windows fallback package folder
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:`50 Passed`,`0 Failed`
|
||||
- 说明:测试项目构建仍显示既有 source generator test analyzer warning;不属于本轮写集
|
||||
- `RP-020` 的验证结果:
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`9 Warning(s)`,`0 Error(s)`;当前项目剩余 warning 均为 `SchemaConfigGenerator.cs` 的 `MA0051`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:`50 Passed`,`0 Failed`
|
||||
- 说明:测试项目构建仍显示既有 source generator test analyzer warning;不属于本轮写集
|
||||
- `RP-021` 的验证结果:
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`;拆分后最大单文件已降到 `851` 行,满足仓库 800-1000 行上限
|
||||
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`;`ContextAwareGenerator` 的字段命名与 provider 契约修复未引入新的 generator warning
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:先并行运行两条 `dotnet test` 时触发共享输出文件锁冲突;改为串行重跑后 `ContextAwareGeneratorSnapshotTests=2 Passed`、
|
||||
`CqrsHandlerRegistryGeneratorTests=14 Passed`
|
||||
- 说明:失败来自测试宿主并行写入同一 build 输出,不是代码回归;串行重跑后快照新增的字段重名场景和 CQRS 快照均通过
|
||||
- `RP-022` 的验证结果:
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过;`EasyEvents`、`CollectionExtensions` 与 logging 配置模型的兼容性回退未引入新的 `net8.0` 构建错误
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过;`ContainingAssembly` null 防御与发射 helper 精简未引入新的构建错误
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过;仍保留既有 `9` 条 `SchemaConfigGenerator.cs` `MA0051` warning,非本轮新增
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`63 Passed`,`0 Failed`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~EasyEventsTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`38 Passed`,`0 Failed`
|
||||
- `RP-023` 的验证结果:
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过;仍保留既有 `9` 条 `SchemaConfigGenerator.cs` `MA0051` warning,未新增新的 generator warning
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过;并行构建时 `GFramework.SourceGenerators.Common.dll` 复制阶段出现一次 `MSB3026` 重试,随后成功完成
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~SchemaConfigGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`63 Passed`,`0 Failed`
|
||||
- 说明:测试项目构建仍会显示既有 `GFramework.SourceGenerators.Tests` analyzer warning;不属于本轮写集
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`27 Passed`,`0 Failed`
|
||||
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 若要继续该主题,先读 active tracking,再按需展开历史归档中的 warning 热点与验证记录
|
||||
2. 下一轮优先在 `MA0016` 与 `MA0002` 之间选择低风险批次继续推进,默认先看 `LoggingConfiguration` /
|
||||
`FilterConfiguration` 与 `CollectionExtensions`
|
||||
3. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build
|
||||
4. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`
|
||||
2. 下一轮优先继续拆分 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的剩余 `MA0051`;建议先从
|
||||
`GenerateBindingsClass`、`AppendGeneratedConfigCatalogType` 或对象/条件 schema target 验证方法切入
|
||||
3. 若改回推进 `MA0158`,先设计 `net8.0` / `net9.0` / `net10.0` 多 target 条件编译方案,不直接批量替换共享源码中的
|
||||
`object` lock
|
||||
4. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build
|
||||
5. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`
|
||||
|
||||
@ -1,5 +1,403 @@
|
||||
# Analyzer Warning Reduction 追踪
|
||||
|
||||
## 2026-04-23 — RP-028
|
||||
|
||||
### 阶段:`CqrsHandlerRegistryGenerator.cs` 文件级冲突化解(RP-028)
|
||||
|
||||
- 启动复核:
|
||||
- 用户指出当前分支与 `main` 在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs`
|
||||
存在冲突,需要人工确认并解决
|
||||
- 本地检查后确认工作树没有 `UU` 或冲突标记;进一步对比 `origin/main` 发现冲突根因不是运行逻辑回退,而是
|
||||
`main` 在旧的单文件版本里新增了 `OrderedRegistrationKind` / `RuntimeTypeReferenceSpec` 的 XML 文档,
|
||||
而当前分支已将这些类型拆分到 `CqrsHandlerRegistryGenerator.Models.cs`
|
||||
- 决策:
|
||||
- 保留当前分支已经完成的 partial 拆分,不把模型重新塞回 `CqrsHandlerRegistryGenerator.cs`
|
||||
- 以“迁移 `main` 侧文档意图到拆分后的归属文件”为人工合并策略,避免既回退结构拆分又遗漏 `main` 新增文档
|
||||
- 实施调整:
|
||||
- 将 `OrderedRegistrationKind` 的枚举说明与 `RuntimeTypeReferenceSpec` / `FromDirectReference` /
|
||||
`FromReflectionLookup` / `FromExternalReflectionLookup` / `FromArray` / `FromConstructedGeneric`
|
||||
的 XML 文档迁移到 `CqrsHandlerRegistryGenerator.Models.cs`
|
||||
- 保持 `CqrsHandlerRegistryGenerator.cs` 主文件只承载主生成管线,不引入重复模型定义
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -clp:"Summary;WarningsOnly" -nologo`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`
|
||||
- 下一步建议:
|
||||
- 若后续继续处理分支冲突,优先先判断 `main` 改动是否已在当前 partial 文件集里存在等价归属,再决定是否需要真正 merge/rebase
|
||||
- 若回到 PR #269 收口,可继续抓取最新 unresolved threads 与 CI 状态
|
||||
|
||||
## 2026-04-23 — RP-027
|
||||
|
||||
### 阶段:PR #269 Greptile inherited-member collision follow-up(RP-027)
|
||||
|
||||
- 启动复核:
|
||||
- 根据用户补充,重新核对 `$gframework-pr-review` 抓下来的 `greptile-apps[bot]` unresolved 线程,确认仍有一条
|
||||
`ContextAwareGenerator` 关于 inherited member names 未参与 collision detection 的 P1 评论
|
||||
- 本地读取 `CreateGeneratedContextMemberNames(...)` 后确认当前实现只收集 `symbol.GetMembers()`,确实没有遍历基类链
|
||||
- 决策:
|
||||
- 保持现有 `_gFrameworkContextAware*` 前缀和数字后缀分配规则不变,只把保留名集合扩展为“当前类型 + 基类链显式成员”
|
||||
- 沿用既有 `ContextAwareGeneratorSnapshotTests` 模式,新增 inherited-field collision 快照,而不是只写松散字符串断言
|
||||
- 实施调整:
|
||||
- 为 `ContextAwareGenerator` 新增 `CollectReservedContextMemberNames(...)` helper,遍历完整 `BaseType` 链收集显式成员名
|
||||
- 为 `ContextAwareGeneratorSnapshotTests` 增加 `InheritedCollisionRule` 场景,并抽出公共测试源码 helper,避免重复样板
|
||||
- 新增快照 `InheritedCollisionRule.ContextAware.g.cs`,锁定基类已声明 `_gFrameworkContextAware*` 时生成器会回退到 `...1` 后缀
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -clp:"Summary;WarningsOnly" -nologo`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests.Snapshot_ContextAwareGenerator_With_Inherited_Field_Name_Collisions|FullyQualifiedName~ContextAwareGeneratorSnapshotTests.Snapshot_ContextAwareGenerator_With_User_Field_Name_Collisions|FullyQualifiedName~ContextAwareGeneratorSnapshotTests.Snapshot_ContextAwareGenerator" -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`3 Passed`,`0 Failed`
|
||||
- 说明:`GFramework.SourceGenerators.Tests` 仍打印既有 `MA0048`、`MA0051`、`MA0004` warning;本轮未扩大到测试项目 warning 清理
|
||||
- 下一步建议:
|
||||
- 若继续收口 PR #269,可再次抓取最新 unresolved threads,确认 Greptile / CodeRabbit 当前是否只剩陈旧信号
|
||||
- 若继续推进 analyzer 主线,可单独评估 `GFramework.SourceGenerators.Tests` 的 warning 清理是否值得开新切片
|
||||
|
||||
## 2026-04-23 — RP-026
|
||||
|
||||
### 阶段:PR #269 failed-test follow-up(RP-026)
|
||||
|
||||
- 启动复核:
|
||||
- 使用 `$gframework-pr-review` 抓取当前分支 PR #269 的 test report,确认最新失败信号来自
|
||||
`SchemaConfigGeneratorTests.Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names`
|
||||
- 本地复测前先对 `GFramework.SourceGenerators.Tests` 执行 `dotnet restore -p:RestoreFallbackFolders=""`,
|
||||
规避当前 WSL worktree 仍残留的 Windows NuGet fallback package folder 资产干扰
|
||||
- 决策:
|
||||
- 保持 `SchemaConfigGenerator` 当前 `GF_ConfigSchema_014` 语义不变;PR 失败是测试输入陈旧,而不是生成器行为回退
|
||||
- 将用例改写为“合法 schema 路径在 reference metadata member name 上碰撞”的场景,继续覆盖全局唯一后缀分配逻辑
|
||||
- 实施调整:
|
||||
- 将测试 schema 从根级 `drop-items` / `drop_items` 非法同层冲突改为 `drop.items`、`drop.items1`、`dropItems`、
|
||||
`dropItems1` 的合法组合
|
||||
- 更新断言,验证 `MonsterConfigBindings.g.cs` 中继续生成 `DropItems`、`DropItems1`、`DropItems2` 与 `DropItems11`
|
||||
- 验证结果:
|
||||
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:通过
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`1 Passed`,`0 Failed`
|
||||
- 说明:`GFramework.SourceGenerators.Tests` 在构建阶段仍会打印既有 `MA0048`、`MA0051`、`MA0004` warning;本轮未扩展到该测试项目的 warning 清理
|
||||
- 下一步建议:
|
||||
- 若继续收口 PR #269,可再次抓取最新 test report / open thread,确认是否还有新的 CI 失败信号
|
||||
- 若回到 analyzer 主线,优先决定是否为 `GFramework.SourceGenerators.Tests` 单独开一轮 warning 清理切片
|
||||
|
||||
## 2026-04-23 — RP-025
|
||||
|
||||
### 阶段:PR #269 第五轮 review follow-up 与模块 build / warning 治理补充(RP-025)
|
||||
|
||||
- 启动复核:
|
||||
- 继续使用 `$gframework-pr-review` 读取 PR #269 当前 latest review、outside-diff comment、nitpick comment 与 open-thread 摘要
|
||||
- 本地核对后确认 `SchemaConfigGenerator` 的取消传播、根 `type` 非字符串防御、`ContextAware` 冲突快照与
|
||||
`Cqrs` error type 线程均已是陈旧信号;仍成立的是归一化字段名冲突与 `dynamic` 运行时类型引用问题
|
||||
- 决策:
|
||||
- `SchemaConfigGenerator` 不复用 `GF_ConfigSchema_006`,改为新增专门的冲突诊断 `GF_ConfigSchema_014`,
|
||||
避免把“标识符非法”和“归一化后重名”混成同一类错误
|
||||
- `CqrsHandlerRegistryGenerator` 对 `dynamic` 采用“生成期归一化为 `global::System.Object`”策略,而不是退回更宽泛的
|
||||
fallback 路径,保持精确注册能力且避免发射 `typeof(dynamic)`
|
||||
- `AGENTS.md` 增加模块级 build / warning 治理规则,要求后续改代码时必须对受影响模块跑 Release build,并处理或显式报告 warning
|
||||
- 实施调整:
|
||||
- 为 `SchemaConfigGenerator` 增加对象级生成属性名登记 helper,在 `ParseObjectSpec(...)` 中拦截 `foo-bar` /
|
||||
`foo_bar` 这类归一化后冲突,并新增 `ConfigSchemaDiagnostics.DuplicateGeneratedIdentifier`
|
||||
- 为 `SchemaConfigGeneratorTests` 补上冲突诊断回归测试;为 `CqrsHandlerRegistryGeneratorTests` 收紧
|
||||
unresolved-type 断言并新增 `dynamic` 类型归一化回归测试
|
||||
- 为 `CqrsHandlerRegistryGenerator.RuntimeTypeReferences` 增加 `TypeKind.Dynamic` 归一化处理,并保持
|
||||
`TypeKind.Error` 的保守回退
|
||||
- 为 `AGENTS.md` 补充“受影响模块必须独立 build 且 warning 不能默认甩给长期分支”的硬性规范
|
||||
- 验证结果:
|
||||
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:通过;并行 restore 时出现一次共享 `obj` 文件已存在的竞争噪音,串行验证后未再复现
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -clp:"Summary;WarningsOnly" -nologo`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -clp:"Summary;WarningsOnly" -nologo`
|
||||
- 结果:`9 Warning(s)`,`0 Error(s)`;维持既有 `SchemaConfigGenerator.cs` `MA0051` 基线,未新增 warning
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~Run_Should_Report_Diagnostic_When_Schema_Keys_Collide_After_Identifier_Normalization|FullyQualifiedName~Emits_Object_Type_Reference_When_Handler_Response_Uses_Dynamic|FullyQualifiedName~Emits_Runtime_Type_Lookup_When_Handler_Contract_Contains_Unresolved_Error_Types" -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`3 Passed`,`0 Failed`
|
||||
- 说明:测试项目构建仍打印既有 `MA0051` / `MA0004` / `MA0048` warning,不属于本轮 generator 模块写集,但已在 tracking 风险中记录
|
||||
- 下一步建议:
|
||||
- 若继续收口 PR #269,可再次抓取最新 unresolved threads,确认 GitHub 上剩余 open thread 是否全部转为陈旧信号
|
||||
- 若回到 analyzer 主线,继续推进 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 剩余 `MA0051`
|
||||
|
||||
## 2026-04-22 — RP-024
|
||||
|
||||
### 阶段:PR #269 第四轮 review follow-up 收口(RP-024)
|
||||
|
||||
- 启动复核:
|
||||
- 延续 `$gframework-pr-review` 对 PR #269 latest-head unresolved threads 的复核,重点核对最新 5 个未解决线程是否仍与当前
|
||||
worktree 一致
|
||||
- 本地确认 `EasyEvents` 异常契约、`SchemaConfigGenerator` 取消传播与 `ContextAwareGenerator` 字段冲突线程已是陈旧信号,
|
||||
真正仍成立的仅剩 `CqrsHandlerRegistryGenerator` 的 Roslyn error type 直接引用,以及根 schema `type` 非字符串时的
|
||||
`GetString()` 防御
|
||||
- 决策:
|
||||
- `CqrsHandlerRegistryGenerator` 保持现有“优先精确重建、必要时退回运行时查找”的设计,不引入新的程序集级 fallback 契约分支;
|
||||
只在 `CanReferenceFromGeneratedRegistry(...)` 中显式拒绝 `TypeKind.Error`,让未解析类型走已有运行时查找路径
|
||||
- `SchemaConfigGenerator` 继续沿用现有 `GF_ConfigSchema_002` 诊断,不新增诊断 ID;仅在根对象校验入口补上
|
||||
`JsonValueKind.String` 前置判断
|
||||
- 实施调整:
|
||||
- 为 `CqrsHandlerRegistryGenerator.RuntimeTypeReferences` 增加 `TypeKind.Error` 防御,避免把未解析类型写成生成代码里的
|
||||
`typeof(...)`
|
||||
- 为 `SchemaConfigGeneratorTests` 补上根 `type` 为数字时返回 `GF_ConfigSchema_002` 的回归测试
|
||||
- 为 `CqrsHandlerRegistryGeneratorTests` 补上未解析 error type 会改走运行时 `GetType(...)` 精确查找的回归测试
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过;仍保留既有 `9` 条 `SchemaConfigGenerator.cs` `MA0051`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests.Run_Should_Report_Diagnostic_When_Root_Type_Metadata_Is_Not_A_String|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Runtime_Type_Lookup_When_Handler_Contract_Contains_Unresolved_Error_Types" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`2 Passed`,`0 Failed`
|
||||
- 说明:测试命令需在无沙箱环境下运行,因为当前 test host 在沙箱内创建本地 socket 会收到 `Permission denied`
|
||||
- 下一步建议:
|
||||
- 若继续压缩 PR #269 的 review backlog,可再次抓取最新 unresolved threads,确认 GitHub 上仅剩陈旧线程后再决定是否继续代码改动
|
||||
- 若回到 analyzer 主线,继续推进 `SchemaConfigGenerator.cs` 剩余 `MA0051`
|
||||
|
||||
## 2026-04-22 — RP-023
|
||||
|
||||
### 阶段:PR #269 第三轮 review follow-up 收口(RP-023)
|
||||
|
||||
- 启动复核:
|
||||
- 延续 `$gframework-pr-review` 对 PR #269 的 latest-head unresolved threads、outside-diff comment 与 nitpick comment
|
||||
- 本地核实后确认剩余仍成立的项集中在 `SchemaConfigGenerator` 根类型名校验、aggregate registration comparer XML 文档转义、
|
||||
`LoggingConfigurationTests` / `CollectionExtensionsTests` 断言补强,以及 `ai-plan` 命令文本可复制性
|
||||
- 决策:
|
||||
- `SchemaConfigGenerator` 沿用现有 `InvalidGeneratedIdentifier` 诊断,不新增诊断 ID;将根类型名校验收敛到独立 helper,
|
||||
让顶层 schema 文件名与属性名共享同一类安全边界
|
||||
- aggregate registration comparer 文档直接复用现有 `EscapeXmlDocumentation(...)`,避免在 `///` 注释里再次写入原始泛型尖括号
|
||||
- `CqrsHandlerRegistryGenerator` 的重复反射查找分支采用小 helper 抽取,不改变 fallback 语义和快照输出
|
||||
- 实施调整:
|
||||
- 为 `SchemaConfigGenerator` 新增 `TryBuildRootTypeIdentifiers(...)`,在进入 `ParseObjectSpec(...)` 前拦截非法根类型名
|
||||
- 调整 aggregate registration comparer 属性的 XML 文档,使用 `<c>...</c>` 包裹并转义泛型类型文本
|
||||
- 为 `SchemaConfigGeneratorTests` 增加非法 schema 文件名诊断回归,并补强 generated catalog 中 comparer 文档断言
|
||||
- 为 `LoggingConfigurationTests` 增加正向键存在和值断言,为 `CollectionExtensionsTests` 补齐返回类型泛型参数绑定断言
|
||||
- 为 `CqrsHandlerRegistryGenerator.RuntimeTypeReferences` 抽取共享反射查找 helper,并修正 active tracking 中的转义引号
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过;仍保留既有 `9` 条 `SchemaConfigGenerator.cs` `MA0051`
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过;并行构建时出现一次 `MSB3026` 文件占用重试,自动恢复后完成
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~SchemaConfigGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`63 Passed`,`0 Failed`
|
||||
- 说明:测试项目构建仍打印既有 source-generator-tests analyzer warning,不属于本轮写集
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`27 Passed`,`0 Failed`
|
||||
- 下一步建议:
|
||||
- 若本轮验证通过,继续回到 `SchemaConfigGenerator.cs` 剩余 `MA0051`
|
||||
- 若 PR #269 仍有未关闭 review thread,再按“先本地复核、再最小修复”的节奏收口
|
||||
|
||||
## 2026-04-22 — RP-022
|
||||
|
||||
### 阶段:PR #269 第二轮 review follow-up 收口(RP-022)
|
||||
|
||||
- 启动复核:
|
||||
- 延续 `$gframework-pr-review` 的 PR #269 结果,继续核对 latest-head unresolved threads 与 nitpick comment
|
||||
- 结合本地实现确认仍成立的项不止第一轮记录的 4 个,还包括公共 API 兼容回退、`SchemaConfigGenerator` 取消传播、
|
||||
`ContextAwareGenerator` 真正的字段名去冲突与锁内读取修正、`Cqrs` 运行时类型 null 防御
|
||||
- 决策:
|
||||
- 对公共 API 兼容项优先保持既有契约,不为了压 analyzer 而继续收窄返回类型、属性类型或异常类型
|
||||
- `ContextAwareGenerator` 采用保守并发修复:移除未加锁 fast-path,统一在锁内读取上下文缓存,并让生成字段名按已有成员去冲突
|
||||
- `SchemaConfigGenerator` 在取消已请求时直接重新抛出 `OperationCanceledException`,避免把取消误报告成普通诊断
|
||||
- 实施调整:
|
||||
- 将 `EasyEvents.AddEvent<T>()` 的重复注册异常恢复为 `ArgumentException`,并在测试中恢复既有异常契约断言
|
||||
- 将 `CollectionExtensions.ToDictionarySafe(...)` 返回类型恢复为 `Dictionary<TKey, TValue>`,并新增反射测试锁定公开 API 形状
|
||||
- 将 `LoggingConfiguration` / `FilterConfiguration` 的公开集合属性恢复为具体 `List<>` / `Dictionary<,>` 类型,
|
||||
并新增反射测试与默认 comparer 语义断言
|
||||
- 为 `CqrsHandlerRegistryGenerator` 的命名类型引用构造补上 `ContainingAssembly is null` 防御,移除发射 helper 冗余布尔参数
|
||||
- 为 `SchemaConfigGenerator` 补上“仅在 cancellationToken 已取消时重抛”的 catch 分支,并为测试驱动添加多 `AdditionalText` 重载
|
||||
- 为 `ContextAwareGenerator` 增加生成成员名分配逻辑,新增 `_gFrameworkContextAware*` 与旧 `_context*` 双冲突快照场景,
|
||||
同时移除 getter 中未加锁 fast-path
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过;仍有既有 `9` 条 `MA0051`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`63 Passed`,`0 Failed`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~EasyEventsTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`38 Passed`,`0 Failed`
|
||||
- 下一步建议:
|
||||
- 回到 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的剩余 `MA0051`
|
||||
- 若后续 review 再提 analyzer 兼容建议,先做公共契约回归检查,再决定是否接受该建议
|
||||
|
||||
## 2026-04-22 — RP-021
|
||||
|
||||
### 阶段:PR #269 review follow-up 收口(RP-021)
|
||||
|
||||
- 启动复核:
|
||||
- 使用 `$gframework-pr-review` 读取当前分支 PR #269 的 CodeRabbit outside-diff 与 nitpick 汇总
|
||||
- 本地复核后确认仍成立的 4 个项分别是:`CqrsHandlerRegistryGenerator.cs` 超过仓库文件大小上限、
|
||||
`ContextAwareGenerator` 生成字段名可能与用户 partial 类型冲突、`SetContextProvider` 缺少运行时 null 防御、
|
||||
`Option<T>` 缺少 `<remarks>` 契约说明
|
||||
- 决策:
|
||||
- `CqrsHandlerRegistryGenerator` 继续采用既有 partial helper 风格,按“主流程 / 运行时类型引用 / 源码发射 / 模型”四个文件拆分,
|
||||
保持生成顺序、日志文本、fallback 契约和快照输出不变
|
||||
- `ContextAwareGenerator` 只收口仍成立的 review 项,不引入未被本地证实的 `Volatile.Read/Write` 变更
|
||||
- 为字段命名冲突新增生成器快照场景,避免后续回退到 `_context` / `_contextProvider` / `_contextSync`
|
||||
- 实施调整:
|
||||
- 将 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 拆为 4 个 partial 文件,分别承载主生成管线、
|
||||
runtime type reference 构造、source emission helper 与嵌套 specs/models
|
||||
- 将 `ContextAwareGenerator` 生成字段统一改为 `_gFrameworkContextAware*` 前缀,同步更新 XML 文档、注释和显式接口实现
|
||||
- 为 `SetContextProvider(...)` 增加 `ArgumentNullException.ThrowIfNull(provider)` 与 XML `<exception>` 说明
|
||||
- 为 `Option<T>` 补充 `<remarks>`,明确 `Some/None`、`null` 约束、不可变语义与推荐使用方式
|
||||
- 新增 `CollisionProneRule.ContextAware.g.cs` 快照,覆盖用户字段名与生成字段名冲突场景
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`;拆分后 `CqrsHandlerRegistryGenerator` 最大单文件为 `851` 行
|
||||
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~CqrsHandlerRegistryGeneratorTests -m:1 -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:`14 Passed`,`0 Failed`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~ContextAwareGeneratorSnapshotTests -m:1 -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:`2 Passed`,`0 Failed`
|
||||
- 说明:最初并行跑两个 `dotnet test` 命令时触发共享输出文件锁冲突;串行重跑后确认是测试宿主环境噪音而非代码回归
|
||||
- 下一步建议:
|
||||
- 若本轮验证通过,可继续回到 `SchemaConfigGenerator` 剩余 `MA0051`
|
||||
- 若 review 再次聚焦 `ContextAwareGenerator` 并发可见性问题,需要先补最小复现测试,再决定是否引入 `Volatile` 语义
|
||||
|
||||
## 2026-04-22 — RP-020
|
||||
|
||||
### 阶段:`SchemaConfigGenerator` 第一批 `MA0051` 结构拆分(RP-020)
|
||||
|
||||
- 启动复核:
|
||||
- 当前 worktree 仍映射到 `analyzer-warning-reduction` active topic
|
||||
- `GFramework.Game.SourceGenerators` warnings-only build 复现 `19` 条 warning,全部为
|
||||
`GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的 `MA0051`
|
||||
- 决策:
|
||||
- 本轮继续低风险结构拆分,不改变 schema 支持范围、诊断 ID、生成类型形状或输出顺序
|
||||
- 未使用 subagent;critical path 是本地复现 warning、拆分语义阶段并用 focused schema generator tests 验证行为
|
||||
- 实施调整:
|
||||
- 将 schema 入口解析拆为文本读取、root 验证、id key 验证和 `SchemaFileSpec` 构造阶段
|
||||
- 将属性解析拆为共享上下文提取、类型分派、标量/对象/数组属性构造 helper
|
||||
- 将统一 schema 遍历拆为对象属性、dependentSchemas、allOf、条件分支、not、array items / contains 等遍历阶段
|
||||
- 将约束文档生成拆为 const、numeric、string、array、object 约束片段
|
||||
- 将 catalog/registration/YAML/lookup/object type 等生成代码发射路径中的小型高收益 helper 拆出
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`9 Warning(s)`,`0 Error(s)`;当前项目剩余 warning 均为 `SchemaConfigGenerator.cs` 的 `MA0051`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:`50 Passed`,`0 Failed`
|
||||
- 说明:测试项目构建仍显示既有 source generator test analyzer warning;不属于本轮写集
|
||||
- 下一步建议:
|
||||
- 继续该主题时,优先拆分 `GenerateBindingsClass`、`AppendGeneratedConfigCatalogType` 或对象/条件 schema target 验证方法
|
||||
- 若转回 `MA0158`,仍需先设计多 target 条件编译方案,再考虑替换共享源码中的 `object` lock
|
||||
|
||||
## 2026-04-22 — RP-019
|
||||
|
||||
### 阶段:`SchemaConfigGenerator` 当前 `MA0006` 收口(RP-019)
|
||||
|
||||
- 启动复核:
|
||||
- 当前 worktree 仍映射到 `analyzer-warning-reduction` active topic
|
||||
- Windows Git interop 在当前 shell 中返回 WSL socket 错误;本轮使用显式 `--git-dir` / `--work-tree` 读取状态
|
||||
- `GFramework.Game.SourceGenerators` 首次 build 受 stale Windows fallback package folder 影响,刷新 restore 资产后复现
|
||||
`46` 条 warning,其中 `MA0006=27`,其余为 `SchemaConfigGenerator.cs` 的 `MA0051`
|
||||
- 决策:
|
||||
- 本轮先收口低风险 `MA0006`,不在同一 slice 中拆分 `SchemaConfigGenerator.cs` 的长方法
|
||||
- 未使用 subagent;critical path 是本地复现 warning、替换 schema 字符串比较并用 focused schema generator tests 验证输出行为
|
||||
- 实施调整:
|
||||
- 为 schema 类型关键字新增 `IsSchemaType` / `IsNumericSchemaType` helper,统一使用 `StringComparison.Ordinal`
|
||||
- 将 id key 类型验证、约束文档生成、required property 文档和路径拼接中的直接字符串比较改为显式 ordinal 比较
|
||||
- 修正 `JsonElement.GetString()` 后的 nullable flow,避免新增 `CS8604`
|
||||
- 验证结果:
|
||||
- `dotnet restore GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:通过
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`19 Warning(s)`,`0 Error(s)`;当前项目输出已无 `MA0006`,剩余均为 `MA0051`
|
||||
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:通过
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:`50 Passed`,`0 Failed`
|
||||
- 说明:测试项目构建仍显示既有 analyzer warning;不属于本轮写集
|
||||
- 下一步建议:
|
||||
- 继续该主题时,优先拆分 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的 `MA0051`
|
||||
- 若回到 `MA0158`,先设计多 target 条件编译方案,再考虑替换共享源码中的 `object` lock
|
||||
|
||||
## 2026-04-22 — RP-018
|
||||
|
||||
### 阶段:`CqrsHandlerRegistryGenerator` 剩余 `MA0051` 收口(RP-018)
|
||||
|
||||
- 启动复核:
|
||||
- 当前 worktree 仍映射到 `analyzer-warning-reduction` active topic
|
||||
- `MA0158` 锁迁移仍然跨 `GFramework.Core` / `GFramework.Cqrs` 多 target 共享源码,继续视为需要单独设计的兼容性问题
|
||||
- `GFramework.Cqrs.SourceGenerators` warnings-only build 复现 `CqrsHandlerRegistryGenerator.cs` 的 `6` 个 `MA0051`
|
||||
- 决策:
|
||||
- 本轮暂缓 `MA0158`,转入单文件、可由生成器测试覆盖的 `GFramework.Cqrs.SourceGenerators` 结构拆分
|
||||
- 未使用 subagent;critical path 是本地复现 warning、拆分源码发射流程并用 focused generator tests 验证输出未变
|
||||
- 实施调整:
|
||||
- 将 handler candidate 分析拆为接口收集、候选构造和单接口注册分类阶段
|
||||
- 将运行时类型引用构造拆为已构造泛型、命名类型反射查找等独立 helper
|
||||
- 将注册器源码生成拆为文件头、程序集特性、注册器类型、`Register` 方法和服务注册日志发射 helper
|
||||
- 将有序注册与精确反射注册输出拆为独立阶段,保留原有排序和生成文本形状
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~CqrsHandlerRegistryGeneratorTests -m:1 -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:`14 Passed`,`0 Failed`
|
||||
- 说明:测试项目构建仍显示 `GFramework.Game.SourceGenerators` 与测试项目中的既有 analyzer warning;不属于本轮写集
|
||||
- 下一步建议:
|
||||
- 继续该主题时,优先处理 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的 `MA0006` 低风险批次
|
||||
- 若回到 `MA0158`,先设计多 target 条件编译方案,再考虑替换共享源码中的 `object` lock
|
||||
|
||||
## 2026-04-22 — RP-017
|
||||
|
||||
### 阶段:`ContextAwareGenerator` 剩余 `MA0051` 收口(RP-017)
|
||||
|
||||
- 启动复核:
|
||||
- 当前 worktree 仍映射到 `analyzer-warning-reduction` active topic
|
||||
- `GFramework.Core` `net10.0` warnings-only build 在刷新 restore fallback 资产后复现 `16` 个 `MA0158`
|
||||
- `GFramework.Core.SourceGenerators` warnings-only build 复现 `ContextAwareGenerator.GenerateContextProperty` 的单个
|
||||
`MA0051`
|
||||
- 决策:
|
||||
- `MA0158` 涉及 `GFramework.Core` 与 `GFramework.Cqrs` 的 object lock 字段,且项目仍多 target 到 `net8.0` / `net9.0`
|
||||
/ `net10.0`,因此本轮不直接批量替换为 `System.Threading.Lock`
|
||||
- 先处理单文件、单 warning、生成输出可由 snapshot 验证的 `ContextAwareGenerator` 结构拆分
|
||||
- 未使用 subagent;本轮 critical path 是本地复现 warning、拆分方法并验证生成输出,拆分后写集只包含单个 generator 文件和
|
||||
active `ai-plan` 文档
|
||||
- 实施调整:
|
||||
- 将 `GenerateContextProperty` 拆为 `GenerateContextBackingFields`、`GenerateContextGetter` 与
|
||||
`GenerateContextProviderConfiguration`
|
||||
- 保留原有 `StringBuilder` 追加顺序与生成代码文本,避免 snapshot 变更
|
||||
- 为新增 helper 补充 XML 注释,说明字段、getter 与 provider 配置 API 的生成职责
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net10.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`16 Warning(s)`,`0 Error(s)`;记录当前 `MA0158` 基线,不作为本轮修改范围
|
||||
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`;`ContextAwareGenerator.cs` 的 `MA0051` 已清零
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`1 Passed`,`0 Failed`
|
||||
- 说明:该 test project 构建仍显示相邻 generator/test 项目的既有 analyzer warning;本轮关注的
|
||||
`GFramework.Core.SourceGenerators` 独立 build 已清零
|
||||
- 下一步建议:
|
||||
- 继续该主题时,优先设计 `MA0158` 的多 target 兼容迁移方案;如果风险过高,再单独切入
|
||||
`GFramework.Cqrs.SourceGenerators` 或 `GFramework.Game.SourceGenerators` 的结构性 warning
|
||||
|
||||
## 2026-04-22 — RP-016
|
||||
|
||||
### 阶段:`GFramework.Core` 剩余低风险 warning 批次清零(RP-016)
|
||||
|
||||
- 依据 `RP-015` 的下一步建议,本轮恢复到 `MA0016` / `MA0002` 低风险批次,并顺手吸收仍集中在
|
||||
`GFramework.Core` 的 `MA0015` 与 `MA0077`
|
||||
- 基线复核:
|
||||
- 首次使用 Linux `dotnet` 时仍被当前 worktree 的 Windows fallback package folder restore 资产阻断
|
||||
- 切换到 host Windows `dotnet` 后,`GFramework.Core` `net8.0` warnings-only build 复现 `9` 条 warning:
|
||||
`MA0016=5`、`MA0002=2`、`MA0015=1`、`MA0077=1`
|
||||
- 实施调整:
|
||||
- 将 `LoggingConfiguration.Appenders` / `LoggerLevels` 与 `FilterConfiguration.Namespaces` / `Filters`
|
||||
的公开类型改为集合抽象接口,同时保留 `List<T>` / `Dictionary<TKey,TValue>` 默认实例,兼顾 analyzer 与现有配置消费路径
|
||||
- 将 `CollectionExtensions.ToDictionarySafe(...)` 返回类型改为 `IDictionary<TKey,TValue>`,内部仍使用 `Dictionary<TKey,TValue>`
|
||||
保留“重复键以后值覆盖前值”的实现语义
|
||||
- 为 `CoroutineScheduler` 的 `_tagged` 与 `_grouped` 字典显式指定 `StringComparer.Ordinal`,将原有默认区分大小写语义写入代码
|
||||
- 将 `EasyEvents.AddEvent<T>()` 重复注册失败从 `ArgumentException` 改为 `InvalidOperationException`;该路径表示状态冲突,
|
||||
不是某个方法参数无效,因此不能为 `MA0015` 人造参数名
|
||||
- 为 `Option<T>` 声明 `IEquatable<Option<T>>`,与已有强类型 `Equals(Option<T>)` 实现对齐
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~EasyEventsTests|FullyQualifiedName~OptionTests|FullyQualifiedName~CoroutineGroupTests|FullyQualifiedName~CoroutineSchedulerTests" -m:1 -nologo`
|
||||
- 结果:`112 Passed`,`0 Failed`
|
||||
- 说明:测试构建仍显示既有 `net10.0` `MA0158` 与 source generator `MA0051` warning;这些不属于本轮
|
||||
`GFramework.Core` `net8.0` 剩余 warning 批次
|
||||
- 当前结论:
|
||||
- `GFramework.Core` `net8.0` 当前 analyzer warning baseline 已清零
|
||||
- analyzer topic 仍可继续,但下一轮应转入 `net10.0` 专属 `MA0158` 兼容性评估,或单独处理 source generator 剩余
|
||||
`MA0051`
|
||||
- 下一步建议:
|
||||
- 优先评估 `MA0158` 在多 target 源码中的安全推进方式;若风险过高,再处理
|
||||
`GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs` 的结构拆分
|
||||
|
||||
## 2026-04-21 — RP-015
|
||||
|
||||
### 阶段:PR #267 failed-test follow-up 收口(RP-015)
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
# Documentation Governance And Refresh 跟踪
|
||||
|
||||
## 目标
|
||||
|
||||
继续以“文档必须可追溯到源码、测试与真实接入方式”为原则,维护 `GFramework` 的仓库入口、模块入口与
|
||||
`docs/zh-CN` 采用链路,避免 README、专题页与教程再次偏离当前实现。
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-019`
|
||||
- 当前阶段:`Completed`
|
||||
- 当前焦点:
|
||||
- 用户已确认 PR #268 合并,本 topic 对应的文档治理收口工作完成
|
||||
- 当前目录将在本轮迁入 `ai-plan/public/archive/documentation-governance-and-refresh/`
|
||||
- 后续若需历史回溯,应从 archive 中恢复,而不是继续把该 topic 作为 active 默认入口
|
||||
|
||||
## 当前状态摘要
|
||||
|
||||
- `docs/zh-CN/godot/` 当前高优先级页面集与 `docs/zh-CN/tutorials/godot-integration.md` 已完成源码优先收口
|
||||
- PR #268 已合并,上一轮保留 active 的唯一原因已经解除
|
||||
- 本 topic 已达到归档条件:实现完成、校验完成、PR 生命周期结束
|
||||
|
||||
## 当前活跃事实
|
||||
|
||||
- 当前 worktree 下未发现 `ai-plan/private/` 恢复目录,本主题一直以 public artifacts 作为唯一恢复入口
|
||||
- 已存在的阶段归档:
|
||||
- `ai-plan/public/documentation-governance-and-refresh/archive/todos/documentation-governance-and-refresh-history-through-2026-04-22.md`
|
||||
- `ai-plan/public/documentation-governance-and-refresh/archive/traces/documentation-governance-and-refresh-history-through-2026-04-22.md`
|
||||
- 2026-04-22 之前的长篇历史已存在于 2026-04-18 与 RP-001 through RP-008 的归档文件中
|
||||
- 当前待处理事项已清零;后续只保留历史查询价值
|
||||
|
||||
## 当前风险
|
||||
|
||||
- 后续历史定位风险:如果不把 topic 从 active 列表中移除,`boot` 会继续把已经完成的文档治理主题当作默认入口
|
||||
- 缓解措施:本轮同步更新 `ai-plan/public/README.md` 并把整个 topic 目录迁入 `ai-plan/public/archive/`
|
||||
- 文档回漂风险:未来若有新的 README / `docs/zh-CN` 变更,仍可能重新引入与源码不一致的表述
|
||||
- 缓解措施:新任务应创建或复用新的 active topic,而不是重启当前已完成主题
|
||||
|
||||
## 活跃文档
|
||||
|
||||
- 当前 trace:[documentation-governance-and-refresh-trace.md](../traces/documentation-governance-and-refresh-trace.md)
|
||||
- 2026-04-22 跟踪归档:[documentation-governance-and-refresh-history-through-2026-04-22.md](../archive/todos/documentation-governance-and-refresh-history-through-2026-04-22.md)
|
||||
- 2026-04-22 trace 归档:[documentation-governance-and-refresh-history-through-2026-04-22.md](../archive/traces/documentation-governance-and-refresh-history-through-2026-04-22.md)
|
||||
- 2026-04-18 历史归档:[documentation-governance-and-refresh-history-through-2026-04-18.md](../archive/todos/documentation-governance-and-refresh-history-through-2026-04-18.md)
|
||||
- RP-001 到 RP-008 trace 归档:[documentation-governance-and-refresh-rp-001-through-rp-008.md](../archive/traces/documentation-governance-and-refresh-rp-001-through-rp-008.md)
|
||||
|
||||
## 验证说明
|
||||
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/index.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/architecture.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/scene.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/ui.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/signal.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/extensions.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/logging.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`
|
||||
- `cd docs && bun run build`
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 将整个 `documentation-governance-and-refresh` 目录迁入 `ai-plan/public/archive/`
|
||||
2. 从 `ai-plan/public/README.md` 删除该 topic 的 active 声明与 worktree 映射
|
||||
@ -0,0 +1,26 @@
|
||||
# Documentation Governance And Refresh Trace
|
||||
|
||||
## 2026-04-22
|
||||
|
||||
### 当前恢复点:RP-019
|
||||
|
||||
- 本轮按 `boot` 恢复 `documentation-governance-and-refresh` 主题
|
||||
- 用户明确说明 PR #268 已合并,因此该主题不再需要保持 active 以等待 review follow-up
|
||||
- 当前主题满足完成条件:文档页已完成校验、`docs` 站点先前已构建通过、PR 生命周期结束
|
||||
- 本轮将把整个主题目录迁入 `ai-plan/public/archive/documentation-governance-and-refresh/`
|
||||
- `ai-plan/public/README.md` 也将在本轮删除该 topic 的 active 声明与 worktree 映射
|
||||
|
||||
### 当前决策
|
||||
|
||||
- 当前主题正式归档,不再作为 `boot` 默认入口
|
||||
- 若未来出现新的文档治理任务,应创建新的 active topic 或挂到新的现役主题,而不是恢复本目录
|
||||
- 现有 tracking / trace 留在 archive 中作为历史恢复材料
|
||||
|
||||
### 验证
|
||||
|
||||
- `cd docs && bun run build`
|
||||
- 结果:通过;无构建失败,主题满足归档前的最终验证要求
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 若需回看本阶段历史,从 `ai-plan/public/archive/documentation-governance-and-refresh/` 读取归档材料
|
||||
@ -0,0 +1,77 @@
|
||||
# Documentation Full Coverage Governance Validation History Through RP-007
|
||||
|
||||
以下内容从 active tracking 中迁出,用于保留 `DOCUMENTATION-FULL-COVERAGE-GOV-RP-001` 到
|
||||
`DOCUMENTATION-FULL-COVERAGE-GOV-RP-007` 的详细验证历史。默认 `boot` 只需要读取 active tracking 中的最新摘要;
|
||||
若需要追溯早期验证命令与结果,再回到本归档文件。
|
||||
|
||||
## 详细验证历史
|
||||
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/index.md`
|
||||
- 结果:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/ecs-arch-abstractions.md`
|
||||
- 结果:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/api-reference/index.md`
|
||||
- 结果:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/index.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在补充 Core XML inventory 后重新验证
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/core-abstractions.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在补充 Core.Abstractions XML inventory 后重新验证
|
||||
- `cd docs && bun run build`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 重新构建通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
- `dotnet build GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj -c Release -p:RestoreFallbackFolders=`
|
||||
- 结果:通过
|
||||
- 备注:`0 Warning(s) / 0 Error(s)`
|
||||
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj -c Release -p:RestoreFallbackFolders=`
|
||||
- 结果:通过
|
||||
- 备注:`0 Warning(s) / 0 Error(s)`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/ecs/index.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在重写 ECS landing 后重新验证
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/ecs/arch.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在重写 Arch ECS 专题页后重新验证
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/ecs-arch-abstractions.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在补充抽象页 XML inventory 后重新验证
|
||||
- `cd docs && bun run build`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在 Ecs 波次重写后重新构建通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/cqrs.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在重写 `Cqrs` family landing 后重新验证
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在新增 `Cqrs.SourceGenerators` 专题页后验证通过
|
||||
- `python3` 轻量 XML inventory 扫描
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 确认 `GFramework.Cqrs` 的 `Internal/` 为 `14/14`、`GFramework.Cqrs.SourceGenerators/Cqrs/` 为 `3/3`、`GFramework.Cqrs.Abstractions/Cqrs/` 为 `20/20`
|
||||
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -p:RestoreFallbackFolders=`
|
||||
- 结果:通过
|
||||
- 备注:保留既有 `NU1900` 与 `MA0051` warnings;无新增编译错误
|
||||
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
|
||||
- 结果:失败
|
||||
- 备注:当前环境会命中失效的 Windows fallback package folder,并在多目标 inner build 阶段触发 `MSB4276` / `MSB4018`;失败原因已记录为环境阻塞,不属于本轮文档改动回归
|
||||
- `cd docs && bun run build`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在 `Cqrs` 波次文档刷新后重新构建通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/game-abstractions.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-23` 在重写 `Game.Abstractions` 页面后验证通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/index.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-23` 在补充 frontmatter 与 XML inventory 后重新验证
|
||||
- `python3` 轻量 XML inventory 扫描
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-23` 确认 `GFramework.Game` 为 `56/56`、`GFramework.Game.Abstractions` 为 `80/80`、`GFramework.Game.SourceGenerators` 为 `2/2`
|
||||
- `cd docs && bun run build`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-23` 在 `Game` 波次文档刷新后重新构建通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
- `cd docs && bun run build`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-23` 在更新 `AGENTS.md` 的 WSL Git 优先级后重新构建通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
- `cd docs && bun run build`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-23` 在推进 `DOCUMENTATION-FULL-COVERAGE-GOV-RP-007`、回写 `Game` family 巡检结论后重新构建通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
@ -0,0 +1,124 @@
|
||||
# Documentation Full Coverage Governance 跟踪
|
||||
|
||||
## 目标
|
||||
|
||||
建立一个长期 active topic,持续治理 `GFramework` 的 README、`docs/zh-CN`、站点导航、XML 文档和 API
|
||||
参考链路,避免历史上的阶段性刷新完成后再次回漂。
|
||||
|
||||
- 用源码、测试、`*.csproj` 和必要的 `ai-libs/` 证据校正文档
|
||||
- 以模块族为单位闭环 README、landing page、专题页、教程入口和 API 参考链路
|
||||
- 明确哪些目录是可直接消费模块,哪些只是内部支撑模块
|
||||
- 把 XML 文档缺口纳入治理范围,而不是只刷新 Markdown
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-008`
|
||||
- 当前阶段:`Phase 5 - Governance Maintenance`
|
||||
- 当前焦点:
|
||||
- 消化 PR #271 的 latest-head review follow-up,修正仍在本地成立的 docs / skill / ai-plan 问题
|
||||
- 将 active tracking 的重复验证明细迁出默认 boot 路径,只保留最新可恢复摘要
|
||||
- 评估是否需要把 `Godot` family 的关键 XML inventory 摘要迁回 active topic
|
||||
|
||||
## 当前状态摘要
|
||||
|
||||
- 已归档的 `documentation-governance-and-refresh` 仅保留为历史证据,不再作为默认 `boot` 入口
|
||||
- 本轮已消化的 PR #271 review follow-up:
|
||||
- 为 `.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py` 补齐 WSL worktree 下的显式 Linux Git 绑定,避免 `git.exe` 在当前会话触发 `Exec format error`
|
||||
- 同步更新 `.agents/skills/gframework-pr-review/SKILL.md`,改为与 `AGENTS.md` 一致的 Git 策略,并把命令示例统一到 `.agents/...` 路径
|
||||
- 为 `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md` 补充 marker 类型放置与命名约定说明
|
||||
- 从 `docs/zh-CN/abstractions/ecs-arch-abstractions.md` 删除误放的 source-generator 内部模块提醒,并微调 `docs/zh-CN/ecs/index.md` 的边界说明语序
|
||||
- 为 `ai-plan/public/archive/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md` 的归档验证补写结果态
|
||||
- 将 RP-001 至 RP-007 的详细验证历史迁入 `ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-validation-history-through-rp-007.md`
|
||||
- 本轮已确认的消费属性结论:
|
||||
- `GFramework.Ecs.Arch.Abstractions`:可打包直接消费模块,需要 README 和文档入口
|
||||
- `GFramework.Core.SourceGenerators.Abstractions`:`IsPackable=false`,按内部支撑模块处理
|
||||
- `GFramework.Godot.SourceGenerators.Abstractions`:`IsPackable=false`,按内部支撑模块处理
|
||||
- `GFramework.SourceGenerators.Common`:`IsPackable=false`,按内部支撑模块处理
|
||||
- 本轮已完成的治理动作:
|
||||
- 新建 `GFramework.Ecs.Arch.Abstractions/README.md`
|
||||
- 在根 `README.md` 中补齐 `GFramework.Ecs.Arch.Abstractions` 入口,并声明内部支撑模块 owner
|
||||
- 为抽象接口栏目补齐 `Ecs.Arch.Abstractions` 页面与 sidebar 入口
|
||||
- 将 `docs/zh-CN/api-reference/index.md` 重写为模块到 XML / README / 教程的阅读链路入口
|
||||
- 为 `GFramework.Core/README.md` 补齐 `Services`、`Configuration`、`Environment`、`Pool`、`Rule`、`Time` 等当前目录映射
|
||||
- 为 `GFramework.Core.Abstractions/README.md` 补齐契约族地图与 XML 阅读重点
|
||||
- 将 `docs/zh-CN/abstractions/core-abstractions.md` 从过时的接口摘录页重写为契约边界 / 包关系 / 最小接入路径页面
|
||||
- 为 `docs/zh-CN/core/index.md` 补齐 frontmatter、能力域导航和 API / XML 阅读入口
|
||||
- 为 `GFramework.Core/README.md`、`GFramework.Core.Abstractions/README.md` 补齐类型族级 XML 覆盖基线入口
|
||||
- 为 `docs/zh-CN/core/index.md`、`docs/zh-CN/abstractions/core-abstractions.md` 增加“类型族 -> XML 覆盖状态 -> 代表类型”的 inventory
|
||||
- 基于顶层目录轻量盘点确认:`Core` / `Core.Abstractions` 当前公开 / 内部类型声明都已带 XML 注释,成员级审计留待后续波次
|
||||
- 重写 `docs/zh-CN/ecs/index.md`,收敛当前 ECS family 的包边界、采用顺序和 XML inventory
|
||||
- 重写 `docs/zh-CN/ecs/arch.md`,明确 `UseArch(...)` 需早于 `Initialize()` 的真实接入时机
|
||||
- 刷新 `GFramework.Ecs.Arch/README.md`,使运行时 README 与源码 / 测试一致
|
||||
- 为 `GFramework.Ecs.Arch.Abstractions/README.md` 与 `docs/zh-CN/abstractions/ecs-arch-abstractions.md` 补齐类型族级 XML inventory
|
||||
- 重写 `docs/zh-CN/core/cqrs.md`,将其收敛为 `Cqrs` family landing,并补齐运行时 / 契约层 / 生成器的 XML inventory
|
||||
- 新建 `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`,为 `Cqrs.SourceGenerators` 补齐站内专题入口
|
||||
- 更新 `docs/zh-CN/source-generators/index.md`、`docs/zh-CN/api-reference/index.md` 与 VitePress sidebar,使 `Cqrs` family 的 generator 入口可导航
|
||||
- 为 `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs` 与 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中缺失的内部类型补齐 XML 注释,使本轮轻量 inventory 达到声明级闭环
|
||||
- 为 `GFramework.Game/README.md`、`GFramework.Game.Abstractions/README.md`、`GFramework.Game.SourceGenerators/README.md` 补齐 `Game` family 的类型族级 XML inventory
|
||||
- 为 `docs/zh-CN/game/index.md` 补齐 frontmatter,并增加 `Game` / `Game.Abstractions` / `Game.SourceGenerators` 的 XML 覆盖基线入口
|
||||
- 将 `docs/zh-CN/abstractions/game-abstractions.md` 从失真的旧接口摘录页重写为契约边界 / 包关系 / 最小接入路径页面
|
||||
- 基于顶层目录轻量盘点确认:`GFramework.Game` 为 `56/56`、`GFramework.Game.Abstractions` 为 `80/80`、`GFramework.Game.SourceGenerators` 为 `2/2`,当前公开 / 内部类型声明都已带 XML 注释
|
||||
- 更新 `AGENTS.md` 的 WSL Git 策略,将显式 `--git-dir` / `--work-tree` 绑定提升为高于 `git.exe` 的默认优先级
|
||||
- 记录当前环境偏差:本会话 `git.exe` 可解析但执行会触发 `Exec format error`,而 plain Linux `git` 会命中 worktree 路径翻译错误,需要显式仓库绑定
|
||||
- 完成 `Game` family 巡检,确认 `docs/zh-CN/game/config-system.md`、`scene.md`、`ui.md` 与 `docs/zh-CN/source-generators/index.md` 的核心采用说明、包关系与交叉引用仍与当前源码 / README 一致,没有发现需要立刻修正的回漂
|
||||
|
||||
## Inventory(第一版)
|
||||
|
||||
| 模块族 | 当前状态 | 当前证据 | 下一动作 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Core` / `Core.Abstractions` | `README / landing / 类型族级 XML inventory 已收口,成员级审计待补齐` | 根 README、模块 README、`docs/zh-CN/core/**`、`docs/zh-CN/abstractions/core-abstractions.md` 已对齐当前目录与类型族基线 | 进入巡检;如有新 API 变更,再追加成员级 XML 审计 |
|
||||
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | `README / landing / generator topic / 类型族级 XML inventory 已收口,成员级审计待补齐` | `GFramework.Cqrs/README.md`、`GFramework.Cqrs.Abstractions/README.md`、`GFramework.Cqrs.SourceGenerators/README.md`、`docs/zh-CN/core/cqrs.md`、`docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`、`docs/zh-CN/api-reference/index.md` 已对齐当前源码与测试 | 转入巡检;下一波切到 `Game` family 的 XML / 教程链路审计 |
|
||||
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | `README / landing / abstractions / 类型族级 XML inventory 已收口,成员级审计待补齐` | `GFramework.Game/README.md`、`GFramework.Game.Abstractions/README.md`、`GFramework.Game.SourceGenerators/README.md`、`docs/zh-CN/game/index.md`、`docs/zh-CN/abstractions/game-abstractions.md` 已对齐当前源码与目录基线 | 转入巡检;优先抽查 `config-system`、`scene`、`ui` 与 `source-generators` 交叉链路是否回漂 |
|
||||
| `Godot` / `Godot.SourceGenerators` | `已验证` | 上一轮归档 topic 已完成核心 landing / topic / tutorial 校验 | 进入巡检周期,重点看回漂 |
|
||||
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | `README / landing / abstractions / 类型族级 XML inventory 已收口,成员级审计待补齐` | `GFramework.Ecs.Arch/README.md`、`GFramework.Ecs.Arch.Abstractions/README.md`、`docs/zh-CN/ecs/**`、`docs/zh-CN/abstractions/ecs-arch-abstractions.md` 已对齐当前源码与测试 | 转入巡检;后续仅在运行时公共 API 变动时补成员级 XML 细审 |
|
||||
| `SourceGenerators.Common` 与 `*.SourceGenerators.Abstractions` | `已判定为内部支撑` | `*.csproj` 明确 `IsPackable=false` | 由所属模块 README 与生成器栏目说明 owner,不建独立采用页 |
|
||||
|
||||
## 缺口分级
|
||||
|
||||
- `P0`
|
||||
- 错误采用路径、错误包关系、错误 API / 生命周期语义
|
||||
- 站点导航死链、空 landing page、明显错误的模块 owner
|
||||
- `P1`
|
||||
- 直接消费模块缺 README 或缺对应 docs 入口
|
||||
- README / docs 示例与源码实现不一致
|
||||
- 教程仍引用已经过时的默认接线方式
|
||||
- `P2`
|
||||
- 结构重复、交叉链接不足、API 参考链路过薄
|
||||
- 站内页面存在事实正确但组织方式不利于定位的内容
|
||||
|
||||
## 当前风险
|
||||
|
||||
- 当前 `Core` / `Core.Abstractions` 只完成了类型族级 XML 基线,不等于成员级契约全审计
|
||||
- 缓解措施:后续只在共享抽象或高风险生命周期接口发生改动时补成员级细审,不在本轮扩张范围
|
||||
- `Godot` family 的治理结论主要留在已归档 topic 中,active topic 当前只保留摘要
|
||||
- 缓解措施:下一恢复点优先判断是否要把关键 XML inventory 摘要迁回 active topic,避免后续 boot 仍过度依赖 archive
|
||||
- 新功能分支若修改 README / docs / 公共 API 却不挂文档 topic,仍可能回漂
|
||||
- 缓解措施:将本 topic 作为长期 active topic 保留,并在后续巡检中记录回漂来源
|
||||
- VitePress 页面不能直接链接到 `docs/` 目录之外的模块 `README.md`
|
||||
- 缓解措施:站内页面用模块路径文本或站内 API 入口表达,仓库级 README 仍保留仓库文件链接
|
||||
- `GFramework.Cqrs` 在当前 WSL / dotnet 环境下,本地 build 仍会读取失效的 fallback package folder 配置,导致无法完成该项目的标准编译验证
|
||||
- 缓解措施:本轮先以 `GFramework.Cqrs.SourceGenerators` 编译通过和 docs site build 通过作为有效验证,并在后续环境治理或构建脚本清理时单独处理 `RestoreFallbackFolders` / 资产文件问题
|
||||
- 当前 WSL 会话中 `git.exe` 虽然可解析,但不能执行
|
||||
- 缓解措施:把显式 `--git-dir` / `--work-tree` 绑定上升为仓库默认回退策略,并仅把 `git.exe` 保留为可执行时的次级 fallback
|
||||
|
||||
## 验证说明
|
||||
|
||||
- 详细验证历史已归档到 `ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-validation-history-through-rp-007.md`
|
||||
- `2026-04-23` `python3 -B -c "from pathlib import Path; compile(Path('.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py').read_text(encoding='utf-8'), '.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py', 'exec')"`
|
||||
- 结果:通过
|
||||
- `2026-04-23` `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json`
|
||||
- 结果:通过;成功抓取 PR `#271`,并确认当前 latest-head review threads 为 `4` 条 open
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
|
||||
- 结果:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/ecs/index.md`
|
||||
- 结果:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/ecs-arch-abstractions.md`
|
||||
- 结果:通过
|
||||
- `2026-04-23` `cd docs && bun run build`
|
||||
- 结果:通过;仅保留既有 VitePress 大 chunk warning,无构建失败
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 完成本轮 PR #271 follow-up 的针对性验证与 docs build,确认 open threads 是否都已被本地收敛
|
||||
2. 推送当前分支后重新执行 `$gframework-pr-review`,确认 PR #271 的 latest-head open threads 是否按预期收敛
|
||||
3. 评估是否需要把 `Godot` family 的关键 XML inventory 摘要迁回 active topic,避免长期治理只依赖 archive 恢复
|
||||
@ -0,0 +1,275 @@
|
||||
# Documentation Full Coverage Governance Trace
|
||||
|
||||
## 2026-04-22
|
||||
|
||||
### 当前恢复点:RP-001
|
||||
|
||||
- 按长期治理计划新建 active topic `documentation-full-coverage-governance`
|
||||
- 在 `ai-plan/public/README.md` 中将当前分支 `docs/sdk-update-documentation` 映射到该 topic
|
||||
- 复核已知缺口模块的 `*.csproj` 后确认:
|
||||
- `GFramework.Ecs.Arch.Abstractions` 是可打包消费模块,需要独立 README
|
||||
- `GFramework.Core.SourceGenerators.Abstractions`、`GFramework.Godot.SourceGenerators.Abstractions`、
|
||||
`GFramework.SourceGenerators.Common` 都是 `IsPackable=false` 的内部支撑模块
|
||||
- 基于该结论,本轮没有为内部支撑模块新增独立 README,而是在根 README 与 abstractions / API 入口中明确其 owner
|
||||
|
||||
### 当前决策
|
||||
|
||||
- 新主题的完成条件采用长期治理口径:`P0` 清零、无 README 缺失、无导航死链,并完成连续两轮稳定巡检
|
||||
- 本轮先做治理基础设施与 inventory,不把整个长期计划伪装成单轮完成
|
||||
- `api-reference` 页面改为“模块 -> README / docs / XML / tutorial”的阅读链路入口,避免继续维护失真的伪签名列表
|
||||
- `Ecs.Arch` family 被列为高优先 backlog:抽象层入口已补齐,但 runtime docs 仍需按源码重写
|
||||
- `Core` / `Core.Abstractions` 波次先收口 README、landing page 和 abstractions 页的目录映射,再补显式 XML 覆盖 inventory
|
||||
- VitePress 站内页面不直接链接仓库根模块 `README.md`;站内仅保留可构建的 docs 链接,模块 README 以文本路径或仓库 README 承接
|
||||
|
||||
### 当前恢复点:RP-002
|
||||
|
||||
- 完成 `Core` / `Core.Abstractions` 的类型族级 XML inventory:
|
||||
- `GFramework.Core/README.md`
|
||||
- `GFramework.Core.Abstractions/README.md`
|
||||
- `docs/zh-CN/core/index.md`
|
||||
- `docs/zh-CN/abstractions/core-abstractions.md`
|
||||
- 通过顶层目录轻量盘点确认:
|
||||
- `GFramework.Core` 当前各目录族的公开 / 内部类型声明都已带 XML 注释
|
||||
- `GFramework.Core.Abstractions` 当前各契约目录族的公开 / 内部类型声明都已带 XML 注释
|
||||
- 这轮 inventory 明确限定为“类型声明级基线”,不把结果表述成成员级 XML 合规审计
|
||||
|
||||
### 当前决策(RP-002)
|
||||
|
||||
- XML inventory 同时落在模块 README 和站内 landing page:
|
||||
- README 提供仓库侧入口,方便从包目录直接恢复上下文
|
||||
- docs landing 提供更细的类型族 / 代表类型 / 阅读重点表格,方便站内导航
|
||||
- `Core` 波次在补齐基线后转入巡检,不继续在本轮展开成员级 ``<param>`` / ``<returns>`` 审计
|
||||
- 下一恢复点切换到 `Ecs` 波次,优先处理仍明显失真的 runtime docs
|
||||
|
||||
### 当前验证
|
||||
|
||||
- 文档校验:
|
||||
- `validate-all.sh docs/zh-CN/abstractions/index.md`:通过
|
||||
- `validate-all.sh docs/zh-CN/abstractions/ecs-arch-abstractions.md`:通过
|
||||
- `validate-all.sh docs/zh-CN/api-reference/index.md`:通过
|
||||
- `validate-all.sh docs/zh-CN/core/index.md`:通过
|
||||
- `validate-all.sh docs/zh-CN/abstractions/core-abstractions.md`:通过
|
||||
- 构建校验:
|
||||
- `cd docs && bun run build`:通过
|
||||
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj -c Release -p:RestoreFallbackFolders=`:通过,`0 Warning(s) / 0 Error(s)`
|
||||
- `dotnet build GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj -c Release -p:RestoreFallbackFolders=`:通过,`0 Warning(s) / 0 Error(s)`
|
||||
|
||||
### 当前验证(RP-002)
|
||||
|
||||
- 文档校验:
|
||||
- `validate-all.sh docs/zh-CN/core/index.md`:通过
|
||||
- `validate-all.sh docs/zh-CN/abstractions/core-abstractions.md`:通过
|
||||
- 构建校验:
|
||||
- `cd docs && bun run build`:通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
|
||||
### 当前恢复点:RP-003
|
||||
|
||||
- 完成 `Ecs.Arch` 波次的运行时文档刷新:
|
||||
- `docs/zh-CN/ecs/index.md`
|
||||
- `docs/zh-CN/ecs/arch.md`
|
||||
- `GFramework.Ecs.Arch/README.md`
|
||||
- 为 `Ecs.Arch.Abstractions` 补齐与运行时页同粒度的 XML inventory:
|
||||
- `GFramework.Ecs.Arch.Abstractions/README.md`
|
||||
- `docs/zh-CN/abstractions/ecs-arch-abstractions.md`
|
||||
- 明确记录一个关键采用事实:
|
||||
- `UseArch(...)` 必须早于 `Initialize()` 调用
|
||||
- 该结论以 `ArchExtensions` 的模块注册方式和 `ExplicitRegistrationTests` 为证据
|
||||
- 将 `Ecs.Arch` family 从“入口存在但失真”推进到“README / landing / abstractions / XML inventory 已对齐源码与测试”
|
||||
|
||||
### 当前决策(RP-003)
|
||||
|
||||
- `Ecs` 波次继续采用与 `Core` 相同的治理粒度:
|
||||
- 模块 README 承担仓库入口
|
||||
- `docs/zh-CN/ecs/index.md` 承担模块族 landing
|
||||
- `docs/zh-CN/ecs/arch.md` 承担运行时默认实现专题页
|
||||
- `docs/zh-CN/abstractions/ecs-arch-abstractions.md` 承担契约边界专题页
|
||||
- `EnableStatistics` 当前仅保留在公开配置面上;文档不再把它写成已验证的运行时行为
|
||||
- 下一恢复点切换到 `Cqrs` 波次,优先解决入口分散和 API / XML 阅读链路不统一的问题
|
||||
|
||||
### 当前验证(RP-003)
|
||||
|
||||
- 文档校验:
|
||||
- `validate-all.sh docs/zh-CN/ecs/index.md`:通过
|
||||
- `validate-all.sh docs/zh-CN/ecs/arch.md`:通过
|
||||
- `validate-all.sh docs/zh-CN/abstractions/ecs-arch-abstractions.md`:通过
|
||||
- 构建校验:
|
||||
- `cd docs && bun run build`:通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 在 `Cqrs` 波次核对模块 README、`docs/zh-CN/core/cqrs.md` 与 `docs/zh-CN/source-generators/**` 的真实 owner
|
||||
2. 决定 `Cqrs` family 是补 dedicated landing 还是拆分现有入口页
|
||||
|
||||
### 当前恢复点:RP-004
|
||||
|
||||
- 完成 `Cqrs` 波次的模块族入口刷新:
|
||||
- 重写 `docs/zh-CN/core/cqrs.md`
|
||||
- 新建 `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
|
||||
- 更新 `docs/zh-CN/source-generators/index.md`
|
||||
- 更新 `docs/zh-CN/api-reference/index.md`
|
||||
- 更新 `docs/.vitepress/config.mts`
|
||||
- 将 `Cqrs` family 从“README 已存在但 generator 入口分散”推进到“runtime / abstractions / source generator 都有明确站内入口”
|
||||
- 为 `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs` 与
|
||||
`GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中缺失的内部类型补齐 XML 注释
|
||||
- 基于轻量扫描确认:
|
||||
- `GFramework.Cqrs.Abstractions/Cqrs/` 当前类型声明级 XML 覆盖为 `20/20`
|
||||
- `GFramework.Cqrs` 根入口与 `Internal/` 已补到 `19/19`
|
||||
- `GFramework.Cqrs.SourceGenerators/Cqrs/` 当前类型声明级 XML 覆盖为 `3/3`
|
||||
|
||||
### 当前决策(RP-004)
|
||||
|
||||
- `docs/zh-CN/core/cqrs.md` 继续保留在 `Core` 栏目,但其角色调整为 `Cqrs` family landing,而不再只是 runtime 简介页
|
||||
- `Cqrs.SourceGenerators` 不单独新建一级导航栏目,而是在 `source-generators` 栏目内补一个专用专题页,保持站点 taxonomy 稳定
|
||||
- generator 入口以“专题页 + API reference 链接 + sidebar”三点联动,而不是只在 `source-generators/index.md` 留一个段落链接
|
||||
- XML inventory 仍维持“类型声明级基线”口径,不在本轮扩展成成员级 `param/returns/exception` 细审
|
||||
|
||||
### 当前验证(RP-004)
|
||||
|
||||
- 文档校验:
|
||||
- `validate-all.sh docs/zh-CN/core/cqrs.md`:通过
|
||||
- `validate-all.sh docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`:通过
|
||||
- 轻量 XML inventory:
|
||||
- `GFramework.Cqrs/Internal/`:`14/14`
|
||||
- `GFramework.Cqrs.Abstractions/Cqrs/`:`20/20`
|
||||
- `GFramework.Cqrs.SourceGenerators/Cqrs/`:`3/3`
|
||||
- 构建校验:
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -p:RestoreFallbackFolders=`:通过
|
||||
- `cd docs && bun run build`:通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`:失败;当前 WSL / dotnet 环境仍引用失效的 Windows fallback package folder,并在多目标 inner build 阶段触发 `MSB4276` / `MSB4018`
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 切换到 `Game` family 波次,按 `Core` / `Ecs` / `Cqrs` 已验证模板继续补 XML inventory 与教程链路
|
||||
2. 把 `GFramework.Cqrs` 的本地构建阻塞留给后续环境治理或构建脚本清理,不在本 topic 内扩张为环境修复任务
|
||||
|
||||
### 当前恢复点:RP-005
|
||||
|
||||
- 完成 `Game` 波次的模块族入口刷新:
|
||||
- 更新 `GFramework.Game/README.md`
|
||||
- 更新 `GFramework.Game.Abstractions/README.md`
|
||||
- 更新 `GFramework.Game.SourceGenerators/README.md`
|
||||
- 更新 `docs/zh-CN/game/index.md`
|
||||
- 重写 `docs/zh-CN/abstractions/game-abstractions.md`
|
||||
- 将 `Game` family 从“README / 页面存在但缺少可审计 XML 入口,且 abstractions 页失真”推进到“runtime / abstractions / source generator 都有声明级 XML inventory 与真实采用边界”
|
||||
- 基于轻量扫描确认:
|
||||
- `GFramework.Game` 当前类型声明级 XML 覆盖为 `56/56`
|
||||
- `GFramework.Game.Abstractions` 当前类型声明级 XML 覆盖为 `80/80`
|
||||
- `GFramework.Game.SourceGenerators` 当前类型声明级 XML 覆盖为 `2/2`
|
||||
|
||||
### 当前决策(RP-005)
|
||||
|
||||
- `docs/zh-CN/abstractions/game-abstractions.md` 不再维护虚构接口摘录,而是与源码中的 `Config` / `Data` / `Setting` / `Scene` / `UI` / `Routing` 契约分组保持一致
|
||||
- `Game.SourceGenerators` 继续以 `README + docs/zh-CN/game/config-system.md + docs/zh-CN/source-generators/index.md` 组成入口,不额外新增只为凑数量的专题页
|
||||
- `docs/zh-CN/game/index.md` 补 frontmatter,并承担 `Game` family 的 XML 基线入口;更细的类型族说明继续留在模块 README 与 abstractions 页
|
||||
|
||||
### 当前验证(RP-005)
|
||||
|
||||
- 文档校验:
|
||||
- `validate-all.sh docs/zh-CN/abstractions/game-abstractions.md`:通过
|
||||
- `validate-all.sh docs/zh-CN/game/index.md`:通过
|
||||
- 轻量 XML inventory:
|
||||
- `GFramework.Game`:`56/56`
|
||||
- `GFramework.Game.Abstractions`:`80/80`
|
||||
- `GFramework.Game.SourceGenerators`:`2/2`
|
||||
- 构建校验:
|
||||
- `cd docs && bun run build`:通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 进入 `Game` family 巡检,优先检查 `config-system.md`、`scene.md`、`ui.md` 与 `source-generators/index.md` 的交叉引用是否回漂
|
||||
2. 评估是否需要把 `Godot` family 的关键 XML inventory 摘要迁回 active topic,减少对 archive 的依赖
|
||||
|
||||
### 当前恢复点:RP-006
|
||||
|
||||
- 更新 `AGENTS.md` 的 WSL Git 规则:
|
||||
- 将显式 `git --git-dir=<...> --work-tree=<...>` 绑定提升为高于 `git.exe` 的默认优先级
|
||||
- 明确 plain Linux `git` 命中 worktree 路径翻译错误时,应先切到显式绑定而不是直接改用 `git.exe`
|
||||
- 明确 `git.exe` 只有在当前会话可执行时才作为次级 fallback
|
||||
- 记录本次恢复任务的环境偏差:
|
||||
- `git.exe` 在当前 WSL 会话中可解析,但执行会触发 `Exec format error`
|
||||
- plain `git` 会把 worktree 元数据路径翻译错并报“not a git repository”
|
||||
- 显式 `--git-dir` / `--work-tree` 绑定是本次已验证可用的 Git 操作方式
|
||||
|
||||
### 当前决策(RP-006)
|
||||
|
||||
- 把 Git 回退顺序写进 `AGENTS.md`,而不是只留在一次性的聊天上下文里
|
||||
- 不额外扩张 `gframework-boot` skill,因为它本身不内嵌 Git 选择逻辑,继续由 `AGENTS.md` 作为唯一准则
|
||||
- 继续把 `git.exe` 保留为 fallback,而不是完全删除,避免在可执行的 WSL 会话里丢掉可用路径
|
||||
|
||||
### 当前验证(RP-006)
|
||||
|
||||
- 构建校验:
|
||||
- `cd docs && bun run build`:通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 继续 `Game` family 巡检,优先检查 `config-system.md`、`scene.md`、`ui.md` 与 `source-generators/index.md` 的交叉引用是否回漂
|
||||
2. 评估是否需要把 `Godot` family 的关键 XML inventory 摘要迁回 active topic,减少对 archive 的依赖
|
||||
|
||||
### 当前恢复点:RP-007
|
||||
|
||||
- 完成 `Game` family 巡检:
|
||||
- 复核 `docs/zh-CN/game/config-system.md`
|
||||
- 复核 `docs/zh-CN/game/scene.md`
|
||||
- 复核 `docs/zh-CN/game/ui.md`
|
||||
- 复核 `docs/zh-CN/source-generators/index.md`
|
||||
- 对照 `GFramework.Game`、`GFramework.Game.Abstractions`、`GFramework.Game.SourceGenerators` README 与相关源码 / 测试后,未发现需要立刻修正的采用语义回漂
|
||||
- 重点确认的真实语义包括:
|
||||
- `GameConfigBootstrap` / `RegisterAllGeneratedConfigTables(...)` / `GFrameworkConfigSchemaDirectory` 的配置入口仍与文档示例一致
|
||||
- `SceneRouterBase` 仍通过 `SemaphoreSlim` 串行化切换,并拒绝重复 `sceneKey` 入栈
|
||||
- `UiRouterBase` 仍将 `Page` 层与 `Overlay` / `Modal` / `Toast` / `Topmost` 分为两套入口,且 `Show(..., UiLayer.Page)` 会直接拒绝
|
||||
|
||||
### 当前决策(RP-007)
|
||||
|
||||
- 本轮不为“巡检通过”硬造文档改动,先把结论写回 active topic,保持恢复点准确
|
||||
- `Game` family 暂时转入稳定巡检,不在没有源码变化的情况下重复改写 landing page
|
||||
- 默认下一步切到 `Godot` family 摘要是否回迁,减少长期治理对 archive topic 的依赖
|
||||
|
||||
### 当前验证(RP-007)
|
||||
|
||||
- 构建校验:
|
||||
- `cd docs && bun run build`:通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 评估是否需要把 `Godot` family 的关键 XML inventory 摘要迁回 active topic
|
||||
2. 若不需要迁回,则继续抽查 README / landing page / API reference 之间的 cross-link 是否出现新的漂移
|
||||
|
||||
### 当前恢复点:RP-008
|
||||
|
||||
- 使用 `$gframework-pr-review` 抓取当前分支 PR `#271` 后,确认 latest head review threads 仍有 `4` 条 open:
|
||||
- `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md` 的 marker 类型约定说明缺口
|
||||
- `docs/zh-CN/ecs/index.md` 的边界说明语序问题
|
||||
- `docs/zh-CN/abstractions/ecs-arch-abstractions.md` 误放的 source-generator 内部模块提醒
|
||||
- `ai-plan/public/documentation-full-coverage-governance/todos/documentation-full-coverage-governance-tracking.md` 的验证历史过长,以及
|
||||
`ai-plan/public/archive/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md` 缺少显式结果态
|
||||
- 在当前 WSL 会话里,`gframework-pr-review` 脚本先命中了 `git.exe` 的 `Exec format error`
|
||||
- 已将 `.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py` 改为优先使用 Linux `git` 的显式
|
||||
`--git-dir` / `--work-tree` 绑定,并仅在无法建立该绑定时回退到旧的可执行解析逻辑
|
||||
- 已同步更新 `.agents/skills/gframework-pr-review/SKILL.md`,使其 Git 策略与命令示例都与当前仓库状态一致
|
||||
- 已把 `DOCUMENTATION-FULL-COVERAGE-GOV-RP-001` 到 `RP-007` 的详细验证历史迁入
|
||||
`ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-validation-history-through-rp-007.md`
|
||||
|
||||
### 当前决策(RP-008)
|
||||
|
||||
- 继续把 latest-head unresolved threads 作为主信号,只修仍在本地成立的评论,不为已失效的历史 summary 做无意义回写
|
||||
- active tracking 只保留最新验证摘要与恢复点;详细验证历史留在 topic 自己的 archive,而不是继续堆在默认 boot 路径
|
||||
- `gframework-pr-review` 的脚本行为、技能文案与 `AGENTS.md` 必须保持同一套 WSL Git 策略,避免再次出现“文档说法正确但工具实现仍跑偏”的情况
|
||||
|
||||
### 当前验证(RP-008)
|
||||
|
||||
- PR review 抓取:
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json`:通过
|
||||
- 脚本语法校验:
|
||||
- `python3 -B -c "from pathlib import Path; compile(Path('.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py').read_text(encoding='utf-8'), '.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py', 'exec')"`:通过
|
||||
- 文档校验:
|
||||
- `validate-all.sh docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`:通过
|
||||
- `validate-all.sh docs/zh-CN/ecs/index.md`:通过
|
||||
- `validate-all.sh docs/zh-CN/abstractions/ecs-arch-abstractions.md`:通过
|
||||
- 构建校验:
|
||||
- `cd docs && bun run build`:通过;仅保留既有 VitePress 大 chunk warning,无构建失败
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 提交本轮 PR review follow-up
|
||||
2. 推送当前分支后重新执行 `$gframework-pr-review`,观察 PR #271 的 open threads 是否收敛
|
||||
@ -252,6 +252,7 @@ export default defineConfig({
|
||||
{ text: 'ContextAware 生成器', link: '/zh-CN/source-generators/context-aware-generator' },
|
||||
{ text: 'Priority 生成器', link: '/zh-CN/source-generators/priority-generator' },
|
||||
{ text: 'Context Get 注入', link: '/zh-CN/source-generators/context-get-generator' },
|
||||
{ text: 'CQRS Handler Registry', link: '/zh-CN/source-generators/cqrs-handler-registry-generator' },
|
||||
{ text: 'Godot 项目元数据', link: '/zh-CN/source-generators/godot-project-generator' },
|
||||
{ text: 'GetNode 生成器 (Godot)', link: '/zh-CN/source-generators/get-node-generator' },
|
||||
{ text: 'BindNodeSignal 生成器 (Godot)', link: '/zh-CN/source-generators/bind-node-signal-generator' }
|
||||
@ -264,7 +265,8 @@ export default defineConfig({
|
||||
text: '抽象接口',
|
||||
items: [
|
||||
{ text: 'Core Abstractions', link: '/zh-CN/abstractions/core-abstractions' },
|
||||
{ text: 'Game Abstractions', link: '/zh-CN/abstractions/game-abstractions' }
|
||||
{ text: 'Game Abstractions', link: '/zh-CN/abstractions/game-abstractions' },
|
||||
{ text: 'Ecs.Arch Abstractions', link: '/zh-CN/abstractions/ecs-arch-abstractions' }
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@ -1,192 +1,104 @@
|
||||
---
|
||||
title: Core Abstractions
|
||||
description: GFramework.Core.Abstractions 的契约边界、包关系与 XML 阅读重点。
|
||||
---
|
||||
|
||||
# Core Abstractions
|
||||
|
||||
> GFramework.Core.Abstractions 核心抽象接口定义
|
||||
`GFramework.Core.Abstractions` 是 `Core` 运行时的契约包。
|
||||
|
||||
## 概述
|
||||
它负责定义架构、生命周期、事件、状态、资源、日志、配置、并发和持久化相关的接口、枚举和值对象,用来建立跨模块协作边界;
|
||||
默认实现、基类、容器适配和运行时装配则在 `GFramework.Core` 中。
|
||||
|
||||
GFramework.Core.Abstractions 包含了框架的所有核心接口定义,这些接口定义了组件之间的契约,实现了依赖倒置和面向接口编程。
|
||||
如果你要开箱即用地使用框架能力,应依赖 `GFramework.Core`;如果你在做扩展包、测试替身、工具层或多模块拆分,才单独消费本包。
|
||||
|
||||
## 核心接口
|
||||
## 什么时候单独依赖它
|
||||
|
||||
### IArchitecture
|
||||
- 你在写插件、模块扩展或测试替身,只想依赖接口而不拉入默认运行时
|
||||
- 你需要让多个程序集共享架构、状态、资源或日志契约
|
||||
- 你希望把公共边界放进 `*.Abstractions`,而把具体实现留在应用层或宿主层
|
||||
|
||||
应用程序架构接口:
|
||||
## 包关系
|
||||
|
||||
- 契约层:`GFramework.Core.Abstractions`
|
||||
- 运行时实现:`GFramework.Core`
|
||||
- 常见相邻契约:`GFramework.Cqrs.Abstractions`、`GFramework.Game.Abstractions`
|
||||
|
||||
## 契约地图
|
||||
|
||||
| 契约族 | 作用 |
|
||||
| --- | --- |
|
||||
| `Architectures/` `Lifecycle/` `Registries/` | `IArchitecture`、上下文、模块、服务模块、阶段监听、注册表与初始化 / 销毁生命周期契约 |
|
||||
| `Bases/` `Controller/` `Model/` `Systems/` `Utility/` `Rule/` | 组件角色接口、优先级 / key 值对象、上下文感知边界 |
|
||||
| `Command/` `Query/` `Cqrs/` | 旧版命令 / 查询执行器接口,以及与新版请求模型衔接的运行时契约 |
|
||||
| `Events/` `Property/` `State/` `StateManagement/` | 事件总线、解绑对象、可绑定属性、状态机、Store / reducer / middleware 契约 |
|
||||
| `Coroutine/` `Time/` `Pause/` `Concurrency/` | 协程状态、时间源、暂停栈、键控异步锁与统计对象 |
|
||||
| `Resource/` `Pool/` `Logging/` `Localization/` | 资源句柄、对象池、日志、logger factory、本地化表与格式化契约 |
|
||||
| `Configuration/` `Environment/` | 配置管理器、环境对象与运行时环境访问契约 |
|
||||
| `Data/` `Serializer/` `Storage/` `Versioning/` | 数据装载、序列化、存储与版本化契约 |
|
||||
| `Enums/` `Properties/` | 架构阶段枚举,以及架构 / logger 相关属性键 |
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
### 1. 只面向契约编程
|
||||
|
||||
```csharp
|
||||
public interface IArchitecture
|
||||
{
|
||||
void Initialize();
|
||||
void Destroy();
|
||||
|
||||
T GetModel<T>() where T : IModel;
|
||||
T GetSystem<T>() where T : ISystem;
|
||||
T GetUtility<T>() where T : IUtility;
|
||||
|
||||
void RegisterModel(IModel model);
|
||||
void RegisterSystem(ISystem system);
|
||||
void RegisterUtility(IUtility utility);
|
||||
}
|
||||
```
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
|
||||
### IModel
|
||||
|
||||
数据模型接口:
|
||||
|
||||
```csharp
|
||||
public interface IModel
|
||||
{
|
||||
void Init();
|
||||
void Dispose();
|
||||
|
||||
IArchitecture Architecture { get; }
|
||||
}
|
||||
```
|
||||
|
||||
### ISystem
|
||||
|
||||
业务逻辑系统接口:
|
||||
|
||||
```csharp
|
||||
public interface ISystem
|
||||
{
|
||||
void Init();
|
||||
void Dispose();
|
||||
|
||||
IArchitecture Architecture { get; }
|
||||
}
|
||||
```
|
||||
|
||||
### IController
|
||||
|
||||
控制器接口:
|
||||
|
||||
```csharp
|
||||
public interface IController : IBelongToArchitecture
|
||||
{
|
||||
void Init();
|
||||
void Dispose();
|
||||
}
|
||||
```
|
||||
|
||||
### IUtility
|
||||
|
||||
工具类接口:
|
||||
|
||||
```csharp
|
||||
public interface IUtility
|
||||
{
|
||||
}
|
||||
```
|
||||
|
||||
## 事件接口
|
||||
|
||||
### IEvent
|
||||
|
||||
事件基接口:
|
||||
|
||||
```csharp
|
||||
public interface IEvent
|
||||
{
|
||||
}
|
||||
```
|
||||
|
||||
### IEventHandler
|
||||
|
||||
事件处理器接口:
|
||||
|
||||
```csharp
|
||||
public interface IEventHandler<TEvent> where TEvent : IEvent
|
||||
{
|
||||
void Handle(TEvent @event);
|
||||
}
|
||||
```
|
||||
|
||||
## 命令查询接口
|
||||
|
||||
### ICommand
|
||||
|
||||
命令接口:
|
||||
|
||||
```csharp
|
||||
public interface ICommand
|
||||
{
|
||||
void Execute();
|
||||
}
|
||||
```
|
||||
|
||||
### IQuery
|
||||
|
||||
查询接口:
|
||||
|
||||
```csharp
|
||||
public interface IQuery<TResult>
|
||||
{
|
||||
TResult Execute();
|
||||
}
|
||||
```
|
||||
|
||||
## 依赖注入接口
|
||||
|
||||
### IIocContainer
|
||||
|
||||
IoC 容器接口:
|
||||
|
||||
```csharp
|
||||
public interface IIocContainer
|
||||
{
|
||||
void Register<TInterface, TImplementation>() where TImplementation : TInterface;
|
||||
void Register<TInterface>(TInterface instance);
|
||||
TInterface Resolve<TInterface>();
|
||||
bool IsRegistered<TInterface>();
|
||||
}
|
||||
```
|
||||
|
||||
## 生命周期接口
|
||||
|
||||
### ILifecycle
|
||||
|
||||
组件生命周期接口:
|
||||
|
||||
```csharp
|
||||
public interface ILifecycle
|
||||
{
|
||||
void OnInit();
|
||||
void OnDestroy();
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 通过接口实现依赖注入
|
||||
|
||||
```csharp
|
||||
public class MyService : IMyService
|
||||
public sealed class DiagnosticsFeature
|
||||
{
|
||||
private readonly IArchitecture _architecture;
|
||||
|
||||
public MyService(IArchitecture architecture)
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public DiagnosticsFeature(IArchitecture architecture, ILogger logger)
|
||||
{
|
||||
_architecture = architecture;
|
||||
_logger = logger;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义事件
|
||||
### 2. 什么时候切到运行时包
|
||||
|
||||
```csharp
|
||||
public class PlayerDiedEvent : IEvent
|
||||
{
|
||||
public int PlayerId { get; set; }
|
||||
public Vector2 Position { get; set; }
|
||||
}
|
||||
```
|
||||
下面这些需求都属于 `GFramework.Core` 的职责,而不是本包:
|
||||
|
||||
---
|
||||
- 继承 `Architecture` 并完成默认初始化流程
|
||||
- 使用 `ContextAwareBase`、`AbstractModel`、`AbstractSystem` 等默认基类
|
||||
- 使用默认的 `CommandExecutor`、`QueryExecutor`、`BindableProperty<T>`、`StateMachine`
|
||||
- 直接启用默认的 `Microsoft.Extensions.DependencyInjection` 容器适配或资源 / 协程 / 日志实现
|
||||
|
||||
**相关文档**:
|
||||
## XML 阅读重点
|
||||
|
||||
- [Core 概述](../core/index.md)
|
||||
- [Architecture](../core/architecture)
|
||||
- [Events](../core/events)
|
||||
- [Command](../core/command)
|
||||
- [Query](../core/query)
|
||||
如果你在做契约审计、采用设计或扩展适配,优先核对这些类型族的 XML 文档:
|
||||
|
||||
- 架构与模块入口:`IArchitecture`、`IArchitectureContext`、`IServiceModule`
|
||||
- 运行时基础设施:`IIocContainer`、`ILogger`、`IResourceManager`、`IConfigurationManager`
|
||||
- 状态与并发能力:`IStateMachine`、`IStore`、`IAsyncKeyLockManager`、`ITimeProvider`
|
||||
- 迁移与组合边界:`ICommandExecutor`、`IQueryExecutor`、`ICqrsRuntime`
|
||||
|
||||
## XML 覆盖基线
|
||||
|
||||
下面这份 inventory 记录的是 `2026-04-22` 对 `GFramework.Core.Abstractions` 做的一轮轻量 XML 盘点结果:只统计公开 /
|
||||
内部类型声明是否带 XML 注释,用来建立契约层阅读入口;成员级参数、返回值、异常与生命周期说明仍需要后续波次继续细化。
|
||||
|
||||
| 契约族 | 基线状态 | 代表类型 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Architectures/` | `12/12` 个类型声明已带 XML 注释 | `IArchitecture`、`IArchitectureContext`、`IArchitectureServices`、`IServiceModule` | 看架构上下文、服务访问面与模块安装 / 生命周期约束 |
|
||||
| `Lifecycle/` `Registries/` | `8/8` 个类型声明已带 XML 注释 | `ILifecycle`、`IAsyncInitializable`、`IRegistry<T, TR>`、`KeyValueRegistryBase<TKey, TValue>` | 看初始化 / 销毁阶段和注册表抽象边界 |
|
||||
| `Command/` `Query/` `Cqrs/` | `10/10` 个类型声明已带 XML 注释 | `ICommandExecutor`、`IAsyncCommand<TResult>`、`IQueryExecutor`、`ICqrsRuntime` | 看旧命令 / 查询接口与新请求模型之间的兼容和迁移边界 |
|
||||
| `Events/` `Property/` | `10/10` 个类型声明已带 XML 注释 | `IEventBus`、`IEventFilter<T>`、`IBindableProperty<T>`、`IReadonlyBindableProperty<T>` | 看事件传播、过滤、解绑对象和属性订阅语义 |
|
||||
| `State/` `StateManagement/` | `15/15` 个类型声明已带 XML 注释 | `IStateMachine`、`IAsyncState`、`IStore<TState>`、`IStoreMiddleware<TState>` | 看状态机契约与 Store 的 reducer / middleware / diagnostics 边界 |
|
||||
| `Coroutine/` `Time/` `Pause/` `Concurrency/` | `17/17` 个类型声明已带 XML 注释 | `IYieldInstruction`、`ICoroutineStatistics`、`ITimeProvider`、`IPauseStackManager`、`IAsyncKeyLockManager` | 看调度模型、时间源、暂停栈和异步锁契约 |
|
||||
| `Resource/` `Pool/` `Logging/` `Localization/` | `27/27` 个类型声明已带 XML 注释 | `IResourceManager`、`IObjectPoolSystem`、`ILogger`、`IStructuredLogger`、`ILocalizationManager` | 看资源 / 池化 / 日志 / 本地化这些基础设施的宿主责任 |
|
||||
| `Configuration/` `Environment/` `Data/` `Serializer/` `Storage/` `Versioning/` | `7/7` 个类型声明已带 XML 注释 | `IConfigurationManager`、`IEnvironment`、`ILoadableFrom<T>`、`ISerializer`、`IStorage`、`IVersioned` | 看配置、环境、序列化和持久化边界,以及谁负责具体实现 |
|
||||
| `Bases/` `Controller/` `Model/` `Systems/` `Utility/` `Rule/` `Enums/` `Properties/` | `19/19` 个类型声明已带 XML 注释 | `IPrioritized`、`IController`、`IModel`、`ISystem`、`IContextUtility`、`ArchitecturePhase` | 看基础角色接口、辅助值对象和架构属性键的复用方式 |
|
||||
|
||||
## 阅读顺序
|
||||
|
||||
1. 先读本页,确认你是否真的只需要契约层
|
||||
2. 再看 [`../core/index.md`](../core/index.md) 了解默认运行时怎么组织这些契约
|
||||
3. 回到模块 README:
|
||||
- `GFramework.Core.Abstractions/README.md`
|
||||
- `GFramework.Core/README.md`
|
||||
4. 需要统一导航时,再看 [`../api-reference/index.md`](../api-reference/index.md)
|
||||
|
||||
94
docs/zh-CN/abstractions/ecs-arch-abstractions.md
Normal file
94
docs/zh-CN/abstractions/ecs-arch-abstractions.md
Normal file
@ -0,0 +1,94 @@
|
||||
---
|
||||
title: Ecs.Arch Abstractions
|
||||
description: GFramework.Ecs.Arch.Abstractions 的契约边界、包关系和最小接入路径。
|
||||
---
|
||||
|
||||
# Ecs.Arch Abstractions
|
||||
|
||||
`GFramework.Ecs.Arch.Abstractions` 是 Arch ECS 集成层的契约包。
|
||||
|
||||
它建立在 `GFramework.Core.Abstractions` 之上,只定义 ECS 模块更新、系统适配和配置对象,不负责默认的 Arch
|
||||
`World` 装配、扩展方法或系统基类。
|
||||
|
||||
如果你需要开箱即用的集成实现,请改为依赖 `GFramework.Ecs.Arch`。
|
||||
|
||||
## 什么时候单独依赖它
|
||||
|
||||
- 你在做共享宿主循环、工具层或 feature 包,只需要 `IArchEcsModule`
|
||||
- 你想让不同程序集共享 `ArchOptions` 或系统适配契约,但不直接绑定默认 runtime
|
||||
- 你需要为测试或外部适配层提供替身实现
|
||||
|
||||
## 包关系
|
||||
|
||||
- 契约层:`GFramework.Ecs.Arch.Abstractions`
|
||||
- 运行时实现:`GFramework.Ecs.Arch`
|
||||
- 底层基础契约:`GFramework.Core.Abstractions`
|
||||
|
||||
## 契约地图
|
||||
|
||||
| 类型 | 作用 |
|
||||
| --- | --- |
|
||||
| `IArchEcsModule` | 统一更新 ECS 系统的服务模块契约 |
|
||||
| `IArchSystemAdapter<T>` | 让 ECS 系统适配到 `ISystem` 生命周期 |
|
||||
| `ArchOptions` | 承载 `WorldCapacity`、`EnableStatistics`、`Priority` 等配置 |
|
||||
|
||||
## 类型族级 XML Inventory
|
||||
|
||||
| 类型族 | 代表类型 | XML 状态 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| 模块契约 | `IArchEcsModule` | 已覆盖 | 统一更新入口、宿主循环边界 |
|
||||
| 系统契约 | `IArchSystemAdapter<T>` | 已覆盖 | 只依赖更新接口而不绑定默认 runtime |
|
||||
| 配置对象 | `ArchOptions` | 已覆盖 | 共享配置字段与跨程序集采用边界 |
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
### 1. 共享模块只依赖更新契约
|
||||
|
||||
```csharp
|
||||
using GFramework.Ecs.Arch.Abstractions;
|
||||
|
||||
public sealed class GameplayHost
|
||||
{
|
||||
private readonly IArchEcsModule _ecsModule;
|
||||
|
||||
public GameplayHost(IArchEcsModule ecsModule)
|
||||
{
|
||||
_ecsModule = ecsModule;
|
||||
}
|
||||
|
||||
public void Tick(float deltaTime)
|
||||
{
|
||||
_ecsModule.Update(deltaTime);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 共享配置对象
|
||||
|
||||
```csharp
|
||||
using GFramework.Ecs.Arch.Abstractions;
|
||||
|
||||
var options = new ArchOptions
|
||||
{
|
||||
WorldCapacity = 2048,
|
||||
EnableStatistics = true,
|
||||
Priority = 40
|
||||
};
|
||||
```
|
||||
|
||||
### 3. 什么时候切到运行时包
|
||||
|
||||
下面这些需求都属于 `GFramework.Ecs.Arch` 的职责,而不是本包:
|
||||
|
||||
- 通过 `UseArch(...)` 把模块挂进架构
|
||||
- 使用默认的 `ArchSystemAdapter<T>` 基类
|
||||
- 访问 Arch `World` 与查询 API
|
||||
- 使用默认的模块装配和生命周期实现
|
||||
|
||||
## 阅读顺序
|
||||
|
||||
1. 先读本页,确认你是否真的只需要契约层
|
||||
2. 如果需要默认实现,再看 [`../ecs/arch.md`](../ecs/arch.md)
|
||||
3. 回到对应模块 README:
|
||||
- `GFramework.Ecs.Arch.Abstractions/README.md`
|
||||
- `GFramework.Ecs.Arch/README.md`
|
||||
@ -1,94 +1,122 @@
|
||||
# Game Abstractions
|
||||
|
||||
> GFramework.Game.Abstractions 游戏模块抽象接口定义
|
||||
|
||||
## 概述
|
||||
|
||||
GFramework.Game.Abstractions 包含了游戏特定功能的抽象接口,这些接口定义了游戏开发中的通用契约。
|
||||
|
||||
## 存档接口
|
||||
|
||||
### ISaveSystem
|
||||
|
||||
存档系统接口:
|
||||
|
||||
```csharp
|
||||
public interface ISaveSystem
|
||||
{
|
||||
void Save(string slotId, SaveData data);
|
||||
SaveData Load(string slotId);
|
||||
bool HasSave(string slotId);
|
||||
void Delete(string slotId);
|
||||
List<SaveSlotInfo> GetAllSaveSlots();
|
||||
}
|
||||
```
|
||||
|
||||
### ISaveData
|
||||
|
||||
存档数据接口:
|
||||
|
||||
```csharp
|
||||
public interface ISaveData
|
||||
{
|
||||
int Version { get; }
|
||||
DateTime Timestamp { get; }
|
||||
void Validate();
|
||||
}
|
||||
```
|
||||
|
||||
## 设置接口
|
||||
|
||||
### IGameSettings
|
||||
|
||||
游戏设置接口:
|
||||
|
||||
```csharp
|
||||
public interface IGameSettings
|
||||
{
|
||||
AudioSettings Audio { get; }
|
||||
GraphicsSettings Graphics { get; }
|
||||
InputSettings Input { get; }
|
||||
|
||||
void Save();
|
||||
void Load();
|
||||
void ResetToDefaults();
|
||||
}
|
||||
```
|
||||
|
||||
## 场景管理接口
|
||||
|
||||
### ISceneManager
|
||||
|
||||
场景管理器接口:
|
||||
|
||||
```csharp
|
||||
public interface ISceneManager
|
||||
{
|
||||
void SwitchScene<TScene>() where TScene : IScene;
|
||||
Task SwitchSceneAsync<TScene>() where TScene : IScene;
|
||||
|
||||
void PushScene<TScene>() where TScene : IScene;
|
||||
void PopScene();
|
||||
|
||||
IScene CurrentScene { get; }
|
||||
}
|
||||
```
|
||||
|
||||
### IScene
|
||||
|
||||
场景接口:
|
||||
|
||||
```csharp
|
||||
public interface IScene
|
||||
{
|
||||
void OnEnter();
|
||||
void OnExit();
|
||||
void OnUpdate(float delta);
|
||||
}
|
||||
```
|
||||
---
|
||||
title: Game Abstractions
|
||||
description: GFramework.Game.Abstractions 的契约边界、包关系与 XML 阅读重点。
|
||||
---
|
||||
|
||||
**相关文档**:
|
||||
# Game Abstractions
|
||||
|
||||
- [Game 概述](../game/index.md)
|
||||
- [核心抽象](./core-abstractions.md)
|
||||
`GFramework.Game.Abstractions` 是 `Game` 运行时的契约包。
|
||||
|
||||
它建立在 `GFramework.Core.Abstractions` 之上,负责定义配置、数据、设置、场景、UI、路由、存储和资源注册表相关的接口、
|
||||
枚举与事件契约;默认实现、路由基类、YAML 加载器、文件存储和设置 / 存档仓库则在 `GFramework.Game` 中。
|
||||
|
||||
如果你要开箱即用地接入游戏运行时能力,应依赖 `GFramework.Game`;如果你在做共享业务层、feature 包、测试替身或引擎适配层,
|
||||
才单独消费本包。
|
||||
|
||||
## 什么时候单独依赖它
|
||||
|
||||
- 你希望公共业务层只依赖 `ISceneRouter`、`IUiRouter`、`ISettingsSystem`、`ISaveRepository<TSaveData>` 这类契约
|
||||
- 你要让多个程序集共享 `ISettingsData`、`IData`、`ISceneEnterParam`、`IUiPageEnterParam` 等数据和路由上下文
|
||||
- 你需要自己实现 factory、root、存储或配置加载器,但不想把默认运行时一起带进来
|
||||
|
||||
## 包关系
|
||||
|
||||
- 契约层:`GFramework.Game.Abstractions`
|
||||
- 运行时实现:`GFramework.Game`
|
||||
- 底层基础契约:`GFramework.Core.Abstractions`
|
||||
|
||||
## 契约地图
|
||||
|
||||
| 契约族 | 作用 |
|
||||
| --- | --- |
|
||||
| `Config/` | `IConfigLoader`、`IConfigRegistry`、`IConfigTable<TKey, TValue>` 和配置失败诊断模型 |
|
||||
| `Data/` | `IData`、`IVersionedData`、`IDataRepository`、`ISettingsDataRepository`、`ISaveRepository<TSaveData>` 及数据事件 |
|
||||
| `Setting/` | `ISettingsData`、`ISettingsModel`、`ISettingsSystem`、设置迁移契约与内置设置数据类型 |
|
||||
| `Scene/` | `IScene`、`ISceneRouter`、`ISceneFactory`、`ISceneRoot`、转场处理器与事件 |
|
||||
| `UI/` | `IUiPage`、`IUiRouter`、`IUiFactory`、`IUiRoot`、交互配置与 UI 转场选项 |
|
||||
| `Routing/` | `IRoute`、`IRouteContext`、`IRouteGuard<TRoute>`,作为 Scene / UI 共享的路由基础约定 |
|
||||
| `Storage/` `Asset/` `Enums/` | 文件存储角色、资源注册表,以及转场 / UI 层级 / 输入动作等跨层枚举 |
|
||||
|
||||
## XML 覆盖基线
|
||||
|
||||
下面这份 inventory 记录的是 `2026-04-23` 对 `GFramework.Game.Abstractions` 做的一轮轻量 XML 盘点结果:只统计公开 /
|
||||
内部类型声明是否带 XML 注释,用来建立契约层阅读入口;成员级参数、返回值、异常和生命周期说明仍需要后续 API 波次继续细化。
|
||||
|
||||
| 契约族 | 基线状态 | 代表类型 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Config/` | `7/7` 个类型声明已带 XML 注释 | `IConfigLoader`、`IConfigRegistry`、`IConfigTable<TKey, TValue>`、`ConfigLoadException` | 看配置表注册、只读访问和失败诊断边界 |
|
||||
| `Data/` | `14/14` 个类型声明已带 XML 注释 | `IDataRepository`、`ISettingsDataRepository`、`ISaveRepository<TSaveData>`、`DataRepositoryOptions` | 看业务数据、统一设置文件、槽位存档与迁移契约 |
|
||||
| `Setting/` | `12/12` 个类型声明已带 XML 注释 | `ISettingsData`、`ISettingsModel`、`ISettingsSystem`、`LocalizationSettings` | 看设置生命周期、应用语义、迁移接口和内置设置对象 |
|
||||
| `Scene/` | `14/14` 个类型声明已带 XML 注释 | `IScene`、`ISceneRouter`、`ISceneFactory`、`SceneTransitionEvent` | 看场景行为、工厂 / root 边界和转场模型 |
|
||||
| `UI/` | `19/19` 个类型声明已带 XML 注释 | `IUiPage`、`IUiRouter`、`IUiFactory`、`UiInteractionProfile`、`UiTransitionHandlerOptions` | 看页面栈、层级 UI、输入动作和 UI 转场契约 |
|
||||
| `Routing/` `Storage/` `Asset/` `Enums/` | `13/13` 个类型声明已带 XML 注释 | `IRoute`、`IRouteContext`、`IFileStorage`、`IAssetRegistry<T>`、`UiLayer`、`SceneTransitionType` | 看公共路由上下文、存储角色、资源注册表与共享枚举 |
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
### 1. 只在公共业务层声明游戏对象
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Abstractions.Data;
|
||||
using GFramework.Game.Abstractions.Scene;
|
||||
using GFramework.Game.Abstractions.UI;
|
||||
|
||||
public sealed class GameSaveData : IVersionedData
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
public DateTime LastModified { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class GameplayEnterParam : ISceneEnterParam
|
||||
{
|
||||
public required string Seed { get; init; }
|
||||
}
|
||||
|
||||
public sealed class PauseMenuParam : IUiPageEnterParam
|
||||
{
|
||||
public bool AllowRestart { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 让 feature 包只依赖抽象
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Abstractions.Data;
|
||||
using GFramework.Game.Abstractions.Scene;
|
||||
|
||||
public sealed class ContinueGameCommandHandler
|
||||
{
|
||||
private readonly ISaveRepository<GameSaveData> _saveRepository;
|
||||
private readonly ISceneRouter _sceneRouter;
|
||||
|
||||
public ContinueGameCommandHandler(
|
||||
ISaveRepository<GameSaveData> saveRepository,
|
||||
ISceneRouter sceneRouter)
|
||||
{
|
||||
_saveRepository = saveRepository;
|
||||
_sceneRouter = sceneRouter;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 什么时候切到运行时包
|
||||
|
||||
下面这些需求都属于 `GFramework.Game` 的职责,而不是本包:
|
||||
|
||||
- 使用默认的 `JsonSerializer`、`FileStorage` 或 `ScopedStorage`
|
||||
- 使用 `SettingsModel<TRepository>`、`SettingsSystem`、`SaveRepository<TSaveData>` 等默认实现
|
||||
- 使用 `YamlConfigLoader`、`GameConfigBootstrap`、`GameConfigModule`
|
||||
- 继承 `SceneRouterBase`、`UiRouterBase` 或默认转场处理器基类
|
||||
|
||||
## 阅读顺序
|
||||
|
||||
1. 先读本页,确认你是否真的只需要契约层
|
||||
2. 再看 [`../game/index.md`](../game/index.md) 了解默认运行时怎么组织这些契约
|
||||
3. 继续读具体专题页:
|
||||
- [`../game/config-system.md`](../game/config-system.md)
|
||||
- [`../game/data.md`](../game/data.md)
|
||||
- [`../game/setting.md`](../game/setting.md)
|
||||
- [`../game/scene.md`](../game/scene.md)
|
||||
- [`../game/ui.md`](../game/ui.md)
|
||||
4. 需要仓库侧入口时,回到:
|
||||
- `GFramework.Game.Abstractions/README.md`
|
||||
- `GFramework.Game/README.md`
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
---
|
||||
title: 抽象接口
|
||||
description: GFramework 各抽象层模块的阅读入口与使用边界。
|
||||
---
|
||||
|
||||
# 抽象接口
|
||||
|
||||
`GFramework.*.Abstractions` 用来承载跨模块协作所需的契约,而不是运行时实现。
|
||||
@ -12,9 +17,19 @@
|
||||
|
||||
- Core 抽象层:[`core-abstractions.md`](./core-abstractions.md)
|
||||
- Game 抽象层:[`game-abstractions.md`](./game-abstractions.md)
|
||||
- Ecs.Arch 抽象层:[`ecs-arch-abstractions.md`](./ecs-arch-abstractions.md)
|
||||
|
||||
## 使用建议
|
||||
|
||||
- 如果你只是想直接使用框架功能,优先从对应运行时模块的 `README.md` 和栏目页开始。
|
||||
- 只有在明确需要“契约层而非实现层”时,才单独依赖 `*.Abstractions` 包。
|
||||
- 抽象层页面会解释接口分组与职责;实际安装与接入路径,仍应以运行时模块 README 与 `getting-started` 为主。
|
||||
- 抽象层页面会解释接口分组与职责;实际安装与接入路径,仍应以运行时模块 README 与 `getting-started` 为主。
|
||||
|
||||
## 当前边界
|
||||
|
||||
- `GFramework.Core.SourceGenerators.Abstractions`
|
||||
- `GFramework.Godot.SourceGenerators.Abstractions`
|
||||
- `GFramework.SourceGenerators.Common`
|
||||
|
||||
这些目录当前不作为独立抽象接口栏目维护,而是作为源码生成器家族的内部支撑模块,分别跟随所属模块 README 和
|
||||
`source-generators` 栏目维护。
|
||||
|
||||
@ -1,571 +1,81 @@
|
||||
# API 参考文档
|
||||
|
||||
本文档提供 GFramework 各模块的完整 API 参考。
|
||||
|
||||
## 核心命名空间
|
||||
|
||||
### GFramework.Core.architecture
|
||||
|
||||
核心架构命名空间,包含所有基础组件。
|
||||
|
||||
#### 主要类型
|
||||
|
||||
| 类型 | 说明 |
|
||||
|--------------------|--------|
|
||||
| `Architecture` | 应用架构基类 |
|
||||
| `AbstractModel` | 数据模型基类 |
|
||||
| `AbstractSystem` | 业务系统基类 |
|
||||
| `AbstractCommand` | 命令基类 |
|
||||
| `AbstractQuery<T>` | 查询基类 |
|
||||
| `IController` | 控制器接口 |
|
||||
| `IUtility` | 工具类接口 |
|
||||
|
||||
### GFramework.Core.events
|
||||
|
||||
事件系统命名空间。
|
||||
|
||||
#### 主要类型
|
||||
|
||||
| 类型 | 说明 |
|
||||
|-------------------|----------|
|
||||
| `IEvent` | 事件接口 |
|
||||
| `IEventSystem` | 事件系统接口 |
|
||||
| `TypeEventSystem` | 类型安全事件系统 |
|
||||
|
||||
### GFramework.Core.property
|
||||
|
||||
属性系统命名空间。
|
||||
|
||||
#### 主要类型
|
||||
|
||||
| 类型 | 说明 |
|
||||
|-----------------------|--------|
|
||||
| `BindableProperty<T>` | 可绑定属性 |
|
||||
| `IUnRegister` | 注销接口 |
|
||||
| `IUnRegisterList` | 注销列表接口 |
|
||||
|
||||
### GFramework.Core.ioc
|
||||
|
||||
IoC 容器命名空间。
|
||||
|
||||
#### 主要类型
|
||||
|
||||
| 类型 | 说明 |
|
||||
|--------------|------|
|
||||
| `IContainer` | 容器接口 |
|
||||
| `Container` | 容器实现 |
|
||||
|
||||
### GFramework.Core.pool
|
||||
|
||||
对象池命名空间。
|
||||
|
||||
#### 主要类型
|
||||
|
||||
| 类型 | 说明 |
|
||||
|------------------|-------|
|
||||
| `IObjectPool<T>` | 对象池接口 |
|
||||
| `ObjectPool<T>` | 对象池实现 |
|
||||
|
||||
### GFramework.Core.Localization
|
||||
|
||||
本地化系统命名空间。
|
||||
|
||||
#### 主要类型
|
||||
|
||||
| 类型 | 说明 |
|
||||
|--------------------------|----------|
|
||||
| `ILocalizationManager` | 本地化管理器接口 |
|
||||
| `ILocalizationTable` | 本地化表接口 |
|
||||
| `ILocalizationString` | 本地化字符串接口 |
|
||||
| `ILocalizationFormatter` | 格式化器接口 |
|
||||
| `LocalizationConfig` | 本地化配置类 |
|
||||
| `LocalizationManager` | 本地化管理器实现 |
|
||||
| `LocalizationTable` | 本地化表实现 |
|
||||
| `LocalizationString` | 本地化字符串实现 |
|
||||
|
||||
## 常用 API
|
||||
|
||||
### Architecture
|
||||
|
||||
```csharp
|
||||
public abstract class Architecture : IBelongToArchitecture
|
||||
{
|
||||
// 初始化架构
|
||||
public void Initialize();
|
||||
|
||||
// 销毁架构
|
||||
public void Destroy();
|
||||
|
||||
// 注册模型
|
||||
public void RegisterModel<T>(T model) where T : IModel;
|
||||
|
||||
// 获取模型
|
||||
public T GetModel<T>() where T : IModel;
|
||||
|
||||
// 注册系统
|
||||
public void RegisterSystem<T>(T system) where T : ISystem;
|
||||
|
||||
// 获取系统
|
||||
public T GetSystem<T>() where T : ISystem;
|
||||
|
||||
// 注册工具
|
||||
public void RegisterUtility<T>(T utility) where T : IUtility;
|
||||
|
||||
// 获取工具
|
||||
public T GetUt>() where T : IUtility;
|
||||
|
||||
// 发送命令
|
||||
public void SendCommand<T>(T command) where T : ICommand;
|
||||
|
||||
// 发送查询
|
||||
public TResult SendQuery<TQuery, TResult>(TQuery query)
|
||||
where TQuery : IQuery<TResult>;
|
||||
|
||||
// 发送事件
|
||||
public void SendEvent<T>(T e) where T : IEvent;
|
||||
}
|
||||
```
|
||||
|
||||
### AbstractModel
|
||||
|
||||
```csharp
|
||||
public abstract class AbstractModel : IBelongToArchitecture
|
||||
{
|
||||
// 初始化模型
|
||||
protected abstract void OnInit();
|
||||
|
||||
// 销毁模型
|
||||
protected virtual void OnDestroy();
|
||||
|
||||
// 获取架构
|
||||
public IArchitecture GetArchitecture();
|
||||
|
||||
// 发送事件
|
||||
protected void SendEvent<T>(T e) where T : IEvent;
|
||||
|
||||
// 获取模型
|
||||
protected T GetModel<T>() where T : IModel;
|
||||
|
||||
// 获取系统
|
||||
protected T GetSystem<T>() where T : ISystem;
|
||||
|
||||
// 获取工具
|
||||
protected T GetUtility<T>() where T : IUtility;
|
||||
}
|
||||
```
|
||||
|
||||
### AbstractSystem
|
||||
|
||||
```csharp
|
||||
public abstract class AbstractSystem : IBelongToArchitecture
|
||||
{
|
||||
// 初始化系统
|
||||
protected abstract void OnInit();
|
||||
|
||||
// 销毁系统
|
||||
protected virtual void OnDestroy();
|
||||
|
||||
// 获取架构
|
||||
public IArchitecture GetArchitecture();
|
||||
|
||||
// 发送事件
|
||||
protected void SendEvent<T>(T e) where T : IEvent;
|
||||
|
||||
// 注册事件
|
||||
protected IUnRegister RegisterEvent<T>(Action<T> onEvent)
|
||||
where T : IEvent;
|
||||
|
||||
// 获取模型
|
||||
protected T GetModel<T>() where T : IModel;
|
||||
|
||||
// 获取系统
|
||||
protected T GetSystem<T>() where T : ISystem;
|
||||
|
||||
// 获取工具
|
||||
protected T GetUtility<T>() where T : IUtility;
|
||||
}
|
||||
```
|
||||
|
||||
### AbstractCommand
|
||||
|
||||
```csharp
|
||||
public abstract class AbstractCommand : IBelongToArchitecture
|
||||
{
|
||||
// 执行命令
|
||||
public void Execute();
|
||||
|
||||
// 命令实现
|
||||
protected abstract void OnDo();
|
||||
|
||||
// 获取架构
|
||||
public IArchitecture GetArchitecture();
|
||||
|
||||
// 发送事件
|
||||
protected void SendEvent<T>(T e) where T : IEvent;
|
||||
|
||||
// 获取模型
|
||||
protected T GetModel<T>() where T : IModel;
|
||||
|
||||
// 获取系统
|
||||
protected T GetSystem<T>() where T : ISystem;
|
||||
|
||||
// 获取工具
|
||||
protected T GetUtility<T>() where T : IUtility;
|
||||
}
|
||||
```
|
||||
|
||||
### AbstractQuery`<T>`
|
||||
|
||||
```csharp
|
||||
public abstract class AbstractQuery<T> : IBelongToArchitecture
|
||||
{
|
||||
// 执行查询
|
||||
public T Do();
|
||||
|
||||
// 查询实现
|
||||
protected abstract T OnDo();
|
||||
|
||||
// 获取架构
|
||||
public IArchitecture GetArchitecture();
|
||||
|
||||
// 获取模型
|
||||
protected T GetModel<T>() where T : IModel;
|
||||
|
||||
// 获取系统
|
||||
protected T GetSystem<T>() where T : ISystem;
|
||||
|
||||
// 获取工具
|
||||
protected T GetUtility<T>() where T : IUtility;
|
||||
}
|
||||
```
|
||||
|
||||
### BindableProperty`<T>`
|
||||
|
||||
```csharp
|
||||
public class BindableProperty<T>
|
||||
{
|
||||
// 构造函数
|
||||
public BindableProperty(T initialValue = default);
|
||||
|
||||
// 获取或设置值
|
||||
public T Value { get; set; }
|
||||
|
||||
// 注册监听器
|
||||
public IUnRegister Register(Action<T> onValueChanged);
|
||||
|
||||
// 注册监听器(包含初始值)
|
||||
public IUnRegister RegisterWithInitValue(Action<T> onValueChanged);
|
||||
|
||||
// 获取当前值
|
||||
public T GetValue();
|
||||
|
||||
// 设置值
|
||||
public void SetValue(T newValue);
|
||||
}
|
||||
```
|
||||
|
||||
### ILocalizationManager
|
||||
|
||||
```csharp
|
||||
public interface ILocalizationManager : ISystem
|
||||
{
|
||||
// 获取当前语言代码
|
||||
string CurrentLanguage { get; }
|
||||
|
||||
// 获取当前文化信息
|
||||
CultureInfo CurrentCulture { get; }
|
||||
|
||||
// 获取可用语言列表
|
||||
IReadOnlyList<string> AvailableLanguages { get; }
|
||||
|
||||
// 设置当前语言
|
||||
void SetLanguage(string languageCode);
|
||||
|
||||
// 获取本地化表
|
||||
ILocalizationTable GetTable(string tableName);
|
||||
|
||||
// 获取本地化文本
|
||||
string GetText(string table, string key);
|
||||
|
||||
// 获取本地化字符串(支持变量)
|
||||
ILocalizationString GetString(string table, string key);
|
||||
|
||||
// 尝试获取本地化文本
|
||||
bool TryGetText(string table, string key, out string text);
|
||||
|
||||
// 注册格式化器
|
||||
void RegisterFormatter(string name, ILocalizationFormatter formatter);
|
||||
|
||||
// 订阅语言变化事件
|
||||
void SubscribeToLanguageChange(Action<string> callback);
|
||||
|
||||
// 取消订阅语言变化事件
|
||||
void UnsubscribeFromLanguageChange(Action<string> callback);
|
||||
}
|
||||
```
|
||||
|
||||
### ILocalizationString
|
||||
|
||||
```csharp
|
||||
public interface ILocalizationString
|
||||
{
|
||||
// 获取表名
|
||||
string Table { get; }
|
||||
|
||||
// 获取键名
|
||||
string Key { get; }
|
||||
|
||||
// 添加变量
|
||||
ILocalizationString WithVariable(string name, object value);
|
||||
|
||||
// 批量添加变量
|
||||
ILocalizationString WithVariables(params (string name, object value)[] variables);
|
||||
|
||||
// 格式化并返回文本
|
||||
string Format();
|
||||
|
||||
// 获取原始文本
|
||||
string GetRaw();
|
||||
|
||||
// 检查键是否存在
|
||||
bool Exists();
|
||||
}
|
||||
```
|
||||
|
||||
### LocalizationConfig
|
||||
|
||||
```csharp
|
||||
public class LocalizationConfig
|
||||
{
|
||||
// 默认语言代码
|
||||
public string DefaultLanguage { get; set; } = "eng";
|
||||
|
||||
// 回退语言代码
|
||||
public string FallbackLanguage { get; set; } = "eng";
|
||||
|
||||
// 本地化文件路径
|
||||
public string LocalizationPath { get; set; } = "res://localization";
|
||||
|
||||
// 用户覆盖路径
|
||||
public string OverridePath { get; set; } = "user://localization_override";
|
||||
|
||||
// 是否启用热重载
|
||||
public bool EnableHotReload { get; set; } = true;
|
||||
|
||||
// 是否在加载时验证
|
||||
public bool ValidateOnLoad { get; set; } = true;
|
||||
}
|
||||
```
|
||||
|
||||
## 扩展方法
|
||||
|
||||
### 架构扩展
|
||||
|
||||
```csharp
|
||||
// 发送命令
|
||||
public static void SendCommand<T>(this IBelongToArchitecture self, T command)
|
||||
where T : ICommand;
|
||||
|
||||
// 发送查询
|
||||
public static TResult SendQuery<TQuery, TResult>(
|
||||
this IBelongToArchitecture self, TQuery query)
|
||||
where TQuery : IQuery<TResult>;
|
||||
|
||||
// 发送事件
|
||||
public static void SendEvent<T>(this IBelongToArchitecture self, T e)
|
||||
where T : IEvent;
|
||||
|
||||
// 获取模型
|
||||
public static T GetModel<T>(this IBelongToArchitecture self)
|
||||
where T : IModel;
|
||||
|
||||
// 获取系统
|
||||
public static T GetSystem<T>(this IBelongToArchitecture self)
|
||||
where T : ISystem;
|
||||
|
||||
// 获取工具
|
||||
public static T GetUtility<T>(this IBelongToArchitecture self)
|
||||
where T : IUtility;
|
||||
|
||||
// 注册事件
|
||||
public static IUnRegister RegisterEvent<T>(
|
||||
this IBelongToArchitecture self, Action<T> onEvent)
|
||||
where T : IEvent;
|
||||
```
|
||||
|
||||
### 属性扩展
|
||||
|
||||
```csharp
|
||||
// 添加到注销列表
|
||||
public static IUnRegister AddToUnregisterList(
|
||||
this IUnRegister self, IUnRegisterList list);
|
||||
|
||||
// 注销所有
|
||||
public static void UnRegisterAll(this IUnRegisterList self);
|
||||
```
|
||||
|
||||
## 游戏模块 API
|
||||
|
||||
### GFramework.Game
|
||||
|
||||
游戏业务扩展模块。
|
||||
|
||||
#### 主要类型
|
||||
|
||||
| 类型 | 说明 |
|
||||
|---------------|--------|
|
||||
| `GameSetting` | 游戏设置 |
|
||||
| `GameState` | 游戏状态 |
|
||||
| `IGameModule` | 游戏模块接口 |
|
||||
|
||||
## Godot 集成 API
|
||||
|
||||
### GFramework.Godot
|
||||
|
||||
Godot 引擎集成模块。
|
||||
|
||||
#### 主要类型
|
||||
|
||||
| 类型 | 说明 |
|
||||
|------------------|------------|
|
||||
| `GodotNode` | Godot 节点扩展 |
|
||||
| `GodotCoroutine` | Godot 协程 |
|
||||
| `GodotSignal` | Godot 信号 |
|
||||
|
||||
## 源码生成器
|
||||
|
||||
### Source Generators 家族
|
||||
|
||||
自动代码生成工具按模块拆分为 `GFramework.Core.SourceGenerators`、`GFramework.Game.SourceGenerators`、
|
||||
`GFramework.Godot.SourceGenerators` 与 `GFramework.Cqrs.SourceGenerators`。面向业务代码声明的 Attribute
|
||||
主要来自 `GFramework.Core.SourceGenerators.Abstractions.*` 与对应模块的 runtime/generator 包。
|
||||
|
||||
#### 支持的生成器
|
||||
|
||||
| 生成器 | 说明 |
|
||||
|--------------------------------------------|-------------|
|
||||
| `LoggingGenerator` | 日志生成器 |
|
||||
| `EnumGenerator` | 枚举扩展生成器 |
|
||||
| `RuleGenerator` | 规则生成器 |
|
||||
| `AutoRegisterModuleGenerator` | 架构模块注册生成器 |
|
||||
| `AutoUiPageGenerator` | UI 页面行为生成器 |
|
||||
| `AutoSceneGenerator` | 场景行为生成器 |
|
||||
| `AutoRegisterExportedCollectionsGenerator` | 导出集合批量注册生成器 |
|
||||
|
||||
#### 常用 Attribute
|
||||
|
||||
| Attribute | 说明 | 文档 |
|
||||
|--------------------------------------------|-------------------------------------------|-------------------------------------------------------------------------------------------------------------|
|
||||
| `AutoRegisterModuleAttribute` | 为模块类生成 `Install(IArchitecture)` | [AutoRegisterModule 生成器](../source-generators/auto-register-module-generator.md) |
|
||||
| `RegisterModelAttribute` | 声明模块内自动注册的 `IModel` 类型 | [AutoRegisterModule 生成器](../source-generators/auto-register-module-generator.md) |
|
||||
| `RegisterSystemAttribute` | 声明模块内自动注册的 `ISystem` 类型 | [AutoRegisterModule 生成器](../source-generators/auto-register-module-generator.md) |
|
||||
| `RegisterUtilityAttribute` | 声明模块内自动注册的 `IUtility` 类型 | [AutoRegisterModule 生成器](../source-generators/auto-register-module-generator.md) |
|
||||
| `AutoUiPageAttribute` | 为 `CanvasItem` 页面节点生成 `GetPage()` | [AutoUiPage 生成器](../source-generators/auto-ui-page-generator.md) |
|
||||
| `AutoSceneAttribute` | 为场景根节点生成 `GetScene()` | [AutoScene 生成器](../source-generators/auto-scene-generator.md) |
|
||||
| `AutoLoadAttribute` | 显式声明 `project.godot` AutoLoad 与 C# 节点类型映射 | [Godot 项目元数据生成器](../source-generators/godot-project-generator.md) |
|
||||
| `AutoRegisterExportedCollectionsAttribute` | 为宿主类开启导出集合批量注册生成 | [AutoRegisterExportedCollections 生成器](../source-generators/auto-register-exported-collections-generator.md) |
|
||||
| `RegisterExportedCollectionAttribute` | 指定集合与注册器成员的映射关系 | [AutoRegisterExportedCollections 生成器](../source-generators/auto-register-exported-collections-generator.md) |
|
||||
|
||||
## 常见用法示例
|
||||
|
||||
### 创建架构
|
||||
|
||||
```csharp
|
||||
public class MyArchitecture : Architecture
|
||||
{
|
||||
protected override void Init()
|
||||
{
|
||||
RegisterModel(new PlayerModel());
|
||||
RegisterSystem(new PlayerSystem());
|
||||
RegisterUtility(new StorageUtility());
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
var arch = new MyArchitecture();
|
||||
arch.Initialize();
|
||||
```
|
||||
|
||||
### 发送命令
|
||||
|
||||
```csharp
|
||||
public class AttackCommand : AbstractCommand
|
||||
{
|
||||
public int Damage { get; set; }
|
||||
|
||||
protected override void OnDo()
|
||||
{
|
||||
var player = this.GetModel<PlayerModel>();
|
||||
this.SendEvent(new AttackEvent { Damage = Damage });
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
arch.SendCommand(new AttackCommand { Damage = 10 });
|
||||
```
|
||||
|
||||
### 发送查询
|
||||
|
||||
```csharp
|
||||
public class GetPlayerHealthQuery : AbstractQuery<int>
|
||||
{
|
||||
protected override int OnDo()
|
||||
{
|
||||
return this.GetModel<PlayerModel>().Health.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
var health = arch.SendQuery(new GetPlayerHealthQuery());
|
||||
```
|
||||
|
||||
### 监听事件
|
||||
|
||||
```csharp
|
||||
public class PlayerSystem : AbstractSystem
|
||||
{
|
||||
protected override void OnInit()
|
||||
{
|
||||
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
|
||||
}
|
||||
|
||||
private void OnPlayerDied(PlayerDiedEvent e)
|
||||
{
|
||||
Console.WriteLine("Player died!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用本地化
|
||||
|
||||
```csharp
|
||||
// 初始化本地化管理器
|
||||
var config = new LocalizationConfig
|
||||
{
|
||||
DefaultLanguage = "eng",
|
||||
LocalizationPath = "res://localization"
|
||||
};
|
||||
var locManager = new LocalizationManager(config);
|
||||
locManager.Initialize();
|
||||
|
||||
// 获取简单文本
|
||||
string title = locManager.GetText("common", "game.title");
|
||||
|
||||
// 使用变量
|
||||
var message = locManager.GetString("common", "ui.message.welcome")
|
||||
.WithVariable("playerName", "Alice")
|
||||
.Format();
|
||||
|
||||
// 切换语言
|
||||
locManager.SetLanguage("zhs");
|
||||
|
||||
// 监听语言变化
|
||||
locManager.SubscribeToLanguageChange(language =>
|
||||
{
|
||||
Console.WriteLine($"Language changed to: {language}");
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
title: API 参考
|
||||
description: GFramework 的 API 阅读入口,按模块映射 README、专题页、XML 文档和教程链路。
|
||||
---
|
||||
|
||||
更多详情请查看各模块的详细文档。
|
||||
# API 参考
|
||||
|
||||
这里不再维护一份脱离源码演化的“伪 API 列表”。
|
||||
|
||||
当前 `GFramework` 的 API 参考链路以四类证据协同为准:
|
||||
|
||||
1. 模块 README:说明包关系、最小接入路径和目录边界
|
||||
2. `docs/zh-CN` 专题页:说明采用顺序、生命周期和使用建议
|
||||
3. 代码中的 XML 文档:说明公开 / 内部类型和关键成员的契约
|
||||
4. 教程页:说明这些 API 在真实接入路径中的组合方式
|
||||
|
||||
## 阅读顺序
|
||||
|
||||
### 想确认“该装哪个包、先看哪类 API”
|
||||
|
||||
先读模块 README,再读对应 landing page:
|
||||
|
||||
- 入门入口:[`../getting-started/index.md`](../getting-started/index.md)
|
||||
- 根模块地图:仓库根 `README.md`
|
||||
|
||||
### 想确认“这个功能属于哪个模块”
|
||||
|
||||
按下面的模块映射进入对应入口:
|
||||
|
||||
| 模块族 | 模块 README | 站内入口 | XML 文档关注点 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Core` / `Core.Abstractions` | `GFramework.Core/README.md`、`GFramework.Core.Abstractions/README.md` | [`../core/index.md`](../core/index.md)、[`../abstractions/core-abstractions.md`](../abstractions/core-abstractions.md) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 |
|
||||
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | `GFramework.Cqrs/README.md`、`GFramework.Cqrs.Abstractions/README.md`、`GFramework.Cqrs.SourceGenerators/README.md` | [`../core/cqrs.md`](../core/cqrs.md)、[`../source-generators/cqrs-handler-registry-generator.md`](../source-generators/cqrs-handler-registry-generator.md) | request / notification / handler / pipeline / registry / fallback contract |
|
||||
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | `GFramework.Game/README.md`、`GFramework.Game.Abstractions/README.md`、`GFramework.Game.SourceGenerators/README.md` | [`../game/index.md`](../game/index.md)、[`../abstractions/game-abstractions.md`](../abstractions/game-abstractions.md) | 配置、数据、设置、场景、UI、存储、序列化契约 |
|
||||
| `Godot` / `Godot.SourceGenerators` | `GFramework.Godot/README.md`、`GFramework.Godot.SourceGenerators/README.md` | [`../godot/index.md`](../godot/index.md)、[`../source-generators/index.md`](../source-generators/index.md) | 节点扩展、场景 / UI 适配、资源 / 存储 / 日志接入 |
|
||||
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | `GFramework.Ecs.Arch/README.md`、`GFramework.Ecs.Arch.Abstractions/README.md` | [`../ecs/index.md`](../ecs/index.md)、[`../ecs/arch.md`](../ecs/arch.md)、[`../abstractions/ecs-arch-abstractions.md`](../abstractions/ecs-arch-abstractions.md) | ECS 模块契约、系统适配、配置对象和运行时装配边界 |
|
||||
|
||||
## 先看 XML,还是先看教程
|
||||
|
||||
### 先看 XML 文档的情况
|
||||
|
||||
- 你在确认公开类型的约束、线程 / 生命周期语义、参数和返回值契约
|
||||
- 你需要区分“抽象层保证了什么”和“默认实现额外提供了什么”
|
||||
- 你在做多模块拆分、测试替身或扩展适配层
|
||||
|
||||
优先关注这些类型族:
|
||||
|
||||
- 架构 / 模块 / 服务入口
|
||||
- 生命周期、注册、路由、工厂、provider 契约
|
||||
- Source Generator 的 attribute、diagnostic 和 generated contract
|
||||
|
||||
### 先看教程和专题页的情况
|
||||
|
||||
- 你要的是最小接入路径,而不是逐个类型审计
|
||||
- 你想确认模块组合方式、目录约定和推荐接线顺序
|
||||
- 你在做从旧入口迁移到新入口的采用决策
|
||||
|
||||
优先入口:
|
||||
|
||||
- 教程概览:[`../tutorials/index.md`](../tutorials/index.md)
|
||||
- 最佳实践:[`../best-practices/index.md`](../best-practices/index.md)
|
||||
- 故障排查:[`../troubleshooting.md`](../troubleshooting.md)
|
||||
|
||||
## 当前边界
|
||||
|
||||
- `GFramework.Core.SourceGenerators.Abstractions`
|
||||
- `GFramework.Godot.SourceGenerators.Abstractions`
|
||||
- `GFramework.SourceGenerators.Common`
|
||||
|
||||
这些目录当前不是独立消费模块,因此不单独维护站内 API 参考入口。它们的公开说明跟随所属模块 README 和
|
||||
`source-generators` 栏目维护。
|
||||
|
||||
## 使用方式
|
||||
|
||||
把本页当成“API 阅读导航”而不是“签名快照”:
|
||||
|
||||
- 先选模块
|
||||
- 再进 README 和专题页确认采用路径
|
||||
- 最后回到代码里的 XML 文档核对具体契约
|
||||
|
||||
当 README、专题页和 XML 文档出现冲突时,以源码和测试所反映的当前实现为准。
|
||||
|
||||
@ -1,15 +1,29 @@
|
||||
---
|
||||
title: CQRS
|
||||
description: 当前推荐的新请求模型,统一覆盖 command、query、notification、stream request 和 pipeline behaviors。
|
||||
description: Cqrs 模块族的运行时、契约层、生成器入口,以及 XML / API 阅读链路。
|
||||
---
|
||||
|
||||
# CQRS
|
||||
|
||||
`GFramework.Cqrs` 是当前推荐的新请求模型 runtime。
|
||||
`Cqrs` 栏目对应三个直接相关的消费模块:
|
||||
|
||||
如果你在写新功能,优先使用这套模型,而不是继续扩展 `GFramework.Core.Command` / `Query` 的兼容层。
|
||||
- `GFramework.Cqrs`
|
||||
- `GFramework.Cqrs.Abstractions`
|
||||
- `GFramework.Cqrs.SourceGenerators`
|
||||
|
||||
## 安装方式
|
||||
如果你在写新功能,优先使用这套请求模型,而不是继续扩展 `GFramework.Core.Command` / `Query` 的兼容层。
|
||||
|
||||
## 模块族边界
|
||||
|
||||
| 模块 | 角色 | 何时安装 |
|
||||
| --- | --- | --- |
|
||||
| `GeWuYou.GFramework.Cqrs.Abstractions` | 纯契约层,定义 request、notification、stream、handler、pipeline、runtime seam | 需要把消息契约放到更稳定的共享层,或只依赖接口做解耦 |
|
||||
| `GeWuYou.GFramework.Cqrs` | 默认 runtime,提供 dispatcher、handler 基类、上下文扩展和程序集注册流程 | 大多数直接消费 CQRS 的业务模块 |
|
||||
| `GeWuYou.GFramework.Cqrs.SourceGenerators` | 编译期生成 `ICqrsHandlerRegistry`,缩小运行时反射扫描范围 | handler 较多,想把注册映射前移到编译期 |
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
最小安装组合是:
|
||||
|
||||
```bash
|
||||
dotnet add package GeWuYou.GFramework.Cqrs
|
||||
@ -22,15 +36,6 @@ dotnet add package GeWuYou.GFramework.Cqrs.Abstractions
|
||||
dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
|
||||
```
|
||||
|
||||
## 先理解分层
|
||||
|
||||
- `GFramework.Cqrs.Abstractions`
|
||||
- 纯契约层,定义请求、处理器、行为等接口
|
||||
- `GFramework.Cqrs`
|
||||
- 默认 runtime、dispatcher、处理器基类和上下文扩展
|
||||
- `GFramework.Cqrs.SourceGenerators`
|
||||
- 可选生成器,为消费端程序集生成 `ICqrsHandlerRegistry`
|
||||
|
||||
## 最小示例
|
||||
|
||||
消息基类和处理器基类在不同命名空间:
|
||||
@ -38,12 +43,10 @@ dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
|
||||
- 消息基类:`GFramework.Cqrs.Command` / `Query` / `Notification`
|
||||
- 处理器基类:`GFramework.Cqrs.Cqrs.Command` / `Query` / `Notification`
|
||||
|
||||
示例:
|
||||
|
||||
```csharp
|
||||
using GFramework.Cqrs.Abstractions.Cqrs.Command;
|
||||
using GFramework.Cqrs.Command;
|
||||
using GFramework.Cqrs.Cqrs.Command;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs.Command;
|
||||
|
||||
public sealed record CreatePlayerInput(string Name) : ICommandInput;
|
||||
|
||||
@ -66,9 +69,7 @@ public sealed class CreatePlayerCommandHandler
|
||||
}
|
||||
```
|
||||
|
||||
## 发送请求
|
||||
|
||||
如果你在 `IContextAware` 对象内部:
|
||||
如果你在 `IContextAware` 对象内部发送请求:
|
||||
|
||||
```csharp
|
||||
using GFramework.Cqrs.Extensions;
|
||||
@ -77,7 +78,7 @@ var playerId = await this.SendAsync(
|
||||
new CreatePlayerCommand(new CreatePlayerInput("Alice")));
|
||||
```
|
||||
|
||||
如果你在组合根或测试里:
|
||||
如果你在组合根或测试里发送请求:
|
||||
|
||||
```csharp
|
||||
var playerId = await architecture.Context.SendRequestAsync(
|
||||
@ -92,7 +93,7 @@ var playerId = await architecture.Context.SendRequestAsync(
|
||||
- `PublishAsync(...)`
|
||||
- `CreateStream(...)`
|
||||
|
||||
## 查询、通知和流
|
||||
## 统一请求模型
|
||||
|
||||
这套 runtime 不只处理 command,也统一处理:
|
||||
|
||||
@ -103,9 +104,9 @@ var playerId = await architecture.Context.SendRequestAsync(
|
||||
- Stream Request
|
||||
- 返回 `IAsyncEnumerable<T>`
|
||||
|
||||
也就是说,新代码通常不需要再分别设计“命令总线”“查询总线”和另一套通知分发语义。
|
||||
新代码通常不需要再分别设计“命令总线”“查询总线”和另一套通知分发语义。
|
||||
|
||||
## 注册处理器
|
||||
## 处理器注册与生成器协作
|
||||
|
||||
在标准 `Architecture` 启动路径中,CQRS runtime 会自动接入基础设施。你通常只需要在 `OnInitialize()` 里追加行为或额外程序集:
|
||||
|
||||
@ -123,11 +124,15 @@ protected override void OnInitialize()
|
||||
}
|
||||
```
|
||||
|
||||
默认逻辑会:
|
||||
默认注册流程当前遵循这些语义:
|
||||
|
||||
1. 优先使用消费端程序集上的生成注册器
|
||||
2. 生成注册器不可用时回退到反射扫描
|
||||
3. 对同一程序集去重,避免重复注册
|
||||
1. 优先读取消费端程序集上的 `CqrsHandlerRegistryAttribute`
|
||||
2. 存在生成注册器时优先使用 `ICqrsHandlerRegistry`
|
||||
3. 生成注册器不可用或元数据异常时记录告警并回退到反射路径
|
||||
4. 如果程序集带有 `CqrsReflectionFallbackAttribute`,只补扫剩余 handler
|
||||
5. 同一程序集按稳定键去重,避免重复注册
|
||||
|
||||
`Cqrs.SourceGenerators` 的专题入口见 [../source-generators/cqrs-handler-registry-generator.md](../source-generators/cqrs-handler-registry-generator.md)。
|
||||
|
||||
## Pipeline Behavior
|
||||
|
||||
@ -145,7 +150,7 @@ RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
|
||||
- 审计
|
||||
- 重试或统一异常封装
|
||||
|
||||
旧的 `Mediator` 兼容别名入口已经移除;当前公开入口只有 `RegisterCqrsPipelineBehavior<TBehavior>()`。
|
||||
当前公开入口只有 `RegisterCqrsPipelineBehavior<TBehavior>()`。
|
||||
|
||||
## 和旧 Command / Query 的关系
|
||||
|
||||
@ -157,15 +162,28 @@ RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
|
||||
- 新路径
|
||||
- `GFramework.Cqrs`
|
||||
|
||||
`IArchitectureContext` 仍然会兼容旧入口,但新代码应优先使用 CQRS runtime。
|
||||
`IArchitectureContext` 仍然兼容旧入口,但新代码应优先使用 CQRS runtime。
|
||||
|
||||
一个简单判断规则:
|
||||
|
||||
- 在维护历史代码:允许继续使用旧 Command / Query
|
||||
- 在写新功能或新模块:优先使用 CQRS
|
||||
|
||||
## XML 覆盖基线
|
||||
|
||||
下面这份 inventory 记录的是 `2026-04-22` 对 `Cqrs` 家族做的一轮轻量 XML 盘点结果:只统计当前运行时、契约层和生成器入口中的类型声明级 XML 覆盖,用来校对 README、landing page 与 API 入口,不把它表述成成员级契约全审计。
|
||||
|
||||
| 类型族 | 基线状态 | 代表类型 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| `GFramework.Cqrs.Abstractions/Cqrs/` | `20/20` 个类型声明已带 XML 注释 | `ICqrsRuntime`、`ICqrsHandlerRegistrar`、`IPipelineBehavior<,>`、`IRequestHandler<,>`、`Unit` | 先看请求、处理器和 runtime seam 的最小契约 |
|
||||
| `GFramework.Cqrs/Command` `Query` `Notification` `Request` `Extensions` | `7/7` 个类型声明已带 XML 注释 | `CommandBase<TInput, TResponse>`、`QueryBase<TInput, TResponse>`、`NotificationBase<TInput>`、`ContextAwareCqrsExtensions` | 看业务侧常用基类和上下文发送入口 |
|
||||
| `GFramework.Cqrs/Cqrs/` | `12/12` 个类型声明已带 XML 注释 | `AbstractCommandHandler<,>`、`AbstractQueryHandler<,>`、`AbstractNotificationHandler<>`、`LoggingBehavior<,>` | 看默认处理器基类、上下文注入与行为管道 |
|
||||
| `GFramework.Cqrs` 根入口与 `Internal/` | `19/19` 个类型声明已带 XML 注释 | `CqrsRuntimeFactory`、`ICqrsHandlerRegistry`、`CqrsHandlerRegistryAttribute`、`CqrsReflectionFallbackAttribute`、`DefaultCqrsRegistrationService` | 看 runtime 创建入口、registry 协议、fallback 语义和程序集去重规则 |
|
||||
| `GFramework.Cqrs.SourceGenerators/Cqrs/` | `3/3` 个类型声明已带 XML 注释 | `CqrsHandlerRegistryGenerator`、`RuntimeTypeReferenceSpec`、`OrderedRegistrationKind` | 看生成注册器、精确 type lookup 和 fallback 诊断边界 |
|
||||
|
||||
## 继续阅读
|
||||
|
||||
- 架构入口:[architecture](./architecture.md)
|
||||
- 上下文入口:[context](./context.md)
|
||||
- 生成器专题:[../source-generators/cqrs-handler-registry-generator.md](../source-generators/cqrs-handler-registry-generator.md)
|
||||
- 模块 README:`GFramework.Cqrs/README.md`
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Core
|
||||
description: GFramework.Core 与 GFramework.Core.Abstractions 的运行时入口、采用顺序和 XML 阅读导航。
|
||||
---
|
||||
|
||||
# Core
|
||||
|
||||
`Core` 栏目对应 `GFramework` 的基础运行时层,主要覆盖 `GFramework.Core` 与 `GFramework.Core.Abstractions`,以及与之直接相邻的旧版
|
||||
@ -29,28 +34,70 @@ dotnet add package GeWuYou.GFramework.Core.Abstractions
|
||||
|
||||
`Core` 栏目不是旧版“完整框架教程”的镜像,而是当前实现的入口导航。这里的页面按能力域组织:
|
||||
|
||||
- 架构与上下文
|
||||
- 架构与生命周期
|
||||
- [architecture](./architecture.md)
|
||||
- [context](./context.md)
|
||||
- [lifecycle](./lifecycle.md)
|
||||
- [async-initialization](./async-initialization.md)
|
||||
- 组件角色与运行时接入
|
||||
- [model](./model.md)
|
||||
- [system](./system.md)
|
||||
- [utility](./utility.md)
|
||||
- [environment](./environment.md)
|
||||
- [extensions](./extensions.md)
|
||||
- 旧版命令 / 查询执行器与迁移入口
|
||||
- [command](./command.md)
|
||||
- [query](./query.md)
|
||||
- [cqrs](./cqrs.md)
|
||||
- 核心横切能力
|
||||
- 状态、事件与规则
|
||||
- [events](./events.md)
|
||||
- [property](./property.md)
|
||||
- [rule](./rule.md)
|
||||
- [logging](./logging.md)
|
||||
- [resource](./resource.md)
|
||||
- [coroutine](./coroutine.md)
|
||||
- [ioc](./ioc.md)
|
||||
- 状态与扩展能力
|
||||
- [state-machine](./state-machine.md)
|
||||
- [state-management](./state-management.md)
|
||||
- 运行时支撑能力
|
||||
- [resource](./resource.md)
|
||||
- [pool](./pool.md)
|
||||
- [coroutine](./coroutine.md)
|
||||
- [pause](./pause.md)
|
||||
- [localization](./localization.md)
|
||||
- [configuration](./configuration.md)
|
||||
- [ioc](./ioc.md)
|
||||
- 通用辅助能力
|
||||
- [functional](./functional.md)
|
||||
- [extensions](./extensions.md)
|
||||
|
||||
## XML 与 API 阅读入口
|
||||
|
||||
如果你已经知道模块归属,但想确认公开类型的契约边界,建议按下面顺序阅读:
|
||||
|
||||
1. 先看模块 README `GFramework.Core/README.md`,确认包关系和目录边界
|
||||
2. 再看本栏目对应专题页,确认采用顺序、生命周期与推荐接线方式
|
||||
3. 最后回到源码中的 XML 文档,重点核对这些类型族:
|
||||
- `Architecture` / `IArchitectureContext`
|
||||
- `CommandExecutor` / `QueryExecutor`
|
||||
- `ILogger` / `ILoggerFactory`
|
||||
- `IResourceManager` / `IConfigurationManager`
|
||||
- `IAsyncKeyLockManager` / `ITimeProvider`
|
||||
|
||||
统一入口见 [`../api-reference/index.md`](../api-reference/index.md)。
|
||||
|
||||
## XML 覆盖基线
|
||||
|
||||
下面这份 inventory 记录的是 `2026-04-22` 对 `GFramework.Core` 做的一轮轻量 XML 盘点结果:只统计顶层目录中的公开 /
|
||||
内部类型声明是否带 XML 注释,用来确认阅读入口和治理优先级;成员级 ``<param>``、``<returns>``、异常语义与线程说明仍需要继续细审。
|
||||
|
||||
| 类型族 | 基线状态 | 代表类型 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Architectures/` | `16/16` 个类型声明已带 XML 注释 | `Architecture`、`ArchitectureContext`、`ArchitectureLifecycle`、`ArchitecturePhaseCoordinator` | 看架构启动、模块安装、阶段切换和上下文暴露边界 |
|
||||
| `Services/` | `6/6` 个类型声明已带 XML 注释 | `ServiceModuleManager`、`CommandExecutorModule`、`CqrsRuntimeModule` | 看服务模块的注册顺序、销毁语义和默认接线 |
|
||||
| `Command/` `Query/` | `15/15` 个类型声明已带 XML 注释 | `CommandExecutor`、`AsyncQueryExecutor`、`AbstractCommand<TInput>`、`AbstractQuery<TResult>` | 看旧入口兼容面与向 `CQRS` 迁移时还保留了哪些执行契约 |
|
||||
| `Events/` `Property/` | `19/19` 个类型声明已带 XML 注释 | `EventBus`、`EnhancedEventBus`、`BindableProperty<T>`、`OrEvent<T>` | 看事件传播、解绑约束和可绑定属性的订阅语义 |
|
||||
| `State/` `StateManagement/` | `10/10` 个类型声明已带 XML 注释 | `StateMachine`、`StateMachineSystem`、`Store<TState>`、`StoreBuilder<TState>` | 看状态切换、selector / middleware / dispatch 的单向流边界 |
|
||||
| `Coroutine/` `Time/` `Pause/` `Concurrency/` | `43/43` 个类型声明已带 XML 注释 | `CoroutineScheduler`、`CoroutineHandle`、`WaitForSecondsRealtime`、`PauseStackManager`、`AsyncKeyLockManager` | 看调度阶段、等待指令、时间源和暂停 / 锁的线程语义 |
|
||||
| `Resource/` `Pool/` | `8/8` 个类型声明已带 XML 注释 | `ResourceManager`、`AutoReleaseStrategy`、`ManualReleaseStrategy`、`AbstractObjectPoolSystem<TKey, TObject>` | 看资源句柄释放策略与对象池复用约束 |
|
||||
| `Logging/` `Localization/` `Configuration/` `Environment/` `Ioc/` | `31/31` 个类型声明已带 XML 注释 | `ConsoleLogger`、`CompositeLogger`、`LocalizationManager`、`ConfigurationManager`、`MicrosoftDiContainer` | 看日志组装、格式化 / filter、配置监听、环境对象与容器适配 |
|
||||
| `Model/` `Systems/` `Utility/` `Rule/` `Extensions/` `Functional/` | `34/34` 个类型声明已带 XML 注释 | `AbstractModel`、`AbstractSystem`、`NumericDisplayFormatter`、`ContextAwareBase`、`Result<T>` | 看默认基类、上下文感知 helper、数值格式化和通用扩展的使用边界 |
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
@ -104,4 +151,5 @@ public sealed class CounterArchitecture : Architecture
|
||||
|
||||
- `GFramework.Core/README.md`
|
||||
- `GFramework.Core.Abstractions/README.md`
|
||||
- `docs/zh-CN/api-reference/index.md`
|
||||
- 仓库根 `README.md`
|
||||
|
||||
@ -1,46 +1,43 @@
|
||||
---
|
||||
title: Arch ECS 集成
|
||||
description: GFramework 的 Arch ECS 集成包使用指南,提供高性能的实体组件系统支持。
|
||||
description: GFramework.Ecs.Arch 的默认运行时装配路径、系统桥接方式与 XML 阅读入口。
|
||||
---
|
||||
|
||||
# Arch ECS 集成
|
||||
|
||||
## 概述
|
||||
`GFramework.Ecs.Arch` 是当前仓库里负责 Arch ECS 默认接入路径的运行时包。它把 Arch `World`、GFramework 的
|
||||
`IServiceModule` 生命周期,以及 `AbstractSystem` / `ISystem` 体系桥接到同一条初始化与更新链路中。
|
||||
|
||||
`GFramework.Ecs.Arch` 是 GFramework 的 Arch ECS 集成包,提供开箱即用的 ECS(Entity Component
|
||||
System)支持。基于 [Arch.Core](https://github.com/genaray/Arch) 实现,具有极致的性能和简洁的 API。
|
||||
## 什么时候依赖它
|
||||
|
||||
**主要特性**:
|
||||
当你需要下面任一能力时,应直接依赖 `GeWuYou.GFramework.Ecs.Arch`:
|
||||
|
||||
- 🎯 **显式集成** - 符合 .NET 生态习惯的显式注册方式
|
||||
- 🔌 **零依赖** - 不使用时,Core 包无 Arch 依赖
|
||||
- 🎯 **类型安全** - 完整的类型系统和编译时检查
|
||||
- ⚡ **高性能** - 基于 Arch ECS 的高性能实现
|
||||
- 🔧 **易扩展** - 简单的系统适配器模式
|
||||
- 📊 **优先级支持** - 系统按优先级顺序执行
|
||||
- 在架构实例上调用 `UseArch(...)`
|
||||
- 让 `World` 在服务模块注册阶段自动创建并注入容器
|
||||
- 让 ECS 系统继承 `ArchSystemAdapter<T>`
|
||||
- 使用仓库自带的 `Position`、`Velocity`、`MovementSystem` 最小示例
|
||||
|
||||
**性能特点**:
|
||||
如果你只想保留共享边界,而不依赖默认实现,请改看
|
||||
[`../abstractions/ecs-arch-abstractions.md`](../abstractions/ecs-arch-abstractions.md)。
|
||||
|
||||
- 10,000 个实体更新 < 100ms
|
||||
- 1,000 个实体创建 < 50ms
|
||||
- 基于 Archetype 的高效内存布局
|
||||
- 零 GC 分配的组件访问
|
||||
## 最小接入路径
|
||||
|
||||
## 安装
|
||||
### 1. 安装包
|
||||
|
||||
```bash
|
||||
dotnet add package GeWuYou.GFramework.Ecs.Arch
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
### 2. 在 `Initialize()` 之前调用 `UseArch(...)`
|
||||
|
||||
### 1. 注册 ECS 模块
|
||||
当前实现通过 `ArchitectureModuleRegistry.Register(...)` 提前登记 `ArchEcsModule`。这意味着调用时机应位于
|
||||
`Initialize()` 之前,而不是放进 `OnInitialize()` 里。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Architecture;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Ecs.Arch.Extensions;
|
||||
|
||||
public class GameArchitecture : Architecture
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
public GameArchitecture() : base(new ArchitectureConfiguration())
|
||||
{
|
||||
@ -48,698 +45,100 @@ public class GameArchitecture : Architecture
|
||||
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
// 显式注册 Arch ECS 模块
|
||||
this.UseArch();
|
||||
RegisterSystem<MovementSystem>();
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化架构
|
||||
var architecture = new GameArchitecture();
|
||||
var architecture = new GameArchitecture()
|
||||
.UseArch(options =>
|
||||
{
|
||||
options.WorldCapacity = 2048;
|
||||
options.Priority = 50;
|
||||
});
|
||||
|
||||
architecture.Initialize();
|
||||
```
|
||||
|
||||
### 2. 带配置的注册
|
||||
### 3. 用 `ArchSystemAdapter<float>` 编写系统
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
// 带配置的注册
|
||||
this.UseArch(options =>
|
||||
{
|
||||
options.WorldCapacity = 2000; // World 初始容量
|
||||
options.EnableStatistics = true; // 启用统计信息
|
||||
options.Priority = 50; // 模块优先级
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 定义组件
|
||||
|
||||
组件是纯数据结构,使用 `struct` 定义:
|
||||
|
||||
```csharp
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MyGame.Components;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct Position(float x, float y)
|
||||
{
|
||||
public float X { get; set; } = x;
|
||||
public float Y { get; set; } = y;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct Velocity(float x, float y)
|
||||
{
|
||||
public float X { get; set; } = x;
|
||||
public float Y { get; set; } = y;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct Health(float current, float max)
|
||||
{
|
||||
public float Current { get; set; } = current;
|
||||
public float Max { get; set; } = max;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 创建系统
|
||||
|
||||
系统继承自 `ArchSystemAdapter<T>`:
|
||||
`ArchSystemAdapter<T>` 在 `OnInit()` 中从当前上下文解析 `World`,再把 Arch 的 `Initialize / BeforeUpdate /
|
||||
AfterUpdate / Dispose` 钩子桥接到可重写方法。
|
||||
|
||||
```csharp
|
||||
using Arch.Core;
|
||||
using GFramework.Ecs.Arch;
|
||||
using MyGame.Components;
|
||||
using GFramework.Ecs.Arch.Components;
|
||||
|
||||
namespace MyGame.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// 移动系统 - 更新实体位置
|
||||
/// </summary>
|
||||
public sealed class MovementSystem : ArchSystemAdapter<float>
|
||||
{
|
||||
private QueryDescription _query;
|
||||
|
||||
protected override void OnArchInitialize()
|
||||
{
|
||||
// 创建查询:查找所有同时拥有 Position 和 Velocity 组件的实体
|
||||
_query = new QueryDescription()
|
||||
.WithAll<Position, Velocity>();
|
||||
}
|
||||
|
||||
protected override void OnUpdate(in float deltaTime)
|
||||
{
|
||||
// 查询并更新所有符合条件的实体
|
||||
World.Query(in _query, (ref Position pos, ref Velocity vel) =>
|
||||
var frameDelta = deltaTime;
|
||||
|
||||
World.Query(in _query, (ref Position position, ref Velocity velocity) =>
|
||||
{
|
||||
pos.X += vel.X * deltaTime;
|
||||
pos.Y += vel.Y * deltaTime;
|
||||
position.X += velocity.X * frameDelta;
|
||||
position.Y += velocity.Y * frameDelta;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 注册系统
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
this.UseArch();
|
||||
|
||||
// 注册 ECS 系统
|
||||
RegisterSystem<MovementSystem>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 创建实体
|
||||
### 4. 初始化后解析 `World` 与模块服务
|
||||
|
||||
```csharp
|
||||
using Arch.Core;
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
using MyGame.Components;
|
||||
|
||||
[ContextAware]
|
||||
public partial class GameController
|
||||
{
|
||||
public void Start()
|
||||
{
|
||||
// 获取 World
|
||||
var world = this.GetService<World>();
|
||||
|
||||
// 创建实体
|
||||
var player = world.Create(
|
||||
new Position(0, 0),
|
||||
new Velocity(0, 0),
|
||||
new Health(100, 100)
|
||||
);
|
||||
|
||||
var enemy = world.Create(
|
||||
new Position(10, 10),
|
||||
new Velocity(-1, 0),
|
||||
new Health(50, 50)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. 更新系统
|
||||
|
||||
```csharp
|
||||
using GFramework.Ecs.Arch.Abstractions;
|
||||
|
||||
public class GameLoop
|
||||
{
|
||||
private IArchEcsModule _ecsModule;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
// 获取 ECS 模块
|
||||
_ecsModule = architecture.Context.GetService<IArchEcsModule>();
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
// 更新所有 ECS 系统
|
||||
_ecsModule.Update(deltaTime);
|
||||
}
|
||||
}
|
||||
var world = architecture.Context.GetService<World>();
|
||||
var ecsModule = architecture.Context.GetService<IArchEcsModule>();
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
### ArchOptions
|
||||
### 5. 由宿主循环显式调用 `Update`
|
||||
|
||||
```csharp
|
||||
public sealed class ArchOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// World 初始容量(默认:1000)
|
||||
/// </summary>
|
||||
public int WorldCapacity { get; set; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用统计信息(默认:false)
|
||||
/// </summary>
|
||||
public bool EnableStatistics { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 模块优先级(默认:50)
|
||||
/// </summary>
|
||||
public int Priority { get; set; } = 50;
|
||||
}
|
||||
ecsModule.Update(deltaTime);
|
||||
```
|
||||
|
||||
### 配置示例
|
||||
这一步在 `GFramework.Ecs.Arch.Tests/Ecs/*.cs` 中也采用同样的驱动方式。
|
||||
|
||||
```csharp
|
||||
this.UseArch(options =>
|
||||
{
|
||||
// 设置 World 初始容量
|
||||
// 根据预期实体数量设置,避免频繁扩容
|
||||
options.WorldCapacity = 2000;
|
||||
## 运行时职责
|
||||
|
||||
// 启用统计信息(开发/调试时使用)
|
||||
options.EnableStatistics = true;
|
||||
| 类型 | 责任 | 证据文件 |
|
||||
| --- | --- | --- |
|
||||
| `ArchExtensions` | 把 `ArchEcsModule` 注册到 `ArchitectureModuleRegistry` | `GFramework.Ecs.Arch/Extensions/ArchExtensions.cs` |
|
||||
| `ArchEcsModule` | 创建并注册 `World`,按优先级收集 `ArchSystemAdapter<float>`,负责初始化、销毁和逐帧更新 | `GFramework.Ecs.Arch/ArchEcsModule.cs` |
|
||||
| `ArchSystemAdapter<T>` | 从 GFramework 系统生命周期桥接到 Arch `ISystem<T>` 生命周期 | `GFramework.Ecs.Arch/ArchSystemAdapter.cs` |
|
||||
| `ArchOptions` | 暴露 `WorldCapacity`、`EnableStatistics`、`Priority` 这组运行时配置对象 | `GFramework.Ecs.Arch/ArchOptions.cs` |
|
||||
|
||||
// 设置模块优先级
|
||||
// 数值越小,优先级越高
|
||||
options.Priority = 50;
|
||||
});
|
||||
```
|
||||
## 配置对象阅读提示
|
||||
|
||||
## 核心概念
|
||||
当前公开配置对象是 `GFramework.Ecs.Arch.ArchOptions`。从源码可直接确认:
|
||||
|
||||
### Entity(实体)
|
||||
- `WorldCapacity` 用于 `World.Create(...)` 的容量参数
|
||||
- `Priority` 影响 `ArchEcsModule` 作为服务模块的排序
|
||||
- `EnableStatistics` 目前保留在公开配置面上;采用时应以源码 XML 注释和实现行为为准,而不是依赖旧文档推断
|
||||
|
||||
实体是游戏世界中的基本对象,本质上是一个唯一标识符:
|
||||
## 类型族级 XML Inventory
|
||||
|
||||
```csharp
|
||||
// 创建空实体
|
||||
var entity = world.Create();
|
||||
| 类型族 | 代表类型 | XML 状态 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| 装配入口 | `ArchExtensions` | 已覆盖 | `UseArch(...)` 的时机、链式调用返回值 |
|
||||
| 服务模块 | `ArchEcsModule` | 已覆盖 | `World` 注册、系统收集、模块销毁顺序 |
|
||||
| 系统桥接层 | `ArchSystemAdapter<T>` | 已覆盖 | `OnArchInitialize` / `OnUpdate` / `OnArchDispose` |
|
||||
| 示例类型 | `Position`、`Velocity`、`MovementSystem` | 已覆盖 | 组件布局、查询写法、最小集成样例 |
|
||||
|
||||
// 创建带组件的实体
|
||||
var entity = world.Create(
|
||||
new Position(0, 0),
|
||||
new Velocity(1, 1)
|
||||
);
|
||||
## 相关入口
|
||||
|
||||
// 销毁实体
|
||||
world.Destroy(entity);
|
||||
```
|
||||
|
||||
### Component(组件)
|
||||
|
||||
组件是纯数据结构,用于存储实体的状态:
|
||||
|
||||
```csharp
|
||||
// 添加组件
|
||||
world.Add(entity, new Position(0, 0));
|
||||
|
||||
// 检查组件
|
||||
if (world.Has<Position>(entity))
|
||||
{
|
||||
// 获取组件引用(零 GC 分配)
|
||||
ref var pos = ref world.Get<Position>(entity);
|
||||
pos.X += 10;
|
||||
}
|
||||
|
||||
// 设置组件(替换现有值)
|
||||
world.Set(entity, new Position(100, 100));
|
||||
|
||||
// 移除组件
|
||||
world.Remove<Velocity>(entity);
|
||||
```
|
||||
|
||||
### System(系统)
|
||||
|
||||
系统包含游戏逻辑,处理具有特定组件组合的实体:
|
||||
|
||||
```csharp
|
||||
public sealed class DamageSystem : ArchSystemAdapter<float>
|
||||
{
|
||||
private QueryDescription _query;
|
||||
|
||||
protected override void OnArchInitialize()
|
||||
{
|
||||
// 初始化查询
|
||||
_query = new QueryDescription()
|
||||
.WithAll<Health, Damage>();
|
||||
}
|
||||
|
||||
protected override void OnUpdate(in float deltaTime)
|
||||
{
|
||||
// 处理伤害
|
||||
World.Query(in _query, (Entity entity, ref Health health, ref Damage damage) =>
|
||||
{
|
||||
health.Current -= damage.Value * deltaTime;
|
||||
|
||||
if (health.Current <= 0)
|
||||
{
|
||||
health.Current = 0;
|
||||
World.Remove<Damage>(entity);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### World(世界)
|
||||
|
||||
World 是 ECS 的核心容器,管理所有实体和组件:
|
||||
|
||||
```csharp
|
||||
// World 由 ArchEcsModule 自动创建和注册
|
||||
var world = this.GetService<World>();
|
||||
|
||||
// 获取实体数量
|
||||
var entityCount = world.Size;
|
||||
|
||||
// 清空所有实体
|
||||
world.Clear();
|
||||
```
|
||||
|
||||
## 系统适配器
|
||||
|
||||
### ArchSystemAdapter<T>
|
||||
|
||||
`ArchSystemAdapter<T>` 桥接 Arch.System.ISystem<T> 到 GFramework 架构:
|
||||
|
||||
```csharp
|
||||
public sealed class MySystem : ArchSystemAdapter<float>
|
||||
{
|
||||
// Arch 系统初始化
|
||||
protected override void OnArchInitialize()
|
||||
{
|
||||
// 创建查询、初始化资源
|
||||
}
|
||||
|
||||
// 更新前调用
|
||||
protected override void OnBeforeUpdate(in float deltaTime)
|
||||
{
|
||||
// 预处理逻辑
|
||||
}
|
||||
|
||||
// 主更新逻辑
|
||||
protected override void OnUpdate(in float deltaTime)
|
||||
{
|
||||
// 处理实体
|
||||
}
|
||||
|
||||
// 更新后调用
|
||||
protected override void OnAfterUpdate(in float deltaTime)
|
||||
{
|
||||
// 后处理逻辑
|
||||
}
|
||||
|
||||
// 资源清理
|
||||
protected override void OnArchDispose()
|
||||
{
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 访问 World
|
||||
|
||||
在系统中可以直接访问 `World` 属性:
|
||||
|
||||
```csharp
|
||||
public sealed class MySystem : ArchSystemAdapter<float>
|
||||
{
|
||||
protected override void OnUpdate(in float deltaTime)
|
||||
{
|
||||
// 访问 World
|
||||
var entityCount = World.Size;
|
||||
|
||||
// 创建实体
|
||||
var entity = World.Create(new Position(0, 0));
|
||||
|
||||
// 查询实体
|
||||
var query = new QueryDescription().WithAll<Position>();
|
||||
World.Query(in query, (ref Position pos) =>
|
||||
{
|
||||
// 处理逻辑
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 访问框架服务
|
||||
|
||||
`ArchSystemAdapter<T>` 继承自 `AbstractSystem`,可以使用所有 GFramework 的扩展方法:
|
||||
|
||||
```csharp
|
||||
public sealed class ServiceAccessSystem : ArchSystemAdapter<float>
|
||||
{
|
||||
protected override void OnUpdate(in float deltaTime)
|
||||
{
|
||||
// 获取 Model
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
|
||||
// 获取 Utility
|
||||
var timeUtility = this.GetUtility<TimeUtility>();
|
||||
|
||||
// 发送命令
|
||||
this.SendCommand(new SaveGameCommand());
|
||||
|
||||
// 发送查询
|
||||
var score = this.SendQuery(new GetScoreQuery());
|
||||
|
||||
// 发送事件
|
||||
this.SendEvent(new GameOverEvent());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 查询实体
|
||||
|
||||
### 基本查询
|
||||
|
||||
```csharp
|
||||
// 查询:必须有 Position 和 Velocity
|
||||
var query = new QueryDescription()
|
||||
.WithAll<Position, Velocity>();
|
||||
|
||||
World.Query(in query, (ref Position pos, ref Velocity vel) =>
|
||||
{
|
||||
pos.X += vel.X * deltaTime;
|
||||
pos.Y += vel.Y * deltaTime;
|
||||
});
|
||||
```
|
||||
|
||||
### 过滤查询
|
||||
|
||||
```csharp
|
||||
// 查询:必须有 Health,但不能有 Damage
|
||||
var query = new QueryDescription()
|
||||
.WithAll<Health>()
|
||||
.WithNone<Damage>();
|
||||
|
||||
World.Query(in query, (ref Health health) =>
|
||||
{
|
||||
// 只处理没有受伤的实体
|
||||
});
|
||||
```
|
||||
|
||||
### 可选组件查询
|
||||
|
||||
```csharp
|
||||
// 查询:必须有 Position,可选 Velocity
|
||||
var query = new QueryDescription()
|
||||
.WithAll<Position>()
|
||||
.WithAny<Velocity>();
|
||||
|
||||
World.Query(in query, (Entity entity, ref Position pos) =>
|
||||
{
|
||||
// 处理逻辑
|
||||
});
|
||||
```
|
||||
|
||||
### 访问实体 ID
|
||||
|
||||
```csharp
|
||||
var query = new QueryDescription().WithAll<Position>();
|
||||
|
||||
World.Query(in query, (Entity entity, ref Position pos) =>
|
||||
{
|
||||
// 可以访问实体 ID
|
||||
Console.WriteLine($"Entity {entity.Id}: ({pos.X}, {pos.Y})");
|
||||
|
||||
// 可以对实体进行操作
|
||||
if (pos.X > 100)
|
||||
{
|
||||
World.Destroy(entity);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 系统优先级
|
||||
|
||||
系统按照优先级顺序执行,数值越小优先级越高:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.bases;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Bases;
|
||||
|
||||
// 使用 Priority 特性设置优先级
|
||||
[Priority(10)] // 高优先级,先执行
|
||||
public sealed class InputSystem : ArchSystemAdapter<float>
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
[Priority(20)] // 中优先级
|
||||
public sealed class MovementSystem : ArchSystemAdapter<float>
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
[Priority(30)] // 低优先级,后执行
|
||||
public sealed class RenderSystem : ArchSystemAdapter<float>
|
||||
{
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
执行顺序:InputSystem → MovementSystem → RenderSystem
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 使用 struct 组件
|
||||
|
||||
```csharp
|
||||
// ✅ 推荐:使用 struct
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct Position(float x, float y)
|
||||
{
|
||||
public float X { get; set; } = x;
|
||||
public float Y { get; set; } = y;
|
||||
}
|
||||
|
||||
// ❌ 不推荐:使用 class
|
||||
public class Position
|
||||
{
|
||||
public float X { get; set; }
|
||||
public float Y { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 缓存查询
|
||||
|
||||
```csharp
|
||||
public class OptimizedSystem : ArchSystemAdapter<float>
|
||||
{
|
||||
// ✅ 推荐:缓存查询
|
||||
private QueryDescription _cachedQuery;
|
||||
|
||||
protected override void OnArchInitialize()
|
||||
{
|
||||
_cachedQuery = new QueryDescription()
|
||||
.WithAll<Position, Velocity>();
|
||||
}
|
||||
|
||||
protected override void OnUpdate(in float deltaTime)
|
||||
{
|
||||
World.Query(in _cachedQuery, (ref Position pos, ref Velocity vel) =>
|
||||
{
|
||||
pos.X += vel.X * deltaTime;
|
||||
pos.Y += vel.Y * deltaTime;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用 ref 访问组件
|
||||
|
||||
```csharp
|
||||
// ✅ 推荐:使用 ref 避免复制
|
||||
World.Query(in query, (ref Position pos, ref Velocity vel) =>
|
||||
{
|
||||
pos.X += vel.X; // 直接修改,零 GC
|
||||
});
|
||||
|
||||
// ❌ 不推荐:不使用 ref
|
||||
World.Query(in query, (Position pos, Velocity vel) =>
|
||||
{
|
||||
pos.X += vel.X; // 复制值,修改不会生效
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 组件大小优化
|
||||
|
||||
```csharp
|
||||
// ✅ 推荐:小而专注的组件
|
||||
public struct Position(float x, float y)
|
||||
{
|
||||
public float X { get; set; } = x;
|
||||
public float Y { get; set; } = y;
|
||||
}
|
||||
|
||||
public struct Velocity(float x, float y)
|
||||
{
|
||||
public float X { get; set; } = x;
|
||||
public float Y { get; set; } = y;
|
||||
}
|
||||
|
||||
// ❌ 不推荐:大而全的组件
|
||||
public struct Transform
|
||||
{
|
||||
public float X, Y, Z;
|
||||
public float RotationX, RotationY, RotationZ;
|
||||
public float ScaleX, ScaleY, ScaleZ;
|
||||
public float VelocityX, VelocityY, VelocityZ;
|
||||
// ... 太多数据
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 组件设计原则
|
||||
|
||||
- 使用 `struct` 而不是 `class`
|
||||
- 只包含数据,不包含逻辑
|
||||
- 使用 `[StructLayout(LayoutKind.Sequential)]` 优化内存布局
|
||||
- 保持组件小而专注
|
||||
|
||||
### 2. 系统设计原则
|
||||
|
||||
- 单一职责:每个系统只负责一件事
|
||||
- 缓存查询:在 `OnArchInitialize` 中创建查询
|
||||
- 使用 ref:访问组件时使用 ref 参数
|
||||
- 批量处理:一次查询处理所有实体
|
||||
|
||||
### 3. 标签组件
|
||||
|
||||
使用空结构体作为标签来分类实体:
|
||||
|
||||
```csharp
|
||||
// 定义标签组件
|
||||
public struct PlayerTag { }
|
||||
public struct EnemyTag { }
|
||||
public struct DeadTag { }
|
||||
|
||||
// 使用标签过滤实体
|
||||
var query = new QueryDescription()
|
||||
.WithAll<Position, Velocity, PlayerTag>()
|
||||
.WithNone<DeadTag>();
|
||||
```
|
||||
|
||||
### 4. 与传统架构结合
|
||||
|
||||
```csharp
|
||||
// ECS 系统可以访问 Model
|
||||
public class EnemySpawnSystem : ArchSystemAdapter<float>
|
||||
{
|
||||
protected override void OnUpdate(in float deltaTime)
|
||||
{
|
||||
var gameState = this.GetModel<GameStateModel>();
|
||||
|
||||
// 根据关卡生成敌人
|
||||
for (int i = 0; i < gameState.Level; i++)
|
||||
{
|
||||
World.Create(
|
||||
new Position(Random.Shared.Next(0, 100), 0),
|
||||
new Velocity(0, -1),
|
||||
new Health(50, 50)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何在运行时动态添加/移除组件?
|
||||
|
||||
A: Arch 支持运行时修改实体的组件:
|
||||
|
||||
```csharp
|
||||
// 动态添加组件
|
||||
if (pos.X > 100 && !World.Has<FastTag>(entity))
|
||||
{
|
||||
World.Add(entity, new FastTag());
|
||||
}
|
||||
|
||||
// 动态移除组件
|
||||
if (pos.X < 0 && World.Has<FastTag>(entity))
|
||||
{
|
||||
World.Remove<FastTag>(entity);
|
||||
}
|
||||
```
|
||||
|
||||
### Q: 如何处理实体之间的交互?
|
||||
|
||||
A: 使用嵌套查询或事件:
|
||||
|
||||
```csharp
|
||||
// 方式 1:嵌套查询
|
||||
World.Query(in playerQuery, (Entity player, ref Position playerPos) =>
|
||||
{
|
||||
World.Query(in enemyQuery, (Entity enemy, ref Position enemyPos) =>
|
||||
{
|
||||
// 检测碰撞
|
||||
});
|
||||
});
|
||||
|
||||
// 方式 2:使用事件
|
||||
this.SendEvent(new CollisionEvent
|
||||
{
|
||||
Entity1 = player,
|
||||
Entity2 = enemy
|
||||
});
|
||||
```
|
||||
|
||||
### Q: 如何调试 ECS 系统?
|
||||
|
||||
A: 使用日志和统计信息:
|
||||
|
||||
```csharp
|
||||
protected override void OnUpdate(in float deltaTime)
|
||||
{
|
||||
// 打印实体数量
|
||||
Console.WriteLine($"Total entities: {World.Size}");
|
||||
|
||||
// 查询特定实体
|
||||
var query = new QueryDescription().WithAll<Position>();
|
||||
var count = 0;
|
||||
World.Query(in query, (Entity entity, ref Position pos) =>
|
||||
{
|
||||
count++;
|
||||
Console.WriteLine($"Entity {entity.Id}: ({pos.X}, {pos.Y})");
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 相关资源
|
||||
|
||||
- [Arch.Core 官方文档](https://github.com/genaray/Arch)
|
||||
- [ECS 概述](./index.md)
|
||||
- ECS 模块总览:[`index.md`](./index.md)
|
||||
- 抽象契约页:[`../abstractions/ecs-arch-abstractions.md`](../abstractions/ecs-arch-abstractions.md)
|
||||
- 仓库模块 README:`GFramework.Ecs.Arch/README.md`
|
||||
- 统一 API / XML 导航:[`../api-reference/index.md`](../api-reference/index.md)
|
||||
|
||||
@ -1,95 +1,39 @@
|
||||
---
|
||||
title: ECS 系统集成
|
||||
description: GFramework 的 ECS(Entity Component System)集成方案,支持多种 ECS 框架。
|
||||
description: GFramework 当前 ECS 模块族的包边界、采用顺序与 XML 阅读入口。
|
||||
---
|
||||
|
||||
# ECS 系统集成
|
||||
|
||||
## 概述
|
||||
GFramework 当前仓库内已经交付并持续维护的 ECS 模块族是 `Ecs.Arch`。它分成运行时实现包
|
||||
`GFramework.Ecs.Arch` 和契约包 `GFramework.Ecs.Arch.Abstractions`,分别覆盖默认装配能力与共享边界约定。
|
||||
|
||||
GFramework 提供了灵活的 ECS(Entity Component System)集成方案,允许你根据项目需求选择合适的 ECS 框架。ECS
|
||||
是一种数据驱动的架构模式,特别适合处理大量相似实体的场景。
|
||||
## 当前模块族
|
||||
|
||||
## 什么是 ECS?
|
||||
| 包 | 适用场景 | 你会得到什么 | 继续阅读 |
|
||||
| --- | --- | --- | --- |
|
||||
| `GFramework.Ecs.Arch` | 需要默认运行时、`UseArch(...)` 装配入口、`World` 注册和系统适配基类 | `ArchEcsModule`、`ArchSystemAdapter<T>`、`ArchExtensions.UseArch(...)`、示例组件与系统 | [`arch.md`](./arch.md) |
|
||||
| `GFramework.Ecs.Arch.Abstractions` | 只想让共享宿主循环、测试替身或扩展模块依赖最小契约,而不引入默认运行时 | `IArchEcsModule`、`IArchSystemAdapter<T>`、`ArchOptions` 契约对象 | [`../abstractions/ecs-arch-abstractions.md`](../abstractions/ecs-arch-abstractions.md) |
|
||||
|
||||
ECS(Entity Component System)是一种架构模式,将游戏对象分解为三个核心概念:
|
||||
## 最小采用路径
|
||||
|
||||
- **Entity(实体)**:游戏世界中的基本对象,本质上是一个唯一标识符
|
||||
- **Component(组件)**:纯数据结构,存储实体的状态
|
||||
- **System(系统)**:包含游戏逻辑,处理具有特定组件组合的实体
|
||||
### 1. 选择包边界
|
||||
|
||||
### ECS 的优势
|
||||
- 需要默认实现时安装 `GeWuYou.GFramework.Ecs.Arch`
|
||||
- 只需要契约时安装 `GeWuYou.GFramework.Ecs.Arch.Abstractions`
|
||||
|
||||
- **高性能**:数据局部性好,缓存友好
|
||||
- **可扩展**:通过组合组件轻松创建新实体类型
|
||||
- **并行处理**:系统之间相互独立,易于并行化
|
||||
- **数据驱动**:逻辑与数据分离,便于序列化和网络同步
|
||||
### 2. 在 `Initialize()` 前显式接入运行时
|
||||
|
||||
### 何时使用 ECS?
|
||||
|
||||
**适合使用 ECS 的场景**:
|
||||
|
||||
- 大量相似实体(敌人、子弹、粒子)
|
||||
- 需要高性能批量处理
|
||||
- 复杂的实体组合和变化
|
||||
- 需要并行处理的系统
|
||||
|
||||
**不适合使用 ECS 的场景**:
|
||||
|
||||
- 全局状态管理
|
||||
- 单例服务
|
||||
- UI 逻辑
|
||||
- 游戏流程控制
|
||||
|
||||
## 支持的 ECS 框架
|
||||
|
||||
GFramework 采用可选集成的设计,你可以根据需求选择合适的 ECS 框架:
|
||||
|
||||
### Arch ECS(推荐)
|
||||
|
||||
[Arch](https://github.com/genaray/Arch) 是一个高性能的 C# ECS 框架,具有以下特点:
|
||||
|
||||
- ✅ **极致性能**:基于 Archetype 的内存布局,零 GC 分配
|
||||
- ✅ **简单易用**:清晰的 API,易于上手
|
||||
- ✅ **功能完整**:支持查询、过滤、并行处理等高级特性
|
||||
- ✅ **活跃维护**:社区活跃,持续更新
|
||||
|
||||
**安装方式**:
|
||||
|
||||
```bash
|
||||
dotnet add package GeWuYou.GFramework.Ecs.Arch
|
||||
```
|
||||
|
||||
**文档链接**:[Arch ECS 集成指南](./arch.md)
|
||||
|
||||
### 其他 ECS 框架
|
||||
|
||||
GFramework 的设计允许集成其他 ECS 框架,未来可能支持:
|
||||
|
||||
- **DefaultEcs**:轻量级 ECS 框架
|
||||
- **Entitas**:成熟的 ECS 框架,Unity 生态常用
|
||||
- **自定义 ECS**:你可以基于 GFramework 的模块系统实现自己的 ECS 集成
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 选择 ECS 框架
|
||||
|
||||
根据项目需求选择合适的 ECS 框架。对于大多数项目,我们推荐使用 Arch ECS。
|
||||
|
||||
### 2. 安装集成包
|
||||
|
||||
```bash
|
||||
# 安装 Arch ECS 集成包
|
||||
dotnet add package GeWuYou.GFramework.Ecs.Arch
|
||||
```
|
||||
|
||||
### 3. 注册 ECS 模块
|
||||
`UseArch(...)` 通过 `ArchitectureModuleRegistry` 注册服务模块。按当前源码与集成测试,它应在架构实例调用
|
||||
`Initialize()` 之前完成。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Architecture;
|
||||
using GFramework.Ecs.Arc;
|
||||
using Arch.Core;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Ecs.Arch.Abstractions;
|
||||
using GFramework.Ecs.Arch.Extensions;
|
||||
|
||||
public class GameArchitecture : Architecture
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
public GameArchitecture() : base(new ArchitectureConfiguration())
|
||||
{
|
||||
@ -97,41 +41,29 @@ public class GameArchitecture : Architecture
|
||||
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
// 显式注册 Arch ECS 模块
|
||||
this.UseArch(options =>
|
||||
{
|
||||
options.WorldCapacity = 2000;
|
||||
options.EnableStatistics = true;
|
||||
});
|
||||
RegisterSystem<MovementSystem>();
|
||||
}
|
||||
}
|
||||
|
||||
var architecture = new GameArchitecture()
|
||||
.UseArch(options =>
|
||||
{
|
||||
options.WorldCapacity = 2048;
|
||||
options.Priority = 50;
|
||||
});
|
||||
|
||||
architecture.Initialize();
|
||||
|
||||
var world = architecture.Context.GetService<World>();
|
||||
var ecsModule = architecture.Context.GetService<IArchEcsModule>();
|
||||
```
|
||||
|
||||
### 4. 定义组件
|
||||
|
||||
```csharp
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct Position(float x, float y)
|
||||
{
|
||||
public float X { get; set; } = x;
|
||||
public float Y { get; set; } = y;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct Velocity(float x, float y)
|
||||
{
|
||||
public float X { get; set; } = x;
|
||||
public float Y { get; set; } = y;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 创建系统
|
||||
### 3. 让 ECS 系统继承 `ArchSystemAdapter<float>`
|
||||
|
||||
```csharp
|
||||
using Arch.Core;
|
||||
using GFramework.Ecs.Arch;
|
||||
using GFramework.Ecs.Arch.Components;
|
||||
|
||||
public sealed class MovementSystem : ArchSystemAdapter<float>
|
||||
{
|
||||
@ -145,147 +77,60 @@ public sealed class MovementSystem : ArchSystemAdapter<float>
|
||||
|
||||
protected override void OnUpdate(in float deltaTime)
|
||||
{
|
||||
World.Query(in _query, (ref Position pos, ref Velocity vel) =>
|
||||
var frameDelta = deltaTime;
|
||||
|
||||
World.Query(in _query, (ref Position position, ref Velocity velocity) =>
|
||||
{
|
||||
pos.X += vel.X * deltaTime;
|
||||
pos.Y += vel.Y * deltaTime;
|
||||
position.X += velocity.X * frameDelta;
|
||||
position.Y += velocity.Y * frameDelta;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 注册系统
|
||||
### 4. 由宿主循环驱动更新
|
||||
|
||||
`IArchEcsModule` 继承自 `IServiceModule`,负责初始化和销毁;真正的帧更新通过 `Update(float deltaTime)` 显式触发。
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
this.UseArch();
|
||||
using GFramework.Ecs.Arch.Abstractions;
|
||||
|
||||
// 注册 ECS 系统
|
||||
RegisterSystem<MovementSystem>();
|
||||
public sealed class GameLoop
|
||||
{
|
||||
private readonly IArchEcsModule _ecsModule;
|
||||
|
||||
public GameLoop(IArchEcsModule ecsModule)
|
||||
{
|
||||
_ecsModule = ecsModule;
|
||||
}
|
||||
|
||||
public void Tick(float deltaTime)
|
||||
{
|
||||
_ecsModule.Update(deltaTime);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 设计理念
|
||||
## 阅读顺序
|
||||
|
||||
### 显式集成
|
||||
1. 先看本页,确认自己要的是运行时包还是契约包
|
||||
2. 需要默认实现时继续读 [`arch.md`](./arch.md)
|
||||
3. 只想保留共享边界时继续读 [`../abstractions/ecs-arch-abstractions.md`](../abstractions/ecs-arch-abstractions.md)
|
||||
4. 统一查阅 README / docs / XML 入口时回到 [`../api-reference/index.md`](../api-reference/index.md)
|
||||
|
||||
GFramework 采用显式集成的设计,而不是自动注册:
|
||||
## 类型族级 XML Inventory
|
||||
|
||||
```csharp
|
||||
// ✅ 显式注册 - 清晰、可控
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
this.UseArch(); // 明确表示使用 Arch ECS
|
||||
}
|
||||
}
|
||||
下表记录当前 `Ecs.Arch` family 的类型声明级 XML 基线,便于从 README、站内 landing 和源码之间建立一致的审计入口。
|
||||
|
||||
// ❌ 自动注册 - 隐式、难以控制
|
||||
// 只需引入包,自动注册(不推荐)
|
||||
```
|
||||
| 包 | 类型族 | 代表类型 | XML 状态 | 阅读重点 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `GFramework.Ecs.Arch` | 运行时装配与模块生命周期 | `ArchExtensions`、`ArchEcsModule` | 已覆盖 | `UseArch(...)` 的接入时机、`World` 注册、模块优先级 |
|
||||
| `GFramework.Ecs.Arch` | 系统桥接层 | `ArchSystemAdapter<T>` | 已覆盖 | GFramework `ISystem` 生命周期如何桥接到 Arch `ISystem<T>` |
|
||||
| `GFramework.Ecs.Arch` | 示例组件与系统 | `Position`、`Velocity`、`MovementSystem` | 已覆盖 | 查询写法、组件布局、最小可运行示例 |
|
||||
| `GFramework.Ecs.Arch.Abstractions` | 契约与配置对象 | `IArchEcsModule`、`IArchSystemAdapter<T>`、`ArchOptions` | 已覆盖 | 共享宿主循环、测试替身、跨程序集配置边界 |
|
||||
|
||||
**优势**:
|
||||
## 边界说明
|
||||
|
||||
- 清晰的依赖关系
|
||||
- 更好的 IDE 支持
|
||||
- 易于测试和调试
|
||||
- 符合 .NET 生态习惯
|
||||
|
||||
### 零依赖原则
|
||||
|
||||
如果你不使用 ECS,GFramework.Core 包不会引入任何 ECS 相关的依赖:
|
||||
|
||||
```xml
|
||||
<!-- GFramework.Core.csproj -->
|
||||
<ItemGroup>
|
||||
<!-- 无 Arch 依赖 -->
|
||||
</ItemGroup>
|
||||
|
||||
<!-- GFramework.Ecs.Arch.csproj -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Arch" Version="2.1.0" />
|
||||
<PackageReference Include="Arch.System" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
### 模块化设计
|
||||
|
||||
ECS 集成基于 GFramework 的模块系统:
|
||||
|
||||
```csharp
|
||||
// ECS 模块实现 IServiceModule 接口
|
||||
public sealed class ArchEcsModule : IArchEcsModule
|
||||
{
|
||||
public string ModuleName => nameof(ArchEcsModule);
|
||||
public int Priority => 50;
|
||||
public bool IsEnabled { get; }
|
||||
|
||||
public void Register(IIocContainer container) { }
|
||||
public void Initialize() { }
|
||||
public ValueTask DestroyAsync() { }
|
||||
public void Update(float deltaTime) { }
|
||||
}
|
||||
```
|
||||
|
||||
## 与传统架构结合
|
||||
|
||||
ECS 可以与 GFramework 的传统架构(Model、System、Utility)无缝结合:
|
||||
|
||||
```csharp
|
||||
// Model 存储全局状态
|
||||
public class GameStateModel : AbstractModel
|
||||
{
|
||||
public int Score { get; set; }
|
||||
public int Level { get; set; }
|
||||
}
|
||||
|
||||
// ECS System 处理实体逻辑
|
||||
public class EnemySpawnSystem : ArchSystemAdapter<float>
|
||||
{
|
||||
protected override void OnUpdate(in float deltaTime)
|
||||
{
|
||||
// 访问 Model
|
||||
var gameState = this.GetModel<GameStateModel>();
|
||||
|
||||
// 根据关卡生成敌人
|
||||
for (int i = 0; i < gameState.Level; i++)
|
||||
{
|
||||
World.Create(
|
||||
new Position(Random.Shared.Next(0, 100), 0),
|
||||
new Velocity(0, -1),
|
||||
new Health(50, 50)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 传统 System 处理游戏逻辑
|
||||
public class ScoreSystem : AbstractSystem
|
||||
{
|
||||
protected override void OnInit()
|
||||
{
|
||||
this.RegisterEvent<EnemyDestroyedEvent>(OnEnemyDestroyed);
|
||||
}
|
||||
|
||||
private void OnEnemyDestroyed(EnemyDestroyedEvent e)
|
||||
{
|
||||
var gameState = this.GetModel<GameStateModel>();
|
||||
gameState.Score += 100;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- [Arch ECS 集成指南](./arch.md) - 详细的 Arch ECS 使用文档
|
||||
|
||||
## 相关资源
|
||||
|
||||
- [Architecture 架构系统](../core/architecture.md)
|
||||
- [System 系统](../core/system.md)
|
||||
- [事件系统](../core/events.md)
|
||||
- 当前仓库没有交付其他可直接消费的 ECS 运行时包;旧文档把“未来可能支持的其他 ECS 框架”写成现有能力,会误导采用路径。
|
||||
- `GFramework.Ecs.Arch.Abstractions` 负责“边界”,`GFramework.Ecs.Arch` 负责“默认实现”。
|
||||
- 站内页面只维护可构建的 docs 链路;仓库根 README 和模块 README 继续承担包目录入口职责。
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Game
|
||||
description: GFramework.Game family 的运行时入口、采用顺序与 XML 阅读基线。
|
||||
---
|
||||
|
||||
# Game
|
||||
|
||||
`Game` 栏目对应 `GFramework.Game` 与 `GFramework.Game.Abstractions` 这层游戏运行时能力。
|
||||
@ -98,6 +103,16 @@ IStorage storage = new FileStorage("GameData", serializer);
|
||||
4. [setting](./setting.md)
|
||||
5. [scene](./scene.md) 或 [ui](./ui.md)
|
||||
|
||||
## Game Family XML 覆盖基线
|
||||
|
||||
下面这份 inventory 记录的是 `2026-04-23` 对 `Game` family 做的一轮轻量 XML 盘点结果:只统计公开 / 内部类型声明是否带 XML 注释,用来建立 README / landing / API 阅读链路;成员级 `param`、`returns`、`exception` 与生命周期说明仍需要后续波次继续细化。
|
||||
|
||||
| 模块 | 基线状态 | 代表类型 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| `GFramework.Game` | `56/56` 个类型声明已带 XML 注释 | `YamlConfigLoader`、`SettingsModel<TRepository>`、`SceneRouterBase`、`UiRouterBase` | 先看运行时默认实现、配置加载、设置编排和路由基类 |
|
||||
| `GFramework.Game.Abstractions` | `80/80` 个类型声明已带 XML 注释 | `IConfigRegistry`、`ISaveRepository<TSaveData>`、`ISettingsSystem`、`ISceneRouter`、`IUiRouter` | 再看契约层边界,决定项目哪些程序集只依赖接口 |
|
||||
| `GFramework.Game.SourceGenerators` | `2/2` 个类型声明已带 XML 注释 | `SchemaConfigGenerator`、`ConfigSchemaDiagnostics` | 最后看 schema 生成入口与诊断模型,确认配置系统的编译期链路 |
|
||||
|
||||
## 与真实接法的关系
|
||||
|
||||
这个栏目以源码、`*.csproj`、模块 `README.md` 与 `ai-libs/` 下已验证的参考接法为准。
|
||||
|
||||
131
docs/zh-CN/source-generators/cqrs-handler-registry-generator.md
Normal file
131
docs/zh-CN/source-generators/cqrs-handler-registry-generator.md
Normal file
@ -0,0 +1,131 @@
|
||||
---
|
||||
title: CQRS Handler Registry 生成器
|
||||
description: 为消费端程序集生成 CQRS handler registry,并在需要时附带精确 reflection fallback 元数据。
|
||||
---
|
||||
|
||||
# CQRS Handler Registry 生成器
|
||||
|
||||
`GFramework.Cqrs.SourceGenerators` 会在编译期为当前业务程序集生成 `ICqrsHandlerRegistry`,让 `GFramework.Cqrs`
|
||||
runtime 在注册 handlers 时优先走静态注册表,而不是先扫描整个程序集。
|
||||
|
||||
它服务的是 `Cqrs` 家族,不是独立运行时:
|
||||
|
||||
- 契约层:`GeWuYou.GFramework.Cqrs.Abstractions`
|
||||
- 默认 runtime:`GeWuYou.GFramework.Cqrs`
|
||||
- 编译期生成器:`GeWuYou.GFramework.Cqrs.SourceGenerators`
|
||||
|
||||
## 生成什么
|
||||
|
||||
当前生成器会分析消费端程序集中的:
|
||||
|
||||
- `IRequestHandler<,>`
|
||||
- `INotificationHandler<>`
|
||||
- `IStreamRequestHandler<,>`
|
||||
|
||||
然后输出两类结果:
|
||||
|
||||
1. 一个实现 `ICqrsHandlerRegistry` 的内部注册器类型
|
||||
2. 程序集级 `CqrsHandlerRegistryAttribute`
|
||||
|
||||
当某些 handler 不能被生成代码安全地直接引用时,还会补发:
|
||||
|
||||
- 程序集级 `CqrsReflectionFallbackAttribute`
|
||||
|
||||
这意味着运行时会先使用生成注册器完成可静态表达的映射,再只对剩余类型做补扫,而不是退回整程序集盲扫。
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
安装方式保持 runtime 包与生成器包版本一致,并把生成器作为编译期依赖引入:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GeWuYou.GFramework.Cqrs" Version="x.y.z" />
|
||||
<PackageReference Include="GeWuYou.GFramework.Cqrs.Abstractions" Version="x.y.z" />
|
||||
<PackageReference Include="GeWuYou.GFramework.Cqrs.SourceGenerators"
|
||||
Version="x.y.z"
|
||||
PrivateAssets="all"
|
||||
ExcludeAssets="runtime" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
运行时侧仍然按 `Core` 的标准入口注册程序集:
|
||||
|
||||
```csharp
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterCqrsHandlersFromAssembly(typeof(GameArchitecture).Assembly);
|
||||
}
|
||||
```
|
||||
|
||||
如果你的 handlers 分布在多个业务程序集里,则改用:
|
||||
|
||||
```csharp
|
||||
RegisterCqrsHandlersFromAssemblies(
|
||||
[
|
||||
typeof(InventoryCqrsMarker).Assembly,
|
||||
typeof(BattleCqrsMarker).Assembly
|
||||
]);
|
||||
```
|
||||
|
||||
文档示例统一用 marker 类型承载程序集引用。框架本身不要求固定目录或固定命名,但团队实践里可以把这类空 marker
|
||||
集中放在每个业务程序集自己的 `Application/Markers` 或等价目录,并采用 `InventoryCqrsMarker` 这类能直接看出来源
|
||||
的名字,避免多人协作时拿无关业务类型充当程序集定位锚点。
|
||||
|
||||
## 运行时如何消费生成结果
|
||||
|
||||
`Cqrs` runtime 当前的注册顺序是:
|
||||
|
||||
1. 先读取程序集上的 `CqrsHandlerRegistryAttribute`
|
||||
2. 优先激活生成的 `ICqrsHandlerRegistry`
|
||||
3. 若生成元数据损坏、registry 不可激活,记录告警并回退到反射路径
|
||||
4. 若存在 `CqrsReflectionFallbackAttribute`,只补扫剩余 handler
|
||||
5. 同一程序集按稳定键去重,避免重复注册
|
||||
|
||||
这个行为由 `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs` 和
|
||||
`GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 共同覆盖。
|
||||
|
||||
## 什么时候值得安装
|
||||
|
||||
推荐安装:
|
||||
|
||||
- 业务程序集内 handler 数量较多
|
||||
- 想把 handler 注册路径前移到编译期
|
||||
- 希望冷启动阶段减少整程序集反射扫描
|
||||
- 需要更明确地观察“哪些 handler 走静态注册,哪些只能走 fallback”
|
||||
|
||||
可以先不装:
|
||||
|
||||
- 项目体量很小,handler 很少
|
||||
- 当前只做原型,尚不关心注册成本
|
||||
- 你还没稳定到 `Cqrs` runtime 的最终接入边界
|
||||
|
||||
## fallback 边界
|
||||
|
||||
生成器并不会承诺“所有 handler 都能被静态表达”。
|
||||
|
||||
当前实现遵循一个保守原则:
|
||||
|
||||
- 能直接引用的 handler,生成直接注册语句
|
||||
- 实现类型不能直接引用、但服务接口还能精确表达时,生成反射实现类型查找
|
||||
- 服务接口本身也需要运行时解析时,生成精确 type lookup
|
||||
- 只有在 runtime 提供 `CqrsReflectionFallbackAttribute` 合同时,才允许发射依赖 fallback 的结果
|
||||
|
||||
如果当前编译环境缺少这个 fallback 合同,而某些 handler 又必须依赖它,生成器会报:
|
||||
|
||||
- `GF_Cqrs_001`
|
||||
|
||||
这条诊断的含义不是“某个 handler 写错了”,而是“当前 runtime 合同不足以安全承载这轮生成结果”。
|
||||
|
||||
## XML / API 阅读入口
|
||||
|
||||
如果你要核对生成器对外暴露的契约,优先看这些类型:
|
||||
|
||||
- `GFramework.Cqrs.ICqrsHandlerRegistry`
|
||||
- `GFramework.Cqrs.CqrsHandlerRegistryAttribute`
|
||||
- `GFramework.Cqrs.CqrsReflectionFallbackAttribute`
|
||||
- `GFramework.Cqrs.SourceGenerators.Cqrs.CqrsHandlerRegistryGenerator`
|
||||
|
||||
模块族入口见:
|
||||
|
||||
- [../core/cqrs.md](../core/cqrs.md)
|
||||
- [./index.md](./index.md)
|
||||
@ -65,7 +65,9 @@ GFramework 当前发布的生成器包是:
|
||||
|
||||
- 配置 schema 生成与运行时接法:
|
||||
- [../game/config-system.md](../game/config-system.md)
|
||||
- CQRS registry 生成入口:
|
||||
- CQRS handler registry 生成器:
|
||||
- [cqrs-handler-registry-generator](./cqrs-handler-registry-generator.md)
|
||||
- CQRS 模块族采用入口:
|
||||
- [../core/cqrs.md](../core/cqrs.md)
|
||||
|
||||
### Godot 专用生成器
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user