Compare commits

...

21 Commits

Author SHA1 Message Date
gewuyou
a980a042ae
Merge pull request #267 from GeWuYou/fix/analyzer-warning-reduction-batch
fix(core): 统一事件签名并清理MA0046告警
2026-04-21 18:21:38 +08:00
GeWuYou
a9f86348ff fix(core): 修复 AsyncLogAppender 刷新竞态
- 修复 AsyncLogAppender 在队列已被后台线程提前清空时 Flush 仍可能超时失败的问题
- 新增 AsyncLogAppender 已处理队列场景的稳定回归测试并重新验证 GFramework.Core.Tests
- 更新 analyzer-warning-reduction 的 tracking 与 trace 记录 PR267 failed-test follow-up
2026-04-21 17:32:50 +08:00
GeWuYou
685897f2de fix(core): 收口 PR267 事件契约遗留问题
- 修复 AsyncLogAppender 接口刷新路径重复触发完成事件,并补充单次通知回归测试
- 补充 Architecture、CoroutineExceptionEventArgs 与阶段协调器的事件契约注释
- 更新 PhaseChanged 迁移文档与 analyzer-warning-reduction recovery 记录
2026-04-21 16:50:56 +08:00
GeWuYou
8831cb42a8 fix(core): 统一事件签名并清理MA0046告警
hBc
2026-04-21 16:15:36 +08:00
gewuyou
9ccfed3ad9
Merge pull request #265 from GeWuYou/fix/analyzer-warning-reduction-batch
Fix/analyzer warning reduction batch
2026-04-21 15:08:56 +08:00
gewuyou
4d306498b9
Merge pull request #266 from GeWuYou/docs/sdk-update-documentation
docs(core): 收口 Core 事件属性与日志专题页
2026-04-21 14:09:11 +08:00
GeWuYou
4a779ac794 fix(tooling): 优化 PR review 输出收窄流程
- 新增 gframework-pr-review 脚本的 JSON 落盘、section 过滤与 path 过滤能力
- 更新文本输出截断与 skill 用法说明以减少超长 review JSON 漏看风险
- 更新 analyzer-warning-reduction 的 tracking 与 trace 以记录 RP-012 验证结果
2026-04-21 14:04:12 +08:00
GeWuYou
a5a35ce6ed docs(core): 收口 Core 事件属性与日志专题页
- 更新 Core events、property 与 logging 专题页,改回当前公开入口、边界与迁移建议
- 记录 state-management 与 coroutine 的复核结论,明确本轮无需继续机械改写
- 推进 documentation-governance-and-refresh 到 RP-005,并更新下一恢复点到 game 与 source-generators 栏目
2026-04-21 13:07:38 +08:00
GeWuYou
240fc761ed fix(events): 修复事件监听器计数偏差
- 修复 Event 泛型事件默认 no-op 委托导致的 GetListenerCount off-by-one
- 补充 Event 单参数与双参数监听器计数回归测试
- 更新 analyzer-warning-reduction 的 tracking 与 trace 以记录 RP-011 验证结果
2026-04-21 13:01:05 +08:00
GeWuYou
aa78dfbf51 fix(core): 修复 PR review 回归问题
- 修复 CoroutineScheduler 在零初始容量下的扩容边界并补充回归测试
- 修复 Store dispatch 快照阶段的异常回滚逻辑并补充异常安全测试
- 更新 analyzer-warning-reduction 的 tracking 与 trace 以记录 RP-010 验证结果
2026-04-21 12:56:28 +08:00
gewuyou
c61ee140a1
Merge branch 'main' into fix/analyzer-warning-reduction-batch 2026-04-21 12:44:28 +08:00
gewuyou
2c678cbdda
Merge pull request #264 from GeWuYou/docs/sdk-update-documentation
docs: Realign Core/Game documentation with current APIs and ai-libs reference implementations
2026-04-21 12:44:01 +08:00
GeWuYou
233195df91 docs(core): 补齐导航链接并收口追踪告警
- 更新 core landing page 的 Godot 与 Source Generators 导航入口为可点击链接
- 修复 documentation-governance-and-refresh active trace 的重复标题并消除 MD024 告警
- 补充 tracking 与 trace 的恢复点、验证记录和 PR review 跟进结论
2026-04-21 12:34:37 +08:00
GeWuYou
33c435bad5 refactor(core): 收拢泛型家族文件以清理MA0048
- 重构 Command 与 Query 抽象基类文件布局,合并同名泛型家族到基名文件
- 迁移泛型 Event 类型到 Event.cs,保持公共 API 与行为不变
- 更新 analyzer warning reduction 的 RP-009 跟踪与验证结果
2026-04-21 12:31:24 +08:00
GeWuYou
26d5d84d26 fix(pr-review): 修复 CodeRabbit 非空评审解析
- 修复最新 head commit 上空 APPROVED review 覆盖非空 COMMENTED review 的选择逻辑
- 保持最新 review 元数据输出不变,并新增用于结构化解析的非空 CodeRabbit review 选择
- 验证当前分支 PR 可重新提取 Nitpick comments
2026-04-21 12:30:53 +08:00
GeWuYou
035c7db18e docs(ai-plan): 更新 warning 批处理策略
- 更新 analyzer warning reduction 的恢复策略,明确按类型优先批处理
- 补充单次 boot 的文件改动上限与非冲突 subagent 并行规则
- 修正文档措辞,明确低数量时可顺手吸收其他低冲突类型而非特指 MA0015 和 MA0077
2026-04-21 11:40:32 +08:00
GeWuYou
f044aeb770 fix(analyzer): 收敛 CoroutineScheduler 长方法 warning
- 重构 CoroutineScheduler 的启动与完成清理阶段,降低 MA0051 并保持取消与完成语义
- 补充辅助方法注释,保留标签分组、统计和等待者唤醒顺序
- 更新 analyzer warning reduction 的恢复点与验证记录
2026-04-21 11:17:47 +08:00
GeWuYou
ec0c9a7bc8 fix(analyzer): 收敛 Store 长方法 warning
- 重构 Store 的 dispatch 进入提交退出阶段,降低 MA0051 并保持锁与通知语义
- 重构 reducer 快照创建流程,保留多态匹配的稳定排序规则
- 更新 analyzer warning reduction 的恢复点与验证记录
2026-04-21 10:30:20 +08:00
GeWuYou
60faf8eaff docs(core): 重写核心专题页文档
- 更新 architecture、context、lifecycle、command、query 与 cqrs 页面,使其对齐当前公开 API 与初始化语义
- 移除 Init、属性式总线、旧输入赋值示例和已移除的 Mediator 兼容入口等过时说明
- 补充 documentation-governance-and-refresh 主题的恢复点、验证结果与下一步专题页计划
2026-04-21 09:14:06 +08:00
GeWuYou
dfeb40ba15 docs(documentation): 更新 ai-libs 参考引用
- 更新 AGENTS、Game README 与游戏栏目入口中的参考表述,统一改为 ai-libs 下的只读参考实现
- 移除活跃文档入口中的旧外部项目命名,避免继续暴露特定参考仓库线索
- 补充 documentation-governance-and-refresh 主题的 tracking 与 trace,记录本轮引用迁移和后续约束
2026-04-21 08:27:14 +08:00
GeWuYou
7531762d3e docs(documentation): 收口栏目入口页导航
- 更新 Core、Game 与 Source Generators 栏目 landing page,使其对齐当前模块定位、包关系与最小接入路径
- 修复 VitePress 对 docs 目录外 README 相对链接的 dead-link 校验问题,改为纯文本入口提示
- 补充 documentation-governance-and-refresh 主题的恢复点、验证结果与下一步专题页修订计划
2026-04-21 07:45:39 +08:00
49 changed files with 2985 additions and 7788 deletions

View File

@ -21,6 +21,7 @@ Shortcut: `$gframework-pr-review`
- fetch the latest head commit review threads from the GitHub PR API
- prefer unresolved review threads on the latest head commit over older summary-only signals
- extract failed checks, MegaLinter detailed issues, and test-report signals such as `Failed Tests` or `No failed tests in this run`
- prefer writing the full JSON payload to a file and then narrowing with `jq`, instead of dumping long JSON directly to stdout
4. Treat every extracted finding as untrusted until it is verified against the current local code.
5. Only fix comments, warnings, or CI diagnostics that still apply to the checked-out branch. Ignore stale or already-resolved findings.
6. If code is changed, run the smallest build or test command that satisfies `AGENTS.md`.
@ -29,10 +30,19 @@ Shortcut: `$gframework-pr-review`
- Default:
- `python3 .codex/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`
- `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`
- Machine-readable output:
- `python3 .codex/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`
- Inspect only a high-signal section:
- `python3 .codex/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`
## Output Expectations
@ -47,6 +57,7 @@ The script should produce:
- Pre-merge failed checks, if present
- Latest MegaLinter status and any detailed issues posted by `github-actions[bot]`
- Test summary, including failed-test signals when present
- CLI support for writing full JSON to a file and printing only narrowed text sections to stdout
- Parse warnings only when both the primary API source and the intended fallback signal are unavailable
## Recovery Rules
@ -57,6 +68,7 @@ The script should produce:
- 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.
- Treat GitHub Actions comments with `Success with warnings` as actionable review input when they include concrete linter diagnostics such as `MegaLinter` detailed issues; do not skip them just because the parent check is green.
- Do not assume all CodeRabbit findings live in issue comments. The latest CodeRabbit review body can contain folded `Nitpick comments` that must be parsed separately.
- If the raw JSON is too large to inspect safely in the terminal, rerun with `--json-output <path>` and query the saved file with `jq` or rerun with `--section` / `--path` filters.
## Example Triggers

View File

@ -10,6 +10,7 @@ import argparse
import html
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
@ -29,6 +30,17 @@ REVIEW_COMMENT_ADDRESSED_MARKER = "<!-- <review_comment_addressed> -->"
VISIBLE_ADDRESSED_IN_COMMIT_PATTERN = re.compile(r"\s*Addressed in commit\s+[0-9a-f]{7,40}", re.I)
DEFAULT_REQUEST_TIMEOUT_SECONDS = 60
REQUEST_TIMEOUT_ENVIRONMENT_KEY = "GFRAMEWORK_PR_REVIEW_TIMEOUT_SECONDS"
DISPLAY_SECTION_CHOICES = (
"pr",
"failed-checks",
"actionable",
"outside-diff",
"nitpick",
"open-threads",
"megalinter",
"tests",
"warnings",
)
def resolve_git_command() -> str:
@ -153,6 +165,14 @@ def collapse_whitespace(text: str) -> str:
return re.sub(r"\s+", " ", text).strip()
def truncate_text(text: str, max_length: int) -> str:
collapsed = collapse_whitespace(text)
if max_length <= 0 or len(collapsed) <= max_length:
return collapsed
return collapsed[: max_length - 3].rstrip() + "..."
def strip_tags(text: str) -> str:
return collapse_whitespace(re.sub(r"<[^>]+>", " ", text))
@ -538,6 +558,27 @@ def build_latest_commit_review_threads(comments: list[dict[str, Any]]) -> list[d
return sorted(threads, key=lambda item: (item["path"], item["line"] or 0, item["thread_id"]))
def select_latest_submitted_review(
reviews: list[dict[str, Any]],
*,
required_user: str | None = None,
prefer_non_empty_body: bool = False,
) -> dict[str, Any] | None:
filtered_reviews = [review for review in reviews if review.get("submitted_at")]
if required_user is not None:
filtered_reviews = [review for review in filtered_reviews if review.get("user", {}).get("login") == required_user]
if not filtered_reviews:
return None
if prefer_non_empty_body:
non_empty_body_reviews = [review for review in filtered_reviews if str(review.get("body") or "").strip()]
if non_empty_body_reviews:
filtered_reviews = non_empty_body_reviews
return max(filtered_reviews, key=lambda review: review.get("submitted_at", ""))
def fetch_latest_commit_review(pr_number: int) -> dict[str, Any]:
api_base = f"https://api.github.com/repos/{OWNER}/{REPO}/pulls/{pr_number}"
commits = fetch_paged_json(f"{api_base}/commits?per_page=100")
@ -558,10 +599,11 @@ def fetch_latest_commit_review(pr_number: int) -> dict[str, Any]:
review for review in reviews if review.get("commit_id") == latest_commit_sha and review.get("submitted_at")
]
candidate_reviews = latest_commit_reviews or [review for review in reviews if review.get("submitted_at")]
latest_review = (
max(candidate_reviews, key=lambda review: review.get("submitted_at", ""))
if candidate_reviews
else None
latest_review = select_latest_submitted_review(candidate_reviews)
latest_coderabbit_review_with_body = select_latest_submitted_review(
candidate_reviews,
required_user=CODERABBIT_LOGIN,
prefer_non_empty_body=True,
)
latest_commit_comments = [comment for comment in comments if comment.get("commit_id") == latest_commit_sha]
@ -581,6 +623,18 @@ def fetch_latest_commit_review(pr_number: int) -> dict[str, Any]:
"user": latest_review.get("user", {}).get("login") if latest_review else "",
"body": latest_review.get("body") if latest_review else "",
},
"latest_coderabbit_review_with_body": {
"id": latest_coderabbit_review_with_body.get("id") if latest_coderabbit_review_with_body else None,
"state": latest_coderabbit_review_with_body.get("state") if latest_coderabbit_review_with_body else "",
"submitted_at": (
latest_coderabbit_review_with_body.get("submitted_at") if latest_coderabbit_review_with_body else ""
),
"commit_id": latest_coderabbit_review_with_body.get("commit_id") if latest_coderabbit_review_with_body else "",
"user": latest_coderabbit_review_with_body.get("user", {}).get("login")
if latest_coderabbit_review_with_body
else "",
"body": latest_coderabbit_review_with_body.get("body") if latest_coderabbit_review_with_body else "",
},
"threads": threads,
"open_threads": open_threads,
}
@ -621,7 +675,7 @@ def build_result(pr_number: int, branch: str) -> dict[str, Any]:
coderabbit_review: dict[str, Any] = {}
try:
latest_commit_review = fetch_latest_commit_review(pr_number)
latest_review = latest_commit_review.get("latest_review", {})
latest_review = latest_commit_review.get("latest_coderabbit_review_with_body", {})
latest_review_body = str(latest_review.get("body") or "")
if latest_review.get("user") == CODERABBIT_LOGIN and latest_review_body:
coderabbit_review = parse_latest_review_body(latest_review_body)
@ -676,64 +730,142 @@ def build_result(pr_number: int, branch: str) -> dict[str, Any]:
}
def format_text(result: dict[str, Any]) -> str:
def write_json_output(result: dict[str, Any], output_path: str) -> str:
destination_path = Path(output_path).expanduser()
destination_path.parent.mkdir(parents=True, exist_ok=True)
destination_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
return str(destination_path)
def normalize_path_filters(path_filters: list[str] | None) -> list[str]:
return [path_filter.replace("\\", "/") for path_filter in (path_filters or []) if path_filter.strip()]
def path_matches_filters(path: str, normalized_path_filters: list[str]) -> bool:
if not normalized_path_filters:
return True
normalized_path = path.replace("\\", "/")
return any(path_filter in normalized_path for path_filter in normalized_path_filters)
def filter_comments_by_path(
comments: list[dict[str, Any]],
normalized_path_filters: list[str],
) -> list[dict[str, Any]]:
return [comment for comment in comments if path_matches_filters(str(comment.get("path") or ""), normalized_path_filters)]
def filter_threads_by_path(
threads: list[dict[str, Any]],
normalized_path_filters: list[str],
) -> list[dict[str, Any]]:
return [thread for thread in threads if path_matches_filters(str(thread.get("path") or ""), normalized_path_filters)]
def format_text(
result: dict[str, Any],
*,
sections: list[str] | None = None,
path_filters: list[str] | None = None,
max_description_length: int = 400,
json_output_path: str | None = None,
) -> str:
lines: list[str] = []
selected_sections = set(sections or DISPLAY_SECTION_CHOICES)
normalized_path_filters = normalize_path_filters(path_filters)
pr = result["pull_request"]
lines.append(f"PR #{pr['number']}: {pr['title']}")
lines.append(f"State: {pr['state']}")
lines.append(f"Branch: {pr['head_branch']} -> {pr['base_branch']}")
lines.append(f"URL: {pr['url']}")
if "pr" in selected_sections:
lines.append(f"PR #{pr['number']}: {pr['title']}")
lines.append(f"State: {pr['state']}")
lines.append(f"Branch: {pr['head_branch']} -> {pr['base_branch']}")
lines.append(f"URL: {pr['url']}")
failed_checks = result["coderabbit_summary"].get("failed_checks", [])
lines.append("")
lines.append(f"Failed checks: {len(failed_checks)}")
for check in failed_checks:
lines.append(f"- {check['name']}: {check['status']}")
lines.append(f" Explanation: {check['explanation']}")
lines.append(f" Resolution: {check['resolution']}")
if "failed-checks" in selected_sections:
lines.append("")
lines.append(f"Failed checks: {len(failed_checks)}")
for check in failed_checks:
lines.append(f"- {check['name']}: {check['status']}")
lines.append(f" Explanation: {truncate_text(check['explanation'], max_description_length)}")
lines.append(f" Resolution: {truncate_text(check['resolution'], max_description_length)}")
coderabbit_comments = result.get("coderabbit_comments", {})
review_feedback = result.get("coderabbit_review", {})
comments = coderabbit_comments.get("comments", [])
visible_comments = filter_comments_by_path(comments, normalized_path_filters)
actionable_count = review_feedback.get("actionable_count") or coderabbit_comments.get("count") or len(comments)
lines.append("")
lines.append(f"CodeRabbit actionable comments: {actionable_count}")
for comment in comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {comment['title']}")
if comment["description"]:
lines.append(f" Description: {comment['description']}")
if actionable_count and not comments:
lines.append(" Details: see latest-commit review threads below.")
if "actionable" in selected_sections:
lines.append("")
lines.append(
f"CodeRabbit actionable comments: {actionable_count} total"
+ (
f", {len(visible_comments)} shown after path filter"
if normalized_path_filters
else ""
)
)
for comment in visible_comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {truncate_text(comment['title'], max_description_length)}")
if comment["description"]:
lines.append(f" Description: {truncate_text(comment['description'], max_description_length)}")
if actionable_count and not visible_comments:
lines.append(" Details: no actionable comments matched the current path filter.")
elif actionable_count and not comments:
lines.append(" Details: see latest-commit review threads below.")
outside_diff_comments = review_feedback.get("outside_diff_comments", [])
visible_outside_diff_comments = filter_comments_by_path(outside_diff_comments, normalized_path_filters)
outside_diff_count = review_feedback.get("outside_diff_count") or len(outside_diff_comments)
lines.append("")
lines.append(f"CodeRabbit outside-diff comments: {outside_diff_count} declared, {len(outside_diff_comments)} parsed")
for comment in outside_diff_comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {comment['title']}")
if comment["description"]:
lines.append(f" Description: {comment['description']}")
if "outside-diff" in selected_sections:
lines.append("")
lines.append(
f"CodeRabbit outside-diff comments: {outside_diff_count} declared, {len(outside_diff_comments)} parsed"
+ (
f", {len(visible_outside_diff_comments)} shown after path filter"
if normalized_path_filters
else ""
)
)
for comment in visible_outside_diff_comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {truncate_text(comment['title'], max_description_length)}")
if comment["description"]:
lines.append(f" Description: {truncate_text(comment['description'], max_description_length)}")
if outside_diff_comments and not visible_outside_diff_comments:
lines.append(" Details: no outside-diff comments matched the current path filter.")
nitpick_comments = review_feedback.get("nitpick_comments", [])
visible_nitpick_comments = filter_comments_by_path(nitpick_comments, normalized_path_filters)
nitpick_count = review_feedback.get("nitpick_count") or len(nitpick_comments)
lines.append("")
lines.append(f"CodeRabbit nitpick comments: {nitpick_count} declared, {len(nitpick_comments)} parsed")
for comment in nitpick_comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {comment['title']}")
if comment["description"]:
lines.append(f" Description: {comment['description']}")
if "nitpick" in selected_sections:
lines.append("")
lines.append(
f"CodeRabbit nitpick comments: {nitpick_count} declared, {len(nitpick_comments)} parsed"
+ (
f", {len(visible_nitpick_comments)} shown after path filter"
if normalized_path_filters
else ""
)
)
for comment in visible_nitpick_comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {truncate_text(comment['title'], max_description_length)}")
if comment["description"]:
lines.append(f" Description: {truncate_text(comment['description'], max_description_length)}")
if nitpick_comments and not visible_nitpick_comments:
lines.append(" Details: no nitpick comments matched the current path filter.")
latest_commit_review = result.get("latest_commit_review", {})
latest_commit = latest_commit_review.get("latest_commit", {})
latest_review = latest_commit_review.get("latest_review", {})
open_threads = latest_commit_review.get("open_threads", [])
if latest_commit:
visible_open_threads = filter_threads_by_path(open_threads, normalized_path_filters)
if latest_commit and "open-threads" in selected_sections:
lines.append("")
lines.append(f"Latest reviewed commit: {latest_commit.get('sha', '')}")
if latest_review:
@ -746,23 +878,32 @@ def format_text(result: dict[str, Any]) -> str:
lines.append(
"Latest commit review threads: "
f"{len(latest_commit_review.get('threads', []))} total, {len(open_threads)} open"
+ (
f", {len(visible_open_threads)} shown after path filter"
if normalized_path_filters
else ""
)
)
for thread in open_threads:
for thread in visible_open_threads:
root_comment = thread["root_comment"]
latest_comment = thread["latest_comment"]
lines.append(f"- {thread['path']}:{thread['line']}")
lines.append(f" Root by {root_comment['user']}: {collapse_whitespace(root_comment['body'])}")
lines.append(f" Root by {root_comment['user']}: {truncate_text(root_comment['body'], max_description_length)}")
if latest_comment["id"] != root_comment["id"]:
lines.append(f" Latest by {latest_comment['user']}: {collapse_whitespace(latest_comment['body'])}")
lines.append(
f" Latest by {latest_comment['user']}: {truncate_text(latest_comment['body'], max_description_length)}"
)
if contains_visible_addressed_commit_text(root_comment["body"]) or contains_visible_addressed_commit_text(
latest_comment["body"]
):
lines.append(
" Note: thread is still open; treat the visible 'Addressed in commit ...' text as unverified until local code matches."
)
if open_threads and not visible_open_threads:
lines.append(" Details: no open threads matched the current path filter.")
megalinter_report = result.get("megalinter_report", {})
if megalinter_report:
if megalinter_report and "megalinter" in selected_sections:
lines.append("")
lines.append(
"MegaLinter: "
@ -784,32 +925,37 @@ def format_text(result: dict[str, Any]) -> str:
for issue in megalinter_report.get("detailed_issues", []):
lines.append(f"- Detailed issue: {issue['summary']}")
lines.append(f" {collapse_whitespace(issue['details'])}")
lines.append(f" {truncate_text(issue['details'], max_description_length)}")
lines.append("")
lines.append(f"Test reports: {len(result['test_reports'])}")
for index, report in enumerate(result["test_reports"], start=1):
stats = report.get("stats", {})
if stats:
lines.append(
f"- Report {index}: tests={stats.get('tests')} passed={stats.get('passed')} "
f"failed={stats.get('failed')} skipped={stats.get('skipped')} flaky={stats.get('flaky')} "
f"duration={stats.get('duration')}"
)
else:
lines.append(f"- Report {index}: no structured test stats parsed")
if "tests" in selected_sections:
lines.append("")
lines.append(f"Test reports: {len(result['test_reports'])}")
for index, report in enumerate(result["test_reports"], start=1):
stats = report.get("stats", {})
if stats:
lines.append(
f"- Report {index}: tests={stats.get('tests')} passed={stats.get('passed')} "
f"failed={stats.get('failed')} skipped={stats.get('skipped')} flaky={stats.get('flaky')} "
f"duration={stats.get('duration')}"
)
else:
lines.append(f"- Report {index}: no structured test stats parsed")
if report["has_failed_tests"]:
for failed_test in report["failed_tests"]:
lines.append(f" Failed test: {failed_test}")
else:
lines.append(" Failed tests: none reported")
if report["has_failed_tests"]:
for failed_test in report["failed_tests"]:
lines.append(f" Failed test: {truncate_text(failed_test, max_description_length)}")
else:
lines.append(" Failed tests: none reported")
if result["parse_warnings"]:
if result["parse_warnings"] and "warnings" in selected_sections:
lines.append("")
lines.append("Warnings:")
for warning in result["parse_warnings"]:
lines.append(f"- {warning}")
lines.append(f"- {truncate_text(warning, max_description_length)}")
if json_output_path:
lines.append("")
lines.append(f"Full JSON written to: {json_output_path}")
return "\n".join(lines)
@ -819,6 +965,27 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--branch", help="Override the current branch name.")
parser.add_argument("--pr", type=int, help="Fetch a specific PR number instead of resolving from branch.")
parser.add_argument("--format", choices=("text", "json"), default="text")
parser.add_argument(
"--json-output",
help="Write the full JSON result to a file. When used with --format text, stdout stays concise and points to the file.",
)
parser.add_argument(
"--section",
action="append",
choices=DISPLAY_SECTION_CHOICES,
help="Limit text output to specific sections. Can be passed multiple times.",
)
parser.add_argument(
"--path",
action="append",
help="Only show comments and review threads whose path contains this fragment. Can be passed multiple times.",
)
parser.add_argument(
"--max-description-length",
type=int,
default=400,
help="Truncate long text bodies in text output to this many characters.",
)
return parser.parse_args()
@ -832,12 +999,27 @@ def main() -> None:
pr_number = resolve_pr_number(branch)
result = build_result(pr_number, branch)
json_output_path: str | None = None
if args.json_output:
json_output_path = write_json_output(result, args.json_output)
if args.format == "json":
if json_output_path:
print(json_output_path)
return
print(json.dumps(result, ensure_ascii=False, indent=2))
return
print(format_text(result))
print(
format_text(
result,
sections=args.section,
path_filters=args.path,
max_description_length=args.max_description_length,
json_output_path=json_output_path,
)
)
if __name__ == "__main__":

View File

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

View File

@ -0,0 +1,24 @@
using GFramework.Core.Abstractions.Enums;
namespace GFramework.Core.Abstractions.Architectures;
/// <summary>
/// 表示架构阶段变化事件的数据。
/// 该类型用于向事件订阅者传递当前已进入的阶段值。
/// </summary>
public sealed class ArchitecturePhaseChangedEventArgs : EventArgs
{
/// <summary>
/// 初始化 <see cref="ArchitecturePhaseChangedEventArgs" /> 的新实例。
/// </summary>
/// <param name="phase">当前已进入的架构阶段。</param>
public ArchitecturePhaseChangedEventArgs(ArchitecturePhase phase)
{
Phase = phase;
}
/// <summary>
/// 获取当前已进入的架构阶段。
/// </summary>
public ArchitecturePhase Phase { get; }
}

View File

@ -0,0 +1,26 @@
namespace GFramework.Core.Abstractions.Logging;
/// <summary>
/// 表示异步日志刷新完成事件的数据。
/// 该类型用于告知订阅者本次刷新是否在超时时间内成功完成。
/// </summary>
public sealed class AsyncLogFlushCompletedEventArgs : EventArgs
{
/// <summary>
/// 初始化 <see cref="AsyncLogFlushCompletedEventArgs" /> 的新实例。
/// </summary>
/// <param name="success">
/// 刷新是否成功完成。
/// 为 <see langword="true" /> 表示所有待处理日志都已在超时前落地;
/// 为 <see langword="false" /> 表示刷新超时或输出器已不可用。
/// </param>
public AsyncLogFlushCompletedEventArgs(bool success)
{
Success = success;
}
/// <summary>
/// 获取刷新是否成功完成。
/// </summary>
public bool Success { get; }
}

View File

@ -62,6 +62,35 @@ public class ArchitectureLifecycleBehaviorTests
await architecture.DestroyAsync();
}
/// <summary>
/// 验证阶段变更事件会以架构实例作为 sender并通过事件参数暴露阶段值。
/// </summary>
[Test]
public async Task InitializeAsync_Should_Raise_PhaseChanged_With_Sender_And_EventArgs()
{
var architecture = new PhaseTrackingArchitecture();
var observations = new List<(object? Sender, ArchitecturePhase Phase)>();
architecture.PhaseChanged += (sender, eventArgs) => observations.Add((sender, eventArgs.Phase));
await architecture.InitializeAsync();
Assert.That(observations, Is.Not.Empty);
Assert.That(observations.All(item => ReferenceEquals(item.Sender, architecture)), Is.True);
Assert.That(observations.Select(static item => item.Phase), Is.EqualTo(new[]
{
ArchitecturePhase.BeforeUtilityInit,
ArchitecturePhase.AfterUtilityInit,
ArchitecturePhase.BeforeModelInit,
ArchitecturePhase.AfterModelInit,
ArchitecturePhase.BeforeSystemInit,
ArchitecturePhase.AfterSystemInit,
ArchitecturePhase.Ready
}));
await architecture.DestroyAsync();
}
/// <summary>
/// 验证用户初始化失败时,等待 Ready 的任务会失败并进入 FailedInitialization 阶段。
/// </summary>
@ -183,7 +212,7 @@ public class ArchitectureLifecycleBehaviorTests
public PhaseTrackingArchitecture(Action? onInitializeAction = null)
{
_onInitializeAction = onInitializeAction;
PhaseChanged += phase => PhaseHistory.Add(phase);
PhaseChanged += (_, eventArgs) => PhaseHistory.Add(eventArgs.Phase);
}
/// <summary>
@ -214,7 +243,7 @@ public class ArchitectureLifecycleBehaviorTests
public DestroyOrderArchitecture(List<string> destroyOrder)
{
_destroyOrder = destroyOrder;
PhaseChanged += phase => PhaseHistory.Add(phase);
PhaseChanged += (_, eventArgs) => PhaseHistory.Add(eventArgs.Phase);
}
/// <summary>
@ -247,7 +276,7 @@ public class ArchitectureLifecycleBehaviorTests
public FailingInitializationArchitecture(List<string> destroyOrder)
{
_destroyOrder = destroyOrder;
PhaseChanged += phase => PhaseHistory.Add(phase);
PhaseChanged += (_, eventArgs) => PhaseHistory.Add(eventArgs.Phase);
}
/// <summary>

