mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-10 02:59:02 +08:00
Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
699d0b4896 | ||
|
|
6d5d4be20b | ||
|
|
9ffe3ba237 | ||
|
|
b7fa3eee29 | ||
|
|
228e954d2d | ||
|
|
d4735aec25 | ||
|
|
a07d1c4076 | ||
|
|
9107e23268 | ||
|
|
f9c9561f40 | ||
|
|
f9dd105bcc | ||
|
|
d85828c533 | ||
|
|
17e7f64e71 | ||
|
|
56dc4fd343 | ||
|
|
3fbc563d59 | ||
|
|
4ccc36aac9 | ||
|
|
a36b5978c4 | ||
|
|
000c3e4c45 | ||
|
|
6af600d7b9 | ||
|
|
d389eb36c1 | ||
|
|
59ceb06f2d | ||
|
|
4121e12909 | ||
|
|
59ec255878 | ||
|
|
310791db5a | ||
|
|
b0102b5206 | ||
|
|
7ff4b628a1 | ||
|
|
c7af175f2e | ||
|
|
98c5b14bd5 | ||
|
|
7ca21af92d | ||
|
|
769d036434 | ||
|
|
9bd8c34693 | ||
|
|
39ac61c095 | ||
|
|
24462b0035 | ||
|
|
c82e981b7e | ||
|
|
d9547dae4b | ||
|
|
120a1487f5 | ||
|
|
4d6dbba6a0 | ||
|
|
32eeb41f29 | ||
|
|
5da4a5893b | ||
|
|
18018966f9 | ||
|
|
6a582d0b0b | ||
|
|
5dc2dd25b9 | ||
|
|
e44c56fb46 | ||
|
|
aebf1e974d | ||
|
|
3e1ce089af | ||
|
|
02a60df718 | ||
|
|
77820da820 | ||
|
|
55639c559c | ||
|
|
042b74473f | ||
|
|
55c2a1ae69 | ||
|
|
debc9f27ac | ||
|
|
8f6e6e121e | ||
|
|
d010026448 | ||
|
|
54b79d99d3 | ||
|
|
ffb0a8aff5 | ||
|
|
44d1a89a0b | ||
|
|
cca413042f | ||
|
|
dc3bd3744e | ||
|
|
6056159866 | ||
|
|
d7293aa475 | ||
|
|
017e689abd | ||
|
|
2c58d8b69e | ||
|
|
14cd1fc9a0 | ||
|
|
577c89fdf3 | ||
|
|
a692190a77 | ||
|
|
c3df2b2c96 | ||
|
|
ee8b6a4deb | ||
|
|
ff04a4fbad | ||
|
|
e3fa0db992 | ||
|
|
c2d22285ed | ||
|
|
e3d6aa5111 | ||
|
|
30ddb841a9 | ||
|
|
c65c131d6a | ||
|
|
f0a2978882 | ||
|
|
3233151207 | ||
|
|
0ec8aa076b | ||
|
|
588800bb7b | ||
|
|
ee41206965 | ||
|
|
db89918333 | ||
|
|
f25ccccad2 | ||
|
|
ab9829044f | ||
|
|
109bce6e9e | ||
|
|
6d619b9a1f | ||
|
|
2cb6216d05 | ||
|
|
f71791ae98 | ||
|
|
2ac02c1a6f | ||
|
|
449eeb9606 | ||
|
|
c01abac06e | ||
|
|
6e1eaf8f5c | ||
|
|
e0bbf13d88 | ||
|
|
f776d09f68 | ||
|
|
a8f98e467d | ||
|
|
e6f98cb4af | ||
|
|
96729ddcf1 | ||
|
|
cb6dd8a510 | ||
|
|
a8c6c11e9e | ||
|
|
d9ceb83c2c | ||
|
|
7288114e33 | ||
|
|
c69942d66e | ||
|
|
212d5b1cce | ||
|
|
b1f406ad99 | ||
|
|
61cc1be1e5 | ||
|
|
915d93d06d | ||
|
|
e17fa15a01 | ||
|
|
857ce08edb | ||
|
|
0ac53a4cee | ||
|
|
ac95202f9c | ||
|
|
478072acc3 | ||
|
|
53870c1f92 |
@ -1,6 +1,6 @@
|
||||
# GFramework Skills
|
||||
|
||||
公开入口目前包含 `gframework-doc-refresh` 与 `gframework-batch-boot`。
|
||||
公开入口目前包含 `gframework-doc-refresh`、`gframework-batch-boot` 与 `gframework-multi-agent-batch`。
|
||||
|
||||
## 公开入口
|
||||
|
||||
@ -66,6 +66,30 @@
|
||||
/gframework-batch-boot keep refactoring repetitive source-generator tests in bounded batches
|
||||
```
|
||||
|
||||
### `gframework-multi-agent-batch`
|
||||
|
||||
当用户希望主 Agent 负责拆分任务、派发互不冲突的 subagent 切片、核对进度、维护 `ai-plan`、验收结果并持续推进时,使用该入口。
|
||||
|
||||
适用场景:
|
||||
|
||||
- 复杂任务已经明确可以拆成多个互不冲突的写面
|
||||
- 主 Agent 需要持续 review / integrate,而不是把执行权完全交给单个 worker
|
||||
- 需要把 delegated scope、验证结果与下一恢复点同步写回 `ai-plan`
|
||||
- 任务仍要受 branch diff、context budget 与 reviewability 边界约束
|
||||
|
||||
推荐调用:
|
||||
|
||||
```bash
|
||||
/gframework-multi-agent-batch <task>
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
/gframework-multi-agent-batch continue the current cqrs optimization by delegating non-conflicting benchmark and runtime slices
|
||||
/gframework-multi-agent-batch coordinate parallel subagents, keep ai-plan updated, and stop when reviewability starts to degrade
|
||||
```
|
||||
|
||||
## 共享资源
|
||||
|
||||
- `_shared/DOCUMENTATION_STANDARDS.md`
|
||||
|
||||
@ -11,6 +11,13 @@ Use this skill when `gframework-boot` is necessary but not sufficient because th
|
||||
batches until a clear stop condition is met.
|
||||
|
||||
Treat `AGENTS.md` as the source of truth. This skill extends `gframework-boot`; it does not replace it.
|
||||
If the task's defining requirement is that the main agent must keep acting as dispatcher, reviewer, `ai-plan` owner,
|
||||
and final integrator for multiple parallel workers, prefer `gframework-multi-agent-batch` and use this skill's stop
|
||||
condition guidance as a secondary reference.
|
||||
|
||||
Context budget is a first-class stop signal. Do not keep batching merely because a file-count threshold still has
|
||||
headroom if the active conversation, loaded repo artifacts, validation output, and pending recovery updates suggest the
|
||||
agent is approaching its safe working-context limit.
|
||||
|
||||
## Startup Workflow
|
||||
|
||||
@ -23,11 +30,17 @@ Treat `AGENTS.md` as the source of truth. This skill extends `gframework-boot`;
|
||||
- the work is repetitive, sliceable, or likely to require multiple similar iterations
|
||||
- each batch can be given an explicit ownership boundary
|
||||
- a stop condition can be measured locally
|
||||
- the task does not primarily need the orchestration-heavy main-agent workflow captured by `gframework-multi-agent-batch`
|
||||
3. Before any delegation, define the batch objective in one sentence:
|
||||
- warning family reduction
|
||||
- repeated test refactor pattern
|
||||
- module-by-module documentation refresh
|
||||
- other repetitive multi-file cleanup
|
||||
4. Before the first implementation batch, estimate whether the current task is likely to stay below roughly 80% of the
|
||||
agent's safe working-context budget through one more full batch cycle:
|
||||
- include already loaded `AGENTS.md`, skills, `ai-plan` files, recent command output, active diffs, and expected validation output
|
||||
- if another batch would probably push the conversation near the limit, plan to stop after the current batch even if
|
||||
branch-size thresholds still have room
|
||||
|
||||
## Baseline Selection
|
||||
|
||||
@ -67,8 +80,15 @@ For shorthand numeric thresholds, use a fixed default baseline:
|
||||
|
||||
Choose one primary stop condition before the first batch and restate it to the user.
|
||||
|
||||
When the user does not explicitly override the priority order, use:
|
||||
|
||||
1. context-budget safety
|
||||
2. semantic batch boundary / reviewability
|
||||
3. the user-requested local metric such as files, lines, warnings, or time
|
||||
|
||||
Common stop conditions:
|
||||
|
||||
- the next batch would likely push the agent above roughly 80% of its safe working-context budget
|
||||
- branch diff vs baseline approaches a file-count threshold
|
||||
- warnings-only build reaches a target count
|
||||
- a specific hotspot list is exhausted
|
||||
@ -76,6 +96,9 @@ Common stop conditions:
|
||||
|
||||
If multiple stop conditions exist, rank them and treat one as primary.
|
||||
|
||||
Treat file-count or line-count thresholds as coarse repository-scope signals, not as a proxy for AI context health.
|
||||
When they disagree with context-budget safety, context-budget safety wins.
|
||||
|
||||
## Shorthand Stop-Condition Syntax
|
||||
|
||||
`gframework-batch-boot` may be invoked with shorthand numeric thresholds when the user clearly wants a branch-size stop
|
||||
@ -108,6 +131,7 @@ When shorthand is used:
|
||||
- current branch and active topic
|
||||
- selected baseline
|
||||
- current stop-condition metric
|
||||
- current context-budget posture and whether one more batch is safe
|
||||
- next candidate slices
|
||||
2. Keep the critical path local.
|
||||
3. Delegate only bounded slices with explicit ownership:
|
||||
@ -128,6 +152,7 @@ When shorthand is used:
|
||||
- integrate or verify the result
|
||||
- rerun the required validation
|
||||
- recompute the primary stop-condition metric
|
||||
- reassess whether one more batch would likely push the agent near or beyond roughly 80% context usage
|
||||
- decide immediately whether to continue or stop
|
||||
7. Do not require the user to manually trigger every round unless:
|
||||
- the next slice is ambiguous
|
||||
@ -158,6 +183,7 @@ For multi-batch work, keep recovery artifacts current.
|
||||
|
||||
Stop the loop when any of the following becomes true:
|
||||
|
||||
- the next batch would likely push the agent near or beyond roughly 80% of its safe working-context budget
|
||||
- the primary stop condition has been reached or exceeded
|
||||
- the remaining slices are no longer low-risk
|
||||
- validation failures indicate the task is no longer repetitive
|
||||
@ -165,6 +191,7 @@ Stop the loop when any of the following becomes true:
|
||||
|
||||
When stopping, report:
|
||||
|
||||
- whether context budget was the deciding factor
|
||||
- which baseline was used
|
||||
- the exact metric value at stop time
|
||||
- completed batches
|
||||
|
||||
@ -9,6 +9,8 @@ description: Repository-specific boot workflow for the GFramework repo. Use when
|
||||
|
||||
Use this skill to bootstrap work in the GFramework repository with minimal user prompting.
|
||||
Treat `AGENTS.md` as the source of truth. Use this skill to enforce a startup sequence, not to replace repository rules.
|
||||
If the task clearly requires the main agent to keep coordinating multiple parallel subagents while maintaining
|
||||
`ai-plan` and reviewing each result, switch to `gframework-multi-agent-batch` after the boot context is established.
|
||||
|
||||
## Startup Workflow
|
||||
|
||||
@ -36,14 +38,20 @@ Treat `AGENTS.md` as the source of truth. Use this skill to enforce a startup se
|
||||
- `simple`: one concern, one file or module, no parallel discovery required
|
||||
- `medium`: a small number of modules, some read-only exploration helpful, critical path still easy to keep local
|
||||
- `complex`: cross-module design, migration, large refactor, or work likely to exceed one context window
|
||||
11. Apply the delegation policy from `AGENTS.md`:
|
||||
11. Estimate the current context-budget posture before substantive execution:
|
||||
- account for loaded startup artifacts, active `ai-plan` files, visible diffs, open validation output, and likely next-step output volume
|
||||
- if the task already appears near roughly 80% of a safe working-context budget, prefer closing the current batch,
|
||||
refreshing recovery artifacts, and stopping at the next natural semantic boundary instead of starting a fresh broad slice
|
||||
12. Apply the delegation policy from `AGENTS.md`:
|
||||
- Keep the critical path local
|
||||
- Use `explorer` with `gpt-5.1-codex-mini` for narrow read-only questions, tracing, inventory, and comparisons
|
||||
- Use `worker` with `gpt-5.4` only for bounded implementation tasks with explicit ownership
|
||||
- Do not delegate purely for ceremony; delegate only when it materially shortens the task or controls context growth
|
||||
12. Before editing files, tell the user what you read, how you classified the task, whether subagents will be used,
|
||||
- If the user explicitly wants the main agent to keep orchestrating multiple workers through several review/integration
|
||||
cycles, prefer `gframework-multi-agent-batch` over ad-hoc delegation
|
||||
13. Before editing files, tell the user what you read, how you classified the task, whether subagents will be used,
|
||||
and the first implementation step.
|
||||
13. Proceed with execution, validation, and documentation updates required by `AGENTS.md`.
|
||||
14. Proceed with execution, validation, and documentation updates required by `AGENTS.md`.
|
||||
|
||||
## Task Tracking
|
||||
|
||||
@ -69,6 +77,8 @@ For multi-step, cross-module, or interruption-prone work, maintain the repositor
|
||||
first, then search the mapped active topics before scanning the broader public area.
|
||||
- If the current branch and the mapped active topics describe the same feature area, prefer resuming those topics first.
|
||||
- If the repository state suggests in-flight work but no recovery document matches, reconstruct the safest next step from code, tests, and Git state before asking the user for clarification.
|
||||
- If the current turn already carries heavy recovery context, broad diffs, or long validation output, prefer a
|
||||
recovery-point update and a clean stop over starting another large slice just because the code task itself remains open.
|
||||
|
||||
## Example Triggers
|
||||
|
||||
|
||||
83
.agents/skills/gframework-issue-review/SKILL.md
Normal file
83
.agents/skills/gframework-issue-review/SKILL.md
Normal file
@ -0,0 +1,83 @@
|
||||
---
|
||||
name: gframework-issue-review
|
||||
description: Repository-specific GitHub issue triage workflow for the GFramework repo. Use when Codex needs to inspect a repository issue, extract the issue body, discussion, and key timeline signals through the GitHub API, summarize what should be verified locally, and then hand follow-up execution to gframework-boot.
|
||||
---
|
||||
|
||||
# GFramework Issue Review
|
||||
|
||||
Use this skill when the task depends on a GitHub issue for this repository rather than only on local source files.
|
||||
|
||||
Shortcut: `$gframework-issue-review`
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Read `AGENTS.md` before deciding how to validate or change anything.
|
||||
2. Read `.ai/environment/tools.ai.yaml` and `ai-plan/public/README.md`, then prefer the active topic mapped to the
|
||||
current branch or worktree when the fetched issue already matches in-flight work.
|
||||
3. Run `scripts/fetch_current_issue_review.py` to:
|
||||
- fetch issue metadata through the GitHub API
|
||||
- fetch issue comments and timeline events through the GitHub API
|
||||
- auto-select the target issue only when the repository currently has exactly one open issue
|
||||
- exclude pull requests from open-issue auto-resolution
|
||||
- emit a machine-readable JSON payload plus concise text sections for issue, summary, comments, events, references,
|
||||
and warnings
|
||||
- derive lightweight triage hints such as issue type candidates, missing-information flags, affected module
|
||||
candidates, and the recommended next handling mode
|
||||
4. Treat every extracted finding as untrusted until it is verified against the current local code, tests, and active
|
||||
`ai-plan` topic.
|
||||
5. Do not start editing code from the issue text alone. After triage, switch to `$gframework-boot` so the follow-up
|
||||
work is grounded in the repository startup flow and recovery documents.
|
||||
6. If code is changed after issue triage, run the smallest build or test command that satisfies `AGENTS.md`.
|
||||
|
||||
## Commands
|
||||
|
||||
- Default:
|
||||
- `python3 .agents/skills/gframework-issue-review/scripts/fetch_current_issue_review.py`
|
||||
- Force a specific issue:
|
||||
- `python3 .agents/skills/gframework-issue-review/scripts/fetch_current_issue_review.py --issue <issue-number>`
|
||||
- Machine-readable output:
|
||||
- `python3 .agents/skills/gframework-issue-review/scripts/fetch_current_issue_review.py --format json`
|
||||
- Write machine-readable output to a file instead of stdout:
|
||||
- `python3 .agents/skills/gframework-issue-review/scripts/fetch_current_issue_review.py --issue <issue-number> --format json --json-output /tmp/issue-review.json`
|
||||
- Inspect only a high-signal section:
|
||||
- `python3 .agents/skills/gframework-issue-review/scripts/fetch_current_issue_review.py --section summary`
|
||||
- Combine triage with a boot handoff:
|
||||
- `python3 .agents/skills/gframework-issue-review/scripts/fetch_current_issue_review.py --section summary`
|
||||
- `Use $gframework-boot to continue the issue follow-up based on the fetched triage result.`
|
||||
|
||||
## Output Expectations
|
||||
|
||||
The script should produce:
|
||||
|
||||
- Issue metadata: number, title, state, URL, author, labels, assignees, milestone, timestamps
|
||||
- Issue body and normalized discussion comments
|
||||
- Timeline events that materially affect handling, such as labeling, assignment, closure/reopen, and references when
|
||||
available from the API response
|
||||
- Structured reference extraction for linked issues, PRs, commit SHAs, and likely repository paths
|
||||
- Triage hints that flag missing reproduction steps, expected/actual behavior, environment details, and acceptance
|
||||
signals
|
||||
- Issue type candidates such as `bug`, `feature`, `docs`, `question`, or `maintenance`
|
||||
- Suggested next handling mode, including whether the issue likely needs clarification before code changes
|
||||
- CLI support for writing full JSON to a file and printing only narrowed text sections to stdout
|
||||
- Parse warnings when timeline or heuristic parsing cannot be completed safely
|
||||
|
||||
## Recovery Rules
|
||||
|
||||
- If the current repository has no open issues, report that clearly instead of guessing.
|
||||
- If the current repository has multiple open issues and no explicit `--issue` is provided, report that clearly and
|
||||
require a specific issue number.
|
||||
- If GitHub access fails because of proxy configuration, rerun the fetch with proxy variables removed.
|
||||
- Prefer GitHub API results over HTML scraping.
|
||||
- Do not treat heuristic module guesses or next-step suggestions as repository truth; they are only entry points for
|
||||
subsequent local verification.
|
||||
- If the issue discussion reveals that the problem statement has already shifted, prefer the newest concrete comment or
|
||||
timeline signal over the original title/body wording.
|
||||
- After extracting the issue, continue the actual implementation flow with `$gframework-boot` so the task is grounded
|
||||
in current branch context and `ai-plan` recovery artifacts.
|
||||
|
||||
## Example Triggers
|
||||
|
||||
- `Use $gframework-issue-review on the current repository issue`
|
||||
- `Check the open GitHub issue and summarize what should be verified locally`
|
||||
- `Inspect issue <issue-number> and tell me whether this looks like bug triage or a feature request`
|
||||
- `先用 $gframework-issue-review 看当前 open issue,再用 $gframework-boot 继续`
|
||||
@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "GFramework Issue Review"
|
||||
short_description: "Inspect the current repository issue and triage next steps"
|
||||
default_prompt: "Use $gframework-issue-review to inspect the current repository issue through the GitHub API, summarize the issue body, discussion, and key timeline signals, highlight what must be verified locally, and then hand follow-up execution to $gframework-boot."
|
||||
@ -0,0 +1,858 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2025-2026 GeWuYou
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""
|
||||
Fetch the current GFramework GitHub issue and extract the signals needed for
|
||||
local follow-up work without relying on gh CLI.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Any
|
||||
|
||||
OWNER = "GeWuYou"
|
||||
REPO = "GFramework"
|
||||
WORKTREE_ROOT_DIRECTORY_NAME = "GFramework-WorkTree"
|
||||
GIT_ENVIRONMENT_KEY = "GFRAMEWORK_WINDOWS_GIT"
|
||||
GIT_DIR_ENVIRONMENT_KEY = "GFRAMEWORK_GIT_DIR"
|
||||
WORK_TREE_ENVIRONMENT_KEY = "GFRAMEWORK_WORK_TREE"
|
||||
REQUEST_TIMEOUT_ENVIRONMENT_KEY = "GFRAMEWORK_ISSUE_REVIEW_TIMEOUT_SECONDS"
|
||||
GITHUB_TOKEN_ENVIRONMENT_KEYS = ("GFRAMEWORK_GITHUB_TOKEN", "GITHUB_TOKEN", "GH_TOKEN")
|
||||
PROXY_ENVIRONMENT_KEYS = ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "all_proxy")
|
||||
DEFAULT_REQUEST_TIMEOUT_SECONDS = 60
|
||||
USER_AGENT = "codex-gframework-issue-review"
|
||||
DISPLAY_SECTION_CHOICES = (
|
||||
"issue",
|
||||
"summary",
|
||||
"comments",
|
||||
"events",
|
||||
"references",
|
||||
"warnings",
|
||||
)
|
||||
ISSUE_TYPE_CANDIDATES = ("bug", "feature", "docs", "question", "maintenance")
|
||||
ACTIVE_TOPIC_KEYWORDS: dict[str, tuple[str, ...]] = {
|
||||
"ai-first-config-system": ("config", "configuration", "gameconfig", "settings"),
|
||||
"coroutine-optimization": ("coroutine", "yield", "await", "scheduler"),
|
||||
"cqrs-rewrite": ("cqrs", "command", "query", "eventbus", "event bus"),
|
||||
"data-repository-persistence": ("repository", "serialization", "persistence", "data", "settings"),
|
||||
"runtime-generator-boundary": ("source generator", "generator", "attribute", "packaging"),
|
||||
"semantic-release-versioning": ("release", "version", "semantic-release", "tag", "publish"),
|
||||
"documentation-full-coverage-governance": ("docs", "documentation", "readme", "vitepress", "api reference"),
|
||||
}
|
||||
ACTUAL_BEHAVIOR_PATTERNS = (
|
||||
"actual",
|
||||
"currently",
|
||||
"instead",
|
||||
"but",
|
||||
"error",
|
||||
"exception",
|
||||
"fails",
|
||||
"failed",
|
||||
"wrong",
|
||||
)
|
||||
EXPECTED_BEHAVIOR_PATTERNS = (
|
||||
"expected",
|
||||
"should",
|
||||
"want",
|
||||
"would like",
|
||||
"needs to",
|
||||
)
|
||||
REPRODUCTION_PATTERNS = (
|
||||
"steps to reproduce",
|
||||
"reproduce",
|
||||
"reproduction",
|
||||
"how to reproduce",
|
||||
"minimal example",
|
||||
"sample",
|
||||
"demo",
|
||||
)
|
||||
ENVIRONMENT_PATTERNS = (
|
||||
"windows",
|
||||
"linux",
|
||||
"macos",
|
||||
"wsl",
|
||||
"godot",
|
||||
".net",
|
||||
"sdk",
|
||||
"version",
|
||||
"environment",
|
||||
)
|
||||
ACCEPTANCE_PATTERNS = (
|
||||
"acceptance",
|
||||
"done when",
|
||||
"definition of done",
|
||||
"verified by",
|
||||
"test plan",
|
||||
)
|
||||
FILE_PATH_PATTERN = re.compile(r"\b(?:[A-Za-z0-9_.-]+/)+[A-Za-z0-9_.-]+\b")
|
||||
ISSUE_REFERENCE_PATTERN = re.compile(r"(?:^|\s)#(\d+)\b")
|
||||
COMMIT_REFERENCE_PATTERN = re.compile(r"\b[0-9a-f]{7,40}\b")
|
||||
LINE_BREAK_NORMALIZER = re.compile(r"\n{3,}")
|
||||
|
||||
|
||||
def resolve_git_command() -> str:
|
||||
"""Resolve the git executable to use for this repository."""
|
||||
candidates = [
|
||||
os.environ.get(GIT_ENVIRONMENT_KEY),
|
||||
"git.exe",
|
||||
"git",
|
||||
]
|
||||
|
||||
for candidate in candidates:
|
||||
if not candidate:
|
||||
continue
|
||||
|
||||
if os.path.isabs(candidate):
|
||||
if os.path.exists(candidate):
|
||||
return candidate
|
||||
continue
|
||||
|
||||
resolved_candidate = shutil.which(candidate)
|
||||
if resolved_candidate:
|
||||
return resolved_candidate
|
||||
|
||||
raise RuntimeError(f"No usable git executable found. Set {GIT_ENVIRONMENT_KEY} to override it.")
|
||||
|
||||
|
||||
def find_repository_root(start_path: Path) -> Path | None:
|
||||
"""Locate the repository root by walking parent directories for repo markers."""
|
||||
for candidate in (start_path, *start_path.parents):
|
||||
if (candidate / "AGENTS.md").exists() and (candidate / ".ai/environment/tools.ai.yaml").exists():
|
||||
return candidate
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def resolve_worktree_git_dir(repository_root: Path) -> Path | None:
|
||||
"""Resolve the main-repository worktree gitdir for this WSL worktree layout."""
|
||||
if repository_root.parent.name != WORKTREE_ROOT_DIRECTORY_NAME:
|
||||
return None
|
||||
|
||||
primary_repository_root = repository_root.parent.parent / REPO
|
||||
candidate_git_dir = primary_repository_root / ".git" / "worktrees" / repository_root.name
|
||||
return candidate_git_dir if candidate_git_dir.exists() else None
|
||||
|
||||
|
||||
def resolve_git_invocation() -> list[str]:
|
||||
"""Resolve the git command arguments, preferring explicit WSL worktree binding."""
|
||||
configured_git_dir = os.environ.get(GIT_DIR_ENVIRONMENT_KEY)
|
||||
configured_work_tree = os.environ.get(WORK_TREE_ENVIRONMENT_KEY)
|
||||
linux_git = shutil.which("git")
|
||||
|
||||
if configured_git_dir and configured_work_tree and linux_git:
|
||||
return [linux_git, f"--git-dir={configured_git_dir}", f"--work-tree={configured_work_tree}"]
|
||||
|
||||
repository_root = find_repository_root(Path.cwd())
|
||||
if repository_root is not None and linux_git:
|
||||
worktree_git_dir = resolve_worktree_git_dir(repository_root)
|
||||
if worktree_git_dir is not None:
|
||||
return [linux_git, f"--git-dir={worktree_git_dir}", f"--work-tree={repository_root}"]
|
||||
|
||||
root_git_dir = repository_root / ".git"
|
||||
if root_git_dir.exists():
|
||||
return [linux_git, f"--git-dir={root_git_dir}", f"--work-tree={repository_root}"]
|
||||
|
||||
return [resolve_git_command()]
|
||||
|
||||
|
||||
def resolve_request_timeout_seconds() -> int:
|
||||
"""Return the GitHub request timeout in seconds."""
|
||||
configured_timeout = os.environ.get(REQUEST_TIMEOUT_ENVIRONMENT_KEY)
|
||||
if not configured_timeout:
|
||||
return DEFAULT_REQUEST_TIMEOUT_SECONDS
|
||||
|
||||
try:
|
||||
parsed_timeout = int(configured_timeout)
|
||||
except ValueError as error:
|
||||
raise RuntimeError(
|
||||
f"{REQUEST_TIMEOUT_ENVIRONMENT_KEY} must be an integer number of seconds."
|
||||
) from error
|
||||
|
||||
if parsed_timeout <= 0:
|
||||
raise RuntimeError(f"{REQUEST_TIMEOUT_ENVIRONMENT_KEY} must be greater than zero.")
|
||||
|
||||
return parsed_timeout
|
||||
|
||||
|
||||
def run_command(args: list[str]) -> str:
|
||||
"""Run a command and return stdout, raising on failure."""
|
||||
process = subprocess.run(args, capture_output=True, text=True, check=False)
|
||||
if process.returncode != 0:
|
||||
stderr = process.stderr.strip()
|
||||
raise RuntimeError(f"Command failed: {' '.join(args)}\n{stderr}")
|
||||
return process.stdout.strip()
|
||||
|
||||
|
||||
def get_current_branch() -> str:
|
||||
"""Return the current git branch name."""
|
||||
return run_command([*resolve_git_invocation(), "rev-parse", "--abbrev-ref", "HEAD"])
|
||||
|
||||
|
||||
def resolve_github_token() -> str | None:
|
||||
"""Return the first configured GitHub token for authenticated API requests."""
|
||||
for environment_key in GITHUB_TOKEN_ENVIRONMENT_KEYS:
|
||||
token = os.environ.get(environment_key)
|
||||
if token:
|
||||
return token
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def build_request_headers(accept: str) -> dict[str, str]:
|
||||
"""Build GitHub request headers and include auth when a token is available."""
|
||||
headers = {"Accept": accept, "User-Agent": USER_AGENT}
|
||||
token = resolve_github_token()
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
def has_proxy_environment() -> bool:
|
||||
"""Return whether the current process is configured to use an outbound proxy."""
|
||||
return any(os.environ.get(environment_key) for environment_key in PROXY_ENVIRONMENT_KEYS)
|
||||
|
||||
|
||||
def perform_request(url: str, headers: dict[str, str], *, disable_proxy: bool) -> tuple[str, Any]:
|
||||
"""Execute a single HTTP request and return decoded text plus response headers."""
|
||||
opener = (
|
||||
urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||
if disable_proxy
|
||||
else urllib.request.build_opener()
|
||||
)
|
||||
request = urllib.request.Request(url, headers=headers)
|
||||
with opener.open(request, timeout=resolve_request_timeout_seconds()) as response:
|
||||
return response.read().decode("utf-8", "replace"), response.headers
|
||||
|
||||
|
||||
def open_url(url: str, accept: str) -> tuple[str, Any]:
|
||||
"""Open a URL, retrying without proxies only when the configured proxy path fails."""
|
||||
headers = build_request_headers(accept)
|
||||
|
||||
try:
|
||||
return perform_request(url, headers, disable_proxy=False)
|
||||
except urllib.error.HTTPError:
|
||||
raise
|
||||
except (urllib.error.URLError, TimeoutError, OSError):
|
||||
if not has_proxy_environment():
|
||||
raise
|
||||
|
||||
return perform_request(url, headers, disable_proxy=True)
|
||||
|
||||
|
||||
def fetch_json(url: str, accept: str = "application/vnd.github+json") -> tuple[Any, Any]:
|
||||
"""Fetch a JSON payload and its response headers from GitHub."""
|
||||
text, headers = open_url(url, accept=accept)
|
||||
return json.loads(text), headers
|
||||
|
||||
|
||||
def extract_next_link(headers: Any) -> str | None:
|
||||
"""Extract the next-page link from GitHub pagination headers."""
|
||||
link_header = headers.get("Link")
|
||||
if not link_header:
|
||||
return None
|
||||
|
||||
match = re.search(r'<([^>]+)>;\s*rel="next"', link_header)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def fetch_paged_json(url: str, accept: str = "application/vnd.github+json") -> list[dict[str, Any]]:
|
||||
"""Fetch every page from a paginated GitHub API endpoint."""
|
||||
items: list[dict[str, Any]] = []
|
||||
next_url: str | None = url
|
||||
while next_url:
|
||||
payload, headers = fetch_json(next_url, accept=accept)
|
||||
if not isinstance(payload, list):
|
||||
raise RuntimeError(f"Expected list payload from GitHub API, got {type(payload).__name__}.")
|
||||
|
||||
items.extend(payload)
|
||||
next_url = extract_next_link(headers)
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def collapse_whitespace(text: str) -> str:
|
||||
"""Collapse repeated whitespace into single spaces while preserving paragraph intent."""
|
||||
normalized = text.replace("\r\n", "\n").replace("\r", "\n")
|
||||
normalized = LINE_BREAK_NORMALIZER.sub("\n\n", normalized)
|
||||
normalized = re.sub(r"[ \t]+", " ", normalized)
|
||||
normalized = re.sub(r" *\n *", "\n", normalized)
|
||||
return normalized.strip()
|
||||
|
||||
|
||||
def truncate_text(text: str, max_length: int) -> str:
|
||||
"""Collapse whitespace and truncate long text for CLI display."""
|
||||
collapsed = collapse_whitespace(text)
|
||||
if max_length <= 0 or len(collapsed) <= max_length:
|
||||
return collapsed
|
||||
|
||||
return collapsed[: max_length - 3].rstrip() + "..."
|
||||
|
||||
|
||||
def filter_open_issue_candidates(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Filter GitHub issue list responses down to non-PR issue items."""
|
||||
return [item for item in items if not item.get("pull_request")]
|
||||
|
||||
|
||||
def select_single_open_issue_number(items: list[dict[str, Any]]) -> int:
|
||||
"""Resolve the target issue number when the repository has exactly one open issue."""
|
||||
issues = filter_open_issue_candidates(items)
|
||||
if not issues:
|
||||
raise RuntimeError("No open GitHub issues found for this repository. Pass --issue <number> to inspect one.")
|
||||
|
||||
if len(issues) > 1:
|
||||
numbers = ", ".join(str(item.get("number")) for item in issues[:5])
|
||||
suffix = "" if len(issues) <= 5 else ", ..."
|
||||
raise RuntimeError(
|
||||
"Multiple open GitHub issues found for this repository "
|
||||
f"({len(issues)} total: {numbers}{suffix}). Pass --issue <number> to inspect one."
|
||||
)
|
||||
|
||||
return int(issues[0]["number"])
|
||||
|
||||
|
||||
def resolve_issue_number(issue_number: int | None) -> tuple[int, str]:
|
||||
"""Resolve the issue number, auto-selecting only when exactly one open issue exists."""
|
||||
if issue_number is not None:
|
||||
return issue_number, "explicit"
|
||||
|
||||
open_items = fetch_paged_json(f"https://api.github.com/repos/{OWNER}/{REPO}/issues?state=open&per_page=100")
|
||||
return select_single_open_issue_number(open_items), "auto-single-open-issue"
|
||||
|
||||
|
||||
def fetch_issue_metadata(issue_number: int) -> dict[str, Any]:
|
||||
"""Fetch normalized metadata for a GitHub issue."""
|
||||
payload, _ = fetch_json(f"https://api.github.com/repos/{OWNER}/{REPO}/issues/{issue_number}")
|
||||
if not isinstance(payload, dict):
|
||||
raise RuntimeError("Failed to fetch GitHub issue metadata.")
|
||||
|
||||
if payload.get("pull_request"):
|
||||
raise RuntimeError(f"Item #{issue_number} is a pull request, not a plain issue.")
|
||||
|
||||
labels = []
|
||||
for label in payload.get("labels", []):
|
||||
if isinstance(label, dict) and label.get("name"):
|
||||
labels.append(str(label["name"]))
|
||||
|
||||
assignees = []
|
||||
for assignee in payload.get("assignees", []):
|
||||
login = assignee.get("login")
|
||||
if login:
|
||||
assignees.append(str(login))
|
||||
|
||||
milestone_title = None
|
||||
milestone = payload.get("milestone")
|
||||
if isinstance(milestone, dict) and milestone.get("title"):
|
||||
milestone_title = str(milestone["title"])
|
||||
|
||||
return {
|
||||
"number": int(payload["number"]),
|
||||
"title": str(payload["title"]),
|
||||
"state": str(payload["state"]).upper(),
|
||||
"url": str(payload["html_url"]),
|
||||
"author": str(payload.get("user", {}).get("login") or ""),
|
||||
"created_at": str(payload.get("created_at") or ""),
|
||||
"updated_at": str(payload.get("updated_at") or ""),
|
||||
"labels": labels,
|
||||
"assignees": assignees,
|
||||
"milestone": milestone_title,
|
||||
"body": str(payload.get("body") or ""),
|
||||
}
|
||||
|
||||
|
||||
def fetch_issue_comments(issue_number: int) -> list[dict[str, Any]]:
|
||||
"""Fetch issue comments for the selected issue."""
|
||||
return fetch_paged_json(f"https://api.github.com/repos/{OWNER}/{REPO}/issues/{issue_number}/comments?per_page=100")
|
||||
|
||||
|
||||
def fetch_issue_timeline(issue_number: int) -> list[dict[str, Any]]:
|
||||
"""Fetch issue timeline events when GitHub exposes them to the current client."""
|
||||
return fetch_paged_json(
|
||||
f"https://api.github.com/repos/{OWNER}/{REPO}/issues/{issue_number}/timeline?per_page=100",
|
||||
accept="application/vnd.github+json",
|
||||
)
|
||||
|
||||
|
||||
def normalize_comment(comment: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Normalize an issue comment for structured output."""
|
||||
return {
|
||||
"id": int(comment.get("id") or 0),
|
||||
"author": str(comment.get("user", {}).get("login") or ""),
|
||||
"created_at": str(comment.get("created_at") or ""),
|
||||
"updated_at": str(comment.get("updated_at") or ""),
|
||||
"body": str(comment.get("body") or ""),
|
||||
}
|
||||
|
||||
|
||||
def normalize_timeline_event(event: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Normalize the GitHub timeline event fields used by triage output."""
|
||||
actor = str(event.get("actor", {}).get("login") or "")
|
||||
created_at = str(event.get("created_at") or event.get("submitted_at") or "")
|
||||
event_type = str(event.get("event") or event.get("__typename") or "unknown")
|
||||
label_name = ""
|
||||
assignee = ""
|
||||
source_issue_number: int | None = None
|
||||
source_issue_url = ""
|
||||
commit_id = ""
|
||||
|
||||
label = event.get("label")
|
||||
if isinstance(label, dict) and label.get("name"):
|
||||
label_name = str(label["name"])
|
||||
|
||||
assignee_payload = event.get("assignee")
|
||||
if isinstance(assignee_payload, dict) and assignee_payload.get("login"):
|
||||
assignee = str(assignee_payload["login"])
|
||||
|
||||
source = event.get("source")
|
||||
if isinstance(source, dict):
|
||||
issue_payload = source.get("issue")
|
||||
if isinstance(issue_payload, dict):
|
||||
if issue_payload.get("number"):
|
||||
source_issue_number = int(issue_payload["number"])
|
||||
if issue_payload.get("html_url"):
|
||||
source_issue_url = str(issue_payload["html_url"])
|
||||
|
||||
commit_id_value = event.get("commit_id")
|
||||
if isinstance(commit_id_value, str):
|
||||
commit_id = commit_id_value
|
||||
|
||||
return {
|
||||
"event": event_type,
|
||||
"actor": actor,
|
||||
"created_at": created_at,
|
||||
"label": label_name,
|
||||
"assignee": assignee,
|
||||
"commit_id": commit_id,
|
||||
"source_issue_number": source_issue_number,
|
||||
"source_issue_url": source_issue_url,
|
||||
}
|
||||
|
||||
|
||||
def gather_text_blocks(issue: dict[str, Any], comments: list[dict[str, Any]]) -> list[str]:
|
||||
"""Return the issue body plus discussion comment bodies for heuristic parsing."""
|
||||
blocks = [issue.get("body", "")]
|
||||
blocks.extend(comment.get("body", "") for comment in comments)
|
||||
return [block for block in blocks if block]
|
||||
|
||||
|
||||
def has_any_pattern(text_blocks: list[str], patterns: tuple[str, ...]) -> bool:
|
||||
"""Return whether any normalized text block contains any requested pattern."""
|
||||
lowered_blocks = [collapse_whitespace(block).lower() for block in text_blocks]
|
||||
return any(pattern in block for block in lowered_blocks for pattern in patterns)
|
||||
|
||||
|
||||
def choose_issue_type_candidates(issue: dict[str, Any], text_blocks: list[str]) -> list[str]:
|
||||
"""Infer lightweight issue-type candidates from labels and discussion text."""
|
||||
labels = [label.lower() for label in issue.get("labels", [])]
|
||||
text = "\n".join(text_blocks).lower()
|
||||
candidates: list[str] = []
|
||||
|
||||
if any(label in {"bug", "regression"} for label in labels) or "bug" in text or "error" in text or "fails" in text:
|
||||
candidates.append("bug")
|
||||
if any(label in {"feature", "enhancement"} for label in labels) or "feature" in text or "support" in text:
|
||||
candidates.append("feature")
|
||||
if any(label in {"documentation", "docs"} for label in labels) or "documentation" in text or "readme" in text:
|
||||
candidates.append("docs")
|
||||
if any(label in {"question", "help wanted"} for label in labels) or "?" in issue.get("title", ""):
|
||||
candidates.append("question")
|
||||
if any(label in {"chore", "maintenance", "refactor"} for label in labels) or "cleanup" in text or "refactor" in text:
|
||||
candidates.append("maintenance")
|
||||
|
||||
if not candidates:
|
||||
candidates.append("question" if issue.get("body", "").strip().endswith("?") else "bug")
|
||||
|
||||
ordered_candidates: list[str] = []
|
||||
for candidate in ISSUE_TYPE_CANDIDATES:
|
||||
if candidate in candidates:
|
||||
ordered_candidates.append(candidate)
|
||||
|
||||
return ordered_candidates
|
||||
|
||||
|
||||
def extract_references_from_text(text: str) -> dict[str, list[str]]:
|
||||
"""Extract issue, commit, and file-path references from one text block."""
|
||||
issue_numbers = sorted({match.group(1) for match in ISSUE_REFERENCE_PATTERN.finditer(text)}, key=int)
|
||||
commit_shas = sorted({match.group(0) for match in COMMIT_REFERENCE_PATTERN.finditer(text)})
|
||||
file_paths = sorted({match.group(0) for match in FILE_PATH_PATTERN.finditer(text)})
|
||||
|
||||
return {
|
||||
"issues": [f"#{number}" for number in issue_numbers],
|
||||
"commit_shas": commit_shas,
|
||||
"file_paths": file_paths,
|
||||
}
|
||||
|
||||
|
||||
def merge_reference_values(values: list[dict[str, list[str]]]) -> dict[str, list[str]]:
|
||||
"""Merge extracted reference lists while preserving sorted unique output."""
|
||||
merged: dict[str, set[str]] = {"issues": set(), "commit_shas": set(), "file_paths": set()}
|
||||
for value in values:
|
||||
for key in merged:
|
||||
merged[key].update(value.get(key, []))
|
||||
|
||||
return {
|
||||
"issues": sorted(merged["issues"], key=lambda item: int(item[1:])),
|
||||
"commit_shas": sorted(merged["commit_shas"]),
|
||||
"file_paths": sorted(merged["file_paths"]),
|
||||
}
|
||||
|
||||
|
||||
def build_references(issue: dict[str, Any], comments: list[dict[str, Any]], events: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""Build structured references from issue text and timeline context."""
|
||||
extracted = [extract_references_from_text(issue.get("body", ""))]
|
||||
extracted.extend(extract_references_from_text(comment.get("body", "")) for comment in comments)
|
||||
merged = merge_reference_values(extracted)
|
||||
referenced_by_timeline = sorted(
|
||||
{
|
||||
f"#{event['source_issue_number']}"
|
||||
for event in events
|
||||
if event.get("source_issue_number") is not None
|
||||
},
|
||||
key=lambda item: int(item[1:]),
|
||||
)
|
||||
|
||||
pull_request_references = sorted(
|
||||
{
|
||||
issue_reference
|
||||
for issue_reference in merged["issues"]
|
||||
if issue_reference != f"#{issue['number']}"
|
||||
},
|
||||
key=lambda item: int(item[1:]),
|
||||
)
|
||||
|
||||
return {
|
||||
"issues": merged["issues"],
|
||||
"pull_requests_or_issues": pull_request_references,
|
||||
"commit_shas": merged["commit_shas"],
|
||||
"file_paths": merged["file_paths"],
|
||||
"timeline_cross_references": referenced_by_timeline,
|
||||
}
|
||||
|
||||
|
||||
def build_information_flags(
|
||||
issue: dict[str, Any],
|
||||
comments: list[dict[str, Any]],
|
||||
issue_type_candidates: list[str],
|
||||
) -> dict[str, bool]:
|
||||
"""Derive missing-information and readiness flags with issue-type-aware heuristics."""
|
||||
text_blocks = gather_text_blocks(issue, comments)
|
||||
has_reproduction_steps = has_any_pattern(text_blocks, REPRODUCTION_PATTERNS)
|
||||
has_expected_behavior = has_any_pattern(text_blocks, EXPECTED_BEHAVIOR_PATTERNS)
|
||||
has_actual_behavior = has_any_pattern(text_blocks, ACTUAL_BEHAVIOR_PATTERNS)
|
||||
has_environment_details = has_any_pattern(text_blocks, ENVIRONMENT_PATTERNS)
|
||||
has_acceptance_signals = has_any_pattern(text_blocks, ACCEPTANCE_PATTERNS)
|
||||
primary_issue_type = issue_type_candidates[0] if issue_type_candidates else "bug"
|
||||
|
||||
if primary_issue_type == "bug":
|
||||
needs_clarification = not (
|
||||
(has_actual_behavior and (has_reproduction_steps or has_environment_details))
|
||||
or has_acceptance_signals
|
||||
)
|
||||
elif primary_issue_type in {"feature", "docs"}:
|
||||
needs_clarification = not (has_expected_behavior or has_acceptance_signals)
|
||||
elif primary_issue_type == "maintenance":
|
||||
needs_clarification = not (has_expected_behavior or has_actual_behavior or has_acceptance_signals)
|
||||
else:
|
||||
needs_clarification = not (has_expected_behavior or has_actual_behavior or has_acceptance_signals)
|
||||
|
||||
return {
|
||||
"has_reproduction_steps": has_reproduction_steps,
|
||||
"has_expected_behavior": has_expected_behavior,
|
||||
"has_actual_behavior": has_actual_behavior,
|
||||
"has_environment_details": has_environment_details,
|
||||
"has_acceptance_signals": has_acceptance_signals,
|
||||
"needs_clarification": needs_clarification,
|
||||
}
|
||||
|
||||
|
||||
def choose_affected_topics(issue: dict[str, Any], comments: list[dict[str, Any]]) -> list[str]:
|
||||
"""Map the issue discussion to likely active topics when obvious keyword matches exist."""
|
||||
text = "\n".join(gather_text_blocks(issue, comments)).lower()
|
||||
matches: list[str] = []
|
||||
for topic, keywords in ACTIVE_TOPIC_KEYWORDS.items():
|
||||
if any(keyword in text for keyword in keywords):
|
||||
matches.append(topic)
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
def choose_next_action(
|
||||
information_flags: dict[str, bool],
|
||||
issue_type_candidates: list[str],
|
||||
affected_topics: list[str],
|
||||
) -> str:
|
||||
"""Choose the next handling mode for boot handoff."""
|
||||
if information_flags["needs_clarification"]:
|
||||
return "clarify-issue-before-code"
|
||||
if affected_topics:
|
||||
return "resume-existing-topic-with-boot"
|
||||
if "docs" in issue_type_candidates and issue_type_candidates[0] == "docs":
|
||||
return "start-new-docs-topic-with-boot"
|
||||
return "start-new-topic-with-boot"
|
||||
|
||||
|
||||
def build_triage_hints(issue: dict[str, Any], comments: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""Build lightweight, reviewable triage hints for boot follow-up."""
|
||||
text_blocks = gather_text_blocks(issue, comments)
|
||||
issue_type_candidates = choose_issue_type_candidates(issue, text_blocks)
|
||||
information_flags = build_information_flags(issue, comments, issue_type_candidates)
|
||||
affected_topics = choose_affected_topics(issue, comments)
|
||||
next_action = choose_next_action(information_flags, issue_type_candidates, affected_topics)
|
||||
|
||||
return {
|
||||
"issue_type_candidates": issue_type_candidates,
|
||||
"information_flags": information_flags,
|
||||
"affected_active_topics": affected_topics,
|
||||
"next_action": next_action,
|
||||
"boot_handoff": {
|
||||
"recommended_skill": "gframework-boot",
|
||||
"mode": "resume" if affected_topics else "new",
|
||||
"notes": (
|
||||
"Use gframework-boot to verify the issue against local code and active ai-plan topics."
|
||||
if not information_flags["needs_clarification"]
|
||||
else "Use gframework-boot to record a clarification-first task before changing code."
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_result(issue_number: int, branch: str, resolution_mode: str) -> dict[str, Any]:
|
||||
"""Build the full issue review payload for the selected issue."""
|
||||
parse_warnings: list[str] = []
|
||||
issue = fetch_issue_metadata(issue_number)
|
||||
raw_comments = fetch_issue_comments(issue_number)
|
||||
comments = [normalize_comment(comment) for comment in raw_comments]
|
||||
|
||||
events: list[dict[str, Any]] = []
|
||||
try:
|
||||
raw_events = fetch_issue_timeline(issue_number)
|
||||
events = [normalize_timeline_event(event) for event in raw_events]
|
||||
except Exception as error: # noqa: BLE001
|
||||
parse_warnings.append(f"Issue timeline could not be fetched or parsed: {error}")
|
||||
|
||||
references = build_references(issue, comments, events)
|
||||
triage_hints = build_triage_hints(issue, comments)
|
||||
|
||||
return {
|
||||
"issue": {
|
||||
**issue,
|
||||
"resolved_from_branch": branch,
|
||||
"resolution_mode": resolution_mode,
|
||||
},
|
||||
"discussion": {
|
||||
"comment_count": len(comments),
|
||||
"comments": comments,
|
||||
},
|
||||
"events": {
|
||||
"count": len(events),
|
||||
"items": events,
|
||||
},
|
||||
"references": references,
|
||||
"triage_hints": triage_hints,
|
||||
"parse_warnings": parse_warnings,
|
||||
}
|
||||
|
||||
|
||||
def write_json_output(result: dict[str, Any], output_path: str) -> str:
|
||||
"""Write the full JSON result to disk and return the destination path."""
|
||||
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 summarize_events(events: list[dict[str, Any]]) -> list[str]:
|
||||
"""Convert normalized events into concise text lines."""
|
||||
lines: list[str] = []
|
||||
for event in events:
|
||||
summary = f"- {event['event']}"
|
||||
details: list[str] = []
|
||||
if event.get("actor"):
|
||||
details.append(f"actor={event['actor']}")
|
||||
if event.get("label"):
|
||||
details.append(f"label={event['label']}")
|
||||
if event.get("assignee"):
|
||||
details.append(f"assignee={event['assignee']}")
|
||||
if event.get("source_issue_number") is not None:
|
||||
details.append(f"source_issue=#{event['source_issue_number']}")
|
||||
if event.get("commit_id"):
|
||||
details.append(f"commit={event['commit_id'][:12]}")
|
||||
if event.get("created_at"):
|
||||
details.append(f"at={event['created_at']}")
|
||||
if details:
|
||||
summary += " (" + ", ".join(details) + ")"
|
||||
lines.append(summary)
|
||||
return lines
|
||||
|
||||
|
||||
def format_text(
|
||||
result: dict[str, Any],
|
||||
*,
|
||||
sections: list[str] | None = None,
|
||||
max_description_length: int = 400,
|
||||
json_output_path: str | None = None,
|
||||
) -> str:
|
||||
"""Format the result payload into concise text output."""
|
||||
lines: list[str] = []
|
||||
selected_sections = set(sections or DISPLAY_SECTION_CHOICES)
|
||||
issue = result["issue"]
|
||||
triage_hints = result["triage_hints"]
|
||||
discussion = result["discussion"]
|
||||
events = result["events"]
|
||||
references = result["references"]
|
||||
|
||||
if "issue" in selected_sections:
|
||||
lines.append(f"Issue #{issue['number']}: {issue['title']}")
|
||||
lines.append(f"State: {issue['state']}")
|
||||
lines.append(f"Author: {issue['author']}")
|
||||
lines.append(f"Labels: {', '.join(issue['labels']) if issue['labels'] else '(none)'}")
|
||||
lines.append(f"Assignees: {', '.join(issue['assignees']) if issue['assignees'] else '(none)'}")
|
||||
lines.append(f"Milestone: {issue['milestone'] or '(none)'}")
|
||||
lines.append(f"Created: {issue['created_at']}")
|
||||
lines.append(f"Updated: {issue['updated_at']}")
|
||||
lines.append(f"Resolved from branch: {issue['resolved_from_branch'] or '(not branch-based)'}")
|
||||
lines.append(f"Resolution mode: {issue['resolution_mode']}")
|
||||
lines.append(f"URL: {issue['url']}")
|
||||
if issue["body"]:
|
||||
lines.append("Body:")
|
||||
lines.append(truncate_text(issue["body"], max_description_length))
|
||||
|
||||
if "summary" in selected_sections:
|
||||
lines.append("")
|
||||
lines.append("Triage summary:")
|
||||
lines.append("- Issue type candidates: " + ", ".join(triage_hints["issue_type_candidates"]))
|
||||
information_flags = triage_hints["information_flags"]
|
||||
lines.append(
|
||||
"- Information flags: "
|
||||
+ ", ".join(
|
||||
[
|
||||
f"repro={'yes' if information_flags['has_reproduction_steps'] else 'no'}",
|
||||
f"expected={'yes' if information_flags['has_expected_behavior'] else 'no'}",
|
||||
f"actual={'yes' if information_flags['has_actual_behavior'] else 'no'}",
|
||||
f"environment={'yes' if information_flags['has_environment_details'] else 'no'}",
|
||||
f"acceptance={'yes' if information_flags['has_acceptance_signals'] else 'no'}",
|
||||
f"needs_clarification={'yes' if information_flags['needs_clarification'] else 'no'}",
|
||||
]
|
||||
)
|
||||
)
|
||||
lines.append(
|
||||
"- Affected active topics: "
|
||||
+ (", ".join(triage_hints["affected_active_topics"]) if triage_hints["affected_active_topics"] else "(none)")
|
||||
)
|
||||
lines.append(f"- Next action: {triage_hints['next_action']}")
|
||||
lines.append(f"- Boot handoff: {triage_hints['boot_handoff']['notes']}")
|
||||
|
||||
if "comments" in selected_sections:
|
||||
lines.append("")
|
||||
lines.append(f"Discussion comments: {discussion['comment_count']}")
|
||||
for comment in discussion["comments"]:
|
||||
lines.append(f"- {comment['author']} at {comment['created_at']}")
|
||||
lines.append(f" {truncate_text(comment['body'], max_description_length)}")
|
||||
|
||||
if "events" in selected_sections:
|
||||
lines.append("")
|
||||
lines.append(f"Timeline events: {events['count']}")
|
||||
lines.extend(summarize_events(events["items"]))
|
||||
|
||||
if "references" in selected_sections:
|
||||
lines.append("")
|
||||
lines.append("References:")
|
||||
lines.append("- Mentioned issues: " + (", ".join(references["issues"]) if references["issues"] else "(none)"))
|
||||
lines.append(
|
||||
"- Cross references: "
|
||||
+ (
|
||||
", ".join(references["timeline_cross_references"])
|
||||
if references["timeline_cross_references"]
|
||||
else "(none)"
|
||||
)
|
||||
)
|
||||
lines.append(
|
||||
"- Related issue/PR mentions: "
|
||||
+ (
|
||||
", ".join(references["pull_requests_or_issues"])
|
||||
if references["pull_requests_or_issues"]
|
||||
else "(none)"
|
||||
)
|
||||
)
|
||||
lines.append("- Commit SHAs: " + (", ".join(references["commit_shas"]) if references["commit_shas"] else "(none)"))
|
||||
lines.append("- File paths: " + (", ".join(references["file_paths"]) if references["file_paths"] else "(none)"))
|
||||
|
||||
if result["parse_warnings"] and "warnings" in selected_sections:
|
||||
lines.append("")
|
||||
lines.append("Warnings:")
|
||||
for warning in result["parse_warnings"]:
|
||||
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)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
"""Parse CLI arguments."""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--branch", help="Override the current branch name.")
|
||||
parser.add_argument("--issue", type=int, help="Fetch a specific issue number instead of auto-selecting one.")
|
||||
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(
|
||||
"--max-description-length",
|
||||
type=int,
|
||||
default=400,
|
||||
help="Truncate long text bodies in text output to this many characters.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the CLI entry point."""
|
||||
args = parse_args()
|
||||
branch = args.branch or get_current_branch()
|
||||
issue_number, resolution_mode = resolve_issue_number(args.issue)
|
||||
result = build_result(issue_number, branch, resolution_mode)
|
||||
|
||||
json_output_path: str | None = None
|
||||
if args.json_output:
|
||||
json_output_path = write_json_output(result, args.json_output)
|
||||
|
||||
if args.format == "json":
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return
|
||||
|
||||
print(
|
||||
format_text(
|
||||
result,
|
||||
sections=args.section,
|
||||
max_description_length=args.max_description_length,
|
||||
json_output_path=json_output_path,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as error: # noqa: BLE001
|
||||
print(str(error), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2025-2026 GeWuYou
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""Regression tests for the GFramework issue review fetch helper."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
SCRIPT_PATH = Path(__file__).with_name("fetch_current_issue_review.py")
|
||||
MODULE_SPEC = importlib.util.spec_from_file_location("fetch_current_issue_review", SCRIPT_PATH)
|
||||
if MODULE_SPEC is None or MODULE_SPEC.loader is None:
|
||||
raise RuntimeError(f"Unable to load module from {SCRIPT_PATH}.")
|
||||
|
||||
MODULE = importlib.util.module_from_spec(MODULE_SPEC)
|
||||
MODULE_SPEC.loader.exec_module(MODULE)
|
||||
|
||||
|
||||
class SelectSingleOpenIssueNumberTests(unittest.TestCase):
|
||||
"""Cover auto-resolution rules for open GitHub issues."""
|
||||
|
||||
def test_select_single_open_issue_number_filters_pull_requests(self) -> None:
|
||||
"""Pull requests in the issues API must not block the single-open-issue path."""
|
||||
selected = MODULE.select_single_open_issue_number(
|
||||
[
|
||||
{"number": 10, "pull_request": {"url": "https://example.test/pr/10"}},
|
||||
{"number": 11},
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(selected, 11)
|
||||
|
||||
def test_select_single_open_issue_number_rejects_multiple_plain_issues(self) -> None:
|
||||
"""Auto-resolution must stop when more than one plain issue is open."""
|
||||
with self.assertRaisesRegex(RuntimeError, "Multiple open GitHub issues found"):
|
||||
MODULE.select_single_open_issue_number([{"number": 11}, {"number": 12}])
|
||||
|
||||
|
||||
class ExtractReferencesFromTextTests(unittest.TestCase):
|
||||
"""Cover lightweight reference extraction used by the text and JSON output."""
|
||||
|
||||
def test_extract_references_from_text_finds_issue_commit_and_path_mentions(self) -> None:
|
||||
"""The helper should retain the high-signal references needed for follow-up triage."""
|
||||
references = MODULE.extract_references_from_text(
|
||||
"See #123, commit abcdef1234567890, and GFramework.Core/Systems/Runner.cs for the failing path."
|
||||
)
|
||||
|
||||
self.assertEqual(references["issues"], ["#123"])
|
||||
self.assertEqual(references["commit_shas"], ["abcdef1234567890"])
|
||||
self.assertEqual(references["file_paths"], ["GFramework.Core/Systems/Runner.cs"])
|
||||
|
||||
|
||||
class BuildTriageHintsTests(unittest.TestCase):
|
||||
"""Cover next-action classification for non-bug issue flows."""
|
||||
|
||||
def test_build_triage_hints_routes_docs_issue_to_docs_topic_without_bug_style_clarification(self) -> None:
|
||||
"""Docs issues with a clear requested change should not be forced through bug-style clarification."""
|
||||
triage_hints = MODULE.build_triage_hints(
|
||||
{
|
||||
"title": "Update documentation landing page",
|
||||
"labels": ["docs"],
|
||||
"body": "The guide should explain the landing-page layout for new contributors.",
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
self.assertEqual(triage_hints["issue_type_candidates"][0], "docs")
|
||||
self.assertEqual(triage_hints["affected_active_topics"], [])
|
||||
self.assertFalse(triage_hints["information_flags"]["needs_clarification"])
|
||||
self.assertEqual(triage_hints["next_action"], "start-new-docs-topic-with-boot")
|
||||
|
||||
def test_build_triage_hints_routes_feature_issue_to_new_topic_when_request_is_clear(self) -> None:
|
||||
"""Feature requests with explicit desired behavior should stay actionable without fake bug repro gates."""
|
||||
triage_hints = MODULE.build_triage_hints(
|
||||
{
|
||||
"title": "Support release note previews",
|
||||
"labels": ["enhancement"],
|
||||
"body": "The workflow should support previewing generated notes before completion.",
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
self.assertEqual(triage_hints["issue_type_candidates"][0], "feature")
|
||||
self.assertEqual(triage_hints["affected_active_topics"], [])
|
||||
self.assertFalse(triage_hints["information_flags"]["needs_clarification"])
|
||||
self.assertEqual(triage_hints["next_action"], "start-new-topic-with-boot")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
114
.agents/skills/gframework-multi-agent-batch/SKILL.md
Normal file
114
.agents/skills/gframework-multi-agent-batch/SKILL.md
Normal file
@ -0,0 +1,114 @@
|
||||
---
|
||||
name: gframework-multi-agent-batch
|
||||
description: Repository-specific multi-agent orchestration workflow for the GFramework repo. Use when the main agent should keep coordinating multiple parallel subagents, maintain ai-plan recovery artifacts, review subagent results, and continue bounded multi-agent waves until reviewability, context budget, or branch-diff limits say to stop.
|
||||
---
|
||||
|
||||
# GFramework Multi-Agent Batch
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill when `gframework-boot` has already established repository context, and the task now benefits from the
|
||||
main agent acting as the persistent coordinator for multiple parallel subagents.
|
||||
|
||||
Treat `AGENTS.md` as the source of truth. This skill expands the repository's multi-agent coordination rules; it does
|
||||
not replace them.
|
||||
|
||||
This skill is for orchestration-heavy work, not for every task that merely happens to use one subagent. Prefer it when
|
||||
the main agent must keep splitting bounded write slices, monitoring progress, updating `ai-plan`, validating accepted
|
||||
results, and deciding whether another delegation wave is still safe.
|
||||
|
||||
## Use When
|
||||
|
||||
Adopt this workflow only when all of the following are true:
|
||||
|
||||
1. The task is complex enough that multiple parallel slices materially shorten the work.
|
||||
2. The candidate write sets can be kept disjoint.
|
||||
3. The main agent still needs to own review, validation, integration, and `ai-plan` updates.
|
||||
4. Another wave is still likely to fit the branch-diff, context-budget, and reviewability budget.
|
||||
|
||||
Prefer `gframework-batch-boot` instead when the task is mainly repetitive bulk progress with a single obvious slice
|
||||
pattern and little need for continuous multi-worker orchestration.
|
||||
|
||||
## Startup Workflow
|
||||
|
||||
1. Execute the normal `gframework-boot` startup sequence first:
|
||||
- read `AGENTS.md`
|
||||
- read `.ai/environment/tools.ai.yaml`
|
||||
- read `ai-plan/public/README.md`
|
||||
- read the mapped active topic `todos/` and `traces/`
|
||||
2. Confirm that the active topic and current branch still match the work you are about to delegate.
|
||||
3. Define the current wave in one sentence:
|
||||
- benchmark-host alignment
|
||||
- runtime hotspot reduction
|
||||
- documentation synchronization
|
||||
- other bounded multi-slice work
|
||||
4. Identify the critical path and keep it local.
|
||||
5. Split only the non-blocking work into disjoint ownership slices.
|
||||
6. Estimate whether one more delegation wave is still safe:
|
||||
- include current branch diff vs baseline
|
||||
- loaded `ai-plan` context
|
||||
- expected validation output
|
||||
- expected integration overhead
|
||||
|
||||
## Worker Design Rules
|
||||
|
||||
For each `worker` subagent, specify:
|
||||
|
||||
- the concrete objective
|
||||
- the exact owned files or subsystem
|
||||
- files or areas the worker must not touch
|
||||
- required validation commands
|
||||
- expected output format
|
||||
- a reminder that other agents may be editing the repo
|
||||
|
||||
Prefer `explorer` subagents when the result is read-only ranking, tracing, or candidate discovery.
|
||||
|
||||
Do not launch two workers whose write sets overlap unless the overlap is trivial and the main agent has already decided
|
||||
how to serialize or reconcile that overlap.
|
||||
|
||||
## Main-Agent Loop
|
||||
|
||||
While workers run, the main agent should only do non-overlapping work:
|
||||
|
||||
- inspect the next candidate slices
|
||||
- recompute branch-diff and context-budget posture
|
||||
- review finished worker output
|
||||
- queue follow-up validation
|
||||
- keep `ai-plan/public/**` current when accepted scope or next steps change
|
||||
|
||||
After each completed worker task:
|
||||
|
||||
1. Review the reported ownership, validation, and changed files.
|
||||
2. Confirm the worker stayed inside its boundary.
|
||||
3. Run or rerun the required validation locally if the slice is accepted.
|
||||
4. Record accepted delegated scope, validation milestones, and the next recovery point in the active `ai-plan` files.
|
||||
5. Reassess whether another wave is still reviewable and safe.
|
||||
|
||||
## Stop Conditions
|
||||
|
||||
Stop the current multi-agent wave when any of the following becomes true:
|
||||
|
||||
- the next wave would likely push the main agent near or beyond a safe context budget
|
||||
- the remaining work no longer splits into clean disjoint ownership slices
|
||||
- branch diff vs baseline is approaching the current reviewability budget
|
||||
- integrating another worker would degrade clarity more than it would save time
|
||||
- validation failures show that the next step belongs on the critical path and should stay local
|
||||
|
||||
If a branch-size threshold is also in play, treat it as a coarse repository-scope signal, not the sole decision rule.
|
||||
|
||||
## Task Tracking
|
||||
|
||||
When this workflow is active, the main agent must keep the active `ai-plan` topic current with:
|
||||
|
||||
- delegated scope that has been accepted
|
||||
- validation results
|
||||
- current branch-diff posture if it affects stop decisions
|
||||
- the next recommended resume step
|
||||
|
||||
The main agent should keep active entries concise enough that `boot` can still recover the current wave quickly.
|
||||
|
||||
## Example Triggers
|
||||
|
||||
- `Use $gframework-multi-agent-batch to coordinate non-conflicting subagents for this complex CQRS task.`
|
||||
- `Keep delegating bounded parallel slices, update ai-plan, and verify each worker result before continuing.`
|
||||
- `Run a multi-agent wave where the main agent owns review, validation, and integration.`
|
||||
@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "GFramework Multi-Agent Batch"
|
||||
short_description: "Coordinate bounded parallel subagents with ai-plan tracking"
|
||||
default_prompt: "Use $gframework-multi-agent-batch to coordinate multiple bounded parallel subagents in this GFramework repository while the main agent owns ai-plan updates, validation, review, and integration."
|
||||
6
.github/workflows/auto-tag.yml
vendored
6
.github/workflows/auto-tag.yml
vendored
@ -17,6 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
published: ${{ steps.semantic_release.outputs.new_release_published }}
|
||||
last_tag: ${{ steps.semantic_release.outputs.last_release_git_tag }}
|
||||
@ -71,7 +72,7 @@ jobs:
|
||||
env:
|
||||
OUTPUT: PREVIEW_RELEASE_NOTES.md
|
||||
GITHUB_REPO: ${{ github.repository }}
|
||||
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Write preview summary
|
||||
env:
|
||||
@ -108,6 +109,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
environment:
|
||||
name: release-approval
|
||||
steps:
|
||||
@ -157,7 +159,7 @@ jobs:
|
||||
env:
|
||||
OUTPUT: PUBLISHED_RELEASE_NOTES.md
|
||||
GITHUB_REPO: ${{ github.repository }}
|
||||
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Write release summary
|
||||
env:
|
||||
|
||||
71
.github/workflows/benchmark.yml
vendored
Normal file
71
.github/workflows/benchmark.yml
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
# Copyright (c) 2025-2026 GeWuYou
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Benchmark
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
benchmark_filter:
|
||||
description: '可选的 BenchmarkDotNet 过滤器;留空时仅执行 benchmark 项目 Release build'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
benchmark:
|
||||
name: Benchmark Build Or Run
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET 10
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.nuget/packages
|
||||
~/.local/share/NuGet
|
||||
key: ${{ runner.os }}-nuget-benchmarks-${{ hashFiles('GFramework.Cqrs.Benchmarks/*.csproj', 'GFramework.Cqrs/*.csproj', 'GFramework.Cqrs.Abstractions/*.csproj', 'GFramework.Core/*.csproj', 'GFramework.Core.Abstractions/*.csproj', '**/nuget.config') }}
|
||||
|
||||
- name: Restore benchmark project
|
||||
run: dotnet restore GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj
|
||||
|
||||
- name: Build benchmark project
|
||||
run: dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-restore
|
||||
|
||||
- name: Report build-only mode
|
||||
if: ${{ inputs.benchmark_filter == '' }}
|
||||
run: |
|
||||
echo "No benchmark filter provided."
|
||||
echo "Workflow completed after validating the benchmark project build."
|
||||
|
||||
- name: Run filtered benchmarks
|
||||
if: ${{ inputs.benchmark_filter != '' }}
|
||||
env:
|
||||
BENCHMARK_FILTER: ${{ inputs.benchmark_filter }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- \
|
||||
--filter "$BENCHMARK_FILTER"
|
||||
|
||||
- name: Upload BenchmarkDotNet artifacts
|
||||
if: ${{ always() && inputs.benchmark_filter != '' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: benchmark-artifacts
|
||||
path: |
|
||||
BenchmarkDotNet.Artifacts/**
|
||||
GFramework.Cqrs.Benchmarks/bin/Release/net10.0/BenchmarkDotNet.Artifacts/**
|
||||
if-no-files-found: ignore
|
||||
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@ -35,6 +35,9 @@ jobs:
|
||||
- name: Validate license headers
|
||||
run: python3 scripts/license-header.py --check
|
||||
|
||||
- name: Validate runtime-generator boundaries
|
||||
run: python3 scripts/validate-runtime-generator-boundaries.py
|
||||
|
||||
# 缓存MegaLinter
|
||||
- name: Cache MegaLinter
|
||||
uses: actions/cache@v5
|
||||
@ -152,6 +155,19 @@ jobs:
|
||||
- name: Build
|
||||
run: dotnet build GFramework.sln -c Release --no-restore
|
||||
|
||||
- name: Pack published modules
|
||||
run: |
|
||||
rm -rf ./packages
|
||||
dotnet pack GFramework.sln \
|
||||
-c Release \
|
||||
--no-build \
|
||||
--no-restore \
|
||||
-o ./packages \
|
||||
-p:IncludeSymbols=false
|
||||
|
||||
- name: Validate packed modules
|
||||
run: bash scripts/validate-packed-modules.sh ./packages
|
||||
|
||||
# 运行单元测试,输出TRX格式结果到TestResults目录
|
||||
# 顺序执行各测试项目,避免并发 dotnet test 进程导致“TRX 全绿但 step 仍返回失败”的假红状态
|
||||
- name: Test All Projects
|
||||
|
||||
38
.github/workflows/publish.yml
vendored
38
.github/workflows/publish.yml
vendored
@ -82,41 +82,10 @@ jobs:
|
||||
-p:IncludeSymbols=false
|
||||
|
||||
- name: Validate packed modules
|
||||
run: |
|
||||
set -euo pipefail
|
||||
run: bash scripts/validate-packed-modules.sh ./packages
|
||||
|
||||
expected_packages=(
|
||||
"GeWuYou.GFramework"
|
||||
"GeWuYou.GFramework.Core"
|
||||
"GeWuYou.GFramework.Core.Abstractions"
|
||||
"GeWuYou.GFramework.Core.SourceGenerators"
|
||||
"GeWuYou.GFramework.Cqrs"
|
||||
"GeWuYou.GFramework.Cqrs.Abstractions"
|
||||
"GeWuYou.GFramework.Cqrs.SourceGenerators"
|
||||
"GeWuYou.GFramework.Ecs.Arch"
|
||||
"GeWuYou.GFramework.Ecs.Arch.Abstractions"
|
||||
"GeWuYou.GFramework.Game"
|
||||
"GeWuYou.GFramework.Game.Abstractions"
|
||||
"GeWuYou.GFramework.Game.SourceGenerators"
|
||||
"GeWuYou.GFramework.Godot"
|
||||
"GeWuYou.GFramework.Godot.SourceGenerators"
|
||||
)
|
||||
|
||||
mapfile -t actual_packages < <(
|
||||
find ./packages -maxdepth 1 -type f -name '*.nupkg' -printf '%f\n' \
|
||||
| sed -E 's/\.[0-9][0-9A-Za-z.-]*\.nupkg$//' \
|
||||
| sort -u
|
||||
)
|
||||
|
||||
printf '%s\n' "${expected_packages[@]}" | sort > expected-packages.txt
|
||||
printf '%s\n' "${actual_packages[@]}" | sort > actual-packages.txt
|
||||
|
||||
echo "Expected packages:"
|
||||
cat expected-packages.txt
|
||||
echo "Actual packages:"
|
||||
cat actual-packages.txt
|
||||
|
||||
diff -u expected-packages.txt actual-packages.txt
|
||||
- name: Validate runtime-generator package boundaries
|
||||
run: python3 scripts/validate-runtime-generator-boundaries.py --package-dir ./packages
|
||||
|
||||
- name: Show packages
|
||||
run: ls -la ./packages || true
|
||||
@ -243,6 +212,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
packages: read
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository (at tag)
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -26,3 +26,4 @@ ai-libs/
|
||||
.codex
|
||||
# tool
|
||||
.venv/
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
55
AGENTS.md
55
AGENTS.md
@ -102,9 +102,13 @@ All AI agents and contributors must follow these rules when writing, reviewing,
|
||||
|
||||
## Repository Boot Skill
|
||||
|
||||
- The repository-maintained Codex boot skill lives at `.codex/skills/gframework-boot/`.
|
||||
- The repository-maintained Codex boot skill lives at `.agents/skills/gframework-boot/`.
|
||||
- The repository-maintained multi-agent coordination skill lives at `.agents/skills/gframework-multi-agent-batch/`.
|
||||
- Prefer invoking `$gframework-boot` when the user uses short startup prompts such as `boot`、`continue`、`next step`、
|
||||
`按 boot 开始`、`先看 AGENTS`、`继续当前任务`.
|
||||
- Prefer invoking `$gframework-multi-agent-batch` when the user explicitly wants the main agent to delegate bounded
|
||||
parallel work, track subagent progress, maintain `ai-plan`, verify subagent output, and keep coordinating until the
|
||||
current multi-agent batch reaches a natural stop boundary.
|
||||
- The boot skill is a startup convenience layer, not a replacement for this document. If the skill and `AGENTS.md`
|
||||
diverge, follow `AGENTS.md` first and update the skill in the same change.
|
||||
- The boot skill MUST read `AGENTS.md`、`.ai/environment/tools.ai.yaml`、`ai-plan/public/README.md` and the relevant
|
||||
@ -131,6 +135,52 @@ All AI agents and contributors must follow these rules when writing, reviewing,
|
||||
- The main agent remains responsible for reviewing and integrating subagent output. Unreviewed subagent conclusions do
|
||||
not count as final results.
|
||||
|
||||
### Multi-Agent Coordination Rules
|
||||
|
||||
The terms below describe the default guardrails for multi-agent batches and how they affect worker-launch decisions.
|
||||
|
||||
- `branch-diff budget`: the maximum acceptable branch diff size in files or lines before another worker wave becomes
|
||||
harder to review as a single PR.
|
||||
- `reviewability budget`: the cumulative complexity limit beyond which accepting more parallel slices would materially
|
||||
reduce review quality, even if the raw file count still looks acceptable.
|
||||
- `context-budget`: the main agent's remaining capacity to track active workers, validation, and integration state
|
||||
without losing critical execution context.
|
||||
- When any of these budgets approaches its safe limit, the main agent SHOULD stop launching more workers and close the
|
||||
current wave first.
|
||||
- `$gframework-multi-agent-batch` contains the fuller workflow and stop-condition guidance for applying these budgets in
|
||||
practice.
|
||||
|
||||
- Prefer the repository's multi-agent coordination mode when the user explicitly wants the main agent to keep
|
||||
orchestrating parallel subagents, or when the work naturally splits into `2+` disjoint write slices that can proceed
|
||||
in parallel without blocking the next local step.
|
||||
- In that mode, the main agent MUST keep ownership of:
|
||||
- critical-path selection
|
||||
- baseline and stop-condition tracking
|
||||
- `ai-plan` updates
|
||||
- validation planning and final validation
|
||||
- review and acceptance of every subagent result
|
||||
- the final integration and completion decision
|
||||
- Before spawning any `worker` subagent, the main agent MUST:
|
||||
- identify the immediate blocking step and keep it local
|
||||
- define disjoint file or subsystem ownership for each worker
|
||||
- state the required validation commands and expected output format
|
||||
- check that the expected write set still fits the current branch-diff and reviewability budget
|
||||
- While workers run, the main agent MUST avoid overlapping edits and focus on non-conflicting work such as:
|
||||
- ranking the next candidate slices
|
||||
- reviewing completed worker output
|
||||
- recomputing branch-diff and context-budget posture
|
||||
- keeping `ai-plan/public/**` recovery artifacts current
|
||||
- Before accepting a worker result, the main agent MUST confirm:
|
||||
- the worker stayed within its owned files or subsystem
|
||||
- the reported validation is sufficient for that slice
|
||||
- any accepted findings or follow-up scope are recorded in the active `ai-plan` todo or trace when the task is
|
||||
complex or multi-step
|
||||
- Do not continue launching workers merely because a file-count threshold still has room. Stop the current wave when
|
||||
ownership boundaries start to overlap, reviewability materially degrades, or the context-budget signal says the main
|
||||
agent should close the batch.
|
||||
- When a complex task uses multiple workers, the main agent SHOULD prefer the public workflow documented by
|
||||
`$gframework-multi-agent-batch` unless a more task-specific skill already provides stricter rules.
|
||||
|
||||
## Commenting Rules (MUST)
|
||||
|
||||
All generated or modified code MUST include clear and meaningful comments where required by the rules below.
|
||||
@ -212,6 +262,9 @@ All generated or modified code MUST include clear and meaningful comments where
|
||||
- Private fields: `_camelCase`
|
||||
- Keep abstractions projects free of implementation details and engine-specific dependencies.
|
||||
- Preserve existing module boundaries. Do not introduce new cross-module dependencies without clear architectural need.
|
||||
- Framework runtime, abstractions, and meta-package projects MUST NOT reference `*.SourceGenerators*` projects or packages,
|
||||
and MUST NOT use source-generator attributes such as `GenerateEnumExtensions` or `ContextAware`. Those capabilities are
|
||||
reserved for consumer projects, generator projects, examples explicitly meant to demonstrate generator usage, and related tests.
|
||||
|
||||
### Formatting
|
||||
|
||||
|
||||
@ -6,11 +6,11 @@
|
||||
<Project>
|
||||
<!-- Keep repository-wide analyzer behavior consistent while allowing only selected projects to opt into polyfills. -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Meziantou.Analyzer" Version="3.0.60">
|
||||
<PackageReference Include="Meziantou.Analyzer" Version="3.0.72">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Update="Meziantou.Polyfill" Version="1.0.121">
|
||||
<PackageReference Update="Meziantou.Polyfill" Version="1.0.123">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@ -84,6 +84,20 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia
|
||||
void RegisterCqrsPipelineBehavior<TBehavior>()
|
||||
where TBehavior : class;
|
||||
|
||||
/// <summary>
|
||||
/// 注册 CQRS 流式请求管道行为。
|
||||
/// 既支持实现 <c>IStreamPipelineBehavior<,></c> 的开放泛型行为类型,
|
||||
/// 也支持绑定到单一流式请求/响应对的封闭行为类型。
|
||||
/// </summary>
|
||||
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
||||
/// <exception cref="InvalidOperationException">当前架构的底层容器已冻结,无法继续注册流式管道行为。</exception>
|
||||
/// <exception cref="ObjectDisposedException">当前架构的底层容器已释放,无法继续注册流式管道行为。</exception>
|
||||
/// <remarks>
|
||||
/// 该入口应在架构初始化冻结容器之前调用;具体开放泛型或封闭行为类型的校验逻辑由底层容器负责。
|
||||
/// </remarks>
|
||||
void RegisterCqrsStreamPipelineBehavior<TBehavior>()
|
||||
where TBehavior : class;
|
||||
|
||||
/// <summary>
|
||||
/// 从指定程序集显式注册 CQRS 处理器。
|
||||
/// 当处理器位于默认架构程序集之外的模块或扩展程序集中时,可在初始化阶段调用该入口接入对应程序集。
|
||||
|
||||
@ -8,9 +8,15 @@ using GFramework.Core.Abstractions.Systems;
|
||||
namespace GFramework.Core.Abstractions.Ioc;
|
||||
|
||||
/// <summary>
|
||||
/// 依赖注入容器接口,定义了服务注册、解析和管理的基本操作
|
||||
/// 依赖注入容器接口,定义服务注册、解析与生命周期管理的统一入口。
|
||||
/// </summary>
|
||||
public interface IIocContainer : IContextAware
|
||||
/// <remarks>
|
||||
/// 实现者必须在 <see cref="IDisposable.Dispose" /> 中释放容器拥有的根 <see cref="IServiceProvider" /> 及其
|
||||
/// 关联同步资源,并保证释放操作幂等。
|
||||
/// 容器一旦释放,后续任何注册、解析、查询或作用域创建调用都必须抛出
|
||||
/// <see cref="ObjectDisposedException" />,避免消费者继续访问失效的运行时状态。
|
||||
/// </remarks>
|
||||
public interface IIocContainer : IContextAware, IDisposable
|
||||
{
|
||||
#region Register Methods
|
||||
|
||||
@ -99,6 +105,20 @@ public interface IIocContainer : IContextAware
|
||||
void RegisterCqrsPipelineBehavior<TBehavior>()
|
||||
where TBehavior : class;
|
||||
|
||||
/// <summary>
|
||||
/// 注册 CQRS 流式请求管道行为。
|
||||
/// </summary>
|
||||
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
||||
/// <exception cref="InvalidOperationException">容器已冻结,无法继续注册流式管道行为。</exception>
|
||||
/// <exception cref="ObjectDisposedException">容器已释放,无法继续注册流式管道行为。</exception>
|
||||
/// <remarks>
|
||||
/// 该入口既支持实现 <c>IStreamPipelineBehavior<,></c> 的开放泛型行为类型,
|
||||
/// 也支持绑定到单一流式请求/响应对的封闭行为类型。
|
||||
/// 应在容器冻结前的注册阶段调用;具体可注册形态由实现容器负责校验。
|
||||
/// </remarks>
|
||||
void RegisterCqrsStreamPipelineBehavior<TBehavior>()
|
||||
where TBehavior : class;
|
||||
|
||||
/// <summary>
|
||||
/// 从指定程序集显式注册 CQRS 处理器。
|
||||
/// 该入口适用于处理器不位于默认架构程序集中的场景,例如扩展包、模块程序集或拆分后的业务程序集。
|
||||
@ -135,6 +155,10 @@ public interface IIocContainer : IContextAware
|
||||
/// </summary>
|
||||
/// <typeparam name="T">期望获取的实例类型</typeparam>
|
||||
/// <returns>找到的第一个实例;如果未找到则返回 null</returns>
|
||||
/// <remarks>
|
||||
/// 在 <see cref="Freeze" /> 之前,该查询只保证返回已经物化为实例绑定的服务。
|
||||
/// 仅通过工厂或实现类型注册的服务在预冻结阶段可能不可见;若需要完整激活语义,请先冻结容器。
|
||||
/// </remarks>
|
||||
T? Get<T>() where T : class;
|
||||
|
||||
/// <summary>
|
||||
@ -143,6 +167,10 @@ public interface IIocContainer : IContextAware
|
||||
/// </summary>
|
||||
/// <param name="type">期望获取的实例类型</param>
|
||||
/// <returns>找到的第一个实例;如果未找到则返回 null</returns>
|
||||
/// <remarks>
|
||||
/// 在 <see cref="Freeze" /> 之前,该查询只保证返回已经物化为实例绑定的服务。
|
||||
/// 仅通过工厂或实现类型注册的服务在预冻结阶段可能不可见;若需要完整激活语义,请先冻结容器。
|
||||
/// </remarks>
|
||||
object? Get(Type type);
|
||||
|
||||
|
||||
@ -168,6 +196,9 @@ public interface IIocContainer : IContextAware
|
||||
/// </summary>
|
||||
/// <typeparam name="T">期望获取的实例类型</typeparam>
|
||||
/// <returns>所有符合条件的实例列表;如果没有则返回空数组</returns>
|
||||
/// <remarks>
|
||||
/// 在 <see cref="Freeze" /> 之前,该查询只会枚举当前已经可见的实例绑定,不会主动执行工厂或创建实现类型。
|
||||
/// </remarks>
|
||||
IReadOnlyList<T> GetAll<T>() where T : class;
|
||||
|
||||
/// <summary>
|
||||
@ -175,6 +206,9 @@ public interface IIocContainer : IContextAware
|
||||
/// </summary>
|
||||
/// <param name="type">期望获取的实例类型</param>
|
||||
/// <returns>所有符合条件的实例列表;如果没有则返回空数组</returns>
|
||||
/// <remarks>
|
||||
/// 在 <see cref="Freeze" /> 之前,该查询只会枚举当前已经可见的实例绑定,不会主动执行工厂或创建实现类型。
|
||||
/// </remarks>
|
||||
IReadOnlyList<object> GetAll(Type type);
|
||||
|
||||
|
||||
@ -213,8 +247,26 @@ public interface IIocContainer : IContextAware
|
||||
/// </summary>
|
||||
/// <typeparam name="T">要检查的类型</typeparam>
|
||||
/// <returns>如果容器中包含指定类型的实例则返回true,否则返回false</returns>
|
||||
/// <remarks>
|
||||
/// 在 <see cref="Freeze" /> 之前,该方法更接近“是否存在对应注册”的检查,而不是完整的 DI 可解析性判断。
|
||||
/// </remarks>
|
||||
bool Contains<T>() where T : class;
|
||||
|
||||
/// <summary>
|
||||
/// 检查容器中是否存在可赋值给指定服务类型的注册项,而不要求解析出实例。
|
||||
/// </summary>
|
||||
/// <param name="type">要检查的服务类型。</param>
|
||||
/// <returns>若存在显式注册或开放泛型映射可满足该服务类型,则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="type" /> 为 <see langword="null" /> 时抛出。</exception>
|
||||
/// <exception cref="ObjectDisposedException">当调用 <see cref="HasRegistration(Type)" /> 时容器已被释放时抛出。</exception>
|
||||
/// <remarks>
|
||||
/// 该入口面向“先判断是否值得解析实例”的热路径优化场景。
|
||||
/// 与 <see cref="Contains{T}" /> 不同,它不会为了判断结果而激活服务实例,因此可避免把瞬态对象创建、
|
||||
/// 多服务枚举或日志分配混入仅需存在性判断的调用链中。
|
||||
/// 该方法按服务键与开放泛型映射判断可见性,不会把“仅以实现类型自身注册”的实例误判成其所有可赋值接口都已注册。
|
||||
/// </remarks>
|
||||
bool HasRegistration(Type type);
|
||||
|
||||
/// <summary>
|
||||
/// 判断容器中是否包含某个具体的实例对象
|
||||
/// </summary>
|
||||
|
||||
@ -10,11 +10,13 @@ using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Abstractions.Query;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.Command;
|
||||
using GFramework.Core.Cqrs;
|
||||
using GFramework.Core.Environment;
|
||||
using GFramework.Core.Events;
|
||||
using GFramework.Core.Ioc;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Core.Query;
|
||||
using GFramework.Core.Services.Modules;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Tests.Architectures;
|
||||
@ -41,9 +43,13 @@ namespace GFramework.Core.Tests.Architectures;
|
||||
/// - GetUtility方法 - 获取未注册工具时抛出异常
|
||||
/// - GetEnvironment方法 - 获取环境对象
|
||||
/// </summary>
|
||||
[NonParallelizable]
|
||||
[TestFixture]
|
||||
public class ArchitectureContextTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化测试所需的容器与默认服务实例。
|
||||
/// </summary>
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
@ -71,10 +77,22 @@ public class ArchitectureContextTests
|
||||
_container.RegisterPlurality(_queryBus);
|
||||
_container.RegisterPlurality(_asyncQueryBus);
|
||||
_container.RegisterPlurality(_environment);
|
||||
new CqrsRuntimeModule().Register(_container);
|
||||
RegisterLegacyBridgeHandlers(_container);
|
||||
|
||||
_context = new ArchitectureContext(_container);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放当前测试创建的容器,并清理 legacy bridge 共享计数状态。
|
||||
/// </summary>
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
LegacyBridgePipelineTracker.Reset();
|
||||
_container?.Dispose();
|
||||
}
|
||||
|
||||
private AsyncQueryExecutor? _asyncQueryBus;
|
||||
private CommandExecutor? _commandBus;
|
||||
private MicrosoftDiContainer? _container;
|
||||
@ -124,6 +142,31 @@ public class ArchitectureContextTests
|
||||
Assert.That(result, Is.EqualTo(42));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 legacy 查询通过 <see cref="ArchitectureContext" /> 发送时会进入统一 CQRS pipeline,
|
||||
/// 并把当前架构上下文注入到查询对象。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void SendQuery_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context()
|
||||
{
|
||||
LegacyBridgePipelineTracker.Reset();
|
||||
var testQuery = new LegacyArchitectureBridgeQuery();
|
||||
var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
|
||||
|
||||
try
|
||||
{
|
||||
var result = bridgeContext.SendQuery(testQuery);
|
||||
|
||||
Assert.That(result, Is.EqualTo(24));
|
||||
Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
|
||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
bridgeContainer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试SendQuery方法在查询为null时应抛出ArgumentNullException
|
||||
/// </summary>
|
||||
@ -146,6 +189,31 @@ public class ArchitectureContextTests
|
||||
Assert.That(testCommand.Executed, Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 legacy 命令通过 <see cref="ArchitectureContext" /> 发送时会进入统一 CQRS pipeline,
|
||||
/// 并把当前架构上下文注入到命令对象。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void SendCommand_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context()
|
||||
{
|
||||
LegacyBridgePipelineTracker.Reset();
|
||||
var testCommand = new LegacyArchitectureBridgeCommand();
|
||||
var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
|
||||
|
||||
try
|
||||
{
|
||||
bridgeContext.SendCommand(testCommand);
|
||||
|
||||
Assert.That(testCommand.Executed, Is.True);
|
||||
Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
|
||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
bridgeContainer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试SendCommand方法在命令为null时应抛出ArgumentNullException
|
||||
/// </summary>
|
||||
@ -168,6 +236,87 @@ public class ArchitectureContextTests
|
||||
Assert.That(result, Is.EqualTo(123));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 legacy 带返回值命令通过 <see cref="ArchitectureContext" /> 发送时会进入统一 CQRS pipeline,
|
||||
/// 并保持原始返回值语义。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void SendCommand_WithResult_Should_Bridge_Through_CqrsRuntime()
|
||||
{
|
||||
LegacyBridgePipelineTracker.Reset();
|
||||
var testCommand = new LegacyArchitectureBridgeCommandWithResult();
|
||||
var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
|
||||
|
||||
try
|
||||
{
|
||||
var result = bridgeContext.SendCommand(testCommand);
|
||||
|
||||
Assert.That(result, Is.EqualTo(42));
|
||||
Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
|
||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
bridgeContainer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 legacy 异步查询通过 <see cref="ArchitectureContext" /> 发送时也会进入统一 CQRS pipeline。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task SendQueryAsync_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context()
|
||||
{
|
||||
LegacyBridgePipelineTracker.Reset();
|
||||
var testQuery = new LegacyArchitectureBridgeAsyncQuery();
|
||||
var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await bridgeContext.SendQueryAsync(testQuery).ConfigureAwait(false);
|
||||
|
||||
Assert.That(result, Is.EqualTo(64));
|
||||
Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
|
||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
bridgeContainer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为需要验证统一 CQRS pipeline 的用例创建一个已冻结的最小 bridge 上下文。
|
||||
/// </summary>
|
||||
/// <param name="container">返回承载当前 bridge 上下文的冻结容器,供测试在 finally 中显式释放。</param>
|
||||
/// <returns>能够执行 legacy bridge request 且会 materialize open-generic pipeline behavior 的上下文。</returns>
|
||||
private static ArchitectureContext CreateFrozenBridgeContext(out MicrosoftDiContainer container)
|
||||
{
|
||||
container = new MicrosoftDiContainer();
|
||||
RegisterLegacyBridgeHandlers(container);
|
||||
new CqrsRuntimeModule().Register(container);
|
||||
container.ExecuteServicesHook(services =>
|
||||
services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LegacyBridgeTrackingPipelineBehavior<,>)));
|
||||
container.Freeze();
|
||||
return new ArchitectureContext(container);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把 GFramework.Core 内部的 legacy bridge handler 实例预先注册成可见的实例绑定。
|
||||
/// </summary>
|
||||
/// <param name="container">目标测试容器。</param>
|
||||
private static void RegisterLegacyBridgeHandlers(MicrosoftDiContainer container)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
container.RegisterPlurality(new LegacyCommandDispatchRequestHandler());
|
||||
container.RegisterPlurality(new LegacyCommandResultDispatchRequestHandler());
|
||||
container.RegisterPlurality(new LegacyAsyncCommandDispatchRequestHandler());
|
||||
container.RegisterPlurality(new LegacyAsyncCommandResultDispatchRequestHandler());
|
||||
container.RegisterPlurality(new LegacyQueryDispatchRequestHandler());
|
||||
container.RegisterPlurality(new LegacyAsyncQueryDispatchRequestHandler());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试SendCommand方法(带返回值)在命令为null时应抛出ArgumentNullException
|
||||
/// </summary>
|
||||
|
||||
@ -6,11 +6,13 @@ using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Enums;
|
||||
using GFramework.Core.Abstractions.Lifecycle;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Abstractions.Utility;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Core.Rule;
|
||||
|
||||
namespace GFramework.Core.Tests.Architectures;
|
||||
|
||||
@ -181,6 +183,80 @@ public class ArchitectureLifecycleBehaviorTests
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证架构销毁后会解除全局 GameContext 绑定。
|
||||
/// 该回归测试用于防止已销毁架构继续充当默认上下文回退入口。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task DestroyAsync_Should_Unbind_Context_From_GameContext()
|
||||
{
|
||||
var architecture = new PhaseTrackingArchitecture();
|
||||
|
||||
await architecture.InitializeAsync();
|
||||
|
||||
Assert.That(GameContext.GetByType(architecture.GetType()), Is.SameAs(architecture.Context));
|
||||
|
||||
await architecture.DestroyAsync();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => GameContext.GetByType(architecture.GetType()));
|
||||
Assert.Throws<InvalidOperationException>(() => GameContext.GetFirstArchitectureContext());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证失败初始化后的销毁同样会解除全局上下文绑定。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task DestroyAsync_After_FailedInitialization_Should_Unbind_Context_From_GameContext()
|
||||
{
|
||||
var destroyOrder = new List<string>();
|
||||
var architecture = new FailingInitializationArchitecture(destroyOrder);
|
||||
|
||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(() => architecture.InitializeAsync());
|
||||
Assert.That(exception, Is.Not.Null);
|
||||
Assert.That(GameContext.GetByType(architecture.GetType()), Is.SameAs(architecture.Context));
|
||||
|
||||
await architecture.DestroyAsync();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => GameContext.GetByType(architecture.GetType()));
|
||||
Assert.Throws<InvalidOperationException>(() => GameContext.GetFirstArchitectureContext());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证销毁后的新 ContextAware 实例不会再通过全局回退命中过期上下文。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task DestroyAsync_Should_Prevent_New_ContextAware_Fallback_From_Using_Destroyed_Context()
|
||||
{
|
||||
var architecture = new PhaseTrackingArchitecture();
|
||||
|
||||
await architecture.InitializeAsync();
|
||||
await architecture.DestroyAsync();
|
||||
|
||||
IContextAware probe = new LifecycleContextAwareProbe();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => probe.GetContext());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证同步兼容销毁入口同样会解除全局 GameContext 绑定。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Destroy_Should_Unbind_Context_From_GameContext()
|
||||
{
|
||||
var architecture = new PhaseTrackingArchitecture();
|
||||
|
||||
await architecture.InitializeAsync();
|
||||
|
||||
Assert.That(GameContext.GetByType(architecture.GetType()), Is.SameAs(architecture.Context));
|
||||
|
||||
#pragma warning disable CS0618
|
||||
architecture.Destroy();
|
||||
#pragma warning restore CS0618
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => GameContext.GetByType(architecture.GetType()));
|
||||
Assert.Throws<InvalidOperationException>(() => GameContext.GetFirstArchitectureContext());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证启用 AllowLateRegistration 时,生命周期层会立即初始化后注册的组件,而不是继续沿用初始化期的拒绝策略。
|
||||
/// 由于公共架构 API 在 Ready 之后会先触发容器限制,此回归测试直接覆盖生命周期协作者的对齐逻辑。
|
||||
@ -232,6 +308,13 @@ public class ArchitectureLifecycleBehaviorTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅用于验证销毁后全局上下文回退是否仍然泄漏的最小 ContextAware 探针。
|
||||
/// </summary>
|
||||
private sealed class LifecycleContextAwareProbe : ContextAwareBase
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在初始化时注册可销毁组件的测试架构。
|
||||
/// </summary>
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Abstractions.Utility;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Cqrs;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using GFramework.Cqrs.Notification;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace GFramework.Core.Tests.Architectures;
|
||||
|
||||
@ -13,6 +18,7 @@ namespace GFramework.Core.Tests.Architectures;
|
||||
/// 验证 Architecture 通过 <c>ArchitectureModules</c> 暴露出的模块安装与 CQRS 行为注册能力。
|
||||
/// 这些测试覆盖模块安装回调和请求管道行为接入,确保模块管理器仍然保持可观察行为不变。
|
||||
/// </summary>
|
||||
[NonParallelizable]
|
||||
[TestFixture]
|
||||
public class ArchitectureModulesBehaviorTests
|
||||
{
|
||||
@ -24,7 +30,9 @@ public class ArchitectureModulesBehaviorTests
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
|
||||
GameContext.Clear();
|
||||
AdditionalAssemblyNotificationHandlerState.Reset();
|
||||
TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount = 0;
|
||||
TrackingStreamPipelineBehavior<ModuleStreamBehaviorRequest, int>.InvocationCount = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -33,8 +41,11 @@ public class ArchitectureModulesBehaviorTests
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
AdditionalAssemblyNotificationHandlerState.Reset();
|
||||
GameContext.Clear();
|
||||
TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount = 0;
|
||||
TrackingStreamPipelineBehavior<ModuleStreamBehaviorRequest, int>.InvocationCount = 0;
|
||||
LegacyBridgePipelineTracker.Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -47,15 +58,19 @@ public class ArchitectureModulesBehaviorTests
|
||||
var architecture = new ModuleTestArchitecture(target => target.InstallModule(module));
|
||||
|
||||
await architecture.InitializeAsync();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
try
|
||||
{
|
||||
Assert.That(module.InstalledArchitecture, Is.SameAs(architecture));
|
||||
Assert.That(module.InstallCallCount, Is.EqualTo(1));
|
||||
Assert.That(architecture.Context.GetUtility<InstalledByModuleUtility>(), Is.Not.Null);
|
||||
});
|
||||
|
||||
await architecture.DestroyAsync();
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(module.InstalledArchitecture, Is.SameAs(architecture));
|
||||
Assert.That(module.InstallCallCount, Is.EqualTo(1));
|
||||
Assert.That(architecture.Context.GetUtility<InstalledByModuleUtility>(), Is.Not.Null);
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
await architecture.DestroyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -68,16 +83,111 @@ public class ArchitectureModulesBehaviorTests
|
||||
target.RegisterCqrsPipelineBehavior<TrackingPipelineBehavior<ModuleBehaviorRequest, string>>());
|
||||
|
||||
await architecture.InitializeAsync();
|
||||
|
||||
var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest());
|
||||
|
||||
Assert.Multiple(() =>
|
||||
try
|
||||
{
|
||||
Assert.That(response, Is.EqualTo("handled"));
|
||||
Assert.That(TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount, Is.EqualTo(1));
|
||||
});
|
||||
var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest());
|
||||
|
||||
await architecture.DestroyAsync();
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(response, Is.EqualTo("handled"));
|
||||
Assert.That(TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount, Is.EqualTo(1));
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
await architecture.DestroyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证注册的 CQRS stream 行为会参与建流处理流程。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task RegisterCqrsStreamPipelineBehavior_Should_Apply_Pipeline_Behavior_To_Stream_Request()
|
||||
{
|
||||
var architecture = new ModuleTestArchitecture(target =>
|
||||
target.RegisterCqrsStreamPipelineBehavior<TrackingStreamPipelineBehavior<ModuleStreamBehaviorRequest, int>>());
|
||||
|
||||
await architecture.InitializeAsync();
|
||||
try
|
||||
{
|
||||
var response = await DrainAsync(architecture.Context.CreateStream(new ModuleStreamBehaviorRequest()));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(response, Is.EqualTo([7]));
|
||||
Assert.That(
|
||||
TrackingStreamPipelineBehavior<ModuleStreamBehaviorRequest, int>.InvocationCount,
|
||||
Is.EqualTo(1));
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
await architecture.DestroyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证默认架构初始化路径会自动扫描 Core 程序集里的 legacy bridge handler,
|
||||
/// 使旧 <c>SendCommand</c> / <c>SendQuery</c> 入口也能进入统一 CQRS pipeline。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task InitializeAsync_Should_AutoRegister_LegacyBridgeHandlers_For_Default_Core_Assemblies()
|
||||
{
|
||||
LegacyBridgePipelineTracker.Reset();
|
||||
var architecture = new LegacyBridgeArchitecture();
|
||||
|
||||
await architecture.InitializeAsync();
|
||||
try
|
||||
{
|
||||
var query = new LegacyArchitectureBridgeQuery();
|
||||
var command = new LegacyArchitectureBridgeCommand();
|
||||
|
||||
var queryResult = architecture.Context.SendQuery(query);
|
||||
architecture.Context.SendCommand(command);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(queryResult, Is.EqualTo(24));
|
||||
Assert.That(query.ObservedContext, Is.SameAs(architecture.Context));
|
||||
Assert.That(command.Executed, Is.True);
|
||||
Assert.That(command.ObservedContext, Is.SameAs(architecture.Context));
|
||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(2));
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
await architecture.DestroyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证标准架构启动路径会复用通过 <see cref="Architecture.Configurator" /> 声明的自定义 notification publisher,
|
||||
/// 而不是在 <see cref="GFramework.Core.Services.Modules.CqrsRuntimeModule" /> 创建 runtime 时提前固化默认顺序策略。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task InitializeAsync_Should_Reuse_Custom_NotificationPublisher_From_Configurator()
|
||||
{
|
||||
var generatedAssembly = CreateGeneratedHandlerAssembly();
|
||||
var architecture = new ConfiguredNotificationPublisherArchitecture(generatedAssembly.Object);
|
||||
|
||||
await architecture.InitializeAsync();
|
||||
try
|
||||
{
|
||||
var probe = architecture.Context.GetService<ArchitectureNotificationPublisherProbe>();
|
||||
|
||||
await architecture.Context.PublishAsync(new AdditionalAssemblyNotification());
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(probe.WasCalled, Is.True);
|
||||
Assert.That(AdditionalAssemblyNotificationHandlerState.InvocationCount, Is.EqualTo(1));
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
await architecture.DestroyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -94,6 +204,52 @@ public class ArchitectureModulesBehaviorTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过公开初始化入口注册测试 pipeline behavior 的最小架构,
|
||||
/// 用于验证默认 Core 程序集扫描是否会自动接入 legacy bridge handler。
|
||||
/// </summary>
|
||||
private sealed class LegacyBridgeArchitecture : Architecture
|
||||
{
|
||||
/// <summary>
|
||||
/// 在容器钩子阶段注册 open-generic pipeline behavior,
|
||||
/// 以便 bridge request 走真实的架构初始化与 handler 自动扫描链路。
|
||||
/// </summary>
|
||||
public override Action<IServiceCollection>? Configurator => services =>
|
||||
services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LegacyBridgeTrackingPipelineBehavior<,>));
|
||||
|
||||
/// <summary>
|
||||
/// 保持空初始化,让测试只聚焦默认 CQRS 接线与 legacy bridge handler 自动发现。
|
||||
/// </summary>
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过标准架构启动路径声明自定义 notification publisher 的最小架构。
|
||||
/// </summary>
|
||||
private sealed class ConfiguredNotificationPublisherArchitecture(Assembly generatedAssembly) : Architecture
|
||||
{
|
||||
/// <summary>
|
||||
/// 在服务钩子阶段注册 probe 与自定义 publisher,
|
||||
/// 以模拟真实项目在组合根里通过 <see cref="IServiceCollection" /> 覆盖默认策略的路径。
|
||||
/// </summary>
|
||||
public override Action<IServiceCollection>? Configurator => services =>
|
||||
{
|
||||
services.AddSingleton<ArchitectureNotificationPublisherProbe>();
|
||||
services.AddSingleton<INotificationPublisher, ArchitectureTrackingNotificationPublisher>();
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 在用户初始化阶段显式接入额外程序集里的 notification handler,
|
||||
/// 让测试聚焦“publisher 是否被复用”,而不是依赖当前测试文件自己的 handler 扫描形状。
|
||||
/// </summary>
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterCqrsHandlersFromAssembly(generatedAssembly);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录模块安装调用情况的测试模块。
|
||||
/// </summary>
|
||||
@ -127,4 +283,85 @@ public class ArchitectureModulesBehaviorTests
|
||||
private sealed class InstalledByModuleUtility : IUtility
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个仅暴露程序集级 CQRS registry 元数据的 mocked Assembly。
|
||||
/// 该测试替身模拟扩展程序集已经提供 notification handler registry,而架构只需在初始化时显式接入该程序集。
|
||||
/// </summary>
|
||||
/// <returns>包含程序集级 notification handler registry 元数据的 mocked Assembly。</returns>
|
||||
private static Mock<Assembly> CreateGeneratedHandlerAssembly()
|
||||
{
|
||||
var generatedAssembly = new Mock<Assembly>();
|
||||
generatedAssembly
|
||||
.SetupGet(static assembly => assembly.FullName)
|
||||
.Returns("GFramework.Core.Tests.Architectures.ExplicitAdditionalHandlers, Version=1.0.0.0");
|
||||
generatedAssembly
|
||||
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
|
||||
.Returns([new CqrsHandlerRegistryAttribute(typeof(AdditionalAssemblyNotificationHandlerRegistry))]);
|
||||
return generatedAssembly;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录自定义 notification publisher 是否真正参与了标准架构启动路径下的 publish 调用。
|
||||
/// </summary>
|
||||
private sealed class ArchitectureNotificationPublisherProbe
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取 probe 是否已被 publisher 标记为执行过。
|
||||
/// </summary>
|
||||
public bool WasCalled { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 记录当前 publish 调用已经命中了自定义 publisher。
|
||||
/// </summary>
|
||||
public void MarkCalled()
|
||||
{
|
||||
WasCalled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 依赖容器内 probe 的自定义 notification publisher。
|
||||
/// 该类型通过显式标记 + 正常转发处理器执行,验证标准架构启动路径不会把自定义策略短路成默认顺序发布器。
|
||||
/// </summary>
|
||||
private sealed class ArchitectureTrackingNotificationPublisher(
|
||||
ArchitectureNotificationPublisherProbe probe) : INotificationPublisher
|
||||
{
|
||||
/// <summary>
|
||||
/// 记录自定义 publisher 已参与当前发布调用,并继续按处理器解析顺序转发执行。
|
||||
/// </summary>
|
||||
public async ValueTask PublishAsync<TNotification>(
|
||||
NotificationPublishContext<TNotification> context,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TNotification : INotification
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
probe.MarkCalled();
|
||||
|
||||
foreach (var handler in context.Handlers)
|
||||
{
|
||||
await context.InvokeHandlerAsync(handler, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 物化异步流为只读列表,便于断言 stream pipeline 行为的最终可观察结果。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">流元素类型。</typeparam>
|
||||
/// <param name="stream">要物化的异步流。</param>
|
||||
/// <returns>按枚举顺序收集的元素列表。</returns>
|
||||
private static async Task<IReadOnlyList<T>> DrainAsync<T>(IAsyncEnumerable<T> stream)
|
||||
{
|
||||
var results = new List<T>();
|
||||
|
||||
await foreach (var item in stream.ConfigureAwait(false))
|
||||
{
|
||||
results.Add(item);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ namespace GFramework.Core.Tests.Architectures;
|
||||
/// <summary>
|
||||
/// ContextProvider 相关类的单元测试
|
||||
/// 测试内容包括:
|
||||
/// - GameContextProvider 获取第一个架构上下文
|
||||
/// - GameContextProvider 获取当前活动架构上下文
|
||||
/// - GameContextProvider 尝试获取指定类型的上下文
|
||||
/// - ScopedContextProvider 获取绑定的上下文
|
||||
/// - ScopedContextProvider 尝试获取指定类型的上下文
|
||||
@ -37,10 +37,10 @@ public class ContextProviderTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 GameContextProvider 是否能正确获取第一个架构上下文
|
||||
/// 测试 GameContextProvider 是否能正确获取当前活动架构上下文
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void GameContextProvider_GetContext_Should_Return_First_Context()
|
||||
public void GameContextProvider_GetContext_Should_Return_Current_Context()
|
||||
{
|
||||
var context = new TestArchitectureContext();
|
||||
GameContext.Bind(typeof(TestArchitecture), context);
|
||||
@ -63,13 +63,13 @@ public class ContextProviderTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 GameContextProvider 的 TryGetContext 方法在找到上下文时返回 true
|
||||
/// 测试 GameContextProvider 的 TryGetContext 方法在仅绑定架构类型时也能返回 true
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void GameContextProvider_TryGetContext_Should_Return_True_When_Found()
|
||||
public void GameContextProvider_TryGetContext_Should_Return_True_When_Current_Context_Matches()
|
||||
{
|
||||
var context = new TestArchitectureContext();
|
||||
GameContext.Bind(typeof(TestArchitectureContext), context);
|
||||
GameContext.Bind(typeof(TestArchitecture), context);
|
||||
|
||||
var provider = new GameContextProvider();
|
||||
var result = provider.TryGetContext<TestArchitectureContext>(out var foundContext);
|
||||
|
||||
@ -6,20 +6,12 @@ using GFramework.Core.Architectures;
|
||||
namespace GFramework.Core.Tests.Architectures;
|
||||
|
||||
/// <summary>
|
||||
/// GameContext类的单元测试
|
||||
/// GameContext 类的单元测试
|
||||
/// 测试内容包括:
|
||||
/// - ArchitectureReadOnlyDictionary在启动时为空
|
||||
/// - Bind方法添加上下文到字典
|
||||
/// - Bind重复类型时抛出异常
|
||||
/// - GetByType返回正确的上下文
|
||||
/// - GetByType未找到时抛出异常
|
||||
/// - Get泛型方法返回正确的上下文
|
||||
/// - TryGet方法在找到时返回true
|
||||
/// - TryGet方法在未找到时返回false
|
||||
/// - GetFirstArchitectureContext在存在时返回
|
||||
/// - GetFirstArchitectureContext为空时抛出异常
|
||||
/// - Unbind移除上下文
|
||||
/// - Clear移除所有上下文
|
||||
/// - 初始状态为空
|
||||
/// - 绑定后可通过架构类型和上下文类型回查
|
||||
/// - 不允许并存绑定两个不同上下文实例
|
||||
/// - 清理和解绑会同步更新当前活动上下文
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class GameContextTests
|
||||
@ -81,6 +73,21 @@ public class GameContextTests
|
||||
GameContext.Bind(typeof(TestArchitecture), context2));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试绑定第二个不同的上下文实例时会被拒绝。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Bind_WithDifferentContextInstance_Should_ThrowInvalidOperationException()
|
||||
{
|
||||
var firstContext = new TestArchitectureContext();
|
||||
var secondContext = new TestArchitectureContext();
|
||||
|
||||
GameContext.Bind(typeof(TestArchitecture), firstContext);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
GameContext.Bind(typeof(AnotherTestArchitectureContext), secondContext));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试GetByType方法是否返回正确的上下文
|
||||
/// </summary>
|
||||
@ -106,13 +113,27 @@ public class GameContextTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试Get泛型方法是否返回正确的上下文
|
||||
/// 测试 GetByType 支持按当前活动上下文的具体类型回查。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void GetGeneric_Should_Return_Correct_Context()
|
||||
public void GetByType_Should_Return_Current_Context_When_Requested_By_Context_Type()
|
||||
{
|
||||
var context = new TestArchitectureContext();
|
||||
GameContext.Bind(typeof(TestArchitectureContext), context);
|
||||
GameContext.Bind(typeof(TestArchitecture), context);
|
||||
|
||||
var result = GameContext.GetByType(typeof(TestArchitectureContext));
|
||||
|
||||
Assert.That(result, Is.SameAs(context));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 Get 泛型方法在仅绑定架构类型时也能返回当前上下文
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void GetGeneric_Should_Return_Current_Context_When_Bound_By_Architecture_Type()
|
||||
{
|
||||
var context = new TestArchitectureContext();
|
||||
GameContext.Bind(typeof(TestArchitecture), context);
|
||||
|
||||
var result = GameContext.Get<TestArchitectureContext>();
|
||||
|
||||
@ -120,13 +141,13 @@ public class GameContextTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试TryGet方法在找到上下文时是否返回true并正确设置输出参数
|
||||
/// 测试 TryGet 方法在仅绑定架构类型时也能找到当前上下文
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TryGet_Should_ReturnTrue_When_Found()
|
||||
public void TryGet_Should_ReturnTrue_When_Bound_By_Architecture_Type()
|
||||
{
|
||||
var context = new TestArchitectureContext();
|
||||
GameContext.Bind(typeof(TestArchitectureContext), context);
|
||||
GameContext.Bind(typeof(TestArchitecture), context);
|
||||
|
||||
var result = GameContext.TryGet(out TestArchitectureContext? foundContext);
|
||||
|
||||
@ -135,7 +156,7 @@ public class GameContextTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试TryGet方法在未找到上下文时是否返回false且输出参数为null
|
||||
/// 测试 TryGet 方法在未找到上下文时是否返回 false 且输出参数为 null
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TryGet_Should_ReturnFalse_When_Not_Found()
|
||||
@ -171,10 +192,10 @@ public class GameContextTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试Unbind方法是否正确移除指定类型的上下文
|
||||
/// 测试 Unbind 方法在移除最后一个别名时会清空当前活动上下文
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Unbind_Should_Remove_Context()
|
||||
public void Unbind_Should_Remove_Context_When_Last_Alias_Is_Removed()
|
||||
{
|
||||
var context = new TestArchitectureContext();
|
||||
GameContext.Bind(typeof(TestArchitecture), context);
|
||||
@ -185,16 +206,34 @@ public class GameContextTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试Clear方法是否正确移除所有上下文
|
||||
/// 测试 Unbind 方法在仍有其他别名时保留当前活动上下文
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Unbind_Should_Keep_Current_Context_When_Another_Alias_Remains()
|
||||
{
|
||||
var context = new TestArchitectureContext();
|
||||
GameContext.Bind(typeof(TestArchitecture), context);
|
||||
GameContext.Bind(typeof(TestArchitectureContext), context);
|
||||
|
||||
GameContext.Unbind(typeof(TestArchitecture));
|
||||
|
||||
Assert.That(GameContext.GetFirstArchitectureContext(), Is.SameAs(context));
|
||||
Assert.That(GameContext.ArchitectureReadOnlyDictionary.Count, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 Clear 方法是否正确移除所有上下文
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Clear_Should_Remove_All_Contexts()
|
||||
{
|
||||
GameContext.Bind(typeof(TestArchitecture), new TestArchitectureContext());
|
||||
GameContext.Bind(typeof(TestArchitectureContext), new TestArchitectureContext());
|
||||
var context = new TestArchitectureContext();
|
||||
GameContext.Bind(typeof(TestArchitecture), context);
|
||||
GameContext.Bind(typeof(TestArchitectureContext), context);
|
||||
|
||||
GameContext.Clear();
|
||||
|
||||
Assert.That(GameContext.ArchitectureReadOnlyDictionary.Count, Is.EqualTo(0));
|
||||
Assert.Throws<InvalidOperationException>(() => GameContext.GetFirstArchitectureContext());
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Query;
|
||||
using GFramework.Core.Rule;
|
||||
|
||||
namespace GFramework.Core.Tests.Architectures;
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证 legacy 异步查询桥接时也会显式注入当前架构上下文。
|
||||
/// </summary>
|
||||
public sealed class LegacyArchitectureBridgeAsyncQuery : ContextAwareBase, IAsyncQuery<int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取执行期间观察到的上下文实例。
|
||||
/// </summary>
|
||||
public IArchitectureContext? ObservedContext { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 执行异步查询并返回测试结果。
|
||||
/// </summary>
|
||||
public Task<int> DoAsync()
|
||||
{
|
||||
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
|
||||
return Task.FromResult(64);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Command;
|
||||
using GFramework.Core.Rule;
|
||||
|
||||
namespace GFramework.Core.Tests.Architectures;
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证 legacy 命令桥接时会把当前 <see cref="IArchitectureContext" /> 注入到命令对象。
|
||||
/// </summary>
|
||||
public sealed class LegacyArchitectureBridgeCommand : ContextAwareBase, ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取执行期间观察到的上下文实例。
|
||||
/// </summary>
|
||||
public IArchitectureContext? ObservedContext { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前命令是否已经执行。
|
||||
/// </summary>
|
||||
public bool Executed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 执行命令并记录 bridge handler 注入的上下文。
|
||||
/// </summary>
|
||||
public void Execute()
|
||||
{
|
||||
Executed = true;
|
||||
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Command;
|
||||
using GFramework.Core.Rule;
|
||||
|
||||
namespace GFramework.Core.Tests.Architectures;
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证 legacy 带返回值命令桥接时会沿用统一 runtime。
|
||||
/// </summary>
|
||||
public sealed class LegacyArchitectureBridgeCommandWithResult : ContextAwareBase, ICommand<int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取执行期间观察到的上下文实例。
|
||||
/// </summary>
|
||||
public IArchitectureContext? ObservedContext { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 执行命令并返回测试结果。
|
||||
/// </summary>
|
||||
public int Execute()
|
||||
{
|
||||
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
|
||||
return 42;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Query;
|
||||
using GFramework.Core.Rule;
|
||||
|
||||
namespace GFramework.Core.Tests.Architectures;
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证 legacy 查询桥接时会把当前 <see cref="IArchitectureContext" /> 注入到查询对象。
|
||||
/// </summary>
|
||||
public sealed class LegacyArchitectureBridgeQuery : ContextAwareBase, IQuery<int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取执行期间观察到的上下文实例。
|
||||
/// </summary>
|
||||
public IArchitectureContext? ObservedContext { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 执行查询并返回测试结果。
|
||||
/// </summary>
|
||||
public int Do()
|
||||
{
|
||||
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
|
||||
return 24;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Threading;
|
||||
using GFramework.Core.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Tests.Architectures;
|
||||
|
||||
/// <summary>
|
||||
/// 为 legacy bridge pipeline 回归测试保存跨泛型闭包共享的计数状态。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该计数器通过 <see cref="Interlocked.Increment(ref int)" /> 原子递增,并使用
|
||||
/// <see cref="Volatile" /> 读写,因此单次读写操作本身是线程安全的。
|
||||
/// 由于状态在同一进程内跨 fixture 共享,所有使用它的测试都必须在清理阶段调用 <see cref="Reset" />,
|
||||
/// 以避免并行或失败测试把旧计数泄露给后续断言。
|
||||
/// </remarks>
|
||||
public static class LegacyBridgePipelineTracker
|
||||
{
|
||||
private static int _invocationCount;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前进程内被识别为 legacy bridge request 的 pipeline 命中次数。
|
||||
/// </summary>
|
||||
public static int InvocationCount => Volatile.Read(ref _invocationCount);
|
||||
|
||||
/// <summary>
|
||||
/// 重置计数器。
|
||||
/// </summary>
|
||||
public static void Reset()
|
||||
{
|
||||
Volatile.Write(ref _invocationCount, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 若当前请求类型属于 Core legacy bridge request,则记录一次命中。
|
||||
/// </summary>
|
||||
public static void Record(Type requestType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(requestType);
|
||||
|
||||
if (typeof(LegacyCqrsDispatchRequestBase).IsAssignableFrom(requestType))
|
||||
{
|
||||
Interlocked.Increment(ref _invocationCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Threading;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Tests.Architectures;
|
||||
|
||||
/// <summary>
|
||||
/// 记录 legacy Core CQRS bridge request 是否经过统一 CQRS pipeline 的测试行为。
|
||||
/// </summary>
|
||||
public sealed class LegacyBridgeTrackingPipelineBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : IRequest<TResponse>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<TResponse> Handle(
|
||||
TRequest message,
|
||||
MessageHandlerDelegate<TRequest, TResponse> next,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
LegacyBridgePipelineTracker.Record(typeof(TRequest));
|
||||
return await next(message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Tests.Architectures;
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证架构公开 stream pipeline 行为注册入口的最小流式请求。
|
||||
/// </summary>
|
||||
public sealed class ModuleStreamBehaviorRequest : IStreamRequest<int>
|
||||
{
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Tests.Architectures;
|
||||
|
||||
/// <summary>
|
||||
/// 处理 <see cref="ModuleStreamBehaviorRequest" /> 并返回一个固定元素。
|
||||
/// </summary>
|
||||
public sealed class ModuleStreamBehaviorRequestHandler : IStreamRequestHandler<ModuleStreamBehaviorRequest, int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回一个固定元素,供架构 stream pipeline 行为回归断言使用。
|
||||
/// </summary>
|
||||
/// <param name="request">当前流式请求。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>包含一个固定元素的异步流。</returns>
|
||||
public async IAsyncEnumerable<int> Handle(
|
||||
ModuleStreamBehaviorRequest request,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
yield return 7;
|
||||
await ValueTask.CompletedTask.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@ -65,6 +65,16 @@ public class TestArchitectureWithRegistry : IArchitecture
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试替身未实现 CQRS 流式管道行为注册。
|
||||
/// </summary>
|
||||
/// <typeparam name="TBehavior">行为类型。</typeparam>
|
||||
/// <exception cref="NotSupportedException">该测试替身不参与 CQRS 流式管道配置验证。</exception>
|
||||
public void RegisterCqrsStreamPipelineBehavior<TBehavior>() where TBehavior : class
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
|
||||
/// </summary>
|
||||
|
||||
@ -63,6 +63,16 @@ public class TestArchitectureWithoutRegistry : IArchitecture
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试替身未实现 CQRS 流式管道行为注册。
|
||||
/// </summary>
|
||||
/// <typeparam name="TBehavior">行为类型。</typeparam>
|
||||
/// <exception cref="NotSupportedException">该测试替身不参与 CQRS 流式管道配置验证。</exception>
|
||||
public void RegisterCqrsStreamPipelineBehavior<TBehavior>() where TBehavior : class
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
|
||||
/// </summary>
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Threading;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Tests.Architectures;
|
||||
|
||||
/// <summary>
|
||||
/// 记录流式请求通过管道次数的测试行为。
|
||||
/// </summary>
|
||||
/// <typeparam name="TRequest">流式请求类型。</typeparam>
|
||||
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
|
||||
public sealed class TrackingStreamPipelineBehavior<TRequest, TResponse> : IStreamPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : IStreamRequest<TResponse>
|
||||
{
|
||||
private static int _invocationCount;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前测试进程中该流式请求类型对应的行为触发次数。
|
||||
/// 该计数器是按泛型闭包共享的静态状态,测试需要在每次运行前显式重置。
|
||||
/// </summary>
|
||||
public static int InvocationCount
|
||||
{
|
||||
get => Volatile.Read(ref _invocationCount);
|
||||
set => Volatile.Write(ref _invocationCount, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以线程安全方式记录一次行为执行,然后继续执行下一个处理阶段。
|
||||
/// </summary>
|
||||
/// <param name="message">当前流式请求消息。</param>
|
||||
/// <param name="next">下一个处理阶段。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>下游处理阶段返回的异步流。</returns>
|
||||
public IAsyncEnumerable<TResponse> Handle(
|
||||
TRequest message,
|
||||
StreamMessageHandlerDelegate<TRequest, TResponse> next,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Interlocked.Increment(ref _invocationCount);
|
||||
return next(message, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Command;
|
||||
using GFramework.Core.Rule;
|
||||
using GFramework.Core.Tests.Architectures;
|
||||
|
||||
namespace GFramework.Core.Tests.Command;
|
||||
|
||||
@ -75,6 +77,95 @@ public class CommandExecutorTests
|
||||
Assert.Throws<ArgumentNullException>(() => _commandExecutor.Send<int>(null!));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 legacy 命令没有可用上下文时,会安全回退到本地直接执行路径。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Send_Should_Fall_Back_To_Legacy_Execution_When_Context_IsMissing()
|
||||
{
|
||||
var runtime = new RecordingCqrsRuntime();
|
||||
var executor = new CommandExecutor(runtime);
|
||||
var command = new MissingContextLegacyCommand();
|
||||
|
||||
executor.Send(command);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(command.Executed, Is.True);
|
||||
Assert.That(runtime.LastRequest, Is.Null);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证非“缺上下文”类型的 <see cref="InvalidOperationException" /> 不会被 bridge 回退逻辑误吞掉。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Send_Should_Propagate_InvalidOperationException_When_ContextAware_Target_Throws_Unexpected_Error()
|
||||
{
|
||||
var runtime = new RecordingCqrsRuntime();
|
||||
var executor = new CommandExecutor(runtime);
|
||||
var command = new ThrowingLegacyCommand();
|
||||
|
||||
Assert.That(
|
||||
() => executor.Send(command),
|
||||
Throws.InvalidOperationException.With.Message.EqualTo(ThrowingLegacyCommand.ExceptionMessage));
|
||||
Assert.That(runtime.LastRequest, Is.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 legacy 同步命令桥接会在线程池上等待 runtime,
|
||||
/// 避免直接继承调用方当前的同步上下文。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Send_Should_Bridge_Through_Runtime_Without_Reusing_Caller_SynchronizationContext()
|
||||
{
|
||||
var runtime = new RecordingCqrsRuntime();
|
||||
var executor = new CommandExecutor(runtime);
|
||||
var command = new ContextAwareLegacyCommand();
|
||||
var expectedContext = new TestArchitectureContextBaseStub();
|
||||
((GFramework.Core.Abstractions.Rule.IContextAware)command).SetContext(expectedContext);
|
||||
var originalContext = SynchronizationContext.Current;
|
||||
|
||||
try
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(new TestLegacySynchronizationContext());
|
||||
|
||||
executor.Send(command);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(runtime.LastRequest, Is.TypeOf<GFramework.Core.Cqrs.LegacyCommandDispatchRequest>());
|
||||
Assert.That(runtime.ObservedSynchronizationContextType, Is.Null);
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(originalContext);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 legacy 带返回值命令桥接也会保留上下文注入与返回值语义。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Send_WithResult_Should_Bridge_Through_Runtime_And_Preserve_Context()
|
||||
{
|
||||
var runtime = new RecordingCqrsRuntime(static _ => 123);
|
||||
var executor = new CommandExecutor(runtime);
|
||||
var command = new ContextAwareLegacyCommandWithResult(123);
|
||||
var expectedContext = new TestArchitectureContextBaseStub();
|
||||
((GFramework.Core.Abstractions.Rule.IContextAware)command).SetContext(expectedContext);
|
||||
|
||||
var result = executor.Send(command);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(result, Is.EqualTo(123));
|
||||
Assert.That(runtime.LastRequest, Is.TypeOf<GFramework.Core.Cqrs.LegacyCommandResultDispatchRequest>());
|
||||
Assert.That(command.ObservedContext, Is.SameAs(expectedContext));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试SendAsync方法执行异步命令
|
||||
/// </summary>
|
||||
@ -122,4 +213,65 @@ public class CommandExecutorTests
|
||||
{
|
||||
Assert.ThrowsAsync<ArgumentNullException>(() => _commandExecutor.SendAsync<int>(null!));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为同步 bridge 测试提供最小架构上下文替身。
|
||||
/// </summary>
|
||||
private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证缺少上下文时仍会走本地 fallback 的测试命令。
|
||||
/// </summary>
|
||||
private sealed class MissingContextLegacyCommand : GFramework.Core.Abstractions.Rule.IContextAware, GFramework.Core.Abstractions.Command.ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取命令是否已经执行。
|
||||
/// </summary>
|
||||
public bool Executed { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext()
|
||||
{
|
||||
throw new InvalidOperationException("Architecture context has not been set. Call SetContext before accessing the context.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Execute()
|
||||
{
|
||||
Executed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证 bridge 上下文解析不会吞掉意外运行时错误的测试命令。
|
||||
/// </summary>
|
||||
private sealed class ThrowingLegacyCommand : GFramework.Core.Abstractions.Rule.IContextAware, GFramework.Core.Abstractions.Command.ICommand
|
||||
{
|
||||
internal const string ExceptionMessage = "Unexpected context failure.";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext()
|
||||
{
|
||||
throw new InvalidOperationException(ExceptionMessage);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Execute()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
GFramework.Core.Tests/Command/ContextAwareLegacyCommand.cs
Normal file
31
GFramework.Core.Tests/Command/ContextAwareLegacyCommand.cs
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Command;
|
||||
using GFramework.Core.Rule;
|
||||
|
||||
namespace GFramework.Core.Tests.Command;
|
||||
|
||||
/// <summary>
|
||||
/// 为 <see cref="CommandExecutorTests" /> 提供可观察上下文注入的 legacy 命令。
|
||||
/// </summary>
|
||||
internal sealed class ContextAwareLegacyCommand : ContextAwareBase, ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取执行期间观察到的架构上下文。
|
||||
/// </summary>
|
||||
public IArchitectureContext? ObservedContext { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取命令是否已经执行。
|
||||
/// </summary>
|
||||
public bool Executed { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Execute()
|
||||
{
|
||||
Executed = true;
|
||||
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Command;
|
||||
using GFramework.Core.Rule;
|
||||
|
||||
namespace GFramework.Core.Tests.Command;
|
||||
|
||||
/// <summary>
|
||||
/// 为 <see cref="CommandExecutorTests" /> 提供可观察上下文注入的带返回值 legacy 命令。
|
||||
/// </summary>
|
||||
internal sealed class ContextAwareLegacyCommandWithResult(int result) : ContextAwareBase, ICommand<int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取执行期间观察到的架构上下文。
|
||||
/// </summary>
|
||||
public IArchitectureContext? ObservedContext { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Execute()
|
||||
{
|
||||
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
207
GFramework.Core.Tests/Command/RecordingCqrsRuntime.cs
Normal file
207
GFramework.Core.Tests/Command/RecordingCqrsRuntime.cs
Normal file
@ -0,0 +1,207 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
using GFramework.Core.Cqrs;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Tests.Command;
|
||||
|
||||
/// <summary>
|
||||
/// 记录 bridge 执行线程与收到请求的最小 CQRS runtime 测试替身。
|
||||
/// </summary>
|
||||
internal sealed class RecordingCqrsRuntime(Func<object?, object?>? responseFactory = null) : ICqrsRuntime
|
||||
{
|
||||
private static readonly Func<object?, object?> DefaultResponseFactory = _ => null;
|
||||
|
||||
private readonly Func<object?, object?> _responseFactory = responseFactory ?? DefaultResponseFactory;
|
||||
|
||||
/// <summary>
|
||||
/// 获取最近一次 <see cref="SendAsync{TResponse}" /> 观察到的同步上下文类型。
|
||||
/// </summary>
|
||||
public Type? ObservedSynchronizationContextType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最近一次收到的请求实例。
|
||||
/// </summary>
|
||||
public object? LastRequest { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<TResponse> SendAsync<TResponse>(
|
||||
ICqrsContext context,
|
||||
IRequest<TResponse> request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
ObservedSynchronizationContextType = SynchronizationContext.Current?.GetType();
|
||||
LastRequest = request;
|
||||
|
||||
object? response = request switch
|
||||
{
|
||||
LegacyCommandDispatchRequest legacyCommandDispatchRequest => ExecuteLegacyCommand(context, legacyCommandDispatchRequest),
|
||||
LegacyCommandResultDispatchRequest legacyCommandResultDispatchRequest => ExecuteContextAwareRequest(
|
||||
context,
|
||||
legacyCommandResultDispatchRequest.Target,
|
||||
legacyCommandResultDispatchRequest.Execute),
|
||||
LegacyQueryDispatchRequest legacyQueryDispatchRequest => ExecuteContextAwareRequest(
|
||||
context,
|
||||
legacyQueryDispatchRequest.Target,
|
||||
legacyQueryDispatchRequest.Execute),
|
||||
LegacyAsyncCommandDispatchRequest legacyAsyncCommandDispatchRequest => await ExecuteLegacyAsyncCommandAsync(
|
||||
context,
|
||||
legacyAsyncCommandDispatchRequest,
|
||||
cancellationToken).ConfigureAwait(false),
|
||||
LegacyAsyncCommandResultDispatchRequest legacyAsyncCommandResultDispatchRequest => await ExecuteContextAwareRequestAsync(
|
||||
context,
|
||||
legacyAsyncCommandResultDispatchRequest.Target,
|
||||
legacyAsyncCommandResultDispatchRequest.ExecuteAsync,
|
||||
cancellationToken).ConfigureAwait(false),
|
||||
LegacyAsyncQueryDispatchRequest legacyAsyncQueryDispatchRequest => await ExecuteContextAwareRequestAsync(
|
||||
context,
|
||||
legacyAsyncQueryDispatchRequest.Target,
|
||||
legacyAsyncQueryDispatchRequest.ExecuteAsync,
|
||||
cancellationToken).ConfigureAwait(false),
|
||||
IRequest<Unit> => Unit.Value,
|
||||
_ => _responseFactory(request)
|
||||
};
|
||||
|
||||
return ConvertResponse<TResponse>(request, response);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask PublishAsync<TNotification>(
|
||||
ICqrsContext context,
|
||||
TNotification notification,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TNotification : INotification
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(
|
||||
ICqrsContext context,
|
||||
IStreamRequest<TResponse> request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将测试替身工厂返回的装箱结果显式还原为目标类型,并在类型不匹配时给出可诊断异常。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前请求声明的响应类型。</typeparam>
|
||||
/// <param name="request">触发响应工厂的请求实例。</param>
|
||||
/// <param name="response">响应工厂返回的装箱结果。</param>
|
||||
/// <returns>还原后的目标类型响应。</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 响应工厂返回 <see langword="null" /> 或错误类型,导致无法还原为 <typeparamref name="TResponse" />。
|
||||
/// </exception>
|
||||
private static TResponse ConvertResponse<TResponse>(IRequest<TResponse> request, object? response)
|
||||
{
|
||||
if (response is TResponse typedResponse)
|
||||
{
|
||||
return typedResponse;
|
||||
}
|
||||
|
||||
if (response is null && !typeof(TResponse).IsValueType)
|
||||
{
|
||||
return (TResponse)response!;
|
||||
}
|
||||
|
||||
string actualType = response?.GetType().FullName ?? "null";
|
||||
throw new InvalidOperationException(
|
||||
$"RecordingCqrsRuntime 无法将响应类型从 '{actualType}' 转换为 '{typeof(TResponse).FullName}'。"
|
||||
+ $" 请求类型:'{request.GetType().FullName}'。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按 bridge handler 语义为 legacy 无返回值命令注入上下文并执行。
|
||||
/// </summary>
|
||||
/// <param name="context">当前运行时接收到的架构上下文。</param>
|
||||
/// <param name="request">待执行的 legacy 命令桥接请求。</param>
|
||||
/// <returns>桥接后的 <see cref="Unit" /> 响应。</returns>
|
||||
private static Unit ExecuteLegacyCommand(
|
||||
ICqrsContext context,
|
||||
LegacyCommandDispatchRequest request)
|
||||
{
|
||||
PrepareTarget(context, request.Command);
|
||||
request.Command.Execute();
|
||||
return Unit.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按 bridge handler 语义为 legacy 异步无返回值命令注入上下文并执行。
|
||||
/// </summary>
|
||||
/// <param name="context">当前运行时接收到的架构上下文。</param>
|
||||
/// <param name="request">待执行的 legacy 异步命令桥接请求。</param>
|
||||
/// <param name="cancellationToken">调用方传入的取消令牌。</param>
|
||||
/// <returns>表示 bridge 执行完成的异步结果。</returns>
|
||||
private static async Task<Unit> ExecuteLegacyAsyncCommandAsync(
|
||||
ICqrsContext context,
|
||||
LegacyAsyncCommandDispatchRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
PrepareTarget(context, request.Command);
|
||||
await request.Command.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
return Unit.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按 bridge handler 语义为带返回值 legacy 请求注入上下文并执行同步委托。
|
||||
/// </summary>
|
||||
/// <param name="context">当前运行时接收到的架构上下文。</param>
|
||||
/// <param name="target">需要接收上下文注入的 legacy 目标对象。</param>
|
||||
/// <param name="execute">实际执行 legacy 目标逻辑的同步委托。</param>
|
||||
/// <returns>同步执行结果。</returns>
|
||||
private static object? ExecuteContextAwareRequest(
|
||||
ICqrsContext context,
|
||||
object target,
|
||||
Func<object?> execute)
|
||||
{
|
||||
PrepareTarget(context, target);
|
||||
return execute();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按 bridge handler 语义为带返回值 legacy 请求注入上下文并执行异步委托。
|
||||
/// </summary>
|
||||
/// <param name="context">当前运行时接收到的架构上下文。</param>
|
||||
/// <param name="target">需要接收上下文注入的 legacy 目标对象。</param>
|
||||
/// <param name="executeAsync">实际执行 legacy 目标逻辑的异步委托。</param>
|
||||
/// <param name="cancellationToken">调用方传入的取消令牌。</param>
|
||||
/// <returns>异步执行结果。</returns>
|
||||
private static async Task<object?> ExecuteContextAwareRequestAsync(
|
||||
ICqrsContext context,
|
||||
object target,
|
||||
Func<Task<object?>> executeAsync,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
PrepareTarget(context, target);
|
||||
return await executeAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 legacy bridge handler 的上下文注入语义,使测试替身与生产桥接行为保持一致。
|
||||
/// </summary>
|
||||
/// <param name="context">当前运行时接收到的架构上下文。</param>
|
||||
/// <param name="target">即将执行的 legacy 目标对象。</param>
|
||||
private static void PrepareTarget(ICqrsContext context, object target)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
|
||||
if (context is not GFramework.Core.Abstractions.Architectures.IArchitectureContext architectureContext)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"RecordingCqrsRuntime 期望收到 IArchitectureContext,但实际为 '{context.GetType().FullName}'。");
|
||||
}
|
||||
|
||||
if (target is IContextAware contextAware)
|
||||
{
|
||||
contextAware.SetContext(architectureContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
namespace GFramework.Core.Tests.Command;
|
||||
|
||||
/// <summary>
|
||||
/// 为 legacy 同步 bridge 回归测试提供可识别的同步上下文占位类型。
|
||||
/// </summary>
|
||||
internal sealed class TestLegacySynchronizationContext : SynchronizationContext
|
||||
{
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Command;
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
using GFramework.Core.Cqrs;
|
||||
using GFramework.Core.Rule;
|
||||
using GFramework.Core.Tests.Architectures;
|
||||
|
||||
namespace GFramework.Core.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 验证 legacy 异步无返回值命令 bridge handler 的取消语义。
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class LegacyAsyncCommandDispatchRequestHandlerTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证当取消令牌在执行前已触发时,handler 不会启动底层 legacy 命令。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Handle_Should_Throw_Without_Executing_Command_When_Cancellation_Is_Already_Requested()
|
||||
{
|
||||
var handler = new LegacyAsyncCommandDispatchRequestHandler();
|
||||
var command = new ProbeAsyncCommand(Task.CompletedTask);
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
Assert.ThrowsAsync<OperationCanceledException>(
|
||||
async () => await handler.Handle(
|
||||
new LegacyAsyncCommandDispatchRequest(command),
|
||||
cancellationTokenSource.Token)
|
||||
.AsTask()
|
||||
.ConfigureAwait(false));
|
||||
Assert.That(command.ExecutionCount, Is.Zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当底层 legacy 命令正在运行时,handler 会通过 <c>WaitAsync</c> 及时向调用方暴露取消。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Handle_Should_Observe_Cancellation_While_Command_Is_Running()
|
||||
{
|
||||
var completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var handler = new LegacyAsyncCommandDispatchRequestHandler();
|
||||
var command = new ProbeAsyncCommand(completionSource.Task);
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
((IContextAware)handler).SetContext(new TestArchitectureContextBaseStub());
|
||||
|
||||
var handleTask = handler.Handle(
|
||||
new LegacyAsyncCommandDispatchRequest(command),
|
||||
cancellationTokenSource.Token)
|
||||
.AsTask();
|
||||
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
Assert.That(
|
||||
async () => await handleTask.ConfigureAwait(false),
|
||||
Throws.InstanceOf<OperationCanceledException>());
|
||||
Assert.That(command.ExecutionCount, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 handler 取消测试提供可控完成时机的异步命令替身。
|
||||
/// </summary>
|
||||
private sealed class ProbeAsyncCommand(Task executionTask) : ContextAwareBase, IAsyncCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取底层命令逻辑的触发次数。
|
||||
/// </summary>
|
||||
public int ExecutionCount { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ExecuteAsync()
|
||||
{
|
||||
ExecutionCount++;
|
||||
return executionTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 handler 取消测试提供最小架构上下文替身。
|
||||
/// </summary>
|
||||
private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -16,7 +16,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1"/>
|
||||
<PackageReference Include="Moq" Version="4.20.72"/>
|
||||
<PackageReference Include="NUnit" Version="4.5.1"/>
|
||||
<PackageReference Include="NUnit" Version="4.6.0"/>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@ -3,6 +3,9 @@
|
||||
|
||||
using GFramework.Core.Ioc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GFramework.Core.Tests.Ioc;
|
||||
|
||||
@ -22,6 +25,18 @@ public class IocContainerLifetimeTests
|
||||
public Guid Id { get; } = Guid.NewGuid();
|
||||
}
|
||||
|
||||
private sealed class DisposableTestService : ITestService, IDisposable
|
||||
{
|
||||
public Guid Id { get; } = Guid.NewGuid();
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
IsDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterSingleton_Should_Return_Same_Instance()
|
||||
{
|
||||
@ -207,4 +222,112 @@ public class IocContainerLifetimeTests
|
||||
scope2.Dispose();
|
||||
scope3.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Dispose_Should_Dispose_Resolved_Singleton_And_Block_Further_Use()
|
||||
{
|
||||
// Arrange
|
||||
var container = new MicrosoftDiContainer();
|
||||
container.RegisterSingleton<DisposableTestService, DisposableTestService>();
|
||||
container.Freeze();
|
||||
var service = container.GetRequired<DisposableTestService>();
|
||||
|
||||
// Act
|
||||
container.Dispose();
|
||||
|
||||
// Assert
|
||||
Assert.That(service.IsDisposed, Is.True);
|
||||
Assert.Throws<ObjectDisposedException>(() => container.Get<DisposableTestService>());
|
||||
Assert.Throws<ObjectDisposedException>(() => container.CreateScope());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Dispose_Should_Be_Idempotent()
|
||||
{
|
||||
var container = new MicrosoftDiContainer();
|
||||
|
||||
Assert.DoesNotThrow(container.Dispose);
|
||||
Assert.DoesNotThrow(container.Dispose);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Dispose_Should_Be_Idempotent_When_Called_Concurrently()
|
||||
{
|
||||
var container = new MicrosoftDiContainer();
|
||||
var containerLock = GetContainerLock(container);
|
||||
var releasedGate = false;
|
||||
|
||||
containerLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
var firstDisposeTask = Task.Run(container.Dispose);
|
||||
Thread.Sleep(50);
|
||||
var secondDisposeTask = Task.Run(container.Dispose);
|
||||
Thread.Sleep(50);
|
||||
|
||||
containerLock.ExitWriteLock();
|
||||
releasedGate = true;
|
||||
|
||||
Assert.That(async () => await Task.WhenAll(firstDisposeTask, secondDisposeTask).ConfigureAwait(false), Throws.Nothing);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!releasedGate)
|
||||
{
|
||||
containerLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Dispose_Should_Only_Attempt_Lock_Disposal_Once_When_Called_Concurrently()
|
||||
{
|
||||
var container = new MicrosoftDiContainer();
|
||||
var containerLock = GetContainerLock(container);
|
||||
var releasedGate = false;
|
||||
|
||||
containerLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
var firstDisposeTask = Task.Run(container.Dispose);
|
||||
Thread.Sleep(50);
|
||||
var secondDisposeTask = Task.Run(container.Dispose);
|
||||
Thread.Sleep(50);
|
||||
|
||||
containerLock.ExitWriteLock();
|
||||
releasedGate = true;
|
||||
|
||||
Assert.That(async () => await Task.WhenAll(firstDisposeTask, secondDisposeTask).ConfigureAwait(false), Throws.Nothing);
|
||||
Assert.That(GetLockDisposalStarted(container), Is.EqualTo(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!releasedGate)
|
||||
{
|
||||
containerLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过反射获取容器内部锁,用于构造可重复的并发释放竞态回归。
|
||||
/// </summary>
|
||||
private static ReaderWriterLockSlim GetContainerLock(MicrosoftDiContainer container)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
var lockField = typeof(MicrosoftDiContainer).GetField("_lock", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
Assert.That(lockField, Is.Not.Null);
|
||||
return (ReaderWriterLockSlim)lockField!.GetValue(container)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取锁销毁启动标记,验证并发释放路径不会重复执行底层锁销毁。
|
||||
/// </summary>
|
||||
private static int GetLockDisposalStarted(MicrosoftDiContainer container)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
var flagField = typeof(MicrosoftDiContainer).GetField("_lockDisposalStarted", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
Assert.That(flagField, Is.Not.Null);
|
||||
return (int)flagField!.GetValue(container)!;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Ioc;
|
||||
using GFramework.Core.Logging;
|
||||
@ -156,6 +158,21 @@ public class MicrosoftDiContainerTests
|
||||
Assert.That(result, Is.SameAs(instance));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试预冻结阶段通过 RegisterPlurality 注册的接口别名仍可通过 Get 解析到同一实例。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Get_Should_Return_RegisterPlurality_Interface_Instance_Before_Freeze()
|
||||
{
|
||||
var instance = new TestService();
|
||||
|
||||
_container.RegisterPlurality(instance);
|
||||
|
||||
var result = _container.Get<IService>();
|
||||
|
||||
Assert.That(result, Is.SameAs(instance));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试当 CQRS 基础设施已手动接线后,再调用处理器注册入口不会重复注册 runtime seam。
|
||||
/// </summary>
|
||||
@ -276,6 +293,32 @@ public class MicrosoftDiContainerTests
|
||||
Assert.That(results.Count, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试预冻结阶段通过实现类型注册的服务不会被当作已物化实例返回。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Get_Should_Return_Null_PreFreeze_For_ImplementationType_Registration()
|
||||
{
|
||||
_container.RegisterSingleton<IService, TestService>();
|
||||
|
||||
var result = _container.Get<IService>();
|
||||
|
||||
Assert.That(result, Is.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试预冻结阶段通过实现类型注册的服务在 GetAll 中同样不可见。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void GetAll_Should_Return_Empty_PreFreeze_For_ImplementationType_Registration()
|
||||
{
|
||||
_container.RegisterSingleton<IService, TestService>();
|
||||
|
||||
var results = _container.GetAll<IService>();
|
||||
|
||||
Assert.That(results, Is.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试容器未冻结时,会折叠“不同服务类型指向同一实例”的兼容别名重复,
|
||||
/// 但会保留同一服务类型的重复显式注册。
|
||||
@ -351,6 +394,21 @@ public class MicrosoftDiContainerTests
|
||||
Assert.That(_container.Get<TestService>(), Is.SameAs(instance));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试预冻结阶段通过 RegisterPlurality 注册的接口别名对 Contains 与 GetAll 都可见。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Contains_Should_Return_True_For_RegisterPlurality_Interface_Alias_Before_Freeze()
|
||||
{
|
||||
var instance = new TestService();
|
||||
_container.RegisterPlurality(instance);
|
||||
|
||||
var services = _container.GetAll<IService>();
|
||||
|
||||
Assert.That(services, Has.Count.EqualTo(1));
|
||||
Assert.That(_container.Contains<IService>(), Is.True);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 测试当不存在实例时检查包含关系应返回 false 的功能
|
||||
@ -361,6 +419,47 @@ public class MicrosoftDiContainerTests
|
||||
Assert.That(_container.Contains<TestService>(), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试显式服务不存在时,HasRegistration 应返回 false,且不会要求先冻结或解析实例。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HasRegistration_WithNoMatchingService_Should_ReturnFalse()
|
||||
{
|
||||
Assert.That(_container.HasRegistration(typeof(IPipelineBehavior<HasRegistrationRequest, int>)), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 HasRegistration 能识别开放泛型 CQRS pipeline 行为对闭合请求/响应对的可见性。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HasRegistration_Should_ReturnTrue_For_Closed_Service_Satisfied_By_Open_Generic_Registration()
|
||||
{
|
||||
_container.GetServicesUnsafe.AddSingleton(
|
||||
typeof(IPipelineBehavior<,>),
|
||||
typeof(OpenGenericHasRegistrationBehavior<,>));
|
||||
|
||||
Assert.That(_container.HasRegistration(typeof(IPipelineBehavior<HasRegistrationRequest, int>)), Is.True);
|
||||
|
||||
_container.Freeze();
|
||||
|
||||
Assert.That(_container.HasRegistration(typeof(IPipelineBehavior<HasRegistrationRequest, int>)), Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 HasRegistration 不会把仅以具体实现类型自注册的服务误判成其接口服务键也已注册。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HasRegistration_Should_ReturnFalse_For_Interface_When_Only_Concrete_Service_Key_Is_Registered()
|
||||
{
|
||||
_container.GetServicesUnsafe.AddSingleton(typeof(SelfRegisteredConcreteBehavior), typeof(SelfRegisteredConcreteBehavior));
|
||||
|
||||
Assert.That(_container.HasRegistration(typeof(IPipelineBehavior<HasRegistrationRequest, int>)), Is.False);
|
||||
|
||||
_container.Freeze();
|
||||
|
||||
Assert.That(_container.HasRegistration(typeof(IPipelineBehavior<HasRegistrationRequest, int>)), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试当实例存在时检查实例包含关系应返回 true 的功能
|
||||
/// </summary>
|
||||
@ -428,6 +527,21 @@ public class MicrosoftDiContainerTests
|
||||
Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 RegisterCqrsHandlersFromAssemblies 会通过注册阶段可见实例解析 CQRS 注册服务。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterCqrsHandlersFromAssemblies_Should_Resolve_Registration_Service_When_Registered_As_Instance()
|
||||
{
|
||||
Assert.DoesNotThrow(() =>
|
||||
_container.RegisterCqrsHandlersFromAssemblies([typeof(DeterministicOrderNotification).Assembly]));
|
||||
|
||||
Assert.That(
|
||||
_container.GetServicesUnsafe.Any(static descriptor =>
|
||||
descriptor.ServiceType == typeof(INotificationHandler<DeterministicOrderNotification>)),
|
||||
Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试当程序集集合中包含空元素时,CQRS handler 注册入口会在委托给注册服务前直接失败。
|
||||
/// </summary>
|
||||
@ -760,4 +874,118 @@ public class MicrosoftDiContainerTests
|
||||
Assert.That(((IPrioritizedService)services[0]).Priority, Is.EqualTo(10));
|
||||
Assert.That(((IPrioritizedService)services[1]).Priority, Is.EqualTo(30));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试容器释放后会阻止后续注册与解析,避免 benchmark 或短生命周期宿主继续使用已回收状态。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Dispose_Should_Block_Subsequent_Registration_And_Query_Operations()
|
||||
{
|
||||
_container.Dispose();
|
||||
|
||||
Assert.Throws<ObjectDisposedException>(() => _container.Register(new TestService()));
|
||||
Assert.Throws<ObjectDisposedException>(() => _container.Contains<TestService>());
|
||||
Assert.Throws<ObjectDisposedException>(() => _container.GetAll<TestService>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试等待中的读取线程在容器释放后也会收到稳定的容器级释放异常,而不是底层锁异常。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Dispose_Should_Translate_Waiting_Readers_To_Container_ObjectDisposedException()
|
||||
{
|
||||
_container.RegisterSingleton(new TestService());
|
||||
_container.Freeze();
|
||||
|
||||
var containerLock = GetContainerLock(_container);
|
||||
var releasedGate = false;
|
||||
using var queryStarted = new ManualResetEventSlim(false);
|
||||
|
||||
containerLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
var queryTask = Task.Run(() =>
|
||||
{
|
||||
queryStarted.Set();
|
||||
return _container.Get<TestService>();
|
||||
});
|
||||
|
||||
Assert.That(queryStarted.Wait(TimeSpan.FromSeconds(1)), Is.True);
|
||||
|
||||
var disposeTask = Task.Run(_container.Dispose);
|
||||
Thread.Sleep(50);
|
||||
|
||||
containerLock.ExitWriteLock();
|
||||
releasedGate = true;
|
||||
|
||||
await disposeTask.ConfigureAwait(false);
|
||||
|
||||
var exception = Assert.ThrowsAsync<ObjectDisposedException>(async () => await queryTask.ConfigureAwait(false));
|
||||
Assert.That(exception, Is.Not.Null);
|
||||
Assert.That(exception!.ObjectName, Is.EqualTo(nameof(MicrosoftDiContainer)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!releasedGate)
|
||||
{
|
||||
containerLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过反射获取容器内部锁,用于构造可重复的并发释放竞态回归。
|
||||
/// </summary>
|
||||
private static ReaderWriterLockSlim GetContainerLock(MicrosoftDiContainer container)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
var lockField = typeof(MicrosoftDiContainer).GetField("_lock", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
Assert.That(lockField, Is.Not.Null);
|
||||
return (ReaderWriterLockSlim)lockField!.GetValue(container)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 供 HasRegistration 回归使用的最小请求类型。
|
||||
/// </summary>
|
||||
private sealed class HasRegistrationRequest : IRequest<int>
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 供 HasRegistration 回归使用的开放泛型 pipeline 行为。
|
||||
/// </summary>
|
||||
/// <typeparam name="TRequest">请求类型。</typeparam>
|
||||
/// <typeparam name="TResponse">响应类型。</typeparam>
|
||||
private sealed class OpenGenericHasRegistrationBehavior<TRequest, TResponse> :
|
||||
IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : IRequest<TResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// 透传到下一个 pipeline 节点,不额外改变请求语义。
|
||||
/// </summary>
|
||||
public ValueTask<TResponse> Handle(
|
||||
TRequest request,
|
||||
MessageHandlerDelegate<TRequest, TResponse> next,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return next(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 供 HasRegistration 服务键判定回归使用的最小封闭 pipeline 行为。
|
||||
/// </summary>
|
||||
private sealed class SelfRegisteredConcreteBehavior : IPipelineBehavior<HasRegistrationRequest, int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 透传到下一个 pipeline 节点,不额外改变请求语义。
|
||||
/// </summary>
|
||||
public ValueTask<int> Handle(
|
||||
HasRegistrationRequest request,
|
||||
MessageHandlerDelegate<HasRegistrationRequest, int> next,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return next(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Query;
|
||||
using GFramework.Core.Tests.Architectures;
|
||||
using GFramework.Core.Tests.Command;
|
||||
|
||||
namespace GFramework.Core.Tests.Query;
|
||||
|
||||
@ -138,4 +140,33 @@ public class AsyncQueryExecutorTests
|
||||
Assert.That(result1, Is.EqualTo(20));
|
||||
Assert.That(result2, Is.EqualTo(40));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 legacy 异步查询桥接会保留上下文注入,并通过 runtime 返回结果。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task SendAsync_Should_Bridge_Through_Runtime_And_Preserve_Context()
|
||||
{
|
||||
var runtime = new RecordingCqrsRuntime(static _ => 64);
|
||||
var executor = new AsyncQueryExecutor(runtime);
|
||||
var query = new ContextAwareLegacyAsyncQuery(64);
|
||||
var expectedContext = new TestArchitectureContextBaseStub();
|
||||
((GFramework.Core.Abstractions.Rule.IContextAware)query).SetContext(expectedContext);
|
||||
|
||||
var result = await executor.SendAsync(query);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(result, Is.EqualTo(64));
|
||||
Assert.That(runtime.LastRequest, Is.TypeOf<GFramework.Core.Cqrs.LegacyAsyncQueryDispatchRequest>());
|
||||
Assert.That(query.ObservedContext, Is.SameAs(expectedContext));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为异步 bridge 测试提供最小架构上下文替身。
|
||||
/// </summary>
|
||||
private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
26
GFramework.Core.Tests/Query/ContextAwareLegacyAsyncQuery.cs
Normal file
26
GFramework.Core.Tests/Query/ContextAwareLegacyAsyncQuery.cs
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Query;
|
||||
using GFramework.Core.Rule;
|
||||
|
||||
namespace GFramework.Core.Tests.Query;
|
||||
|
||||
/// <summary>
|
||||
/// 为 <see cref="AsyncQueryExecutorTests" /> 提供可观察上下文注入的 legacy 异步查询。
|
||||
/// </summary>
|
||||
internal sealed class ContextAwareLegacyAsyncQuery(int result) : ContextAwareBase, IAsyncQuery<int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取执行期间观察到的架构上下文。
|
||||
/// </summary>
|
||||
public IArchitectureContext? ObservedContext { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> DoAsync()
|
||||
{
|
||||
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
}
|
||||
26
GFramework.Core.Tests/Query/ContextAwareLegacyQuery.cs
Normal file
26
GFramework.Core.Tests/Query/ContextAwareLegacyQuery.cs
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Query;
|
||||
using GFramework.Core.Rule;
|
||||
|
||||
namespace GFramework.Core.Tests.Query;
|
||||
|
||||
/// <summary>
|
||||
/// 为 <see cref="QueryExecutorTests" /> 提供可观察上下文注入的 legacy 查询。
|
||||
/// </summary>
|
||||
internal sealed class ContextAwareLegacyQuery(int result) : ContextAwareBase, IQuery<int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取执行期间观察到的架构上下文。
|
||||
/// </summary>
|
||||
public IArchitectureContext? ObservedContext { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Do()
|
||||
{
|
||||
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Query;
|
||||
using GFramework.Core.Tests.Architectures;
|
||||
using GFramework.Core.Tests.Command;
|
||||
|
||||
namespace GFramework.Core.Tests.Query;
|
||||
|
||||
@ -61,4 +63,44 @@ public class QueryExecutorTests
|
||||
|
||||
Assert.That(result, Is.EqualTo("Result: 10"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 legacy 同步查询桥接会在线程池上等待 runtime,
|
||||
/// 避免直接复用调用方的同步上下文。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Send_Should_Bridge_Through_Runtime_Without_Reusing_Caller_SynchronizationContext()
|
||||
{
|
||||
var runtime = new RecordingCqrsRuntime(static _ => 24);
|
||||
var executor = new QueryExecutor(runtime);
|
||||
var query = new ContextAwareLegacyQuery(24);
|
||||
var expectedContext = new TestArchitectureContextBaseStub();
|
||||
((GFramework.Core.Abstractions.Rule.IContextAware)query).SetContext(expectedContext);
|
||||
var originalContext = SynchronizationContext.Current;
|
||||
|
||||
try
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(new TestLegacySynchronizationContext());
|
||||
|
||||
var result = executor.Send(query);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(result, Is.EqualTo(24));
|
||||
Assert.That(runtime.LastRequest, Is.TypeOf<GFramework.Core.Cqrs.LegacyQueryDispatchRequest>());
|
||||
Assert.That(runtime.ObservedSynchronizationContextType, Is.Null);
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(originalContext);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为同步 bridge 测试提供最小架构上下文替身。
|
||||
/// </summary>
|
||||
private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,22 +83,15 @@ public class ContextAwareTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 GetContext 方法在未设置上下文时的行为
|
||||
/// 验证当内部 Context 为 null 时,GetContext 方法不会抛出异常
|
||||
/// 此时应返回第一个架构上下文(在测试环境中验证不抛出异常即可)
|
||||
/// 测试 GetContext 方法在未设置上下文时会回退到当前活动上下文
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void GetContext_Should_Return_FirstArchitectureContext_When_Not_Set()
|
||||
public void GetContext_Should_Return_CurrentArchitectureContext_When_Not_Set()
|
||||
{
|
||||
// Arrange - 暂时不调用 SetContext,让 Context 为 null
|
||||
IContextAware aware = _contextAware;
|
||||
|
||||
// Act - 当 Context 为 null 时,应该返回第一个 Architecture Context
|
||||
// 由于测试环境中没有实际的 Architecture Context,这里只测试调用不会抛出异常
|
||||
// 在实际使用中,当 Context 为 null 时会调用 GameContext.GetFirstArchitectureContext()
|
||||
var result = aware.GetContext();
|
||||
|
||||
// Assert - 验证在没有设置 Context 时的行为
|
||||
// 注意:由于测试环境中可能没有 Architecture Context,这里我们只测试不抛出异常
|
||||
Assert.DoesNotThrow(() => aware.GetContext());
|
||||
Assert.That(result, Is.SameAs(_mockContext));
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,6 +177,21 @@ public abstract class Architecture : IArchitecture
|
||||
_modules.RegisterCqrsPipelineBehavior<TBehavior>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册 CQRS 流式请求管道行为。
|
||||
/// 可以传入开放泛型行为类型,也可以传入绑定到特定流式请求的封闭行为类型。
|
||||
/// </summary>
|
||||
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
||||
/// <exception cref="InvalidOperationException">当前架构的底层容器已冻结,无法继续注册流式管道行为。</exception>
|
||||
/// <exception cref="ObjectDisposedException">当前架构的底层容器已释放,无法继续注册流式管道行为。</exception>
|
||||
/// <remarks>
|
||||
/// 该调用会委托到底层容器完成校验与注册,因此应在初始化冻结前完成所有流式行为接线。
|
||||
/// </remarks>
|
||||
public void RegisterCqrsStreamPipelineBehavior<TBehavior>() where TBehavior : class
|
||||
{
|
||||
_modules.RegisterCqrsStreamPipelineBehavior<TBehavior>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从指定程序集显式注册 CQRS 处理器。
|
||||
/// 该入口适用于把拆分到其他模块或扩展包程序集中的 handlers 接入当前架构。
|
||||
@ -359,18 +374,44 @@ public abstract class Architecture : IArchitecture
|
||||
/// <summary>
|
||||
/// 异步销毁架构及所有组件
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 无论 <c>_lifecycle.DestroyAsync()</c> 是否抛出异常,该方法都会在 <see langword="finally" /> 中调用
|
||||
/// <see cref="GameContext.Unbind" />(<see cref="object.GetType" />()),移除当前架构类型在全局上下文表中的绑定。
|
||||
/// 这样可以阻止新的惰性上下文回退命中已销毁实例;但已经缓存上下文的对象不会被自动重置。
|
||||
/// </remarks>
|
||||
public virtual async ValueTask DestroyAsync()
|
||||
{
|
||||
await _lifecycle.DestroyAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await _lifecycle.DestroyAsync().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 架构初始化时会把当前实例绑定到 GameContext;销毁后必须解除该全局回退入口,
|
||||
// 避免后续惰性 ContextAware 调用继续命中过期的运行时上下文。
|
||||
GameContext.Unbind(GetType());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 销毁架构并清理所有组件资源(同步方法,保留用于向后兼容)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该同步兼容入口会与 <see cref="DestroyAsync" /> 保持相同的全局解绑语义;即使销毁过程抛出异常,
|
||||
/// 也会在 <see langword="finally" /> 中调用 <see cref="GameContext.Unbind" />(<see cref="object.GetType" />())。
|
||||
/// </remarks>
|
||||
[Obsolete("建议使用 DestroyAsync() 以支持异步清理")]
|
||||
public virtual void Destroy()
|
||||
{
|
||||
_lifecycle.Destroy();
|
||||
try
|
||||
{
|
||||
_lifecycle.Destroy();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 同步销毁同样需要解除全局回退入口,避免兼容调用路径保留过期上下文。
|
||||
GameContext.Unbind(GetType());
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@ -11,6 +11,7 @@ using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Query;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Abstractions.Utility;
|
||||
using GFramework.Core.Cqrs;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
|
||||
|
||||
@ -111,7 +112,8 @@ public class ArchitectureContext : IArchitectureContext
|
||||
/// <returns>响应结果</returns>
|
||||
public TResponse SendRequest<TResponse>(IRequest<TResponse> request)
|
||||
{
|
||||
return SendRequestAsync(request).AsTask().GetAwaiter().GetResult();
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -180,10 +182,12 @@ public class ArchitectureContext : IArchitectureContext
|
||||
/// <returns>查询结果</returns>
|
||||
public TResult SendQuery<TResult>(IQuery<TResult> query)
|
||||
{
|
||||
if (query == null) throw new ArgumentNullException(nameof(query));
|
||||
var queryBus = GetOrCache<IQueryExecutor>();
|
||||
if (queryBus == null) throw new InvalidOperationException("IQueryExecutor not registered");
|
||||
return queryBus.Send(query);
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
var boxedResult = SendRequest(
|
||||
new LegacyQueryDispatchRequest(
|
||||
query,
|
||||
() => query.Do()));
|
||||
return (TResult)boxedResult!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -192,9 +196,10 @@ public class ArchitectureContext : IArchitectureContext
|
||||
/// <typeparam name="TResponse">查询响应类型</typeparam>
|
||||
/// <param name="query">要发送的查询对象</param>
|
||||
/// <returns>查询结果</returns>
|
||||
public TResponse SendQuery<TResponse>(Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
|
||||
public TResponse SendQuery<TResponse>(global::GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
|
||||
{
|
||||
return SendQueryAsync(query).AsTask().GetAwaiter().GetResult();
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, query);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -205,10 +210,13 @@ public class ArchitectureContext : IArchitectureContext
|
||||
/// <returns>查询结果</returns>
|
||||
public async Task<TResult> SendQueryAsync<TResult>(IAsyncQuery<TResult> query)
|
||||
{
|
||||
if (query == null) throw new ArgumentNullException(nameof(query));
|
||||
var asyncQueryBus = GetOrCache<IAsyncQueryExecutor>();
|
||||
if (asyncQueryBus == null) throw new InvalidOperationException("IAsyncQueryExecutor not registered");
|
||||
return await asyncQueryBus.SendAsync(query).ConfigureAwait(false);
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
var boxedResult = await SendRequestAsync(
|
||||
new LegacyAsyncQueryDispatchRequest(
|
||||
query,
|
||||
async () => await query.DoAsync().ConfigureAwait(false)))
|
||||
.ConfigureAwait(false);
|
||||
return (TResult)boxedResult!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -218,7 +226,7 @@ public class ArchitectureContext : IArchitectureContext
|
||||
/// <param name="query">要发送的查询对象</param>
|
||||
/// <param name="cancellationToken">取消令牌,用于取消操作</param>
|
||||
/// <returns>包含查询结果的ValueTask</returns>
|
||||
public async ValueTask<TResponse> SendQueryAsync<TResponse>(Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
|
||||
public async ValueTask<TResponse> SendQueryAsync<TResponse>(global::GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
@ -355,7 +363,7 @@ public class ArchitectureContext : IArchitectureContext
|
||||
/// <param name="cancellationToken">取消令牌,用于取消操作</param>
|
||||
/// <returns>包含命令执行结果的ValueTask</returns>
|
||||
public async ValueTask<TResponse> SendCommandAsync<TResponse>(
|
||||
Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
|
||||
global::GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
@ -369,9 +377,7 @@ public class ArchitectureContext : IArchitectureContext
|
||||
public async Task SendCommandAsync(IAsyncCommand command)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
var commandBus = GetOrCache<ICommandExecutor>();
|
||||
if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered");
|
||||
await commandBus.SendAsync(command).ConfigureAwait(false);
|
||||
await SendRequestAsync(new LegacyAsyncCommandDispatchRequest(command)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -383,9 +389,12 @@ public class ArchitectureContext : IArchitectureContext
|
||||
public async Task<TResult> SendCommandAsync<TResult>(IAsyncCommand<TResult> command)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
var commandBus = GetOrCache<ICommandExecutor>();
|
||||
if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered");
|
||||
return await commandBus.SendAsync(command).ConfigureAwait(false);
|
||||
var boxedResult = await SendRequestAsync(
|
||||
new LegacyAsyncCommandResultDispatchRequest(
|
||||
command,
|
||||
async () => await command.ExecuteAsync().ConfigureAwait(false)))
|
||||
.ConfigureAwait(false);
|
||||
return (TResult)boxedResult!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -394,9 +403,10 @@ public class ArchitectureContext : IArchitectureContext
|
||||
/// <typeparam name="TResponse">命令响应类型</typeparam>
|
||||
/// <param name="command">要发送的命令对象</param>
|
||||
/// <returns>命令执行结果</returns>
|
||||
public TResponse SendCommand<TResponse>(Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
|
||||
public TResponse SendCommand<TResponse>(global::GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
|
||||
{
|
||||
return SendCommandAsync(command).AsTask().GetAwaiter().GetResult();
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -406,8 +416,7 @@ public class ArchitectureContext : IArchitectureContext
|
||||
public void SendCommand(ICommand command)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
var commandBus = GetOrCache<ICommandExecutor>();
|
||||
commandBus.Send(command);
|
||||
SendRequest(new LegacyCommandDispatchRequest(command));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -419,9 +428,11 @@ public class ArchitectureContext : IArchitectureContext
|
||||
public TResult SendCommand<TResult>(ICommand<TResult> command)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
var commandBus = GetOrCache<ICommandExecutor>();
|
||||
if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered");
|
||||
return commandBus.Send(command);
|
||||
var boxedResult = SendRequest(
|
||||
new LegacyCommandResultDispatchRequest(
|
||||
command,
|
||||
() => command.Execute()));
|
||||
return (TResult)boxedResult!;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@ -27,6 +27,19 @@ internal sealed class ArchitectureModules(
|
||||
services.Container.RegisterCqrsPipelineBehavior<TBehavior>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册 CQRS 流式请求管道行为。
|
||||
/// 支持开放泛型行为类型和针对单一流式请求的封闭行为类型。
|
||||
/// </summary>
|
||||
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
||||
/// <exception cref="InvalidOperationException">底层容器已冻结,无法继续注册流式管道行为。</exception>
|
||||
/// <exception cref="ObjectDisposedException">底层容器已释放,无法继续注册流式管道行为。</exception>
|
||||
public void RegisterCqrsStreamPipelineBehavior<TBehavior>() where TBehavior : class
|
||||
{
|
||||
logger.Debug($"Registering CQRS stream pipeline behavior: {typeof(TBehavior).Name}");
|
||||
services.Container.RegisterCqrsStreamPipelineBehavior<TBehavior>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从指定程序集显式注册 CQRS 处理器。
|
||||
/// 该入口用于把默认架构程序集之外的扩展处理器接入当前架构容器。
|
||||
|
||||
@ -2,58 +2,101 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
|
||||
namespace GFramework.Core.Architectures;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏上下文管理类,用于管理当前的架构上下文实例
|
||||
/// 游戏上下文管理类,用于管理当前活动的架构上下文实例及其兼容类型别名。
|
||||
/// </summary>
|
||||
public static class GameContext
|
||||
{
|
||||
// ConcurrentDictionary 负责向外暴露安全的实时视图;该锁负责维护“别名字典 + 当前活动上下文”之间的组合不变式。
|
||||
#if NET9_0_OR_GREATER
|
||||
private static readonly Lock SyncRoot = new();
|
||||
#else
|
||||
private static readonly object SyncRoot = new();
|
||||
#endif
|
||||
private static readonly ConcurrentDictionary<Type, IArchitectureContext> ArchitectureDictionary
|
||||
= new();
|
||||
private static IArchitectureContext? _currentArchitectureContext;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有已注册的架构上下文的只读字典
|
||||
/// 获取所有已注册的架构上下文类型别名映射。
|
||||
/// 该只读视图会反映当前并发状态,不保证是稳定快照。
|
||||
/// </summary>
|
||||
public static IReadOnlyDictionary<Type, IArchitectureContext> ArchitectureReadOnlyDictionary =>
|
||||
ArchitectureDictionary;
|
||||
|
||||
/// <summary>
|
||||
/// 绑定指定类型的架构上下文到管理器中
|
||||
/// 绑定指定类型的架构上下文到管理器中。
|
||||
/// 同一时刻只允许存在一个活动上下文实例,但可以为其绑定多个兼容类型别名。
|
||||
/// </summary>
|
||||
/// <param name="architectureType">架构类型</param>
|
||||
/// <param name="context">架构上下文实例</param>
|
||||
/// <exception cref="InvalidOperationException">当指定类型的架构上下文已存在时抛出</exception>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <paramref name="architectureType" /> 或 <paramref name="context" /> 为 <see langword="null" />。
|
||||
/// </exception>
|
||||
/// <exception cref="InvalidOperationException">当指定类型的架构上下文已存在,或尝试绑定第二个不同上下文实例时抛出。</exception>
|
||||
public static void Bind(Type architectureType, IArchitectureContext context)
|
||||
{
|
||||
if (!ArchitectureDictionary.TryAdd(architectureType, context))
|
||||
throw new InvalidOperationException(
|
||||
$"Architecture context for '{architectureType.Name}' already exists");
|
||||
ArgumentNullException.ThrowIfNull(architectureType);
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (_currentArchitectureContext != null && !ReferenceEquals(_currentArchitectureContext, context))
|
||||
throw new InvalidOperationException(
|
||||
$"GameContext already tracks active context '{_currentArchitectureContext.GetType().Name}'. " +
|
||||
$"Cannot bind a different context '{context.GetType().Name}'.");
|
||||
|
||||
if (!ArchitectureDictionary.TryAdd(architectureType, context))
|
||||
throw new InvalidOperationException(
|
||||
$"Architecture context for '{architectureType.Name}' already exists");
|
||||
|
||||
_currentArchitectureContext ??= context;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字典中的第一个架构上下文
|
||||
/// 获取当前活动的架构上下文。
|
||||
/// 该方法保留原有名称以兼容存量调用方,但语义已经收敛为“当前上下文”,而不是任意字典首项。
|
||||
/// </summary>
|
||||
/// <returns>返回字典中的第一个架构上下文实例</returns>
|
||||
/// <exception cref="InvalidOperationException">当字典为空时抛出</exception>
|
||||
/// <returns>当前活动的架构上下文实例。</returns>
|
||||
/// <exception cref="InvalidOperationException">当当前没有活动上下文时抛出。</exception>
|
||||
public static IArchitectureContext GetFirstArchitectureContext()
|
||||
{
|
||||
return ArchitectureDictionary.Values.First();
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (_currentArchitectureContext is { } context)
|
||||
return context;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("No active architecture context is currently bound.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据类型获取对应的架构上下文
|
||||
/// 根据类型获取对应的架构上下文。
|
||||
/// 兼容层会优先查找显式绑定的类型别名,然后回退到当前上下文的类型兼容判断。
|
||||
/// </summary>
|
||||
/// <param name="type">要查找的架构类型</param>
|
||||
/// <returns>返回指定类型的架构上下文实例</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="type" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">当指定类型的架构上下文不存在时抛出</exception>
|
||||
public static IArchitectureContext GetByType(Type type)
|
||||
{
|
||||
if (ArchitectureDictionary.TryGetValue(type, out var context))
|
||||
return context;
|
||||
ArgumentNullException.ThrowIfNull(type);
|
||||
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (ArchitectureDictionary.TryGetValue(type, out var context))
|
||||
return context;
|
||||
|
||||
if (_currentArchitectureContext != null && type.IsInstanceOfType(_currentArchitectureContext))
|
||||
return _currentArchitectureContext;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Architecture context for '{type.Name}' not found");
|
||||
@ -61,22 +104,30 @@ public static class GameContext
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定类型的架构上下文实例
|
||||
/// 获取指定类型的架构上下文实例。
|
||||
/// 该方法会优先复用当前活动上下文,再回退到显式注册的类型别名。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">架构上下文类型,必须实现IArchitectureContext接口</typeparam>
|
||||
/// <returns>指定类型的架构上下文实例</returns>
|
||||
/// <exception cref="InvalidOperationException">当指定类型的架构上下文不存在时抛出</exception>
|
||||
public static T Get<T>() where T : class, IArchitectureContext
|
||||
{
|
||||
if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx))
|
||||
return (T)ctx;
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (_currentArchitectureContext is T currentContext)
|
||||
return currentContext;
|
||||
|
||||
if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx))
|
||||
return (T)ctx;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Architecture context '{typeof(T).Name}' not found");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取指定类型的架构上下文实例
|
||||
/// 尝试获取指定类型的架构上下文实例。
|
||||
/// 该方法会优先检查当前活动上下文是否兼容目标类型,再回退到显式注册的类型别名。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">架构上下文类型,必须实现IArchitectureContext接口</typeparam>
|
||||
/// <param name="context">输出参数,如果找到则返回对应的架构上下文实例,否则返回null</param>
|
||||
@ -84,10 +135,19 @@ public static class GameContext
|
||||
public static bool TryGet<T>(out T? context)
|
||||
where T : class, IArchitectureContext
|
||||
{
|
||||
if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx))
|
||||
lock (SyncRoot)
|
||||
{
|
||||
context = (T)ctx;
|
||||
return true;
|
||||
if (_currentArchitectureContext is T currentContext)
|
||||
{
|
||||
context = currentContext;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx))
|
||||
{
|
||||
context = (T)ctx;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
context = null;
|
||||
@ -95,20 +155,54 @@ public static class GameContext
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除指定类型的架构上下文绑定
|
||||
/// 移除指定类型的架构上下文绑定。
|
||||
/// 当最后一个指向当前活动上下文的别名被移除时,也会同步清空当前活动上下文指针。
|
||||
/// </summary>
|
||||
/// <param name="architectureType">要移除的架构类型</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="architectureType" /> 为 <see langword="null" />。</exception>
|
||||
public static void Unbind(Type architectureType)
|
||||
{
|
||||
ArchitectureDictionary.TryRemove(architectureType, out _);
|
||||
ArgumentNullException.ThrowIfNull(architectureType);
|
||||
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (!ArchitectureDictionary.TryRemove(architectureType, out var removedContext))
|
||||
return;
|
||||
|
||||
if (_currentArchitectureContext == null || !ReferenceEquals(_currentArchitectureContext, removedContext))
|
||||
return;
|
||||
|
||||
if (!HasAliasForContext(removedContext))
|
||||
_currentArchitectureContext = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有架构上下文绑定
|
||||
/// 清空所有架构上下文绑定,并重置当前活动上下文。
|
||||
/// </summary>
|
||||
public static void Clear()
|
||||
{
|
||||
ArchitectureDictionary.Clear();
|
||||
lock (SyncRoot)
|
||||
{
|
||||
ArchitectureDictionary.Clear();
|
||||
_currentArchitectureContext = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断当前是否仍存在指向同一上下文实例的其他类型别名。
|
||||
/// </summary>
|
||||
/// <param name="context">被移除绑定原本指向的上下文实例。</param>
|
||||
/// <returns>如果还有其他别名指向同一实例则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
private static bool HasAliasForContext(IArchitectureContext context)
|
||||
{
|
||||
foreach (var current in ArchitectureDictionary.Values)
|
||||
{
|
||||
if (ReferenceEquals(current, context))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,21 +6,24 @@ using GFramework.Core.Abstractions.Architectures;
|
||||
namespace GFramework.Core.Architectures;
|
||||
|
||||
/// <summary>
|
||||
/// 基于 GameContext 的默认上下文提供者
|
||||
/// 基于 GameContext 的默认上下文提供者。
|
||||
/// 默认只面向当前活动上下文工作,而不是维护多个并存的全局上下文。
|
||||
/// </summary>
|
||||
public sealed class GameContextProvider : IArchitectureContextProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前的架构上下文(返回第一个注册的架构上下文)
|
||||
/// 获取当前的架构上下文。
|
||||
/// </summary>
|
||||
/// <returns>架构上下文实例</returns>
|
||||
/// <exception cref="InvalidOperationException">当前没有已绑定的活动架构上下文时抛出。</exception>
|
||||
public IArchitectureContext GetContext()
|
||||
{
|
||||
return GameContext.GetFirstArchitectureContext();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取指定类型的架构上下文
|
||||
/// 尝试获取指定类型的架构上下文。
|
||||
/// 若当前活动上下文本身兼容 <typeparamref name="T" />,则无需显式类型别名也会返回成功。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">架构上下文类型</typeparam>
|
||||
/// <param name="context">输出的上下文实例</param>
|
||||
@ -29,4 +32,4 @@ public sealed class GameContextProvider : IArchitectureContextProvider
|
||||
{
|
||||
return GameContext.TryGet(out context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Command;
|
||||
using GFramework.Core.Cqrs;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using IAsyncCommand = GFramework.Core.Abstractions.Command.IAsyncCommand;
|
||||
|
||||
namespace GFramework.Core.Command;
|
||||
@ -10,8 +12,20 @@ namespace GFramework.Core.Command;
|
||||
/// 表示一个命令执行器,用于执行命令操作。
|
||||
/// 该类实现了 ICommandExecutor 接口,提供命令执行的核心功能。
|
||||
/// </summary>
|
||||
public sealed class CommandExecutor : ICommandExecutor
|
||||
public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExecutor
|
||||
{
|
||||
private readonly ICqrsRuntime? _runtime = runtime;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前执行器是否已接入统一 CQRS runtime。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 当调用方只是直接 new 一个执行器做纯单元测试时,这里允许为空,并回退到 legacy 直接执行路径;
|
||||
/// 当执行器由架构容器提供给 <see cref="Architectures.ArchitectureContext" /> 使用时,应始终传入 runtime,
|
||||
/// 以便旧入口也复用统一 pipeline 与 handler 调度链路。
|
||||
/// </remarks>
|
||||
public bool UsesCqrsRuntime => _runtime is not null;
|
||||
|
||||
/// <summary>
|
||||
/// 发送并执行无返回值的命令
|
||||
/// </summary>
|
||||
@ -21,6 +35,11 @@ public sealed class CommandExecutor : ICommandExecutor
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
|
||||
if (TryExecuteThroughCqrsRuntime(command, static currentCommand => new LegacyCommandDispatchRequest(currentCommand)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
command.Execute();
|
||||
}
|
||||
|
||||
@ -35,6 +54,16 @@ public sealed class CommandExecutor : ICommandExecutor
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
|
||||
if (TryExecuteThroughCqrsRuntime(
|
||||
command,
|
||||
static currentCommand => new LegacyCommandResultDispatchRequest(
|
||||
currentCommand,
|
||||
() => currentCommand.Execute()),
|
||||
out TResult? result))
|
||||
{
|
||||
return result!;
|
||||
}
|
||||
|
||||
return command.Execute();
|
||||
}
|
||||
|
||||
@ -47,6 +76,13 @@ public sealed class CommandExecutor : ICommandExecutor
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
|
||||
var cqrsRuntime = _runtime;
|
||||
|
||||
if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, command, out var context))
|
||||
{
|
||||
return cqrsRuntime.SendAsync(context, new LegacyAsyncCommandDispatchRequest(command)).AsTask();
|
||||
}
|
||||
|
||||
return command.ExecuteAsync();
|
||||
}
|
||||
|
||||
@ -61,6 +97,90 @@ public sealed class CommandExecutor : ICommandExecutor
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
|
||||
var cqrsRuntime = _runtime;
|
||||
|
||||
if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, command, out var context))
|
||||
{
|
||||
return BridgeAsyncCommandWithResultAsync(cqrsRuntime, context, command);
|
||||
}
|
||||
|
||||
return command.ExecuteAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试通过统一 CQRS runtime 执行当前 legacy 请求。
|
||||
/// </summary>
|
||||
/// <typeparam name="TTarget">legacy 目标对象类型。</typeparam>
|
||||
/// <typeparam name="TRequest">bridge request 类型。</typeparam>
|
||||
/// <param name="target">即将执行的 legacy 目标对象。</param>
|
||||
/// <param name="requestFactory">用于创建 bridge request 的工厂。</param>
|
||||
/// <returns>若成功切入 CQRS runtime 则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
private bool TryExecuteThroughCqrsRuntime<TTarget, TRequest>(
|
||||
TTarget target,
|
||||
Func<TTarget, TRequest> requestFactory)
|
||||
where TTarget : class
|
||||
where TRequest : IRequest<Unit>
|
||||
{
|
||||
var cqrsRuntime = _runtime;
|
||||
|
||||
if (!LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, target, out var context))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
LegacyCqrsDispatchHelper.SendSynchronously(cqrsRuntime, context, requestFactory(target));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试通过统一 CQRS runtime 执行当前 legacy 请求,并返回装箱结果。
|
||||
/// </summary>
|
||||
/// <typeparam name="TTarget">legacy 目标对象类型。</typeparam>
|
||||
/// <typeparam name="TResult">预期结果类型。</typeparam>
|
||||
/// <typeparam name="TRequest">bridge request 类型。</typeparam>
|
||||
/// <param name="target">即将执行的 legacy 目标对象。</param>
|
||||
/// <param name="requestFactory">用于创建 bridge request 的工厂。</param>
|
||||
/// <param name="result">若命中 bridge,则返回执行结果;否则返回默认值。</param>
|
||||
/// <returns>若成功切入 CQRS runtime 则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
private bool TryExecuteThroughCqrsRuntime<TTarget, TResult, TRequest>(
|
||||
TTarget target,
|
||||
Func<TTarget, TRequest> requestFactory,
|
||||
out TResult? result)
|
||||
where TTarget : class
|
||||
where TRequest : IRequest<object?>
|
||||
{
|
||||
var cqrsRuntime = _runtime;
|
||||
|
||||
if (!LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, target, out var context))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var boxedResult = LegacyCqrsDispatchHelper.SendSynchronously(cqrsRuntime, context, requestFactory(target));
|
||||
result = (TResult)boxedResult!;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过统一 CQRS runtime 异步执行 legacy 带返回值命令,并把装箱结果还原为目标类型。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">命令返回值类型。</typeparam>
|
||||
/// <param name="runtime">负责调度当前 bridge request 的统一 CQRS runtime。</param>
|
||||
/// <param name="context">当前架构上下文。</param>
|
||||
/// <param name="command">要桥接的 legacy 命令。</param>
|
||||
/// <returns>命令执行结果。</returns>
|
||||
private static async Task<TResult> BridgeAsyncCommandWithResultAsync<TResult>(
|
||||
ICqrsRuntime runtime,
|
||||
GFramework.Core.Abstractions.Architectures.IArchitectureContext context,
|
||||
IAsyncCommand<TResult> command)
|
||||
{
|
||||
var boxedResult = await runtime.SendAsync(
|
||||
context,
|
||||
new LegacyAsyncCommandResultDispatchRequest(
|
||||
command,
|
||||
async () => await command.ExecuteAsync().ConfigureAwait(false)))
|
||||
.ConfigureAwait(false);
|
||||
return (TResult)boxedResult!;
|
||||
}
|
||||
}
|
||||
|
||||
25
GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequest.cs
Normal file
25
GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequest.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using CoreCommand = GFramework.Core.Abstractions.Command;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 包装 legacy 异步无返回值命令,使其能够通过自有 CQRS runtime 调度。
|
||||
/// </summary>
|
||||
/// <param name="command">当前 bridge request 代理的 legacy 异步命令实例。</param>
|
||||
internal sealed class LegacyAsyncCommandDispatchRequest(CoreCommand.IAsyncCommand command)
|
||||
: LegacyCqrsDispatchRequestBase(ValidateCommand(command)), IRequest<Unit>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前 bridge request 代理的异步命令实例。
|
||||
/// </summary>
|
||||
public CoreCommand.IAsyncCommand Command { get; } = command;
|
||||
|
||||
private static CoreCommand.IAsyncCommand ValidateCommand(CoreCommand.IAsyncCommand command)
|
||||
{
|
||||
return command ?? throw new ArgumentNullException(nameof(command));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 处理 legacy 异步无返回值命令的 bridge handler。
|
||||
/// </summary>
|
||||
internal sealed class LegacyAsyncCommandDispatchRequestHandler
|
||||
: LegacyCqrsDispatchHandlerBase, IRequestHandler<LegacyAsyncCommandDispatchRequest, Unit>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<Unit> Handle(
|
||||
LegacyAsyncCommandDispatchRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
// Legacy ExecuteAsync contract does not accept CancellationToken; use WaitAsync so the caller can still observe cancellation promptly.
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
PrepareTarget(request.Command);
|
||||
await request.Command.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 包装 legacy 异步带返回值命令,使其能够通过自有 CQRS runtime 调度。
|
||||
/// </summary>
|
||||
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 命令目标实例。</param>
|
||||
/// <param name="executeAsync">封装 legacy 异步命令执行逻辑并返回装箱结果的委托。</param>
|
||||
internal sealed class LegacyAsyncCommandResultDispatchRequest(object target, Func<Task<object?>> executeAsync)
|
||||
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
|
||||
{
|
||||
private readonly Func<Task<object?>> _executeAsync = executeAsync ?? throw new ArgumentNullException(nameof(executeAsync));
|
||||
|
||||
/// <summary>
|
||||
/// 异步执行底层 legacy 命令并返回装箱后的结果。
|
||||
/// </summary>
|
||||
/// <returns>表示异步执行结果的任务;任务结果为底层 legacy 命令返回的装箱值。</returns>
|
||||
public Task<object?> ExecuteAsync() => _executeAsync();
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 处理 legacy 异步带返回值命令的 bridge handler。
|
||||
/// </summary>
|
||||
internal sealed class LegacyAsyncCommandResultDispatchRequestHandler
|
||||
: LegacyCqrsDispatchHandlerBase, IRequestHandler<LegacyAsyncCommandResultDispatchRequest, object?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<object?> Handle(
|
||||
LegacyAsyncCommandResultDispatchRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
// Legacy ExecuteAsync contract does not accept CancellationToken; use WaitAsync so the caller can observe cancellation promptly.
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
PrepareTarget(request.Target);
|
||||
return await request.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
23
GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequest.cs
Normal file
23
GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequest.cs
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 包装 legacy 异步查询,使其能够通过自有 CQRS runtime 调度。
|
||||
/// </summary>
|
||||
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 查询目标实例。</param>
|
||||
/// <param name="executeAsync">封装 legacy 异步查询执行逻辑并返回装箱结果的委托。</param>
|
||||
internal sealed class LegacyAsyncQueryDispatchRequest(object target, Func<Task<object?>> executeAsync)
|
||||
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
|
||||
{
|
||||
private readonly Func<Task<object?>> _executeAsync = executeAsync ?? throw new ArgumentNullException(nameof(executeAsync));
|
||||
|
||||
/// <summary>
|
||||
/// 异步执行底层 legacy 查询并返回装箱后的结果。
|
||||
/// </summary>
|
||||
/// <returns>表示异步执行结果的任务;任务结果为底层 legacy 查询返回的装箱值。</returns>
|
||||
public Task<object?> ExecuteAsync() => _executeAsync();
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 处理 legacy 异步查询的 bridge handler。
|
||||
/// </summary>
|
||||
internal sealed class LegacyAsyncQueryDispatchRequestHandler
|
||||
: LegacyCqrsDispatchHandlerBase, IRequestHandler<LegacyAsyncQueryDispatchRequest, object?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<object?> Handle(
|
||||
LegacyAsyncQueryDispatchRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
// Legacy DoAsync contract does not accept CancellationToken; use WaitAsync so the caller can observe cancellation promptly.
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
PrepareTarget(request.Target);
|
||||
return await request.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
25
GFramework.Core/Cqrs/LegacyCommandDispatchRequest.cs
Normal file
25
GFramework.Core/Cqrs/LegacyCommandDispatchRequest.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using CoreCommand = GFramework.Core.Abstractions.Command;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 包装 legacy 无返回值命令,使其能够通过自有 CQRS runtime 调度。
|
||||
/// </summary>
|
||||
/// <param name="command">当前 bridge request 代理的 legacy 命令实例。</param>
|
||||
internal sealed class LegacyCommandDispatchRequest(CoreCommand.ICommand command)
|
||||
: LegacyCqrsDispatchRequestBase(ValidateCommand(command)), IRequest<Unit>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前 bridge request 代理的命令实例。
|
||||
/// </summary>
|
||||
public CoreCommand.ICommand Command { get; } = command;
|
||||
|
||||
private static CoreCommand.ICommand ValidateCommand(CoreCommand.ICommand command)
|
||||
{
|
||||
return command ?? throw new ArgumentNullException(nameof(command));
|
||||
}
|
||||
}
|
||||
22
GFramework.Core/Cqrs/LegacyCommandDispatchRequestHandler.cs
Normal file
22
GFramework.Core/Cqrs/LegacyCommandDispatchRequestHandler.cs
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 处理 legacy 无返回值命令的 bridge handler。
|
||||
/// </summary>
|
||||
internal sealed class LegacyCommandDispatchRequestHandler
|
||||
: LegacyCqrsDispatchHandlerBase, IRequestHandler<LegacyCommandDispatchRequest, Unit>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ValueTask<Unit> Handle(LegacyCommandDispatchRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
PrepareTarget(request.Command);
|
||||
request.Command.Execute();
|
||||
return ValueTask.FromResult(Unit.Value);
|
||||
}
|
||||
}
|
||||
23
GFramework.Core/Cqrs/LegacyCommandResultDispatchRequest.cs
Normal file
23
GFramework.Core/Cqrs/LegacyCommandResultDispatchRequest.cs
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 包装 legacy 带返回值命令,使其能够通过自有 CQRS runtime 调度。
|
||||
/// </summary>
|
||||
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 命令目标实例。</param>
|
||||
/// <param name="execute">封装 legacy 命令执行逻辑并返回装箱结果的委托。</param>
|
||||
internal sealed class LegacyCommandResultDispatchRequest(object target, Func<object?> execute)
|
||||
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
|
||||
{
|
||||
private readonly Func<object?> _execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
|
||||
/// <summary>
|
||||
/// 执行底层 legacy 命令并返回装箱后的结果。
|
||||
/// </summary>
|
||||
/// <returns>底层 legacy 命令执行后的装箱结果;若命令语义无返回值则为 <see langword="null" />。</returns>
|
||||
public object? Execute() => _execute();
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 处理 legacy 带返回值命令的 bridge handler。
|
||||
/// </summary>
|
||||
internal sealed class LegacyCommandResultDispatchRequestHandler
|
||||
: LegacyCqrsDispatchHandlerBase, IRequestHandler<LegacyCommandResultDispatchRequest, object?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ValueTask<object?> Handle(LegacyCommandResultDispatchRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
PrepareTarget(request.Target);
|
||||
return ValueTask.FromResult(request.Execute());
|
||||
}
|
||||
}
|
||||
33
GFramework.Core/Cqrs/LegacyCqrsDispatchHandlerBase.cs
Normal file
33
GFramework.Core/Cqrs/LegacyCqrsDispatchHandlerBase.cs
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
using GFramework.Core.Rule;
|
||||
|
||||
namespace GFramework.Core.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 为 legacy Core CQRS bridge handler 提供共享的上下文注入辅助逻辑。
|
||||
/// </summary>
|
||||
internal abstract class LegacyCqrsDispatchHandlerBase : ContextAwareBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 在执行 legacy 命令或查询前,把当前架构上下文显式注入给支持 <see cref="IContextAware" /> 的目标对象。
|
||||
/// </summary>
|
||||
/// <param name="target">即将执行的 legacy 目标对象。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="target" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 目标对象实现了 <see cref="IContextAware" />,但当前 handler 还没有可用的架构上下文。
|
||||
/// </exception>
|
||||
protected void PrepareTarget(object target)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
|
||||
if (target is IContextAware contextAware)
|
||||
{
|
||||
var context = Context ?? throw new InvalidOperationException(
|
||||
"Legacy CQRS bridge handler requires an active architecture context before executing a context-aware target.");
|
||||
contextAware.SetContext(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
116
GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs
Normal file
116
GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs
Normal file
@ -0,0 +1,116 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 为 legacy Core CQRS bridge 提供共享的上下文解析与同步兼容辅助逻辑。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 旧的同步 Command/Query 入口仍需要阻塞等待统一 <see cref="ICqrsRuntime" /> 返回结果。
|
||||
/// 这里统一通过 <see cref="Task.Run(System.Func{System.Threading.Tasks.Task})" /> 把等待动作切换到线程池,
|
||||
/// 避免直接占用调用方的 <see cref="SynchronizationContext" /> 导致 legacy 同步入口与异步 pipeline 互相卡死。
|
||||
/// </remarks>
|
||||
internal static class LegacyCqrsDispatchHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 解析当前 legacy 目标对象是否能够绑定到统一 CQRS runtime 的架构上下文。
|
||||
/// </summary>
|
||||
/// <param name="runtime">当前执行器可用的统一 CQRS runtime。</param>
|
||||
/// <param name="target">即将执行的 legacy 目标对象。</param>
|
||||
/// <param name="context">命中时返回可用于 CQRS runtime 的架构上下文。</param>
|
||||
/// <returns>
|
||||
/// 当 <paramref name="runtime" /> 可用且 <paramref name="target" /> 能稳定提供
|
||||
/// <see cref="IArchitectureContext" /> 时返回 <see langword="true" />;否则返回 <see langword="false" />。
|
||||
/// </returns>
|
||||
internal static bool TryResolveDispatchContext(
|
||||
[NotNullWhen(true)] ICqrsRuntime? runtime,
|
||||
object target,
|
||||
out IArchitectureContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
|
||||
context = null!;
|
||||
|
||||
if (runtime is null || target is not IContextAware contextAware)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
context = contextAware.GetContext();
|
||||
return true;
|
||||
}
|
||||
catch (InvalidOperationException exception) when (IsMissingContextException(exception))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断当前 <see cref="InvalidOperationException" /> 是否表示 legacy 目标尚未具备可桥接的架构上下文。
|
||||
/// </summary>
|
||||
/// <param name="exception">由 <see cref="IContextAware.GetContext" /> 抛出的异常。</param>
|
||||
/// <returns>
|
||||
/// 仅当异常明确表示“上下文尚未设置”或“当前没有活动上下文”时返回 <see langword="true" />;
|
||||
/// 其他运行时错误必须继续向上传播,避免把真实故障误判为可安全回退。
|
||||
/// </returns>
|
||||
private static bool IsMissingContextException(InvalidOperationException exception)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(exception);
|
||||
|
||||
return string.Equals(
|
||||
exception.Message,
|
||||
"Architecture context has not been set. Call SetContext before accessing the context.",
|
||||
StringComparison.Ordinal)
|
||||
|| string.Equals(
|
||||
exception.Message,
|
||||
"No active architecture context is currently bound.",
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同步等待统一 CQRS runtime 完成无返回值请求。
|
||||
/// </summary>
|
||||
/// <param name="runtime">负责分发当前请求的统一 CQRS runtime。</param>
|
||||
/// <param name="context">当前架构上下文。</param>
|
||||
/// <param name="request">要同步等待的请求。</param>
|
||||
internal static void SendSynchronously(
|
||||
ICqrsRuntime runtime,
|
||||
IArchitectureContext context,
|
||||
IRequest<Unit> request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
Task.Run(() => runtime.SendAsync(context, request).AsTask()).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同步等待统一 CQRS runtime 完成带返回值请求,并返回实际响应。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">请求响应类型。</typeparam>
|
||||
/// <param name="runtime">负责分发当前请求的统一 CQRS runtime。</param>
|
||||
/// <param name="context">当前架构上下文。</param>
|
||||
/// <param name="request">要同步等待的请求。</param>
|
||||
/// <returns>统一 CQRS runtime 返回的响应结果。</returns>
|
||||
internal static TResponse SendSynchronously<TResponse>(
|
||||
ICqrsRuntime runtime,
|
||||
IArchitectureContext context,
|
||||
IRequest<TResponse> request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
return Task.Run(() => runtime.SendAsync(context, request).AsTask()).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
16
GFramework.Core/Cqrs/LegacyCqrsDispatchRequestBase.cs
Normal file
16
GFramework.Core/Cqrs/LegacyCqrsDispatchRequestBase.cs
Normal file
@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
namespace GFramework.Core.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 为 legacy Command / Query 到自有 CQRS runtime 的桥接请求提供共享的目标对象封装。
|
||||
/// </summary>
|
||||
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 目标对象。</param>
|
||||
internal abstract class LegacyCqrsDispatchRequestBase(object target)
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前 bridge request 代理的 legacy 目标对象。
|
||||
/// </summary>
|
||||
public object Target { get; } = target ?? throw new ArgumentNullException(nameof(target));
|
||||
}
|
||||
23
GFramework.Core/Cqrs/LegacyQueryDispatchRequest.cs
Normal file
23
GFramework.Core/Cqrs/LegacyQueryDispatchRequest.cs
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 包装 legacy 同步查询,使其能够通过自有 CQRS runtime 调度。
|
||||
/// </summary>
|
||||
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 查询目标实例。</param>
|
||||
/// <param name="execute">封装 legacy 查询执行逻辑并返回装箱结果的委托。</param>
|
||||
internal sealed class LegacyQueryDispatchRequest(object target, Func<object?> execute)
|
||||
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
|
||||
{
|
||||
private readonly Func<object?> _execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
|
||||
/// <summary>
|
||||
/// 执行底层 legacy 查询并返回装箱后的结果。
|
||||
/// </summary>
|
||||
/// <returns>底层 legacy 查询执行后的装箱结果;若查询无返回值则为 <see langword="null" />。</returns>
|
||||
public object? Execute() => _execute();
|
||||
}
|
||||
21
GFramework.Core/Cqrs/LegacyQueryDispatchRequestHandler.cs
Normal file
21
GFramework.Core/Cqrs/LegacyQueryDispatchRequestHandler.cs
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 处理 legacy 同步查询的 bridge handler。
|
||||
/// </summary>
|
||||
internal sealed class LegacyQueryDispatchRequestHandler
|
||||
: LegacyCqrsDispatchHandlerBase, IRequestHandler<LegacyQueryDispatchRequest, object?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ValueTask<object?> Handle(LegacyQueryDispatchRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
PrepareTarget(request.Target);
|
||||
return ValueTask.FromResult(request.Execute());
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using GFramework.Core.Abstractions.Bases;
|
||||
using GFramework.Core.Abstractions.Ioc;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
@ -17,11 +18,143 @@ namespace GFramework.Core.Ioc;
|
||||
/// 将 Microsoft DI 包装为 IIocContainer 接口实现
|
||||
/// 提供线程安全的依赖注入容器功能
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该适配器负责维护服务注册表、冻结后的根 <see cref="IServiceProvider" /> 以及并发访问控制。
|
||||
/// 容器释放后会阻止任何进一步访问,并统一抛出 <see cref="ObjectDisposedException" />,
|
||||
/// 以避免 benchmark、测试宿主或短生命周期架构误用失效的 DI 状态。
|
||||
/// </remarks>
|
||||
/// <param name="serviceCollection">可选的IServiceCollection实例,默认创建新的ServiceCollection</param>
|
||||
public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) : ContextAwareBase, IIocContainer
|
||||
{
|
||||
#region Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// 抛出统一的容器释放异常,避免并发路径泄露底层锁类型的实现细节。
|
||||
/// </summary>
|
||||
/// <exception cref="ObjectDisposedException">始终抛出,且对象名固定为当前容器类型。</exception>
|
||||
private void ThrowDisposedException()
|
||||
{
|
||||
const string objectName = nameof(MicrosoftDiContainer);
|
||||
_logger.Warn("Attempted to use a disposed MicrosoftDiContainer.");
|
||||
throw new ObjectDisposedException(objectName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查容器是否已释放,避免访问已经失效的服务提供者与同步原语。
|
||||
/// </summary>
|
||||
/// <exception cref="ObjectDisposedException">当容器已释放时抛出。</exception>
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (!_disposed) return;
|
||||
ThrowDisposedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 进入读锁,并在获取锁前后都复核释放状态,确保等待中的线程也能稳定得到容器级异常。
|
||||
/// </summary>
|
||||
/// <exception cref="ObjectDisposedException">当容器已释放,或等待期间被其他线程释放时抛出。</exception>
|
||||
private void EnterReadLockOrThrowDisposed()
|
||||
{
|
||||
var lockTaken = false;
|
||||
|
||||
try
|
||||
{
|
||||
_lock.EnterReadLock();
|
||||
lockTaken = true;
|
||||
}
|
||||
catch (ObjectDisposedException) when (_disposed)
|
||||
{
|
||||
ThrowDisposedException();
|
||||
}
|
||||
|
||||
if (!_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (lockTaken)
|
||||
{
|
||||
_lock.ExitReadLock();
|
||||
}
|
||||
|
||||
ThrowDisposedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 进入写锁,并在获取锁前后都复核释放状态,确保等待中的线程不会泄露底层锁异常。
|
||||
/// </summary>
|
||||
/// <exception cref="ObjectDisposedException">当容器已释放,或等待期间被其他线程释放时抛出。</exception>
|
||||
private void EnterWriteLockOrThrowDisposed()
|
||||
{
|
||||
var lockTaken = false;
|
||||
|
||||
try
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
lockTaken = true;
|
||||
}
|
||||
catch (ObjectDisposedException) when (_disposed)
|
||||
{
|
||||
ThrowDisposedException();
|
||||
}
|
||||
|
||||
if (!_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (lockTaken)
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
|
||||
ThrowDisposedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在释放标志已经对外可见后,等待遗留 waiter 退场,再尝试释放底层锁。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 容器会先把 <see cref="_disposed" /> 置为 <see langword="true" /> 并退出写锁,
|
||||
/// 这样所有已在等待队列中的线程都能醒来并通过统一路径抛出容器级
|
||||
/// <see cref="ObjectDisposedException" />。只有当这些线程退场后,底层锁才可安全释放。
|
||||
/// 该步骤只允许一个释放调用者执行,避免并发 <see cref="Dispose" /> 重复销毁同一个
|
||||
/// <see cref="ReaderWriterLockSlim" /> 并破坏幂等契约。
|
||||
/// </remarks>
|
||||
private void DisposeLockWhenQuiescent()
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _lockDisposalStarted, 1, 0) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int maxDisposeSpinAttempts = 512;
|
||||
var spinWait = new SpinWait();
|
||||
|
||||
for (var attempt = 0; attempt < maxDisposeSpinAttempts; attempt++)
|
||||
{
|
||||
if (_lock.CurrentReadCount == 0 &&
|
||||
_lock.WaitingReadCount == 0 &&
|
||||
_lock.WaitingWriteCount == 0 &&
|
||||
_lock.WaitingUpgradeCount == 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
_lock.Dispose();
|
||||
return;
|
||||
}
|
||||
catch (SynchronizationLockException)
|
||||
{
|
||||
// 等待中的线程刚好在本轮检查后切换状态;继续自旋直到锁真正静默。
|
||||
}
|
||||
}
|
||||
|
||||
spinWait.SpinOnce();
|
||||
}
|
||||
|
||||
_logger.Warn("MicrosoftDiContainer lock disposal was skipped because waiters did not quiesce in time.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查容器是否已冻结,如果已冻结则抛出异常
|
||||
/// 用于保护注册操作的安全性
|
||||
@ -52,11 +185,27 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// </summary>
|
||||
private IServiceProvider? _provider;
|
||||
|
||||
/// <summary>
|
||||
/// 冻结后可复用的服务类型可见性索引。
|
||||
/// 容器冻结后注册集合不再变化,因此 <see cref="HasRegistration(Type)" /> 可以安全复用该索引。
|
||||
/// </summary>
|
||||
private FrozenServiceTypeIndex? _frozenServiceTypeIndex;
|
||||
|
||||
/// <summary>
|
||||
/// 容器冻结状态标志,true表示容器已冻结不可修改
|
||||
/// </summary>
|
||||
private volatile bool _frozen;
|
||||
|
||||
/// <summary>
|
||||
/// 容器释放状态标志,true 表示容器已释放,不允许继续访问。
|
||||
/// </summary>
|
||||
private volatile bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// 标记底层读写锁的销毁流程是否已经启动,确保并发释放时最多只有一个线程尝试销毁锁实例。
|
||||
/// </summary>
|
||||
private int _lockDisposalStarted;
|
||||
|
||||
/// <summary>
|
||||
/// 读写锁,确保多线程环境下的线程安全操作
|
||||
/// </summary>
|
||||
@ -85,8 +234,9 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// <exception cref="InvalidOperationException">当容器已冻结或类型已被注册时抛出</exception>
|
||||
public void RegisterSingleton<T>(T instance)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var type = typeof(T);
|
||||
_lock.EnterWriteLock();
|
||||
EnterWriteLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
ThrowIfFrozen();
|
||||
@ -119,7 +269,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
where TImpl : class, TService
|
||||
where TService : class
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
ThrowIfDisposed();
|
||||
EnterWriteLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
ThrowIfFrozen();
|
||||
@ -142,7 +293,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
where TImpl : class, TService
|
||||
where TService : class
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
ThrowIfDisposed();
|
||||
EnterWriteLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
ThrowIfFrozen();
|
||||
@ -165,7 +317,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
where TImpl : class, TService
|
||||
where TService : class
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
ThrowIfDisposed();
|
||||
EnterWriteLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
ThrowIfFrozen();
|
||||
@ -187,10 +340,11 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// <exception cref="InvalidOperationException">当容器已冻结时抛出</exception>
|
||||
public void RegisterPlurality(object instance)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var concreteType = instance.GetType();
|
||||
var interfaces = concreteType.GetInterfaces();
|
||||
|
||||
_lock.EnterWriteLock();
|
||||
EnterWriteLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
ThrowIfFrozen();
|
||||
@ -219,7 +373,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// </summary>
|
||||
public void RegisterPlurality<T>() where T : class
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
ThrowIfDisposed();
|
||||
EnterWriteLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
ThrowIfFrozen();
|
||||
@ -262,7 +417,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// <exception cref="InvalidOperationException">当容器已冻结时抛出</exception>
|
||||
public void Register<T>(T instance)
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
ThrowIfDisposed();
|
||||
EnterWriteLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
ThrowIfFrozen();
|
||||
@ -284,7 +440,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// <exception cref="InvalidOperationException">当容器已冻结时抛出</exception>
|
||||
public void Register(Type type, object instance)
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
ThrowIfDisposed();
|
||||
EnterWriteLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
ThrowIfFrozen();
|
||||
@ -307,7 +464,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
public void RegisterFactory<TService>(
|
||||
Func<IServiceProvider, TService> factory) where TService : class
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
ThrowIfDisposed();
|
||||
EnterWriteLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
ThrowIfFrozen();
|
||||
@ -328,40 +486,16 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
||||
public void RegisterCqrsPipelineBehavior<TBehavior>() where TBehavior : class
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
ThrowIfDisposed();
|
||||
EnterWriteLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
ThrowIfFrozen();
|
||||
|
||||
var behaviorType = typeof(TBehavior);
|
||||
|
||||
if (behaviorType.IsGenericTypeDefinition)
|
||||
{
|
||||
GetServicesUnsafe.AddSingleton(typeof(IPipelineBehavior<,>), behaviorType);
|
||||
}
|
||||
else
|
||||
{
|
||||
var pipelineInterfaces = behaviorType
|
||||
.GetInterfaces()
|
||||
.Where(type => type.IsGenericType &&
|
||||
type.GetGenericTypeDefinition() == typeof(IPipelineBehavior<,>))
|
||||
.ToList();
|
||||
|
||||
if (pipelineInterfaces.Count == 0)
|
||||
{
|
||||
var errorMessage = $"{behaviorType.Name} does not implement IPipelineBehavior<,>";
|
||||
_logger.Error(errorMessage);
|
||||
throw new InvalidOperationException(errorMessage);
|
||||
}
|
||||
|
||||
// 为每个已闭合的管道接口建立显式映射,支持针对特定请求/响应的专用行为。
|
||||
foreach (var pipelineInterface in pipelineInterfaces)
|
||||
{
|
||||
GetServicesUnsafe.AddSingleton(pipelineInterface, behaviorType);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug($"CQRS pipeline behavior registered: {behaviorType.Name}");
|
||||
RegisterCqrsPipelineBehaviorCore(
|
||||
typeof(TBehavior),
|
||||
typeof(IPipelineBehavior<,>),
|
||||
"IPipelineBehavior<,>",
|
||||
"pipeline behavior");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -369,6 +503,75 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册 CQRS 流式请求管道行为。
|
||||
/// 同时支持开放泛型行为类型和已闭合的具体行为类型,
|
||||
/// 以兼容通用行为和针对单一流式请求的专用行为两种注册方式。
|
||||
/// </summary>
|
||||
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
||||
public void RegisterCqrsStreamPipelineBehavior<TBehavior>() where TBehavior : class
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
EnterWriteLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
ThrowIfFrozen();
|
||||
RegisterCqrsPipelineBehaviorCore(
|
||||
typeof(TBehavior),
|
||||
typeof(IStreamPipelineBehavior<,>),
|
||||
"IStreamPipelineBehavior<,>",
|
||||
"stream pipeline behavior");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复用 CQRS 行为注册的开放泛型/封闭接口校验逻辑,
|
||||
/// 让 request 与 stream 两条入口保持一致的容器注册语义。
|
||||
/// </summary>
|
||||
/// <param name="behaviorType">待注册的行为运行时类型。</param>
|
||||
/// <param name="openGenericInterfaceType">行为必须实现的开放泛型接口类型。</param>
|
||||
/// <param name="interfaceTypeDisplayName">用于日志与异常的接口显示名称。</param>
|
||||
/// <param name="registrationLabel">用于日志的注册类别名称。</param>
|
||||
/// <exception cref="InvalidOperationException"><paramref name="behaviorType" /> 未实现目标行为接口。</exception>
|
||||
private void RegisterCqrsPipelineBehaviorCore(
|
||||
Type behaviorType,
|
||||
Type openGenericInterfaceType,
|
||||
string interfaceTypeDisplayName,
|
||||
string registrationLabel)
|
||||
{
|
||||
if (behaviorType.IsGenericTypeDefinition)
|
||||
{
|
||||
GetServicesUnsafe.AddSingleton(openGenericInterfaceType, behaviorType);
|
||||
}
|
||||
else
|
||||
{
|
||||
var pipelineInterfaces = behaviorType
|
||||
.GetInterfaces()
|
||||
.Where(type => type.IsGenericType &&
|
||||
type.GetGenericTypeDefinition() == openGenericInterfaceType)
|
||||
.ToList();
|
||||
|
||||
if (pipelineInterfaces.Count == 0)
|
||||
{
|
||||
var errorMessage = $"{behaviorType.Name} does not implement {interfaceTypeDisplayName}";
|
||||
_logger.Error(errorMessage);
|
||||
throw new InvalidOperationException(errorMessage);
|
||||
}
|
||||
|
||||
// 为每个已闭合的行为接口建立显式映射,支持针对特定请求/响应对的专用行为。
|
||||
foreach (var pipelineInterface in pipelineInterfaces)
|
||||
{
|
||||
GetServicesUnsafe.AddSingleton(pipelineInterface, behaviorType);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug($"CQRS {registrationLabel} registered: {behaviorType.Name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从指定程序集显式注册 CQRS 处理器。
|
||||
/// </summary>
|
||||
@ -392,6 +595,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(assemblies);
|
||||
ThrowIfDisposed();
|
||||
var assemblyArray = assemblies.ToArray();
|
||||
foreach (var assembly in assemblyArray)
|
||||
{
|
||||
@ -401,7 +605,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
}
|
||||
}
|
||||
|
||||
_lock.EnterWriteLock();
|
||||
EnterWriteLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
ThrowIfFrozen();
|
||||
@ -419,7 +623,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// <param name="configurator">服务配置委托</param>
|
||||
public void ExecuteServicesHook(Action<IServiceCollection>? configurator = null)
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
ThrowIfDisposed();
|
||||
EnterWriteLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
ThrowIfFrozen();
|
||||
@ -444,14 +649,17 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// <exception cref="InvalidOperationException">未找到可用的 CQRS 程序集注册协调器实例时抛出。</exception>
|
||||
private ICqrsRegistrationService ResolveCqrsRegistrationService()
|
||||
{
|
||||
var descriptor = GetServicesUnsafe.LastOrDefault(static service =>
|
||||
service.ServiceType == typeof(ICqrsRegistrationService));
|
||||
var registrationService = CollectRegisteredImplementationInstances(typeof(ICqrsRegistrationService))
|
||||
.OfType<ICqrsRegistrationService>()
|
||||
.LastOrDefault();
|
||||
|
||||
if (descriptor?.ImplementationInstance is ICqrsRegistrationService registrationService)
|
||||
if (registrationService != null)
|
||||
return registrationService;
|
||||
|
||||
const string errorMessage =
|
||||
"ICqrsRegistrationService not registered. Ensure the CQRS runtime module has been installed before registering handlers.";
|
||||
"ICqrsRegistrationService is not visible during the registration stage. Ensure the CQRS runtime module " +
|
||||
"has been installed and that the registration service is pre-materialized as an instance binding before " +
|
||||
"registering handlers.";
|
||||
_logger.Error(errorMessage);
|
||||
throw new InvalidOperationException(errorMessage);
|
||||
}
|
||||
@ -464,23 +672,13 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// <returns>服务实例或null</returns>
|
||||
public T? Get<T>() where T : class
|
||||
{
|
||||
_lock.EnterReadLock();
|
||||
ThrowIfDisposed();
|
||||
EnterReadLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
if (_provider == null)
|
||||
{
|
||||
// 如果容器未冻结,从服务集合中查找已注册的实例
|
||||
var serviceType = typeof(T);
|
||||
var descriptor = GetServicesUnsafe.FirstOrDefault(s =>
|
||||
s.ServiceType == serviceType || serviceType.IsAssignableFrom(s.ServiceType));
|
||||
|
||||
if (descriptor?.ImplementationInstance is T instance)
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
|
||||
// 在未冻结状态下无法调用工厂方法或创建实例,返回null
|
||||
return null;
|
||||
return CollectRegisteredImplementationInstances(typeof(T)).OfType<T>().FirstOrDefault();
|
||||
}
|
||||
|
||||
var result = _provider!.GetService<T>();
|
||||
@ -503,23 +701,23 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// <returns>服务实例或null</returns>
|
||||
public object? Get(Type type)
|
||||
{
|
||||
_lock.EnterReadLock();
|
||||
ArgumentNullException.ThrowIfNull(type);
|
||||
ThrowIfDisposed();
|
||||
EnterReadLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
if (_provider == null)
|
||||
{
|
||||
// 如果容器未冻结,从服务集合中查找已注册的实例
|
||||
var descriptor =
|
||||
GetServicesUnsafe.FirstOrDefault(s =>
|
||||
s.ServiceType == type || type.IsAssignableFrom(s.ServiceType));
|
||||
|
||||
return descriptor?.ImplementationInstance;
|
||||
return CollectRegisteredImplementationInstances(type).FirstOrDefault();
|
||||
}
|
||||
|
||||
var result = _provider!.GetService(type);
|
||||
_logger.Debug(result != null
|
||||
? $"Retrieved instance: {type.Name}"
|
||||
: $"No instance found for type: {type.Name}");
|
||||
if (_logger.IsDebugEnabled())
|
||||
{
|
||||
_logger.Debug(result != null
|
||||
? $"Retrieved instance: {type.Name}"
|
||||
: $"No instance found for type: {type.Name}");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
@ -593,7 +791,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// <returns>只读的服务实例列表</returns>
|
||||
public IReadOnlyList<T> GetAll<T>() where T : class
|
||||
{
|
||||
_lock.EnterReadLock();
|
||||
ThrowIfDisposed();
|
||||
EnterReadLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
if (_provider == null)
|
||||
@ -602,7 +801,10 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
}
|
||||
|
||||
var services = _provider!.GetServices<T>().ToList();
|
||||
_logger.Debug($"Retrieved {services.Count} instances of {typeof(T).Name}");
|
||||
if (_logger.IsDebugEnabled())
|
||||
{
|
||||
_logger.Debug($"Retrieved {services.Count} instances of {typeof(T).Name}");
|
||||
}
|
||||
return services;
|
||||
}
|
||||
finally
|
||||
@ -620,8 +822,9 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
public IReadOnlyList<object> GetAll(Type type)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(type);
|
||||
ThrowIfDisposed();
|
||||
|
||||
_lock.EnterReadLock();
|
||||
EnterReadLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
if (_provider == null)
|
||||
@ -630,7 +833,10 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
}
|
||||
|
||||
var services = _provider!.GetServices(type).ToList();
|
||||
_logger.Debug($"Retrieved {services.Count} instances of {type.Name}");
|
||||
if (_logger.IsDebugEnabled())
|
||||
{
|
||||
_logger.Debug($"Retrieved {services.Count} instances of {type.Name}");
|
||||
}
|
||||
return services.Where(o => o != null).Cast<object>().ToList();
|
||||
}
|
||||
finally
|
||||
@ -750,6 +956,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// <returns>排序后的只读服务实例列表</returns>
|
||||
public IReadOnlyList<T> GetAllSorted<T>(Comparison<T> comparison) where T : class
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var list = GetAll<T>().ToList();
|
||||
list.Sort(comparison);
|
||||
return list;
|
||||
@ -816,7 +1023,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// <returns>true表示包含该类型实例,false表示不包含</returns>
|
||||
public bool Contains<T>() where T : class
|
||||
{
|
||||
_lock.EnterReadLock();
|
||||
ThrowIfDisposed();
|
||||
EnterReadLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
if (_provider == null)
|
||||
@ -830,6 +1038,31 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查容器中是否存在可赋值给指定服务类型的注册项,而不要求先解析实例。
|
||||
/// </summary>
|
||||
/// <param name="type">要检查的服务类型。</param>
|
||||
/// <returns>若存在显式注册或开放泛型映射可满足该服务类型,则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
public bool HasRegistration(Type type)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(type);
|
||||
ThrowIfDisposed();
|
||||
EnterReadLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
if (_frozenServiceTypeIndex is not null)
|
||||
{
|
||||
return _frozenServiceTypeIndex.Contains(type);
|
||||
}
|
||||
|
||||
return HasRegistrationCore(type);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断容器中是否包含某个具体的实例对象
|
||||
/// 通过已注册实例集合进行快速查找
|
||||
@ -838,7 +1071,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// <returns>true表示包含该实例,false表示不包含</returns>
|
||||
public bool ContainsInstance(object instance)
|
||||
{
|
||||
_lock.EnterReadLock();
|
||||
ThrowIfDisposed();
|
||||
EnterReadLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
return _registeredInstances.Contains(instance);
|
||||
@ -849,13 +1083,60 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在当前容器状态下检查指定服务类型是否存在可见注册。
|
||||
/// </summary>
|
||||
/// <param name="requestedType">要检查的服务类型。</param>
|
||||
/// <returns>存在可满足该类型的注册时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
/// <remarks>
|
||||
/// 该检查只回答“是否可能解析到服务”,不会为了判断结果而激活实例。
|
||||
/// 预冻结阶段只基于当前服务描述符推断;冻结后则同样只观察描述符,
|
||||
/// 避免把瞬态/多实例解析成本混入热路径中的存在性判断。
|
||||
/// </remarks>
|
||||
private bool HasRegistrationCore(Type requestedType)
|
||||
{
|
||||
foreach (var descriptor in GetServicesUnsafe)
|
||||
{
|
||||
if (CanSatisfyServiceType(descriptor.ServiceType, requestedType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断某个服务描述符声明的服务类型是否能满足当前请求类型。
|
||||
/// </summary>
|
||||
/// <param name="registeredServiceType">注册时声明的服务类型。</param>
|
||||
/// <param name="requestedType">调用方请求的服务类型。</param>
|
||||
/// <returns>若当前注册可用于解析 <paramref name="requestedType" />,则返回 <see langword="true" />。</returns>
|
||||
private static bool CanSatisfyServiceType(Type registeredServiceType, Type requestedType)
|
||||
{
|
||||
// 这里刻意与 Get/GetAll 的“按服务键解析”语义保持一致:
|
||||
// 只有注册时声明的服务类型本身命中,或开放泛型服务键能闭合到请求类型时,才视为存在可见注册。
|
||||
if (registeredServiceType == requestedType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (requestedType.IsConstructedGenericType && registeredServiceType.IsGenericTypeDefinition)
|
||||
{
|
||||
return requestedType.GetGenericTypeDefinition() == registeredServiceType;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空容器中的所有实例和服务注册
|
||||
/// 只有在容器未冻结状态下才能执行清空操作
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
ThrowIfDisposed();
|
||||
EnterWriteLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
// 冻结的容器不允许清空操作
|
||||
@ -865,9 +1146,12 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
return;
|
||||
}
|
||||
|
||||
// 未冻结的容器不会构建根 ServiceProvider,因此这里仅重置注册状态即可。
|
||||
GetServicesUnsafe.Clear();
|
||||
_registeredInstances.Clear();
|
||||
_provider = null;
|
||||
_frozenServiceTypeIndex = null;
|
||||
_frozen = false;
|
||||
_logger.Info("Container cleared");
|
||||
}
|
||||
finally
|
||||
@ -882,7 +1166,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// </summary>
|
||||
public void Freeze()
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
ThrowIfDisposed();
|
||||
EnterWriteLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
// 防止重复冻结
|
||||
@ -893,6 +1178,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
}
|
||||
|
||||
_provider = GetServicesUnsafe.BuildServiceProvider();
|
||||
_frozenServiceTypeIndex = FrozenServiceTypeIndex.Create(GetServicesUnsafe);
|
||||
_frozen = true;
|
||||
_logger.Info("IOC Container frozen - ServiceProvider built");
|
||||
}
|
||||
@ -902,6 +1188,59 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存冻结后按服务键可见的精确服务类型与开放泛型定义集合。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该索引只回答“按当前服务键语义是否可见”,因此与 <see cref="Get(Type)" /> /
|
||||
/// <see cref="GetAll(Type)" /> 一样不会退化为更宽松的可赋值匹配。
|
||||
/// </remarks>
|
||||
private sealed class FrozenServiceTypeIndex(HashSet<Type> exactServiceTypes, HashSet<Type> openGenericServiceTypes)
|
||||
{
|
||||
private readonly HashSet<Type> _exactServiceTypes = exactServiceTypes;
|
||||
private readonly HashSet<Type> _openGenericServiceTypes = openGenericServiceTypes;
|
||||
|
||||
/// <summary>
|
||||
/// 基于冻结时最终确定的服务描述符集合创建索引。
|
||||
/// </summary>
|
||||
/// <param name="descriptors">冻结时的服务描述符序列。</param>
|
||||
/// <returns>供存在性判断热路径复用的服务键索引。</returns>
|
||||
public static FrozenServiceTypeIndex Create(IEnumerable<ServiceDescriptor> descriptors)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(descriptors);
|
||||
|
||||
var exactServiceTypes = new HashSet<Type>();
|
||||
var openGenericServiceTypes = new HashSet<Type>();
|
||||
|
||||
foreach (var descriptor in descriptors)
|
||||
{
|
||||
var serviceType = descriptor.ServiceType;
|
||||
exactServiceTypes.Add(serviceType);
|
||||
|
||||
if (serviceType.IsGenericTypeDefinition)
|
||||
{
|
||||
openGenericServiceTypes.Add(serviceType);
|
||||
}
|
||||
}
|
||||
|
||||
return new FrozenServiceTypeIndex(exactServiceTypes, openGenericServiceTypes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断当前索引是否声明了目标服务键。
|
||||
/// </summary>
|
||||
/// <param name="requestedType">要检查的服务类型。</param>
|
||||
/// <returns>命中精确服务键或可闭合的开放泛型服务键时返回 <see langword="true" />。</returns>
|
||||
public bool Contains(Type requestedType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(requestedType);
|
||||
|
||||
return _exactServiceTypes.Contains(requestedType) ||
|
||||
requestedType.IsConstructedGenericType &&
|
||||
_openGenericServiceTypes.Contains(requestedType.GetGenericTypeDefinition());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取底层的服务集合
|
||||
/// 提供对内部IServiceCollection的访问权限,用于高级配置和自定义操作
|
||||
@ -917,7 +1256,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// <exception cref="InvalidOperationException">当容器未冻结时抛出</exception>
|
||||
public IServiceScope CreateScope()
|
||||
{
|
||||
_lock.EnterReadLock();
|
||||
ThrowIfDisposed();
|
||||
EnterReadLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
// 在锁内检查,避免竞态条件
|
||||
@ -938,5 +1278,59 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放容器持有的服务提供者、注册状态和同步原语。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 冻结后的根 <see cref="IServiceProvider" /> 会拥有 DI 创建的单例与作用域根缓存,因此 benchmark、
|
||||
/// 测试宿主或短生命周期架构在结束时需要显式释放容器,避免这些对象与内部
|
||||
/// <see cref="ReaderWriterLockSlim" /> 一起滞留。
|
||||
/// 释放是幂等的;首次释放后所有后续访问都会抛出 <see cref="ObjectDisposedException" />。
|
||||
/// </remarks>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var lockTaken = false;
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
lockTaken = true;
|
||||
}
|
||||
catch (ObjectDisposedException) when (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
(_provider as IDisposable)?.Dispose();
|
||||
_provider = null;
|
||||
_frozenServiceTypeIndex = null;
|
||||
GetServicesUnsafe.Clear();
|
||||
_registeredInstances.Clear();
|
||||
_frozen = false;
|
||||
_logger.Info("IOC Container disposed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (lockTaken)
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
DisposeLockWhenQuiescent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
6
GFramework.Core/Properties/AssemblyInfo.cs
Normal file
6
GFramework.Core/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,6 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("GFramework.Core.Tests")]
|
||||
@ -2,14 +2,23 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Query;
|
||||
using GFramework.Core.Cqrs;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Query;
|
||||
|
||||
/// <summary>
|
||||
/// 异步查询总线实现,用于处理异步查询请求
|
||||
/// </summary>
|
||||
public sealed class AsyncQueryExecutor : IAsyncQueryExecutor
|
||||
public sealed class AsyncQueryExecutor(ICqrsRuntime? runtime = null) : IAsyncQueryExecutor
|
||||
{
|
||||
private readonly ICqrsRuntime? _runtime = runtime;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前执行器是否已接入统一 CQRS runtime。
|
||||
/// </summary>
|
||||
public bool UsesCqrsRuntime => _runtime is not null;
|
||||
|
||||
/// <summary>
|
||||
/// 异步发送查询请求并返回结果
|
||||
/// </summary>
|
||||
@ -18,8 +27,38 @@ public sealed class AsyncQueryExecutor : IAsyncQueryExecutor
|
||||
/// <returns>包含查询结果的异步任务</returns>
|
||||
public Task<TResult> SendAsync<TResult>(IAsyncQuery<TResult> query)
|
||||
{
|
||||
// 验证查询参数不为空
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
var cqrsRuntime = _runtime;
|
||||
|
||||
if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, query, out var context))
|
||||
{
|
||||
return BridgeAsyncQueryAsync(cqrsRuntime, context, query);
|
||||
}
|
||||
|
||||
return query.DoAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过统一 CQRS runtime 异步执行 legacy 查询,并把装箱结果还原为目标类型。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">查询结果类型。</typeparam>
|
||||
/// <param name="runtime">负责调度当前 bridge request 的统一 CQRS runtime。</param>
|
||||
/// <param name="context">当前架构上下文。</param>
|
||||
/// <param name="query">要桥接的 legacy 查询。</param>
|
||||
/// <returns>查询执行结果。</returns>
|
||||
private static async Task<TResult> BridgeAsyncQueryAsync<TResult>(
|
||||
ICqrsRuntime runtime,
|
||||
GFramework.Core.Abstractions.Architectures.IArchitectureContext context,
|
||||
IAsyncQuery<TResult> query)
|
||||
{
|
||||
var boxedResult = await runtime.SendAsync(
|
||||
context,
|
||||
new LegacyAsyncQueryDispatchRequest(
|
||||
query,
|
||||
async () => await query.DoAsync().ConfigureAwait(false)))
|
||||
.ConfigureAwait(false);
|
||||
return (TResult)boxedResult!;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Query;
|
||||
using GFramework.Core.Cqrs;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Query;
|
||||
|
||||
@ -10,21 +12,47 @@ namespace GFramework.Core.Query;
|
||||
/// QueryExecutor 类负责执行查询操作,实现 IQueryExecutor 接口。
|
||||
/// 该类是密封的,防止被继承。
|
||||
/// </summary>
|
||||
public sealed class QueryExecutor : IQueryExecutor
|
||||
public sealed class QueryExecutor(ICqrsRuntime? runtime = null) : IQueryExecutor
|
||||
{
|
||||
private readonly ICqrsRuntime? _runtime = runtime;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前执行器是否已接入统一 CQRS runtime。
|
||||
/// </summary>
|
||||
public bool UsesCqrsRuntime => _runtime is not null;
|
||||
|
||||
/// <summary>
|
||||
/// 执行指定的查询并返回结果。
|
||||
/// 该方法通过调用查询对象的 Do 方法来获取结果。
|
||||
/// 当查询对象携带可用的架构上下文且执行器已接入统一 runtime 时,
|
||||
/// 该方法会先把 legacy 查询包装成内部 request 并交给 <see cref="ICqrsRuntime" />,
|
||||
/// 以复用统一的 dispatch / pipeline 入口;否则回退到 legacy 直接执行。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">查询结果的类型。</typeparam>
|
||||
/// <param name="query">要执行的查询对象,必须实现 IQuery<TResult> 接口。</param>
|
||||
/// <returns>查询执行的结果,类型为 TResult。</returns>
|
||||
/// <returns>查询执行成功后还原出的 <typeparamref name="TResult" /> 结果。</returns>
|
||||
/// <exception cref="NullReferenceException">
|
||||
/// 统一 CQRS runtime 返回 <see langword="null" />,但 <typeparamref name="TResult" /> 为值类型。
|
||||
/// </exception>
|
||||
/// <exception cref="InvalidCastException">
|
||||
/// 统一 CQRS runtime 返回的装箱结果无法转换为 <typeparamref name="TResult" />。
|
||||
/// </exception>
|
||||
public TResult Send<TResult>(IQuery<TResult> query)
|
||||
{
|
||||
// 验证查询参数不为 null,如果为 null 则抛出 ArgumentNullException 异常
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
// 调用查询对象的 Do 方法执行查询并返回结果
|
||||
var cqrsRuntime = _runtime;
|
||||
|
||||
if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, query, out var context))
|
||||
{
|
||||
var boxedResult = LegacyCqrsDispatchHelper.SendSynchronously(
|
||||
cqrsRuntime,
|
||||
context,
|
||||
new LegacyQueryDispatchRequest(
|
||||
query,
|
||||
() => query.Do()));
|
||||
return (TResult)boxedResult!;
|
||||
}
|
||||
|
||||
return query.Do();
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,9 @@
|
||||
- 资源、对象池、日志、协程、并发、环境、配置与本地化
|
||||
- 服务模块管理、时间提供器与默认的 IoC 容器适配
|
||||
|
||||
标准架构启动路径下,旧 `Command` / `Query` 兼容入口现在会继续保持原有使用方式,
|
||||
但底层会通过 `GFramework.Cqrs` 的统一 runtime、pipeline 与上下文注入链路执行。
|
||||
|
||||
它不负责:
|
||||
|
||||
- 游戏内容配置、Scene / UI / Storage 等游戏层能力
|
||||
|
||||
@ -46,8 +46,10 @@ public abstract class ContextAwareBase : IContextAware
|
||||
/// </summary>
|
||||
/// <returns>当前架构上下文对象。</returns>
|
||||
/// <remarks>
|
||||
/// 当 <see cref="Context" /> 为空时,该实现会直接回退到 <see cref="GameContext.GetFirstArchitectureContext" />。
|
||||
/// 当 <see cref="Context" /> 为空时,该实现会直接回退到 <see cref="GameContext.GetFirstArchitectureContext" /> 返回的当前活动上下文。
|
||||
/// 该回退过程不执行额外同步,也不支持替换 provider;如需这些能力,请改用生成的 ContextAware 实现。
|
||||
/// 一旦回退结果被写入 <see cref="Context" />,后续即使关联架构解除 <see cref="GameContext" /> 绑定,
|
||||
/// 该实例仍会保留原引用,调用方需要自行约束其生命周期或改用支持 provider 协调的生成实现。
|
||||
/// </remarks>
|
||||
IArchitectureContext IContextAware.GetContext()
|
||||
{
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Ioc;
|
||||
using GFramework.Core.Query;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Services.Modules;
|
||||
|
||||
@ -32,10 +33,19 @@ public sealed class AsyncQueryExecutorModule : IServiceModule
|
||||
/// 注册异步查询执行器到依赖注入容器。
|
||||
/// 创建异步查询执行器实例并将其注册为多例服务。
|
||||
/// </summary>
|
||||
/// <param name="container">依赖注入容器实例。</param>
|
||||
/// <param name="container">承载异步查询执行器与 CQRS runtime 的依赖注入容器实例。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 容器中尚未注册唯一的 <see cref="ICqrsRuntime" /> 实例,无法构建统一 runtime 版本的异步查询执行器。
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// 该模块会在注册阶段立即解析 <see cref="ICqrsRuntime" />,因此
|
||||
/// <see cref="CqrsRuntimeModule" /> 必须先于当前模块完成注册。
|
||||
/// </remarks>
|
||||
public void Register(IIocContainer container)
|
||||
{
|
||||
container.RegisterPlurality(new AsyncQueryExecutor());
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
container.RegisterPlurality(new AsyncQueryExecutor(container.GetRequired<ICqrsRuntime>()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -55,4 +65,4 @@ public sealed class AsyncQueryExecutorModule : IServiceModule
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Ioc;
|
||||
using GFramework.Core.Command;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Services.Modules;
|
||||
|
||||
@ -32,10 +33,19 @@ public sealed class CommandExecutorModule : IServiceModule
|
||||
/// 注册命令执行器到依赖注入容器。
|
||||
/// 创建命令执行器实例并将其注册为多例服务。
|
||||
/// </summary>
|
||||
/// <param name="container">依赖注入容器实例。</param>
|
||||
/// <param name="container">承载命令执行器与 CQRS runtime 的依赖注入容器实例。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 容器中尚未注册唯一的 <see cref="ICqrsRuntime" /> 实例,无法构建统一 runtime 版本的命令执行器。
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// 该模块会在注册阶段立即解析 <see cref="ICqrsRuntime" />,因此
|
||||
/// <see cref="CqrsRuntimeModule" /> 必须先于当前模块完成注册。
|
||||
/// </remarks>
|
||||
public void Register(IIocContainer container)
|
||||
{
|
||||
container.RegisterPlurality(new CommandExecutor());
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
container.RegisterPlurality(new CommandExecutor(container.GetRequired<ICqrsRuntime>()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -55,4 +65,4 @@ public sealed class CommandExecutorModule : IServiceModule
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ using GFramework.Core.Abstractions.Ioc;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Cqrs;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using GFramework.Cqrs.Notification;
|
||||
using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime;
|
||||
|
||||
namespace GFramework.Core.Services.Modules;
|
||||
@ -46,8 +45,7 @@ public sealed class CqrsRuntimeModule : IServiceModule
|
||||
var dispatcherLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher");
|
||||
var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar");
|
||||
var registrationLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsRegistrationService");
|
||||
var notificationPublisher = container.Get<INotificationPublisher>();
|
||||
var runtime = CqrsRuntimeFactory.CreateRuntime(container, dispatcherLogger, notificationPublisher);
|
||||
var runtime = CqrsRuntimeFactory.CreateRuntime(container, dispatcherLogger);
|
||||
var registrar = CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger);
|
||||
|
||||
container.Register(runtime);
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Ioc;
|
||||
using GFramework.Core.Query;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Services.Modules;
|
||||
|
||||
@ -32,10 +33,19 @@ public sealed class QueryExecutorModule : IServiceModule
|
||||
/// 注册查询执行器到依赖注入容器。
|
||||
/// 创建查询执行器实例并将其注册为多例服务。
|
||||
/// </summary>
|
||||
/// <param name="container">依赖注入容器实例。</param>
|
||||
/// <param name="container">承载查询执行器与 CQRS runtime 的依赖注入容器实例。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 容器中尚未注册唯一的 <see cref="ICqrsRuntime" /> 实例,无法构建统一 runtime 版本的查询执行器。
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// 该模块会在注册阶段立即解析 <see cref="ICqrsRuntime" />,因此
|
||||
/// <see cref="CqrsRuntimeModule" /> 必须先于当前模块完成注册。
|
||||
/// </remarks>
|
||||
public void Register(IIocContainer container)
|
||||
{
|
||||
container.RegisterPlurality(new QueryExecutor());
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
container.RegisterPlurality(new QueryExecutor(container.GetRequired<ICqrsRuntime>()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -55,4 +65,4 @@ public sealed class QueryExecutorModule : IServiceModule
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,9 @@ public interface ICqrsRuntime
|
||||
/// <remarks>
|
||||
/// 该契约允许调用方传入任意 <see cref="ICqrsContext" />,
|
||||
/// 但默认运行时在需要向处理器或行为注入框架上下文时,仍要求该上下文同时实现 <c>IArchitectureContext</c>。
|
||||
/// 为了兼容 legacy 同步入口,<c>ArchitectureContext</c>、<c>QueryExecutor</c> 与 <c>CommandExecutor</c>
|
||||
/// 可能会在后台线程上同步等待该异步结果;实现者与 pipeline 行为不应依赖调用方的
|
||||
/// <see cref="SynchronizationContext" />,并应优先在内部异步链路上使用 <c>ConfigureAwait(false)</c>。
|
||||
/// </remarks>
|
||||
ValueTask<TResponse> SendAsync<TResponse>(
|
||||
ICqrsContext context,
|
||||
|
||||
25
GFramework.Cqrs.Abstractions/Cqrs/IStreamPipelineBehavior.cs
Normal file
25
GFramework.Cqrs.Abstractions/Cqrs/IStreamPipelineBehavior.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
namespace GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 定义流式 CQRS 请求在建流阶段使用的管道行为。
|
||||
/// </summary>
|
||||
/// <typeparam name="TRequest">流式请求类型。</typeparam>
|
||||
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
|
||||
public interface IStreamPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : IStreamRequest<TResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理当前流式请求,并决定是否继续调用后续行为或最终处理器。
|
||||
/// </summary>
|
||||
/// <param name="message">当前流式请求消息。</param>
|
||||
/// <param name="next">下一个处理委托。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>异步响应序列。</returns>
|
||||
IAsyncEnumerable<TResponse> Handle(
|
||||
TRequest message,
|
||||
StreamMessageHandlerDelegate<TRequest, TResponse> next,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
namespace GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 表示流式 CQRS 请求在管道中继续向下执行的处理委托。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>stream 行为可以通过不调用该委托来短路整个流式处理链。</para>
|
||||
/// <para>除显式实现重试、回放或分支等高级语义外,行为通常应最多调用一次该委托,以维持单次建流的确定性。</para>
|
||||
/// <para>调用方应传递当前收到的 <paramref name="cancellationToken" />,确保取消信号沿建流入口与后续枚举链路一致传播。</para>
|
||||
/// </remarks>
|
||||
/// <typeparam name="TRequest">流式请求类型。</typeparam>
|
||||
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
|
||||
/// <param name="message">当前流式请求消息。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>异步响应序列。</returns>
|
||||
public delegate IAsyncEnumerable<TResponse> StreamMessageHandlerDelegate<in TRequest, out TResponse>(
|
||||
TRequest message,
|
||||
CancellationToken cancellationToken)
|
||||
where TRequest : IStreamRequest<TResponse>;
|
||||
@ -19,7 +19,7 @@
|
||||
推荐按职责引用:
|
||||
|
||||
- `GeWuYou.GFramework.Cqrs.Abstractions`
|
||||
- 提供 `IRequest<TResponse>`、`INotification`、`IStreamRequest<TResponse>`、`IRequestHandler<,>`、`INotificationHandler<>`、`IPipelineBehavior<,>`、`ICqrsRuntime`、`ICqrsContext`、`Unit` 等基础契约。
|
||||
- 提供 `IRequest<TResponse>`、`INotification`、`IStreamRequest<TResponse>`、`IRequestHandler<,>`、`INotificationHandler<>`、`IPipelineBehavior<,>`、`IStreamPipelineBehavior<,>`、`ICqrsRuntime`、`ICqrsContext`、`Unit` 等基础契约。
|
||||
- `GeWuYou.GFramework.Cqrs`
|
||||
- 引用本包,并提供默认 runtime、处理器注册、消息基类、处理器基类、上下文扩展方法。
|
||||
- `GeWuYou.GFramework.Cqrs.SourceGenerators`
|
||||
@ -38,7 +38,7 @@
|
||||
- 运行时协作接口
|
||||
- `ICqrsRuntime`、`ICqrsContext`、`ICqrsHandlerRegistrar`
|
||||
- 管道与辅助类型
|
||||
- `IPipelineBehavior<,>`、`MessageHandlerDelegate<,>`、`Unit`
|
||||
- `IPipelineBehavior<,>`、`IStreamPipelineBehavior<,>`、`MessageHandlerDelegate<,>`、`StreamMessageHandlerDelegate<,>`、`Unit`
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
|
||||
71
GFramework.Cqrs.Benchmarks/CustomColumn.cs
Normal file
71
GFramework.Cqrs.Benchmarks/CustomColumn.cs
Normal file
@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using BenchmarkDotNet.Columns;
|
||||
using BenchmarkDotNet.Reports;
|
||||
using BenchmarkDotNet.Running;
|
||||
using System;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks;
|
||||
|
||||
/// <summary>
|
||||
/// 为 CQRS benchmark 结果补充可读的场景标签列。
|
||||
/// </summary>
|
||||
/// <param name="columnName">列名。</param>
|
||||
/// <param name="getValue">从 benchmark case 提取列值的委托。</param>
|
||||
public sealed class CustomColumn(string columnName, Func<Summary, BenchmarkCase, string> getValue) : IColumn
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Id => $"{nameof(CustomColumn)}.{ColumnName}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ColumnName { get; } = columnName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool AlwaysShow => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ColumnCategory Category => ColumnCategory.Params;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int PriorityInCategory => 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsNumeric => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public UnitType UnitType => UnitType.Dimensionless;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Legend => $"Custom '{ColumnName}' tag column";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsAvailable(Summary summary)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetValue(Summary summary, BenchmarkCase benchmarkCase)
|
||||
{
|
||||
return getValue(summary, benchmarkCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style)
|
||||
{
|
||||
return GetValue(summary, benchmarkCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return ColumnName;
|
||||
}
|
||||
}
|
||||
38
GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj
Normal file
38
GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj
Normal file
@ -0,0 +1,38 @@
|
||||
<!--
|
||||
Copyright (c) 2025-2026 GeWuYou
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<!-- Keep benchmark infrastructure out of the published NuGet package set. -->
|
||||
<IsPackable>false</IsPackable>
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
|
||||
<PackageReference Include="Mediator.Abstractions" Version="3.0.2" />
|
||||
<PackageReference Include="Mediator.SourceGenerator" Version="3.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MediatR" Version="13.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GFramework.Cqrs.Abstractions\GFramework.Cqrs.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\GFramework.Cqrs\GFramework.Cqrs.csproj" />
|
||||
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.ExceptionServices;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 统一处理 benchmark 宿主的资源释放,避免前一个 <see cref="IDisposable" /> 抛错后中断后续清理。
|
||||
/// </summary>
|
||||
internal static class BenchmarkCleanupHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 按顺序释放一组 benchmark 资源,并在全部资源都尝试释放后再回抛异常。
|
||||
/// </summary>
|
||||
/// <param name="disposables">当前 benchmark 宿主拥有并负责释放的资源。</param>
|
||||
/// <exception cref="Exception">
|
||||
/// 当且仅当至少一个资源释放失败时抛出。
|
||||
/// 单个失败会回抛原始异常,多个失败会聚合为 <see cref="AggregateException" />。
|
||||
/// </exception>
|
||||
public static void DisposeAll(params IDisposable?[] disposables)
|
||||
{
|
||||
List<Exception>? exceptions = null;
|
||||
|
||||
foreach (var disposable in disposables)
|
||||
{
|
||||
if (disposable is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
exceptions ??= [];
|
||||
exceptions.Add(exception);
|
||||
}
|
||||
}
|
||||
|
||||
if (exceptions is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (exceptions.Count == 1)
|
||||
{
|
||||
ExceptionDispatchInfo.Capture(exceptions[0]).Throw();
|
||||
}
|
||||
|
||||
throw new AggregateException("One or more benchmark resources failed to dispose cleanly.", exceptions);
|
||||
}
|
||||
}
|
||||
17
GFramework.Cqrs.Benchmarks/Messaging/BenchmarkContext.cs
Normal file
17
GFramework.Cqrs.Benchmarks/Messaging/BenchmarkContext.cs
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 为纯 runtime benchmark 提供最小 CQRS 上下文标记,避免把完整架构上下文初始化成本混入 steady-state dispatch。
|
||||
/// </summary>
|
||||
internal sealed class BenchmarkContext : ICqrsContext
|
||||
{
|
||||
/// <summary>
|
||||
/// 共享的最小 CQRS 上下文实例。
|
||||
/// </summary>
|
||||
public static BenchmarkContext Instance { get; } = new();
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 提供 benchmark 共享的 dispatcher 静态缓存清理入口。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// `GFramework.Cqrs` runtime 会把反射绑定与 generated invoker 元数据缓存在静态字段中。
|
||||
/// benchmark 需要在同一进程内重复比较 cold-start、reflection 与 generated 路径时,
|
||||
/// 显式清空这些缓存,避免前一组 benchmark 污染后续结果。
|
||||
/// </remarks>
|
||||
internal static class BenchmarkDispatcherCacheHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 清空 dispatcher 上与 benchmark 对照相关的全部静态缓存。
|
||||
/// </summary>
|
||||
public static void ClearDispatcherCaches()
|
||||
{
|
||||
ClearDispatcherCache("NotificationDispatchBindings");
|
||||
ClearDispatcherCache("RequestDispatchBindings");
|
||||
ClearDispatcherCache("StreamDispatchBindings");
|
||||
ClearDispatcherCache("GeneratedRequestInvokers");
|
||||
ClearDispatcherCache("GeneratedStreamInvokers");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过反射定位并清空 dispatcher 的指定缓存字段。
|
||||
/// </summary>
|
||||
/// <param name="fieldName">要清理的静态缓存字段名。</param>
|
||||
/// <exception cref="InvalidOperationException">指定缓存字段不存在、返回空值或未暴露清理方法。</exception>
|
||||
internal static void ClearDispatcherCache(string fieldName)
|
||||
{
|
||||
var field = typeof(GFramework.Cqrs.CqrsRuntimeFactory).Assembly
|
||||
.GetType("GFramework.Cqrs.Internal.CqrsDispatcher", throwOnError: true)!
|
||||
.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException($"Missing dispatcher cache field {fieldName}.");
|
||||
var cache = field.GetValue(null)
|
||||
?? throw new InvalidOperationException($"Dispatcher cache field {fieldName} returned null.");
|
||||
var clearMethod = cache.GetType().GetMethod("Clear", BindingFlags.Public | BindingFlags.Instance)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Dispatcher cache field {fieldName} does not expose a Clear method.");
|
||||
_ = clearMethod.Invoke(cache, null);
|
||||
}
|
||||
}
|
||||
194
GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs
Normal file
194
GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs
Normal file
@ -0,0 +1,194 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Ioc;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using GFramework.Cqrs.Internal;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 为 benchmark 场景构建最小且可重复的 GFramework / MediatR 对照宿主。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 基准工程里的对照目标是“相同消息合同下的调度差异”,而不是程序集扫描量或容器生命周期差异。
|
||||
/// 因此这里统一封装两类宿主的最小注册形状,确保:
|
||||
/// 1. GFramework 容器在首次发送前已经冻结,可真实解析按类型注册的 handler;
|
||||
/// 2. MediatR 只扫描当前 benchmark 明确拥有的 handler / behavior 类型,避免整个程序集的额外注册污染结果。
|
||||
/// </remarks>
|
||||
internal static class BenchmarkHostFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建一个已经冻结的 GFramework benchmark 容器。
|
||||
/// </summary>
|
||||
/// <param name="configure">向容器写入 benchmark 所需 handler / pipeline 的注册动作。</param>
|
||||
/// <returns>已冻结、可立即用于 runtime 分发的容器。</returns>
|
||||
internal static MicrosoftDiContainer CreateFrozenGFrameworkContainer(Action<MicrosoftDiContainer> configure)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
|
||||
var container = new MicrosoftDiContainer();
|
||||
RegisterCqrsInfrastructure(container);
|
||||
configure(container);
|
||||
container.Freeze();
|
||||
return container;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 benchmark 宿主补齐默认 CQRS runtime seam,确保它既能手工注册 handler,也能走真实的程序集注册入口。
|
||||
/// </summary>
|
||||
/// <param name="container">当前 benchmark 拥有的 GFramework 容器。</param>
|
||||
/// <remarks>
|
||||
/// `RegisterCqrsHandlersFromAssembly(...)` 依赖预先可见的 runtime / registrar / registration service 实例绑定。
|
||||
/// benchmark 宿主直接使用裸 <see cref="MicrosoftDiContainer" />,因此需要在配置阶段先补齐这组基础设施,
|
||||
/// 避免各个 benchmark 用例各自复制同一段前置接线逻辑。
|
||||
/// </remarks>
|
||||
private static void RegisterCqrsInfrastructure(MicrosoftDiContainer container)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
if (container.Get<ICqrsRuntime>() is null)
|
||||
{
|
||||
var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher");
|
||||
var notificationPublisher = container.Get<GFramework.Cqrs.Notification.INotificationPublisher>();
|
||||
var runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, runtimeLogger, notificationPublisher);
|
||||
container.Register(runtime);
|
||||
RegisterLegacyRuntimeAlias(container, runtime);
|
||||
}
|
||||
else if (container.Get<LegacyICqrsRuntime>() is null)
|
||||
{
|
||||
RegisterLegacyRuntimeAlias(container, container.GetRequired<ICqrsRuntime>());
|
||||
}
|
||||
|
||||
if (container.Get<ICqrsHandlerRegistrar>() is null)
|
||||
{
|
||||
var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar");
|
||||
var registrar = GFramework.Cqrs.CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger);
|
||||
container.Register<ICqrsHandlerRegistrar>(registrar);
|
||||
}
|
||||
|
||||
if (container.Get<ICqrsRegistrationService>() is null)
|
||||
{
|
||||
var registrationLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsRegistrationService");
|
||||
var registrar = container.GetRequired<ICqrsHandlerRegistrar>();
|
||||
var registrationService = GFramework.Cqrs.CqrsRuntimeFactory.CreateRegistrationService(registrar, registrationLogger);
|
||||
container.Register<ICqrsRegistrationService>(registrationService);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 只激活当前 benchmark 场景明确拥有的 generated registry,避免同一程序集里的其他 benchmark registry
|
||||
/// 扩大冻结后服务索引与 dispatcher descriptor 基线。
|
||||
/// </summary>
|
||||
/// <typeparam name="TRegistry">当前 benchmark 需要接入的 generated registry 类型。</typeparam>
|
||||
/// <param name="container">承载 generated registry 注册结果的 GFramework benchmark 容器。</param>
|
||||
internal static void RegisterGeneratedBenchmarkRegistry<TRegistry>(MicrosoftDiContainer container)
|
||||
where TRegistry : class, GFramework.Cqrs.ICqrsHandlerRegistry
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar");
|
||||
CqrsHandlerRegistrar.RegisterGeneratedRegistry(container, typeof(TRegistry), registrarLogger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为旧命名空间下的 CQRS runtime 契约注册兼容别名。
|
||||
/// </summary>
|
||||
/// <param name="container">承载 runtime 别名的 benchmark 容器。</param>
|
||||
/// <param name="runtime">当前正式 CQRS runtime 实例。</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// <paramref name="runtime" /> 未同时实现 legacy CQRS runtime 契约。
|
||||
/// </exception>
|
||||
private static void RegisterLegacyRuntimeAlias(MicrosoftDiContainer container, ICqrsRuntime runtime)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
|
||||
if (runtime is not LegacyICqrsRuntime legacyRuntime)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The registered {typeof(ICqrsRuntime).FullName} must also implement {typeof(LegacyICqrsRuntime).FullName}. Actual runtime type: {runtime.GetType().FullName}.");
|
||||
}
|
||||
|
||||
container.Register<LegacyICqrsRuntime>(legacyRuntime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建只承载当前 benchmark handler 集合的最小 MediatR 宿主。
|
||||
/// </summary>
|
||||
/// <param name="configure">补充当前场景的显式服务注册,例如手工单例 handler 或 pipeline 行为。</param>
|
||||
/// <param name="handlerAssemblyMarkerType">用于限定扫描程序集的标记类型。</param>
|
||||
/// <param name="handlerTypeFilter">
|
||||
/// 仅允许当前 benchmark 场景需要的 handler / behavior 类型通过扫描;
|
||||
/// 这样可保留 `AddMediatR` 的正常装配路径,同时避免整个基准程序集里的其他 handler 被一并注册。
|
||||
/// </param>
|
||||
/// <param name="lifetime">当前 benchmark 希望 MediatR 使用的默认注册生命周期。</param>
|
||||
/// <returns>只承载当前 benchmark 场景所需服务的 DI 宿主。</returns>
|
||||
internal static ServiceProvider CreateMediatRServiceProvider(
|
||||
Action<IServiceCollection>? configure,
|
||||
Type handlerAssemblyMarkerType,
|
||||
Func<Type, bool> handlerTypeFilter,
|
||||
ServiceLifetime lifetime = ServiceLifetime.Transient)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(handlerAssemblyMarkerType);
|
||||
ArgumentNullException.ThrowIfNull(handlerTypeFilter);
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(static builder =>
|
||||
Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter(
|
||||
builder,
|
||||
"LuckyPennySoftware.MediatR.License",
|
||||
Microsoft.Extensions.Logging.LogLevel.None));
|
||||
|
||||
configure?.Invoke(services);
|
||||
|
||||
services.AddMediatR(options =>
|
||||
{
|
||||
options.Lifetime = lifetime;
|
||||
options.TypeEvaluator = handlerTypeFilter;
|
||||
options.RegisterServicesFromAssembly(handlerAssemblyMarkerType.Assembly);
|
||||
});
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建承载 NuGet `Mediator` source-generated concrete mediator 的最小对照宿主。
|
||||
/// </summary>
|
||||
/// <param name="configure">补充当前场景的显式服务注册。</param>
|
||||
/// <returns>可直接解析 generated `Mediator.Mediator` 的 DI 宿主。</returns>
|
||||
/// <remarks>
|
||||
/// 当前 benchmark 只把 `Mediator` 作为单例 steady-state 对照组接入,
|
||||
/// 因为它的 lifetime 由 source generator 在编译期塑形;若后续需要 `Transient` / `Scoped` 矩阵,
|
||||
/// 应按 `Mediator` 官方 benchmark 的做法拆成独立 build config,而不是在同一编译产物里混用多个 lifetime。
|
||||
/// </remarks>
|
||||
internal static ServiceProvider CreateMediatorServiceProvider(Action<IServiceCollection>? configure)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
configure?.Invoke(services);
|
||||
services.AddMediator();
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断某个类型是否正好实现了指定的闭合或开放 MediatR 合同。
|
||||
/// </summary>
|
||||
/// <param name="candidateType">待判断类型。</param>
|
||||
/// <param name="openGenericContract">目标开放泛型合同,例如 <see cref="MediatR.IRequestHandler{TRequest,TResponse}" />。</param>
|
||||
/// <returns>命中任一实现接口时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
internal static bool ImplementsOpenGenericContract(Type candidateType, Type openGenericContract)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(candidateType);
|
||||
ArgumentNullException.ThrowIfNull(openGenericContract);
|
||||
|
||||
return candidateType.GetInterfaces().Any(interfaceType =>
|
||||
interfaceType.IsGenericType &&
|
||||
interfaceType.GetGenericTypeDefinition() == openGenericContract);
|
||||
}
|
||||
}
|
||||
35
GFramework.Cqrs.Benchmarks/Messaging/Fixture.cs
Normal file
35
GFramework.Cqrs.Benchmarks/Messaging/Fixture.cs
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using BenchmarkDotNet.Loggers;
|
||||
using System;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 为 CQRS benchmark 运行打印并验证当前场景配置,避免矩阵配置与实际运行环境漂移。
|
||||
/// </summary>
|
||||
internal static class Fixture
|
||||
{
|
||||
/// <summary>
|
||||
/// 输出当前 benchmark 配置并验证关键环境变量。
|
||||
/// </summary>
|
||||
/// <param name="scenario">当前 benchmark 场景名称。</param>
|
||||
/// <param name="handlerCount">当前场景的处理器数量。</param>
|
||||
/// <param name="pipelineCount">当前场景的 pipeline 行为数量。</param>
|
||||
public static void Setup(string scenario, int handlerCount, int pipelineCount)
|
||||
{
|
||||
ConsoleLogger.Default.WriteLineHeader("GFramework.Cqrs benchmark config");
|
||||
ConsoleLogger.Default.WriteLineInfo($"Scenario = {scenario}");
|
||||
ConsoleLogger.Default.WriteLineInfo($"HandlerCount = {handlerCount}");
|
||||
ConsoleLogger.Default.WriteLineInfo($"PipelineCount = {pipelineCount}");
|
||||
|
||||
var environmentScenario = Environment.GetEnvironmentVariable("GFRAMEWORK_CQRS_BENCHMARK_SCENARIO");
|
||||
if (!string.IsNullOrWhiteSpace(environmentScenario) &&
|
||||
!string.Equals(environmentScenario, scenario, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Scenario mismatch. Expected '{environmentScenario}', actual '{scenario}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute(
|
||||
typeof(GFramework.Cqrs.Benchmarks.Messaging.GeneratedDefaultRequestBenchmarkRegistry))]
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 为默认 request steady-state benchmark 提供 hand-written generated registry,
|
||||
/// 以便验证“默认宿主吸收 generated request invoker provider”后的热路径收益。
|
||||
/// </summary>
|
||||
public sealed class GeneratedDefaultRequestBenchmarkRegistry :
|
||||
GFramework.Cqrs.ICqrsHandlerRegistry,
|
||||
GFramework.Cqrs.ICqrsRequestInvokerProvider,
|
||||
GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors
|
||||
{
|
||||
private static readonly GFramework.Cqrs.CqrsRequestInvokerDescriptor Descriptor =
|
||||
new(
|
||||
typeof(IRequestHandler<
|
||||
RequestBenchmarks.BenchmarkRequest,
|
||||
RequestBenchmarks.BenchmarkResponse>),
|
||||
typeof(GeneratedDefaultRequestBenchmarkRegistry).GetMethod(
|
||||
nameof(InvokeBenchmarkRequestHandler),
|
||||
BindingFlags.Public | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("Missing generated default request benchmark method."));
|
||||
|
||||
private static readonly IReadOnlyList<GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> Descriptors =
|
||||
[
|
||||
new GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(
|
||||
typeof(RequestBenchmarks.BenchmarkRequest),
|
||||
typeof(RequestBenchmarks.BenchmarkResponse),
|
||||
Descriptor)
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 把默认 request benchmark handler 注册为单例,保持与原先 steady-state 宿主一致的生命周期语义。
|
||||
/// </summary>
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddSingleton(
|
||||
typeof(IRequestHandler<RequestBenchmarks.BenchmarkRequest, RequestBenchmarks.BenchmarkResponse>),
|
||||
typeof(RequestBenchmarks.BenchmarkRequestHandler));
|
||||
logger.Debug("Registered generated default request benchmark handler.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前 provider 暴露的全部 generated request invoker 描述符。
|
||||
/// </summary>
|
||||
public IReadOnlyList<GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return Descriptors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为目标请求/响应类型对返回 generated request invoker 描述符。
|
||||
/// </summary>
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out GFramework.Cqrs.CqrsRequestInvokerDescriptor? descriptor)
|
||||
{
|
||||
if (requestType == typeof(RequestBenchmarks.BenchmarkRequest) &&
|
||||
responseType == typeof(RequestBenchmarks.BenchmarkResponse))
|
||||
{
|
||||
descriptor = Descriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 generated invoker provider 为默认 request benchmark 产出的开放静态调用入口。
|
||||
/// </summary>
|
||||
public static ValueTask<RequestBenchmarks.BenchmarkResponse> InvokeBenchmarkRequestHandler(
|
||||
object handler,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var typedHandler = (IRequestHandler<
|
||||
RequestBenchmarks.BenchmarkRequest,
|
||||
RequestBenchmarks.BenchmarkResponse>)handler;
|
||||
var typedRequest = (RequestBenchmarks.BenchmarkRequest)request;
|
||||
return typedHandler.Handle(typedRequest, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 为默认 stream steady-state benchmark 提供 hand-written generated registry,
|
||||
/// 以便验证“默认 stream 宿主吸收 generated stream invoker provider”后的完整枚举收益。
|
||||
/// </summary>
|
||||
public sealed class GeneratedDefaultStreamingBenchmarkRegistry :
|
||||
GFramework.Cqrs.ICqrsHandlerRegistry,
|
||||
GFramework.Cqrs.ICqrsStreamInvokerProvider,
|
||||
GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors
|
||||
{
|
||||
private static readonly GFramework.Cqrs.CqrsStreamInvokerDescriptor Descriptor =
|
||||
new(
|
||||
typeof(IStreamRequestHandler<
|
||||
StreamingBenchmarks.BenchmarkStreamRequest,
|
||||
StreamingBenchmarks.BenchmarkResponse>),
|
||||
typeof(GeneratedDefaultStreamingBenchmarkRegistry).GetMethod(
|
||||
nameof(InvokeBenchmarkStreamHandler),
|
||||
BindingFlags.Public | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("Missing generated default streaming benchmark method."));
|
||||
|
||||
private static readonly IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> Descriptors =
|
||||
[
|
||||
new GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(
|
||||
typeof(StreamingBenchmarks.BenchmarkStreamRequest),
|
||||
typeof(StreamingBenchmarks.BenchmarkResponse),
|
||||
Descriptor)
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 把默认 stream benchmark handler 注册为单例,保持与原先 steady-state 宿主一致的生命周期语义。
|
||||
/// </summary>
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddSingleton(
|
||||
typeof(IStreamRequestHandler<StreamingBenchmarks.BenchmarkStreamRequest, StreamingBenchmarks.BenchmarkResponse>),
|
||||
typeof(StreamingBenchmarks.BenchmarkStreamHandler));
|
||||
logger.Debug("Registered generated default streaming benchmark handler.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前 provider 暴露的全部 generated stream invoker 描述符。
|
||||
/// </summary>
|
||||
public IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return Descriptors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为目标流式请求/响应类型对返回 generated stream invoker 描述符。
|
||||
/// </summary>
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out GFramework.Cqrs.CqrsStreamInvokerDescriptor? descriptor)
|
||||
{
|
||||
if (requestType == typeof(StreamingBenchmarks.BenchmarkStreamRequest) &&
|
||||
responseType == typeof(StreamingBenchmarks.BenchmarkResponse))
|
||||
{
|
||||
descriptor = Descriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 generated stream invoker provider 为默认 stream benchmark 产出的开放静态调用入口。
|
||||
/// </summary>
|
||||
public static object InvokeBenchmarkStreamHandler(
|
||||
object handler,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var typedHandler = (IStreamRequestHandler<
|
||||
StreamingBenchmarks.BenchmarkStreamRequest,
|
||||
StreamingBenchmarks.BenchmarkResponse>)handler;
|
||||
var typedRequest = (StreamingBenchmarks.BenchmarkStreamRequest)request;
|
||||
return typedHandler.Handle(typedRequest, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 为 benchmark 手写一个“生成后等价物” registry,用于驱动真实的 generated invoker provider 运行时接线路径。
|
||||
/// </summary>
|
||||
public sealed class GeneratedRequestInvokerBenchmarkRegistry :
|
||||
GFramework.Cqrs.ICqrsHandlerRegistry,
|
||||
GFramework.Cqrs.ICqrsRequestInvokerProvider,
|
||||
GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors
|
||||
{
|
||||
private static readonly GFramework.Cqrs.CqrsRequestInvokerDescriptor Descriptor =
|
||||
new(
|
||||
typeof(IRequestHandler<
|
||||
RequestInvokerBenchmarks.GeneratedBenchmarkRequest,
|
||||
RequestInvokerBenchmarks.GeneratedBenchmarkResponse>),
|
||||
typeof(GeneratedRequestInvokerBenchmarkRegistry).GetMethod(
|
||||
nameof(InvokeGeneratedRequestHandler),
|
||||
BindingFlags.Public | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("Missing generated request invoker benchmark method."));
|
||||
|
||||
private static readonly IReadOnlyList<GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> Descriptors =
|
||||
[
|
||||
new GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(
|
||||
typeof(RequestInvokerBenchmarks.GeneratedBenchmarkRequest),
|
||||
typeof(RequestInvokerBenchmarks.GeneratedBenchmarkResponse),
|
||||
Descriptor)
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 将 generated benchmark request handler 注册到目标服务集合。
|
||||
/// </summary>
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddTransient(
|
||||
typeof(IRequestHandler<
|
||||
RequestInvokerBenchmarks.GeneratedBenchmarkRequest,
|
||||
RequestInvokerBenchmarks.GeneratedBenchmarkResponse>),
|
||||
typeof(RequestInvokerBenchmarks.GeneratedBenchmarkRequestHandler));
|
||||
logger.Debug("Registered generated request invoker benchmark handler.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前 provider 暴露的全部 generated request invoker 描述符。
|
||||
/// </summary>
|
||||
public IReadOnlyList<GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return Descriptors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为目标请求/响应类型对返回 generated request invoker 描述符。
|
||||
/// </summary>
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out GFramework.Cqrs.CqrsRequestInvokerDescriptor? descriptor)
|
||||
{
|
||||
if (requestType == typeof(RequestInvokerBenchmarks.GeneratedBenchmarkRequest) &&
|
||||
responseType == typeof(RequestInvokerBenchmarks.GeneratedBenchmarkResponse))
|
||||
{
|
||||
descriptor = Descriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 generated invoker provider 产出的开放静态调用入口。
|
||||
/// </summary>
|
||||
public static ValueTask<RequestInvokerBenchmarks.GeneratedBenchmarkResponse> InvokeGeneratedRequestHandler(
|
||||
object handler,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var typedHandler = (IRequestHandler<
|
||||
RequestInvokerBenchmarks.GeneratedBenchmarkRequest,
|
||||
RequestInvokerBenchmarks.GeneratedBenchmarkResponse>)handler;
|
||||
var typedRequest = (RequestInvokerBenchmarks.GeneratedBenchmarkRequest)request;
|
||||
return typedHandler.Handle(typedRequest, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute(
|
||||
typeof(GFramework.Cqrs.Benchmarks.Messaging.GeneratedRequestLifetimeBenchmarkRegistry))]
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 为 request 生命周期矩阵 benchmark 提供 hand-written generated registry,
|
||||
/// 以便在默认 generated-provider 宿主路径上比较不同 handler 生命周期的 dispatch 成本。
|
||||
/// </summary>
|
||||
public sealed class GeneratedRequestLifetimeBenchmarkRegistry :
|
||||
GFramework.Cqrs.ICqrsHandlerRegistry,
|
||||
GFramework.Cqrs.ICqrsRequestInvokerProvider,
|
||||
GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors
|
||||
{
|
||||
private static readonly GFramework.Cqrs.CqrsRequestInvokerDescriptor Descriptor =
|
||||
new(
|
||||
typeof(IRequestHandler<
|
||||
RequestLifetimeBenchmarks.BenchmarkRequest,
|
||||
RequestLifetimeBenchmarks.BenchmarkResponse>),
|
||||
typeof(GeneratedRequestLifetimeBenchmarkRegistry).GetMethod(
|
||||
nameof(InvokeBenchmarkRequestHandler),
|
||||
BindingFlags.Public | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("Missing generated request lifetime benchmark method."));
|
||||
|
||||
private static readonly IReadOnlyList<GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> Descriptors =
|
||||
[
|
||||
new GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(
|
||||
typeof(RequestLifetimeBenchmarks.BenchmarkRequest),
|
||||
typeof(RequestLifetimeBenchmarks.BenchmarkResponse),
|
||||
Descriptor)
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 参与程序集注册入口,但不在这里直接写入 handler 生命周期。
|
||||
/// </summary>
|
||||
/// <param name="services">当前 generated registry 拥有的服务集合。</param>
|
||||
/// <param name="logger">用于记录 generated registry 注册行为的日志器。</param>
|
||||
/// <remarks>
|
||||
/// 生命周期矩阵需要让 benchmark 主体显式控制 `Singleton / Transient` 变量。
|
||||
/// 因此 registry 只负责暴露 generated descriptor,不在这里抢先注册 handler,避免把默认单例注册混入比较结果。
|
||||
/// </remarks>
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
logger.Debug("Registered generated request lifetime benchmark descriptors.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前 provider 暴露的全部 generated request invoker 描述符。
|
||||
/// </summary>
|
||||
/// <returns>当前 benchmark 需要的 request invoker 描述符集合。</returns>
|
||||
public IReadOnlyList<GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return Descriptors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为目标请求/响应类型对返回 generated request invoker 描述符。
|
||||
/// </summary>
|
||||
/// <param name="requestType">待匹配的请求类型。</param>
|
||||
/// <param name="responseType">待匹配的响应类型。</param>
|
||||
/// <param name="descriptor">命中时返回的 generated descriptor。</param>
|
||||
/// <returns>命中当前 benchmark 的请求/响应类型对时返回 <see langword="true" />。</returns>
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out GFramework.Cqrs.CqrsRequestInvokerDescriptor? descriptor)
|
||||
{
|
||||
if (requestType == typeof(RequestLifetimeBenchmarks.BenchmarkRequest) &&
|
||||
responseType == typeof(RequestLifetimeBenchmarks.BenchmarkResponse))
|
||||
{
|
||||
descriptor = Descriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 generated request invoker provider 为生命周期矩阵 benchmark 产出的开放静态调用入口。
|
||||
/// </summary>
|
||||
/// <param name="handler">当前请求对应的 handler 实例。</param>
|
||||
/// <param name="request">待分发的 request。</param>
|
||||
/// <param name="cancellationToken">调用方传入的取消令牌。</param>
|
||||
/// <returns>交给目标 request handler 处理后的响应任务。</returns>
|
||||
public static ValueTask<RequestLifetimeBenchmarks.BenchmarkResponse> InvokeBenchmarkRequestHandler(
|
||||
object handler,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var typedHandler = (IRequestHandler<
|
||||
RequestLifetimeBenchmarks.BenchmarkRequest,
|
||||
RequestLifetimeBenchmarks.BenchmarkResponse>)handler;
|
||||
var typedRequest = (RequestLifetimeBenchmarks.BenchmarkRequest)request;
|
||||
return typedHandler.Handle(typedRequest, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute(
|
||||
typeof(GFramework.Cqrs.Benchmarks.Messaging.GeneratedRequestPipelineBenchmarkRegistry))]
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 为 request pipeline benchmark 提供 handwritten generated registry,
|
||||
/// 让默认 pipeline 宿主也能走真实的 generated request invoker provider 接线路径。
|
||||
/// </summary>
|
||||
public sealed class GeneratedRequestPipelineBenchmarkRegistry :
|
||||
GFramework.Cqrs.ICqrsHandlerRegistry,
|
||||
GFramework.Cqrs.ICqrsRequestInvokerProvider,
|
||||
GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors
|
||||
{
|
||||
private static readonly GFramework.Cqrs.CqrsRequestInvokerDescriptor Descriptor =
|
||||
new(
|
||||
typeof(IRequestHandler<
|
||||
RequestPipelineBenchmarks.BenchmarkRequest,
|
||||
RequestPipelineBenchmarks.BenchmarkResponse>),
|
||||
typeof(GeneratedRequestPipelineBenchmarkRegistry).GetMethod(
|
||||
nameof(InvokeBenchmarkRequestHandler),
|
||||
BindingFlags.Public | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("Missing generated request pipeline benchmark method."));
|
||||
|
||||
private static readonly IReadOnlyList<GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> Descriptors =
|
||||
[
|
||||
new GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(
|
||||
typeof(RequestPipelineBenchmarks.BenchmarkRequest),
|
||||
typeof(RequestPipelineBenchmarks.BenchmarkResponse),
|
||||
Descriptor)
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 将 request pipeline benchmark handler 注册为单例,保持与当前矩阵宿主一致的生命周期语义。
|
||||
/// </summary>
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddSingleton(
|
||||
typeof(IRequestHandler<RequestPipelineBenchmarks.BenchmarkRequest, RequestPipelineBenchmarks.BenchmarkResponse>),
|
||||
typeof(RequestPipelineBenchmarks.BenchmarkRequestHandler));
|
||||
logger.Debug("Registered generated request pipeline benchmark handler.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前 provider 暴露的全部 generated request invoker 描述符。
|
||||
/// </summary>
|
||||
public IReadOnlyList<GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return Descriptors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为目标请求/响应类型对返回 generated request invoker 描述符。
|
||||
/// </summary>
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out GFramework.Cqrs.CqrsRequestInvokerDescriptor? descriptor)
|
||||
{
|
||||
if (requestType == typeof(RequestPipelineBenchmarks.BenchmarkRequest) &&
|
||||
responseType == typeof(RequestPipelineBenchmarks.BenchmarkResponse))
|
||||
{
|
||||
descriptor = Descriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 generated invoker provider 为 request pipeline benchmark 产出的开放静态调用入口。
|
||||
/// </summary>
|
||||
public static ValueTask<RequestPipelineBenchmarks.BenchmarkResponse> InvokeBenchmarkRequestHandler(
|
||||
object handler,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var typedHandler = (IRequestHandler<
|
||||
RequestPipelineBenchmarks.BenchmarkRequest,
|
||||
RequestPipelineBenchmarks.BenchmarkResponse>)handler;
|
||||
var typedRequest = (RequestPipelineBenchmarks.BenchmarkRequest)request;
|
||||
return typedHandler.Handle(typedRequest, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 为 benchmark 手写一个“生成后等价物” stream registry,用于驱动真实的 generated stream invoker provider 运行时接线路径。
|
||||
/// </summary>
|
||||
public sealed class GeneratedStreamInvokerBenchmarkRegistry :
|
||||
GFramework.Cqrs.ICqrsHandlerRegistry,
|
||||
GFramework.Cqrs.ICqrsStreamInvokerProvider,
|
||||
GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors
|
||||
{
|
||||
private static readonly GFramework.Cqrs.CqrsStreamInvokerDescriptor Descriptor =
|
||||
new(
|
||||
typeof(IStreamRequestHandler<
|
||||
StreamInvokerBenchmarks.GeneratedBenchmarkStreamRequest,
|
||||
StreamInvokerBenchmarks.GeneratedBenchmarkResponse>),
|
||||
typeof(GeneratedStreamInvokerBenchmarkRegistry).GetMethod(
|
||||
nameof(InvokeGeneratedStreamHandler),
|
||||
BindingFlags.Public | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("Missing generated stream invoker benchmark method."));
|
||||
|
||||
private static readonly IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> Descriptors =
|
||||
[
|
||||
new GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(
|
||||
typeof(StreamInvokerBenchmarks.GeneratedBenchmarkStreamRequest),
|
||||
typeof(StreamInvokerBenchmarks.GeneratedBenchmarkResponse),
|
||||
Descriptor)
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 将 generated benchmark stream handler 注册到目标服务集合。
|
||||
/// </summary>
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddTransient(
|
||||
typeof(IStreamRequestHandler<
|
||||
StreamInvokerBenchmarks.GeneratedBenchmarkStreamRequest,
|
||||
StreamInvokerBenchmarks.GeneratedBenchmarkResponse>),
|
||||
typeof(StreamInvokerBenchmarks.GeneratedBenchmarkStreamHandler));
|
||||
logger.Debug("Registered generated stream invoker benchmark handler.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前 provider 暴露的全部 generated stream invoker 描述符。
|
||||
/// </summary>
|
||||
public IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return Descriptors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为目标流式请求/响应类型对返回 generated stream invoker 描述符。
|
||||
/// </summary>
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out GFramework.Cqrs.CqrsStreamInvokerDescriptor? descriptor)
|
||||
{
|
||||
if (requestType == typeof(StreamInvokerBenchmarks.GeneratedBenchmarkStreamRequest) &&
|
||||
responseType == typeof(StreamInvokerBenchmarks.GeneratedBenchmarkResponse))
|
||||
{
|
||||
descriptor = Descriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 generated stream invoker provider 产出的开放静态调用入口。
|
||||
/// </summary>
|
||||
public static object InvokeGeneratedStreamHandler(object handler, object request, CancellationToken cancellationToken)
|
||||
{
|
||||
var typedHandler = (IStreamRequestHandler<
|
||||
StreamInvokerBenchmarks.GeneratedBenchmarkStreamRequest,
|
||||
StreamInvokerBenchmarks.GeneratedBenchmarkResponse>)handler;
|
||||
var typedRequest = (StreamInvokerBenchmarks.GeneratedBenchmarkStreamRequest)request;
|
||||
return typedHandler.Handle(typedRequest, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute(
|
||||
typeof(GFramework.Cqrs.Benchmarks.Messaging.GeneratedStreamLifetimeBenchmarkRegistry))]
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 为 stream 生命周期矩阵 benchmark 提供 hand-written generated registry,
|
||||
/// 以便在默认 generated-provider 宿主路径上比较不同 handler 生命周期的完整枚举成本。
|
||||
/// </summary>
|
||||
public sealed class GeneratedStreamLifetimeBenchmarkRegistry :
|
||||
GFramework.Cqrs.ICqrsHandlerRegistry,
|
||||
GFramework.Cqrs.ICqrsStreamInvokerProvider,
|
||||
GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors
|
||||
{
|
||||
private static readonly GFramework.Cqrs.CqrsStreamInvokerDescriptor Descriptor =
|
||||
new(
|
||||
typeof(IStreamRequestHandler<
|
||||
StreamLifetimeBenchmarks.GeneratedBenchmarkStreamRequest,
|
||||
StreamLifetimeBenchmarks.GeneratedBenchmarkResponse>),
|
||||
typeof(GeneratedStreamLifetimeBenchmarkRegistry).GetMethod(
|
||||
nameof(InvokeBenchmarkStreamHandler),
|
||||
BindingFlags.Public | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("Missing generated stream lifetime benchmark method."));
|
||||
|
||||
private static readonly IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> Descriptors =
|
||||
[
|
||||
new GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(
|
||||
typeof(StreamLifetimeBenchmarks.GeneratedBenchmarkStreamRequest),
|
||||
typeof(StreamLifetimeBenchmarks.GeneratedBenchmarkResponse),
|
||||
Descriptor)
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 参与程序集注册入口,但不在这里直接写入 handler 生命周期。
|
||||
/// </summary>
|
||||
/// <param name="services">当前 generated registry 拥有的服务集合。</param>
|
||||
/// <param name="logger">用于记录 generated registry 注册行为的日志器。</param>
|
||||
/// <remarks>
|
||||
/// 生命周期矩阵需要让 benchmark 主体显式控制 `Singleton / Transient` 变量。
|
||||
/// 因此 registry 只负责暴露 generated descriptor,不在这里抢先注册 handler,避免把默认单例注册混入比较结果。
|
||||
/// </remarks>
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
logger.Debug("Registered generated stream lifetime benchmark descriptors.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前 provider 暴露的全部 generated stream invoker 描述符。
|
||||
/// </summary>
|
||||
public IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return Descriptors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为目标流式请求/响应类型对返回 generated stream invoker 描述符。
|
||||
/// </summary>
|
||||
/// <param name="requestType">待匹配的请求类型。</param>
|
||||
/// <param name="responseType">待匹配的响应类型。</param>
|
||||
/// <param name="descriptor">命中时返回的 generated descriptor。</param>
|
||||
/// <returns>命中当前 benchmark 的请求/响应类型对时返回 <see langword="true" />。</returns>
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out GFramework.Cqrs.CqrsStreamInvokerDescriptor? descriptor)
|
||||
{
|
||||
if (requestType == typeof(StreamLifetimeBenchmarks.GeneratedBenchmarkStreamRequest) &&
|
||||
responseType == typeof(StreamLifetimeBenchmarks.GeneratedBenchmarkResponse))
|
||||
{
|
||||
descriptor = Descriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 generated stream invoker provider 为生命周期矩阵 benchmark 产出的开放静态调用入口。
|
||||
/// </summary>
|
||||
/// <param name="handler">当前请求对应的 handler 实例。</param>
|
||||
/// <param name="request">待分发的流式请求。</param>
|
||||
/// <param name="cancellationToken">调用方传入的取消令牌。</param>
|
||||
/// <returns>交给目标 stream handler 处理后的异步枚举。</returns>
|
||||
public static object InvokeBenchmarkStreamHandler(
|
||||
object handler,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var typedHandler = (IStreamRequestHandler<
|
||||
StreamLifetimeBenchmarks.GeneratedBenchmarkStreamRequest,
|
||||
StreamLifetimeBenchmarks.GeneratedBenchmarkResponse>)handler;
|
||||
var typedRequest = (StreamLifetimeBenchmarks.GeneratedBenchmarkStreamRequest)request;
|
||||
return typedHandler.Handle(typedRequest, cancellationToken);
|
||||
}
|
||||
}
|
||||
167
GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs
Normal file
167
GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs
Normal file
@ -0,0 +1,167 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Columns;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Diagnosers;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using BenchmarkDotNet.Order;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Ioc;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using GeneratedMediator = Mediator.Mediator;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 对比单处理器 notification 在 GFramework.CQRS 与 MediatR 之间的 publish 开销。
|
||||
/// </summary>
|
||||
[Config(typeof(Config))]
|
||||
public class NotificationBenchmarks
|
||||
{
|
||||
private MicrosoftDiContainer _container = null!;
|
||||
private ICqrsRuntime _runtime = null!;
|
||||
private ServiceProvider _mediatrServiceProvider = null!;
|
||||
private ServiceProvider _mediatorServiceProvider = null!;
|
||||
private IPublisher _mediatrPublisher = null!;
|
||||
private GeneratedMediator _mediator = null!;
|
||||
private BenchmarkNotification _notification = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 配置 notification benchmark 的公共输出格式。
|
||||
/// </summary>
|
||||
private sealed class Config : ManualConfig
|
||||
{
|
||||
public Config()
|
||||
{
|
||||
AddJob(Job.Default);
|
||||
AddColumnProvider(DefaultColumnProviders.Instance);
|
||||
AddColumn(new CustomColumn("Scenario", static (_, _) => "Notification"));
|
||||
AddDiagnoser(MemoryDiagnoser.Default);
|
||||
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建 notification publish 所需的最小 runtime 宿主和对照对象。
|
||||
/// </summary>
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Fatal
|
||||
};
|
||||
Fixture.Setup("Notification", handlerCount: 1, pipelineCount: 0);
|
||||
|
||||
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>>(
|
||||
new BenchmarkNotificationHandler());
|
||||
});
|
||||
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_container,
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(NotificationBenchmarks)));
|
||||
|
||||
_mediatrServiceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
services => services.AddSingleton<MediatR.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler>(),
|
||||
typeof(NotificationBenchmarks),
|
||||
static candidateType => candidateType == typeof(BenchmarkNotificationHandler),
|
||||
ServiceLifetime.Singleton);
|
||||
_mediatrPublisher = _mediatrServiceProvider.GetRequiredService<IPublisher>();
|
||||
|
||||
_mediatorServiceProvider = BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null);
|
||||
_mediator = _mediatorServiceProvider.GetRequiredService<GeneratedMediator>();
|
||||
|
||||
_notification = new BenchmarkNotification(Guid.NewGuid());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放 MediatR 与 `Mediator` 对照组使用的 DI 宿主。
|
||||
/// </summary>
|
||||
[GlobalCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _mediatrServiceProvider, _mediatorServiceProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 GFramework.CQRS runtime 发布 notification。
|
||||
/// </summary>
|
||||
[Benchmark(Baseline = true)]
|
||||
public ValueTask PublishNotification_GFrameworkCqrs()
|
||||
{
|
||||
return _runtime.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 MediatR 发布 notification,作为外部设计对照。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public Task PublishNotification_MediatR()
|
||||
{
|
||||
return _mediatrPublisher.Publish(_notification, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 `Mediator` source-generated concrete mediator 发布 notification,作为高性能对照组。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public ValueTask PublishNotification_Mediator()
|
||||
{
|
||||
return _mediator.Publish(_notification, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark notification。
|
||||
/// </summary>
|
||||
/// <param name="Id">通知标识。</param>
|
||||
public sealed record BenchmarkNotification(Guid Id) :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.INotification,
|
||||
Mediator.INotification,
|
||||
MediatR.INotification;
|
||||
|
||||
/// <summary>
|
||||
/// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 notification handler。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkNotificationHandler :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>,
|
||||
Mediator.INotificationHandler<BenchmarkNotification>,
|
||||
MediatR.INotificationHandler<BenchmarkNotification>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 GFramework.CQRS notification。
|
||||
/// </summary>
|
||||
public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 NuGet `Mediator` notification。
|
||||
/// </summary>
|
||||
ValueTask Mediator.INotificationHandler<BenchmarkNotification>.Handle(
|
||||
BenchmarkNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Handle(notification, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 MediatR notification。
|
||||
/// </summary>
|
||||
Task MediatR.INotificationHandler<BenchmarkNotification>.Handle(
|
||||
BenchmarkNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,350 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Columns;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Diagnosers;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using BenchmarkDotNet.Order;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Ioc;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using GFramework.Cqrs.Notification;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using GeneratedMediator = Mediator.Mediator;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 对比固定 4 个处理器的 notification fan-out publish 在 baseline、GFramework.CQRS、NuGet `Mediator`
|
||||
/// 与 MediatR 之间的开销。
|
||||
/// </summary>
|
||||
[Config(typeof(Config))]
|
||||
public class NotificationFanOutBenchmarks
|
||||
{
|
||||
private MicrosoftDiContainer _container = null!;
|
||||
private ICqrsRuntime _sequentialRuntime = null!;
|
||||
private ICqrsRuntime _taskWhenAllRuntime = null!;
|
||||
private ServiceProvider _mediatrServiceProvider = null!;
|
||||
private ServiceProvider _mediatorServiceProvider = null!;
|
||||
private IPublisher _mediatrPublisher = null!;
|
||||
private GeneratedMediator _mediator = null!;
|
||||
private BenchmarkNotification _notification = null!;
|
||||
private BenchmarkNotificationHandler1 _baselineHandler1 = null!;
|
||||
private BenchmarkNotificationHandler2 _baselineHandler2 = null!;
|
||||
private BenchmarkNotificationHandler3 _baselineHandler3 = null!;
|
||||
private BenchmarkNotificationHandler4 _baselineHandler4 = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 配置 notification fan-out benchmark 的公共输出格式。
|
||||
/// </summary>
|
||||
private sealed class Config : ManualConfig
|
||||
{
|
||||
public Config()
|
||||
{
|
||||
AddJob(Job.Default);
|
||||
AddColumnProvider(DefaultColumnProviders.Instance);
|
||||
AddColumn(new CustomColumn("Scenario", static (_, _) => "NotificationFanOut"));
|
||||
AddDiagnoser(MemoryDiagnoser.Default);
|
||||
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建固定 4 处理器 notification publish 所需的最小 runtime 宿主和对照对象。
|
||||
/// </summary>
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Fatal
|
||||
};
|
||||
Fixture.Setup("NotificationFanOut", handlerCount: 4, pipelineCount: 0);
|
||||
|
||||
_baselineHandler1 = new BenchmarkNotificationHandler1();
|
||||
_baselineHandler2 = new BenchmarkNotificationHandler2();
|
||||
_baselineHandler3 = new BenchmarkNotificationHandler3();
|
||||
_baselineHandler4 = new BenchmarkNotificationHandler4();
|
||||
|
||||
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler1>();
|
||||
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler2>();
|
||||
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler3>();
|
||||
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler4>();
|
||||
});
|
||||
_sequentialRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_container,
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(NotificationFanOutBenchmarks)));
|
||||
_taskWhenAllRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_container,
|
||||
LoggerFactoryResolver.Provider.CreateLogger($"{nameof(NotificationFanOutBenchmarks)}.{nameof(TaskWhenAllNotificationPublisher)}"),
|
||||
new TaskWhenAllNotificationPublisher());
|
||||
|
||||
_mediatrServiceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
services =>
|
||||
{
|
||||
services.AddSingleton<MediatR.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler1>();
|
||||
services.AddSingleton<MediatR.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler2>();
|
||||
services.AddSingleton<MediatR.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler3>();
|
||||
services.AddSingleton<MediatR.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler4>();
|
||||
},
|
||||
typeof(NotificationFanOutBenchmarks),
|
||||
static candidateType =>
|
||||
candidateType == typeof(BenchmarkNotificationHandler1) ||
|
||||
candidateType == typeof(BenchmarkNotificationHandler2) ||
|
||||
candidateType == typeof(BenchmarkNotificationHandler3) ||
|
||||
candidateType == typeof(BenchmarkNotificationHandler4),
|
||||
ServiceLifetime.Singleton);
|
||||
_mediatrPublisher = _mediatrServiceProvider.GetRequiredService<IPublisher>();
|
||||
|
||||
_mediatorServiceProvider = BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null);
|
||||
_mediator = _mediatorServiceProvider.GetRequiredService<GeneratedMediator>();
|
||||
|
||||
_notification = new BenchmarkNotification(Guid.NewGuid());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放 MediatR 与 `Mediator` 对照组使用的 DI 宿主。
|
||||
/// </summary>
|
||||
[GlobalCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _mediatrServiceProvider, _mediatorServiceProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接依次调用 4 个处理器,作为 fan-out dispatch 额外开销的 baseline。
|
||||
/// </summary>
|
||||
[Benchmark(Baseline = true)]
|
||||
public async ValueTask PublishNotification_Baseline()
|
||||
{
|
||||
await _baselineHandler1.Handle(_notification, CancellationToken.None).ConfigureAwait(false);
|
||||
await _baselineHandler2.Handle(_notification, CancellationToken.None).ConfigureAwait(false);
|
||||
await _baselineHandler3.Handle(_notification, CancellationToken.None).ConfigureAwait(false);
|
||||
await _baselineHandler4.Handle(_notification, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过默认顺序发布器的 GFramework.CQRS runtime 发布固定 4 处理器的 notification。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public ValueTask PublishNotification_GFrameworkCqrsSequential()
|
||||
{
|
||||
return _sequentialRuntime.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过内置 <c>Task.WhenAll(...)</c> 发布器的 GFramework.CQRS runtime 发布固定 4 处理器的 notification。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public ValueTask PublishNotification_GFrameworkCqrsTaskWhenAll()
|
||||
{
|
||||
return _taskWhenAllRuntime.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 MediatR 发布固定 4 处理器的 notification,作为外部设计对照。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public Task PublishNotification_MediatR()
|
||||
{
|
||||
return _mediatrPublisher.Publish(_notification, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 `Mediator` source-generated concrete mediator 发布固定 4 处理器的 notification,作为高性能对照组。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public ValueTask PublishNotification_Mediator()
|
||||
{
|
||||
return _mediator.Publish(_notification, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark notification。
|
||||
/// </summary>
|
||||
/// <param name="Id">通知标识。</param>
|
||||
public sealed record BenchmarkNotification(Guid Id) :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.INotification,
|
||||
Mediator.INotification,
|
||||
MediatR.INotification;
|
||||
|
||||
/// <summary>
|
||||
/// 为 fan-out benchmark 提供统一的 no-op 处理逻辑。
|
||||
/// </summary>
|
||||
public abstract class BenchmarkNotificationHandlerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行 benchmark 使用的最小处理逻辑。
|
||||
/// </summary>
|
||||
/// <param name="notification">当前 notification。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>已完成的值任务。</returns>
|
||||
protected static ValueTask HandleCore(BenchmarkNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(notification);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// fan-out benchmark 的第 1 个 notification handler。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkNotificationHandler1 :
|
||||
BenchmarkNotificationHandlerBase,
|
||||
GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>,
|
||||
Mediator.INotificationHandler<BenchmarkNotification>,
|
||||
MediatR.INotificationHandler<BenchmarkNotification>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 GFramework.CQRS notification。
|
||||
/// </summary>
|
||||
public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
return HandleCore(notification, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 NuGet `Mediator` notification。
|
||||
/// </summary>
|
||||
ValueTask Mediator.INotificationHandler<BenchmarkNotification>.Handle(
|
||||
BenchmarkNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return HandleCore(notification, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 MediatR notification。
|
||||
/// </summary>
|
||||
Task MediatR.INotificationHandler<BenchmarkNotification>.Handle(
|
||||
BenchmarkNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return HandleCore(notification, cancellationToken).AsTask();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// fan-out benchmark 的第 2 个 notification handler。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkNotificationHandler2 :
|
||||
BenchmarkNotificationHandlerBase,
|
||||
GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>,
|
||||
Mediator.INotificationHandler<BenchmarkNotification>,
|
||||
MediatR.INotificationHandler<BenchmarkNotification>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 GFramework.CQRS notification。
|
||||
/// </summary>
|
||||
public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
return HandleCore(notification, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 NuGet `Mediator` notification。
|
||||
/// </summary>
|
||||
ValueTask Mediator.INotificationHandler<BenchmarkNotification>.Handle(
|
||||
BenchmarkNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return HandleCore(notification, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 MediatR notification。
|
||||
/// </summary>
|
||||
Task MediatR.INotificationHandler<BenchmarkNotification>.Handle(
|
||||
BenchmarkNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return HandleCore(notification, cancellationToken).AsTask();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// fan-out benchmark 的第 3 个 notification handler。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkNotificationHandler3 :
|
||||
BenchmarkNotificationHandlerBase,
|
||||
GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>,
|
||||
Mediator.INotificationHandler<BenchmarkNotification>,
|
||||
MediatR.INotificationHandler<BenchmarkNotification>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 GFramework.CQRS notification。
|
||||
/// </summary>
|
||||
public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
return HandleCore(notification, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 NuGet `Mediator` notification。
|
||||
/// </summary>
|
||||
ValueTask Mediator.INotificationHandler<BenchmarkNotification>.Handle(
|
||||
BenchmarkNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return HandleCore(notification, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 MediatR notification。
|
||||
/// </summary>
|
||||
Task MediatR.INotificationHandler<BenchmarkNotification>.Handle(
|
||||
BenchmarkNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return HandleCore(notification, cancellationToken).AsTask();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// fan-out benchmark 的第 4 个 notification handler。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkNotificationHandler4 :
|
||||
BenchmarkNotificationHandlerBase,
|
||||
GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>,
|
||||
Mediator.INotificationHandler<BenchmarkNotification>,
|
||||
MediatR.INotificationHandler<BenchmarkNotification>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 GFramework.CQRS notification。
|
||||
/// </summary>
|
||||
public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
return HandleCore(notification, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 NuGet `Mediator` notification。
|
||||
/// </summary>
|
||||
ValueTask Mediator.INotificationHandler<BenchmarkNotification>.Handle(
|
||||
BenchmarkNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return HandleCore(notification, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 MediatR notification。
|
||||
/// </summary>
|
||||
Task MediatR.INotificationHandler<BenchmarkNotification>.Handle(
|
||||
BenchmarkNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return HandleCore(notification, cancellationToken).AsTask();
|
||||
}
|
||||
}
|
||||
}
|
||||
191
GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs
Normal file
191
GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs
Normal file
@ -0,0 +1,191 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Columns;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Diagnosers;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using BenchmarkDotNet.Order;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Ioc;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using GeneratedMediator = Mediator.Mediator;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 对比单个 request 在直接调用、GFramework.CQRS runtime、NuGet `Mediator` 与 MediatR 之间的 steady-state dispatch 开销。
|
||||
/// </summary>
|
||||
[Config(typeof(Config))]
|
||||
public class RequestBenchmarks
|
||||
{
|
||||
private MicrosoftDiContainer _container = null!;
|
||||
private ICqrsRuntime _runtime = null!;
|
||||
private ServiceProvider _mediatrServiceProvider = null!;
|
||||
private ServiceProvider _mediatorServiceProvider = null!;
|
||||
private IMediator _mediatr = null!;
|
||||
private GeneratedMediator _mediator = null!;
|
||||
private BenchmarkRequestHandler _baselineHandler = null!;
|
||||
private BenchmarkRequest _request = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 配置 request benchmark 的公共输出格式。
|
||||
/// </summary>
|
||||
private sealed class Config : ManualConfig
|
||||
{
|
||||
public Config()
|
||||
{
|
||||
AddJob(Job.Default);
|
||||
AddColumnProvider(DefaultColumnProviders.Instance);
|
||||
AddColumn(new CustomColumn("Scenario", static (_, _) => "Request"));
|
||||
AddDiagnoser(MemoryDiagnoser.Default);
|
||||
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建 request dispatch 所需的最小 runtime 宿主和对照对象。
|
||||
/// </summary>
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Fatal
|
||||
};
|
||||
Fixture.Setup("Request", handlerCount: 1, pipelineCount: 0);
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
|
||||
_baselineHandler = new BenchmarkRequestHandler();
|
||||
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedDefaultRequestBenchmarkRegistry>(container);
|
||||
});
|
||||
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_container,
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestBenchmarks)));
|
||||
|
||||
_mediatrServiceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
configure: null,
|
||||
typeof(RequestBenchmarks),
|
||||
static candidateType => candidateType == typeof(BenchmarkRequestHandler),
|
||||
ServiceLifetime.Singleton);
|
||||
_mediatr = _mediatrServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
_mediatorServiceProvider = BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null);
|
||||
_mediator = _mediatorServiceProvider.GetRequiredService<GeneratedMediator>();
|
||||
|
||||
_request = new BenchmarkRequest(Guid.NewGuid());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放 MediatR 与 `Mediator` 对照组使用的 DI 宿主。
|
||||
/// </summary>
|
||||
[GlobalCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
try
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _mediatrServiceProvider, _mediatorServiceProvider);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接调用 handler,作为 dispatch 额外开销的 baseline。
|
||||
/// </summary>
|
||||
[Benchmark(Baseline = true)]
|
||||
public ValueTask<BenchmarkResponse> SendRequest_Baseline()
|
||||
{
|
||||
return _baselineHandler.Handle(_request, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 GFramework.CQRS runtime 发送 request。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public ValueTask<BenchmarkResponse> SendRequest_GFrameworkCqrs()
|
||||
{
|
||||
return _runtime.SendAsync(BenchmarkContext.Instance, _request, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 MediatR 发送 request,作为外部设计对照。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public Task<BenchmarkResponse> SendRequest_MediatR()
|
||||
{
|
||||
return _mediatr.Send(_request, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 `ai-libs/Mediator` 的 source-generated concrete mediator 发送 request,作为高性能对照组。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public ValueTask<BenchmarkResponse> SendRequest_Mediator()
|
||||
{
|
||||
return _mediator.Send(_request, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark request。
|
||||
/// </summary>
|
||||
/// <param name="Id">请求标识。</param>
|
||||
public sealed record BenchmarkRequest(Guid Id) :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IRequest<BenchmarkResponse>,
|
||||
Mediator.IRequest<BenchmarkResponse>,
|
||||
MediatR.IRequest<BenchmarkResponse>;
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark response。
|
||||
/// </summary>
|
||||
/// <param name="Id">响应标识。</param>
|
||||
public sealed record BenchmarkResponse(Guid Id);
|
||||
|
||||
/// <summary>
|
||||
/// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 request handler。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkRequestHandler :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>,
|
||||
Mediator.IRequestHandler<BenchmarkRequest, BenchmarkResponse>,
|
||||
MediatR.IRequestHandler<BenchmarkRequest, BenchmarkResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 GFramework.CQRS request。
|
||||
/// </summary>
|
||||
public ValueTask<BenchmarkResponse> Handle(BenchmarkRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return ValueTask.FromResult(new BenchmarkResponse(request.Id));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 NuGet `Mediator` request。
|
||||
/// </summary>
|
||||
ValueTask<BenchmarkResponse> Mediator.IRequestHandler<BenchmarkRequest, BenchmarkResponse>.Handle(
|
||||
BenchmarkRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Handle(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 MediatR request。
|
||||
/// </summary>
|
||||
Task<BenchmarkResponse> MediatR.IRequestHandler<BenchmarkRequest, BenchmarkResponse>.Handle(
|
||||
BenchmarkRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(new BenchmarkResponse(request.Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user