mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-12 13:14:30 +08:00
Compare commits
21 Commits
b553d7cbc6
...
a980a042ae
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a980a042ae | ||
|
|
a9f86348ff | ||
|
|
685897f2de | ||
|
|
8831cb42a8 | ||
|
|
9ccfed3ad9 | ||
|
|
4d306498b9 | ||
|
|
4a779ac794 | ||
|
|
a5a35ce6ed | ||
|
|
240fc761ed | ||
|
|
aa78dfbf51 | ||
|
|
c61ee140a1 | ||
|
|
2c678cbdda | ||
|
|
233195df91 | ||
|
|
33c435bad5 | ||
|
|
26d5d84d26 | ||
|
|
035c7db18e | ||
|
|
f044aeb770 | ||
|
|
ec0c9a7bc8 | ||
|
|
60faf8eaff | ||
|
|
dfeb40ba15 | ||
|
|
7531762d3e |
@ -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
|
||||
|
||||
|
||||
@ -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__":
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -43,6 +43,6 @@ public abstract class TestArchitectureBase : Architecture
|
||||
_postRegistrationHook?.Invoke(this);
|
||||
|
||||
// 订阅阶段变更事件以记录历史
|
||||
PhaseChanged += phase => PhaseHistory.Add(phase);
|
||||
PhaseChanged += (_, eventArgs) => PhaseHistory.Add(eventArgs.Phase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
30
GFramework.Core/Coroutine/CoroutineExceptionEventArgs.cs
Normal file
30
GFramework.Core/Coroutine/CoroutineExceptionEventArgs.cs
Normal 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; }
|
||||
}
|
||||
42
GFramework.Core/Coroutine/CoroutineFinishedEventArgs.cs
Normal file
42
GFramework.Core/Coroutine/CoroutineFinishedEventArgs.cs
Normal 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; }
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
/// 上报后台处理异常,同时隔离观察者自身抛出的错误,避免终止处理循环。
|
||||
/// 取消相关异常表示关闭流程中的预期控制流,不应被视为后台处理失败。
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
这正是本包的设计目标:让业务层依赖稳定契约,而不是依赖具体运行时细节。
|
||||
|
||||
|
||||
@ -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` 的派生类型
|
||||
|
||||
这说明本包更适合做“游戏基础设施层”,而不是把所有引擎对象耦死在包内部。
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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/`
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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/`,避免默认启动入口再次膨胀
|
||||
|
||||
@ -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/` 粒度归档旧阶段细节
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -1,656 +1,171 @@
|
||||
---
|
||||
title: CQRS
|
||||
description: GFramework 内建 CQRS runtime,用统一请求分发、通知发布和流式处理组织业务逻辑。
|
||||
description: 当前推荐的新请求模型,统一覆盖 command、query、notification、stream request 和 pipeline behaviors。
|
||||
---
|
||||
|
||||
# CQRS
|
||||
|
||||
## 概述
|
||||
`GFramework.Cqrs` 是当前推荐的新请求模型 runtime。
|
||||
|
||||
CQRS(Command 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`
|
||||
|
||||
@ -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;如果你只是广播
|
||||
“这件事发生了”,事件系统更直接。
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 组合。
|
||||
|
||||
@ -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 的读取方式。
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user