View File

@ -43,6 +43,6 @@ public abstract class TestArchitectureBase : Architecture
_postRegistrationHook?.Invoke(this);
// 订阅阶段变更事件以记录历史
PhaseChanged += phase => PhaseHistory.Add(phase);
PhaseChanged += (_, eventArgs) => PhaseHistory.Add(eventArgs.Phase);
}
}
}

View File

@ -331,6 +331,63 @@ public class CoroutineSchedulerTests
Assert.That(_scheduler.ActiveCoroutineCount, Is.EqualTo(0));
}
/// <summary>
/// 验证完成事件会把调度器实例、句柄和完成结果暴露给订阅者。
/// </summary>
[Test]
public void Run_Should_Raise_OnCoroutineFinished_With_EventArgs()
{
object? observedSender = null;
CoroutineFinishedEventArgs? observedArgs = null;
_scheduler.OnCoroutineFinished += (sender, eventArgs) =>
{
observedSender = sender;
observedArgs = eventArgs;
};
var handle = _scheduler.Run(CreateSimpleCoroutine());
_scheduler.Update();
Assert.Multiple(() =>
{
Assert.That(observedSender, Is.SameAs(_scheduler));
Assert.That(observedArgs, Is.Not.Null);
Assert.That(observedArgs!.Handle, Is.EqualTo(handle));
Assert.That(observedArgs.CompletionStatus, Is.EqualTo(CoroutineCompletionStatus.Completed));
Assert.That(observedArgs.Exception, Is.Null);
});
}
/// <summary>
/// 验证异常事件会把调度器实例、失败句柄和异常对象暴露给订阅者。
/// </summary>
[Test]
public async Task Scheduler_Should_Raise_OnCoroutineException_With_EventArgs()
{
var exceptionSource =
new TaskCompletionSource<(object? Sender, CoroutineExceptionEventArgs EventArgs)>(
TaskCreationOptions.RunContinuationsAsynchronously);
_scheduler.OnCoroutineException += (sender, eventArgs) =>
{
exceptionSource.TrySetResult((sender, eventArgs));
};
var handle = _scheduler.Run(CreateExceptionCoroutine());
_scheduler.Update();
var observation = await exceptionSource.Task.WaitAsync(TimeSpan.FromSeconds(3));
Assert.Multiple(() =>
{
Assert.That(observation.Sender, Is.SameAs(_scheduler));
Assert.That(observation.EventArgs.Handle, Is.EqualTo(handle));
Assert.That(observation.EventArgs.Exception, Is.TypeOf<InvalidOperationException>());
Assert.That(observation.EventArgs.Exception.Message, Is.EqualTo("Test exception"));
});
}
/// <summary>
/// 验证协程调度器应该扩展容量当槽位已满
/// </summary>
@ -345,6 +402,20 @@ public class CoroutineSchedulerTests
Assert.That(_scheduler.ActiveCoroutineCount, Is.EqualTo(10));
}
/// <summary>
/// 验证调度器在零初始容量下会在首次启动协程时自动扩容,而不是写入越界。
/// </summary>
[Test]
public void Run_Should_Grow_From_Zero_Initial_Capacity()
{
var scheduler = new CoroutineScheduler(new TestTimeSource(), initialCapacity: 0);
var handle = scheduler.Run(CreateYieldingCoroutine(new WaitOneFrame()));
Assert.That(handle.IsValid, Is.True);
Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(1));
}
/// <summary>
/// 验证协程调度器应该使用提供的时间源
/// </summary>
@ -563,4 +634,4 @@ public class TestTimeSource : ITimeSource
DeltaTime = 0.1;
CurrentTime += DeltaTime;
}
}
}

View File

@ -124,6 +124,24 @@ public class EventTests
Assert.That(values, Does.Contain(10));
}
/// <summary>
/// 测试单参数事件的监听器计数只统计真实注册的处理器。
/// </summary>
[Test]
public void EventT_GetListenerCount_Should_Exclude_Placeholder_Handler()
{
Assert.That(_eventInt.GetListenerCount(), Is.EqualTo(0));
Action<int> handler = _ => { };
_eventInt.Register(handler);
Assert.That(_eventInt.GetListenerCount(), Is.EqualTo(1));
_eventInt.UnRegister(handler);
Assert.That(_eventInt.GetListenerCount(), Is.EqualTo(0));
}
/// <summary>
/// 测试带两个泛型参数的事件注册功能是否正确添加处理器
/// </summary>
@ -161,4 +179,22 @@ public class EventTests
_eventIntString.Trigger(2, "b");
Assert.That(count, Is.EqualTo(1));
}
}
/// <summary>
/// 测试双参数事件的监听器计数只统计真实注册的处理器。
/// </summary>
[Test]
public void EventTTK_GetListenerCount_Should_Exclude_Placeholder_Handler()
{
Assert.That(_eventIntString.GetListenerCount(), Is.EqualTo(0));
Action<int, string> handler = (_, _) => { };
_eventIntString.Register(handler);
Assert.That(_eventIntString.GetListenerCount(), Is.EqualTo(1));
_eventIntString.UnRegister(handler);
Assert.That(_eventIntString.GetListenerCount(), Is.EqualTo(0));
}
}

View File

@ -77,6 +77,73 @@ public class AsyncLogAppenderTests
Assert.That(innerAppender.Entries.Count, Is.EqualTo(100));
}
[Test]
public void Flush_Should_Raise_OnFlushCompleted_With_Sender_And_Result()
{
var innerAppender = new TestAppender();
using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 10);
object? observedSender = null;
AsyncLogFlushCompletedEventArgs? observedArgs = null;
asyncAppender.Append(new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", "Flush check", null, null));
asyncAppender.OnFlushCompleted += (sender, eventArgs) =>
{
observedSender = sender;
observedArgs = eventArgs;
};
var result = asyncAppender.Flush(TimeSpan.FromSeconds(1));
Assert.Multiple(() =>
{
Assert.That(observedSender, Is.SameAs(asyncAppender));
Assert.That(observedArgs, Is.Not.Null);
Assert.That(observedArgs!.Success, Is.EqualTo(result));
});
}
[Test]
public void ILogAppender_Flush_Should_Raise_OnFlushCompleted_Only_Once()
{
var innerAppender = new TestAppender();
using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 10);
ILogAppender logAppender = asyncAppender;
var observedResults = new List<bool>();
asyncAppender.Append(new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", "Interface flush check", null, null));
asyncAppender.OnFlushCompleted += (_, eventArgs) => observedResults.Add(eventArgs.Success);
logAppender.Flush();
Assert.That(observedResults, Has.Count.EqualTo(1));
Assert.That(observedResults, Has.All.True);
}
[Test]
public void Flush_WhenEntriesAlreadyProcessed_Should_Still_ReportSuccess()
{
using var appendCompleted = new ManualResetEventSlim();
var innerAppender = new SignalingAppender(appendCompleted);
using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 10);
var observedResults = new List<bool>();
asyncAppender.Append(new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", "Already processed", null, null));
Assert.That(appendCompleted.Wait(TimeSpan.FromSeconds(1)), Is.True);
asyncAppender.OnFlushCompleted += (_, eventArgs) => observedResults.Add(eventArgs.Success);
var result = asyncAppender.Flush(TimeSpan.FromSeconds(1));
Assert.Multiple(() =>
{
Assert.That(result, Is.True);
Assert.That(observedResults, Has.Count.EqualTo(1));
Assert.That(observedResults, Has.All.True);
Assert.That(innerAppender.FlushCount, Is.EqualTo(1));
});
}
[Test]
public void Dispose_ShouldProcessRemainingEntries()
{
@ -265,6 +332,32 @@ public class AsyncLogAppenderTests
}
}
private sealed class SignalingAppender : ILogAppender
{
private readonly ManualResetEventSlim _appendCompleted;
public SignalingAppender(ManualResetEventSlim appendCompleted)
{
_appendCompleted = appendCompleted;
}
public int FlushCount { get; private set; }
public void Append(LogEntry entry)
{
_appendCompleted.Set();
}
public void Flush()
{
FlushCount++;
}
public void Dispose()
{
}
}
private class ThrowingAppender : ILogAppender
{
public void Append(LogEntry entry)
@ -296,4 +389,4 @@ public class AsyncLogAppenderTests
{
}
}
}
}

View File

@ -1,3 +1,6 @@
using System.Collections;
using System.Reflection;
namespace GFramework.Core.Tests.StateManagement;
/// <summary>
@ -384,6 +387,32 @@ public class StoreTests
Assert.That(logs, Is.EqualTo(new[] { "dynamic:before", "dynamic:after" }));
}
/// <summary>
/// 测试当 dispatch 作用域在快照阶段抛出异常时Store 不会残留“正在分发”标记而锁死后续调用。
/// </summary>
[Test]
public void Dispatch_Should_Reset_Dispatching_Flag_When_Snapshot_Creation_Throws()
{
var store = new Store<CounterState>(
new CounterState(0, "Player"),
actionMatchingMode: StoreActionMatchingMode.IncludeAssignableTypes);
store.RegisterReducer<IncrementAction>((state, action) => state with { Count = state.Count + action.Amount });
var reducers = GetReducersDictionary(store);
var throwingActionType = new ThrowingAssignableType(typeof(IncrementAction), "simulated reducer snapshot failure");
reducers.Add(throwingActionType, CreateEmptyReducerRegistrationList(reducers));
Assert.That(
() => store.Dispatch(new IncrementAction(1)),
Throws.InvalidOperationException.With.Message.EqualTo("simulated reducer snapshot failure"));
reducers.Remove(throwingActionType);
Assert.That(() => store.Dispatch(new IncrementAction(1)), Throws.Nothing);
Assert.That(store.State.Count, Is.EqualTo(1));
}
/// <summary>
/// 测试未命中的 action 仍会记录诊断信息,但不会改变状态。
/// </summary>
@ -681,6 +710,32 @@ public class StoreTests
return store;
}
/// <summary>
/// 读取 Store 内部 reducer 字典,以便在异常安全回归测试中注入受控的异常源。
/// </summary>
/// <param name="store">要读取的 Store 实例。</param>
/// <returns>Store 当前持有的 reducer 字典引用。</returns>
private static IDictionary GetReducersDictionary(Store<CounterState> store)
{
var reducersField = typeof(Store<CounterState>).GetField("_reducers", BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new InvalidOperationException("Unable to locate Store reducer dictionary field.");
return (IDictionary)(reducersField.GetValue(store)
?? throw new InvalidOperationException("Store reducer dictionary should not be null."));
}
/// <summary>
/// 创建与 Store 私有 reducer 注册列表兼容的空列表实例。
/// </summary>
/// <param name="reducers">现有 reducer 字典,用于推断私有列表元素类型。</param>
/// <returns>可写入私有 reducer 字典的空列表。</returns>
private static object CreateEmptyReducerRegistrationList(IDictionary reducers)
{
var valueType = reducers.GetType().GenericTypeArguments[1];
return Activator.CreateInstance(valueType)
?? throw new InvalidOperationException("Unable to create an empty reducer registration list.");
}
/// <summary>
/// 用于测试的计数器状态。
/// 使用 record 保持逻辑不可变语义,便于 Store 基于状态快照进行比较和断言。
@ -876,4 +931,20 @@ public class StoreTests
next();
}
}
}
/// <summary>
/// 用于在回归测试中稳定模拟 <see cref="Type.IsAssignableFrom(Type)"/> 失败的代理类型。
/// </summary>
private sealed class ThrowingAssignableType(Type delegatingType, string message) : TypeDelegator(delegatingType)
{
/// <summary>
/// 在 Store 创建 reducer 快照时抛出受控异常,验证 dispatch 作用域能够正确回滚。
/// </summary>
/// <param name="typeInfo">待比较的 action 运行时类型。</param>
/// <returns>此实现不会正常返回。</returns>
public override bool IsAssignableFrom(Type? typeInfo)
{
throw new InvalidOperationException(message);
}
}
}

View File

@ -49,6 +49,7 @@ public abstract class Architecture : IArchitecture
// 初始化管理器
_bootstrapper = new ArchitectureBootstrapper(GetType(), resolvedEnvironment, resolvedServices, _logger);
_lifecycle = new ArchitectureLifecycle(this, resolvedConfiguration, resolvedServices, _logger);
_lifecycle.PhaseChanged += HandleLifecyclePhaseChanged;
_componentRegistry = new ArchitectureComponentRegistry(
this,
resolvedConfiguration,
@ -98,13 +99,17 @@ public abstract class Architecture : IArchitecture
public virtual Action<IServiceCollection>? Configurator => null;
/// <summary>
/// 阶段变更事件(用于测试和扩展)
/// 在架构生命周期阶段发生变化时触发。
/// </summary>
public event Action<ArchitecturePhase>? PhaseChanged
{
add => _lifecycle.PhaseChanged += value;
remove => _lifecycle.PhaseChanged -= value;
}
/// <remarks>
/// <para>
/// 订阅者应通过 <see cref="ArchitecturePhaseChangedEventArgs.Phase" /> 读取当前阶段,而不是依赖内部生命周期对象。
/// </para>
/// <para>
/// 事件委托中的 <c>sender</c> 始终为当前 <see cref="Architecture" /> 实例,便于测试与外部扩展保持稳定的发布者契约。
/// </para>
/// </remarks>
public event EventHandler<ArchitecturePhaseChangedEventArgs>? PhaseChanged;
#endregion
@ -142,6 +147,21 @@ public abstract class Architecture : IArchitecture
#endregion
#region Event Relays
/// <summary>
/// 把生命周期协作者的阶段广播重新映射到当前架构实例,
/// 以便公开事件的 sender 始终反映真实的架构发布者。
/// </summary>
/// <param name="sender">生命周期协作者实例。</param>
/// <param name="eventArgs">阶段变化事件数据。</param>
private void HandleLifecyclePhaseChanged(object? sender, ArchitecturePhaseChangedEventArgs eventArgs)
{
PhaseChanged?.Invoke(this, eventArgs);
}
#endregion
#region Module Management
/// <summary>

View File

@ -71,6 +71,7 @@ internal sealed class ArchitectureLifecycle(
public void EnterPhase(ArchitecturePhase next)
{
_phaseCoordinator.EnterPhase(next);
PhaseChanged?.Invoke(this, new ArchitecturePhaseChangedEventArgs(next));
}
#endregion
@ -127,11 +128,7 @@ internal sealed class ArchitectureLifecycle(
/// <summary>
/// 阶段变更事件(用于测试和扩展)
/// </summary>
public event Action<ArchitecturePhase>? PhaseChanged
{
add => _phaseCoordinator.PhaseChanged += value;
remove => _phaseCoordinator.PhaseChanged -= value;
}
public event EventHandler<ArchitecturePhaseChangedEventArgs>? PhaseChanged;
#endregion

View File

@ -22,12 +22,6 @@ internal sealed class ArchitecturePhaseCoordinator(
/// </summary>
public ArchitecturePhase CurrentPhase { get; private set; }
/// <summary>
/// 在架构阶段变更时触发。
/// 该事件用于测试和扩展场景,保持现有公共行为不变。
/// </summary>
public event Action<ArchitecturePhase>? PhaseChanged;
/// <summary>
/// 注册一个生命周期钩子。
/// 就绪后是否允许追加注册由架构配置控制,以保证阶段回调的一致性。
@ -45,8 +39,8 @@ internal sealed class ArchitecturePhaseCoordinator(
/// <summary>
/// 进入指定阶段并广播给所有阶段消费者。
/// 顺序保持为“更新阶段值 → 生命周期钩子 → 容器中的阶段监听器 → 外部事件”,
/// 以兼容既有调用约定
/// 顺序保持为“更新阶段值 → 生命周期钩子 → 容器中的阶段监听器”,
/// 以保证框架扩展与运行时组件看到一致的阶段视图
/// </summary>
/// <param name="next">目标阶段。</param>
public void EnterPhase(ArchitecturePhase next)
@ -61,7 +55,6 @@ internal sealed class ArchitecturePhaseCoordinator(
NotifyLifecycleHooks(next);
NotifyPhaseListeners(next);
PhaseChanged?.Invoke(next);
}
/// <summary>
@ -113,4 +106,4 @@ internal sealed class ArchitecturePhaseCoordinator(
listener.OnArchitecturePhase(phase);
}
}
}
}

View File

@ -1,5 +1,6 @@
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
namespace GFramework.Core.Command;
@ -26,3 +27,54 @@ public abstract class AbstractAsyncCommand : ContextAwareBase, IAsyncCommand
/// <returns>表示异步操作的任务</returns>
protected abstract Task OnExecuteAsync();
}
/// <summary>
/// 抽象异步命令基类,为需要命令输入且无返回值的异步命令提供统一执行骨架。
/// </summary>
/// <typeparam name="TInput">命令输入类型,必须实现 <see cref="ICommandInput" /> 接口。</typeparam>
/// <param name="input">命令输入参数。</param>
public abstract class AbstractAsyncCommand<TInput>(TInput input) : ContextAwareBase, IAsyncCommand
where TInput : ICommandInput
{
/// <summary>
/// 执行异步命令的实现方法。
/// </summary>
/// <returns>表示异步操作的任务。</returns>
async Task IAsyncCommand.ExecuteAsync()
{
await OnExecuteAsync(input).ConfigureAwait(false);
}
/// <summary>
/// 定义异步执行逻辑的抽象方法,由派生类实现具体业务逻辑。
/// </summary>
/// <param name="input">命令输入参数。</param>
/// <returns>表示异步操作的任务。</returns>
protected abstract Task OnExecuteAsync(TInput input);
}
/// <summary>
/// 抽象异步命令基类,为需要命令输入且返回结果的异步命令提供统一执行骨架。
/// </summary>
/// <typeparam name="TInput">命令输入类型,必须实现 <see cref="ICommandInput" /> 接口。</typeparam>
/// <typeparam name="TResult">命令执行结果类型。</typeparam>
/// <param name="input">命令输入参数。</param>
public abstract class AbstractAsyncCommand<TInput, TResult>(TInput input) : ContextAwareBase, IAsyncCommand<TResult>
where TInput : ICommandInput
{
/// <summary>
/// 执行异步命令并返回结果的实现方法。
/// </summary>
/// <returns>表示异步操作且包含结果的任务。</returns>
async Task<TResult> IAsyncCommand<TResult>.ExecuteAsync()
{
return await OnExecuteAsync(input).ConfigureAwait(false);
}
/// <summary>
/// 定义异步执行逻辑的抽象方法,由派生类实现具体业务逻辑并返回结果。
/// </summary>
/// <param name="input">命令输入参数。</param>
/// <returns>表示异步操作且包含结果的任务。</returns>
protected abstract Task<TResult> OnExecuteAsync(TInput input);
}

View File

@ -1,29 +0,0 @@
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
namespace GFramework.Core.Command;
/// <summary>
/// 抽象异步命令基类,用于处理无返回值的异步命令操作
/// </summary>
/// <typeparam name="TInput">命令输入类型必须实现ICommandInput接口</typeparam>
public abstract class AbstractAsyncCommand<TInput>(TInput input) : ContextAwareBase, IAsyncCommand
where TInput : ICommandInput
{
/// <summary>
/// 执行异步命令的实现方法
/// </summary>
/// <returns>表示异步操作的任务</returns>
async Task IAsyncCommand.ExecuteAsync()
{
await OnExecuteAsync(input).ConfigureAwait(false);
}
/// <summary>
/// 定义异步执行逻辑的抽象方法,由派生类实现具体业务逻辑
/// </summary>
/// <param name="input">命令输入参数</param>
/// <returns>表示异步操作的任务</returns>
protected abstract Task OnExecuteAsync(TInput input);
}

View File

@ -1,30 +0,0 @@
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
namespace GFramework.Core.Command;
/// <summary>
/// 抽象异步命令基类,用于处理有返回值的异步命令操作
/// </summary>
/// <typeparam name="TInput">命令输入类型必须实现ICommandInput接口</typeparam>
/// <typeparam name="TResult">命令执行结果类型</typeparam>
public abstract class AbstractAsyncCommand<TInput, TResult>(TInput input) : ContextAwareBase, IAsyncCommand<TResult>
where TInput : ICommandInput
{
/// <summary>
/// 执行异步命令并返回结果的实现方法
/// </summary>
/// <returns>表示异步操作且包含结果的任务</returns>
async Task<TResult> IAsyncCommand<TResult>.ExecuteAsync()
{
return await OnExecuteAsync(input).ConfigureAwait(false);
}
/// <summary>
/// 定义异步执行逻辑的抽象方法,由派生类实现具体业务逻辑并返回结果
/// </summary>
/// <param name="input">命令输入参数</param>
/// <returns>表示异步操作且包含结果的任务</returns>
protected abstract Task<TResult> OnExecuteAsync(TInput input);
}

View File

@ -1,5 +1,7 @@
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
namespace GFramework.Core.Command;
@ -20,4 +22,55 @@ public abstract class AbstractCommand : ContextAwareBase, ICommand
/// 命令执行的抽象方法,由派生类实现具体的命令逻辑
/// </summary>
protected abstract void OnExecute();
}
}
/// <summary>
/// 抽象命令类,实现 <see cref="ICommand" /> 接口,为需要命令输入的具体命令提供基础架构支持。
/// </summary>
/// <typeparam name="TInput">命令输入参数类型,必须实现 <see cref="ICommandInput" /> 接口。</typeparam>
/// <param name="input">命令执行所需的输入参数。</param>
public abstract class AbstractCommand<TInput>(TInput input) : ContextAwareBase, ICommand
where TInput : ICommandInput
{
/// <summary>
/// 执行命令的入口方法,实现 <see cref="ICommand" /> 接口的 <c>Execute</c> 方法。
/// </summary>
void ICommand.Execute()
{
OnExecute(input);
}
/// <summary>
/// 命令执行的抽象方法,由派生类实现具体的命令逻辑。
/// </summary>
/// <param name="input">命令执行所需的输入参数。</param>
protected abstract void OnExecute(TInput input);
}
/// <summary>
/// 带返回值的抽象命令类,为需要输入和返回值的命令提供统一执行骨架。
/// </summary>
/// <typeparam name="TInput">命令输入参数类型,必须实现 <see cref="ICommandInput" /> 接口。</typeparam>
/// <typeparam name="TResult">命令执行后返回的结果类型。</typeparam>
/// <param name="input">命令执行所需的输入参数。</param>
public abstract class AbstractCommand<TInput, TResult>(TInput input)
: ContextAwareBase, GFramework.Core.Abstractions.Command.ICommand<TResult>
where TInput : ICommandInput
{
/// <summary>
/// 执行命令的入口方法,实现 <see cref="GFramework.Core.Abstractions.Command.ICommand{TResult}" /> 接口的
/// <c>Execute</c> 方法。
/// </summary>
/// <returns>命令执行后的结果。</returns>
TResult GFramework.Core.Abstractions.Command.ICommand<TResult>.Execute()
{
return OnExecute(input);
}
/// <summary>
/// 命令执行的抽象方法,由派生类实现具体的命令逻辑。
/// </summary>
/// <param name="input">命令执行所需的输入参数。</param>
/// <returns>命令执行后的结果。</returns>
protected abstract TResult OnExecute(TInput input);
}

View File

@ -1,28 +0,0 @@
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
namespace GFramework.Core.Command;
/// <summary>
/// 抽象命令类,实现 ICommand 接口,为具体命令提供基础架构支持
/// </summary>
/// <typeparam name="TInput">命令输入参数类型,必须实现 ICommandInput 接口</typeparam>
/// <param name="input">命令执行所需的输入参数</param>
public abstract class AbstractCommand<TInput>(TInput input) : ContextAwareBase, ICommand
where TInput : ICommandInput
{
/// <summary>
/// 执行命令的入口方法,实现 ICommand 接口的 Execute 方法
/// </summary>
void ICommand.Execute()
{
OnExecute(input);
}
/// <summary>
/// 命令执行的抽象方法,由派生类实现具体的命令逻辑
/// </summary>
/// <param name="input">命令执行所需的输入参数</param>
protected abstract void OnExecute(TInput input);
}

View File

@ -1,31 +0,0 @@
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
namespace GFramework.Core.Command;
/// <summary>
/// 带返回值的抽象命令类,实现 ICommand{TResult} 接口,为需要返回结果的命令提供基础架构支持
/// </summary>
/// <typeparam name="TInput">命令输入参数类型,必须实现 ICommandInput 接口</typeparam>
/// <typeparam name="TResult">命令执行后返回的结果类型</typeparam>
/// <param name="input">命令执行所需的输入参数</param>
public abstract class AbstractCommand<TInput, TResult>(TInput input)
: ContextAwareBase, Abstractions.Command.ICommand<TResult>
where TInput : ICommandInput
{
/// <summary>
/// 执行命令的入口方法,实现 ICommand{TResult} 接口的 Execute 方法
/// </summary>
/// <returns>命令执行后的结果</returns>
TResult Abstractions.Command.ICommand<TResult>.Execute()
{
return OnExecute(input);
}
/// <summary>
/// 命令执行的抽象方法,由派生类实现具体的命令逻辑
/// </summary>
/// <param name="input">命令执行所需的输入参数</param>
/// <returns>命令执行后的结果</returns>
protected abstract TResult OnExecute(TInput input);
}

View File

@ -0,0 +1,30 @@
namespace GFramework.Core.Coroutine;
/// <summary>
/// 表示协程异常事件的数据。
/// 该类型用于把失败协程的句柄与实际异常一起传递给订阅者。
/// </summary>
public sealed class CoroutineExceptionEventArgs : EventArgs
{
/// <summary>
/// 初始化 <see cref="CoroutineExceptionEventArgs" /> 的新实例。
/// </summary>
/// <param name="handle">发生异常的协程句柄。</param>
/// <param name="exception">协程执行过程中抛出的异常。</param>
/// <exception cref="ArgumentNullException"><paramref name="exception" /> 为 <see langword="null" />。</exception>
public CoroutineExceptionEventArgs(CoroutineHandle handle, Exception exception)
{
Handle = handle;
Exception = exception ?? throw new ArgumentNullException(nameof(exception));
}
/// <summary>
/// 获取发生异常的协程句柄。
/// </summary>
public CoroutineHandle Handle { get; }
/// <summary>
/// 获取协程执行过程中抛出的异常。
/// </summary>
public Exception Exception { get; }
}

View File

@ -0,0 +1,42 @@
using GFramework.Core.Abstractions.Coroutine;
namespace GFramework.Core.Coroutine;
/// <summary>
/// 表示协程结束事件的数据。
/// 该类型统一描述协程完成、取消或失败后的最终结果。
/// </summary>
public sealed class CoroutineFinishedEventArgs : EventArgs
{
/// <summary>
/// 初始化 <see cref="CoroutineFinishedEventArgs" /> 的新实例。
/// </summary>
/// <param name="handle">已结束的协程句柄。</param>
/// <param name="completionStatus">协程最终结果。</param>
/// <param name="exception">若协程以失败结束,则为对应异常;否则为 <see langword="null" />。</param>
public CoroutineFinishedEventArgs(
CoroutineHandle handle,
CoroutineCompletionStatus completionStatus,
Exception? exception)
{
Handle = handle;
CompletionStatus = completionStatus;
Exception = exception;
}
/// <summary>
/// 获取已结束的协程句柄。
/// </summary>
public CoroutineHandle Handle { get; }
/// <summary>
/// 获取协程最终结果。
/// </summary>
public CoroutineCompletionStatus CompletionStatus { get; }
/// <summary>
/// 获取协程失败时对应的异常对象。
/// 对于完成或取消结果,该值为 <see langword="null" />。
/// </summary>
public Exception? Exception { get; }
}

View File

@ -16,7 +16,7 @@ namespace GFramework.Core.Coroutine;
/// </remarks>
/// <param name="timeSource">缩放时间源,提供调度器默认推进所使用的时间数据。</param>
/// <param name="instanceId">协程实例编号,用于生成带宿主前缀的句柄。</param>
/// <param name="initialCapacity">调度器初始槽位容量。</param>
/// <param name="initialCapacity">调度器初始槽位容量;允许为 0此时首次启动协程会按需自动扩容。</param>
/// <param name="enableStatistics">是否启用协程统计功能。</param>
/// <param name="realtimeTimeSource">
/// 非缩放时间源。
@ -91,7 +91,7 @@ public sealed class CoroutineScheduler(
/// 为了避免阻塞调度器主循环,该事件会被派发到线程池回调中执行。
/// 如果调用方需要与宿主线程保持一致,请同时订阅 <see cref="OnCoroutineFinished" />。
/// </remarks>
public event Action<CoroutineHandle, Exception>? OnCoroutineException;
public event EventHandler<CoroutineExceptionEventArgs>? OnCoroutineException;
/// <summary>
/// 当协程以完成、取消或失败任一结果结束时触发。
@ -99,7 +99,7 @@ public sealed class CoroutineScheduler(
/// <remarks>
/// 该事件在调度器所在的驱动线程中同步触发,适合与宿主生命周期管理逻辑集成。
/// </remarks>
public event Action<CoroutineHandle, CoroutineCompletionStatus, Exception?>? OnCoroutineFinished;
public event EventHandler<CoroutineFinishedEventArgs>? OnCoroutineFinished;
/// <summary>
/// 检查指定协程句柄是否仍然处于活跃状态。
@ -211,58 +211,10 @@ public sealed class CoroutineScheduler(
return default;
}
if (_nextSlot >= _slots.Length)
{
Expand();
}
var handle = new CoroutineHandle(instanceId);
var slotIndex = _nextSlot++;
var slot = new CoroutineSlot
{
CancellationToken = cancellationToken,
Enumerator = coroutine,
State = CoroutineState.Running,
Handle = handle,
Priority = priority
};
if (cancellationToken.CanBeCanceled)
{
// 取消回调可能在任意线程触发,因此这里只做排队,真正清理由 Update 主线程完成。
slot.CancellationRegistration = cancellationToken.Register(() => _pendingKills.Enqueue(handle));
}
_slots[slotIndex] = slot;
_metadata[handle] = new CoroutineMetadata
{
ExecutionStage = executionStage,
Group = group,
Priority = priority,
SlotIndex = slotIndex,
StartTime = _timeSource.CurrentTime * 1000,
State = CoroutineState.Running,
Tag = tag
};
_completionSources[handle] =
new TaskCompletionSource<CoroutineCompletionStatus>(TaskCreationOptions.RunContinuationsAsynchronously);
_completionStatuses.Remove(handle);
if (!string.IsNullOrEmpty(tag))
{
AddTag(tag, handle);
}
if (!string.IsNullOrEmpty(group))
{
AddGroup(group, handle);
}
_statistics?.RecordStart(priority, tag);
ActiveCoroutineCount++;
var slotIndex = AllocateSlotIndex();
var slot = CreateRunningSlot(handle, coroutine, priority, cancellationToken);
RegisterStartedCoroutine(handle, slotIndex, slot, priority, tag, group);
Prewarm(slotIndex);
UpdateStatisticsSnapshot();
@ -662,71 +614,15 @@ public sealed class CoroutineScheduler(
CoroutineCompletionStatus completionStatus,
Exception? exception = null)
{
var slot = _slots[slotIndex];
if (slot == null)
if (!TryGetFinalizableCoroutine(slotIndex, out var slot, out var handle))
{
return;
}
var handle = slot.Handle;
if (!handle.IsValid)
{
return;
}
if (_metadata.TryGetValue(handle, out var meta))
{
if (meta.State == CoroutineState.Paused && _pausedCount > 0)
{
_pausedCount--;
}
var executionTime = _timeSource.CurrentTime * 1000 - meta.StartTime;
switch (completionStatus)
{
case CoroutineCompletionStatus.Completed:
meta.State = CoroutineState.Completed;
_statistics?.RecordComplete(executionTime, meta.Priority, meta.Tag);
break;
case CoroutineCompletionStatus.Faulted:
meta.State = CoroutineState.Completed;
_statistics?.RecordFailure(meta.Priority, meta.Tag);
break;
case CoroutineCompletionStatus.Cancelled:
meta.State = CoroutineState.Cancelled;
break;
default:
throw new ArgumentOutOfRangeException(
nameof(completionStatus),
completionStatus,
"Unsupported coroutine completion status.");
}
}
DisposeSlotResources(slot);
_slots[slotIndex] = null;
if (ActiveCoroutineCount > 0)
{
ActiveCoroutineCount--;
}
RemoveTag(handle);
RemoveGroup(handle);
_metadata.Remove(handle);
WakeWaiters(handle);
if (_completionSources.Remove(handle, out var source))
{
source.TrySetResult(completionStatus);
}
RecordCompletionStatus(handle, completionStatus);
OnCoroutineFinished?.Invoke(handle, completionStatus, exception);
UpdateCompletionMetadata(handle, completionStatus);
ReleaseCompletedCoroutine(slotIndex, slot, handle);
CompleteCoroutineLifecycle(handle, completionStatus);
OnCoroutineFinished?.Invoke(this, new CoroutineFinishedEventArgs(handle, completionStatus, exception));
}
/// <summary>
@ -746,7 +642,7 @@ public sealed class CoroutineScheduler(
{
try
{
handler(handle, ex);
handler(this, new CoroutineExceptionEventArgs(handle, ex));
}
catch (Exception callbackEx)
{
@ -799,6 +695,139 @@ public sealed class CoroutineScheduler(
}
}
/// <summary>
/// 为新协程分配槽位索引,并在需要时扩容槽位数组。
/// </summary>
/// <returns>可写入的新槽位索引。</returns>
private int AllocateSlotIndex()
{
if (_nextSlot >= _slots.Length)
{
Expand();
}
return _nextSlot++;
}
/// <summary>
/// 创建处于运行态的协程槽位,并在需要时挂接跨线程取消回调。
/// </summary>
/// <param name="handle">新协程句柄。</param>
/// <param name="coroutine">协程枚举器。</param>
/// <param name="priority">协程优先级。</param>
/// <param name="cancellationToken">外部取消令牌。</param>
/// <returns>已初始化的协程槽位。</returns>
private CoroutineSlot CreateRunningSlot(
CoroutineHandle handle,
IEnumerator<IYieldInstruction> coroutine,
CoroutinePriority priority,
CancellationToken cancellationToken)
{
var slot = new CoroutineSlot
{
CancellationToken = cancellationToken,
Enumerator = coroutine,
State = CoroutineState.Running,
Handle = handle,
Priority = priority
};
RegisterCancellationCallback(slot, handle, cancellationToken);
return slot;
}
/// <summary>
/// 为支持取消的协程注册待终止排队回调。
/// </summary>
/// <param name="slot">目标协程槽位。</param>
/// <param name="handle">协程句柄。</param>
/// <param name="cancellationToken">外部取消令牌。</param>
private void RegisterCancellationCallback(
CoroutineSlot slot,
CoroutineHandle handle,
CancellationToken cancellationToken)
{
if (!cancellationToken.CanBeCanceled)
{
return;
}
// 取消回调可能在任意线程触发,因此这里只做排队,真正清理由 Update 主线程完成。
slot.CancellationRegistration = cancellationToken.Register(() => _pendingKills.Enqueue(handle));
}
/// <summary>
/// 将新协程写入调度器的槽位、元数据、标签分组和完成状态跟踪结构。
/// </summary>
/// <param name="handle">协程句柄。</param>
/// <param name="slotIndex">槽位索引。</param>
/// <param name="slot">已初始化的协程槽位。</param>
/// <param name="priority">协程优先级。</param>
/// <param name="tag">可选标签。</param>
/// <param name="group">可选分组。</param>
private void RegisterStartedCoroutine(
CoroutineHandle handle,
int slotIndex,
CoroutineSlot slot,
CoroutinePriority priority,
string? tag,
string? group)
{
_slots[slotIndex] = slot;
_metadata[handle] = CreateCoroutineMetadata(slotIndex, priority, tag, group);
ResetCompletionTracking(handle);
if (!string.IsNullOrEmpty(tag))
{
AddTag(tag, handle);
}
if (!string.IsNullOrEmpty(group))
{
AddGroup(group, handle);
}
_statistics?.RecordStart(priority, tag);
ActiveCoroutineCount++;
}
/// <summary>
/// 创建新协程的初始元数据。
/// </summary>
/// <param name="slotIndex">槽位索引。</param>
/// <param name="priority">协程优先级。</param>
/// <param name="tag">可选标签。</param>
/// <param name="group">可选分组。</param>
/// <returns>与新槽位对应的元数据对象。</returns>
private CoroutineMetadata CreateCoroutineMetadata(
int slotIndex,
CoroutinePriority priority,
string? tag,
string? group)
{
return new CoroutineMetadata
{
ExecutionStage = executionStage,
Group = group,
Priority = priority,
SlotIndex = slotIndex,
StartTime = _timeSource.CurrentTime * 1000,
State = CoroutineState.Running,
Tag = tag
};
}
/// <summary>
/// 重置协程完成跟踪,使复用句柄不会携带上一轮完成结果。
/// </summary>
/// <param name="handle">协程句柄。</param>
private void ResetCompletionTracking(CoroutineHandle handle)
{
_completionSources[handle] =
new TaskCompletionSource<CoroutineCompletionStatus>(TaskCreationOptions.RunContinuationsAsynchronously);
_completionStatuses.Remove(handle);
}
/// <summary>
/// 释放单个槽位持有的资源。
/// </summary>
@ -824,6 +853,125 @@ public sealed class CoroutineScheduler(
slot.Waiting = null;
}
/// <summary>
/// 读取可被完成处理的协程槽位与句柄。
/// 当槽位已空或句柄已失效时,说明该协程已经被其他路径清理,无需重复执行结束逻辑。
/// </summary>
/// <param name="slotIndex">槽位索引。</param>
/// <param name="slot">若成功则返回槽位。</param>
/// <param name="handle">若成功则返回句柄。</param>
/// <returns>当存在可完成的协程时返回 <see langword="true" />。</returns>
private bool TryGetFinalizableCoroutine(int slotIndex, out CoroutineSlot slot, out CoroutineHandle handle)
{
var candidate = _slots[slotIndex];
if (candidate == null)
{
slot = null!;
handle = default;
return false;
}
handle = candidate.Handle;
if (!handle.IsValid)
{
slot = null!;
return false;
}
slot = candidate;
return true;
}
/// <summary>
/// 根据最终状态更新协程元数据与统计信息。
/// </summary>
/// <param name="handle">协程句柄。</param>
/// <param name="completionStatus">最终结果。</param>
private void UpdateCompletionMetadata(CoroutineHandle handle, CoroutineCompletionStatus completionStatus)
{
if (!_metadata.TryGetValue(handle, out var meta))
{
return;
}
if (meta.State == CoroutineState.Paused && _pausedCount > 0)
{
_pausedCount--;
}
ApplyCompletionMetadata(meta, completionStatus);
}
/// <summary>
/// 将最终结果映射到元数据状态和统计记录。
/// </summary>
/// <param name="meta">协程元数据。</param>
/// <param name="completionStatus">最终结果。</param>
private void ApplyCompletionMetadata(CoroutineMetadata meta, CoroutineCompletionStatus completionStatus)
{
var executionTime = _timeSource.CurrentTime * 1000 - meta.StartTime;
switch (completionStatus)
{
case CoroutineCompletionStatus.Completed:
meta.State = CoroutineState.Completed;
_statistics?.RecordComplete(executionTime, meta.Priority, meta.Tag);
break;
case CoroutineCompletionStatus.Faulted:
meta.State = CoroutineState.Completed;
_statistics?.RecordFailure(meta.Priority, meta.Tag);
break;
case CoroutineCompletionStatus.Cancelled:
meta.State = CoroutineState.Cancelled;
break;
default:
throw new ArgumentOutOfRangeException(
nameof(completionStatus),
completionStatus,
"Unsupported coroutine completion status.");
}
}
/// <summary>
/// 释放已结束协程占用的槽位和索引结构。
/// </summary>
/// <param name="slotIndex">槽位索引。</param>
/// <param name="slot">已结束的协程槽位。</param>
/// <param name="handle">协程句柄。</param>
private void ReleaseCompletedCoroutine(int slotIndex, CoroutineSlot slot, CoroutineHandle handle)
{
DisposeSlotResources(slot);
_slots[slotIndex] = null;
if (ActiveCoroutineCount > 0)
{
ActiveCoroutineCount--;
}
RemoveTag(handle);
RemoveGroup(handle);
_metadata.Remove(handle);
}
/// <summary>
/// 完成协程的等待者唤醒、任务结果和完成历史记录。
/// </summary>
/// <param name="handle">协程句柄。</param>
/// <param name="completionStatus">最终结果。</param>
private void CompleteCoroutineLifecycle(CoroutineHandle handle, CoroutineCompletionStatus completionStatus)
{
WakeWaiters(handle);
if (_completionSources.Remove(handle, out var source))
{
source.TrySetResult(completionStatus);
}
RecordCompletionStatus(handle, completionStatus);
}
/// <summary>
/// 唤醒所有等待目标协程完成的协程。
/// </summary>
@ -888,7 +1036,9 @@ public sealed class CoroutineScheduler(
/// </summary>
private void Expand()
{
Array.Resize(ref _slots, _slots.Length * 2);
// 允许构造器以 0 容量启动,用于极简场景或测试;首次分配时至少扩到 1避免后续写槽位越界。
var expandedLength = Math.Max(1, _slots.Length * 2);
Array.Resize(ref _slots, expandedLength);
}
/// <summary>

View File

@ -3,24 +3,24 @@
namespace GFramework.Core.Events;
/// <summary>
/// 泛型事件类,支持一个泛型参数 T 的事件注册、注销与触发。
/// 实现了 IEvent 接口以提供统一的事件操作接口。
/// 泛型事件类,支持一个泛型参数 <typeparamref name="T" /> 的事件注册、注销与触发。
/// 实现了 <see cref="IEvent" /> 接口以提供统一的事件操作接口。
/// </summary>
/// <typeparam name="T">事件回调函数的第一个参数类型。</typeparam>
public class Event<T> : IEvent
{
/// <summary>
/// 存储已注册的事件处理委托。
/// 默认为空操作no-op委托避免 null 检查
/// 未注册监听器时保持 <see langword="null" />,从而让监听器计数与真实订阅数量保持一致
/// </summary>
private Action<T>? _mOnEvent = _ => { };
private Action<T>? _mOnEvent;
/// <summary>
/// 显式实现 IEvent 接口中的 Register 方法。
/// 允许使用无参 Action 来订阅当前带参事件。
/// 显式实现 <see cref="IEvent" /> 接口中的 <c>Register</c> 方法。
/// 允许使用无参 <see cref="Action" /> 来订阅当前带参事件。
/// </summary>
/// <param name="onEvent">无参事件处理方法。</param>
/// <returns>IUnRegister 对象,用于稍后注销该事件监听器。</returns>
/// <returns><see cref="IUnRegister" /> 对象,用于稍后注销该事件监听器。</returns>
IUnRegister IEvent.Register(Action onEvent)
{
return Register(Action);
@ -35,7 +35,7 @@ public class Event<T> : IEvent
/// 注册一个事件监听器,并返回可用于取消注册的对象。
/// </summary>
/// <param name="onEvent">要注册的事件处理方法。</param>
/// <returns>IUnRegister 对象,用于稍后注销该事件监听器。</returns>
/// <returns><see cref="IUnRegister" /> 对象,用于稍后注销该事件监听器。</returns>
public IUnRegister Register(Action<T> onEvent)
{
_mOnEvent += onEvent;
@ -52,7 +52,7 @@ public class Event<T> : IEvent
}
/// <summary>
/// 触发所有已注册的事件处理程序,并传递参数 t。
/// 触发所有已注册的事件处理程序,并传递参数 <paramref name="t" />
/// </summary>
/// <param name="t">传递给事件处理程序的参数。</param>
public void Trigger(T t)
@ -61,9 +61,9 @@ public class Event<T> : IEvent
}
/// <summary>
/// 获取当前已注册的监听器数量
/// 获取当前已注册的监听器数量
/// </summary>
/// <returns>监听器数量</returns>
/// <returns>监听器数量</returns>
public int GetListenerCount()
{
return _mOnEvent?.GetInvocationList().Length ?? 0;
@ -71,30 +71,30 @@ public class Event<T> : IEvent
}
/// <summary>
/// 支持两个泛型参数 T 和 TK 的事件类。
/// 支持两个泛型参数 <typeparamref name="T" /><typeparamref name="TK" /> 的事件类。
/// 提供事件注册、注销和触发功能。
/// </summary>
/// <typeparam name="T">第一个参数类型。</typeparam>
/// <typeparam name="Tk">第二个参数类型。</typeparam>
public class Event<T, Tk> : IEvent
/// <typeparam name="TK">第二个参数类型。</typeparam>
public class Event<T, TK> : IEvent
{
/// <summary>
/// 存储已注册的双参数事件处理委托。
/// 默认为空操作no-op委托
/// 未注册监听器时保持 <see langword="null" />,从而让监听器计数与真实订阅数量保持一致
/// </summary>
private Action<T, Tk>? _mOnEvent = (_, _) => { };
private Action<T, TK>? _mOnEvent;
/// <summary>
/// 显式实现 IEvent 接口中的 Register 方法。
/// 允许使用无参 Action 来订阅当前带参事件。
/// 显式实现 <see cref="IEvent" /> 接口中的 <c>Register</c> 方法。
/// 允许使用无参 <see cref="Action" /> 来订阅当前带参事件。
/// </summary>
/// <param name="onEvent">无参事件处理方法。</param>
/// <returns>IUnRegister 对象,用于稍后注销该事件监听器。</returns>
/// <returns><see cref="IUnRegister" /> 对象,用于稍后注销该事件监听器。</returns>
IUnRegister IEvent.Register(Action onEvent)
{
return Register(Action);
void Action(T _, Tk __)
void Action(T _, TK __)
{
onEvent();
}
@ -104,8 +104,8 @@ public class Event<T, Tk> : IEvent
/// 注册一个接受两个参数的事件监听器,并返回可用于取消注册的对象。
/// </summary>
/// <param name="onEvent">要注册的事件处理方法。</param>
/// <returns>IUnRegister 对象,用于稍后注销该事件监听器。</returns>
public IUnRegister Register(Action<T, Tk> onEvent)
/// <returns><see cref="IUnRegister" /> 对象,用于稍后注销该事件监听器。</returns>
public IUnRegister Register(Action<T, TK> onEvent)
{
_mOnEvent += onEvent;
return new DefaultUnRegister(() => UnRegister(onEvent));
@ -115,27 +115,27 @@ public class Event<T, Tk> : IEvent
/// 取消指定的双参数事件监听器。
/// </summary>
/// <param name="onEvent">需要被注销的事件处理方法。</param>
public void UnRegister(Action<T, Tk> onEvent)
public void UnRegister(Action<T, TK> onEvent)
{
_mOnEvent -= onEvent;
}
/// <summary>
/// 触发所有已注册的事件处理程序,并传递参数 t 和 k。
/// 触发所有已注册的事件处理程序,并传递参数 <paramref name="t" /><paramref name="k" />
/// </summary>
/// <param name="t">第一个参数。</param>
/// <param name="k">第二个参数。</param>
public void Trigger(T t, Tk k)
public void Trigger(T t, TK k)
{
_mOnEvent?.Invoke(t, k);
}
/// <summary>
/// 获取当前已注册的监听器数量
/// 获取当前已注册的监听器数量
/// </summary>
/// <returns>监听器数量</returns>
/// <returns>监听器数量</returns>
public int GetListenerCount()
{
return _mOnEvent?.GetInvocationList().Length ?? 0;
}
}
}

View File

@ -23,6 +23,7 @@ public sealed class AsyncLogAppender : ILogAppender
private readonly Action<Exception>? _processingErrorHandler;
private readonly Task _processingTask;
private bool _disposed;
private int _isProcessingEntry;
private volatile bool _flushRequested;
/// <summary>
@ -117,14 +118,14 @@ public sealed class AsyncLogAppender : ILogAppender
/// </summary>
void ILogAppender.Flush()
{
var success = Flush();
OnFlushCompleted?.Invoke(success);
Flush();
}
/// <summary>
/// Flush 操作完成事件参数指示是否成功true或超时false
/// Flush 操作完成事件。
/// 事件数据通过 <see cref="AsyncLogFlushCompletedEventArgs" /> 提供。
/// </summary>
public event Action<bool>? OnFlushCompleted;
public event EventHandler<AsyncLogFlushCompletedEventArgs>? OnFlushCompleted;
/// <summary>
/// 刷新缓冲区,等待所有日志写入完成
@ -140,12 +141,13 @@ public sealed class AsyncLogAppender : ILogAppender
// 请求刷新
_flushRequested = true;
TrySignalFlushCompletion();
try
{
// 等待处理任务发出完成信号
var success = _flushSemaphore.Wait(actualTimeout);
OnFlushCompleted?.Invoke(success);
OnFlushCompleted?.Invoke(this, new AsyncLogFlushCompletedEventArgs(success));
return success;
}
finally
@ -166,6 +168,7 @@ public sealed class AsyncLogAppender : ILogAppender
{
try
{
Volatile.Write(ref _isProcessingEntry, 1);
_innerAppender.Append(entry);
}
catch (Exception ex)
@ -173,18 +176,12 @@ public sealed class AsyncLogAppender : ILogAppender
// 后台消费失败只通过显式回调暴露,避免测试宿主将 stderr 误判为测试告警。
ReportProcessingError(ex);
}
// 检查是否有刷新请求且通道已空
if (_flushRequested && _channel.Reader.Count == 0)
finally
{
_innerAppender.Flush();
// 发出完成信号
if (_flushSemaphore.CurrentCount == 0)
{
_flushSemaphore.Release();
}
Volatile.Write(ref _isProcessingEntry, 0);
}
TrySignalFlushCompletion();
}
}
catch (OperationCanceledException)
@ -209,6 +206,29 @@ public sealed class AsyncLogAppender : ILogAppender
}
}
/// <summary>
/// 在后台消费者已经处理完当前条目且队列为空时完成挂起的 Flush 请求。
/// </summary>
private void TrySignalFlushCompletion()
{
if (!_flushRequested)
{
return;
}
if (Volatile.Read(ref _isProcessingEntry) != 0 || _channel.Reader.Count != 0)
{
return;
}
_innerAppender.Flush();
if (_flushSemaphore.CurrentCount == 0)
{
_flushSemaphore.Release();
}
}
/// <summary>
/// 上报后台处理异常,同时隔离观察者自身抛出的错误,避免终止处理循环。
/// 取消相关异常表示关闭流程中的预期控制流,不应被视为后台处理失败。

View File

@ -1,5 +1,6 @@
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Query;
namespace GFramework.Core.Query;
@ -24,4 +25,31 @@ public abstract class AbstractAsyncQuery<TResult> : ContextAwareBase, IAsyncQuer
/// </summary>
/// <returns>返回查询结果的异步任务</returns>
protected abstract Task<TResult> OnDoAsync();
}
}
/// <summary>
/// 抽象异步查询基类,为需要输入参数的异步查询提供统一执行骨架。
/// </summary>
/// <typeparam name="TInput">查询输入类型,必须实现 <see cref="IQueryInput" /> 接口。</typeparam>
/// <typeparam name="TResult">查询结果类型。</typeparam>
/// <param name="input">查询输入参数。</param>
public abstract class AbstractAsyncQuery<TInput, TResult>(TInput input)
: ContextAwareBase, IAsyncQuery<TResult>
where TInput : IQueryInput
{
/// <summary>
/// 执行异步查询操作。
/// </summary>
/// <returns>返回查询结果的异步任务。</returns>
public Task<TResult> DoAsync()
{
return OnDoAsync(input);
}
/// <summary>
/// 抽象方法,用于实现具体的异步查询逻辑。
/// </summary>
/// <param name="input">查询输入参数。</param>
/// <returns>返回查询结果的异步任务。</returns>
protected abstract Task<TResult> OnDoAsync(TInput input);
}

View File

@ -1,33 +0,0 @@
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Query;
namespace GFramework.Core.Query;
/// <summary>
/// 抽象异步查询基类用于处理输入类型为TInput、结果类型为TResult的异步查询操作
/// </summary>
/// <typeparam name="TInput">查询输入类型必须实现IQueryInput接口</typeparam>
/// <typeparam name="TResult">查询结果类型</typeparam>
/// <param name="input">查询输入参数</param>
public abstract class AbstractAsyncQuery<TInput, TResult>(
TInput input
) : ContextAwareBase, IAsyncQuery<TResult>
where TInput : IQueryInput
{
/// <summary>
/// 执行异步查询操作
/// </summary>
/// <returns>返回查询结果的异步任务</returns>
public Task<TResult> DoAsync()
{
return OnDoAsync(input);
}
/// <summary>
/// 抽象方法,用于实现具体的异步查询逻辑
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>返回查询结果的异步任务</returns>
protected abstract Task<TResult> OnDoAsync(TInput input);
}

View File

@ -1,5 +1,6 @@
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Query;
namespace GFramework.Core.Query;
@ -7,7 +8,7 @@ namespace GFramework.Core.Query;
/// 抽象查询类,提供查询操作的基础实现
/// </summary>
/// <typeparam name="TResult">查询结果的类型</typeparam>
public abstract class AbstractQuery<TResult> : ContextAwareBase, IQuery<TResult>
public abstract class AbstractQuery<TResult> : ContextAwareBase, GFramework.Core.Abstractions.Query.IQuery<TResult>
{
/// <summary>
@ -25,4 +26,31 @@ public abstract class AbstractQuery<TResult> : ContextAwareBase, IQuery<TResult>
/// </summary>
/// <returns>查询结果类型为TResult</returns>
protected abstract TResult OnDo();
}
}
/// <summary>
/// 抽象查询类,为需要输入参数的同步查询提供基础实现。
/// </summary>
/// <typeparam name="TInput">查询输入参数的类型,必须实现 <see cref="IQueryInput" /> 接口。</typeparam>
/// <typeparam name="TResult">查询结果的类型。</typeparam>
/// <param name="input">查询输入参数。</param>
public abstract class AbstractQuery<TInput, TResult>(TInput input)
: ContextAwareBase, GFramework.Core.Abstractions.Query.IQuery<TResult>
where TInput : IQueryInput
{
/// <summary>
/// 执行查询操作。
/// </summary>
/// <returns>查询结果,类型为 <typeparamref name="TResult" />。</returns>
public TResult Do()
{
return OnDo(input);
}
/// <summary>
/// 抽象方法,用于实现具体的查询逻辑。
/// </summary>
/// <param name="input">查询输入参数。</param>
/// <returns>查询结果。</returns>
protected abstract TResult OnDo(TInput input);
}

View File

@ -1,30 +0,0 @@
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Query;
namespace GFramework.Core.Query;
/// <summary>
/// 抽象查询类,提供查询操作的基础实现
/// </summary>
/// <typeparam name="TInput">查询输入参数的类型必须实现IQueryInput接口</typeparam>
/// <typeparam name="TResult">查询结果的类型</typeparam>
public abstract class AbstractQuery<TInput, TResult>(TInput input)
: ContextAwareBase, Abstractions.Query.IQuery<TResult>
where TInput : IQueryInput
{
/// <summary>
/// 执行查询操作
/// </summary>
/// <returns>查询结果类型为TResult</returns>
public TResult Do()
{
return OnDo(input);
}
/// <summary>
/// 抽象方法,用于实现具体的查询逻辑
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>查询结果类型为TResult</returns>
protected abstract TResult OnDo(TInput input);
}

View File

@ -212,10 +212,6 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
ArgumentNullException.ThrowIfNull(action);
Action<TState>[] listenersSnapshot = Array.Empty<Action<TState>>();
IStoreMiddleware<TState>[] middlewaresSnapshot = Array.Empty<IStoreMiddleware<TState>>();
IStoreReducerAdapter[] reducersSnapshot = Array.Empty<IStoreReducerAdapter>();
IEqualityComparer<TState> stateComparerSnapshot = _stateComparer;
StoreDispatchContext<TState>? context = null;
TState notificationState = default!;
var hasNotification = false;
var enteredDispatchScope = false;
@ -224,49 +220,25 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
{
try
{
lock (_lock)
{
EnsureNotDispatching();
_isDispatching = true;
enteredDispatchScope = true;
context = new StoreDispatchContext<TState>(action!, _state);
stateComparerSnapshot = _stateComparer;
middlewaresSnapshot = CreateMiddlewareSnapshotCore();
reducersSnapshot = CreateReducerSnapshotCore(context.ActionType);
}
var context = EnterDispatchScope(
action,
out var middlewaresSnapshot,
out var reducersSnapshot,
out var stateComparerSnapshot);
enteredDispatchScope = true;
// middleware 和 reducer 可能包含较重的同步逻辑,因此仅持有 dispatch 串行门,
// 不占用状态锁,让读取、订阅和注册操作只在需要访问共享状态时短暂阻塞。
ExecuteDispatchPipeline(context, middlewaresSnapshot, reducersSnapshot, stateComparerSnapshot);
lock (_lock)
{
_lastActionType = context.ActionType;
_lastDispatchRecord = new StoreDispatchRecord<TState>(
context.Action,
context.PreviousState,
context.NextState,
context.HasStateChanged,
context.DispatchedAt);
if (!context.HasStateChanged)
{
return;
}
ApplyCommittedStateChange(context.NextState, context.DispatchedAt, context.Action);
listenersSnapshot = CaptureListenersOrDeferNotification(context.NextState, out notificationState);
hasNotification = listenersSnapshot.Length > 0;
}
hasNotification = TryCommitDispatchResult(context, out listenersSnapshot, out notificationState);
}
finally
{
if (enteredDispatchScope)
{
lock (_lock)
{
_isDispatching = false;
}
ExitDispatchScope();
}
}
}
@ -831,6 +803,99 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
context.HasStateChanged = !stateComparer.Equals(context.PreviousState, nextState);
}
/// <summary>
/// 进入一次新的 dispatch 作用域,并在状态锁内抓取本次执行所需的上下文快照。
/// 该方法只做最短路径的共享状态访问,把 middleware/reducer 的实际执行留到锁外完成。
/// </summary>
/// <typeparam name="TAction">action 的具体类型。</typeparam>
/// <param name="action">本次分发的 action。</param>
/// <param name="middlewaresSnapshot">返回本次 dispatch 使用的中间件快照。</param>
/// <param name="reducersSnapshot">返回本次 dispatch 使用的 reducer 快照。</param>
/// <param name="stateComparerSnapshot">返回本次 dispatch 使用的状态比较器快照。</param>
/// <returns>已初始化的 dispatch 上下文。</returns>
private StoreDispatchContext<TState> EnterDispatchScope<TAction>(
TAction action,
out IStoreMiddleware<TState>[] middlewaresSnapshot,
out IStoreReducerAdapter[] reducersSnapshot,
out IEqualityComparer<TState> stateComparerSnapshot)
{
Debug.Assert(
Monitor.IsEntered(_dispatchGate),
"Caller must hold _dispatchGate before entering a dispatch scope.");
lock (_lock)
{
EnsureNotDispatching();
_isDispatching = true;
try
{
var context = new StoreDispatchContext<TState>(action!, _state);
stateComparerSnapshot = _stateComparer;
middlewaresSnapshot = CreateMiddlewareSnapshotCore();
reducersSnapshot = CreateReducerSnapshotCore(context.ActionType);
return context;
}
catch
{
// 进入 dispatch 标记早于快照构建;如果这里抛异常,必须同步回滚标记,避免后续调用被永久判定为嵌套分发。
_isDispatching = false;
throw;
}
}
}
/// <summary>
/// 在 dispatch 管线执行完成后提交诊断信息和状态变更。
/// 状态与订阅集合的更新统一在该阶段完成,从而保证 dispatch 与 time-travel 共享同一提交流程。
/// </summary>
/// <param name="context">刚完成 middleware/reducer 管线的 dispatch 上下文。</param>
/// <param name="listenersSnapshot">若需要立即通知,则返回锁外回放的监听器快照。</param>
/// <param name="notificationState">若需要立即通知,则返回要通知的状态。</param>
/// <returns>本次 dispatch 是否需要在锁外执行监听器通知。</returns>
private bool TryCommitDispatchResult(
StoreDispatchContext<TState> context,
out Action<TState>[] listenersSnapshot,
out TState notificationState)
{
Debug.Assert(
Monitor.IsEntered(_dispatchGate),
"Caller must hold _dispatchGate before committing a dispatch result.");
lock (_lock)
{
_lastActionType = context.ActionType;
_lastDispatchRecord = new StoreDispatchRecord<TState>(
context.Action,
context.PreviousState,
context.NextState,
context.HasStateChanged,
context.DispatchedAt);
if (!context.HasStateChanged)
{
listenersSnapshot = Array.Empty<Action<TState>>();
notificationState = default!;
return false;
}
ApplyCommittedStateChange(context.NextState, context.DispatchedAt, context.Action);
listenersSnapshot = CaptureListenersOrDeferNotification(context.NextState, out notificationState);
return listenersSnapshot.Length > 0;
}
}
/// <summary>
/// 退出当前 dispatch 作用域,允许后续 dispatch 或历史控制继续进入。
/// </summary>
private void ExitDispatchScope()
{
lock (_lock)
{
_isDispatching = false;
}
}
/// <summary>
/// 确保当前 Store 没有发生重入分发或在 dispatch 中执行历史控制。
/// </summary>
@ -949,20 +1014,65 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
if (_actionMatchingMode == StoreActionMatchingMode.ExactTypeOnly)
{
if (!_reducers.TryGetValue(actionType, out var exactReducers) || exactReducers.Count == 0)
{
return Array.Empty<IStoreReducerAdapter>();
}
var exactSnapshot = new IStoreReducerAdapter[exactReducers.Count];
for (var i = 0; i < exactReducers.Count; i++)
{
exactSnapshot[i] = exactReducers[i].Adapter;
}
return exactSnapshot;
return CreateExactReducerSnapshot(actionType);
}
return CreateAssignableReducerSnapshot(actionType);
}
/// <summary>
/// 为精确类型匹配模式创建 reducer 快照。
/// </summary>
/// <param name="actionType">当前 action 的运行时类型。</param>
/// <returns>精确匹配到的 reducer 快照;若未注册则返回空数组。</returns>
private IStoreReducerAdapter[] CreateExactReducerSnapshot(Type actionType)
{
if (!_reducers.TryGetValue(actionType, out var exactReducers) || exactReducers.Count == 0)
{
return Array.Empty<IStoreReducerAdapter>();
}
var exactSnapshot = new IStoreReducerAdapter[exactReducers.Count];
for (var i = 0; i < exactReducers.Count; i++)
{
exactSnapshot[i] = exactReducers[i].Adapter;
}
return exactSnapshot;
}
/// <summary>
/// 为多态匹配模式创建 reducer 快照。
/// 该路径会收集所有可赋值的注册桶,并按“精确类型 -> 基类距离 -> 接口 -> 注册顺序”的稳定规则排序。
/// </summary>
/// <param name="actionType">当前 action 的运行时类型。</param>
/// <returns>多态模式下的 reducer 快照;若未注册则返回空数组。</returns>
private IStoreReducerAdapter[] CreateAssignableReducerSnapshot(Type actionType)
{
var matches = CollectReducerMatches(actionType);
if (matches is null || matches.Count == 0)
{
return Array.Empty<IStoreReducerAdapter>();
}
matches.Sort(CompareReducerMatch);
var snapshot = new IStoreReducerAdapter[matches.Count];
for (var i = 0; i < matches.Count; i++)
{
snapshot[i] = matches[i].Adapter;
}
return snapshot;
}
/// <summary>
/// 收集当前 action 类型可命中的 reducer 注册,并附带稳定排序所需的匹配元数据。
/// </summary>
/// <param name="actionType">当前 action 的运行时类型。</param>
/// <returns>匹配结果列表;若没有任何匹配则返回 <see langword="null"/>。</returns>
private List<ReducerMatch>? CollectReducerMatches(Type actionType)
{
List<ReducerMatch>? matches = null;
foreach (var reducerBucket in _reducers)
@ -984,35 +1094,30 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
}
}
if (matches is null || matches.Count == 0)
return matches;
}
/// <summary>
/// 比较两个 reducer 匹配结果的优先级,保证多态匹配下的执行顺序稳定可预测。
/// </summary>
/// <param name="left">左侧匹配结果。</param>
/// <param name="right">右侧匹配结果。</param>
/// <returns>排序比较结果。</returns>
private static int CompareReducerMatch(ReducerMatch left, ReducerMatch right)
{
var categoryComparison = left.MatchCategory.CompareTo(right.MatchCategory);
if (categoryComparison != 0)
{
return Array.Empty<IStoreReducerAdapter>();
return categoryComparison;
}
matches.Sort(static (left, right) =>
var distanceComparison = left.InheritanceDistance.CompareTo(right.InheritanceDistance);
if (distanceComparison != 0)
{
var categoryComparison = left.MatchCategory.CompareTo(right.MatchCategory);
if (categoryComparison != 0)
{
return categoryComparison;
}
var distanceComparison = left.InheritanceDistance.CompareTo(right.InheritanceDistance);
if (distanceComparison != 0)
{
return distanceComparison;
}
return left.Sequence.CompareTo(right.Sequence);
});
var snapshot = new IStoreReducerAdapter[matches.Count];
for (var i = 0; i < matches.Count; i++)
{
snapshot[i] = matches[i].Adapter;
return distanceComparison;
}
return snapshot;
return left.Sequence.CompareTo(right.Sequence);
}
/// <summary>
@ -1412,4 +1517,4 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
/// </summary>
public TState PendingState { get; set; } = default!;
}
}
}

View File

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

View File

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

View File

@ -781,15 +781,13 @@ public partial class Timing : Node
/// <summary>
/// 在协程结束时解除节点归属回调并清理索引。
/// </summary>
/// <param name="handle">已结束的协程句柄。</param>
/// <param name="status">协程最终状态。</param>
/// <param name="exception">若失败则为异常对象。</param>
/// <param name="sender">触发事件的协程调度器。</param>
/// <param name="eventArgs">协程结束事件数据。</param>
private void HandleCoroutineFinished(
CoroutineHandle handle,
CoroutineCompletionStatus status,
Exception? exception)
object? sender,
CoroutineFinishedEventArgs eventArgs)
{
CleanupOwnedCoroutineRegistration(handle);
CleanupOwnedCoroutineRegistration(eventArgs.Handle);
}
/// <summary>

View File

@ -7,22 +7,35 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-005`
- 当前阶段:`Phase 5`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-015`
- 当前阶段:`Phase 15`
- 当前焦点:
- 已完成 `GFramework.Core/Pause/PauseStackManager.cs``MA0051` 收口:将 `DestroyAsync``Pop` 拆分为锁内状态迁移、
栈调整和锁外通知三个阶段,同时保持日志、事件与销毁补发语义不变
- 已为销毁路径补充 `PauseStackManagerTests.DestroyAsync_Should_NotifyResumedGroups` 回归测试,覆盖“销毁时向所有仍暂停组补发恢复通知”
- 下一轮若继续推进,优先在 `CoroutineScheduler``Store` 的剩余 `MA0051` 中只选一个切入点,不回到已完成的
`PauseStackManager`
- 当前分支 PR #267 的失败测试已通过 `$gframework-pr-review` 与本地整包测试完成复核
- 已确认并修复 `AsyncLogAppender.Flush()` 在“后台线程先清空队列”场景下可能超时返回 `false` 的竞态
- 已补上稳定回归测试,避免只在整包 `GFramework.Core.Tests` 里偶发暴露的刷新完成信号问题再次回归
- 下一轮默认恢复到 `MA0016``MA0002` 低风险批次;`MA0015``MA0077` 继续作为尾项顺手吸收
- `GFramework.Godot``Timing.cs` 已同步适配新事件签名,但当前 worktree 的 Godot restore 资产仍受 Windows fallback package folder 干扰,独立 build 需在修复资产后补跑
- 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进
- 当某一轮主类型数量不足时,允许顺手合并其他低冲突 warning 类型,`MA0015``MA0077`
只是当前最明显的低数量示例,不构成限定
- 单次 `boot` 的工作树改动上限控制在约 `100` 个文件以内,避免 recovery context 与 review 面同时失控
- 若任务边界互不冲突,允许使用不同模型的 subagent 并行处理不同 warning 类型或不同目录,但必须遵守显式 ownership
## 当前状态摘要
- 已完成 `GFramework.Core``GFramework.Cqrs``GFramework.Godot` 与部分 source generator 的低风险 warning 清理
- 已完成多轮 CodeRabbit follow-up 修复,并用定向测试与项目/解决方案构建验证了关键回归风险
- 当前 `PauseStackManager` 的长方法 warning 已从 active 入口移除;主题内剩余 warning 主要集中在
`GFramework.Core/Coroutine/CoroutineScheduler.cs``GFramework.Core/StateManagement/Store.cs`、文件/类型命名冲突、
delegate 形状和少量公共集合抽象接口问题
- 已完成当前 PR #265 review follow-up修复 `CoroutineScheduler` 的零容量扩容边界,并补上 `Store` dispatch 作用域的异常安全回滚
- 已继续完成当前 PR #265 review follow-up修复 `Event<T>``Event<T, TK>` 监听器计数的 off-by-one并补充回归测试
- 已增强 `gframework-pr-review` 脚本与 skill 文档,降低超长 JSON 直出导致的 review 信号漏看风险
- 已完成 `GFramework.Core` 当前 `MA0046` 批次:将阶段、协程与异步日志事件统一迁移到 `EventHandler<TEventArgs>` 形状,
并同步更新 `GFramework.Godot` 订阅点、定向测试与 `docs/zh-CN` 示例
- 已完成当前 PR #267 review follow-up修复 `AsyncLogAppender``ILogAppender.Flush()` 双重完成通知,并补齐
`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` 两个低数量尾项
## 当前活跃事实
@ -32,16 +45,41 @@
- `RP-003` 已在不改生命周期契约的前提下完成 `ArchitectureLifecycle` 初始化主流程拆分,并通过定向 build/test 验证
- `RP-004` 已完成当前 PR review follow-up修复 `TryCreateGeneratedRegistry` 的可空 `out` 契约并清理 trace 文档重复标题
- `RP-005` 已在不改公共 API 的前提下完成 `PauseStackManager` 两个 `MA0051` 的结构拆分,并补充销毁通知回归测试
- `RP-006` 已在不改公共 API 的前提下完成 `Store` 两个 `MA0051` 的结构拆分,并通过定向 build/test 验证 dispatch、
多态 reducer 匹配与历史语义未回归
- `RP-007` 已在不改公共 API 的前提下完成 `CoroutineScheduler` 两个 `MA0051` 的结构拆分,并通过定向 build/test 验证
调度、取消与完成状态语义未回归
- `RP-008` 将后续策略从“单文件 warning 切片”切换为“按类型批处理 + 文件数上限控制”,并允许在非冲突前提下使用
不同模型的 subagent 并行处理
- `RP-009` 在不改公共 API 的前提下,将同名泛型家族收拢到与类型名一致的单文件中,清空当前 `GFramework.Core`
`net8.0` 基线中的 `MA0048`,并通过定向 build/test 验证 `Command``Query``Event` 路径未回归
- `RP-010` 使用 `gframework-pr-review` 复核当前分支 PR #265 后,修复了仍在本地成立的两个 follow-up 风险:
`CoroutineScheduler``initialCapacity: 0` 扩容越界,以及 `Store` 在 dispatch 快照阶段抛异常时可能残留
`_isDispatching = true` 的锁死问题
- `RP-011` 根据补充复核继续收口 PR #265 的 outside-diff comment修复 `Event<T>` / `Event<T, TK>` 默认 no-op
委托导致的 `GetListenerCount()` off-by-one并以定向事件测试验证注册、注销和计数语义
- `RP-012``gframework-pr-review` 增加 `--json-output``--section``--path` 与文本截断能力,并更新 skill 推荐用法,
让“先落盘、再定向抽取”成为默认可操作路径
- `RP-013` 已完成 `GFramework.Core` 当前 `MA0046` 批次,并以新的事件参数类型替换阶段、协程和异步日志事件的
非标准签名;`GFramework.Core` `net8.0` warnings-only 基线由 `15` 降至 `9`
- `RP-014` 使用 `gframework-pr-review` 复核当前分支 PR #267 的 latest head review threads、outside-diff comment 与
nitpick comment 后,确认 8 条高信号项中仍成立的是 1 个行为 bug 与 7 个文档/测试/跟踪缺口,并按最小改动收口
- `RP-015` 使用 `$gframework-pr-review` 复核 PR #267 的 CTRF 失败测试评论后,确认 `AsyncLogAppender` 仍存在
“队列已空但 Flush 仍超时失败”的竞态;该问题在本地整包 `GFramework.Core.Tests` 中可复现,现已修复并补上稳定回归测试
- 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射
## 当前风险
- 结构性重构风险:剩余 `GFramework.Core``MA0051``MA0048` 可能要求较大的文件拆分或类型重命名
- 缓解措施:只在下一轮明确接受结构调整成本时再继续推进,不在恢复点模糊的情况下顺手扩面
- 公共契约兼容风险:剩余 `MA0016` 若直接改公开集合类型,可能波及用户代码
- 缓解措施:优先选择不改公共 API 的低风险切法;若必须触达公共契约,先补齐 XML 契约说明与定向测试
- 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定
- 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证
- 多目标框架 warning 解释风险:同一源位置会在多个 target framework 下重复计数
- 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数
- Godot 资产文件环境风险:当前 worktree 的 `GFramework.Godot` restore/build 仍会命中 Windows fallback package folder
- 缓解措施:后续若继续触达 Godot 模块,先用 Linux 侧 restore 资产或 Windows-hosted 构建链刷新该项目,再补跑定向 build
- 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构
- 缓解措施:只在 warning 类型或目录边界清晰时并行;每个 subagent 必须有独占文件 ownership主代理负责合并验证
## 活跃文档
@ -65,11 +103,67 @@
- 结果:`27 Warning(s)``0 Error(s)``PauseStackManager.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~PauseStackManagerTests -p:RestoreFallbackFolders=`
- 结果:`25 Passed``0 Failed`
- `RP-006` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`25 Warning(s)``0 Error(s)``Store.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~StoreTests -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo`
- 结果:`30 Passed``0 Failed`
- `RP-007` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`23 Warning(s)``0 Error(s)``CoroutineScheduler.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~CoroutineScheduler -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo`
- 结果:`34 Passed``0 Failed`
- `RP-008` 的策略基线:
- 当前 `GFramework.Core` 剩余 warning 分布:`MA0048=8``MA0046=6``MA0016=5``MA0002=2``MA0015=1``MA0077=1`
- 后续批处理规则:优先按类型推进;若当轮主类型数量不足,可顺手吸收其他低冲突类型,不限定于 `MA0015``MA0077`
- `RP-009` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`15 Warning(s)``0 Error(s)`;当前 `GFramework.Core` `net8.0` warnings-only 输出中已不再出现 `MA0048`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~AbstractAsyncCommandTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AbstractAsyncQueryTests|FullyQualifiedName~EventTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`83 Passed``0 Failed`
- `RP-010` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`15 Warning(s)``0 Error(s)`;新增修复未引入新的 `GFramework.Core` `net8.0` 构建错误
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CoroutineSchedulerTests.Run_Should_Grow_From_Zero_Initial_Capacity|FullyQualifiedName~StoreTests.Dispatch_Should_Reset_Dispatching_Flag_When_Snapshot_Creation_Throws" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`2 Passed``0 Failed`
- `RP-011` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`15 Warning(s)``0 Error(s)``Event.cs` 的 listener count 修复未引入新的 `GFramework.Core` `net8.0` 构建错误
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~EventTests.EventT_GetListenerCount_Should_Exclude_Placeholder_Handler|FullyQualifiedName~EventTests.EventTTK_GetListenerCount_Should_Exclude_Placeholder_Handler" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`2 Passed``0 Failed`
- `RP-012` 的定向验证结果:
- `python3 -m py_compile .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py`
- 结果:通过;使用 `PYTHONPYCACHEPREFIX=/tmp/codex-pycache` 规避技能目录只读导致的 `__pycache__` 写入限制
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --help`
- 结果:通过;`--json-output``--section``--path``--max-description-length` 已出现在 CLI 帮助中
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`0 Warning(s)``0 Error(s)`
- `RP-013` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`9 Warning(s)``0 Error(s)`;相对 `RP-009` / `RP-011` 的 warnings-only 基线 `15 Warning(s)` 已降到 `9 Warning(s)`
当前 `GFramework.Core` `net8.0` 输出中已不再出现 `MA0046`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureLifecycleBehaviorTests|FullyQualifiedName~CoroutineSchedulerTests|FullyQualifiedName~AsyncLogAppenderTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`50 Passed``0 Failed`
- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -nologo`
- 结果:失败;当前 worktree 的 Godot restore 资产仍引用 Windows fallback package folder尚未完成独立项目编译验证
- `RP-014` 的定向验证结果:
- `dotnet restore GFramework.Core.Tests/GFramework.Core.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
- 结果通过host Windows `dotnet` 首次验证前补齐了缺失的 `Meziantou.Analyzer 3.0.48`
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`9 Warning(s)``0 Error(s)``AsyncLogAppender` 行为修复与 XML / 文档补充未引入新的 `GFramework.Core` `net8.0` 构建错误
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CoroutineSchedulerTests.Scheduler_Should_Raise_OnCoroutineException_With_EventArgs|FullyQualifiedName~AsyncLogAppenderTests.Flush_Should_Raise_OnFlushCompleted_With_Sender_And_Result|FullyQualifiedName~AsyncLogAppenderTests.ILogAppender_Flush_Should_Raise_OnFlushCompleted_Only_Once|FullyQualifiedName~ArchitectureLifecycleBehaviorTests.InitializeAsync_Should_Raise_PhaseChanged_With_Sender_And_EventArgs" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`4 Passed``0 Failed`
- `RP-015` 的验证结果:
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --disable-build-servers --filter "FullyQualifiedName~AsyncLogAppenderTests"`
- 结果:`15 Passed``0 Failed`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --disable-build-servers`
- 结果:`1607 Passed``0 Failed`
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
## 下一步
1. 若要继续该主题,先读 active tracking再按需展开历史归档中的 warning 热点与验证记录
2. 优先在 `GFramework.Core/Coroutine/CoroutineScheduler.cs``GFramework.Core/StateManagement/Store.cs`
`MA0051` 中只选一个继续,不要在同一轮同时扩多个风险面
3. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`
2. 下一轮优先在 `MA0016``MA0002` 之间选择低风险批次继续推进,默认先看 `LoggingConfiguration` /
`FilterConfiguration``CollectionExtensions`
3. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build
4. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`

View File

@ -1,5 +1,297 @@
# Analyzer Warning Reduction 追踪
## 2026-04-21 — RP-015
### 阶段PR #267 failed-test follow-up 收口RP-015
- 触发背景:
- 用户指出“测试好像挂了”,按 `$gframework-pr-review` 重新抓取当前分支 PR #267 的 review / checks / CTRF 评论
- PR 评论里同时存在一次 `2143 passed / 0 failed` 与一次 `1 failed` 的 CTRF 报告;失败用例为
`AsyncLogAppenderTests.ILogAppender_Flush_Should_Raise_OnFlushCompleted_Only_Once`
- 复核过程:
- 先跑定向单测时该用例可以单独通过,因此继续核对 PR head commit 与本地整包测试,避免把旧评论误判成当前状态
- 在 `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --disable-build-servers`
下成功复现相同失败,确认问题仍存在于当前代码,而不是单纯的 PR 评论残留
- 同时发现当前沙箱内如果用 shell 循环反复启动 `dotnet test`,会触发 `MSBuild` named pipe `Permission denied`
的环境噪音;后续验证改为单次命令并显式加 `--disable-build-servers`
- 根因结论:
- `AsyncLogAppender.Flush()` 只依赖后台消费循环在处理完某个条目后检查 `_flushRequested`
- 当调用方执行 `Flush()` 前,后台线程已经把最后一个条目消费完并离开检查点时,`Flush()` 会一直等到默认超时,
最终通过 `OnFlushCompleted` 发出一次 `Success=false` 的错误完成通知
- 实施修复:
- 为 `AsyncLogAppender` 增加“当前是否仍有条目在途处理”的状态跟踪
- 抽出 `TrySignalFlushCompletion()`,让 `Flush()` 在请求发出后先做一次即时完成判定;后台循环在每次处理结束后也复用
这条判定路径
- 在 `AsyncLogAppenderTests` 中新增 `Flush_WhenEntriesAlreadyProcessed_Should_Still_ReportSuccess`,稳定覆盖
“调用 Flush 前队列已被后台线程清空”的场景
- 验证结果:
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --disable-build-servers --filter "FullyQualifiedName~AsyncLogAppenderTests"`
- 结果:`15 Passed``0 Failed`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --disable-build-servers`
- 结果:`1607 Passed``0 Failed`
- 当前结论:
- PR #267 的 failed-test 信号不是纯粹的历史评论噪音,而是当前实现里仍存在的时序竞态
- 修复后该竞态已被稳定回归测试覆盖,当前 `GFramework.Core.Tests` 整包通过
- 下一步建议:
- 若继续 analyzer warning reduction 主题,恢复到 `MA0016` / `MA0002` 低风险批次
## 2026-04-21 — RP-014
### 阶段PR #267 review follow-up 收口RP-014
- 使用 `gframework-pr-review` 抓取当前分支 PR #267 的 latest head review threads、outside-diff comment、nitpick comment、
MegaLinter 摘要与测试报告,并确认本轮除了 6 条 open thread 之外,还存在 1 条 outside-diff 与 1 条 nitpick 需要一并复核
- 本地复核后确认仍成立的项:
- `AsyncLogAppender` 的显式接口实现 `ILogAppender.Flush()` 会在调用 `Flush()` 后再次手动触发 `OnFlushCompleted`
导致接口路径重复通知
- `Architecture.PhaseChanged``CoroutineExceptionEventArgs``ArchitecturePhaseCoordinator.EnterPhase` 的 XML/注释契约仍未完全同步
- `CoroutineSchedulerTests` 的异常事件测试缺少测试级超时
- `docs/zh-CN/core/architecture.md``docs/zh-CN/core/lifecycle.md` 仍缺少明确的 `PhaseChanged` 迁移说明
- `ai-plan` active tracking 中 `RP-013``9 Warning(s)` 需要明确是相对 `RP-009` / `RP-011` 的 warnings-only 基线收敛
- 实施最小修复:
- 删除 `ILogAppender.Flush()` 中重复的完成事件触发,只保留 `Flush(TimeSpan?)` 内的单一通知源
- 为接口调用路径补充单次完成通知回归测试,并为协程异常事件测试增加 `WaitAsync(TimeSpan.FromSeconds(3))`
- 补齐 `Architecture.PhaseChanged``CoroutineExceptionEventArgs``ArchitecturePhaseCoordinator.EnterPhase` 的契约文档
- 在 `docs/zh-CN/core/architecture.md``docs/zh-CN/core/lifecycle.md` 中加入 `phase => ...` 迁移到 `(_, args) => ...` 的说明
- 更新 `ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md` 的恢复点、基线描述与验证结果
- 验证结果:
- `dotnet restore GFramework.Core.Tests/GFramework.Core.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
- 结果通过host Windows `dotnet` 首次验证前补齐了缺失的 `Meziantou.Analyzer 3.0.48`
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`9 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CoroutineSchedulerTests.Scheduler_Should_Raise_OnCoroutineException_With_EventArgs|FullyQualifiedName~AsyncLogAppenderTests.Flush_Should_Raise_OnFlushCompleted_With_Sender_And_Result|FullyQualifiedName~AsyncLogAppenderTests.ILogAppender_Flush_Should_Raise_OnFlushCompleted_Only_Once|FullyQualifiedName~ArchitectureLifecycleBehaviorTests.InitializeAsync_Should_Raise_PhaseChanged_With_Sender_And_EventArgs" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`4 Passed``0 Failed`
- 当前结论:
- PR #267 里当前仍成立的 CodeRabbit 高信号项已在本地收口
- 修复内容没有改变 `EventHandler<TEventArgs>` 迁移方向,只是补齐行为、文档与恢复信息
- 下一步建议:
- 恢复到 `MA0016` / `MA0002` 主批次,默认先看 `LoggingConfiguration``FilterConfiguration``CollectionExtensions`
## 2026-04-21 — RP-013
### 阶段:`MA0046` 事件签名批次收口RP-013
- 依据 `RP-012` 的下一步建议,本轮恢复到 `GFramework.Core``MA0046` 主批次,而不是继续停留在 PR review workflow 优化
- 本地 warnings-only 基线确认当前 `GFramework.Core` `net8.0` 仍有 `6``MA0046`
- `Architecture.cs`
- `ArchitectureLifecycle.cs`
- `ArchitecturePhaseCoordinator.cs`
- `AsyncLogAppender.cs`
- `CoroutineScheduler.cs` 两处事件
- 方案选择:
- 不再保留 `Action<...>` 事件签名,统一改为标准 `EventHandler<TEventArgs>`
- 为 `Architecture``AsyncLogAppender` 新增放在 `GFramework.Core.Abstractions` 的事件参数类型
- 为 `CoroutineScheduler` 新增放在 `GFramework.Core` 的事件参数类型,因为 `CoroutineHandle` 定义在 runtime 层,不适合反向放入 Abstractions
- `Architecture` 相关事件采用 `Coordinator -> Lifecycle -> Architecture` relay而不是直接透传底层事件确保公开事件的 sender 始终是实际发布者,并避免引入新的 `MA0091`
- 同步适配:
- 更新 `GFramework.Godot/Coroutine/Timing.cs``OnCoroutineFinished` 订阅签名
- 更新 `ArchitectureLifecycleBehaviorTests``CoroutineSchedulerTests``AsyncLogAppenderTests` 以覆盖 sender / event args 契约
- 更新 `docs/zh-CN/core/architecture.md``docs/zh-CN/core/lifecycle.md``PhaseChanged` 示例
- 验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`9 Warning(s)``0 Error(s)`;当前 `GFramework.Core` `net8.0` 输出中已无 `MA0046`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureLifecycleBehaviorTests|FullyQualifiedName~CoroutineSchedulerTests|FullyQualifiedName~AsyncLogAppenderTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`50 Passed``0 Failed`
- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -nologo`
- 结果:失败;当前 worktree 的 `project.assets.json` 仍引用 Windows fallback package folder尚未完成 Godot 独立编译验证
- 当前结论:
- `MA0046` 已从 active 批次中移除
- 剩余 `GFramework.Core` `net8.0` warning 分布更新为:`MA0016=5``MA0002=2``MA0015=1``MA0077=1`
- 若继续本主题,下一步默认转入 `MA0016` 批次;若继续触达 Godot再先修复该项目 restore 资产
## 2026-04-21 — RP-012
### 阶段PR review workflow 输出收窄增强RP-012
- 背景:上一轮虽然脚本已经能解析 `outside_diff_comments`,但直接把超长 JSON 打到终端时仍可能因为输出截断而漏看高价值 review 信号
- 本轮对 `gframework-pr-review` 做了工作流级增强,而不是继续依赖 shell 重定向技巧:
- 为 `fetch_current_pr_review.py` 增加 `--json-output <path>`,允许把完整 JSON 稳定写入文件
- 增加 `--section`,可只输出 `outside-diff``open-threads``megalinter` 等高信号文本摘要
- 增加 `--path`,允许把文本输出收窄到特定文件或路径片段
- 增加 `--max-description-length`,避免超长 comment/body 在 text 模式下刷屏
- 当 text 模式搭配 `--json-output`stdout 保持精简,并显式提示完整 JSON 文件路径
- 同步更新 `SKILL.md`
- 将“先落盘,再用 `jq``--section` / `--path` 缩小范围”写成推荐机器工作流
- 补充按 section 和按路径聚焦的示例命令
- 预期收益:
- 不再要求操作者肉眼阅读整份长 JSON
- outside-diff、nitpick 和 open thread 都能成为一等可过滤输出
- 即使终端输出有 token/长度上限,完整结果仍可通过文件稳定回查
- 定向验证命令:
- `python3 -m py_compile .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py`
- 结果:通过;使用 `PYTHONPYCACHEPREFIX=/tmp/codex-pycache` 规避 `__pycache__` 写入限制
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --help`
- 结果:通过;新增 CLI 选项均已出现在帮助输出中
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`0 Warning(s)``0 Error(s)`
- 下一步建议:
- 之后执行 `$gframework-pr-review` 时,默认优先使用 `--json-output`
- 在 review 跟进阶段,先看 `outside-diff``open-threads``megalinter` 三个 section再决定是否需要打开完整 JSON
## 2026-04-21 — RP-011
### 阶段PR #265 outside-diff follow-up 补收口RP-011
- 用户补充指出 CodeRabbit 在 `Some comments are outside the diff` 中还有 `GFramework.Core/Events/Event.cs` 的 minor finding
默认 no-op 委托会被 `GetInvocationList()` 计入,导致 `GetListenerCount()` 在无监听器和单监听器场景分别返回 `1``2`
- 本地复核确认该问题仍成立:
- `Event<T>` 当前字段初始化为 `_ => { }`
- `Event<T, TK>` 当前字段初始化为 `(_, _) => { }`
- 两个 `Trigger(...)` 实现本身已是 null-safe因此无需依赖占位委托规避空引用
- 实施最小修复:
- 移除两个事件字段的 no-op 初始委托,改为以 `null` 表示“无监听器”
- 保持 `Register` / `UnRegister` / `Trigger` 的公开 API 和调用方式不变
- 在 `EventTests` 中新增单参数与双参数 `GetListenerCount()` 回归测试,覆盖初始值、注册后和注销后的计数语义
- 过程说明:
- 这条不是 skill 设计遗漏;`gframework-pr-review` 的目标本来就包含 latest review body 和 outside-diff 信号
- 上一轮是我在处理时漏看了这条 outside-diff item且终端里展示的超长 JSON 输出被截断,未单独把 `Event.cs` 项再抽出来复核
- 定向验证命令:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`15 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~EventTests.EventT_GetListenerCount_Should_Exclude_Placeholder_Handler|FullyQualifiedName~EventTests.EventTTK_GetListenerCount_Should_Exclude_Placeholder_Handler" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`2 Passed``0 Failed`
- 下一步建议:
- 若继续 PR #265 follow-up只接受当前本地仍成立的剩余 outside-diff 或 unresolved review 项
- 若没有新的有效 review 点,再恢复到 `MA0046` 主批次
## 2026-04-21 — RP-010
### 阶段PR #265 follow-up 收口RP-010
- 使用 `gframework-pr-review` 抓取当前分支 PR #265 的 latest head review threads、CodeRabbit review body、MegaLinter 摘要与 CTRF
测试结果;确认最新 unresolved thread 只剩 `CoroutineScheduler` 零容量扩容边界
- 本地复核后确认两处仍成立的风险:
- `CoroutineScheduler.Expand()``_slots.Length == 0` 时会把容量从 `0` 扩到 `0`,首次 `Run` 写槽位会越界
- `Store.EnterDispatchScope()``_isDispatching = true` 之后、快照构建完成之前若抛异常,会留下永久的嵌套分发误判
- 实施最小修复:
- 将 `Expand()` 调整为 `Math.Max(1, _slots.Length * 2)`,保持已有倍增策略,只补上零容量边界
- 为 `EnterDispatchScope()` 增加快照阶段的异常回滚,确保 `_isDispatching` 与实际 dispatch 生命周期保持一致
- 新增回归测试覆盖零容量启动路径,以及 dispatch 快照阶段抛错后的可恢复性
- 当前 PR 信号复核结论:
- CTRF最新评论显示 `2135 passed / 0 failed`
- MegaLinter唯一告警仍是 CI 中 `dotnet-format` restore 失败,未发现新的本地代码格式问题
- 旧 review body 中提到的 `Store` 异常安全问题虽未表现为最新 open thread但在本地代码中仍可成立因此一并收口
- 定向验证命令:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`15 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CoroutineSchedulerTests.Run_Should_Grow_From_Zero_Initial_Capacity|FullyQualifiedName~StoreTests.Dispatch_Should_Reset_Dispatching_Flag_When_Snapshot_Creation_Throws" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`2 Passed``0 Failed`
- 下一步建议:
- 若继续本主题,恢复到 `MA0046` 主批次,不再停留在当前 PR follow-up
- 若 PR review 还出现新线程,继续遵守“只修复当前本地仍成立的问题”的策略
## 2026-04-21 — RP-009
### 阶段:`MA0048` 批次收口RP-009
- 依据 `RP-008` 的批处理策略,本轮继续从 `GFramework.Core``MA0048` 启动,但不采用重命名公共类型的高风险做法;
改为把同名不同泛型 arity 的家族收拢到与类型名一致的单文件中
- 具体调整:
- 将 `AbstractCommand<TInput>``AbstractCommand<TInput, TResult>` 合并进 `AbstractCommand.cs`
- 将 `AbstractAsyncCommand<TInput>``AbstractAsyncCommand<TInput, TResult>` 合并进 `AbstractAsyncCommand.cs`
- 将 `AbstractQuery<TInput, TResult>` 合并进 `AbstractQuery.cs`
- 将 `AbstractAsyncQuery<TInput, TResult>` 合并进 `AbstractAsyncQuery.cs`
- 将泛型 `Event<T>` / `Event<T, TK>``EasyEventGeneric.cs` 迁移到 `Event.cs`
- 首次构建暴露出合并后的 `ICommand<TResult>` / `IQuery<TResult>` 命名空间歧义;随后改用
`GFramework.Core.Abstractions.*` 的限定名完成最小修正,没有引入行为改动
- 定向验证通过:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`15 Warning(s)``0 Error(s)``MA0048` 已从当前 `GFramework.Core` `net8.0` warnings-only 基线中清空
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~AbstractAsyncCommandTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AbstractAsyncQueryTests|FullyQualifiedName~EventTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`83 Passed``0 Failed`
- 当前建议的下一批次顺序更新为:
- 第一优先级:`MA0046`
- 第二优先级:`MA0016`
- 顺手吸收:`MA0015``MA0077`
- 单独评估:`MA0002`
## 2026-04-21 — RP-008
### 阶段批处理策略切换RP-008
- 根据当前 `GFramework.Core` warnings-only build 的剩余分布,后续不再默认沿用“单文件、单 warning family”的切片节奏
改为按 warning 类型和数量优先级批量推进
- 当前数量基线:
- `MA0048 = 8`
- `MA0046 = 6`
- `MA0016 = 5`
- `MA0002 = 2`
- `MA0015 = 1`
- `MA0077 = 1`
- 新的批处理规则:
- 先按类型选择主批次,而不是按单文件选切入点
- 若主批次数量不够,则允许顺手并入其他低冲突类型;`MA0015``MA0077` 只是当前明显的低数量尾项示例,不是限定范围
- 单次 `boot` 的工作树改动规模控制在约 `100` 个文件以内,避免 recovery context 和 review 面同时膨胀
- 当 warning 类型或目录边界清晰且写集不冲突时,允许使用不同模型的 subagent 并行处理,但必须先定义独占 ownership
- 当前建议的下一批次顺序:
- 第一优先级:`MA0048`
- 第二优先级:`MA0046`
- 顺手吸收:其他低冲突类型,当前可见示例包括 `MA0015``MA0077`
- 单独评估:`MA0016``MA0002`
- 本轮仅更新 recovery strategy不改生产代码验证继续沿用当前基线构建
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`23 Warning(s)``0 Error(s)`
## 2026-04-21 — RP-007
### 阶段CoroutineScheduler `MA0051` 收口RP-007
- 依据 active tracking 中“继续只选一个 `GFramework.Core` 结构性切入点”的约束,本轮选择
`GFramework.Core/Coroutine/CoroutineScheduler.cs`,因为剩余两个 `MA0051` 都集中在协程启动与完成清理路径,且已有
`CoroutineSchedulerTests``CoroutineSchedulerAdvancedTests` 覆盖句柄创建、取消、完成状态、标签分组和等待语义
- 将 `Run` 拆分为:
- `AllocateSlotIndex`
- `CreateRunningSlot`
- `RegisterCancellationCallback`
- `RegisterStartedCoroutine`
- `CreateCoroutineMetadata`
- `ResetCompletionTracking`
- 将 `FinalizeCoroutine` 拆分为:
- `TryGetFinalizableCoroutine`
- `UpdateCompletionMetadata`
- `ApplyCompletionMetadata`
- `ReleaseCompletedCoroutine`
- `CompleteCoroutineLifecycle`
- 保持取消回调只做跨线程入队、`Prewarm` 时机、统计记录文本、`RemoveTag` / `RemoveGroup` / `WakeWaiters` 顺序以及
`OnCoroutineFinished` 的同步触发时机不变,只收缩主方法长度并补齐辅助方法意图注释
- 验证通过:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`23 Warning(s)``0 Error(s)``CoroutineScheduler.cs` 已不再出现在 `MA0051` 列表
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~CoroutineScheduler -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo`
- 结果:`34 Passed``0 Failed`
- 当前 `MA0051` 主线已经在本主题下完成;下一步若继续,应先重新评估剩余 `MA0048``MA0046``MA0002``MA0016`
收敛价值与改动风险,再决定是否开启下一轮 warning family
## 2026-04-21 — RP-006
### 阶段Store `MA0051` 收口RP-006
- 依据 active tracking 中“继续只选一个 `GFramework.Core` 结构性切入点”的约束,本轮选择
`GFramework.Core/StateManagement/Store.cs`,因为该文件的两个 `MA0051` 都集中在 dispatch / reducer snapshot 逻辑,
且已有 `StoreTests` 覆盖 dispatch、batch、history 和多态 reducer 匹配语义
- 在正式验证前先处理 WSL 环境噪音:当前 worktree 的 `GFramework.Core/obj/project.assets.json` 是 Windows 侧 restore
产物,`--no-restore` 构建会继续引用宿主 Windows fallback package folder本轮先执行一次 Linux 侧
`dotnet restore GFramework.Core/GFramework.Core.csproj -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> --ignore-failed-sources -nologo`
刷新资产文件,再继续 warnings-only build
- 将 `Dispatch` 拆分为:
- `EnterDispatchScope`
- `TryCommitDispatchResult`
- `ExitDispatchScope`
- 将 `CreateReducerSnapshotCore` 拆分为:
- `CreateExactReducerSnapshot`
- `CreateAssignableReducerSnapshot`
- `CollectReducerMatches`
- `CompareReducerMatch`
- 保持 `_dispatchGate -> _lock` 的锁顺序、middleware 锁外执行、批处理通知折叠以及“精确类型 -> 基类 -> 接口 ->
注册顺序”的 reducer 稳定排序语义不变,只收缩主方法长度并补齐辅助方法意图注释
- 验证通过:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`25 Warning(s)``0 Error(s)``Store.cs` 已不再出现在 `MA0051` 列表
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~StoreTests -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo`
- 结果:`30 Passed``0 Failed`
- 下一步保持同一节奏:只在 `CoroutineScheduler.cs``Run` / `FinalizeCoroutine` 两个 `MA0051` 中继续,不与其他
warning 家族混做
## 2026-04-21 — RP-005
### 阶段PauseStackManager `MA0051` 收口RP-005

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,602 +1,130 @@
# Events 包使用说明
# Events
## 概述
`GFramework.Core.Events` 是架构内的轻量广播层。它适合表达“某件事已经发生”的运行时信号、模块间松耦合通知,
以及为旧模块保留 `EventBus` 语义;如果你需要请求/响应、pipeline behavior 或 handler registry优先使用
[cqrs](./cqrs.md)。
Events 包提供了一套完整的事件系统实现了观察者模式Observer Pattern。通过事件系统可以实现组件间的松耦合通信支持无参和带参事件、事件注册/注销、以及灵活的事件组合。
## 安装方式
事件系统是 GFramework 架构中组件间通信的核心机制,与命令模式和查询模式共同构成了完整的 CQRS 架构。
## 核心接口
### IEvent
基础事件接口,定义了事件注册的基本功能。
**核心方法:**
```csharp
IUnRegister Register(Action onEvent); // 注册事件处理函数
```bash
dotnet add package GeWuYou.GFramework.Core
dotnet add package GeWuYou.GFramework.Core.Abstractions
```
### IUnRegister
事件实现位于 `GFramework.Core`,抽象接口位于 `GFramework.Core.Abstractions`
注销接口,用于取消事件注册。
## 最常用入口
**核心方法:**
如果你已经在 `ArchitectureContext` 或任何 `IContextAware` 对象里,最常见的入口仍然是:
- `SendEvent<TEvent>()`
- `SendEvent(eventData)`
- `RegisterEvent(Action<TEvent>)`
- `UnRegisterEvent(Action<TEvent>)`
示例:
```csharp
void UnRegister(); // 执行注销操作
```
using GFramework.Core.Extensions;
using GFramework.Core.System;
### IUnRegisterList
public sealed record PlayerDiedEvent(int PlayerId);
注销列表接口,用于批量管理注销对象。
**属性:**
```csharp
IList<IUnRegister> UnregisterList { get; } // 获取注销列表
```
### IEventBus
事件总线接口,提供基于类型的事件发送和注册。
**核心方法:**
```csharp
IUnRegister Register<T>(Action<T> onEvent); // 注册类型化事件
void Send<T>(T e); // 发送事件实例
void Send<T>() where T : new(); // 发送事件(自动创建实例)
void UnRegister<T>(Action<T> onEvent); // 注销事件监听器
```
## 核心类
### EasyEvent
无参事件类,支持注册、注销和触发无参事件。
**核心方法:**
```csharp
IUnRegister Register(Action onEvent); // 注册事件监听器
void Trigger(); // 触发事件
```
**使用示例:**
```csharp
// 创建事件
var onClicked = new EasyEvent();
// 注册监听
var unregister = onClicked.Register(() =>
public sealed class CombatSystem : AbstractSystem
{
Console.WriteLine("Button clicked!");
});
protected override void OnInit()
{
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
}
// 触发事件
onClicked.Trigger();
private void OnPlayerDied(PlayerDiedEvent @event)
{
Logger.Info("Player died: {0}", @event.PlayerId);
}
// 取消注册
unregister.UnRegister();
public void KillPlayer(int playerId)
{
this.SendEvent(new PlayerDiedEvent(playerId));
}
}
```
### Event`<T>`
如果你在架构外单独使用,也可以直接构造 `EventBus`
单参数泛型事件类,支持一个参数的事件。
## EventBus 与 EnhancedEventBus
**核心方法:**
默认实现是 `EventBus`,提供类型化发送与订阅:
```csharp
IUnRegister Register(Action<T> onEvent); // 注册事件监听器
void Trigger(T eventData); // 触发事件并传递参数
```
using GFramework.Core.Events;
**使用示例:**
```csharp
// 创建带参数的事件
var onScoreChanged = new Event<int>();
// 注册监听
onScoreChanged.Register(newScore =>
{
Console.WriteLine($"Score changed to: {newScore}");
});
// 触发事件并传递参数
onScoreChanged.Trigger(100);
```
### Event<T, TK>
双参数泛型事件类。
**核心方法:**
```csharp
IUnRegister Register(Action<T, TK> onEvent); // 注册事件监听器
void Trigger(T param1, TK param2); // 触发事件并传递两个参数
```
**使用示例:**
```csharp
// 伤害事件:攻击者、伤害值
var onDamageDealt = new Event<string, int>();
onDamageDealt.Register((attacker, damage) =>
{
Console.WriteLine($"{attacker} dealt {damage} damage!");
});
onDamageDealt.Trigger("Player", 50);
```
### EasyEvents
全局事件管理器,提供类型安全的事件注册和获取。
**核心方法:**
```csharp
static void Register<T>() where T : IEvent, new(); // 注册事件类型
static T Get<T>() where T : IEvent, new(); // 获取事件实例
static T GetOrAddEvent<T>() where T : IEvent, new(); // 获取或创建事件实例
```
**使用示例:**
```csharp
// 注册全局事件类型
EasyEvents.Register<GameStartEvent>();
// 获取事件实例
var gameStartEvent = EasyEvents.Get<GameStartEvent>();
// 注册监听
gameStartEvent.Register(() =>
{
Console.WriteLine("Game started!");
});
// 触发事件
gameStartEvent.Trigger();
```
### EventBus
类型化事件系统,支持基于类型的事件发送和注册。这是架构中默认的事件总线实现。
**核心方法:**
```csharp
IUnRegister Register<T>(Action<T> onEvent); // 注册类型化事件
void Send<T>(T e); // 发送事件实例
void Send<T>() where T : new(); // 发送事件(自动创建实例)
void UnRegister<T>(Action<T> onEvent); // 注销事件监听器
```
**使用示例:**
```csharp
// 使用全局事件系统
var eventBus = new EventBus();
// 注册类型化事件
eventBus.Register<PlayerDiedEvent>(e =>
eventBus.Register<PlayerJoinedEvent>(e =>
{
Console.WriteLine($"Player died at position: {e.Position}");
Console.WriteLine(e.Name);
});
// 发送事件(传递实例)
eventBus.Send(new PlayerDiedEvent
{
Position = new Vector3(10, 0, 5)
});
// 发送事件(自动创建实例)
eventBus.Send<PlayerDiedEvent>();
// 注销事件监听器
eventBus.UnRegister<PlayerDiedEvent>(OnPlayerDied);
eventBus.Send(new PlayerJoinedEvent("Alice"));
```
### DefaultUnRegister
如果你还需要统计、过滤或弱引用订阅,可以改用 `EnhancedEventBus`。它在 `EventBus` 基础上额外提供:
默认注销器实现,封装注销回调。
- `Statistics`
- `SendFilterable(...)` / `RegisterFilterable(...)`
- `SendWeak(...)` / `RegisterWeak(...)`
**使用示例:**
这类能力更适合工具层、编辑器层或长生命周期对象,不必默认扩散到每个业务事件。
## 优先级、传播与上下文事件
当事件处理顺序或“是否继续传播”本身就是语义的一部分时,使用优先级入口:
```csharp
Action onUnregister = () => Console.WriteLine("Unregistered");
var unregister = new DefaultUnRegister(onUnregister);
using GFramework.Core.Abstractions.Events;
using GFramework.Core.Events;
// 执行注销
unregister.UnRegister();
```
public sealed record InputCommand(string Name);
### OrEvent
var eventBus = new EventBus();
事件或运算组合器,当任意一个事件触发时触发。
**核心方法:**
```csharp
OrEvent Or(IEvent @event); // 添加要组合的事件
```
**使用示例:**
```csharp
var onAnyInput = new OrEvent()
.Or(onKeyPressed)
.Or(onMouseClicked)
.Or(onTouchDetected);
// 当上述任意事件触发时,执行回调
onAnyInput.Register(() =>
eventBus.RegisterWithContext<InputCommand>(ctx =>
{
Console.WriteLine("Input detected!");
});
```
### UnRegisterList
批量管理注销对象的列表。
**核心方法:**
```csharp
void Add(IUnRegister unRegister); // 添加注销器到列表
void UnRegisterAll(); // 批量注销所有事件
```
**使用示例:**
```csharp
var unregisterList = new UnRegisterList();
// 添加到列表
someEvent.Register(OnEvent).AddToUnregisterList(unregisterList);
// 批量注销
unregisterList.UnRegisterAll();
```
### ArchitectureEvents
定义了架构生命周期相关的事件。
**包含事件:**
- `ArchitectureLifecycleReadyEvent` - 架构生命周期准备就绪
- `ArchitectureDestroyingEvent` - 架构销毁中
- `ArchitectureDestroyedEvent` - 架构已销毁
- `ArchitectureFailedInitializationEvent` - 架构初始化失败
## 在架构中使用事件
### 定义事件类
```csharp
// 简单事件
public struct GameStartedEvent { }
// 带数据的事件
public struct PlayerDiedEvent
{
public Vector3 Position;
public string Cause;
}
// 复杂事件
public struct LevelCompletedEvent
{
public int LevelId;
public float CompletionTime;
public int Score;
public List<string> Achievements;
}
```
### Model 中发送事件
```csharp
public class PlayerModel : AbstractModel
{
public BindableProperty<int> Health { get; } = new(100);
protected override void OnInit()
if (ctx.Data.Name == "Pause")
{
// 监听生命值变化
Health.Register(newHealth =>
{
if (newHealth <= 0)
{
// 发送玩家死亡事件
this.SendEvent(new PlayerDiedEvent
{
Position = Position,
Cause = "Health depleted"
});
}
});
Console.WriteLine("Pause handled");
ctx.MarkAsHandled();
}
}
}, priority: 10);
eventBus.Send(new InputCommand("Pause"), EventPropagation.UntilHandled);
```
### System 中发送事件
当前公开语义是:
```csharp
public class CombatSystem : AbstractSystem
{
protected override void OnInit() { }
public void DealDamage(Character attacker, Character target, int damage)
{
target.Health -= damage;
// 发送伤害事件
this.SendEvent(new DamageDealtEvent
{
Attacker = attacker.Name,
Target = target.Name,
Damage = damage
});
}
}
```
- `Register<T>(handler, priority)`:按优先级订阅
- `RegisterWithContext<T>(...)`:拿到 `EventContext<T>`
- `EventPropagation.All`:广播给全部监听器
- `EventPropagation.UntilHandled`:直到上下文事件被标记为 handled
- `EventPropagation.Highest`:只执行最高优先级层
### Controller 中注册事件
## 局部事件对象
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
如果事件只在一个对象或一个小模块内部流动,不必一定挂到 `EventBus`。当前仍可直接使用:
[ContextAware]
public partial class GameController : IController
{
private IUnRegisterList _unregisterList = new UnRegisterList();
- `EasyEvent`
- `Event<T>`
- `Event<T1, T2>`
- `OrEvent`
- `EventListenerScope<TEvent>`
public void Initialize()
{
// 注册多个事件
this.RegisterEvent<GameStartedEvent>(OnGameStarted)
.AddToUnregisterList(_unregisterList);
这类类型更适合局部组合和 UI/工具层内聚逻辑,不适合作为全局消息总线的替代品。
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied)
.AddToUnregisterList(_unregisterList);
## 与 Store / CQRS 的边界
this.RegisterEvent<LevelCompletedEvent>(OnLevelCompleted)
.AddToUnregisterList(_unregisterList);
}
- 轻量运行时广播:`EventBus`
- 聚合状态演进:`Store<TState>`,必要时用 `BridgeToEventBus(...)` 兼容旧事件消费者
- 新业务请求模型:`GFramework.Cqrs`
private void OnGameStarted(GameStartedEvent e)
{
Console.WriteLine("Game started!");
}
private void OnPlayerDied(PlayerDiedEvent e)
{
Console.WriteLine($"Player died at {e.Position}: {e.Cause}");
ShowGameOverScreen();
}
private void OnLevelCompleted(LevelCompletedEvent e)
{
Console.WriteLine($"Level {e.LevelId} completed! Score: {e.Score}");
ShowVictoryScreen(e);
}
public void Cleanup()
{
_unregisterList.UnRegisterAll();
}
}
```
## 高级用法
### 1. 事件链式组合
```csharp
// 使用 Or 组合多个事件
var onAnyDamage = new OrEvent()
.Or(onPhysicalDamage)
.Or(onMagicDamage)
.Or(onPoisonDamage);
onAnyDamage.Register(() =>
{
PlayDamageSound();
});
```
### 2. 事件过滤
```csharp
// 只处理高伤害事件
this.RegisterEvent<DamageDealtEvent>(e =>
{
if (e.Damage >= 50)
{
ShowCriticalHitEffect();
}
});
```
### 3. 事件转发
```csharp
public class EventBridge : AbstractSystem
{
protected override void OnInit()
{
// 将内部事件转发为公共事件
this.RegisterEvent<InternalPlayerDiedEvent>(e =>
{
this.SendEvent(new PublicPlayerDiedEvent
{
PlayerId = e.Id,
Timestamp = DateTime.UtcNow
});
});
}
}
```
### 4. 临时事件监听
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class TutorialController : IController
{
public void Initialize()
{
// 只监听一次
IUnRegister unregister = null;
unregister = this.RegisterEvent<FirstEnemyKilledEvent>(e =>
{
ShowTutorialComplete();
unregister?.UnRegister(); // 立即注销
});
}
}
```
### 5. 条件事件
```csharp
public class AchievementSystem : AbstractSystem
{
private int _killCount = 0;
protected override void OnInit()
{
this.RegisterEvent<EnemyKilledEvent>(e =>
{
_killCount++;
// 条件满足时发送成就事件
if (_killCount >= 100)
{
this.SendEvent(new AchievementUnlockedEvent
{
AchievementId = "kill_100_enemies"
});
}
});
}
}
```
## 生命周期管理
### 使用 UnRegisterList
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class MyController : IController
{
// 统一管理所有注销对象
private IUnRegisterList _unregisterList = new UnRegisterList();
public void Initialize()
{
// 所有注册都添加到列表
this.RegisterEvent<Event1>(OnEvent1)
.AddToUnregisterList(_unregisterList);
this.RegisterEvent<Event2>(OnEvent2)
.AddToUnregisterList(_unregisterList);
}
public void Cleanup()
{
// 一次性注销所有
_unregisterList.UnRegisterAll();
}
}
```
## 最佳实践
1. **事件命名规范**
- 使用过去式:`PlayerDiedEvent``LevelCompletedEvent`
- 使用 `Event` 后缀:便于识别
- 使用结构体:减少内存分配
2. **事件数据设计**
- 只包含必要信息
- 使用值类型struct提高性能
- 避免传递可变引用
3. **避免事件循环**
- 事件处理器中谨慎发送新事件
- 使用命令打破循环依赖
4. **合理使用事件**
- 用于通知状态变化
- 用于跨模块通信
- 不用于返回数据(使用 Query
5. **注销管理**
- 始终注销事件监听
- 使用 `IUnRegisterList` 批量管理
- 在适当的生命周期点调用 `Cleanup()`
6. **性能考虑**
- 避免频繁触发的事件(如每帧)
- 事件处理器保持轻量
- 使用结构体事件减少 GC
7. **事件设计原则**
- 高内聚:事件应该代表一个完整的业务概念
- 低耦合:事件发送者不需要知道接收者
- 可测试:事件应该易于模拟和测试
## 事件 vs 其他通信方式
| 方式 | 适用场景 | 优点 | 缺点 |
|----------------------|--------------|-----------|---------|
| **Event** | 状态变化通知、跨模块通信 | 松耦合、一对多 | 难以追踪调用链 |
| **Command** | 执行操作、修改状态 | 封装逻辑、可撤销 | 单向通信 |
| **Query** | 查询数据 | 职责清晰、有返回值 | 同步调用 |
| **BindableProperty** | UI 数据绑定 | 自动更新、响应式 | 仅限单一属性 |
## 事件系统架构
事件系统在 GFramework 中的架构位置:
```
Architecture (架构核心)
├── EventBus (事件总线)
├── CommandBus (命令总线)
├── QueryBus (查询总线)
└── IocContainer (IoC容器)
Components (组件)
├── Model (发送事件)
├── System (发送/接收事件)
└── Controller (接收事件)
```
## 相关包
- [`architecture`](./architecture.md) - 提供全局事件系统
- [`extensions`](./extensions.md) - 提供事件扩展方法
- [`property`](./property.md) - 可绑定属性基于事件实现
- **Controller** - 控制器监听事件(接口定义在 Core.Abstractions 中)
- [`model`](./model.md) - 模型发送事件
- [`system`](./system.md) - 系统发送和监听事件
- [`command`](./command.md) - 与事件配合实现 CQRS
- [`query`](./query.md) - 与事件配合实现 CQRS
一个简单判断规则是:如果你关心“谁来处理、是否有返回值、是否要挂 pipeline”用 CQRS如果你只是广播
“这件事发生了”,事件系统更直接。

View File

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

View File

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

View File

@ -1,364 +1,86 @@
# Logging 包使用说明
# Logging
## 概述
`GFramework.Core.Logging` 是 Core runtime 的默认日志实现。只加载抽象层时,`LoggerFactoryResolver` 会退回
silent provider加载 `GFramework.Core` 或在 `ArchitectureConfiguration` 里显式提供 provider 后,日志才会
真正输出。
Logging 包提供了灵活的日志系统,支持多级别日志记录。默认日志级别为 `Info`,确保框架的关键操作都能被记录下来。
## 核心接口
### ILogger
日志记录器接口,定义了日志记录的基本功能。
**核心方法:**
## 最小用法
```csharp
// 日志级别检查
bool IsTraceEnabled();
bool IsDebugEnabled();
bool IsInfoEnabled();
bool IsWarnEnabled();
bool IsErrorEnabled();
bool IsFatalEnabled();
using GFramework.Core.Abstractions.Logging;
// 记录日志
void Trace(string msg);
void Trace(string format, object arg);
void Trace(string format, object arg1, object arg2);
void Trace(string format, params object[] arguments);
void Trace(string msg, Exception t);
var logger = LoggerFactoryResolver.Provider.CreateLogger("Bootstrap");
void Debug(string msg);
void Debug(string format, object arg);
void Debug(string format, object arg1, object arg2);
void Debug(string format, params object[] arguments);
void Debug(string msg, Exception t);
void Info(string msg);
void Info(string format, object arg);
void Info(string format, object arg1, object arg2);
void Info(string format, params object[] arguments);
void Info(string msg, Exception t);
void Warn(string msg);
void Warn(string format, object arg);
void Warn(string format, object arg1, object arg2);
void Warn(string format, params object[] arguments);
void Warn(string msg, Exception t);
void Error(string msg);
void Error(string format, object arg);
void Error(string format, object arg1, object arg2);
void Error(string format, params object[] arguments);
void Error(string msg, Exception t);
void Fatal(string msg);
void Fatal(string format, object arg);
void Fatal(string format, object arg1, object arg2);
void Fatal(string format, params object[] arguments);
void Fatal(string msg, Exception t);
// 获取日志器名称
string Name();
logger.Info("Application started");
logger.Warn("Config file missing");
```
### ILoggerFactory
默认 `ArchitectureConfiguration` 会把 provider 配成 `ConsoleLoggerFactoryProvider`,最小级别是 `Info`。如果你
直接走标准 `Architecture` 启动路径,这条配置会自动生效。
日志工厂接口,用于创建日志记录器实例。
**核心方法:**
## 在 Architecture 中调整日志级别
```csharp
ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info);
```
using GFramework.Core.Architectures;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Properties;
using GFramework.Core.Logging;
### ILoggerFactoryProvider
日志工厂提供程序接口,用于获取日志工厂。
**核心方法:**
```csharp
ILoggerFactory GetLoggerFactory();
ILogger CreateLogger(string name);
```
### LogLevel
日志级别枚举。
```csharp
public enum LogLevel
var configuration = new ArchitectureConfiguration
{
Trace = 0, // 最详细的跟踪信息
Debug = 1, // 调试信息
Info = 2, // 一般信息(默认级别)
Warning = 3, // 警告信息
Error = 4, // 错误信息
Fatal = 5 // 致命错误
}
```
## 核心类
### AbstractLogger
抽象日志基类,封装了日志级别判断、格式化与异常处理逻辑。平台日志器只需实现 `Write` 方法即可。
**使用示例:**
```csharp
public class CustomLogger : AbstractLogger
{
public CustomLogger(string? name = null, LogLevel minLevel = LogLevel.Info)
: base(name, minLevel)
LoggerProperties = new LoggerProperties
{
}
protected override void Write(LogLevel level, string message, Exception? exception)
{
// 自定义日志输出逻辑
var logMessage = $"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] [{level}] {message}";
if (exception != null)
logMessage += $"\n{exception}";
Console.WriteLine(logMessage);
}
}
```
### ConsoleLogger
控制台日志记录器实现,支持彩色输出。
**使用示例:**
```csharp
// 创建控制台日志记录器
var logger = new ConsoleLogger("MyLogger", LogLevel.Debug);
// 记录不同级别的日志
logger.Info("应用程序启动");
logger.Debug("调试信息");
logger.Warn("警告信息");
logger.Error("错误信息");
logger.Fatal("致命错误");
```
**输出格式:**
```
[2025-01-09 01:40:00.000] INFO [MyLogger] 应用程序启动
[2025-01-09 01:40:01.000] DEBUG [MyLogger] 调试信息
[2025-01-09 01:40:02.000] WARN [MyLogger] 警告信息
```
**日志级别颜色:**
- **Trace**: 深灰色
- **Debug**: 青色
- **Info**: 白色
- **Warning**: 黄色
- **Error**: 红色
- **Fatal**: 洋红色
**构造函数参数:**
- `name`:日志器名称,默认为 "ROOT"
- `minLevel`:最低日志级别,默认为 LogLevel.Info
- `writer`TextWriter 输出流,默认为 Console.Out
- `useColors`:是否使用颜色,默认为 true仅在输出到控制台时生效
### ConsoleLoggerFactory
控制台日志工厂,用于创建控制台日志记录器实例。
**使用示例:**
```csharp
var factory = new ConsoleLoggerFactory();
var logger = factory.GetLogger("MyModule", LogLevel.Debug);
logger.Info("日志记录器创建成功");
```
### ConsoleLoggerFactoryProvider
控制台日志工厂提供程序实现。
**使用示例:**
```csharp
var provider = new ConsoleLoggerFactoryProvider();
provider.MinLevel = LogLevel.Debug; // 设置最低日志级别
var logger = provider.CreateLogger("MyApp");
logger.Info("应用程序启动");
```
### LoggerFactoryResolver
日志工厂提供程序解析器,用于管理和提供日志工厂提供程序实例。
**使用示例:**
```csharp
// 设置日志工厂提供程序
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
// 设置最小日志级别
LoggerFactoryResolver.MinLevel = LogLevel.Debug;
// 获取日志记录器
var logger = LoggerFactoryResolver.Provider.CreateLogger("MyApp");
logger.Info("应用程序启动");
```
## 在架构中使用日志
### 1. 在 Architecture 中使用
```csharp
public class GameArchitecture : Architecture
{
protected override void Init()
{
var logger = LoggerFactoryResolver.Provider.CreateLogger("GameArchitecture");
logger.Info("游戏架构初始化开始");
RegisterModel(new PlayerModel());
RegisterSystem(new GameSystem());
logger.Info("游戏架构初始化完成");
}
}
```
### 2. 在 System 中使用
```csharp
public class CombatSystem : AbstractSystem
{
protected override void OnInit()
{
var logger = LoggerFactoryResolver.Provider.CreateLogger("CombatSystem");
logger.Info("战斗系统初始化完成");
}
protected override void OnDestroy()
{
var logger = LoggerFactoryResolver.Provider.CreateLogger("CombatSystem");
logger.Info("战斗系统已销毁");
}
}
```
### 3. 在 Model 中使用
```csharp
public class PlayerModel : AbstractModel
{
protected override void OnInit()
{
var logger = LoggerFactoryResolver.Provider.CreateLogger("PlayerModel");
logger.Info("玩家模型初始化完成");
}
}
```
### 4. 自定义日志级别
```
public class DebugLogger : AbstractLogger
{
public DebugLogger() : base("Debug", LogLevel.Debug)
{
}
protected override void Write(LogLevel level, string message, Exception? exception)
{
// 只输出调试及更高级别的日志
if (level >= LogLevel.Debug)
LoggerFactoryProvider = new ConsoleLoggerFactoryProvider
{
Console.WriteLine($"[{level}] {message}");
if (exception != null)
Console.WriteLine(exception);
MinLevel = LogLevel.Debug
}
}
};
```
如果你只是想减少噪音或临时打开 `Debug`,通常只调 `MinLevel` 就够了。
## 结构化日志与上下文
默认 Core logger 实现支持 `IStructuredLogger``LogContext`。当你需要把 `requestId``sceneName` 之类的
上下文随异步流透传时,优先用上下文属性,而不是把所有信息拼进字符串。
```csharp
using GFramework.Core.Abstractions.Logging;
var logger = LoggerFactoryResolver.Provider.CreateLogger("Matchmaking");
using (LogContext.Push("RequestId", requestId))
{
if (logger is IStructuredLogger structured)
{
structured.Log(
LogLevel.Info,
"Player matched",
("PlayerId", playerId),
("RoomId", roomId));
}
}
```
## 日志级别说明
## 当前仓库内置的常用实现
| 级别 | 说明 | 使用场景 |
|-------------|----------|-------------------|
| **Trace** | 最详细的跟踪信息 | 调试复杂的执行流程,记录函数调用等 |
| **Debug** | 调试信息 | 开发阶段,记录变量值、流程分支等 |
| **Info** | 一般信息 | 记录重要的业务流程和系统状态 |
| **Warning** | 警告信息 | 可能的问题但不中断程序执行 |
| **Error** | 错误信息 | 影响功能但不致命的问题 |
| **Fatal** | 致命错误 | 导致程序无法继续运行的严重错误 |
- `ConsoleLoggerFactoryProvider`
- `ConsoleLoggerFactory`
- `CompositeLogger`
- `LoggingConfigurationLoader`
## 最佳实践
如果你需要文件输出、rolling file、async appender 或 JSON formatter可以先用
`LoggingConfigurationLoader` 读取 `LoggingConfiguration`,再把自定义 `ILoggerFactoryProvider` 挂到
`ArchitectureConfiguration.LoggerProperties.LoggerFactoryProvider``LoggerFactoryResolver.Provider`
1. **使用合适的日志级别**
- 使用 `Info` 记录重要业务流程
- 使用 `Debug` 记录调试信息
- 使用 `Warning` 记录异常情况
- 使用 `Error` 记录错误但不影响程序运行
- 使用 `Fatal` 记录严重错误
## 什么时候该换 provider
2. **提供上下文信息**
```csharp
logger.Info($"用户登录成功: UserId={userId}, UserName={userName}");
```
下面这些场景通常不该只靠改 `MinLevel`
3. **异常日志记录**
```csharp
try
{
// 业务逻辑
}
catch (Exception ex)
{
logger.Error("数据库操作失败", ex);
}
```
- 需要文件输出、rolling file 或 async appender
- 需要按 namespace / level 做过滤
- 需要 JSON 格式日志
- 需要组合多个 appender
4. **分类使用日志**
```csharp
var dbLogger = LoggerFactoryResolver.Provider.CreateLogger("Database");
var netLogger = LoggerFactoryResolver.Provider.CreateLogger("Network");
dbLogger.Info("查询用户数据");
netLogger.Debug("发送HTTP请求");
```
5. **在框架组件中合理使用日志**
```csharp
// 在系统初始化时记录
var logger = LoggerFactoryResolver.Provider.CreateLogger("System");
logger.Info("系统初始化完成");
```
## 注意事项
1. **日志级别检查**
- 每个日志方法都会自动检查日志级别
- 如果当前级别低于最小级别,不会输出日志
2. **格式化参数**
- 支持字符串格式化参数
- 支持异常信息传递
3. **ConsoleLogger 的额外参数**
- ConsoleLogger 现在支持自定义TextWriter输出流
- 支持禁用颜色输出的功能useColors参数
## 相关包
- [architecture](./architecture.md) - 架构核心,使用日志系统记录生命周期事件
- [property](./property.md) - 可绑定属性基于事件系统实现
- [extensions](./extensions.md) - 提供便捷的扩展方法
---
**许可证**: Apache 2.0
这时更合理的做法是保留 `ILogger` 调用面不变,只替换 provider / factory / formatter / appender 组合。

View File

@ -1,477 +1,97 @@
# Property 包使用说明
# Property
## 概述
`GFramework.Core.Property` 负责字段级响应式值。它最适合“一个字段变化就足以驱动视图或局部业务逻辑”的场景;
如果你的状态已经是聚合状态树、需要 reducer / middleware / history再切到
[state-management](./state-management.md)。
Property 包提供了可绑定属性BindableProperty的实现支持属性值的监听和响应式编程。这是实现数据绑定和响应式编程的核心组件。
## 安装方式
BindableProperty 是 GFramework 中 Model 层数据管理的基础,通过事件机制实现属性变化的通知。
> 对于简单字段和局部 UI 绑定,`BindableProperty<T>` 仍然是首选方案。
> 如果你需要统一管理复杂状态树、通过 action / reducer 演进状态,或复用局部状态选择器,
> 请同时参考 [`state-management`](./state-management)。
## 核心接口
### IReadonlyBindableProperty`<T>`
只读可绑定属性接口,提供属性值的读取和变更监听功能。
**核心成员:**
```csharp
// 获取属性值
T Value { get; }
// 注册监听(不立即触发回调)
IUnRegister Register(Action<T> onValueChanged);
// 注册监听并立即触发回调传递当前值
IUnRegister RegisterWithInitValue(Action<T> action);
// 取消监听
void UnRegister(Action<T> onValueChanged);
```bash
dotnet add package GeWuYou.GFramework.Core
dotnet add package GeWuYou.GFramework.Core.Abstractions
```
### IBindableProperty`<T>`
## 最常用类型
可绑定属性接口,继承自只读接口,增加了修改能力。
当前最常见的公开类型是:
**核心成员:**
- `IReadonlyBindableProperty<T>`
- `IBindableProperty<T>`
- `BindableProperty<T>`
一般做法是:内部持有 `BindableProperty<T>`,对外只暴露 `IReadonlyBindableProperty<T>`
## 最小示例
```csharp
// 可读写的属性值
new T Value { get; set; }
using GFramework.Core.Property;
using GFramework.Core.Abstractions.Property;
using GFramework.Core.Model;
// 设置值但不触发事件
void SetValueWithoutEvent(T newValue);
```
## 核心类
### BindableProperty`<T>`
可绑定属性的完整实现。
**核心方法:**
```csharp
// 构造函数
BindableProperty(T defaultValue = default!);
// 属性值
T Value { get; set; }
// 注册监听
IUnRegister Register(Action<T> onValueChanged);
IUnRegister RegisterWithInitValue(Action<T> action);
// 取消监听
void UnRegister(Action<T> onValueChanged);
// 设置值但不触发事件
void SetValueWithoutEvent(T newValue);
// 设置自定义比较器
BindableProperty<T> WithComparer(Func<T, T, bool> comparer);
```
**使用示例:**
```csharp
// 创建可绑定属性
var health = new BindableProperty<int>(100);
// 监听值变化(不会立即触发)
var unregister = health.Register(newValue =>
public sealed class PlayerModel : AbstractModel
{
Console.WriteLine($"Health changed to: {newValue}");
});
// 设置值(会触发监听器)
health.Value = 50; // 输出: Health changed to: 50
// 取消监听
unregister.UnRegister();
// 设置值但不触发事件
health.SetValueWithoutEvent(75);
```
**高级功能:**
```csharp
// 1. 注册并立即获得当前值
health.RegisterWithInitValue(value =>
{
Console.WriteLine($"Current health: {value}"); // 立即输出当前值
// 后续值变化时也会调用
});
// 2. 自定义比较器(静态方法)
BindableProperty<int>.Comparer = (a, b) => Math.Abs(a - b) < 1;
// 3. 使用实例方法设置比较器
var position = new BindableProperty<Vector3>(Vector3.Zero)
.WithComparer((a, b) => a.DistanceTo(b) < 0.01f); // 距离小于0.01认为相等
// 4. 字符串比较器示例
var name = new BindableProperty<string>("Player")
.WithComparer((a, b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase));
```
### BindablePropertyUnRegister`<T>`
可绑定属性的注销器,负责清理监听。
**使用示例:**
```csharp
var unregister = health.Register(OnHealthChanged);
// 当需要取消监听时
unregister.UnRegister();
```
## BindableProperty 工作原理
BindableProperty 基于事件系统实现属性变化通知:
1. **值设置**:当设置 `Value` 属性时,首先进行值比较
2. **变化检测**:使用 `EqualityComparer<T>.Default` 或自定义比较器检测值变化
3. **事件触发**:如果值发生变化,调用所有注册的回调函数
4. **内存管理**:通过 `IUnRegister` 机制管理监听器的生命周期
## 在 Model 中使用
### 什么时候继续使用 BindableProperty
以下场景仍然优先推荐 `BindableProperty<T>`
- 单个字段变化就能驱动视图更新
- 状态范围局限在单个 Model 内
- 不需要统一的 action / reducer 写入入口
- 不需要从聚合状态树中复用局部选择逻辑
如果你的状态已经演化为“多个字段必须一起更新”或“多个模块共享同一聚合状态”,
可以在 Model 内部组合 `Store<TState>`,而不是把所有字段都继续拆成独立属性。
### 与 Store / StateMachine 的边界
- `BindableProperty<T>`:字段级响应式值
- `Store<TState>`:聚合状态容器,负责统一归约状态变化
- `StateMachine`:流程状态切换,不负责数据状态归约
一个复杂 Model 可以同时持有 Store 和 BindableProperty
```csharp
public class PlayerStateModel : AbstractModel
{
public Store<PlayerState> Store { get; } = new(new PlayerState(100, "Player"));
public BindableProperty<bool> IsDirty { get; } = new(false);
protected override void OnInit()
{
Store.RegisterReducer<DamageAction>((state, action) =>
state with { Health = Math.Max(0, state.Health - action.Amount) });
}
}
public sealed record PlayerState(int Health, string Name);
public sealed record DamageAction(int Amount);
```
### 定义可绑定属性
```csharp
public class PlayerModel : AbstractModel
{
// 可读写属性
public BindableProperty<string> Name { get; } = new("Player");
public BindableProperty<int> Level { get; } = new(1);
public BindableProperty<int> Health { get; } = new(100);
public BindableProperty<int> MaxHealth { get; } = new(100);
public BindableProperty<Vector3> Position { get; } = new(Vector3.Zero);
// 只读属性(外部只能读取和监听)
public IReadonlyBindableProperty<int> ReadonlyHealth => Health;
protected override void OnInit()
public void Damage(int amount)
{
// 内部监听属性变化
Health.Register(hp =>
{
if (hp <= 0)
{
this.SendEvent(new PlayerDiedEvent());
}
else if (hp < MaxHealth.Value * 0.3f)
{
this.SendEvent(new LowHealthWarningEvent());
}
});
// 监听等级变化
Level.Register(newLevel =>
{
this.SendEvent(new PlayerLevelUpEvent { NewLevel = newLevel });
});
}
// 业务方法
public void TakeDamage(int damage)
{
Health.Value = Math.Max(0, Health.Value - damage);
}
public void Heal(int amount)
{
Health.Value = Math.Min(MaxHealth.Value, Health.Value + amount);
}
public float GetHealthPercentage()
{
return (float)Health.Value / MaxHealth.Value;
Health.Value = Math.Max(0, Health.Value - amount);
}
}
```
## 在 Controller 中监听
### UI 数据绑定
监听方式:
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class PlayerUI : Control, IController
var unRegister = playerModel.ReadonlyHealth.RegisterWithInitValue(health =>
{
[Export] private Label _healthLabel;
[Export] private Label _nameLabel;
[Export] private ProgressBar _healthBar;
private IUnRegisterList _unregisterList = new UnRegisterList();
public override void _Ready()
{
var playerModel = this.GetModel<PlayerModel>();
// 绑定生命值到UI立即显示当前值
playerModel.Health
.RegisterWithInitValue(health =>
{
_healthLabel.Text = $"HP: {health}/{playerModel.MaxHealth.Value}";
_healthBar.Value = (float)health / playerModel.MaxHealth.Value * 100;
})
.AddToUnregisterList(_unregisterList);
// 绑定最大生命值
playerModel.MaxHealth
.RegisterWithInitValue(maxHealth =>
{
_healthBar.MaxValue = maxHealth;
})
.AddToUnregisterList(_unregisterList);
// 绑定名称
playerModel.Name
.RegisterWithInitValue(name =>
{
_nameLabel.Text = name;
})
.AddToUnregisterList(_unregisterList);
// 绑定位置(仅用于调试显示)
playerModel.Position
.RegisterWithInitValue(pos =>
{
// 仅在调试模式下显示
#if DEBUG
Console.WriteLine($"Player position: {pos}");
#endif
})
.AddToUnregisterList(_unregisterList);
}
public override void _ExitTree()
{
_unregisterList.UnRegisterAll();
}
}
Console.WriteLine($"Current HP: {health}");
});
```
## 常见使用模式
## 当前公开语义
### 1. 双向绑定
- `Value`
- 读写当前值;只有值被判定为“真的变化”时才会触发回调
- `Register(...)`
- 订阅后续变化,不会立即回放当前值
- `RegisterWithInitValue(...)`
- 先回放当前值,再继续订阅
- `SetValueWithoutEvent(...)`
- 更新值但不触发通知
- `UnRegister(...)`
- 显式移除某个处理器
- `WithComparer(...)`
- 改写值变化判定逻辑
```c#
// Model
public class SettingsModel : AbstractModel
{
public BindableProperty<float> MasterVolume { get; } = new(1.0f);
protected override void OnInit() { }
}
## 一个需要注意的兼容点
// UI Controller
[ContextAware]
public partial class VolumeSlider : HSlider, IController
{
private BindableProperty<float> _volumeProperty;
`BindableProperty<T>.Comparer` 是按闭合泛型 `T` 共享的静态比较器,`WithComparer(...)` 本质上会改写这一共享
比较器。也就是说,多个 `BindableProperty<int>` 实例会观察到同一比较规则;只有当你确定整个 `T` 族都要共享同一
判等语义时,再去改它。
public override void _Ready()
{
_volumeProperty = this.GetModel<SettingsModel>().MasterVolume;
## 什么时候继续用 Property
// Model -> UI
_volumeProperty.RegisterWithInitValue(vol => Value = vol)
.UnRegisterWhenNodeExitTree(this);
下面这些场景仍然优先使用 `BindableProperty<T>`
// UI -> Model
ValueChanged += newValue => _volumeProperty.Value = (float)newValue;
}
}
```
- 单个字段变化就能驱动 UI
- 状态范围局限在单个 Model 或单个页面
- 不需要统一的 action / reducer 写入口
- 不需要撤销/重做、历史快照或中间件
### 2. 计算属性
## 什么时候该切到 Store
```c#
public class PlayerModel : AbstractModel
{
public BindableProperty<int> Health { get; } = new(100);
public BindableProperty<int> MaxHealth { get; } = new(100);
public BindableProperty<float> HealthPercent { get; } = new(1.0f);
protected override void OnInit()
{
// 自动计算百分比
Action updatePercent = () =>
{
HealthPercent.Value = (float)Health.Value / MaxHealth.Value;
};
Health.Register(_ => updatePercent());
MaxHealth.Register(_ => updatePercent());
updatePercent(); // 初始计算
}
}
```
如果状态已经演化为下面这些形态,更适合用 `Store<TState>`
### 3. 属性验证
- 多个字段必须作为一个原子状态一起演进
- 多个模块共享同一聚合状态
- 需要 reducer / middleware / 历史回放
- 需要从整棵状态树中复用局部选择逻辑
```c#
public class PlayerModel : AbstractModel
{
private BindableProperty<int> _health = new(100);
public BindableProperty<int> Health
{
get => _health;
set
{
// 限制范围
var clampedValue = Math.Clamp(value.Value, 0, MaxHealth.Value);
_health.Value = clampedValue;
}
}
public BindableProperty<int> MaxHealth { get; } = new(100);
protected override void OnInit() { }
}
```
迁移时不必一次性抛弃旧绑定风格。当前已经提供:
### 4. 条件监听
- `store.Select(...)`
- `store.ToBindableProperty(...)`
```c#
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class CombatController : Node, IController
{
public override void _Ready()
{
var playerModel = this.GetModel<PlayerModel>();
// 只在生命值低于30%时显示警告
playerModel.Health.Register(hp =>
{
if (hp < playerModel.MaxHealth.Value * 0.3f)
{
ShowLowHealthWarning();
}
else
{
HideLowHealthWarning();
}
}).UnRegisterWhenNodeExitTree(this);
}
}
```
## 性能优化
### 1. 避免频繁触发
```c#
// 使用 SetValueWithoutEvent 批量修改
public void LoadPlayerData(SaveData data)
{
// 临时关闭事件
Health.SetValueWithoutEvent(data.Health);
Mana.SetValueWithoutEvent(data.Mana);
Gold.SetValueWithoutEvent(data.Gold);
// 最后统一触发一次更新事件
this.SendEvent(new PlayerDataLoadedEvent());
}
```
### 2. 自定义比较器
```c#
// 避免浮点数精度问题导致的频繁触发
var position = new BindableProperty<Vector3>()
.WithComparer((a, b) => a.DistanceTo(b) < 0.001f);
```
## 实现原理
### 值变化检测
```c#
// 使用 EqualityComparer<T>.Default 进行比较
if (!EqualityComparer<T>.Default.Equals(value, MValue))
{
MValue = value;
_mOnValueChanged?.Invoke(value);
}
```
### 事件触发机制
```c#
// 当值变化时触发所有注册的回调
_mOnValueChanged?.Invoke(value);
```
## 最佳实践
1. **在 Model 中定义属性** - BindableProperty 主要用于 Model 层
2. **使用只读接口暴露** - 防止外部随意修改
3. **及时注销监听** - 使用 UnRegisterList 或 UnRegisterWhenNodeExitTree
4. **使用 RegisterWithInitValue** - UI 绑定时立即获取初始值
5. **避免循环依赖** - 属性监听器中修改其他属性要小心
6. **使用自定义比较器** - 对于浮点数等需要精度控制的属性
7. **复杂聚合状态使用 Store** - 当多个字段必须统一演进时,使用 Store 管理聚合状态更清晰
## 相关包
- [`model`](./model.md) - Model 中大量使用 BindableProperty
- [`events`](./events.md) - BindableProperty 基于事件系统实现
- [`state-management`](./state-management) - 复杂状态树的集中式管理方案
- [`extensions`](./extensions.md) - 提供便捷的注销扩展方法
---
**许可证**: Apache 2.0
这意味着你可以先把写路径统一到 `Store<TState>`,再渐进迁移现有 UI 或 Controller 的读取方式。

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff