mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-13 14:14:29 +08:00
Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4837aa2a23 | ||
|
|
2dd9435cea | ||
|
|
e3532fc2c8 | ||
|
|
092946e91a | ||
|
|
c32a1ec4ae | ||
|
|
555c7c07ac | ||
|
|
ab422b05db | ||
|
|
a016e3d4a4 | ||
|
|
f346110a8a | ||
|
|
2b2bec6532 | ||
|
|
e746fede7e | ||
|
|
d85b85c614 | ||
|
|
d7e142f03f | ||
|
|
370ea20de8 | ||
|
|
40efb4f763 | ||
|
|
4cc7060fcf | ||
|
|
ee8d65279e | ||
|
|
5e9b903d0f | ||
|
|
75e7785592 | ||
|
|
ef4d3d5ddf | ||
|
|
4e98b63e9c | ||
|
|
babd132e81 | ||
|
|
f650bc5776 | ||
|
|
683034579e | ||
|
|
e156b5f000 | ||
|
|
af4e988a5b | ||
|
|
7fa9d5ff17 | ||
|
|
8990749d91 | ||
|
|
337ffbd580 | ||
|
|
d9e47abdb6 | ||
|
|
3b2e6899d5 | ||
|
|
cacf238911 | ||
|
|
1420bd4340 | ||
|
|
158f98a465 | ||
|
|
0baa662ae4 | ||
|
|
5f9589ed3c | ||
|
|
594798dcb9 | ||
|
|
11a6b6abe4 | ||
|
|
79ae5f0b5a | ||
|
|
ebbef321ad | ||
|
|
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 |
@ -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
|
||||
|
||||
|
||||
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."
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -26,3 +26,4 @@ ai-libs/
|
||||
.codex
|
||||
# tool
|
||||
.venv/
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
52
AGENTS.md
52
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.
|
||||
|
||||
@ -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 处理器。
|
||||
/// 当处理器位于默认架构程序集之外的模块或扩展程序集中时,可在初始化阶段调用该入口接入对应程序集。
|
||||
|
||||
@ -105,6 +105,20 @@ public interface IIocContainer : IContextAware, IDisposable
|
||||
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 处理器。
|
||||
/// 该入口适用于处理器不位于默认架构程序集中的场景,例如扩展包、模块程序集或拆分后的业务程序集。
|
||||
@ -141,6 +155,10 @@ public interface IIocContainer : IContextAware, IDisposable
|
||||
/// </summary>
|
||||
/// <typeparam name="T">期望获取的实例类型</typeparam>
|
||||
/// <returns>找到的第一个实例;如果未找到则返回 null</returns>
|
||||
/// <remarks>
|
||||
/// 在 <see cref="Freeze" /> 之前,该查询只保证返回已经物化为实例绑定的服务。
|
||||
/// 仅通过工厂或实现类型注册的服务在预冻结阶段可能不可见;若需要完整激活语义,请先冻结容器。
|
||||
/// </remarks>
|
||||
T? Get<T>() where T : class;
|
||||
|
||||
/// <summary>
|
||||
@ -149,6 +167,10 @@ public interface IIocContainer : IContextAware, IDisposable
|
||||
/// </summary>
|
||||
/// <param name="type">期望获取的实例类型</param>
|
||||
/// <returns>找到的第一个实例;如果未找到则返回 null</returns>
|
||||
/// <remarks>
|
||||
/// 在 <see cref="Freeze" /> 之前,该查询只保证返回已经物化为实例绑定的服务。
|
||||
/// 仅通过工厂或实现类型注册的服务在预冻结阶段可能不可见;若需要完整激活语义,请先冻结容器。
|
||||
/// </remarks>
|
||||
object? Get(Type type);
|
||||
|
||||
|
||||
@ -174,6 +196,9 @@ public interface IIocContainer : IContextAware, IDisposable
|
||||
/// </summary>
|
||||
/// <typeparam name="T">期望获取的实例类型</typeparam>
|
||||
/// <returns>所有符合条件的实例列表;如果没有则返回空数组</returns>
|
||||
/// <remarks>
|
||||
/// 在 <see cref="Freeze" /> 之前,该查询只会枚举当前已经可见的实例绑定,不会主动执行工厂或创建实现类型。
|
||||
/// </remarks>
|
||||
IReadOnlyList<T> GetAll<T>() where T : class;
|
||||
|
||||
/// <summary>
|
||||
@ -181,6 +206,9 @@ public interface IIocContainer : IContextAware, IDisposable
|
||||
/// </summary>
|
||||
/// <param name="type">期望获取的实例类型</param>
|
||||
/// <returns>所有符合条件的实例列表;如果没有则返回空数组</returns>
|
||||
/// <remarks>
|
||||
/// 在 <see cref="Freeze" /> 之前,该查询只会枚举当前已经可见的实例绑定,不会主动执行工厂或创建实现类型。
|
||||
/// </remarks>
|
||||
IReadOnlyList<object> GetAll(Type type);
|
||||
|
||||
|
||||
@ -219,8 +247,26 @@ public interface IIocContainer : IContextAware, IDisposable
|
||||
/// </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>
|
||||
|
||||
|
||||
@ -158,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>
|
||||
@ -278,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>
|
||||
/// 测试容器未冻结时,会折叠“不同服务类型指向同一实例”的兼容别名重复,
|
||||
/// 但会保留同一服务类型的重复显式注册。
|
||||
@ -353,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 的功能
|
||||
@ -363,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>
|
||||
@ -430,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>
|
||||
@ -831,4 +943,49 @@ public class MicrosoftDiContainerTests
|
||||
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());
|
||||
}
|
||||
}
|
||||
@ -185,6 +185,12 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// </summary>
|
||||
private IServiceProvider? _provider;
|
||||
|
||||
/// <summary>
|
||||
/// 冻结后可复用的服务类型可见性索引。
|
||||
/// 容器冻结后注册集合不再变化,因此 <see cref="HasRegistration(Type)" /> 可以安全复用该索引。
|
||||
/// </summary>
|
||||
private FrozenServiceTypeIndex? _frozenServiceTypeIndex;
|
||||
|
||||
/// <summary>
|
||||
/// 容器冻结状态标志,true表示容器已冻结不可修改
|
||||
/// </summary>
|
||||
@ -485,36 +491,11 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
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
|
||||
{
|
||||
@ -522,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>
|
||||
@ -599,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);
|
||||
}
|
||||
@ -625,18 +678,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
{
|
||||
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>();
|
||||
@ -659,24 +701,23 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// <returns>服务实例或null</returns>
|
||||
public object? Get(Type type)
|
||||
{
|
||||
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
|
||||
@ -760,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
|
||||
@ -789,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
|
||||
@ -991,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>
|
||||
/// 判断容器中是否包含某个具体的实例对象
|
||||
/// 通过已注册实例集合进行快速查找
|
||||
@ -1011,6 +1083,52 @@ 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>
|
||||
/// 清空容器中的所有实例和服务注册
|
||||
/// 只有在容器未冻结状态下才能执行清空操作
|
||||
@ -1032,6 +1150,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
GetServicesUnsafe.Clear();
|
||||
_registeredInstances.Clear();
|
||||
_provider = null;
|
||||
_frozenServiceTypeIndex = null;
|
||||
_frozen = false;
|
||||
_logger.Info("Container cleared");
|
||||
}
|
||||
@ -1059,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");
|
||||
}
|
||||
@ -1068,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的访问权限,用于高级配置和自定义操作
|
||||
@ -1143,6 +1316,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
_disposed = true;
|
||||
(_provider as IDisposable)?.Dispose();
|
||||
_provider = null;
|
||||
_frozenServiceTypeIndex = null;
|
||||
GetServicesUnsafe.Clear();
|
||||
_registeredInstances.Clear();
|
||||
_frozen = false;
|
||||
|
||||
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`
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
|
||||
@ -18,9 +18,14 @@
|
||||
|
||||
<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.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -2,11 +2,17 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
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;
|
||||
|
||||
@ -31,11 +37,91 @@ internal static class BenchmarkHostFactory
|
||||
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>
|
||||
@ -75,6 +161,133 @@ internal static class BenchmarkHostFactory
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在真实的 request 级作用域内执行一次 GFramework.CQRS request 分发。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">请求响应类型。</typeparam>
|
||||
/// <param name="runtime">复用的 scoped benchmark runtime。</param>
|
||||
/// <param name="scopedContainer">负责为每次 request 激活独立作用域的只读容器适配层。</param>
|
||||
/// <param name="context">当前 CQRS 分发上下文。</param>
|
||||
/// <param name="request">要发送的 request。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>当前 request 的响应结果。</returns>
|
||||
/// <remarks>
|
||||
/// 该入口只服务 request lifetime benchmark:它会复用同一个 dispatcher/runtime 实例,
|
||||
/// 但在每次调用前后显式创建并释放新的 DI 作用域,
|
||||
/// 让 `Scoped` handler 在真实 request 边界内解析,而不是退化为根容器解析或额外计入 runtime 构造成本。
|
||||
/// </remarks>
|
||||
internal static async ValueTask<TResponse> SendScopedGFrameworkRequestAsync<TResponse>(
|
||||
ICqrsRuntime runtime,
|
||||
ScopedBenchmarkContainer scopedContainer,
|
||||
ICqrsContext context,
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IRequest<TResponse> request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
ArgumentNullException.ThrowIfNull(scopedContainer);
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
using var scopeLease = scopedContainer.EnterScope();
|
||||
return await runtime.SendAsync(context, request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在真实的 request 级作用域内执行一次 MediatR request 分发。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">请求响应类型。</typeparam>
|
||||
/// <param name="rootServiceProvider">当前 benchmark 的根 <see cref="ServiceProvider" />。</param>
|
||||
/// <param name="request">要发送的 request。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>当前 request 的响应结果。</returns>
|
||||
/// <remarks>
|
||||
/// 这里显式从新的 scope 解析 <see cref="IMediator" />,确保 `Scoped` handler 与其依赖绑定到 request 边界。
|
||||
/// </remarks>
|
||||
internal static async Task<TResponse> SendScopedMediatRRequestAsync<TResponse>(
|
||||
ServiceProvider rootServiceProvider,
|
||||
MediatR.IRequest<TResponse> request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(rootServiceProvider);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
using var scope = rootServiceProvider.CreateScope();
|
||||
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
return await mediator.Send(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在真实的 request 级作用域内创建一次 GFramework.CQRS stream,并让该作用域覆盖整个异步枚举周期。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">stream 响应元素类型。</typeparam>
|
||||
/// <param name="runtime">复用的 scoped benchmark runtime。</param>
|
||||
/// <param name="scopedContainer">负责为每次 stream 激活独立作用域的只读容器适配层。</param>
|
||||
/// <param name="context">当前 CQRS 分发上下文。</param>
|
||||
/// <param name="request">要创建 stream 的 request。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>绑定到单次显式作用域的异步响应序列。</returns>
|
||||
/// <remarks>
|
||||
/// stream 与 request 的区别在于:handler 解析发生在建流时,但 scoped 依赖必须一直存活到枚举完成。
|
||||
/// 因此这里返回一个包装后的 async iterator,把 scope 的释放时机推迟到调用方结束枚举之后,
|
||||
/// 避免 `Scoped` handler 退化成“建流后立刻释放 scope,再在根容器语义下继续枚举”的错误模型。
|
||||
/// </remarks>
|
||||
internal static IAsyncEnumerable<TResponse> CreateScopedGFrameworkStream<TResponse>(
|
||||
ICqrsRuntime runtime,
|
||||
ScopedBenchmarkContainer scopedContainer,
|
||||
ICqrsContext context,
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest<TResponse> request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
ArgumentNullException.ThrowIfNull(scopedContainer);
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
return EnumerateScopedGFrameworkStreamAsync(runtime, scopedContainer, context, request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在真实的 request 级作用域内创建一次 MediatR stream,并让该作用域覆盖整个异步枚举周期。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">stream 响应元素类型。</typeparam>
|
||||
/// <param name="rootServiceProvider">当前 benchmark 的根 <see cref="ServiceProvider" />。</param>
|
||||
/// <param name="request">要创建 stream 的 request。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>绑定到单次显式作用域的异步响应序列。</returns>
|
||||
/// <remarks>
|
||||
/// 这里与 scoped request helper 保持同一组边界约束,但把 scope 生命周期延长到 stream 完整枚举结束,
|
||||
/// 确保 `Scoped` handler 与依赖不会在首个元素产出前后被提前释放。
|
||||
/// </remarks>
|
||||
internal static IAsyncEnumerable<TResponse> CreateScopedMediatRStream<TResponse>(
|
||||
ServiceProvider rootServiceProvider,
|
||||
MediatR.IStreamRequest<TResponse> request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(rootServiceProvider);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
return EnumerateScopedMediatRStreamAsync(rootServiceProvider, request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建承载 NuGet `Mediator` source-generated concrete mediator 的最小对照宿主。
|
||||
/// </summary>
|
||||
/// <param name="configure">补充当前场景的显式服务注册。</param>
|
||||
/// <returns>可直接解析 generated `Mediator.Mediator` 的 DI 宿主。</returns>
|
||||
/// <remarks>
|
||||
/// `Mediator` 的 DI lifetime 由 source generator 在编译期固定到整个位于当前项目中的生成产物上。
|
||||
/// 因此 benchmark 工程必须统一使用一套 compile-time 常量配置;这里显式收敛为 `Singleton`,
|
||||
/// 避免同一编译产物里混入多个 `AddMediator` lifetime 形状后,在 BenchmarkDotNet 自动生成宿主中触发
|
||||
/// “generated code lifetime 与 runtime options 不一致”的启动失败。
|
||||
/// </remarks>
|
||||
internal static ServiceProvider CreateMediatorServiceProvider(Action<IServiceCollection>? configure)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
configure?.Invoke(services);
|
||||
services.AddMediator(static options => options.ServiceLifetime = ServiceLifetime.Singleton);
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断某个类型是否正好实现了指定的闭合或开放 MediatR 合同。
|
||||
/// </summary>
|
||||
@ -90,4 +303,43 @@ internal static class BenchmarkHostFactory
|
||||
interfaceType.IsGenericType &&
|
||||
interfaceType.GetGenericTypeDefinition() == openGenericContract);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在单个显式作用域内创建并枚举 GFramework.CQRS stream。
|
||||
/// </summary>
|
||||
private static async IAsyncEnumerable<TResponse> EnumerateScopedGFrameworkStreamAsync<TResponse>(
|
||||
ICqrsRuntime runtime,
|
||||
ScopedBenchmarkContainer scopedContainer,
|
||||
ICqrsContext context,
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest<TResponse> request,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
using var scopeLease = scopedContainer.EnterScope();
|
||||
var stream = runtime.CreateStream(context, request, cancellationToken);
|
||||
|
||||
await foreach (var response in stream.ConfigureAwait(false))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
yield return response;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在单个显式作用域内创建并枚举 MediatR stream。
|
||||
/// </summary>
|
||||
private static async IAsyncEnumerable<TResponse> EnumerateScopedMediatRStreamAsync<TResponse>(
|
||||
ServiceProvider rootServiceProvider,
|
||||
MediatR.IStreamRequest<TResponse> request,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
using var scope = rootServiceProvider.CreateScope();
|
||||
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
var stream = mediator.CreateStream(request, cancellationToken);
|
||||
|
||||
await foreach (var response in stream.ConfigureAwait(false))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
yield return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,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,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);
|
||||
}
|
||||
}
|
||||
@ -16,19 +16,22 @@ 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 开销。
|
||||
/// 对比单处理器 notification 在 GFramework.CQRS、NuGet `Mediator` 与 MediatR 之间的 publish 开销。
|
||||
/// </summary>
|
||||
[Config(typeof(Config))]
|
||||
public class NotificationBenchmarks
|
||||
{
|
||||
private MicrosoftDiContainer _container = null!;
|
||||
private ICqrsRuntime _runtime = null!;
|
||||
private ServiceProvider _serviceProvider = null!;
|
||||
private IPublisher _publisher = null!;
|
||||
private ServiceProvider _mediatrServiceProvider = null!;
|
||||
private ServiceProvider _mediatorServiceProvider = null!;
|
||||
private IPublisher _mediatrPublisher = null!;
|
||||
private GeneratedMediator _mediator = null!;
|
||||
private BenchmarkNotification _notification = null!;
|
||||
|
||||
/// <summary>
|
||||
@ -67,28 +70,32 @@ public class NotificationBenchmarks
|
||||
_container,
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(NotificationBenchmarks)));
|
||||
|
||||
_serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
_mediatrServiceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
services => services.AddSingleton<MediatR.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler>(),
|
||||
typeof(NotificationBenchmarks),
|
||||
static candidateType => candidateType == typeof(BenchmarkNotificationHandler),
|
||||
ServiceLifetime.Singleton);
|
||||
_publisher = _serviceProvider.GetRequiredService<IPublisher>();
|
||||
_mediatrPublisher = _mediatrServiceProvider.GetRequiredService<IPublisher>();
|
||||
|
||||
_mediatorServiceProvider = BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null);
|
||||
_mediator = _mediatorServiceProvider.GetRequiredService<GeneratedMediator>();
|
||||
|
||||
_notification = new BenchmarkNotification(Guid.NewGuid());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放 MediatR 对照组使用的 DI 宿主。
|
||||
/// 释放 MediatR 与 `Mediator` 对照组使用的 DI 宿主。
|
||||
/// </summary>
|
||||
[GlobalCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _mediatrServiceProvider, _mediatorServiceProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 GFramework.CQRS runtime 发布 notification。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 GFramework.CQRS publish 完成的值任务。</returns>
|
||||
[Benchmark(Baseline = true)]
|
||||
public ValueTask PublishNotification_GFrameworkCqrs()
|
||||
{
|
||||
@ -98,10 +105,21 @@ public class NotificationBenchmarks
|
||||
/// <summary>
|
||||
/// 通过 MediatR 发布 notification,作为外部设计对照。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 MediatR publish 完成的任务。</returns>
|
||||
[Benchmark]
|
||||
public Task PublishNotification_MediatR()
|
||||
{
|
||||
return _publisher.Publish(_notification, CancellationToken.None);
|
||||
return _mediatrPublisher.Publish(_notification, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 `Mediator` source-generated concrete mediator 发布 notification,作为高性能对照组。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 `Mediator` publish 完成的值任务。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask PublishNotification_Mediator()
|
||||
{
|
||||
return _mediator.Publish(_notification, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -110,13 +128,15 @@ public class NotificationBenchmarks
|
||||
/// <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。
|
||||
/// 同时实现 GFramework.CQRS、NuGet `Mediator` 与 MediatR 契约的最小 notification handler。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkNotificationHandler :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>,
|
||||
Mediator.INotificationHandler<BenchmarkNotification>,
|
||||
MediatR.INotificationHandler<BenchmarkNotification>
|
||||
{
|
||||
/// <summary>
|
||||
@ -127,6 +147,16 @@ public class NotificationBenchmarks
|
||||
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>
|
||||
|
||||
@ -0,0 +1,355 @@
|
||||
// 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>
|
||||
/// <returns>代表基线顺序调用 4 个处理器完成当前 notification 处理的值任务。</returns>
|
||||
[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>
|
||||
/// <returns>代表当前默认顺序发布器 publish 完成的值任务。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask PublishNotification_GFrameworkCqrsSequential()
|
||||
{
|
||||
return _sequentialRuntime.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过内置 <c>Task.WhenAll(...)</c> 发布器的 GFramework.CQRS runtime 发布固定 4 处理器的 notification。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 <c>Task.WhenAll(...)</c> 发布器 publish 完成的值任务。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask PublishNotification_GFrameworkCqrsTaskWhenAll()
|
||||
{
|
||||
return _taskWhenAllRuntime.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 MediatR 发布固定 4 处理器的 notification,作为外部设计对照。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 MediatR publish 完成的任务。</returns>
|
||||
[Benchmark]
|
||||
public Task PublishNotification_MediatR()
|
||||
{
|
||||
return _mediatrPublisher.Publish(_notification, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 `Mediator` source-generated concrete mediator 发布固定 4 处理器的 notification,作为高性能对照组。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 `Mediator` publish 完成的值任务。</returns>
|
||||
[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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,321 @@
|
||||
// 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;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 对比单处理器 notification publish 在不同 handler 生命周期下的额外开销。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 当前矩阵覆盖 <c>Singleton</c>、<c>Scoped</c> 与 <c>Transient</c>。
|
||||
/// 其中 <c>Scoped</c> 会在每次 notification publish 前显式创建并释放真实的 DI 作用域,
|
||||
/// 避免把 scoped handler 错误地压到根容器解析而扭曲生命周期对照。
|
||||
/// </remarks>
|
||||
[Config(typeof(Config))]
|
||||
public class NotificationLifetimeBenchmarks
|
||||
{
|
||||
private MicrosoftDiContainer _container = null!;
|
||||
private ICqrsRuntime? _runtime;
|
||||
private ScopedBenchmarkContainer? _scopedContainer;
|
||||
private ICqrsRuntime? _scopedRuntime;
|
||||
private ServiceProvider _serviceProvider = null!;
|
||||
private IPublisher? _publisher;
|
||||
private BenchmarkNotificationHandler _baselineHandler = null!;
|
||||
private BenchmarkNotification _notification = null!;
|
||||
private ILogger _runtimeLogger = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 控制当前 benchmark 使用的 handler 生命周期。
|
||||
/// </summary>
|
||||
[Params(HandlerLifetime.Singleton, HandlerLifetime.Scoped, HandlerLifetime.Transient)]
|
||||
public HandlerLifetime Lifetime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 可公平比较的 benchmark handler 生命周期集合。
|
||||
/// </summary>
|
||||
public enum HandlerLifetime
|
||||
{
|
||||
/// <summary>
|
||||
/// 复用单个 handler 实例。
|
||||
/// </summary>
|
||||
Singleton,
|
||||
|
||||
/// <summary>
|
||||
/// 每次 publish 在显式作用域内解析并复用 handler 实例。
|
||||
/// </summary>
|
||||
Scoped,
|
||||
|
||||
/// <summary>
|
||||
/// 每次 publish 都重新解析新的 handler 实例。
|
||||
/// </summary>
|
||||
Transient
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 notification lifetime benchmark 的公共输出格式。
|
||||
/// </summary>
|
||||
private sealed class Config : ManualConfig
|
||||
{
|
||||
public Config()
|
||||
{
|
||||
AddJob(Job.Default);
|
||||
AddColumnProvider(DefaultColumnProviders.Instance);
|
||||
AddColumn(new CustomColumn("Scenario", static (_, _) => "NotificationLifetime"));
|
||||
AddDiagnoser(MemoryDiagnoser.Default);
|
||||
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建当前生命周期下的 GFramework 与 MediatR notification 对照宿主。
|
||||
/// </summary>
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Fatal
|
||||
};
|
||||
Fixture.Setup($"NotificationLifetime/{Lifetime}", handlerCount: 1, pipelineCount: 0);
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
|
||||
_baselineHandler = new BenchmarkNotificationHandler();
|
||||
_notification = new BenchmarkNotification(Guid.NewGuid());
|
||||
_runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(NotificationLifetimeBenchmarks) + "." + Lifetime);
|
||||
|
||||
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
RegisterGFrameworkHandler(container, Lifetime);
|
||||
});
|
||||
|
||||
if (Lifetime != HandlerLifetime.Scoped)
|
||||
{
|
||||
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(_container, _runtimeLogger);
|
||||
}
|
||||
else
|
||||
{
|
||||
_scopedContainer = new ScopedBenchmarkContainer(_container);
|
||||
_scopedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(_scopedContainer, _runtimeLogger);
|
||||
}
|
||||
|
||||
_serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
configure: null,
|
||||
typeof(NotificationLifetimeBenchmarks),
|
||||
static candidateType => candidateType == typeof(BenchmarkNotificationHandler),
|
||||
ResolveMediatRLifetime(Lifetime));
|
||||
if (Lifetime != HandlerLifetime.Scoped)
|
||||
{
|
||||
_publisher = _serviceProvider.GetRequiredService<IPublisher>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放当前生命周期矩阵持有的 benchmark 宿主资源。
|
||||
/// </summary>
|
||||
[GlobalCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
try
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_scopedContainer, _container, _serviceProvider);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接调用 handler,作为不同生命周期矩阵下的 publish 额外开销 baseline。
|
||||
/// </summary>
|
||||
/// <returns>代表基线 handler 完成当前 notification 处理的值任务。</returns>
|
||||
[Benchmark(Baseline = true)]
|
||||
public ValueTask PublishNotification_Baseline()
|
||||
{
|
||||
return _baselineHandler.Handle(_notification, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 GFramework.CQRS runtime 发布 notification。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 GFramework.CQRS publish 完成的值任务。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask PublishNotification_GFrameworkCqrs()
|
||||
{
|
||||
if (Lifetime == HandlerLifetime.Scoped)
|
||||
{
|
||||
return PublishScopedGFrameworkNotificationAsync(
|
||||
_scopedRuntime!,
|
||||
_scopedContainer!,
|
||||
_notification,
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
return _runtime!.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 MediatR 发布 notification,作为外部对照。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 MediatR publish 完成的任务。</returns>
|
||||
[Benchmark]
|
||||
public Task PublishNotification_MediatR()
|
||||
{
|
||||
if (Lifetime == HandlerLifetime.Scoped)
|
||||
{
|
||||
return PublishScopedMediatRNotificationAsync(_serviceProvider, _notification, CancellationToken.None);
|
||||
}
|
||||
|
||||
return _publisher!.Publish(_notification, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按生命周期把 benchmark notification handler 注册到 GFramework 容器。
|
||||
/// </summary>
|
||||
/// <param name="container">当前 benchmark 拥有并负责释放的容器。</param>
|
||||
/// <param name="lifetime">待比较的 handler 生命周期。</param>
|
||||
private static void RegisterGFrameworkHandler(MicrosoftDiContainer container, HandlerLifetime lifetime)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
switch (lifetime)
|
||||
{
|
||||
case HandlerLifetime.Singleton:
|
||||
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler>();
|
||||
return;
|
||||
|
||||
case HandlerLifetime.Scoped:
|
||||
container.RegisterScoped<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler>();
|
||||
return;
|
||||
|
||||
case HandlerLifetime.Transient:
|
||||
container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler>();
|
||||
return;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 benchmark 生命周期映射为 MediatR 组装所需的 <see cref="ServiceLifetime" />。
|
||||
/// </summary>
|
||||
/// <param name="lifetime">待比较的 handler 生命周期。</param>
|
||||
private static ServiceLifetime ResolveMediatRLifetime(HandlerLifetime lifetime)
|
||||
{
|
||||
return lifetime switch
|
||||
{
|
||||
HandlerLifetime.Singleton => ServiceLifetime.Singleton,
|
||||
HandlerLifetime.Scoped => ServiceLifetime.Scoped,
|
||||
HandlerLifetime.Transient => ServiceLifetime.Transient,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在真实的 publish 级作用域内执行一次 GFramework.CQRS notification 分发。
|
||||
/// </summary>
|
||||
/// <param name="runtime">复用的 scoped benchmark runtime。</param>
|
||||
/// <param name="scopedContainer">负责为每次 publish 激活独立作用域的只读容器适配层。</param>
|
||||
/// <param name="notification">要发布的 notification。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>代表当前 publish 完成的值任务。</returns>
|
||||
/// <remarks>
|
||||
/// notification lifetime benchmark 只关心 handler 解析和 publish 本身的热路径,
|
||||
/// 因此这里复用同一个 runtime,但在每次调用前后显式创建并释放新的 DI 作用域,
|
||||
/// 让 scoped handler 真正绑定到 publish 边界。
|
||||
/// </remarks>
|
||||
private static async ValueTask PublishScopedGFrameworkNotificationAsync(
|
||||
ICqrsRuntime runtime,
|
||||
ScopedBenchmarkContainer scopedContainer,
|
||||
BenchmarkNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
ArgumentNullException.ThrowIfNull(scopedContainer);
|
||||
ArgumentNullException.ThrowIfNull(notification);
|
||||
|
||||
using var scopeLease = scopedContainer.EnterScope();
|
||||
await runtime.PublishAsync(BenchmarkContext.Instance, notification, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在真实的 publish 级作用域内执行一次 MediatR notification 分发。
|
||||
/// </summary>
|
||||
/// <param name="rootServiceProvider">当前 benchmark 的根 <see cref="ServiceProvider" />。</param>
|
||||
/// <param name="notification">要发布的 notification。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>代表当前 publish 完成的任务。</returns>
|
||||
/// <remarks>
|
||||
/// 这里显式从新的 scope 解析 <see cref="IPublisher" />,确保 <c>Scoped</c> handler 与依赖绑定到 publish 边界。
|
||||
/// </remarks>
|
||||
private static async Task PublishScopedMediatRNotificationAsync(
|
||||
ServiceProvider rootServiceProvider,
|
||||
BenchmarkNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(rootServiceProvider);
|
||||
ArgumentNullException.ThrowIfNull(notification);
|
||||
|
||||
using var scope = rootServiceProvider.CreateScope();
|
||||
var publisher = scope.ServiceProvider.GetRequiredService<IPublisher>();
|
||||
await publisher.Publish(notification, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark notification。
|
||||
/// </summary>
|
||||
/// <param name="Id">通知标识。</param>
|
||||
public sealed record BenchmarkNotification(Guid Id) :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.INotification,
|
||||
MediatR.INotification;
|
||||
|
||||
/// <summary>
|
||||
/// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 notification handler。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkNotificationHandler :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>,
|
||||
MediatR.INotificationHandler<BenchmarkNotification>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 GFramework.CQRS notification。
|
||||
/// </summary>
|
||||
/// <param name="notification">当前要处理的 notification。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>代表当前 notification 处理完成的值任务。</returns>
|
||||
public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(notification);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 MediatR notification。
|
||||
/// </summary>
|
||||
Task MediatR.INotificationHandler<BenchmarkNotification>.Handle(
|
||||
BenchmarkNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(notification);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,289 @@
|
||||
// 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 ILogger = GFramework.Core.Abstractions.Logging.ILogger;
|
||||
using GeneratedMediator = Mediator.Mediator;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 对比 notification 宿主在 GFramework.CQRS、NuGet `Mediator` 与 MediatR 之间的初始化与首次发布成本。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该矩阵刻意保持“单 notification + 单 handler + 最小宿主”的对称形状,
|
||||
/// 只观察宿主构建与首个 publish 命中的额外开销,不把 fan-out 或自定义发布策略混入 startup 结论。
|
||||
/// </remarks>
|
||||
[Config(typeof(Config))]
|
||||
public class NotificationStartupBenchmarks
|
||||
{
|
||||
private static readonly ILogger RuntimeLogger = CreateLogger(nameof(NotificationStartupBenchmarks));
|
||||
private static readonly BenchmarkNotification Notification = new(Guid.NewGuid());
|
||||
|
||||
private MicrosoftDiContainer _container = null!;
|
||||
private ICqrsRuntime _runtime = null!;
|
||||
private ServiceProvider _serviceProvider = null!;
|
||||
private IPublisher _publisher = null!;
|
||||
private ServiceProvider _mediatorServiceProvider = null!;
|
||||
private GeneratedMediator _mediator = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 配置 notification startup benchmark 的公共输出格式。
|
||||
/// </summary>
|
||||
private sealed class Config : ManualConfig
|
||||
{
|
||||
public Config()
|
||||
{
|
||||
AddJob(Job.Default
|
||||
.WithId("ColdStart")
|
||||
.WithInvocationCount(1)
|
||||
.WithUnrollFactor(1));
|
||||
AddColumnProvider(DefaultColumnProviders.Instance);
|
||||
AddColumn(new CustomColumn("Scenario", static (_, _) => "NotificationStartup"), TargetMethodColumn.Method, CategoriesColumn.Default);
|
||||
AddDiagnoser(MemoryDiagnoser.Default);
|
||||
AddLogicalGroupRules(BenchmarkLogicalGroupRule.ByCategory);
|
||||
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建 startup benchmark 复用的最小 notification 宿主对象。
|
||||
/// </summary>
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
Fixture.Setup("NotificationStartup", handlerCount: 1, pipelineCount: 0);
|
||||
|
||||
_serviceProvider = CreateMediatRServiceProvider();
|
||||
_publisher = _serviceProvider.GetRequiredService<IPublisher>();
|
||||
|
||||
_mediatorServiceProvider = CreateMediatorServiceProvider();
|
||||
_mediator = _mediatorServiceProvider.GetRequiredService<GeneratedMediator>();
|
||||
|
||||
_container = CreateGFrameworkContainer();
|
||||
_runtime = CreateGFrameworkRuntime(_container);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在每次 cold-start 迭代前清空 dispatcher 静态缓存,确保每组 benchmark 都重新命中首次绑定路径。
|
||||
/// </summary>
|
||||
[IterationSetup]
|
||||
public void ResetColdStartCaches()
|
||||
{
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放 startup benchmark 复用的宿主对象。
|
||||
/// </summary>
|
||||
[GlobalCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider, _mediatorServiceProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回已构建宿主中的 MediatR publisher,作为 initialization 组的句柄解析 baseline。
|
||||
/// </summary>
|
||||
/// <returns>当前 benchmark 复用的 MediatR publisher。</returns>
|
||||
[Benchmark(Baseline = true)]
|
||||
[BenchmarkCategory("Initialization")]
|
||||
public IPublisher Initialization_MediatR()
|
||||
{
|
||||
return _publisher;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回已构建宿主中的 GFramework.CQRS runtime,确保与 MediatR baseline 处于相同初始化阶段。
|
||||
/// </summary>
|
||||
/// <returns>当前 benchmark 复用的 GFramework.CQRS runtime。</returns>
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("Initialization")]
|
||||
public ICqrsRuntime Initialization_GFrameworkCqrs()
|
||||
{
|
||||
return _runtime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回已构建宿主中的 `Mediator` concrete mediator,作为 source-generated 对照组的初始化句柄。
|
||||
/// </summary>
|
||||
/// <returns>当前 benchmark 复用的 `Mediator` concrete mediator。</returns>
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("Initialization")]
|
||||
public GeneratedMediator Initialization_Mediator()
|
||||
{
|
||||
return _mediator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在新宿主上首次发布 notification,作为 MediatR 的 cold-start baseline。
|
||||
/// </summary>
|
||||
/// <returns>代表首次 publish 完成的任务。</returns>
|
||||
[Benchmark(Baseline = true)]
|
||||
[BenchmarkCategory("ColdStart")]
|
||||
public async Task ColdStart_MediatR()
|
||||
{
|
||||
using var serviceProvider = CreateMediatRServiceProvider();
|
||||
var publisher = serviceProvider.GetRequiredService<IPublisher>();
|
||||
await publisher.Publish(Notification, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在新 runtime 上首次发布 notification,量化 GFramework.CQRS 的 first-hit 成本。
|
||||
/// </summary>
|
||||
/// <returns>代表首次 publish 完成的值任务。</returns>
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("ColdStart")]
|
||||
public async ValueTask ColdStart_GFrameworkCqrs()
|
||||
{
|
||||
using var container = CreateGFrameworkContainer();
|
||||
var runtime = CreateGFrameworkRuntime(container);
|
||||
await runtime.PublishAsync(BenchmarkContext.Instance, Notification, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在新的 `Mediator` 宿主上首次发布 notification,量化 source-generated concrete path 的 cold-start 成本。
|
||||
/// </summary>
|
||||
/// <returns>代表首次 publish 完成的值任务。</returns>
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("ColdStart")]
|
||||
public async ValueTask ColdStart_Mediator()
|
||||
{
|
||||
using var serviceProvider = CreateMediatorServiceProvider();
|
||||
var mediator = serviceProvider.GetRequiredService<GeneratedMediator>();
|
||||
await mediator.Publish(Notification, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建只承载当前 benchmark notification 的最小 GFramework.CQRS runtime。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// startup benchmark 只需要验证单 handler publish 的首击路径,
|
||||
/// 因此这里继续使用单点手工注册,避免把更广泛的注册协调逻辑混入结果。
|
||||
/// </remarks>
|
||||
private static MicrosoftDiContainer CreateGFrameworkContainer()
|
||||
{
|
||||
return BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static container =>
|
||||
{
|
||||
container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler>();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于已冻结的 benchmark 容器构建最小 GFramework.CQRS runtime。
|
||||
/// </summary>
|
||||
/// <param name="container">当前 benchmark 拥有并负责释放的容器。</param>
|
||||
/// <returns>可直接发布 notification 的 runtime。</returns>
|
||||
private static ICqrsRuntime CreateGFrameworkRuntime(MicrosoftDiContainer container)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
return GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, RuntimeLogger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建只承载当前 benchmark notification handler 的最小 MediatR 对照宿主。
|
||||
/// </summary>
|
||||
/// <returns>可直接解析 <see cref="IPublisher" /> 的 DI 宿主。</returns>
|
||||
private static ServiceProvider CreateMediatRServiceProvider()
|
||||
{
|
||||
return BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
configure: null,
|
||||
typeof(NotificationStartupBenchmarks),
|
||||
static candidateType => candidateType == typeof(BenchmarkNotificationHandler),
|
||||
ServiceLifetime.Transient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建只承载当前 benchmark notification handler 的最小 `Mediator` 对照宿主。
|
||||
/// </summary>
|
||||
/// <returns>可直接解析 generated `Mediator.Mediator` 的 DI 宿主。</returns>
|
||||
private static ServiceProvider CreateMediatorServiceProvider()
|
||||
{
|
||||
return BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 benchmark 创建稳定的 fatal 级 logger,避免把日志成本混入 startup 测量。
|
||||
/// </summary>
|
||||
/// <param name="categoryName">logger 分类名。</param>
|
||||
/// <returns>当前 benchmark 使用的稳定 logger。</returns>
|
||||
private static ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Fatal
|
||||
};
|
||||
return LoggerFactoryResolver.Provider.CreateLogger(categoryName);
|
||||
}
|
||||
|
||||
/// <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、NuGet `Mediator` 与 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>
|
||||
/// <param name="notification">当前 notification。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>表示处理完成的值任务。</returns>
|
||||
public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(notification);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 NuGet `Mediator` notification。
|
||||
/// </summary>
|
||||
/// <param name="notification">当前 notification。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>表示处理完成的值任务。</returns>
|
||||
ValueTask Mediator.INotificationHandler<BenchmarkNotification>.Handle(
|
||||
BenchmarkNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Handle(notification, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 MediatR notification。
|
||||
/// </summary>
|
||||
/// <param name="notification">当前 notification。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>表示处理完成的任务。</returns>
|
||||
Task MediatR.INotificationHandler<BenchmarkNotification>.Handle(
|
||||
BenchmarkNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Handle(notification, cancellationToken).AsTask();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -16,19 +16,22 @@ 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 与 MediatR 之间的 steady-state dispatch 开销。
|
||||
/// 对比单个 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 _serviceProvider = 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!;
|
||||
|
||||
@ -58,39 +61,50 @@ public class RequestBenchmarks
|
||||
MinLevel = LogLevel.Fatal
|
||||
};
|
||||
Fixture.Setup("Request", handlerCount: 1, pipelineCount: 0);
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
|
||||
_baselineHandler = new BenchmarkRequestHandler();
|
||||
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>>(
|
||||
_baselineHandler);
|
||||
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedDefaultRequestBenchmarkRegistry>(container);
|
||||
});
|
||||
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_container,
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestBenchmarks)));
|
||||
|
||||
_serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
_mediatrServiceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
configure: null,
|
||||
typeof(RequestBenchmarks),
|
||||
static candidateType => candidateType == typeof(BenchmarkRequestHandler),
|
||||
ServiceLifetime.Singleton);
|
||||
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
|
||||
_mediatr = _mediatrServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
_mediatorServiceProvider = BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null);
|
||||
_mediator = _mediatorServiceProvider.GetRequiredService<GeneratedMediator>();
|
||||
|
||||
_request = new BenchmarkRequest(Guid.NewGuid());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放 MediatR 对照组使用的 DI 宿主。
|
||||
/// 释放 MediatR 与 `Mediator` 对照组使用的 DI 宿主。
|
||||
/// </summary>
|
||||
[GlobalCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
|
||||
try
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _mediatrServiceProvider, _mediatorServiceProvider);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接调用 handler,作为 dispatch 额外开销的 baseline。
|
||||
/// </summary>
|
||||
/// <returns>代表基线 handler 完成当前 request 处理的值任务。</returns>
|
||||
[Benchmark(Baseline = true)]
|
||||
public ValueTask<BenchmarkResponse> SendRequest_Baseline()
|
||||
{
|
||||
@ -100,6 +114,7 @@ public class RequestBenchmarks
|
||||
/// <summary>
|
||||
/// 通过 GFramework.CQRS runtime 发送 request。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 GFramework.CQRS request dispatch 完成的值任务。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask<BenchmarkResponse> SendRequest_GFrameworkCqrs()
|
||||
{
|
||||
@ -109,18 +124,30 @@ public class RequestBenchmarks
|
||||
/// <summary>
|
||||
/// 通过 MediatR 发送 request,作为外部设计对照。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 MediatR request dispatch 完成的任务。</returns>
|
||||
[Benchmark]
|
||||
public Task<BenchmarkResponse> SendRequest_MediatR()
|
||||
{
|
||||
return _mediatr.Send(_request, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 `ai-libs/Mediator` 的 source-generated concrete mediator 发送 request,作为高性能对照组。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 `Mediator` request dispatch 完成的值任务。</returns>
|
||||
[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>
|
||||
@ -130,10 +157,11 @@ public class RequestBenchmarks
|
||||
public sealed record BenchmarkResponse(Guid Id);
|
||||
|
||||
/// <summary>
|
||||
/// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 request handler。
|
||||
/// 同时实现 GFramework.CQRS、NuGet `Mediator` 与 MediatR 契约的最小 request handler。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkRequestHandler :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>,
|
||||
Mediator.IRequestHandler<BenchmarkRequest, BenchmarkResponse>,
|
||||
MediatR.IRequestHandler<BenchmarkRequest, BenchmarkResponse>
|
||||
{
|
||||
/// <summary>
|
||||
@ -144,6 +172,16 @@ public class RequestBenchmarks
|
||||
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>
|
||||
|
||||
@ -83,7 +83,7 @@ public class RequestInvokerBenchmarks
|
||||
|
||||
_generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
container.RegisterCqrsHandlersFromAssembly(typeof(RequestInvokerBenchmarks).Assembly);
|
||||
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedRequestInvokerBenchmarkRegistry>(container);
|
||||
});
|
||||
_generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_generatedContainer,
|
||||
@ -116,6 +116,7 @@ public class RequestInvokerBenchmarks
|
||||
/// <summary>
|
||||
/// 直接调用最小 request handler,作为 dispatch 额外开销 baseline。
|
||||
/// </summary>
|
||||
/// <returns>代表基线 request handler 完成当前 request 处理的值任务。</returns>
|
||||
[Benchmark(Baseline = true)]
|
||||
public ValueTask<ReflectionBenchmarkResponse> SendRequest_Baseline()
|
||||
{
|
||||
@ -125,6 +126,7 @@ public class RequestInvokerBenchmarks
|
||||
/// <summary>
|
||||
/// 通过 GFramework.CQRS 反射 request binding 路径发送 request。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 reflection request dispatch 完成的值任务。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask<ReflectionBenchmarkResponse> SendRequest_GFrameworkReflection()
|
||||
{
|
||||
@ -134,6 +136,7 @@ public class RequestInvokerBenchmarks
|
||||
/// <summary>
|
||||
/// 通过 generated request invoker provider 预热后的 GFramework.CQRS runtime 发送 request。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 generated request dispatch 完成的值任务。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask<GeneratedBenchmarkResponse> SendRequest_GFrameworkGenerated()
|
||||
{
|
||||
@ -143,6 +146,7 @@ public class RequestInvokerBenchmarks
|
||||
/// <summary>
|
||||
/// 通过 MediatR 发送 request,作为外部对照。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 MediatR request dispatch 完成的任务。</returns>
|
||||
[Benchmark]
|
||||
public Task<MediatRBenchmarkResponse> SendRequest_MediatR()
|
||||
{
|
||||
|
||||
@ -0,0 +1,289 @@
|
||||
// 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;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 对比 request steady-state dispatch 在不同 handler 生命周期下的额外开销。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 当前矩阵覆盖 `Singleton`、`Scoped` 与 `Transient`。
|
||||
/// 其中 `Scoped` 会在每次 request 分发时显式创建并释放真实的 DI 作用域,
|
||||
/// 避免把 scoped handler 错误地压到根容器解析而扭曲生命周期对照。
|
||||
/// NuGet `Mediator` 的 DI lifetime 由 source generator 在编译期固定到整个 benchmark 项目,
|
||||
/// 不能在同一份生成产物里同时切换 `Singleton`、`Scoped` 与 `Transient`。
|
||||
/// 因此该矩阵当前只比较 `GFramework.Cqrs` 与 `MediatR` 的生命周期开销;`Mediator` 仍保留在其他
|
||||
/// steady-state / startup benchmark 中作为单一 compile-time 形状对照。
|
||||
/// </remarks>
|
||||
[Config(typeof(Config))]
|
||||
public class RequestLifetimeBenchmarks
|
||||
{
|
||||
private MicrosoftDiContainer _container = null!;
|
||||
private ICqrsRuntime? _runtime;
|
||||
private ScopedBenchmarkContainer? _scopedContainer;
|
||||
private ICqrsRuntime? _scopedRuntime;
|
||||
private ServiceProvider _serviceProvider = null!;
|
||||
private IMediator? _mediatr;
|
||||
private BenchmarkRequestHandler _baselineHandler = null!;
|
||||
private BenchmarkRequest _request = null!;
|
||||
private ILogger _runtimeLogger = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 控制当前 benchmark 使用的 handler 生命周期。
|
||||
/// </summary>
|
||||
[Params(HandlerLifetime.Singleton, HandlerLifetime.Scoped, HandlerLifetime.Transient)]
|
||||
public HandlerLifetime Lifetime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 可公平比较的 benchmark handler 生命周期集合。
|
||||
/// </summary>
|
||||
public enum HandlerLifetime
|
||||
{
|
||||
/// <summary>
|
||||
/// 复用单个 handler 实例。
|
||||
/// </summary>
|
||||
Singleton,
|
||||
|
||||
/// <summary>
|
||||
/// 每次 request 在显式作用域内解析并复用 handler 实例。
|
||||
/// </summary>
|
||||
Scoped,
|
||||
|
||||
/// <summary>
|
||||
/// 每次分发都重新解析新的 handler 实例。
|
||||
/// </summary>
|
||||
Transient
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 request lifetime benchmark 的公共输出格式。
|
||||
/// </summary>
|
||||
private sealed class Config : ManualConfig
|
||||
{
|
||||
public Config()
|
||||
{
|
||||
AddJob(Job.Default);
|
||||
AddColumnProvider(DefaultColumnProviders.Instance);
|
||||
AddColumn(new CustomColumn("Scenario", static (_, _) => "RequestLifetime"));
|
||||
AddDiagnoser(MemoryDiagnoser.Default);
|
||||
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建当前生命周期下的 GFramework 与 MediatR request 对照宿主。
|
||||
/// </summary>
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Fatal
|
||||
};
|
||||
Fixture.Setup($"RequestLifetime/{Lifetime}", handlerCount: 1, pipelineCount: 0);
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
|
||||
_baselineHandler = new BenchmarkRequestHandler();
|
||||
_request = new BenchmarkRequest(Guid.NewGuid());
|
||||
|
||||
_runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestLifetimeBenchmarks) + "." + Lifetime);
|
||||
|
||||
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedRequestLifetimeBenchmarkRegistry>(container);
|
||||
RegisterGFrameworkHandler(container, Lifetime);
|
||||
});
|
||||
// 容器内已提前保留默认 runtime 以支撑 generated registry 接线;
|
||||
// 这里额外创建带生命周期后缀的 runtime,只是为了区分不同 benchmark 矩阵的 dispatcher 日志。
|
||||
if (Lifetime != HandlerLifetime.Scoped)
|
||||
{
|
||||
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_container,
|
||||
_runtimeLogger);
|
||||
}
|
||||
else
|
||||
{
|
||||
_scopedContainer = new ScopedBenchmarkContainer(_container);
|
||||
_scopedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_scopedContainer,
|
||||
_runtimeLogger);
|
||||
}
|
||||
|
||||
_serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
configure: null,
|
||||
typeof(RequestLifetimeBenchmarks),
|
||||
static candidateType => candidateType == typeof(BenchmarkRequestHandler),
|
||||
ResolveMediatRLifetime(Lifetime));
|
||||
if (Lifetime != HandlerLifetime.Scoped)
|
||||
{
|
||||
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放当前生命周期矩阵持有的 benchmark 宿主资源。
|
||||
/// </summary>
|
||||
[GlobalCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
try
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接调用 handler,作为不同生命周期矩阵下的 dispatch 额外开销 baseline。
|
||||
/// </summary>
|
||||
/// <returns>代表基线 request handler 完成当前 request 处理的值任务。</returns>
|
||||
[Benchmark(Baseline = true)]
|
||||
public ValueTask<BenchmarkResponse> SendRequest_Baseline()
|
||||
{
|
||||
return _baselineHandler.Handle(_request, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 GFramework.CQRS runtime 发送 request。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 GFramework.CQRS request dispatch 完成的值任务。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask<BenchmarkResponse> SendRequest_GFrameworkCqrs()
|
||||
{
|
||||
if (Lifetime == HandlerLifetime.Scoped)
|
||||
{
|
||||
return BenchmarkHostFactory.SendScopedGFrameworkRequestAsync(
|
||||
_scopedRuntime!,
|
||||
_scopedContainer!,
|
||||
BenchmarkContext.Instance,
|
||||
_request,
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
return _runtime!.SendAsync(BenchmarkContext.Instance, _request, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 MediatR 发送 request,作为外部对照。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 MediatR request dispatch 完成的任务。</returns>
|
||||
[Benchmark]
|
||||
public Task<BenchmarkResponse> SendRequest_MediatR()
|
||||
{
|
||||
if (Lifetime == HandlerLifetime.Scoped)
|
||||
{
|
||||
return BenchmarkHostFactory.SendScopedMediatRRequestAsync(
|
||||
_serviceProvider,
|
||||
_request,
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
return _mediatr!.Send(_request, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按生命周期把 benchmark request handler 注册到 GFramework 容器。
|
||||
/// </summary>
|
||||
/// <param name="container">当前 benchmark 拥有并负责释放的容器。</param>
|
||||
/// <param name="lifetime">待比较的 handler 生命周期。</param>
|
||||
/// <remarks>
|
||||
/// 先通过 generated registry 提供静态 descriptor,再显式覆盖 handler 生命周期,
|
||||
/// 可以把比较变量收敛到 handler 解析成本,而不是 descriptor 发现路径本身。
|
||||
/// </remarks>
|
||||
private static void RegisterGFrameworkHandler(MicrosoftDiContainer container, HandlerLifetime lifetime)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
switch (lifetime)
|
||||
{
|
||||
case HandlerLifetime.Singleton:
|
||||
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>, BenchmarkRequestHandler>();
|
||||
return;
|
||||
|
||||
case HandlerLifetime.Scoped:
|
||||
container.RegisterScoped<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>, BenchmarkRequestHandler>();
|
||||
return;
|
||||
|
||||
case HandlerLifetime.Transient:
|
||||
container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>, BenchmarkRequestHandler>();
|
||||
return;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 benchmark 生命周期映射为 MediatR 组装所需的 <see cref="ServiceLifetime" />。
|
||||
/// </summary>
|
||||
/// <param name="lifetime">待比较的 handler 生命周期。</param>
|
||||
private static ServiceLifetime ResolveMediatRLifetime(HandlerLifetime lifetime)
|
||||
{
|
||||
return lifetime switch
|
||||
{
|
||||
HandlerLifetime.Singleton => ServiceLifetime.Singleton,
|
||||
HandlerLifetime.Scoped => ServiceLifetime.Scoped,
|
||||
HandlerLifetime.Transient => ServiceLifetime.Transient,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark request。
|
||||
/// </summary>
|
||||
/// <param name="Id">请求标识。</param>
|
||||
public sealed record BenchmarkRequest(Guid Id) :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.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>,
|
||||
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>
|
||||
/// 处理 MediatR request。
|
||||
/// </summary>
|
||||
Task<BenchmarkResponse> MediatR.IRequestHandler<BenchmarkRequest, BenchmarkResponse>.Handle(
|
||||
BenchmarkRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(new BenchmarkResponse(request.Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -69,8 +69,7 @@ public class RequestPipelineBenchmarks
|
||||
_baselineHandler = new BenchmarkRequestHandler();
|
||||
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>>(
|
||||
_baselineHandler);
|
||||
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedRequestPipelineBenchmarkRegistry>(container);
|
||||
RegisterGFrameworkPipelineBehaviors(container, PipelineCount);
|
||||
});
|
||||
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
@ -114,6 +113,7 @@ public class RequestPipelineBenchmarks
|
||||
/// <summary>
|
||||
/// 直接调用 handler,作为 pipeline 编排之外的基线。
|
||||
/// </summary>
|
||||
/// <returns>代表基线 handler 完成当前 request 处理的值任务。</returns>
|
||||
[Benchmark(Baseline = true)]
|
||||
public ValueTask<BenchmarkResponse> SendRequest_Baseline()
|
||||
{
|
||||
@ -123,6 +123,7 @@ public class RequestPipelineBenchmarks
|
||||
/// <summary>
|
||||
/// 通过 GFramework.CQRS runtime 发送 request,并按当前矩阵配置执行 pipeline。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 GFramework.CQRS request pipeline dispatch 完成的值任务。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask<BenchmarkResponse> SendRequest_GFrameworkCqrs()
|
||||
{
|
||||
@ -132,6 +133,7 @@ public class RequestPipelineBenchmarks
|
||||
/// <summary>
|
||||
/// 通过 MediatR 发送 request,并按当前矩阵配置执行 pipeline,作为外部设计对照。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 MediatR request pipeline dispatch 完成的任务。</returns>
|
||||
[Benchmark]
|
||||
public Task<BenchmarkResponse> SendRequest_MediatR()
|
||||
{
|
||||
|
||||
@ -17,11 +17,12 @@ using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ILogger = GFramework.Core.Abstractions.Logging.ILogger;
|
||||
using GeneratedMediator = Mediator.Mediator;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 对比 request 宿主的初始化与首次分发成本,作为后续吸收 `Mediator` comparison benchmark 设计的 startup 基线。
|
||||
/// 对比 request 宿主在 GFramework.CQRS、NuGet `Mediator` 与 MediatR 之间的初始化与首次分发成本。
|
||||
/// </summary>
|
||||
[Config(typeof(Config))]
|
||||
public class RequestStartupBenchmarks
|
||||
@ -31,7 +32,9 @@ public class RequestStartupBenchmarks
|
||||
|
||||
private MicrosoftDiContainer _container = null!;
|
||||
private ServiceProvider _serviceProvider = null!;
|
||||
private ServiceProvider _mediatorServiceProvider = null!;
|
||||
private IMediator _mediatr = null!;
|
||||
private GeneratedMediator _mediator = null!;
|
||||
private ICqrsRuntime _runtime = null!;
|
||||
|
||||
/// <summary>
|
||||
@ -63,6 +66,8 @@ public class RequestStartupBenchmarks
|
||||
|
||||
_serviceProvider = CreateMediatRServiceProvider();
|
||||
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
|
||||
_mediatorServiceProvider = CreateMediatorServiceProvider();
|
||||
_mediator = _mediatorServiceProvider.GetRequiredService<GeneratedMediator>();
|
||||
_container = CreateGFrameworkContainer();
|
||||
_runtime = CreateGFrameworkRuntime(_container);
|
||||
}
|
||||
@ -86,12 +91,13 @@ public class RequestStartupBenchmarks
|
||||
[GlobalCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider, _mediatorServiceProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回已构建宿主中的 MediatR mediator,作为 initialization 组的句柄解析 baseline。
|
||||
/// </summary>
|
||||
/// <returns>当前 benchmark 复用的 MediatR mediator。</returns>
|
||||
[Benchmark(Baseline = true)]
|
||||
[BenchmarkCategory("Initialization")]
|
||||
public IMediator Initialization_MediatR()
|
||||
@ -102,6 +108,7 @@ public class RequestStartupBenchmarks
|
||||
/// <summary>
|
||||
/// 返回已构建宿主中的 GFramework.CQRS runtime,确保与 MediatR baseline 处于相同初始化阶段。
|
||||
/// </summary>
|
||||
/// <returns>当前 benchmark 复用的 GFramework.CQRS runtime。</returns>
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("Initialization")]
|
||||
public ICqrsRuntime Initialization_GFrameworkCqrs()
|
||||
@ -109,9 +116,21 @@ public class RequestStartupBenchmarks
|
||||
return _runtime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回已构建宿主中的 `Mediator` concrete mediator,作为 source-generated 对照组的初始化句柄。
|
||||
/// </summary>
|
||||
/// <returns>当前 benchmark 复用的 `Mediator` concrete mediator。</returns>
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("Initialization")]
|
||||
public GeneratedMediator Initialization_Mediator()
|
||||
{
|
||||
return _mediator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在新宿主上首次发送 request,作为 MediatR 的 cold-start baseline。
|
||||
/// </summary>
|
||||
/// <returns>当前 request 的响应结果。</returns>
|
||||
[Benchmark(Baseline = true)]
|
||||
[BenchmarkCategory("ColdStart")]
|
||||
public async Task<BenchmarkResponse> ColdStart_MediatR()
|
||||
@ -124,6 +143,7 @@ public class RequestStartupBenchmarks
|
||||
/// <summary>
|
||||
/// 在新 runtime 上首次发送 request,量化 GFramework.CQRS 的 first-hit 成本。
|
||||
/// </summary>
|
||||
/// <returns>当前 request 的响应结果。</returns>
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("ColdStart")]
|
||||
public async ValueTask<BenchmarkResponse> ColdStart_GFrameworkCqrs()
|
||||
@ -133,6 +153,19 @@ public class RequestStartupBenchmarks
|
||||
return await runtime.SendAsync(BenchmarkContext.Instance, Request, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在新的 `Mediator` 宿主上首次发送 request,量化 source-generated concrete path 的 cold-start 成本。
|
||||
/// </summary>
|
||||
/// <returns>当前 request 的响应结果。</returns>
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("ColdStart")]
|
||||
public async ValueTask<BenchmarkResponse> ColdStart_Mediator()
|
||||
{
|
||||
using var serviceProvider = CreateMediatorServiceProvider();
|
||||
var mediator = serviceProvider.GetRequiredService<GeneratedMediator>();
|
||||
return await mediator.Send(Request, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建只承载当前 benchmark request 的最小 GFramework.CQRS runtime。
|
||||
/// </summary>
|
||||
@ -170,6 +203,14 @@ public class RequestStartupBenchmarks
|
||||
ServiceLifetime.Transient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建只承载当前 benchmark request 的最小 `Mediator` 对照宿主。
|
||||
/// </summary>
|
||||
private static ServiceProvider CreateMediatorServiceProvider()
|
||||
{
|
||||
return BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 benchmark 创建稳定的 fatal 级 logger,避免把日志成本混入 startup 测量。
|
||||
/// </summary>
|
||||
@ -188,6 +229,7 @@ public class RequestStartupBenchmarks
|
||||
/// <param name="Id">请求标识。</param>
|
||||
public sealed record BenchmarkRequest(Guid Id) :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IRequest<BenchmarkResponse>,
|
||||
Mediator.IRequest<BenchmarkResponse>,
|
||||
MediatR.IRequest<BenchmarkResponse>;
|
||||
|
||||
/// <summary>
|
||||
@ -197,10 +239,11 @@ public class RequestStartupBenchmarks
|
||||
public sealed record BenchmarkResponse(Guid Id);
|
||||
|
||||
/// <summary>
|
||||
/// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 request handler。
|
||||
/// 同时实现 GFramework.CQRS、NuGet `Mediator` 与 MediatR 契约的最小 request handler。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkRequestHandler :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>,
|
||||
Mediator.IRequestHandler<BenchmarkRequest, BenchmarkResponse>,
|
||||
MediatR.IRequestHandler<BenchmarkRequest, BenchmarkResponse>
|
||||
{
|
||||
/// <summary>
|
||||
@ -211,6 +254,16 @@ public class RequestStartupBenchmarks
|
||||
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>
|
||||
|
||||
515
GFramework.Cqrs.Benchmarks/Messaging/ScopedBenchmarkContainer.cs
Normal file
515
GFramework.Cqrs.Benchmarks/Messaging/ScopedBenchmarkContainer.cs
Normal file
@ -0,0 +1,515 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using GFramework.Core.Abstractions.Bases;
|
||||
using GFramework.Core.Abstractions.Ioc;
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Ioc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 把冻结后的 benchmark 根容器适配成可重复进入的 request 级解析视图。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// `CqrsDispatcher` 会直接依赖 <see cref="IIocContainer" /> 做 handler / pipeline 解析,
|
||||
/// 因此 request lifetime benchmark 需要一个既保留根容器注册元数据,又能在每次 benchmark 调用时把实例解析切换到
|
||||
/// 显式作用域 provider 的最小适配层。该类型只覆盖 benchmark 当前 request 路径会使用到的解析相关入口;
|
||||
/// 任何注册、清空或冻结修改操作都应继续发生在根容器构建阶段,因此这里统一拒绝可变更 API。
|
||||
/// </remarks>
|
||||
internal sealed class ScopedBenchmarkContainer : IIocContainer
|
||||
{
|
||||
private readonly MicrosoftDiContainer _rootContainer;
|
||||
private IServiceScope? _activeScope;
|
||||
private IServiceProvider? _scopedProvider;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化一个绑定到单个 request 作用域的 benchmark 容器适配器。
|
||||
/// </summary>
|
||||
/// <param name="rootContainer">已冻结的 benchmark 根容器。</param>
|
||||
internal ScopedBenchmarkContainer(MicrosoftDiContainer rootContainer)
|
||||
{
|
||||
_rootContainer = rootContainer ?? throw new ArgumentNullException(nameof(rootContainer));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为当前 benchmark 调用创建并持有一个新的 request 级作用域。
|
||||
/// </summary>
|
||||
/// <returns>离开作用域时负责释放本次 request 级作用域的租约。</returns>
|
||||
/// <exception cref="InvalidOperationException">当前适配器仍持有上一次尚未释放的作用域。</exception>
|
||||
internal ScopeLease EnterScope()
|
||||
{
|
||||
if (_activeScope is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Scoped benchmark containers do not support overlapping active scopes.");
|
||||
}
|
||||
|
||||
_activeScope = _rootContainer.CreateScope();
|
||||
_scopedProvider = _activeScope.ServiceProvider;
|
||||
return new ScopeLease(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前适配器不支持在 request 作用域内追加注册。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">实例类型。</typeparam>
|
||||
/// <param name="instance">原本要注册到根容器中的单例实例。</param>
|
||||
/// <exception cref="InvalidOperationException">当前适配器始终为只读视图。</exception>
|
||||
public void RegisterSingleton<T>(T instance)
|
||||
{
|
||||
throw CreateMutationNotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前适配器不支持在 request 作用域内追加注册。
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">服务契约类型。</typeparam>
|
||||
/// <typeparam name="TImpl">服务实现类型。</typeparam>
|
||||
/// <exception cref="InvalidOperationException">当前适配器始终为只读视图。</exception>
|
||||
public void RegisterSingleton<TService, TImpl>()
|
||||
where TImpl : class, TService
|
||||
where TService : class
|
||||
{
|
||||
throw CreateMutationNotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前适配器不支持在 request 作用域内追加注册。
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">服务契约类型。</typeparam>
|
||||
/// <typeparam name="TImpl">服务实现类型。</typeparam>
|
||||
/// <exception cref="InvalidOperationException">当前适配器始终为只读视图。</exception>
|
||||
public void RegisterTransient<TService, TImpl>()
|
||||
where TImpl : class, TService
|
||||
where TService : class
|
||||
{
|
||||
throw CreateMutationNotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前适配器不支持在 request 作用域内追加注册。
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">服务契约类型。</typeparam>
|
||||
/// <typeparam name="TImpl">服务实现类型。</typeparam>
|
||||
/// <exception cref="InvalidOperationException">当前适配器始终为只读视图。</exception>
|
||||
public void RegisterScoped<TService, TImpl>()
|
||||
where TImpl : class, TService
|
||||
where TService : class
|
||||
{
|
||||
throw CreateMutationNotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前适配器不支持在 request 作用域内追加注册。
|
||||
/// </summary>
|
||||
/// <param name="instance">原本要附加到复数注册集合中的实例。</param>
|
||||
/// <exception cref="InvalidOperationException">当前适配器始终为只读视图。</exception>
|
||||
public void RegisterPlurality(object instance)
|
||||
{
|
||||
throw CreateMutationNotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前适配器不支持在 request 作用域内追加注册。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">复数注册项类型。</typeparam>
|
||||
/// <exception cref="InvalidOperationException">当前适配器始终为只读视图。</exception>
|
||||
public void RegisterPlurality<T>() where T : class
|
||||
{
|
||||
throw CreateMutationNotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前适配器不支持在 request 作用域内追加注册。
|
||||
/// </summary>
|
||||
/// <param name="system">原本要注册到容器中的系统实例。</param>
|
||||
/// <exception cref="InvalidOperationException">当前适配器始终为只读视图。</exception>
|
||||
public void RegisterSystem(ISystem system)
|
||||
{
|
||||
throw CreateMutationNotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前适配器不支持在 request 作用域内追加注册。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">实例类型。</typeparam>
|
||||
/// <param name="instance">原本要注册到容器中的实例。</param>
|
||||
/// <exception cref="InvalidOperationException">当前适配器始终为只读视图。</exception>
|
||||
public void Register<T>(T instance)
|
||||
{
|
||||
throw CreateMutationNotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前适配器不支持在 request 作用域内追加注册。
|
||||
/// </summary>
|
||||
/// <param name="type">原本要绑定的服务类型。</param>
|
||||
/// <param name="instance">原本要绑定到该类型的实例。</param>
|
||||
/// <exception cref="InvalidOperationException">当前适配器始终为只读视图。</exception>
|
||||
public void Register(Type type, object instance)
|
||||
{
|
||||
throw CreateMutationNotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前适配器不支持在 request 作用域内追加注册。
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">工厂要创建的服务类型。</typeparam>
|
||||
/// <param name="factory">原本要注册的工厂委托。</param>
|
||||
/// <exception cref="InvalidOperationException">当前适配器始终为只读视图。</exception>
|
||||
public void RegisterFactory<TService>(Func<IServiceProvider, TService> factory) where TService : class
|
||||
{
|
||||
throw CreateMutationNotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前适配器不支持在 request 作用域内追加注册。
|
||||
/// </summary>
|
||||
/// <typeparam name="TBehavior">原本要注册的 request pipeline 行为类型。</typeparam>
|
||||
/// <exception cref="InvalidOperationException">当前适配器始终为只读视图。</exception>
|
||||
public void RegisterCqrsPipelineBehavior<TBehavior>() where TBehavior : class
|
||||
{
|
||||
throw CreateMutationNotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前适配器不支持在 request 作用域内追加注册。
|
||||
/// </summary>
|
||||
/// <typeparam name="TBehavior">原本要注册的 stream pipeline 行为类型。</typeparam>
|
||||
/// <exception cref="InvalidOperationException">当前适配器始终为只读视图。</exception>
|
||||
public void RegisterCqrsStreamPipelineBehavior<TBehavior>() where TBehavior : class
|
||||
{
|
||||
throw CreateMutationNotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前适配器不支持在 request 作用域内追加注册。
|
||||
/// </summary>
|
||||
/// <param name="assembly">原本要扫描 CQRS handler 的程序集。</param>
|
||||
/// <exception cref="InvalidOperationException">当前适配器始终为只读视图。</exception>
|
||||
public void RegisterCqrsHandlersFromAssembly(System.Reflection.Assembly assembly)
|
||||
{
|
||||
throw CreateMutationNotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前适配器不支持在 request 作用域内追加注册。
|
||||
/// </summary>
|
||||
/// <param name="assemblies">原本要扫描 CQRS handler 的程序集集合。</param>
|
||||
/// <exception cref="InvalidOperationException">当前适配器始终为只读视图。</exception>
|
||||
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<System.Reflection.Assembly> assemblies)
|
||||
{
|
||||
throw CreateMutationNotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前适配器不支持执行额外的服务配置钩子。
|
||||
/// </summary>
|
||||
/// <param name="configurator">原本要执行的服务配置委托。</param>
|
||||
/// <exception cref="InvalidOperationException">当前适配器始终为只读视图。</exception>
|
||||
public void ExecuteServicesHook(Action<IServiceCollection>? configurator = null)
|
||||
{
|
||||
throw CreateMutationNotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从当前 request 作用域解析单个服务实例。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标服务类型。</typeparam>
|
||||
/// <returns>解析到的服务实例;若当前作用域未注册则返回 <see langword="null"/>。</returns>
|
||||
/// <exception cref="InvalidOperationException">调用方尚未通过 <see cref="EnterScope"/> 激活作用域。</exception>
|
||||
public T? Get<T>() where T : class
|
||||
{
|
||||
return GetScopedProvider().GetService<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从当前 request 作用域解析单个服务实例。
|
||||
/// </summary>
|
||||
/// <param name="type">目标服务类型。</param>
|
||||
/// <returns>解析到的服务实例;若当前作用域未注册则返回 <see langword="null"/>。</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="type"/> 为 <see langword="null"/>。</exception>
|
||||
/// <exception cref="InvalidOperationException">调用方尚未通过 <see cref="EnterScope"/> 激活作用域。</exception>
|
||||
public object? Get(Type type)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(type);
|
||||
return GetScopedProvider().GetService(type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从当前 request 作用域解析必需的单个服务实例。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标服务类型。</typeparam>
|
||||
/// <returns>解析到的服务实例。</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 调用方尚未通过 <see cref="EnterScope"/> 激活作用域,或当前作用域缺少必需服务。
|
||||
/// </exception>
|
||||
public T GetRequired<T>() where T : class
|
||||
{
|
||||
return GetScopedProvider().GetRequiredService<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从当前 request 作用域解析必需的单个服务实例。
|
||||
/// </summary>
|
||||
/// <param name="type">目标服务类型。</param>
|
||||
/// <returns>解析到的服务实例。</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="type"/> 为 <see langword="null"/>。</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 调用方尚未通过 <see cref="EnterScope"/> 激活作用域,或当前作用域缺少必需服务。
|
||||
/// </exception>
|
||||
public object GetRequired(Type type)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(type);
|
||||
return GetScopedProvider().GetRequiredService(type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从当前 request 作用域解析全部服务实例。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标服务类型。</typeparam>
|
||||
/// <returns>当前作用域中该服务类型的全部实例。</returns>
|
||||
/// <exception cref="InvalidOperationException">调用方尚未通过 <see cref="EnterScope"/> 激活作用域。</exception>
|
||||
public IReadOnlyList<T> GetAll<T>() where T : class
|
||||
{
|
||||
return GetScopedProvider().GetServices<T>().ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从当前 request 作用域解析全部服务实例。
|
||||
/// </summary>
|
||||
/// <param name="type">目标服务类型。</param>
|
||||
/// <returns>当前作用域中该服务类型的全部实例。</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="type"/> 为 <see langword="null"/>。</exception>
|
||||
/// <exception cref="InvalidOperationException">调用方尚未通过 <see cref="EnterScope"/> 激活作用域。</exception>
|
||||
public IReadOnlyList<object> GetAll(Type type)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(type);
|
||||
return GetScopedProvider().GetServices(type).Where(static service => service is not null).Cast<object>().ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从当前 request 作用域解析全部服务实例,并按调用方比较器排序。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标服务类型。</typeparam>
|
||||
/// <param name="comparison">用于排序的比较器。</param>
|
||||
/// <returns>按比较器排序后的服务列表。</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="comparison"/> 为 <see langword="null"/>。</exception>
|
||||
/// <exception cref="InvalidOperationException">调用方尚未通过 <see cref="EnterScope"/> 激活作用域。</exception>
|
||||
public IReadOnlyList<T> GetAllSorted<T>(Comparison<T> comparison) where T : class
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(comparison);
|
||||
|
||||
var services = GetAll<T>().ToList();
|
||||
services.Sort(comparison);
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从当前 request 作用域解析全部服务实例,并按优先级排序。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标服务类型。</typeparam>
|
||||
/// <returns>按优先级稳定排序后的服务列表。</returns>
|
||||
/// <exception cref="InvalidOperationException">调用方尚未通过 <see cref="EnterScope"/> 激活作用域。</exception>
|
||||
public IReadOnlyList<T> GetAllByPriority<T>() where T : class
|
||||
{
|
||||
return SortByPriority(GetAll<T>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从当前 request 作用域解析全部服务实例,并按优先级排序。
|
||||
/// </summary>
|
||||
/// <param name="type">目标服务类型。</param>
|
||||
/// <returns>按优先级稳定排序后的服务列表。</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="type"/> 为 <see langword="null"/>。</exception>
|
||||
/// <exception cref="InvalidOperationException">调用方尚未通过 <see cref="EnterScope"/> 激活作用域。</exception>
|
||||
public IReadOnlyList<object> GetAllByPriority(Type type)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(type);
|
||||
return SortByPriority(GetAll(type));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断根容器是否声明了目标服务键。
|
||||
/// </summary>
|
||||
/// <param name="type">目标服务类型。</param>
|
||||
/// <returns>根容器中声明了该服务键时返回 <see langword="true"/>。</returns>
|
||||
/// <remarks>
|
||||
/// `CqrsDispatcher` 在热路径上先做注册存在性判断,再决定是否枚举 pipeline;这里沿用根容器冻结后的注册视图,
|
||||
/// 避免把“当前 scope 还未物化实例”误判成“没有注册该行为”。
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="type"/> 为 <see langword="null"/>。</exception>
|
||||
public bool HasRegistration(Type type)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(type);
|
||||
return _rootContainer.HasRegistration(type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断根容器是否声明了目标服务键。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标服务类型。</typeparam>
|
||||
/// <returns>根容器中声明了该服务键时返回 <see langword="true"/>。</returns>
|
||||
public bool Contains<T>() where T : class
|
||||
{
|
||||
return _rootContainer.Contains<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前 request 作用域适配器不追踪实例归属。
|
||||
/// </summary>
|
||||
/// <param name="instance">待检查的实例。</param>
|
||||
/// <returns>若根容器已追踪该实例,则返回根容器的检查结果。</returns>
|
||||
public bool ContainsInstance(object instance)
|
||||
{
|
||||
return _rootContainer.ContainsInstance(instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前适配器不支持清空注册。
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">当前适配器始终为只读视图。</exception>
|
||||
public void Clear()
|
||||
{
|
||||
throw CreateMutationNotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前适配器不支持重新冻结。
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">当前适配器始终为只读视图。</exception>
|
||||
public void Freeze()
|
||||
{
|
||||
throw CreateMutationNotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 继续暴露根容器底层服务集合,仅用于接口兼容。
|
||||
/// </summary>
|
||||
/// <returns>根容器当前持有的底层服务集合。</returns>
|
||||
public IServiceCollection GetServicesUnsafe => _rootContainer.GetServicesUnsafe;
|
||||
|
||||
/// <summary>
|
||||
/// 基于当前 request 作用域继续创建嵌套作用域。
|
||||
/// </summary>
|
||||
/// <returns>从当前激活作用域继续派生出的子作用域。</returns>
|
||||
/// <exception cref="InvalidOperationException">调用方尚未通过 <see cref="EnterScope"/> 激活作用域。</exception>
|
||||
public IServiceScope CreateScope()
|
||||
{
|
||||
return GetScopedProvider().CreateScope();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将上下文转发给根容器,保持与 request 生命周期无关的上下文缓存行为一致。
|
||||
/// </summary>
|
||||
/// <param name="context">要绑定到根容器的架构上下文。</param>
|
||||
public void SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
|
||||
{
|
||||
((IContextAware)_rootContainer).SetContext(context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取根容器当前持有的架构上下文。
|
||||
/// </summary>
|
||||
/// <returns>根容器当前保存的架构上下文。</returns>
|
||||
public GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext()
|
||||
{
|
||||
return ((IContextAware)_rootContainer).GetContext();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放当前 request 适配器时,同时兜底释放任何尚未归还的激活作用域。
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
ReleaseActiveScope();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取当前激活的 request 级作用域服务提供器。
|
||||
/// </summary>
|
||||
/// <returns>当前作用域对应的服务提供器。</returns>
|
||||
/// <exception cref="InvalidOperationException">调用方尚未通过 <see cref="EnterScope"/> 激活作用域。</exception>
|
||||
private IServiceProvider GetScopedProvider()
|
||||
{
|
||||
return _scopedProvider ?? throw new InvalidOperationException(
|
||||
"Scoped benchmark containers require an active scope. Call EnterScope() before resolving scoped services.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放当前激活的 request 级作用域,并清空解析视图。
|
||||
/// </summary>
|
||||
private void ReleaseActiveScope()
|
||||
{
|
||||
_scopedProvider = null;
|
||||
|
||||
var activeScope = _activeScope;
|
||||
_activeScope = null;
|
||||
activeScope?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成统一的只读适配器异常,避免 benchmark 误把 request 级容器当成可变组合根。
|
||||
/// </summary>
|
||||
/// <returns>描述当前适配器只读语义的统一异常。</returns>
|
||||
private static InvalidOperationException CreateMutationNotSupportedException()
|
||||
{
|
||||
return new InvalidOperationException(
|
||||
"Scoped benchmark containers are read-only request views. Mutate registrations on the root benchmark host before freezing it.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复用与根容器一致的优先级排序语义。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">服务实例类型。</typeparam>
|
||||
/// <param name="services">待排序服务集合。</param>
|
||||
/// <returns>按优先级稳定排序后的服务列表。</returns>
|
||||
private static IReadOnlyList<T> SortByPriority<T>(IReadOnlyList<T> services) where T : class
|
||||
{
|
||||
if (services.Count <= 1)
|
||||
{
|
||||
return services;
|
||||
}
|
||||
|
||||
return services
|
||||
.Select((service, index) => new { Service = service, Index = index })
|
||||
.OrderBy(static x =>
|
||||
{
|
||||
var priority = x.Service is IPrioritized prioritized ? prioritized.Priority : 0;
|
||||
return (priority, x.Index);
|
||||
})
|
||||
.Select(static x => x.Service)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示一次激活中的 request 级作用域租约。
|
||||
/// </summary>
|
||||
internal readonly struct ScopeLease : IDisposable
|
||||
{
|
||||
private readonly ScopedBenchmarkContainer _owner;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化一个绑定到目标适配器的作用域租约。
|
||||
/// </summary>
|
||||
/// <param name="owner">拥有当前作用域的 benchmark 容器适配器。</param>
|
||||
internal ScopeLease(ScopedBenchmarkContainer owner)
|
||||
{
|
||||
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放当前 request 级作用域。
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_owner.ReleaseActiveScope();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -24,8 +24,13 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 对比 stream 完整枚举在 direct handler、GFramework 反射路径、GFramework generated invoker 路径与 MediatR 之间的开销差异。
|
||||
/// 对比 stream invoker 在 direct handler、GFramework 反射路径、GFramework generated invoker 路径与 MediatR 之间的开销差异。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该矩阵只保留单一 handler 生命周期,避免把 invoker 路径差异与生命周期解析成本混在一起。
|
||||
/// <see cref="StreamObservation.FirstItem" /> 用于近似观察建流到首个元素的瞬时成本,
|
||||
/// <see cref="StreamObservation.DrainAll" /> 则保留原有完整枚举口径。
|
||||
/// </remarks>
|
||||
[Config(typeof(Config))]
|
||||
public class StreamInvokerBenchmarks
|
||||
{
|
||||
@ -40,6 +45,28 @@ public class StreamInvokerBenchmarks
|
||||
private GeneratedBenchmarkStreamRequest _generatedRequest = null!;
|
||||
private MediatRBenchmarkStreamRequest _mediatrRequest = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 控制当前 benchmark 观察“只推进首个元素”还是“完整枚举整个 stream”。
|
||||
/// </summary>
|
||||
[Params(StreamObservation.FirstItem, StreamObservation.DrainAll)]
|
||||
public StreamObservation Observation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用于拆分 stream invoker 固定成本与后续枚举成本的观测模式。
|
||||
/// </summary>
|
||||
public enum StreamObservation
|
||||
{
|
||||
/// <summary>
|
||||
/// 只推进到首个元素后立即释放枚举器。
|
||||
/// </summary>
|
||||
FirstItem,
|
||||
|
||||
/// <summary>
|
||||
/// 完整枚举整个 stream,保留原有 benchmark 语义。
|
||||
/// </summary>
|
||||
DrainAll
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 stream invoker benchmark 的公共输出格式。
|
||||
/// </summary>
|
||||
@ -83,7 +110,7 @@ public class StreamInvokerBenchmarks
|
||||
|
||||
_generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
container.RegisterCqrsHandlersFromAssembly(typeof(StreamInvokerBenchmarks).Assembly);
|
||||
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedStreamInvokerBenchmarkRegistry>(container);
|
||||
});
|
||||
_generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_generatedContainer,
|
||||
@ -114,50 +141,110 @@ public class StreamInvokerBenchmarks
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接调用最小 stream handler 并完整枚举,作为 dispatch 额外开销 baseline。
|
||||
/// 直接调用最小 stream handler,并按当前观测模式消费 stream,作为 dispatch 额外开销 baseline。
|
||||
/// </summary>
|
||||
/// <returns>代表基线 stream 按当前观测模式消费完成的值任务。</returns>
|
||||
[Benchmark(Baseline = true)]
|
||||
public async ValueTask Stream_Baseline()
|
||||
public ValueTask Stream_Baseline()
|
||||
{
|
||||
await foreach (var response in _baselineHandler.Handle(_reflectionRequest, CancellationToken.None).ConfigureAwait(false))
|
||||
return ObserveAsync(_baselineHandler.Handle(_reflectionRequest, CancellationToken.None), Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 GFramework.CQRS 反射 stream binding 路径创建 stream,并按当前观测模式消费。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 GFramework.CQRS 反射 stream 按观测模式消费完成的值任务。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask Stream_GFrameworkReflection()
|
||||
{
|
||||
return ObserveAsync(
|
||||
_reflectionRuntime.CreateStream(
|
||||
BenchmarkContext.Instance,
|
||||
_reflectionRequest,
|
||||
CancellationToken.None),
|
||||
Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 generated stream invoker provider 预热后的 GFramework.CQRS runtime 创建 stream,并按当前观测模式消费。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 GFramework.CQRS generated stream 按观测模式消费完成的值任务。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask Stream_GFrameworkGenerated()
|
||||
{
|
||||
return ObserveAsync(
|
||||
_generatedRuntime.CreateStream(
|
||||
BenchmarkContext.Instance,
|
||||
_generatedRequest,
|
||||
CancellationToken.None),
|
||||
Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 MediatR 创建 stream,并按当前观测模式消费,作为外部对照。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 MediatR stream 按观测模式消费完成的值任务。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask Stream_MediatR()
|
||||
{
|
||||
return ObserveAsync(_mediatr.CreateStream(_mediatrRequest, CancellationToken.None), Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按观测模式消费 stream,便于把“建流/首个元素”和“完整枚举”分开观察。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前 stream 的响应类型。</typeparam>
|
||||
/// <param name="responses">待观察的异步响应序列。</param>
|
||||
/// <param name="observation">当前 benchmark 选定的观测模式。</param>
|
||||
/// <returns>异步消费完成后的等待句柄。</returns>
|
||||
private static ValueTask ObserveAsync<TResponse>(
|
||||
IAsyncEnumerable<TResponse> responses,
|
||||
StreamObservation observation)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(responses);
|
||||
|
||||
return observation switch
|
||||
{
|
||||
_ = response;
|
||||
StreamObservation.FirstItem => ConsumeFirstItemAsync(responses, CancellationToken.None),
|
||||
StreamObservation.DrainAll => DrainAsync(responses),
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(observation),
|
||||
observation,
|
||||
"Unsupported stream observation mode.")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 只推进到首个元素后立即释放枚举器,用来近似隔离建流与首个 <c>MoveNextAsync</c> 的固定成本。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前 stream 的响应类型。</typeparam>
|
||||
/// <param name="responses">待观察的异步响应序列。</param>
|
||||
/// <param name="cancellationToken">用于向异步枚举器传播取消的令牌。</param>
|
||||
/// <returns>消费首个元素后的等待句柄。</returns>
|
||||
private static async ValueTask ConsumeFirstItemAsync<TResponse>(
|
||||
IAsyncEnumerable<TResponse> responses,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var enumerator = responses.GetAsyncEnumerator(cancellationToken);
|
||||
await using (enumerator.ConfigureAwait(false))
|
||||
{
|
||||
// 这里显式读取 Current,只为了让所有路径都完成首个元素的同等消费。
|
||||
if (await enumerator.MoveNextAsync().ConfigureAwait(false))
|
||||
{
|
||||
_ = enumerator.Current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 GFramework.CQRS 反射 stream binding 路径创建并完整枚举 stream。
|
||||
/// 完整枚举整个 stream,保留原 benchmark 的总成本观测口径。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public async ValueTask Stream_GFrameworkReflection()
|
||||
/// <typeparam name="TResponse">当前 stream 的响应类型。</typeparam>
|
||||
/// <param name="responses">待完整枚举的异步响应序列。</param>
|
||||
/// <returns>完整枚举结束后的等待句柄。</returns>
|
||||
private static async ValueTask DrainAsync<TResponse>(IAsyncEnumerable<TResponse> responses)
|
||||
{
|
||||
await foreach (var response in _reflectionRuntime.CreateStream(BenchmarkContext.Instance, _reflectionRequest, CancellationToken.None)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
_ = response;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 generated stream invoker provider 预热后的 GFramework.CQRS runtime 创建并完整枚举 stream。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public async ValueTask Stream_GFrameworkGenerated()
|
||||
{
|
||||
await foreach (var response in _generatedRuntime.CreateStream(BenchmarkContext.Instance, _generatedRequest, CancellationToken.None)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
_ = response;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 MediatR 创建并完整枚举 stream,作为外部对照。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public async ValueTask Stream_MediatR()
|
||||
{
|
||||
await foreach (var response in _mediatr.CreateStream(_mediatrRequest, CancellationToken.None).ConfigureAwait(false))
|
||||
await foreach (var response in responses.ConfigureAwait(false))
|
||||
{
|
||||
_ = response;
|
||||
}
|
||||
@ -214,6 +301,9 @@ public class StreamInvokerBenchmarks
|
||||
/// <summary>
|
||||
/// 处理 reflection benchmark stream request。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 reflection benchmark stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>完整枚举所需的低噪声异步响应序列。</returns>
|
||||
public IAsyncEnumerable<ReflectionBenchmarkResponse> Handle(
|
||||
ReflectionBenchmarkStreamRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@ -235,6 +325,9 @@ public class StreamInvokerBenchmarks
|
||||
/// <summary>
|
||||
/// 处理 generated benchmark stream request。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 generated benchmark stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>完整枚举所需的低噪声异步响应序列。</returns>
|
||||
public IAsyncEnumerable<GeneratedBenchmarkResponse> Handle(
|
||||
GeneratedBenchmarkStreamRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@ -256,6 +349,9 @@ public class StreamInvokerBenchmarks
|
||||
/// <summary>
|
||||
/// 处理 MediatR benchmark stream request。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 MediatR benchmark stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>完整枚举所需的低噪声异步响应序列。</returns>
|
||||
public IAsyncEnumerable<MediatRBenchmarkResponse> Handle(
|
||||
MediatRBenchmarkStreamRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@ -271,6 +367,12 @@ public class StreamInvokerBenchmarks
|
||||
/// <summary>
|
||||
/// 为三组 stream benchmark 构造相同形状的低噪声异步枚举,避免枚举体差异干扰 invoker 对照。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前 stream 的响应类型。</typeparam>
|
||||
/// <param name="id">每个响应复用的稳定标识。</param>
|
||||
/// <param name="itemCount">待返回的响应元素数量。</param>
|
||||
/// <param name="responseFactory">将稳定标识映射为响应对象的工厂。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>供各对照路径共享的低噪声异步响应序列。</returns>
|
||||
private static async IAsyncEnumerable<TResponse> EnumerateAsync<TResponse>(
|
||||
Guid id,
|
||||
int itemCount,
|
||||
|
||||
605
GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs
Normal file
605
GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs
Normal file
@ -0,0 +1,605 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
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;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 对比 stream 在不同 handler 生命周期与观测方式下的额外开销。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 当前矩阵覆盖 `Singleton`、`Scoped` 与 `Transient`。
|
||||
/// 其中 `Scoped` 会在每次建流与枚举期间显式创建并持有真实的 DI 作用域,
|
||||
/// 避免把 scoped handler 错误地下沉到根容器解析,或在异步枚举尚未结束时提前释放作用域。
|
||||
/// <see cref="StreamObservation" /> 当前只保留 <see cref="StreamObservation.FirstItem" /> 与
|
||||
/// <see cref="StreamObservation.DrainAll" /> 两种模式,分别用于观察建流到首个元素的固定成本与完整枚举的总成本,
|
||||
/// 以避免把更多观测策略与 <see cref="StreamLifetimeBenchmarks" /> 的生命周期对照目标混在一起。
|
||||
/// </remarks>
|
||||
[Config(typeof(Config))]
|
||||
public class StreamLifetimeBenchmarks
|
||||
{
|
||||
private MicrosoftDiContainer _reflectionContainer = null!;
|
||||
private ICqrsRuntime _reflectionRuntime = null!;
|
||||
private ScopedBenchmarkContainer? _scopedReflectionContainer;
|
||||
private ICqrsRuntime? _scopedReflectionRuntime;
|
||||
private MicrosoftDiContainer _generatedContainer = null!;
|
||||
private ICqrsRuntime _generatedRuntime = null!;
|
||||
private ScopedBenchmarkContainer? _scopedGeneratedContainer;
|
||||
private ICqrsRuntime? _scopedGeneratedRuntime;
|
||||
private ServiceProvider _serviceProvider = null!;
|
||||
private IMediator _mediatr = null!;
|
||||
private ReflectionBenchmarkStreamHandler _baselineHandler = null!;
|
||||
private ReflectionBenchmarkStreamRequest _reflectionRequest = null!;
|
||||
private GeneratedBenchmarkStreamRequest _generatedRequest = null!;
|
||||
private MediatRBenchmarkStreamRequest _mediatrRequest = null!;
|
||||
private ILogger _reflectionRuntimeLogger = null!;
|
||||
private ILogger _generatedRuntimeLogger = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 控制当前 benchmark 使用的 handler 生命周期。
|
||||
/// </summary>
|
||||
[Params(HandlerLifetime.Singleton, HandlerLifetime.Scoped, HandlerLifetime.Transient)]
|
||||
public HandlerLifetime Lifetime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 控制当前 benchmark 观察“只推进首个元素”还是“完整枚举整个 stream”。
|
||||
/// </summary>
|
||||
[Params(StreamObservation.FirstItem, StreamObservation.DrainAll)]
|
||||
public StreamObservation Observation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 可公平比较的 benchmark handler 生命周期集合。
|
||||
/// </summary>
|
||||
public enum HandlerLifetime
|
||||
{
|
||||
/// <summary>
|
||||
/// 复用单个 handler 实例。
|
||||
/// </summary>
|
||||
Singleton,
|
||||
|
||||
/// <summary>
|
||||
/// 每次建流在显式作用域内解析并复用 handler 实例,且作用域会覆盖整个枚举周期。
|
||||
/// </summary>
|
||||
Scoped,
|
||||
|
||||
/// <summary>
|
||||
/// 每次建流都重新解析新的 handler 实例。
|
||||
/// </summary>
|
||||
Transient
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于拆分 stream dispatch 与后续枚举成本的观测模式。
|
||||
/// </summary>
|
||||
public enum StreamObservation
|
||||
{
|
||||
/// <summary>
|
||||
/// 只推进到首个元素后立即释放枚举器。
|
||||
/// </summary>
|
||||
FirstItem,
|
||||
|
||||
/// <summary>
|
||||
/// 完整枚举整个 stream,保留原有 benchmark 语义。
|
||||
/// </summary>
|
||||
DrainAll
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 stream 生命周期 benchmark 的公共输出格式。
|
||||
/// </summary>
|
||||
private sealed class Config : ManualConfig
|
||||
{
|
||||
public Config()
|
||||
{
|
||||
AddJob(Job.Default);
|
||||
AddColumnProvider(DefaultColumnProviders.Instance);
|
||||
AddColumn(new CustomColumn("Scenario", static (_, _) => "StreamLifetime"));
|
||||
AddDiagnoser(MemoryDiagnoser.Default);
|
||||
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建当前生命周期下的 GFramework reflection、GFramework generated 与 MediatR stream 对照宿主。
|
||||
/// </summary>
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
ConfigureBenchmarkInfrastructure();
|
||||
InitializeRequestsAndLoggers();
|
||||
InitializeReflectionRuntime();
|
||||
InitializeGeneratedRuntime();
|
||||
InitializeMediatRRuntime();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放当前生命周期矩阵持有的 benchmark 宿主资源,并清理 dispatcher 缓存。
|
||||
/// </summary>
|
||||
[GlobalCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
try
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_reflectionContainer, _generatedContainer, _serviceProvider);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接调用 handler,并按当前观测模式消费 stream,作为不同生命周期矩阵下的 dispatch 额外开销 baseline。
|
||||
/// </summary>
|
||||
/// <returns>代表基线 handler stream 按当前观测模式消费完成的值任务。</returns>
|
||||
[Benchmark(Baseline = true)]
|
||||
public ValueTask Stream_Baseline()
|
||||
{
|
||||
return ObserveAsync(_baselineHandler.Handle(_reflectionRequest, CancellationToken.None), Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 GFramework.CQRS reflection stream binding 路径创建 stream,并按当前观测模式消费。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 reflection stream 按当前观测模式消费完成的值任务。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask Stream_GFrameworkReflection()
|
||||
{
|
||||
if (Lifetime == HandlerLifetime.Scoped)
|
||||
{
|
||||
return ObserveAsync(
|
||||
BenchmarkHostFactory.CreateScopedGFrameworkStream(
|
||||
_scopedReflectionRuntime!,
|
||||
_scopedReflectionContainer!,
|
||||
BenchmarkContext.Instance,
|
||||
_reflectionRequest,
|
||||
CancellationToken.None),
|
||||
Observation);
|
||||
}
|
||||
|
||||
return ObserveAsync(
|
||||
_reflectionRuntime.CreateStream(
|
||||
BenchmarkContext.Instance,
|
||||
_reflectionRequest,
|
||||
CancellationToken.None),
|
||||
Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 generated stream invoker provider 预热后的 GFramework.CQRS runtime 创建 stream,并按当前观测模式消费。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 generated stream 按当前观测模式消费完成的值任务。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask Stream_GFrameworkGenerated()
|
||||
{
|
||||
if (Lifetime == HandlerLifetime.Scoped)
|
||||
{
|
||||
return ObserveAsync(
|
||||
BenchmarkHostFactory.CreateScopedGFrameworkStream(
|
||||
_scopedGeneratedRuntime!,
|
||||
_scopedGeneratedContainer!,
|
||||
BenchmarkContext.Instance,
|
||||
_generatedRequest,
|
||||
CancellationToken.None),
|
||||
Observation);
|
||||
}
|
||||
|
||||
return ObserveAsync(
|
||||
_generatedRuntime.CreateStream(
|
||||
BenchmarkContext.Instance,
|
||||
_generatedRequest,
|
||||
CancellationToken.None),
|
||||
Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 MediatR 创建 stream,并按当前观测模式消费,作为外部对照。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 MediatR stream 按当前观测模式消费完成的值任务。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask Stream_MediatR()
|
||||
{
|
||||
if (Lifetime == HandlerLifetime.Scoped)
|
||||
{
|
||||
return ObserveAsync(
|
||||
BenchmarkHostFactory.CreateScopedMediatRStream(
|
||||
_serviceProvider,
|
||||
_mediatrRequest,
|
||||
CancellationToken.None),
|
||||
Observation);
|
||||
}
|
||||
|
||||
return ObserveAsync(_mediatr.CreateStream(_mediatrRequest, CancellationToken.None), Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按生命周期把 reflection benchmark stream handler 注册到 GFramework 容器。
|
||||
/// </summary>
|
||||
/// <param name="container">当前 benchmark 拥有并负责释放的容器。</param>
|
||||
/// <param name="lifetime">待比较的 handler 生命周期。</param>
|
||||
private static void RegisterReflectionHandler(MicrosoftDiContainer container, HandlerLifetime lifetime)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
switch (lifetime)
|
||||
{
|
||||
case HandlerLifetime.Singleton:
|
||||
container.RegisterSingleton<
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<ReflectionBenchmarkStreamRequest, ReflectionBenchmarkResponse>,
|
||||
ReflectionBenchmarkStreamHandler>();
|
||||
return;
|
||||
|
||||
case HandlerLifetime.Scoped:
|
||||
container.RegisterScoped<
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<ReflectionBenchmarkStreamRequest, ReflectionBenchmarkResponse>,
|
||||
ReflectionBenchmarkStreamHandler>();
|
||||
return;
|
||||
|
||||
case HandlerLifetime.Transient:
|
||||
container.RegisterTransient<
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<ReflectionBenchmarkStreamRequest, ReflectionBenchmarkResponse>,
|
||||
ReflectionBenchmarkStreamHandler>();
|
||||
return;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按生命周期把 generated benchmark stream handler 注册到 GFramework 容器。
|
||||
/// </summary>
|
||||
/// <param name="container">当前 benchmark 拥有并负责释放的容器。</param>
|
||||
/// <param name="lifetime">待比较的 handler 生命周期。</param>
|
||||
/// <remarks>
|
||||
/// generated registry 只负责暴露静态 descriptor;
|
||||
/// 生命周期矩阵仍由 benchmark 主体显式覆盖 handler 注册,避免把 descriptor 发现与实例解析混在一起。
|
||||
/// </remarks>
|
||||
private static void RegisterGeneratedHandler(MicrosoftDiContainer container, HandlerLifetime lifetime)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
switch (lifetime)
|
||||
{
|
||||
case HandlerLifetime.Singleton:
|
||||
container.RegisterSingleton<
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<GeneratedBenchmarkStreamRequest, GeneratedBenchmarkResponse>,
|
||||
GeneratedBenchmarkStreamHandler>();
|
||||
return;
|
||||
|
||||
case HandlerLifetime.Scoped:
|
||||
container.RegisterScoped<
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<GeneratedBenchmarkStreamRequest, GeneratedBenchmarkResponse>,
|
||||
GeneratedBenchmarkStreamHandler>();
|
||||
return;
|
||||
|
||||
case HandlerLifetime.Transient:
|
||||
container.RegisterTransient<
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<GeneratedBenchmarkStreamRequest, GeneratedBenchmarkResponse>,
|
||||
GeneratedBenchmarkStreamHandler>();
|
||||
return;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 benchmark 生命周期映射为 MediatR 组装所需的 <see cref="ServiceLifetime" />。
|
||||
/// </summary>
|
||||
/// <param name="lifetime">待比较的 handler 生命周期。</param>
|
||||
/// <returns>当前生命周期对应的 MediatR 注册方式。</returns>
|
||||
private static ServiceLifetime ResolveMediatRLifetime(HandlerLifetime lifetime)
|
||||
{
|
||||
return lifetime switch
|
||||
{
|
||||
HandlerLifetime.Singleton => ServiceLifetime.Singleton,
|
||||
HandlerLifetime.Scoped => ServiceLifetime.Scoped,
|
||||
HandlerLifetime.Transient => ServiceLifetime.Transient,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化当前 benchmark 所需的全局日志与夹具基础设施。
|
||||
/// </summary>
|
||||
private void ConfigureBenchmarkInfrastructure()
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Fatal
|
||||
};
|
||||
Fixture.Setup($"StreamLifetime/{Lifetime}", handlerCount: 1, pipelineCount: 0);
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化当前 benchmark 会复用的请求对象、baseline handler 与日志器。
|
||||
/// </summary>
|
||||
private void InitializeRequestsAndLoggers()
|
||||
{
|
||||
_baselineHandler = new ReflectionBenchmarkStreamHandler();
|
||||
_reflectionRequest = new ReflectionBenchmarkStreamRequest(Guid.NewGuid(), 3);
|
||||
_generatedRequest = new GeneratedBenchmarkStreamRequest(Guid.NewGuid(), 3);
|
||||
_mediatrRequest = new MediatRBenchmarkStreamRequest(Guid.NewGuid(), 3);
|
||||
_reflectionRuntimeLogger =
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + ".Reflection." + Lifetime);
|
||||
_generatedRuntimeLogger =
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + ".Generated." + Lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 reflection 路径的 GFramework runtime。
|
||||
/// </summary>
|
||||
private void InitializeReflectionRuntime()
|
||||
{
|
||||
_reflectionContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
RegisterReflectionHandler(container, Lifetime);
|
||||
});
|
||||
|
||||
if (Lifetime != HandlerLifetime.Scoped)
|
||||
{
|
||||
_reflectionRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_reflectionContainer,
|
||||
_reflectionRuntimeLogger);
|
||||
return;
|
||||
}
|
||||
|
||||
_scopedReflectionContainer = new ScopedBenchmarkContainer(_reflectionContainer);
|
||||
_scopedReflectionRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_scopedReflectionContainer,
|
||||
_reflectionRuntimeLogger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 generated registry 路径的 GFramework runtime。
|
||||
/// </summary>
|
||||
private void InitializeGeneratedRuntime()
|
||||
{
|
||||
_generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedStreamLifetimeBenchmarkRegistry>(container);
|
||||
RegisterGeneratedHandler(container, Lifetime);
|
||||
});
|
||||
|
||||
// 容器内已提前保留默认 runtime 以支撑 generated registry 接线;
|
||||
// 这里额外创建带生命周期后缀的 runtime,只是为了区分不同 benchmark 矩阵的 dispatcher 日志。
|
||||
if (Lifetime != HandlerLifetime.Scoped)
|
||||
{
|
||||
_generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_generatedContainer,
|
||||
_generatedRuntimeLogger);
|
||||
return;
|
||||
}
|
||||
|
||||
_scopedGeneratedContainer = new ScopedBenchmarkContainer(_generatedContainer);
|
||||
_scopedGeneratedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_scopedGeneratedContainer,
|
||||
_generatedRuntimeLogger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 MediatR 对照宿主。
|
||||
/// </summary>
|
||||
private void InitializeMediatRRuntime()
|
||||
{
|
||||
_serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
configure: null,
|
||||
typeof(StreamLifetimeBenchmarks),
|
||||
static candidateType => candidateType == typeof(MediatRBenchmarkStreamHandler),
|
||||
ResolveMediatRLifetime(Lifetime));
|
||||
|
||||
if (Lifetime != HandlerLifetime.Scoped)
|
||||
{
|
||||
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按观测模式消费 stream,便于把“建流/首个元素”和“完整枚举”分开观察。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前 stream 的响应类型。</typeparam>
|
||||
/// <param name="responses">待观察的异步响应序列。</param>
|
||||
/// <param name="observation">当前 benchmark 选定的观测模式。</param>
|
||||
/// <returns>异步消费完成后的等待句柄。</returns>
|
||||
private static ValueTask ObserveAsync<TResponse>(
|
||||
IAsyncEnumerable<TResponse> responses,
|
||||
StreamObservation observation)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(responses);
|
||||
|
||||
return observation switch
|
||||
{
|
||||
StreamObservation.FirstItem => ConsumeFirstItemAsync(responses, CancellationToken.None),
|
||||
StreamObservation.DrainAll => DrainAsync(responses),
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(observation),
|
||||
observation,
|
||||
"Unsupported stream observation mode.")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 只推进到首个元素后立即释放枚举器,用来近似隔离建流与首个 `MoveNextAsync` 的固定成本。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前 stream 的响应类型。</typeparam>
|
||||
/// <param name="responses">待观察的异步响应序列。</param>
|
||||
/// <param name="cancellationToken">用于向异步枚举器传播取消的令牌。</param>
|
||||
/// <returns>消费首个元素后的等待句柄。</returns>
|
||||
private static async ValueTask ConsumeFirstItemAsync<TResponse>(
|
||||
IAsyncEnumerable<TResponse> responses,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var enumerator = responses.GetAsyncEnumerator(cancellationToken);
|
||||
await using (enumerator.ConfigureAwait(false))
|
||||
{
|
||||
if (await enumerator.MoveNextAsync().ConfigureAwait(false))
|
||||
{
|
||||
_ = enumerator.Current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 完整枚举整个 stream,保留原 benchmark 的总成本观测口径。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前 stream 的响应类型。</typeparam>
|
||||
/// <param name="responses">待完整枚举的异步响应序列。</param>
|
||||
/// <returns>完整枚举结束后的等待句柄。</returns>
|
||||
private static async ValueTask DrainAsync<TResponse>(IAsyncEnumerable<TResponse> responses)
|
||||
{
|
||||
await foreach (var response in responses.ConfigureAwait(false))
|
||||
{
|
||||
_ = response;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reflection runtime stream request。
|
||||
/// </summary>
|
||||
/// <param name="Id">请求标识。</param>
|
||||
/// <param name="ItemCount">返回元素数量。</param>
|
||||
public sealed record ReflectionBenchmarkStreamRequest(Guid Id, int ItemCount) :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest<ReflectionBenchmarkResponse>;
|
||||
|
||||
/// <summary>
|
||||
/// Reflection runtime stream response。
|
||||
/// </summary>
|
||||
/// <param name="Id">响应标识。</param>
|
||||
public sealed record ReflectionBenchmarkResponse(Guid Id);
|
||||
|
||||
/// <summary>
|
||||
/// Generated runtime stream request。
|
||||
/// </summary>
|
||||
/// <param name="Id">请求标识。</param>
|
||||
/// <param name="ItemCount">返回元素数量。</param>
|
||||
public sealed record GeneratedBenchmarkStreamRequest(Guid Id, int ItemCount) :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest<GeneratedBenchmarkResponse>;
|
||||
|
||||
/// <summary>
|
||||
/// Generated runtime stream response。
|
||||
/// </summary>
|
||||
/// <param name="Id">响应标识。</param>
|
||||
public sealed record GeneratedBenchmarkResponse(Guid Id);
|
||||
|
||||
/// <summary>
|
||||
/// MediatR stream request。
|
||||
/// </summary>
|
||||
/// <param name="Id">请求标识。</param>
|
||||
/// <param name="ItemCount">返回元素数量。</param>
|
||||
public sealed record MediatRBenchmarkStreamRequest(Guid Id, int ItemCount) :
|
||||
MediatR.IStreamRequest<MediatRBenchmarkResponse>;
|
||||
|
||||
/// <summary>
|
||||
/// MediatR stream response。
|
||||
/// </summary>
|
||||
/// <param name="Id">响应标识。</param>
|
||||
public sealed record MediatRBenchmarkResponse(Guid Id);
|
||||
|
||||
/// <summary>
|
||||
/// Reflection runtime 的最小 stream request handler。
|
||||
/// </summary>
|
||||
public sealed class ReflectionBenchmarkStreamHandler :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<ReflectionBenchmarkStreamRequest, ReflectionBenchmarkResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 reflection benchmark stream request。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 reflection benchmark stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>完整枚举所需的低噪声异步响应序列。</returns>
|
||||
public IAsyncEnumerable<ReflectionBenchmarkResponse> Handle(
|
||||
ReflectionBenchmarkStreamRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return EnumerateAsync(
|
||||
request.Id,
|
||||
request.ItemCount,
|
||||
static id => new ReflectionBenchmarkResponse(id),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generated runtime 的最小 stream request handler。
|
||||
/// </summary>
|
||||
public sealed class GeneratedBenchmarkStreamHandler :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<GeneratedBenchmarkStreamRequest, GeneratedBenchmarkResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 generated benchmark stream request。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 generated benchmark stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>完整枚举所需的低噪声异步响应序列。</returns>
|
||||
public IAsyncEnumerable<GeneratedBenchmarkResponse> Handle(
|
||||
GeneratedBenchmarkStreamRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return EnumerateAsync(
|
||||
request.Id,
|
||||
request.ItemCount,
|
||||
static id => new GeneratedBenchmarkResponse(id),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MediatR 对照组的最小 stream request handler。
|
||||
/// </summary>
|
||||
public sealed class MediatRBenchmarkStreamHandler :
|
||||
MediatR.IStreamRequestHandler<MediatRBenchmarkStreamRequest, MediatRBenchmarkResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 MediatR benchmark stream request。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 MediatR benchmark stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>完整枚举所需的低噪声异步响应序列。</returns>
|
||||
public IAsyncEnumerable<MediatRBenchmarkResponse> Handle(
|
||||
MediatRBenchmarkStreamRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return EnumerateAsync(
|
||||
request.Id,
|
||||
request.ItemCount,
|
||||
static id => new MediatRBenchmarkResponse(id),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为生命周期矩阵构造相同形状的低噪声异步枚举,避免不同口径的枚举体差异干扰 dispatch 对照。
|
||||
/// </summary>
|
||||
private static async IAsyncEnumerable<TResponse> EnumerateAsync<TResponse>(
|
||||
Guid id,
|
||||
int itemCount,
|
||||
Func<Guid, TResponse> responseFactory,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
for (var index = 0; index < itemCount; index++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
yield return responseFactory(id);
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
527
GFramework.Cqrs.Benchmarks/Messaging/StreamPipelineBenchmarks.cs
Normal file
527
GFramework.Cqrs.Benchmarks/Messaging/StreamPipelineBenchmarks.cs
Normal file
@ -0,0 +1,527 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
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;
|
||||
|
||||
[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute(
|
||||
typeof(GFramework.Cqrs.Benchmarks.Messaging.StreamPipelineBenchmarks.GeneratedStreamPipelineBenchmarkRegistry))]
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 对比不同 stream pipeline 行为数量下,单个 stream request 在直接调用、GFramework.CQRS runtime 与 MediatR 之间的 steady-state dispatch 开销。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 当前矩阵同时覆盖 <c>0 / 1 / 4</c> 个 stream pipeline 行为,以及
|
||||
/// <see cref="StreamObservation.FirstItem" /> 与 <see cref="StreamObservation.DrainAll" /> 两种观测口径,
|
||||
/// 以便把建流固定成本与完整枚举成本拆开观察。
|
||||
/// </remarks>
|
||||
[Config(typeof(Config))]
|
||||
public class StreamPipelineBenchmarks
|
||||
{
|
||||
private MicrosoftDiContainer _container = null!;
|
||||
private ICqrsRuntime _runtime = null!;
|
||||
private ServiceProvider _serviceProvider = null!;
|
||||
private IMediator _mediatr = null!;
|
||||
private BenchmarkStreamHandler _baselineHandler = null!;
|
||||
private BenchmarkStreamRequest _request = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 控制当前场景注册的 stream pipeline 行为数量,保持与 request pipeline benchmark 相同的 <c>0 / 1 / 4</c> 矩阵。
|
||||
/// </summary>
|
||||
[Params(0, 1, 4)]
|
||||
public int PipelineCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 控制当前 benchmark 观察“只推进首个元素”还是“完整枚举整个 stream”。
|
||||
/// </summary>
|
||||
[Params(StreamObservation.FirstItem, StreamObservation.DrainAll)]
|
||||
public StreamObservation Observation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用于拆分 stream dispatch 固定成本与后续枚举成本的观测模式。
|
||||
/// </summary>
|
||||
public enum StreamObservation
|
||||
{
|
||||
/// <summary>
|
||||
/// 只推进到首个元素后立即释放枚举器。
|
||||
/// </summary>
|
||||
FirstItem,
|
||||
|
||||
/// <summary>
|
||||
/// 完整枚举整个 stream,保留原有 benchmark 语义。
|
||||
/// </summary>
|
||||
DrainAll
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 stream pipeline benchmark 的公共输出格式。
|
||||
/// </summary>
|
||||
private sealed class Config : ManualConfig
|
||||
{
|
||||
public Config()
|
||||
{
|
||||
AddJob(Job.Default);
|
||||
AddColumnProvider(DefaultColumnProviders.Instance);
|
||||
AddColumn(new CustomColumn("Scenario", static (_, _) => "StreamPipeline"));
|
||||
AddDiagnoser(MemoryDiagnoser.Default);
|
||||
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建 stream pipeline dispatch 所需的最小 runtime 宿主和对照对象。
|
||||
/// </summary>
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Fatal
|
||||
};
|
||||
Fixture.Setup("StreamPipeline", handlerCount: 1, pipelineCount: PipelineCount);
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
|
||||
_baselineHandler = new BenchmarkStreamHandler();
|
||||
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedStreamPipelineBenchmarkRegistry>(container);
|
||||
RegisterGFrameworkPipelineBehaviors(container, PipelineCount);
|
||||
});
|
||||
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_container,
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamPipelineBenchmarks)));
|
||||
|
||||
_serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
services =>
|
||||
{
|
||||
RegisterMediatRStreamPipelineBehaviors(services, PipelineCount);
|
||||
},
|
||||
typeof(StreamPipelineBenchmarks),
|
||||
static candidateType =>
|
||||
candidateType == typeof(BenchmarkStreamHandler) ||
|
||||
candidateType == typeof(BenchmarkStreamPipelineBehavior1) ||
|
||||
candidateType == typeof(BenchmarkStreamPipelineBehavior2) ||
|
||||
candidateType == typeof(BenchmarkStreamPipelineBehavior3) ||
|
||||
candidateType == typeof(BenchmarkStreamPipelineBehavior4),
|
||||
ServiceLifetime.Singleton);
|
||||
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
_request = new BenchmarkStreamRequest(Guid.NewGuid(), 3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放 MediatR 对照组使用的 DI 宿主。
|
||||
/// </summary>
|
||||
[GlobalCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
try
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接调用 handler,并按当前观测模式消费响应序列,作为 stream pipeline 编排之外的基线。
|
||||
/// </summary>
|
||||
/// <returns>按当前观测模式完成 stream 消费后的等待句柄。</returns>
|
||||
[Benchmark(Baseline = true)]
|
||||
public ValueTask Stream_Baseline()
|
||||
{
|
||||
return ObserveAsync(_baselineHandler.Handle(_request, CancellationToken.None), Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 GFramework.CQRS runtime 创建 stream,并按当前矩阵配置执行 stream pipeline。
|
||||
/// </summary>
|
||||
/// <returns>按当前观测模式完成 stream 消费后的等待句柄。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask Stream_GFrameworkCqrs()
|
||||
{
|
||||
return ObserveAsync(
|
||||
_runtime.CreateStream(
|
||||
BenchmarkContext.Instance,
|
||||
_request,
|
||||
CancellationToken.None),
|
||||
Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 MediatR 创建 stream,并按当前矩阵配置执行 stream pipeline,作为外部设计对照。
|
||||
/// </summary>
|
||||
/// <returns>按当前观测模式完成 stream 消费后的等待句柄。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask Stream_MediatR()
|
||||
{
|
||||
return ObserveAsync(_mediatr.CreateStream(_request, CancellationToken.None), Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按指定数量向 GFramework.CQRS 宿主注册最小 no-op stream pipeline 行为。
|
||||
/// </summary>
|
||||
/// <param name="container">当前 benchmark 使用的容器。</param>
|
||||
/// <param name="pipelineCount">要注册的行为数量。</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">行为数量不在支持的矩阵内时抛出。</exception>
|
||||
private static void RegisterGFrameworkPipelineBehaviors(MicrosoftDiContainer container, int pipelineCount)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
switch (pipelineCount)
|
||||
{
|
||||
case 0:
|
||||
return;
|
||||
case 1:
|
||||
container.RegisterCqrsStreamPipelineBehavior<BenchmarkStreamPipelineBehavior1>();
|
||||
return;
|
||||
case 4:
|
||||
container.RegisterCqrsStreamPipelineBehavior<BenchmarkStreamPipelineBehavior1>();
|
||||
container.RegisterCqrsStreamPipelineBehavior<BenchmarkStreamPipelineBehavior2>();
|
||||
container.RegisterCqrsStreamPipelineBehavior<BenchmarkStreamPipelineBehavior3>();
|
||||
container.RegisterCqrsStreamPipelineBehavior<BenchmarkStreamPipelineBehavior4>();
|
||||
return;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(pipelineCount), pipelineCount,
|
||||
"Only the 0/1/4 pipeline matrix is supported.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按指定数量向 MediatR 宿主注册最小 no-op stream pipeline 行为。
|
||||
/// </summary>
|
||||
/// <param name="services">当前 benchmark 使用的服务集合。</param>
|
||||
/// <param name="pipelineCount">要注册的行为数量。</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">行为数量不在支持的矩阵内时抛出。</exception>
|
||||
private static void RegisterMediatRStreamPipelineBehaviors(IServiceCollection services, int pipelineCount)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
switch (pipelineCount)
|
||||
{
|
||||
case 0:
|
||||
return;
|
||||
case 1:
|
||||
services.AddSingleton<MediatR.IStreamPipelineBehavior<BenchmarkStreamRequest, BenchmarkResponse>, BenchmarkStreamPipelineBehavior1>();
|
||||
return;
|
||||
case 4:
|
||||
services.AddSingleton<MediatR.IStreamPipelineBehavior<BenchmarkStreamRequest, BenchmarkResponse>, BenchmarkStreamPipelineBehavior1>();
|
||||
services.AddSingleton<MediatR.IStreamPipelineBehavior<BenchmarkStreamRequest, BenchmarkResponse>, BenchmarkStreamPipelineBehavior2>();
|
||||
services.AddSingleton<MediatR.IStreamPipelineBehavior<BenchmarkStreamRequest, BenchmarkResponse>, BenchmarkStreamPipelineBehavior3>();
|
||||
services.AddSingleton<MediatR.IStreamPipelineBehavior<BenchmarkStreamRequest, BenchmarkResponse>, BenchmarkStreamPipelineBehavior4>();
|
||||
return;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(pipelineCount), pipelineCount,
|
||||
"Only the 0/1/4 pipeline matrix is supported.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按观测模式消费 stream,便于把“建流/首个元素”和“完整枚举”分开观察。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前 stream 的响应类型。</typeparam>
|
||||
/// <param name="responses">待观察的异步响应序列。</param>
|
||||
/// <param name="observation">当前 benchmark 选定的观测模式。</param>
|
||||
/// <returns>异步消费完成后的等待句柄。</returns>
|
||||
private static ValueTask ObserveAsync<TResponse>(
|
||||
IAsyncEnumerable<TResponse> responses,
|
||||
StreamObservation observation)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(responses);
|
||||
|
||||
return observation switch
|
||||
{
|
||||
StreamObservation.FirstItem => ConsumeFirstItemAsync(responses, CancellationToken.None),
|
||||
StreamObservation.DrainAll => DrainAsync(responses),
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(observation),
|
||||
observation,
|
||||
"Unsupported stream observation mode.")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 只推进到首个元素后立即释放枚举器,用来近似隔离建流与首个 <c>MoveNextAsync</c> 的固定成本。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前 stream 的响应类型。</typeparam>
|
||||
/// <param name="responses">待观察的异步响应序列。</param>
|
||||
/// <param name="cancellationToken">用于向异步枚举器传播取消的令牌。</param>
|
||||
/// <returns>消费首个元素后的等待句柄。</returns>
|
||||
private static async ValueTask ConsumeFirstItemAsync<TResponse>(
|
||||
IAsyncEnumerable<TResponse> responses,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var enumerator = responses.GetAsyncEnumerator(cancellationToken);
|
||||
await using (enumerator.ConfigureAwait(false))
|
||||
{
|
||||
if (await enumerator.MoveNextAsync().ConfigureAwait(false))
|
||||
{
|
||||
_ = enumerator.Current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 完整枚举整个 stream,保留原 benchmark 的总成本观测口径。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前 stream 的响应类型。</typeparam>
|
||||
/// <param name="responses">待完整枚举的异步响应序列。</param>
|
||||
/// <returns>完整枚举结束后的等待句柄。</returns>
|
||||
private static async ValueTask DrainAsync<TResponse>(IAsyncEnumerable<TResponse> responses)
|
||||
{
|
||||
await foreach (var response in responses.ConfigureAwait(false))
|
||||
{
|
||||
_ = response;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark stream request。
|
||||
/// </summary>
|
||||
/// <param name="Id">请求标识。</param>
|
||||
/// <param name="ItemCount">返回元素数量。</param>
|
||||
public sealed record BenchmarkStreamRequest(Guid Id, int ItemCount) :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest<BenchmarkResponse>,
|
||||
MediatR.IStreamRequest<BenchmarkResponse>;
|
||||
|
||||
/// <summary>
|
||||
/// 复用 stream benchmark 的响应结构,保持跨场景可比性。
|
||||
/// </summary>
|
||||
/// <param name="Id">响应标识。</param>
|
||||
public sealed record BenchmarkResponse(Guid Id);
|
||||
|
||||
/// <summary>
|
||||
/// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 stream handler。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkStreamHandler :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>,
|
||||
MediatR.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 GFramework.CQRS stream request。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 benchmark stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>低噪声、可重复的异步响应序列。</returns>
|
||||
public IAsyncEnumerable<BenchmarkResponse> Handle(
|
||||
BenchmarkStreamRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return EnumerateAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 MediatR stream request。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 benchmark stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>低噪声、可重复的异步响应序列。</returns>
|
||||
IAsyncEnumerable<BenchmarkResponse> MediatR.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>.Handle(
|
||||
BenchmarkStreamRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return EnumerateAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 benchmark 构造稳定、低噪声的异步响应序列。
|
||||
/// </summary>
|
||||
/// <param name="request">决定元素数量和标识的 benchmark 请求。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>按请求数量生成的响应序列。</returns>
|
||||
private static async IAsyncEnumerable<BenchmarkResponse> EnumerateAsync(
|
||||
BenchmarkStreamRequest request,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
for (int index = 0; index < request.ItemCount; index++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
yield return new BenchmarkResponse(request.Id);
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 benchmark 提供统一的 no-op stream pipeline 行为实现,尽量把测量焦点保持在调度器与行为编排本身。
|
||||
/// </summary>
|
||||
public abstract class BenchmarkStreamPipelineBehaviorBase :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamPipelineBehavior<BenchmarkStreamRequest, BenchmarkResponse>,
|
||||
MediatR.IStreamPipelineBehavior<BenchmarkStreamRequest, BenchmarkResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// 透传 GFramework.CQRS stream pipeline,避免引入额外业务逻辑噪音。
|
||||
/// </summary>
|
||||
/// <param name="message">当前 benchmark stream 请求。</param>
|
||||
/// <param name="next">继续向下执行的 stream pipeline 委托。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>下游 handler 产出的异步响应序列。</returns>
|
||||
public IAsyncEnumerable<BenchmarkResponse> Handle(
|
||||
BenchmarkStreamRequest message,
|
||||
GFramework.Cqrs.Abstractions.Cqrs.StreamMessageHandlerDelegate<BenchmarkStreamRequest, BenchmarkResponse> next,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return next(message, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 透传 MediatR stream pipeline,保持与 GFramework.CQRS 相同的 no-op 语义。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 benchmark stream 请求。</param>
|
||||
/// <param name="next">继续向下执行的 MediatR stream pipeline 委托。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>下游 handler 产出的异步响应序列。</returns>
|
||||
IAsyncEnumerable<BenchmarkResponse> MediatR.IStreamPipelineBehavior<BenchmarkStreamRequest, BenchmarkResponse>.Handle(
|
||||
BenchmarkStreamRequest request,
|
||||
MediatR.StreamHandlerDelegate<BenchmarkResponse> next,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_ = request;
|
||||
_ = cancellationToken;
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// pipeline 矩阵中的第一个 no-op stream 行为。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkStreamPipelineBehavior1 : BenchmarkStreamPipelineBehaviorBase
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// pipeline 矩阵中的第二个 no-op stream 行为。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkStreamPipelineBehavior2 : BenchmarkStreamPipelineBehaviorBase
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// pipeline 矩阵中的第三个 no-op stream 行为。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkStreamPipelineBehavior3 : BenchmarkStreamPipelineBehaviorBase
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// pipeline 矩阵中的第四个 no-op stream 行为。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkStreamPipelineBehavior4 : BenchmarkStreamPipelineBehaviorBase
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 stream pipeline benchmark 提供 handwritten generated registry,
|
||||
/// 让默认 pipeline 宿主也能走真实的 generated stream invoker provider 接线路径。
|
||||
/// </summary>
|
||||
public sealed class GeneratedStreamPipelineBenchmarkRegistry :
|
||||
GFramework.Cqrs.ICqrsHandlerRegistry,
|
||||
GFramework.Cqrs.ICqrsStreamInvokerProvider,
|
||||
GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors
|
||||
{
|
||||
private static readonly GFramework.Cqrs.CqrsStreamInvokerDescriptor Descriptor =
|
||||
new(
|
||||
typeof(GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<
|
||||
BenchmarkStreamRequest,
|
||||
BenchmarkResponse>),
|
||||
typeof(GeneratedStreamPipelineBenchmarkRegistry).GetMethod(
|
||||
nameof(InvokeBenchmarkStreamHandler),
|
||||
BindingFlags.Public | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("Missing generated stream pipeline benchmark method."));
|
||||
|
||||
private static readonly IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> Descriptors =
|
||||
[
|
||||
new GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(
|
||||
typeof(BenchmarkStreamRequest),
|
||||
typeof(BenchmarkResponse),
|
||||
Descriptor)
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 把 stream pipeline benchmark handler 注册为单例,保持与当前矩阵宿主一致的生命周期语义。
|
||||
/// </summary>
|
||||
/// <param name="services">用于承载 generated handler 注册的服务集合。</param>
|
||||
/// <param name="logger">记录 generated registry 接线结果的日志器。</param>
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddSingleton(
|
||||
typeof(GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>),
|
||||
typeof(BenchmarkStreamHandler));
|
||||
logger.Debug("Registered generated stream pipeline benchmark handler.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前 provider 暴露的全部 generated stream invoker 描述符。
|
||||
/// </summary>
|
||||
/// <returns>当前 benchmark 的 generated stream invoker 描述符集合。</returns>
|
||||
public IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return Descriptors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为目标流式请求/响应类型对返回 generated stream invoker 描述符。
|
||||
/// </summary>
|
||||
/// <param name="requestType">要匹配的 stream 请求类型。</param>
|
||||
/// <param name="responseType">要匹配的 stream 响应类型。</param>
|
||||
/// <param name="descriptor">命中时返回的 generated stream invoker 描述符。</param>
|
||||
/// <returns>是否命中了当前 benchmark 的 stream 请求/响应类型对。</returns>
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out GFramework.Cqrs.CqrsStreamInvokerDescriptor? descriptor)
|
||||
{
|
||||
if (requestType == typeof(BenchmarkStreamRequest) &&
|
||||
responseType == typeof(BenchmarkResponse))
|
||||
{
|
||||
descriptor = Descriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 generated stream invoker provider 为 stream pipeline benchmark 产出的开放静态调用入口。
|
||||
/// </summary>
|
||||
/// <param name="handler">当前要调用的 stream handler 实例。</param>
|
||||
/// <param name="request">当前要分发的 stream 请求实例。</param>
|
||||
/// <param name="cancellationToken">用于向 handler 传播的取消令牌。</param>
|
||||
/// <returns>handler 产出的异步响应序列。</returns>
|
||||
public static object InvokeBenchmarkStreamHandler(
|
||||
object handler,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var typedHandler = (GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<
|
||||
BenchmarkStreamRequest,
|
||||
BenchmarkResponse>)handler;
|
||||
var typedRequest = (BenchmarkStreamRequest)request;
|
||||
return typedHandler.Handle(typedRequest, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
482
GFramework.Cqrs.Benchmarks/Messaging/StreamStartupBenchmarks.cs
Normal file
482
GFramework.Cqrs.Benchmarks/Messaging/StreamStartupBenchmarks.cs
Normal file
@ -0,0 +1,482 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
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;
|
||||
|
||||
[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute(
|
||||
typeof(GFramework.Cqrs.Benchmarks.Messaging.StreamStartupBenchmarks.GeneratedRegistry))]
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 对比 stream 宿主在 GFramework.CQRS reflection / generated、NuGet `Mediator` 与 MediatR 之间的初始化与首次建流命中成本。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该场景与 <see cref="RequestStartupBenchmarks" /> 保持相同的 `Initialization + ColdStart` 结构,
|
||||
/// 但 cold-start 边界改为“新宿主 + 首个元素命中”,因为 stream 的首个 <c>MoveNextAsync</c>
|
||||
/// 才会真正覆盖建流后的首次处理链路。
|
||||
/// </remarks>
|
||||
[Config(typeof(Config))]
|
||||
public class StreamStartupBenchmarks
|
||||
{
|
||||
private static readonly ILogger ReflectionRuntimeLogger = CreateLogger(nameof(StreamStartupBenchmarks) + ".Reflection");
|
||||
private static readonly ILogger GeneratedRuntimeLogger = CreateLogger(nameof(StreamStartupBenchmarks) + ".Generated");
|
||||
private static readonly BenchmarkStreamRequest Request = new(Guid.NewGuid(), 3);
|
||||
|
||||
private MicrosoftDiContainer _reflectionContainer = null!;
|
||||
private ICqrsRuntime _reflectionRuntime = null!;
|
||||
private MicrosoftDiContainer _generatedContainer = null!;
|
||||
private ICqrsRuntime _generatedRuntime = null!;
|
||||
private ServiceProvider _serviceProvider = null!;
|
||||
private ServiceProvider _mediatorServiceProvider = null!;
|
||||
private IMediator _mediatr = null!;
|
||||
private GeneratedMediator _mediator = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 配置 stream startup benchmark 的公共输出格式。
|
||||
/// </summary>
|
||||
private sealed class Config : ManualConfig
|
||||
{
|
||||
public Config()
|
||||
{
|
||||
AddJob(Job.Default
|
||||
.WithId("ColdStart")
|
||||
.WithInvocationCount(1)
|
||||
.WithUnrollFactor(1));
|
||||
AddColumnProvider(DefaultColumnProviders.Instance);
|
||||
AddColumn(new CustomColumn("Scenario", static (_, _) => "StreamStartup"), TargetMethodColumn.Method, CategoriesColumn.Default);
|
||||
AddDiagnoser(MemoryDiagnoser.Default);
|
||||
AddLogicalGroupRules(BenchmarkLogicalGroupRule.ByCategory);
|
||||
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建 startup benchmark 复用的 reflection / generated / `Mediator` / MediatR 宿主对象。
|
||||
/// </summary>
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
Fixture.Setup("StreamStartup", handlerCount: 1, pipelineCount: 0);
|
||||
|
||||
_reflectionContainer = CreateReflectionContainer();
|
||||
_reflectionRuntime = CreateRuntime(_reflectionContainer, ReflectionRuntimeLogger);
|
||||
|
||||
_generatedContainer = CreateGeneratedContainer();
|
||||
_generatedRuntime = CreateRuntime(_generatedContainer, GeneratedRuntimeLogger);
|
||||
|
||||
_serviceProvider = CreateMediatRServiceProvider();
|
||||
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
|
||||
_mediatorServiceProvider = CreateMediatorServiceProvider();
|
||||
_mediator = _mediatorServiceProvider.GetRequiredService<GeneratedMediator>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在每次 cold-start 迭代前清空 dispatcher 静态缓存,确保首次绑定路径可重复观察。
|
||||
/// </summary>
|
||||
[IterationSetup]
|
||||
public void ResetColdStartCaches()
|
||||
{
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放 startup benchmark 复用的宿主对象。
|
||||
/// </summary>
|
||||
[GlobalCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_reflectionContainer, _generatedContainer, _serviceProvider, _mediatorServiceProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回已构建宿主中的 MediatR mediator,作为 initialization 组的句柄解析 baseline。
|
||||
/// </summary>
|
||||
/// <returns>当前 benchmark 复用的 MediatR mediator。</returns>
|
||||
[Benchmark(Baseline = true)]
|
||||
[BenchmarkCategory("Initialization")]
|
||||
public IMediator Initialization_MediatR()
|
||||
{
|
||||
return _mediatr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回已构建宿主中的 GFramework.CQRS reflection runtime,观察默认 stream binding 宿主句柄解析成本。
|
||||
/// </summary>
|
||||
/// <returns>当前 benchmark 复用的 reflection CQRS runtime。</returns>
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("Initialization")]
|
||||
public ICqrsRuntime Initialization_GFrameworkReflection()
|
||||
{
|
||||
return _reflectionRuntime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回已构建宿主中的 GFramework.CQRS generated runtime,观察 generated stream invoker 宿主句柄解析成本。
|
||||
/// </summary>
|
||||
/// <returns>当前 benchmark 复用的 generated CQRS runtime。</returns>
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("Initialization")]
|
||||
public ICqrsRuntime Initialization_GFrameworkGenerated()
|
||||
{
|
||||
return _generatedRuntime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回已构建宿主中的 `Mediator` concrete mediator,作为 source-generated concrete path 的初始化句柄。
|
||||
/// </summary>
|
||||
/// <returns>当前 benchmark 复用的 `Mediator` concrete mediator。</returns>
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("Initialization")]
|
||||
public GeneratedMediator Initialization_Mediator()
|
||||
{
|
||||
return _mediator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在新宿主上首次创建并推进 stream,作为 MediatR 的 cold-start baseline。
|
||||
/// </summary>
|
||||
/// <returns>首个 stream 响应元素。</returns>
|
||||
[Benchmark(Baseline = true)]
|
||||
[BenchmarkCategory("ColdStart")]
|
||||
public async Task<BenchmarkResponse> ColdStart_MediatR()
|
||||
{
|
||||
using var serviceProvider = CreateMediatRServiceProvider();
|
||||
var mediator = serviceProvider.GetRequiredService<IMediator>();
|
||||
return await ConsumeFirstItemAsync(mediator.CreateStream(Request, CancellationToken.None), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在新的 reflection runtime 上首次创建并推进 stream,量化默认 stream binding 的 first-hit 成本。
|
||||
/// </summary>
|
||||
/// <returns>首个 stream 响应元素。</returns>
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("ColdStart")]
|
||||
public async ValueTask<BenchmarkResponse> ColdStart_GFrameworkReflection()
|
||||
{
|
||||
using var container = CreateReflectionContainer();
|
||||
var runtime = CreateRuntime(container, ReflectionRuntimeLogger);
|
||||
return await ConsumeFirstItemAsync(
|
||||
runtime.CreateStream(BenchmarkContext.Instance, Request, CancellationToken.None),
|
||||
CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在新的 generated runtime 上首次创建并推进 stream,量化 generated stream invoker 路径的 first-hit 成本。
|
||||
/// </summary>
|
||||
/// <returns>首个 stream 响应元素。</returns>
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("ColdStart")]
|
||||
public async ValueTask<BenchmarkResponse> ColdStart_GFrameworkGenerated()
|
||||
{
|
||||
using var container = CreateGeneratedContainer();
|
||||
var runtime = CreateRuntime(container, GeneratedRuntimeLogger);
|
||||
return await ConsumeFirstItemAsync(
|
||||
runtime.CreateStream(BenchmarkContext.Instance, Request, CancellationToken.None),
|
||||
CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在新的 `Mediator` 宿主上首次创建并推进 stream,量化 source-generated concrete path 的 first-hit 成本。
|
||||
/// </summary>
|
||||
/// <returns>首个 stream 响应元素。</returns>
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("ColdStart")]
|
||||
public async ValueTask<BenchmarkResponse> ColdStart_Mediator()
|
||||
{
|
||||
using var serviceProvider = CreateMediatorServiceProvider();
|
||||
var mediator = serviceProvider.GetRequiredService<GeneratedMediator>();
|
||||
return await ConsumeFirstItemAsync(mediator.CreateStream(Request, CancellationToken.None), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建只承载当前 benchmark handler 的最小 reflection GFramework.CQRS 容器。
|
||||
/// </summary>
|
||||
private static MicrosoftDiContainer CreateReflectionContainer()
|
||||
{
|
||||
return BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static container =>
|
||||
{
|
||||
container.RegisterTransient<
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>,
|
||||
BenchmarkStreamHandler>();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建只承载当前 benchmark generated registry 的最小 generated GFramework.CQRS 容器。
|
||||
/// </summary>
|
||||
private static MicrosoftDiContainer CreateGeneratedContainer()
|
||||
{
|
||||
return BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static container =>
|
||||
{
|
||||
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedRegistry>(container);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于已冻结的 benchmark 容器构建最小 GFramework.CQRS runtime。
|
||||
/// </summary>
|
||||
/// <param name="container">当前 benchmark 拥有并负责释放的容器。</param>
|
||||
/// <param name="logger">当前 runtime 使用的 benchmark logger。</param>
|
||||
private static ICqrsRuntime CreateRuntime(MicrosoftDiContainer container, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
return GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建只承载当前 benchmark handler 的最小 MediatR 对照宿主。
|
||||
/// </summary>
|
||||
private static ServiceProvider CreateMediatRServiceProvider()
|
||||
{
|
||||
return BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
configure: null,
|
||||
typeof(StreamStartupBenchmarks),
|
||||
static candidateType => candidateType == typeof(BenchmarkStreamHandler),
|
||||
ServiceLifetime.Transient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建只承载当前 benchmark handler 的最小 `Mediator` 对照宿主。
|
||||
/// </summary>
|
||||
private static ServiceProvider CreateMediatorServiceProvider()
|
||||
{
|
||||
return BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 推进 stream 到首个元素,并返回该元素作为 cold-start 结果。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前 stream 的响应类型。</typeparam>
|
||||
/// <param name="responses">待推进的异步响应序列。</param>
|
||||
/// <param name="cancellationToken">用于向异步枚举器传播取消的令牌。</param>
|
||||
/// <returns>首个元素。</returns>
|
||||
/// <exception cref="InvalidOperationException">stream 未产生任何元素。</exception>
|
||||
private static async ValueTask<TResponse> ConsumeFirstItemAsync<TResponse>(
|
||||
IAsyncEnumerable<TResponse> responses,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(responses);
|
||||
|
||||
var enumerator = responses.GetAsyncEnumerator(cancellationToken);
|
||||
await using (enumerator.ConfigureAwait(false))
|
||||
{
|
||||
if (await enumerator.MoveNextAsync().ConfigureAwait(false))
|
||||
{
|
||||
return enumerator.Current;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("The benchmark stream must yield at least one response.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 benchmark 创建稳定的 fatal 级 logger,避免把日志成本混入 startup 测量。
|
||||
/// </summary>
|
||||
private static ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Fatal
|
||||
};
|
||||
return LoggerFactoryResolver.Provider.CreateLogger(categoryName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark stream request。
|
||||
/// </summary>
|
||||
/// <param name="Id">请求标识。</param>
|
||||
/// <param name="ItemCount">返回元素数量。</param>
|
||||
public sealed record BenchmarkStreamRequest(Guid Id, int ItemCount) :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest<BenchmarkResponse>,
|
||||
Mediator.IStreamRequest<BenchmarkResponse>,
|
||||
MediatR.IStreamRequest<BenchmarkResponse>;
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark stream response。
|
||||
/// </summary>
|
||||
/// <param name="Id">响应标识。</param>
|
||||
public sealed record BenchmarkResponse(Guid Id);
|
||||
|
||||
/// <summary>
|
||||
/// 同时实现 GFramework.CQRS、NuGet `Mediator` 与 MediatR 契约的最小 stream handler。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkStreamHandler :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>,
|
||||
Mediator.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>,
|
||||
MediatR.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 GFramework.CQRS stream request。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>按请求元素数量延迟生成的异步响应序列。</returns>
|
||||
public IAsyncEnumerable<BenchmarkResponse> Handle(
|
||||
BenchmarkStreamRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return EnumerateAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 NuGet `Mediator` stream request。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>按请求元素数量延迟生成的异步响应序列。</returns>
|
||||
IAsyncEnumerable<BenchmarkResponse> Mediator.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>.Handle(
|
||||
BenchmarkStreamRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Handle(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 MediatR stream request。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>按请求元素数量延迟生成的异步响应序列。</returns>
|
||||
IAsyncEnumerable<BenchmarkResponse> MediatR.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>.Handle(
|
||||
BenchmarkStreamRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return EnumerateAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成固定长度的 benchmark stream,确保 cold-start 与 steady-state 维度共用同一份响应形状。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于向异步枚举器传播取消的令牌。</param>
|
||||
/// <returns>按请求数量生成的异步响应序列。</returns>
|
||||
private static async IAsyncEnumerable<BenchmarkResponse> EnumerateAsync(
|
||||
BenchmarkStreamRequest request,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
for (var index = 0; index < request.ItemCount; index++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
yield return new BenchmarkResponse(request.Id);
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 stream startup benchmark 提供 hand-written generated registry,
|
||||
/// 以便独立比较 generated stream invoker 的初始化与首次命中成本。
|
||||
/// </summary>
|
||||
public sealed class GeneratedRegistry :
|
||||
GFramework.Cqrs.ICqrsHandlerRegistry,
|
||||
GFramework.Cqrs.ICqrsStreamInvokerProvider,
|
||||
GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors
|
||||
{
|
||||
private static readonly GFramework.Cqrs.CqrsStreamInvokerDescriptor Descriptor =
|
||||
new(
|
||||
typeof(GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<
|
||||
BenchmarkStreamRequest,
|
||||
BenchmarkResponse>),
|
||||
typeof(GeneratedRegistry).GetMethod(
|
||||
nameof(InvokeBenchmarkStreamHandler),
|
||||
BindingFlags.Public | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("Missing generated stream startup benchmark method."));
|
||||
|
||||
private static readonly IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> Descriptors =
|
||||
[
|
||||
new GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(
|
||||
typeof(BenchmarkStreamRequest),
|
||||
typeof(BenchmarkResponse),
|
||||
Descriptor)
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 把 startup benchmark handler 注册为 transient,保持与 cold-start 对照宿主一致的 handler 生命周期。
|
||||
/// </summary>
|
||||
/// <param name="services">承载 generated handler 注册结果的目标服务集合。</param>
|
||||
/// <param name="logger">记录 generated registry 注册过程的日志器。</param>
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddTransient(
|
||||
typeof(GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<
|
||||
BenchmarkStreamRequest,
|
||||
BenchmarkResponse>),
|
||||
typeof(BenchmarkStreamHandler));
|
||||
logger.Debug("Registered generated stream startup benchmark handler.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前 provider 暴露的全部 generated stream invoker 描述符。
|
||||
/// </summary>
|
||||
/// <returns>当前 startup benchmark 的 generated stream invoker 描述符集合。</returns>
|
||||
public IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return Descriptors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为目标流式请求/响应类型对返回 generated stream invoker 描述符。
|
||||
/// </summary>
|
||||
/// <param name="requestType">待匹配的 stream 请求类型。</param>
|
||||
/// <param name="responseType">待匹配的 stream 响应类型。</param>
|
||||
/// <param name="descriptor">匹配成功时返回的 generated stream invoker 描述符。</param>
|
||||
/// <returns>命中当前 benchmark 请求/响应类型对时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out GFramework.Cqrs.CqrsStreamInvokerDescriptor? descriptor)
|
||||
{
|
||||
if (requestType == typeof(BenchmarkStreamRequest) &&
|
||||
responseType == typeof(BenchmarkResponse))
|
||||
{
|
||||
descriptor = Descriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 generated stream invoker provider 为 startup benchmark 产出的开放静态调用入口。
|
||||
/// </summary>
|
||||
/// <param name="handler">当前 benchmark 注册的 stream handler 实例。</param>
|
||||
/// <param name="request">当前 benchmark 的 stream 请求对象。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>由 handler 产生的异步响应序列。</returns>
|
||||
public static object InvokeBenchmarkStreamHandler(object handler, object request, CancellationToken cancellationToken)
|
||||
{
|
||||
var typedHandler = (GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<
|
||||
BenchmarkStreamRequest,
|
||||
BenchmarkResponse>)handler;
|
||||
var typedRequest = (BenchmarkStreamRequest)request;
|
||||
return typedHandler.Handle(typedRequest, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,22 +17,55 @@ using GFramework.Core.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using GeneratedMediator = Mediator.Mediator;
|
||||
|
||||
[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute(
|
||||
typeof(GFramework.Cqrs.Benchmarks.Messaging.GeneratedDefaultStreamingBenchmarkRegistry))]
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 对比单个 stream request 在直接调用、GFramework.CQRS runtime 与 MediatR 之间的完整枚举开销。
|
||||
/// 对比单个 stream request 在直接调用、GFramework.CQRS runtime、NuGet `Mediator` 与 MediatR 之间的 steady-state stream 开销。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 默认 generated-provider stream 宿主同时暴露 <see cref="StreamObservation.FirstItem" /> 与
|
||||
/// <see cref="StreamObservation.DrainAll" /> 两种观测口径,
|
||||
/// 以便把“建流到首个元素”的固定成本与“完整枚举整个 stream”的总成本拆开观察。
|
||||
/// </remarks>
|
||||
[Config(typeof(Config))]
|
||||
public class StreamingBenchmarks
|
||||
{
|
||||
private MicrosoftDiContainer _container = null!;
|
||||
private ICqrsRuntime _runtime = null!;
|
||||
private ServiceProvider _serviceProvider = null!;
|
||||
private ServiceProvider _mediatrServiceProvider = null!;
|
||||
private ServiceProvider _mediatorServiceProvider = null!;
|
||||
private IMediator _mediatr = null!;
|
||||
private GeneratedMediator _mediator = null!;
|
||||
private BenchmarkStreamHandler _baselineHandler = null!;
|
||||
private BenchmarkStreamRequest _request = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 控制当前 benchmark 观察“只推进首个元素”还是“完整枚举整个 stream”。
|
||||
/// </summary>
|
||||
[Params(StreamObservation.FirstItem, StreamObservation.DrainAll)]
|
||||
public StreamObservation Observation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用于拆分 stream dispatch 与后续枚举成本的观测模式。
|
||||
/// </summary>
|
||||
public enum StreamObservation
|
||||
{
|
||||
/// <summary>
|
||||
/// 只推进到首个元素后立即释放枚举器。
|
||||
/// </summary>
|
||||
FirstItem,
|
||||
|
||||
/// <summary>
|
||||
/// 完整枚举整个 stream,保留原有 benchmark 语义。
|
||||
/// </summary>
|
||||
DrainAll
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 stream benchmark 的公共输出格式。
|
||||
/// </summary>
|
||||
@ -59,68 +92,145 @@ public class StreamingBenchmarks
|
||||
MinLevel = LogLevel.Fatal
|
||||
};
|
||||
Fixture.Setup("StreamRequest", handlerCount: 1, pipelineCount: 0);
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
|
||||
_baselineHandler = new BenchmarkStreamHandler();
|
||||
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>>(
|
||||
_baselineHandler);
|
||||
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedDefaultStreamingBenchmarkRegistry>(container);
|
||||
});
|
||||
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_container,
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamingBenchmarks)));
|
||||
|
||||
_serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
_mediatrServiceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
configure: null,
|
||||
typeof(StreamingBenchmarks),
|
||||
static candidateType => candidateType == typeof(BenchmarkStreamHandler),
|
||||
ServiceLifetime.Singleton);
|
||||
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
|
||||
_mediatr = _mediatrServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
_mediatorServiceProvider = BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null);
|
||||
_mediator = _mediatorServiceProvider.GetRequiredService<GeneratedMediator>();
|
||||
|
||||
_request = new BenchmarkStreamRequest(Guid.NewGuid(), 3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放 MediatR 对照组使用的 DI 宿主。
|
||||
/// 释放 MediatR 与 `Mediator` 对照组使用的 DI 宿主。
|
||||
/// </summary>
|
||||
[GlobalCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
|
||||
try
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _mediatrServiceProvider, _mediatorServiceProvider);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接调用 handler 并完整枚举响应序列,作为 stream dispatch 额外开销的 baseline。
|
||||
/// 直接调用 handler,并按当前观测模式消费响应序列,作为 stream dispatch 额外开销的 baseline。
|
||||
/// </summary>
|
||||
/// <returns>按当前观测模式完成 stream 消费后的等待句柄。</returns>
|
||||
[Benchmark(Baseline = true)]
|
||||
public async ValueTask Stream_Baseline()
|
||||
public ValueTask Stream_Baseline()
|
||||
{
|
||||
await foreach (var response in _baselineHandler.Handle(_request, CancellationToken.None).ConfigureAwait(false))
|
||||
return ObserveAsync(_baselineHandler.Handle(_request, CancellationToken.None), Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 GFramework.CQRS runtime 创建 stream,并按当前观测模式消费。
|
||||
/// </summary>
|
||||
/// <returns>按当前观测模式完成 stream 消费后的等待句柄。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask Stream_GFrameworkCqrs()
|
||||
{
|
||||
return ObserveAsync(
|
||||
_runtime.CreateStream(
|
||||
BenchmarkContext.Instance,
|
||||
_request,
|
||||
CancellationToken.None),
|
||||
Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 MediatR 创建 stream,并按当前观测模式消费,作为外部设计对照。
|
||||
/// </summary>
|
||||
/// <returns>按当前观测模式完成 stream 消费后的等待句柄。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask Stream_MediatR()
|
||||
{
|
||||
return ObserveAsync(_mediatr.CreateStream(_request, CancellationToken.None), Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 `ai-libs/Mediator` 的 source-generated concrete mediator 创建 stream,并按当前观测模式消费。
|
||||
/// </summary>
|
||||
/// <returns>按当前观测模式完成 stream 消费后的等待句柄。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask Stream_Mediator()
|
||||
{
|
||||
return ObserveAsync(_mediator.CreateStream(_request, CancellationToken.None), Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按观测模式消费 stream,便于把“建流/首个元素”和“完整枚举”分开观察。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前 stream 的响应类型。</typeparam>
|
||||
/// <param name="responses">待观察的异步响应序列。</param>
|
||||
/// <param name="observation">当前 benchmark 选定的观测模式。</param>
|
||||
/// <returns>异步消费完成后的等待句柄。</returns>
|
||||
private static ValueTask ObserveAsync<TResponse>(
|
||||
IAsyncEnumerable<TResponse> responses,
|
||||
StreamObservation observation)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(responses);
|
||||
|
||||
return observation switch
|
||||
{
|
||||
_ = response;
|
||||
StreamObservation.FirstItem => ConsumeFirstItemAsync(responses, CancellationToken.None),
|
||||
StreamObservation.DrainAll => DrainAsync(responses),
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(observation),
|
||||
observation,
|
||||
"Unsupported stream observation mode.")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 只推进到首个元素后立即释放枚举器,用来近似隔离建流与首个 `MoveNextAsync` 的固定成本。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前 stream 的响应类型。</typeparam>
|
||||
/// <param name="responses">待观察的异步响应序列。</param>
|
||||
/// <param name="cancellationToken">用于向异步枚举器传播取消的令牌。</param>
|
||||
/// <returns>消费首个元素后的等待句柄。</returns>
|
||||
private static async ValueTask ConsumeFirstItemAsync<TResponse>(
|
||||
IAsyncEnumerable<TResponse> responses,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var enumerator = responses.GetAsyncEnumerator(cancellationToken);
|
||||
await using (enumerator.ConfigureAwait(false))
|
||||
{
|
||||
if (await enumerator.MoveNextAsync().ConfigureAwait(false))
|
||||
{
|
||||
_ = enumerator.Current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 GFramework.CQRS runtime 创建并完整枚举 stream。
|
||||
/// 完整枚举整个 stream,保留原 benchmark 的总成本观测口径。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public async ValueTask Stream_GFrameworkCqrs()
|
||||
/// <typeparam name="TResponse">当前 stream 的响应类型。</typeparam>
|
||||
/// <param name="responses">待完整枚举的异步响应序列。</param>
|
||||
/// <returns>完整枚举结束后的等待句柄。</returns>
|
||||
private static async ValueTask DrainAsync<TResponse>(IAsyncEnumerable<TResponse> responses)
|
||||
{
|
||||
await foreach (var response in _runtime.CreateStream(BenchmarkContext.Instance, _request, CancellationToken.None)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
_ = response;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 MediatR 创建并完整枚举 stream,作为外部设计对照。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public async ValueTask Stream_MediatR()
|
||||
{
|
||||
await foreach (var response in _mediatr.CreateStream(_request, CancellationToken.None).ConfigureAwait(false))
|
||||
await foreach (var response in responses.ConfigureAwait(false))
|
||||
{
|
||||
_ = response;
|
||||
}
|
||||
@ -133,6 +243,7 @@ public class StreamingBenchmarks
|
||||
/// <param name="ItemCount">返回元素数量。</param>
|
||||
public sealed record BenchmarkStreamRequest(Guid Id, int ItemCount) :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest<BenchmarkResponse>,
|
||||
Mediator.IStreamRequest<BenchmarkResponse>,
|
||||
MediatR.IStreamRequest<BenchmarkResponse>;
|
||||
|
||||
/// <summary>
|
||||
@ -142,10 +253,11 @@ public class StreamingBenchmarks
|
||||
public sealed record BenchmarkResponse(Guid Id);
|
||||
|
||||
/// <summary>
|
||||
/// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 stream handler。
|
||||
/// 同时实现 GFramework.CQRS、NuGet `Mediator` 与 MediatR 契约的最小 stream handler。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkStreamHandler :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>,
|
||||
Mediator.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>,
|
||||
MediatR.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>
|
||||
{
|
||||
/// <summary>
|
||||
@ -158,6 +270,16 @@ public class StreamingBenchmarks
|
||||
return EnumerateAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 NuGet `Mediator` stream request。
|
||||
/// </summary>
|
||||
IAsyncEnumerable<BenchmarkResponse> Mediator.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>.Handle(
|
||||
BenchmarkStreamRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Handle(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 MediatR stream request。
|
||||
/// </summary>
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Loggers;
|
||||
using BenchmarkDotNet.Running;
|
||||
|
||||
@ -11,13 +16,302 @@ namespace GFramework.Cqrs.Benchmarks;
|
||||
/// </summary>
|
||||
internal static class Program
|
||||
{
|
||||
private const string ArtifactsSuffixOption = "--artifacts-suffix";
|
||||
private const string ArtifactsSuffixEnvironmentVariable = "GFRAMEWORK_CQRS_BENCHMARK_ARTIFACTS_SUFFIX";
|
||||
private const string ArtifactsPathEnvironmentVariable = "GFRAMEWORK_CQRS_BENCHMARK_ARTIFACTS_PATH";
|
||||
private const string IsolatedHostEnvironmentVariable = "GFRAMEWORK_CQRS_BENCHMARK_ISOLATED_HOST";
|
||||
private const string DefaultArtifactsDirectoryName = "BenchmarkDotNet.Artifacts";
|
||||
private const string IsolatedHostDirectoryName = "host";
|
||||
|
||||
/// <summary>
|
||||
/// 运行当前程序集中的全部 benchmark。
|
||||
/// </summary>
|
||||
/// <param name="args">透传给 BenchmarkDotNet 的命令行参数。</param>
|
||||
/// <param name="args">仓库入口参数与透传给 BenchmarkDotNet 的命令行参数。</param>
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
var invocation = ParseInvocation(args);
|
||||
|
||||
ConsoleLogger.Default.WriteLine("Running GFramework.Cqrs benchmarks");
|
||||
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
|
||||
|
||||
if (invocation.RequiresHostIsolation &&
|
||||
!string.Equals(
|
||||
Environment.GetEnvironmentVariable(IsolatedHostEnvironmentVariable),
|
||||
"1",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
Environment.Exit(RunFromIsolatedHost(invocation, args));
|
||||
}
|
||||
|
||||
if (invocation.ArtifactsPath is null)
|
||||
{
|
||||
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(invocation.BenchmarkDotNetArguments);
|
||||
return;
|
||||
}
|
||||
|
||||
ConsoleLogger.Default.WriteLine(
|
||||
$"Using isolated BenchmarkDotNet artifacts path: {invocation.ArtifactsPath}");
|
||||
|
||||
BenchmarkSwitcher
|
||||
.FromAssembly(typeof(Program).Assembly)
|
||||
.Run(invocation.BenchmarkDotNetArguments, DefaultConfig.Instance.WithArtifactsPath(invocation.ArtifactsPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析仓库自定义参数,并生成实际传递给 BenchmarkDotNet 的参数与隔离后的 artifacts 路径。
|
||||
/// </summary>
|
||||
/// <param name="args">当前进程收到的完整命令行参数。</param>
|
||||
/// <returns>入口解析后的 benchmark 调用选项。</returns>
|
||||
/// <exception cref="ArgumentException">自定义参数缺失值或包含非法路径片段时抛出。</exception>
|
||||
private static BenchmarkInvocation ParseInvocation(string[] args)
|
||||
{
|
||||
var benchmarkDotNetArguments = new List<string>(args.Length);
|
||||
string? commandLineSuffix = null;
|
||||
|
||||
for (var index = 0; index < args.Length; index++)
|
||||
{
|
||||
var argument = args[index];
|
||||
if (!string.Equals(argument, ArtifactsSuffixOption, StringComparison.Ordinal))
|
||||
{
|
||||
benchmarkDotNetArguments.Add(argument);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (index == args.Length - 1)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The {ArtifactsSuffixOption} option requires a suffix value.",
|
||||
nameof(args));
|
||||
}
|
||||
|
||||
if (commandLineSuffix is not null)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The {ArtifactsSuffixOption} option can only be provided once.",
|
||||
nameof(args));
|
||||
}
|
||||
|
||||
// 剥离仓库自定义参数,避免将它误传给 BenchmarkDotNet 自身的命令行解析器。
|
||||
commandLineSuffix = args[++index];
|
||||
}
|
||||
|
||||
var artifactsPath = ResolveArtifactsPath(commandLineSuffix);
|
||||
return new BenchmarkInvocation(
|
||||
benchmarkDotNetArguments.ToArray(),
|
||||
commandLineSuffix,
|
||||
artifactsPath,
|
||||
artifactsPath is not null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前 benchmark 入口重启到独立的宿主工作目录,避免多个并发进程共享同一份 auto-generated build 目录。
|
||||
/// </summary>
|
||||
/// <param name="invocation">当前入口解析后的 benchmark 调用选项。</param>
|
||||
/// <param name="originalArgs">原始命令行参数,用于透传给隔离后的宿主进程。</param>
|
||||
/// <returns>隔离后宿主进程的退出码。</returns>
|
||||
private static int RunFromIsolatedHost(BenchmarkInvocation invocation, string[] originalArgs)
|
||||
{
|
||||
var artifactsPath = invocation.ArtifactsPath
|
||||
?? throw new ArgumentNullException(nameof(invocation), "An isolated benchmark host requires an artifacts path.");
|
||||
|
||||
var currentAssemblyPath = typeof(Program).Assembly.Location;
|
||||
var sourceHostDirectory = AppContext.BaseDirectory;
|
||||
var isolatedHostDirectory = Path.Combine(artifactsPath, IsolatedHostDirectoryName);
|
||||
|
||||
PrepareIsolatedHostDirectory(sourceHostDirectory, isolatedHostDirectory);
|
||||
|
||||
var isolatedAssemblyPath = Path.Combine(
|
||||
isolatedHostDirectory,
|
||||
Path.GetFileName(currentAssemblyPath));
|
||||
|
||||
var startInfo = new ProcessStartInfo("dotnet")
|
||||
{
|
||||
WorkingDirectory = isolatedHostDirectory,
|
||||
UseShellExecute = false
|
||||
};
|
||||
|
||||
startInfo.ArgumentList.Add(isolatedAssemblyPath);
|
||||
foreach (var argument in originalArgs)
|
||||
{
|
||||
startInfo.ArgumentList.Add(argument);
|
||||
}
|
||||
|
||||
startInfo.Environment[IsolatedHostEnvironmentVariable] = "1";
|
||||
startInfo.Environment[ArtifactsPathEnvironmentVariable] = artifactsPath;
|
||||
|
||||
ConsoleLogger.Default.WriteLine(
|
||||
$"Launching isolated benchmark host in: {isolatedHostDirectory}");
|
||||
|
||||
using var process = Process.Start(startInfo) ??
|
||||
throw new InvalidOperationException("Failed to launch the isolated benchmark host process.");
|
||||
|
||||
process.WaitForExit();
|
||||
return process.ExitCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据命令行或环境变量中的 suffix 生成当前 benchmark 运行的独立 artifacts 目录。
|
||||
/// </summary>
|
||||
/// <param name="commandLineSuffix">命令行显式提供的 suffix。</param>
|
||||
/// <returns>隔离后的 artifacts 目录;若未提供 suffix,则返回 <see langword="null"/>。</returns>
|
||||
private static string? ResolveArtifactsPath(string? commandLineSuffix)
|
||||
{
|
||||
var explicitArtifactsPath = Environment.GetEnvironmentVariable(ArtifactsPathEnvironmentVariable);
|
||||
if (!string.IsNullOrWhiteSpace(explicitArtifactsPath))
|
||||
{
|
||||
return Path.GetFullPath(explicitArtifactsPath);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(commandLineSuffix))
|
||||
{
|
||||
var validatedCommandLineSuffix = ValidateArtifactsSuffix(
|
||||
commandLineSuffix,
|
||||
ArtifactsSuffixOption);
|
||||
|
||||
return Path.GetFullPath(Path.Combine(DefaultArtifactsDirectoryName, validatedCommandLineSuffix));
|
||||
}
|
||||
|
||||
var environmentSuffix = Environment.GetEnvironmentVariable(ArtifactsSuffixEnvironmentVariable);
|
||||
if (string.IsNullOrWhiteSpace(environmentSuffix))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var validatedEnvironmentSuffix = ValidateArtifactsSuffix(
|
||||
environmentSuffix,
|
||||
ArtifactsSuffixEnvironmentVariable);
|
||||
|
||||
return Path.GetFullPath(Path.Combine(DefaultArtifactsDirectoryName, validatedEnvironmentSuffix));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验自定义 suffix,避免路径穿越、分隔符注入或不可移植字符污染 BenchmarkDotNet 的输出目录。
|
||||
/// </summary>
|
||||
/// <param name="suffix">待校验的后缀值。</param>
|
||||
/// <param name="sourceName">后缀来源名称,用于错误提示。</param>
|
||||
/// <returns>可安全用于单级目录名的后缀。</returns>
|
||||
/// <exception cref="ArgumentException">当后缀为空或包含未允许字符时抛出。</exception>
|
||||
private static string ValidateArtifactsSuffix(string suffix, string sourceName)
|
||||
{
|
||||
var trimmedSuffix = suffix.Trim();
|
||||
if (trimmedSuffix.Length == 0)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The {sourceName} value must not be empty.",
|
||||
nameof(suffix));
|
||||
}
|
||||
|
||||
foreach (var character in trimmedSuffix)
|
||||
{
|
||||
if (char.IsAsciiLetterOrDigit(character) || character is '.' or '-' or '_')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new ArgumentException(
|
||||
$"The {sourceName} value '{trimmedSuffix}' contains unsupported characters. " +
|
||||
"Only ASCII letters, digits, '.', '-' and '_' are allowed.",
|
||||
nameof(suffix));
|
||||
}
|
||||
|
||||
return trimmedSuffix;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前 benchmark 宿主输出复制到独立目录,确保并发运行时的 auto-generated benchmark 项目不会写入同一路径。
|
||||
/// </summary>
|
||||
/// <param name="sourceHostDirectory">当前 benchmark 宿主输出目录。</param>
|
||||
/// <param name="isolatedHostDirectory">当前 suffix 对应的独立宿主目录。</param>
|
||||
private static void PrepareIsolatedHostDirectory(string sourceHostDirectory, string isolatedHostDirectory)
|
||||
{
|
||||
ValidateIsolatedHostDirectory(sourceHostDirectory, isolatedHostDirectory);
|
||||
Directory.CreateDirectory(isolatedHostDirectory);
|
||||
CopyDirectoryRecursively(sourceHostDirectory, isolatedHostDirectory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拒绝把隔离宿主目录放到当前宿主输出目录内部,避免递归复制把 `host/host/...` 无限扩张。
|
||||
/// </summary>
|
||||
/// <param name="sourceHostDirectory">当前 benchmark 宿主输出目录。</param>
|
||||
/// <param name="isolatedHostDirectory">目标隔离宿主目录。</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// <paramref name="isolatedHostDirectory"/> 等于或位于 <paramref name="sourceHostDirectory"/> 之内。
|
||||
/// </exception>
|
||||
private static void ValidateIsolatedHostDirectory(string sourceHostDirectory, string isolatedHostDirectory)
|
||||
{
|
||||
var normalizedSourceDirectory = Path.TrimEndingDirectorySeparator(Path.GetFullPath(sourceHostDirectory));
|
||||
var normalizedIsolatedHostDirectory = Path.TrimEndingDirectorySeparator(Path.GetFullPath(isolatedHostDirectory));
|
||||
|
||||
if (string.Equals(
|
||||
normalizedSourceDirectory,
|
||||
normalizedIsolatedHostDirectory,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The isolated benchmark host directory must differ from the current host output directory.");
|
||||
}
|
||||
|
||||
var relativePath = Path.GetRelativePath(normalizedSourceDirectory, normalizedIsolatedHostDirectory);
|
||||
if (IsCurrentDirectoryOrChild(relativePath))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The isolated benchmark host directory '{normalizedIsolatedHostDirectory}' must not be nested inside the current host output directory '{normalizedSourceDirectory}'.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断一个相对路径是否仍指向当前目录或其子目录。
|
||||
/// </summary>
|
||||
/// <param name="relativePath">相对路径。</param>
|
||||
/// <returns>目标位于当前目录或其子目录时返回 <see langword="true"/>。</returns>
|
||||
private static bool IsCurrentDirectoryOrChild(string relativePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(relativePath) || string.Equals(relativePath, ".", StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Path.IsPathRooted(relativePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !string.Equals(relativePath, "..", StringComparison.Ordinal) &&
|
||||
!relativePath.StartsWith(".." + Path.DirectorySeparatorChar, StringComparison.Ordinal) &&
|
||||
!relativePath.StartsWith(".." + Path.AltDirectorySeparatorChar, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归复制 benchmark 宿主输出目录,覆盖同名文件以支持同一 suffix 的重复运行。
|
||||
/// </summary>
|
||||
/// <param name="sourceDirectory">源目录。</param>
|
||||
/// <param name="destinationDirectory">目标目录。</param>
|
||||
private static void CopyDirectoryRecursively(string sourceDirectory, string destinationDirectory)
|
||||
{
|
||||
foreach (var directory in Directory.GetDirectories(sourceDirectory, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var relativeDirectory = Path.GetRelativePath(sourceDirectory, directory);
|
||||
Directory.CreateDirectory(Path.Combine(destinationDirectory, relativeDirectory));
|
||||
}
|
||||
|
||||
foreach (var file in Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var relativeFile = Path.GetRelativePath(sourceDirectory, file);
|
||||
var destinationFile = Path.Combine(destinationDirectory, relativeFile);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destinationFile)!);
|
||||
File.Copy(file, destinationFile, overwrite: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示一次 benchmark 入口调用在剥离仓库自定义参数后的最终配置。
|
||||
/// </summary>
|
||||
/// <param name="BenchmarkDotNetArguments">实际传递给 BenchmarkDotNet 的命令行参数。</param>
|
||||
/// <param name="ArtifactsSuffix">当前运行声明的隔离后缀;若未声明则为 <see langword="null"/>。</param>
|
||||
/// <param name="ArtifactsPath">本次运行的 artifacts 目录;若未隔离则为 <see langword="null"/>。</param>
|
||||
/// <param name="RequiresHostIsolation">本次运行是否需要重启到隔离宿主目录。</param>
|
||||
private readonly record struct BenchmarkInvocation(
|
||||
string[] BenchmarkDotNetArguments,
|
||||
string? ArtifactsSuffix,
|
||||
string? ArtifactsPath,
|
||||
bool RequiresHostIsolation);
|
||||
}
|
||||
|
||||
@ -1,45 +1,108 @@
|
||||
# GFramework.Cqrs.Benchmarks
|
||||
|
||||
该模块承载 `GFramework.Cqrs` 的独立性能基准工程,用于持续比较运行时 dispatch、publish、cold-start 与后续 generator / pipeline 收口的成本变化。
|
||||
该模块承载 `GFramework.Cqrs` 的独立性能基准工程,用于在当前 HEAD 上复核 request、stream、notification 的 steady-state 与 startup 成本边界。
|
||||
|
||||
## 目的
|
||||
|
||||
- 为 `GFramework.Cqrs` 建立独立于 NUnit 集成测试的 BenchmarkDotNet 基线
|
||||
- 参考 `ai-libs/Mediator/benchmarks` 的场景组织方式,逐步补齐 request、notification、stream 与初始化成本对比
|
||||
- 为后续吸收 `Mediator` 的 dispatch 设计、fixture 组织和对比矩阵提供可重复验证入口
|
||||
- 为 `GFramework.Cqrs` 提供独立于测试工程的 BenchmarkDotNet 复核入口
|
||||
- 让 request、stream、notification 的热路径与 cold-start 变化有可重复的对照矩阵
|
||||
- 在不引入“未来已存在”假设的前提下,明确当前 benchmark 已覆盖什么、还没有覆盖什么
|
||||
|
||||
## 当前内容
|
||||
## 当前 coverage
|
||||
|
||||
- `Program.cs`
|
||||
- benchmark 命令行入口
|
||||
- `Messaging/Fixture.cs`
|
||||
- 运行前输出并校验场景配置
|
||||
- `Messaging/RequestBenchmarks.cs`
|
||||
- direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
|
||||
- `Messaging/RequestPipelineBenchmarks.cs`
|
||||
- `0 / 1 / 4` 个 pipeline 行为下,direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
|
||||
- `Messaging/RequestStartupBenchmarks.cs`
|
||||
- `Initialization` 与 `ColdStart` 两组 request startup 成本对比,补齐与 `Mediator` comparison benchmark 更接近的 startup 维度
|
||||
- `Messaging/RequestInvokerBenchmarks.cs`
|
||||
- direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 request steady-state dispatch 对比
|
||||
- `Messaging/StreamInvokerBenchmarks.cs`
|
||||
- direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 stream 完整枚举对比
|
||||
- `Messaging/NotificationBenchmarks.cs`
|
||||
- `GFramework.Cqrs` runtime 与 `MediatR` 的单处理器 notification publish 对比
|
||||
- `Messaging/StreamingBenchmarks.cs`
|
||||
- direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 stream request 完整枚举对比
|
||||
当前工程已经覆盖以下矩阵:
|
||||
|
||||
- request steady-state
|
||||
- `Messaging/RequestBenchmarks.cs`
|
||||
- direct handler、默认 `GFramework.Cqrs` runtime、NuGet `Mediator` source-generated concrete path、`MediatR`
|
||||
- `Messaging/RequestLifetimeBenchmarks.cs`
|
||||
- `Singleton / Scoped / Transient` 三类 handler 生命周期下,baseline、默认 generated-provider 宿主接线的 `GFramework.Cqrs` runtime 与 `MediatR`
|
||||
- `Messaging/RequestPipelineBenchmarks.cs`
|
||||
- `0 / 1 / 4` 个 pipeline 行为下,baseline、默认 generated-provider 宿主接线的 `GFramework.Cqrs` runtime 与 `MediatR`
|
||||
- `Messaging/RequestInvokerBenchmarks.cs`
|
||||
- baseline、`GFramework.Cqrs` reflection request binding、`GFramework.Cqrs` generated request invoker、`MediatR`
|
||||
- request startup
|
||||
- `Messaging/RequestStartupBenchmarks.cs`
|
||||
- `Initialization` 与 `ColdStart` 两组下,`GFramework.Cqrs`、NuGet `Mediator`、`MediatR`
|
||||
- 其中 `GFramework.Cqrs` 路径是“单 handler 最小宿主 + 手工注册”的 startup/cold-start 模型,不包含更大范围的程序集扫描或完整注册协调器接线
|
||||
- stream steady-state
|
||||
- `Messaging/StreamingBenchmarks.cs`
|
||||
- baseline、默认 generated-provider 宿主接线的 `GFramework.Cqrs` runtime、NuGet `Mediator` source-generated concrete path 与 `MediatR`
|
||||
- 同时提供 `FirstItem` 与 `DrainAll` 两种观测口径
|
||||
- `Messaging/StreamLifetimeBenchmarks.cs`
|
||||
- `Singleton / Scoped / Transient` 三类 handler 生命周期下,baseline、`GFramework.Cqrs` reflection stream binding、`GFramework.Cqrs` generated stream registry、`MediatR`
|
||||
- 同时提供 `FirstItem` 与 `DrainAll` 两种观测口径
|
||||
- `Messaging/StreamInvokerBenchmarks.cs`
|
||||
- baseline、`GFramework.Cqrs` reflection stream binding、`GFramework.Cqrs` generated stream invoker、`MediatR`
|
||||
- 同时提供 `FirstItem` 与 `DrainAll` 两种观测口径
|
||||
- `Messaging/StreamPipelineBenchmarks.cs`
|
||||
- `0 / 1 / 4` 个 stream pipeline 行为下,baseline、默认 generated-provider 宿主接线的 `GFramework.Cqrs` runtime 与 `MediatR`
|
||||
- 同时提供 `FirstItem` 与 `DrainAll` 两种观测口径
|
||||
- stream startup
|
||||
- `Messaging/StreamStartupBenchmarks.cs`
|
||||
- `Initialization` 与 `ColdStart` 两组下,覆盖 `MediatR`、`GFramework.Cqrs` reflection、`GFramework.Cqrs` generated、NuGet `Mediator` 四组 initialization/cold-start 对照
|
||||
- 其中 `ColdStart` 的边界是“新宿主 + 首个元素命中”,不是完整枚举整个 stream
|
||||
- notification steady-state
|
||||
- `Messaging/NotificationBenchmarks.cs`
|
||||
- 单处理器 publish 下,`GFramework.Cqrs` runtime、NuGet `Mediator` source-generated concrete path、`MediatR`
|
||||
- `Messaging/NotificationLifetimeBenchmarks.cs`
|
||||
- 单处理器 publish 在 `Singleton / Scoped / Transient` 三类 handler 生命周期下的 baseline、`GFramework.Cqrs` 与 `MediatR` 对照
|
||||
- `Messaging/NotificationFanOutBenchmarks.cs`
|
||||
- 固定 `4 handler` fan-out 下的 baseline、`GFramework.Cqrs` 默认顺序发布器、内置 `TaskWhenAllNotificationPublisher`、NuGet `Mediator`、`MediatR`
|
||||
- notification startup
|
||||
- `Messaging/NotificationStartupBenchmarks.cs`
|
||||
- `Initialization` 与 `ColdStart` 两组下,`GFramework.Cqrs`、NuGet `Mediator`、`MediatR`
|
||||
- 其中 `GFramework.Cqrs` 路径是“单 handler 最小宿主 + 手工注册”的 startup/cold-start 模型,不包含 fan-out、发布策略变体或更大范围的注册协调逻辑
|
||||
|
||||
## 最小使用方式
|
||||
|
||||
```bash
|
||||
dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release
|
||||
```
|
||||
|
||||
上面的命令只验证 benchmark 工程当前可以正常编译。
|
||||
|
||||
如需实际运行 benchmark,再执行:
|
||||
|
||||
```bash
|
||||
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release
|
||||
```
|
||||
|
||||
也可以通过 `BenchmarkDotNet` 过滤器只运行某一类场景。
|
||||
如需只复核某一类场景,可把 `BenchmarkDotNet` 参数放在 `--` 之后,例如:
|
||||
|
||||
## 后续扩展方向
|
||||
```bash
|
||||
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*RequestLifetimeBenchmarks.SendRequest_*"
|
||||
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*StreamLifetimeBenchmarks.Stream_*"
|
||||
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*StreamPipelineBenchmarks.Stream_*"
|
||||
```
|
||||
|
||||
- generated invoker provider 与纯反射 dispatch 对比
|
||||
- generated stream invoker provider 与纯反射建流对比
|
||||
- registration / service lifetime 矩阵
|
||||
- request / stream 的真实 source-generator 产物与 handwritten generated provider 对照
|
||||
## 并发运行约束
|
||||
|
||||
当两个 benchmark 进程需要并发运行时,必须为每个进程追加不同的 `--artifacts-suffix <suffix>`。当前入口会把这个 suffix 解析成独立的 `BenchmarkDotNet.Artifacts/<suffix>/` 目录,并在该目录下复制隔离的 benchmark host,避免多个进程写入同一份 auto-generated build 与 artifacts 输出。
|
||||
|
||||
例如:
|
||||
|
||||
```bash
|
||||
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix req-lifetime-a --filter "*RequestLifetimeBenchmarks.SendRequest_*"
|
||||
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix stream-lifetime-b --filter "*StreamLifetimeBenchmarks.Stream_*"
|
||||
```
|
||||
|
||||
如果不并发运行,就不需要额外传入 `--artifacts-suffix`。`BenchmarkDotNet.Artifacts/` 仍然是本地生成输出,默认不作为常规提交内容。
|
||||
|
||||
## 结果解读边界
|
||||
|
||||
- `RequestLifetimeBenchmarks` 的 `Scoped` 场景会在每次 request 分发时显式创建并释放真实 DI 作用域;它观察的是 scoped handler 的解析与 dispatch 成本,不把 runtime 构造常量成本混入生命周期对照
|
||||
- `NotificationLifetimeBenchmarks` 的 `Scoped` 场景也采用真实 DI 作用域;它比较的是 publish 路径上的生命周期额外开销,不是根容器解析退化后的近似值
|
||||
- `StreamingBenchmarks`、`StreamLifetimeBenchmarks`、`StreamInvokerBenchmarks`、`StreamPipelineBenchmarks` 同时暴露 `FirstItem` 与 `DrainAll`
|
||||
- `FirstItem` 适合观察“建流到首个元素”的固定成本
|
||||
- `DrainAll` 适合观察完整枚举整个 stream 的总成本
|
||||
- `StreamStartupBenchmarks` 的 `ColdStart` 只推进到首个元素,因此它回答的是“新宿主下首次建流命中”的边界,不回答完整枚举总成本
|
||||
- `RequestStartupBenchmarks` 与 `NotificationStartupBenchmarks` 的 `GFramework.Cqrs` startup 路径都固定在单 handler、最小宿主、手工注册模型;它们回答的是首次 request / publish 命中的额外成本,不代表程序集扫描或完整注册协调器场景
|
||||
- 当前 HEAD 没有单独固化的 short-job benchmark 类或 checked-in short-job 结果;如果手动使用 short job / short run 只做 smoke 复核,应把它理解为“确认矩阵与路径能跑通”
|
||||
- 特别是 `StreamInvokerBenchmarks` 的 `DrainAll` 在 short-job smoke 下不应直接写成 reflection、generated 或 `MediatR` 之间的稳定排序结论;若要比较名次或小幅差值,应复跑默认作业或更完整的批次
|
||||
|
||||
## 当前缺口
|
||||
|
||||
- 当前没有 stream 生命周期版的 NuGet `Mediator` source-generated concrete path 对照;`StreamLifetimeBenchmarks` 现在只覆盖 `GFramework.Cqrs` 与 `MediatR`
|
||||
- 当前没有 request 生命周期版的 NuGet `Mediator` source-generated concrete path 对照;`Mediator` 的 DI lifetime 由 source generator 在 benchmark 项目编译期固定,若要比较 `Singleton / Scoped / Transient`,需要拆成独立 build config 或独立 benchmark 工程,而不是在同一份生成产物里切换
|
||||
- 当前没有 notification fan-out 的生命周期矩阵;`NotificationFanOutBenchmarks` 只覆盖固定 `4 handler` 的已装配宿主
|
||||
|
||||
@ -27,20 +27,13 @@ internal sealed class CqrsDispatcherCacheTests
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
|
||||
_container = new MicrosoftDiContainer();
|
||||
_container.RegisterCqrsPipelineBehavior<DispatcherPipelineCacheBehavior>();
|
||||
_container.RegisterCqrsPipelineBehavior<DispatcherPipelineContextRefreshBehavior>();
|
||||
_container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderOuterBehavior>();
|
||||
_container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderInnerBehavior>();
|
||||
|
||||
CqrsTestRuntime.RegisterHandlers(
|
||||
_container,
|
||||
typeof(CqrsDispatcherCacheTests).Assembly,
|
||||
typeof(ArchitectureContext).Assembly);
|
||||
ConfigureDispatcherCacheFixture(_container);
|
||||
|
||||
_container.Freeze();
|
||||
_context = new ArchitectureContext(_container);
|
||||
DispatcherNotificationContextRefreshState.Reset();
|
||||
DispatcherPipelineContextRefreshState.Reset();
|
||||
DispatcherStreamPipelineOrderState.Reset();
|
||||
DispatcherStreamContextRefreshState.Reset();
|
||||
ClearDispatcherCaches();
|
||||
}
|
||||
@ -155,6 +148,102 @@ internal sealed class CqrsDispatcherCacheTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 request 的“是否存在 pipeline behavior”判定会按 dispatcher 实例缓存,
|
||||
/// 让零行为请求在首次分发后不再重复查询容器,同时不同 dispatcher 不共享该实例级状态。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Dispatcher_Should_Cache_Zero_Pipeline_Request_Presence_Per_Dispatcher_Instance()
|
||||
{
|
||||
var firstContext = new ArchitectureContext(_container!);
|
||||
var secondContext = new ArchitectureContext(_container!);
|
||||
var firstDispatcher = GetDispatcherFromContext(firstContext);
|
||||
var secondDispatcher = GetDispatcherFromContext(secondContext);
|
||||
using var isolatedContainer = CreateFrozenContainer();
|
||||
var isolatedContext = new ArchitectureContext(isolatedContainer);
|
||||
var isolatedDispatcher = GetDispatcherFromContext(isolatedContext);
|
||||
|
||||
AssertRequestBehaviorPresenceIsUnset(firstDispatcher, typeof(IPipelineBehavior<DispatcherCacheRequest, int>));
|
||||
AssertRequestBehaviorPresenceIsUnset(secondDispatcher, typeof(IPipelineBehavior<DispatcherCacheRequest, int>));
|
||||
AssertRequestBehaviorPresenceIsUnset(isolatedDispatcher, typeof(IPipelineBehavior<DispatcherCacheRequest, int>));
|
||||
AssertRequestBehaviorPresenceIsUnset(
|
||||
firstDispatcher,
|
||||
typeof(IPipelineBehavior<DispatcherPipelineCacheRequest, int>));
|
||||
|
||||
await firstContext.SendRequestAsync(new DispatcherCacheRequest());
|
||||
await firstContext.SendRequestAsync(new DispatcherPipelineCacheRequest());
|
||||
|
||||
var zeroPipelinePresence = GetRequestBehaviorPresenceCacheValue(
|
||||
firstDispatcher,
|
||||
typeof(IPipelineBehavior<DispatcherCacheRequest, int>));
|
||||
var onePipelinePresence = GetRequestBehaviorPresenceCacheValue(
|
||||
firstDispatcher,
|
||||
typeof(IPipelineBehavior<DispatcherPipelineCacheRequest, int>));
|
||||
|
||||
AssertSharedDispatcherCacheState(
|
||||
firstDispatcher,
|
||||
secondDispatcher,
|
||||
isolatedDispatcher,
|
||||
zeroPipelinePresence,
|
||||
onePipelinePresence);
|
||||
|
||||
await isolatedContext.SendRequestAsync(new DispatcherCacheRequest());
|
||||
|
||||
AssertRequestBehaviorPresenceEquals(
|
||||
isolatedDispatcher,
|
||||
typeof(IPipelineBehavior<DispatcherCacheRequest, int>),
|
||||
false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 stream 的“是否存在 pipeline behavior”判定会按 dispatcher 实例缓存,
|
||||
/// 并与当前容器的实际服务可见性保持一致,同时不同 dispatcher 不共享该实例级状态。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Dispatcher_Should_Cache_Stream_Behavior_Presence_Per_Dispatcher_Instance()
|
||||
{
|
||||
var firstContext = new ArchitectureContext(_container!);
|
||||
var secondContext = new ArchitectureContext(_container!);
|
||||
var firstDispatcher = GetDispatcherFromContext(firstContext);
|
||||
var secondDispatcher = GetDispatcherFromContext(secondContext);
|
||||
using var isolatedContainer = CreateFrozenContainer();
|
||||
var isolatedContext = new ArchitectureContext(isolatedContainer);
|
||||
var isolatedDispatcher = GetDispatcherFromContext(isolatedContext);
|
||||
var zeroPipelineBehaviorType = typeof(IStreamPipelineBehavior<DispatcherZeroPipelineStreamRequest, int>);
|
||||
var twoPipelineBehaviorType = typeof(IStreamPipelineBehavior<DispatcherStreamPipelineOrderRequest, int>);
|
||||
var expectedZeroPipelinePresence = _container!.HasRegistration(zeroPipelineBehaviorType);
|
||||
var expectedTwoPipelinePresence = _container.HasRegistration(twoPipelineBehaviorType);
|
||||
|
||||
AssertStreamBehaviorPresenceIsUnset(firstDispatcher, zeroPipelineBehaviorType);
|
||||
AssertStreamBehaviorPresenceIsUnset(secondDispatcher, zeroPipelineBehaviorType);
|
||||
AssertStreamBehaviorPresenceIsUnset(isolatedDispatcher, zeroPipelineBehaviorType);
|
||||
AssertStreamBehaviorPresenceIsUnset(firstDispatcher, twoPipelineBehaviorType);
|
||||
|
||||
await DrainAsync(firstContext.CreateStream(new DispatcherZeroPipelineStreamRequest()));
|
||||
await DrainAsync(firstContext.CreateStream(new DispatcherStreamPipelineOrderRequest()));
|
||||
|
||||
var zeroPipelinePresence = GetStreamBehaviorPresenceCacheValue(
|
||||
firstDispatcher,
|
||||
zeroPipelineBehaviorType);
|
||||
var twoPipelinePresence = GetStreamBehaviorPresenceCacheValue(
|
||||
firstDispatcher,
|
||||
twoPipelineBehaviorType);
|
||||
|
||||
AssertSharedStreamDispatcherCacheState(
|
||||
firstDispatcher,
|
||||
secondDispatcher,
|
||||
isolatedDispatcher,
|
||||
zeroPipelinePresence,
|
||||
twoPipelinePresence,
|
||||
zeroPipelineBehaviorType,
|
||||
expectedZeroPipelinePresence,
|
||||
expectedTwoPipelinePresence);
|
||||
|
||||
await DrainAsync(isolatedContext.CreateStream(new DispatcherZeroPipelineStreamRequest()));
|
||||
|
||||
AssertStreamBehaviorPresenceEquals(isolatedDispatcher, zeroPipelineBehaviorType, expectedZeroPipelinePresence);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 request pipeline executor 会按行为数量在 binding 内首次创建并在后续分发中复用。
|
||||
/// </summary>
|
||||
@ -220,6 +309,179 @@ internal sealed class CqrsDispatcherCacheTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证同一 request dispatch binding 先走零行为直连路径时不会提前创建 pipeline executor,
|
||||
/// 后续另一 dispatcher 命中相同 binding 且存在行为时,仍会按实际行为数量补建缓存 executor。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Dispatcher_Should_Create_Request_Pipeline_Executor_Only_When_Shared_Binding_Sees_Behaviors()
|
||||
{
|
||||
var requestBindings = GetCacheField("RequestDispatchBindings");
|
||||
var behaviorType = typeof(IPipelineBehavior<DispatcherPipelineCacheRequest, int>);
|
||||
using var zeroBehaviorContainer = CreateFrozenContainer(
|
||||
new DispatcherCacheFixtureOptions
|
||||
{
|
||||
IncludeRequestPipelineCacheBehavior = false
|
||||
});
|
||||
var zeroBehaviorContext = new ArchitectureContext(zeroBehaviorContainer);
|
||||
var behaviorContext = new ArchitectureContext(_container!);
|
||||
var zeroBehaviorDispatcher = GetDispatcherFromContext(zeroBehaviorContext);
|
||||
var behaviorDispatcher = GetDispatcherFromContext(behaviorContext);
|
||||
|
||||
await zeroBehaviorContext.SendRequestAsync(new DispatcherPipelineCacheRequest());
|
||||
|
||||
var bindingAfterZeroBehaviorDispatch = GetPairCacheValue(
|
||||
requestBindings,
|
||||
typeof(DispatcherPipelineCacheRequest),
|
||||
typeof(int));
|
||||
var executorAfterZeroBehaviorDispatch = GetRequestPipelineExecutorValue(
|
||||
requestBindings,
|
||||
typeof(DispatcherPipelineCacheRequest),
|
||||
typeof(int),
|
||||
1);
|
||||
|
||||
await behaviorContext.SendRequestAsync(new DispatcherPipelineCacheRequest());
|
||||
|
||||
var bindingAfterBehaviorDispatch = GetPairCacheValue(
|
||||
requestBindings,
|
||||
typeof(DispatcherPipelineCacheRequest),
|
||||
typeof(int));
|
||||
var executorAfterBehaviorDispatch = GetRequestPipelineExecutorValue(
|
||||
requestBindings,
|
||||
typeof(DispatcherPipelineCacheRequest),
|
||||
typeof(int),
|
||||
1);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(bindingAfterZeroBehaviorDispatch, Is.Not.Null);
|
||||
Assert.That(bindingAfterBehaviorDispatch, Is.SameAs(bindingAfterZeroBehaviorDispatch));
|
||||
Assert.That(executorAfterZeroBehaviorDispatch, Is.Null);
|
||||
Assert.That(executorAfterBehaviorDispatch, Is.Not.Null);
|
||||
AssertRequestBehaviorPresenceEquals(zeroBehaviorDispatcher, behaviorType, false);
|
||||
AssertRequestBehaviorPresenceEquals(behaviorDispatcher, behaviorType, true);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 stream pipeline executor 会按行为数量在 binding 内首次创建并在后续建流中复用。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Dispatcher_Should_Cache_Stream_Pipeline_Executors_Per_Behavior_Count()
|
||||
{
|
||||
var streamBindings = GetCacheField("StreamDispatchBindings");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(
|
||||
GetStreamPipelineExecutorValue(
|
||||
streamBindings,
|
||||
typeof(DispatcherCacheStreamRequest),
|
||||
typeof(int),
|
||||
1),
|
||||
Is.Null);
|
||||
Assert.That(
|
||||
GetStreamPipelineExecutorValue(
|
||||
streamBindings,
|
||||
typeof(DispatcherStreamPipelineOrderRequest),
|
||||
typeof(int),
|
||||
2),
|
||||
Is.Null);
|
||||
});
|
||||
|
||||
await DrainAsync(_context!.CreateStream(new DispatcherCacheStreamRequest()));
|
||||
await DrainAsync(_context.CreateStream(new DispatcherStreamPipelineOrderRequest()));
|
||||
|
||||
var singleBehaviorExecutor = GetStreamPipelineExecutorValue(
|
||||
streamBindings,
|
||||
typeof(DispatcherCacheStreamRequest),
|
||||
typeof(int),
|
||||
1);
|
||||
var twoBehaviorExecutor = GetStreamPipelineExecutorValue(
|
||||
streamBindings,
|
||||
typeof(DispatcherStreamPipelineOrderRequest),
|
||||
typeof(int),
|
||||
2);
|
||||
|
||||
await DrainAsync(_context.CreateStream(new DispatcherCacheStreamRequest()));
|
||||
await DrainAsync(_context.CreateStream(new DispatcherStreamPipelineOrderRequest()));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(singleBehaviorExecutor, Is.Not.Null);
|
||||
Assert.That(twoBehaviorExecutor, Is.Not.Null);
|
||||
Assert.That(singleBehaviorExecutor, Is.Not.SameAs(twoBehaviorExecutor));
|
||||
Assert.That(
|
||||
GetStreamPipelineExecutorValue(
|
||||
streamBindings,
|
||||
typeof(DispatcherCacheStreamRequest),
|
||||
typeof(int),
|
||||
1),
|
||||
Is.SameAs(singleBehaviorExecutor));
|
||||
Assert.That(
|
||||
GetStreamPipelineExecutorValue(
|
||||
streamBindings,
|
||||
typeof(DispatcherStreamPipelineOrderRequest),
|
||||
typeof(int),
|
||||
2),
|
||||
Is.SameAs(twoBehaviorExecutor));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证同一 stream dispatch binding 先走零行为直连路径时不会提前创建 pipeline executor,
|
||||
/// 后续另一 dispatcher 命中相同 binding 且存在行为时,仍会按实际行为数量补建缓存 executor。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Dispatcher_Should_Create_Stream_Pipeline_Executor_Only_When_Shared_Binding_Sees_Behaviors()
|
||||
{
|
||||
var streamBindings = GetCacheField("StreamDispatchBindings");
|
||||
var behaviorType = typeof(IStreamPipelineBehavior<DispatcherCacheStreamRequest, int>);
|
||||
using var zeroBehaviorContainer = CreateFrozenContainer(
|
||||
new DispatcherCacheFixtureOptions
|
||||
{
|
||||
IncludeStreamPipelineCacheBehavior = false
|
||||
});
|
||||
var zeroBehaviorContext = new ArchitectureContext(zeroBehaviorContainer);
|
||||
var behaviorContext = new ArchitectureContext(_container!);
|
||||
var zeroBehaviorDispatcher = GetDispatcherFromContext(zeroBehaviorContext);
|
||||
var behaviorDispatcher = GetDispatcherFromContext(behaviorContext);
|
||||
|
||||
await DrainAsync(zeroBehaviorContext.CreateStream(new DispatcherCacheStreamRequest()));
|
||||
|
||||
var bindingAfterZeroBehaviorDispatch = GetPairCacheValue(
|
||||
streamBindings,
|
||||
typeof(DispatcherCacheStreamRequest),
|
||||
typeof(int));
|
||||
var executorAfterZeroBehaviorDispatch = GetStreamPipelineExecutorValue(
|
||||
streamBindings,
|
||||
typeof(DispatcherCacheStreamRequest),
|
||||
typeof(int),
|
||||
1);
|
||||
|
||||
await DrainAsync(behaviorContext.CreateStream(new DispatcherCacheStreamRequest()));
|
||||
|
||||
var bindingAfterBehaviorDispatch = GetPairCacheValue(
|
||||
streamBindings,
|
||||
typeof(DispatcherCacheStreamRequest),
|
||||
typeof(int));
|
||||
var executorAfterBehaviorDispatch = GetStreamPipelineExecutorValue(
|
||||
streamBindings,
|
||||
typeof(DispatcherCacheStreamRequest),
|
||||
typeof(int),
|
||||
1);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(bindingAfterZeroBehaviorDispatch, Is.Not.Null);
|
||||
Assert.That(bindingAfterBehaviorDispatch, Is.SameAs(bindingAfterZeroBehaviorDispatch));
|
||||
Assert.That(executorAfterZeroBehaviorDispatch, Is.Null);
|
||||
Assert.That(executorAfterBehaviorDispatch, Is.Not.Null);
|
||||
AssertStreamBehaviorPresenceEquals(zeroBehaviorDispatcher, behaviorType, false);
|
||||
AssertStreamBehaviorPresenceEquals(behaviorDispatcher, behaviorType, true);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证复用缓存的 request pipeline executor 后,行为顺序和最终处理器顺序保持不变。
|
||||
/// </summary>
|
||||
@ -252,6 +514,38 @@ internal sealed class CqrsDispatcherCacheTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证复用缓存的 stream pipeline executor 后,行为顺序和最终处理器顺序保持不变。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Dispatcher_Should_Preserve_Stream_Pipeline_Order_When_Reusing_Cached_Executor()
|
||||
{
|
||||
DispatcherStreamPipelineOrderState.Reset();
|
||||
|
||||
await DrainAsync(_context!.CreateStream(new DispatcherStreamPipelineOrderRequest()));
|
||||
var firstInvocation = DispatcherStreamPipelineOrderState.Steps.ToArray();
|
||||
|
||||
DispatcherStreamPipelineOrderState.Reset();
|
||||
|
||||
await DrainAsync(_context.CreateStream(new DispatcherStreamPipelineOrderRequest()));
|
||||
var secondInvocation = DispatcherStreamPipelineOrderState.Steps.ToArray();
|
||||
|
||||
var expectedOrder = new[]
|
||||
{
|
||||
"Outer:Before",
|
||||
"Inner:Before",
|
||||
"Handler",
|
||||
"Inner:After",
|
||||
"Outer:After"
|
||||
};
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(firstInvocation, Is.EqualTo(expectedOrder));
|
||||
Assert.That(secondInvocation, Is.EqualTo(expectedOrder));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证缓存的 request pipeline executor 在重复分发时仍会重新解析 handler/behavior,
|
||||
/// 并为当次实例重新注入当前架构上下文。
|
||||
@ -306,6 +600,71 @@ internal sealed class CqrsDispatcherCacheTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证同一 request dispatch binding 先在零行为 dispatcher 上命中后,
|
||||
/// 后续切换到存在行为的 dispatcher 仍会重新解析 behavior/handler,并为当前上下文重新注入架构实例。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Dispatcher_Should_Reinject_Current_Request_Context_When_Shared_Binding_Switches_From_Zero_Pipeline()
|
||||
{
|
||||
DispatcherPipelineContextRefreshState.Reset();
|
||||
|
||||
var requestBindings = GetCacheField("RequestDispatchBindings");
|
||||
using var zeroBehaviorContainer = CreateFrozenContainer(
|
||||
new DispatcherCacheFixtureOptions
|
||||
{
|
||||
IncludeRequestPipelineContextRefreshBehavior = false
|
||||
});
|
||||
var zeroBehaviorContext = new ArchitectureContext(zeroBehaviorContainer);
|
||||
var behaviorContext = new ArchitectureContext(_container!);
|
||||
|
||||
await zeroBehaviorContext.SendRequestAsync(new DispatcherPipelineContextRefreshRequest("without-behavior"));
|
||||
|
||||
var bindingAfterZeroBehaviorDispatch = GetPairCacheValue(
|
||||
requestBindings,
|
||||
typeof(DispatcherPipelineContextRefreshRequest),
|
||||
typeof(int));
|
||||
var executorAfterZeroBehaviorDispatch = GetRequestPipelineExecutorValue(
|
||||
requestBindings,
|
||||
typeof(DispatcherPipelineContextRefreshRequest),
|
||||
typeof(int),
|
||||
1);
|
||||
|
||||
await behaviorContext.SendRequestAsync(new DispatcherPipelineContextRefreshRequest("with-behavior"));
|
||||
|
||||
var bindingAfterBehaviorDispatch = GetPairCacheValue(
|
||||
requestBindings,
|
||||
typeof(DispatcherPipelineContextRefreshRequest),
|
||||
typeof(int));
|
||||
var executorAfterBehaviorDispatch = GetRequestPipelineExecutorValue(
|
||||
requestBindings,
|
||||
typeof(DispatcherPipelineContextRefreshRequest),
|
||||
typeof(int),
|
||||
1);
|
||||
var behaviorSnapshots = DispatcherPipelineContextRefreshState.BehaviorSnapshots.ToArray();
|
||||
var handlerSnapshots = DispatcherPipelineContextRefreshState.HandlerSnapshots.ToArray();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(bindingAfterZeroBehaviorDispatch, Is.Not.Null);
|
||||
Assert.That(bindingAfterBehaviorDispatch, Is.SameAs(bindingAfterZeroBehaviorDispatch));
|
||||
Assert.That(executorAfterZeroBehaviorDispatch, Is.Null);
|
||||
Assert.That(executorAfterBehaviorDispatch, Is.Not.Null);
|
||||
|
||||
Assert.That(behaviorSnapshots, Has.Length.EqualTo(1));
|
||||
Assert.That(behaviorSnapshots[0].DispatchId, Is.EqualTo("with-behavior"));
|
||||
Assert.That(behaviorSnapshots[0].Context, Is.SameAs(behaviorContext));
|
||||
|
||||
Assert.That(handlerSnapshots, Has.Length.EqualTo(2));
|
||||
Assert.That(handlerSnapshots[0].DispatchId, Is.EqualTo("without-behavior"));
|
||||
Assert.That(handlerSnapshots[0].Context, Is.SameAs(zeroBehaviorContext));
|
||||
Assert.That(handlerSnapshots[1].DispatchId, Is.EqualTo("with-behavior"));
|
||||
Assert.That(handlerSnapshots[1].Context, Is.SameAs(behaviorContext));
|
||||
Assert.That(handlerSnapshots[1].Context, Is.Not.SameAs(handlerSnapshots[0].Context));
|
||||
Assert.That(handlerSnapshots[1].InstanceId, Is.Not.EqualTo(handlerSnapshots[0].InstanceId));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证缓存的 notification dispatch binding 在重复分发时仍会重新解析 handler,
|
||||
/// 并为当次实例重新注入当前架构上下文。
|
||||
@ -392,6 +751,162 @@ internal sealed class CqrsDispatcherCacheTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证缓存的 stream pipeline executor 在重复建流时仍会重新解析 behavior/handler,
|
||||
/// 并为当次实例重新注入当前架构上下文。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Stream_Pipeline_Executor()
|
||||
{
|
||||
DispatcherStreamContextRefreshState.Reset();
|
||||
|
||||
var streamBindings = GetCacheField("StreamDispatchBindings");
|
||||
var firstContext = new ArchitectureContext(_container!);
|
||||
var secondContext = new ArchitectureContext(_container!);
|
||||
|
||||
await DrainAsync(firstContext.CreateStream(new DispatcherStreamContextRefreshRequest("first")));
|
||||
|
||||
var executorAfterFirstDispatch = GetStreamPipelineExecutorValue(
|
||||
streamBindings,
|
||||
typeof(DispatcherStreamContextRefreshRequest),
|
||||
typeof(int),
|
||||
1);
|
||||
|
||||
await DrainAsync(secondContext.CreateStream(new DispatcherStreamContextRefreshRequest("second")));
|
||||
|
||||
var executorAfterSecondDispatch = GetStreamPipelineExecutorValue(
|
||||
streamBindings,
|
||||
typeof(DispatcherStreamContextRefreshRequest),
|
||||
typeof(int),
|
||||
1);
|
||||
var behaviorSnapshots = DispatcherStreamContextRefreshState.BehaviorSnapshots.ToArray();
|
||||
var handlerSnapshots = DispatcherStreamContextRefreshState.HandlerSnapshots.ToArray();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(executorAfterFirstDispatch, Is.Not.Null);
|
||||
Assert.That(executorAfterSecondDispatch, Is.SameAs(executorAfterFirstDispatch));
|
||||
|
||||
Assert.That(behaviorSnapshots, Has.Length.EqualTo(2));
|
||||
Assert.That(handlerSnapshots, Has.Length.EqualTo(2));
|
||||
|
||||
Assert.That(behaviorSnapshots[0].DispatchId, Is.EqualTo("first"));
|
||||
Assert.That(behaviorSnapshots[0].Context, Is.SameAs(firstContext));
|
||||
Assert.That(behaviorSnapshots[1].DispatchId, Is.EqualTo("second"));
|
||||
Assert.That(behaviorSnapshots[1].Context, Is.SameAs(secondContext));
|
||||
Assert.That(behaviorSnapshots[1].Context, Is.Not.SameAs(behaviorSnapshots[0].Context));
|
||||
|
||||
Assert.That(handlerSnapshots[0].DispatchId, Is.EqualTo("first"));
|
||||
Assert.That(handlerSnapshots[0].Context, Is.SameAs(firstContext));
|
||||
Assert.That(handlerSnapshots[1].DispatchId, Is.EqualTo("second"));
|
||||
Assert.That(handlerSnapshots[1].Context, Is.SameAs(secondContext));
|
||||
Assert.That(handlerSnapshots[1].Context, Is.Not.SameAs(handlerSnapshots[0].Context));
|
||||
Assert.That(handlerSnapshots[1].InstanceId, Is.Not.EqualTo(handlerSnapshots[0].InstanceId));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证同一 stream dispatch binding 先在零行为 dispatcher 上命中后,
|
||||
/// 后续切换到存在行为的 dispatcher 仍会重新解析 behavior/handler,并为当前上下文重新注入架构实例。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Dispatcher_Should_Reinject_Current_Stream_Context_When_Shared_Binding_Switches_From_Zero_Pipeline()
|
||||
{
|
||||
DispatcherStreamContextRefreshState.Reset();
|
||||
|
||||
var streamBindings = GetCacheField("StreamDispatchBindings");
|
||||
using var zeroBehaviorContainer = CreateFrozenContainer(
|
||||
new DispatcherCacheFixtureOptions
|
||||
{
|
||||
IncludeStreamPipelineContextRefreshBehavior = false
|
||||
});
|
||||
var zeroBehaviorContext = new ArchitectureContext(zeroBehaviorContainer);
|
||||
var behaviorContext = new ArchitectureContext(_container!);
|
||||
|
||||
await DrainAsync(zeroBehaviorContext.CreateStream(new DispatcherStreamContextRefreshRequest("without-behavior")));
|
||||
|
||||
var bindingAfterZeroBehaviorDispatch = GetPairCacheValue(
|
||||
streamBindings,
|
||||
typeof(DispatcherStreamContextRefreshRequest),
|
||||
typeof(int));
|
||||
var executorAfterZeroBehaviorDispatch = GetStreamPipelineExecutorValue(
|
||||
streamBindings,
|
||||
typeof(DispatcherStreamContextRefreshRequest),
|
||||
typeof(int),
|
||||
1);
|
||||
|
||||
await DrainAsync(behaviorContext.CreateStream(new DispatcherStreamContextRefreshRequest("with-behavior")));
|
||||
|
||||
var bindingAfterBehaviorDispatch = GetPairCacheValue(
|
||||
streamBindings,
|
||||
typeof(DispatcherStreamContextRefreshRequest),
|
||||
typeof(int));
|
||||
var executorAfterBehaviorDispatch = GetStreamPipelineExecutorValue(
|
||||
streamBindings,
|
||||
typeof(DispatcherStreamContextRefreshRequest),
|
||||
typeof(int),
|
||||
1);
|
||||
var behaviorSnapshots = DispatcherStreamContextRefreshState.BehaviorSnapshots.ToArray();
|
||||
var handlerSnapshots = DispatcherStreamContextRefreshState.HandlerSnapshots.ToArray();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(bindingAfterZeroBehaviorDispatch, Is.Not.Null);
|
||||
Assert.That(bindingAfterBehaviorDispatch, Is.SameAs(bindingAfterZeroBehaviorDispatch));
|
||||
Assert.That(executorAfterZeroBehaviorDispatch, Is.Null);
|
||||
Assert.That(executorAfterBehaviorDispatch, Is.Not.Null);
|
||||
|
||||
Assert.That(behaviorSnapshots, Has.Length.EqualTo(1));
|
||||
Assert.That(behaviorSnapshots[0].DispatchId, Is.EqualTo("with-behavior"));
|
||||
Assert.That(behaviorSnapshots[0].Context, Is.SameAs(behaviorContext));
|
||||
|
||||
Assert.That(handlerSnapshots, Has.Length.EqualTo(2));
|
||||
Assert.That(handlerSnapshots[0].DispatchId, Is.EqualTo("without-behavior"));
|
||||
Assert.That(handlerSnapshots[0].Context, Is.SameAs(zeroBehaviorContext));
|
||||
Assert.That(handlerSnapshots[1].DispatchId, Is.EqualTo("with-behavior"));
|
||||
Assert.That(handlerSnapshots[1].Context, Is.SameAs(behaviorContext));
|
||||
Assert.That(handlerSnapshots[1].Context, Is.Not.SameAs(handlerSnapshots[0].Context));
|
||||
Assert.That(handlerSnapshots[1].InstanceId, Is.Not.EqualTo(handlerSnapshots[0].InstanceId));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 描述缓存测试 fixture 需要启用的可选 pipeline 行为集合,
|
||||
/// 用于构造“同一静态 binding 对应不同 dispatcher 注册可见性”的组合场景。
|
||||
/// </summary>
|
||||
private sealed class DispatcherCacheFixtureOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取是否注册 <see cref="DispatcherPipelineCacheBehavior" />。
|
||||
/// </summary>
|
||||
public bool IncludeRequestPipelineCacheBehavior { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否注册 <see cref="DispatcherPipelineContextRefreshBehavior" />。
|
||||
/// </summary>
|
||||
public bool IncludeRequestPipelineContextRefreshBehavior { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否注册 request 顺序验证所需的两层 pipeline 行为。
|
||||
/// </summary>
|
||||
public bool IncludeRequestPipelineOrderBehaviors { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否注册 <see cref="DispatcherStreamPipelineCacheBehavior" />。
|
||||
/// </summary>
|
||||
public bool IncludeStreamPipelineCacheBehavior { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否注册 <see cref="DispatcherStreamPipelineContextRefreshBehavior" />。
|
||||
/// </summary>
|
||||
public bool IncludeStreamPipelineContextRefreshBehavior { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否注册 stream 顺序验证所需的两层 pipeline 行为。
|
||||
/// </summary>
|
||||
public bool IncludeStreamPipelineOrderBehaviors { get; init; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过反射读取 dispatcher 的静态缓存对象。
|
||||
/// </summary>
|
||||
@ -409,6 +924,95 @@ internal sealed class CqrsDispatcherCacheTests
|
||||
$"Dispatcher cache field {fieldName} returned null.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从架构上下文中解析当前延迟创建的 dispatcher 实例,便于验证其实例级热路径缓存。
|
||||
/// </summary>
|
||||
private static object GetDispatcherFromContext(ArchitectureContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
var lazyRuntimeField = typeof(ArchitectureContext).GetField(
|
||||
"_cqrsRuntime",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
|
||||
Assert.That(lazyRuntimeField, Is.Not.Null, "Missing ArchitectureContext._cqrsRuntime field.");
|
||||
|
||||
var lazyRuntime = lazyRuntimeField!.GetValue(context)
|
||||
?? throw new InvalidOperationException(
|
||||
"ArchitectureContext._cqrsRuntime returned null.");
|
||||
var lazyValueProperty = lazyRuntime.GetType().GetProperty(
|
||||
"Value",
|
||||
BindingFlags.Instance | BindingFlags.Public);
|
||||
|
||||
Assert.That(lazyValueProperty, Is.Not.Null, "Missing Lazy<ICqrsRuntime>.Value accessor.");
|
||||
|
||||
return lazyValueProperty!.GetValue(lazyRuntime)
|
||||
?? throw new InvalidOperationException("Resolved CQRS runtime instance was null.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建与当前 fixture 注册形状一致、但拥有独立 runtime 实例的冻结容器,
|
||||
/// 用于验证 dispatcher 的实例级缓存不会跨容器共享。
|
||||
/// </summary>
|
||||
/// <param name="options">控制当前隔离容器要启用哪些可选 pipeline 行为的配置。</param>
|
||||
private static MicrosoftDiContainer CreateFrozenContainer(DispatcherCacheFixtureOptions? options = null)
|
||||
{
|
||||
var container = new MicrosoftDiContainer();
|
||||
ConfigureDispatcherCacheFixture(container, options);
|
||||
|
||||
container.Freeze();
|
||||
return container;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 组装当前 fixture 依赖的 CQRS 容器注册形状,确保默认上下文与隔离容器复用同一份装配基线。
|
||||
/// </summary>
|
||||
/// <param name="container">待补齐 CQRS 注册的目标容器。</param>
|
||||
/// <param name="options">控制是否跳过特定 pipeline 行为注册的可选配置。</param>
|
||||
private static void ConfigureDispatcherCacheFixture(
|
||||
MicrosoftDiContainer container,
|
||||
DispatcherCacheFixtureOptions? options = null)
|
||||
{
|
||||
options ??= new DispatcherCacheFixtureOptions();
|
||||
|
||||
if (options.IncludeRequestPipelineCacheBehavior)
|
||||
{
|
||||
container.RegisterCqrsPipelineBehavior<DispatcherPipelineCacheBehavior>();
|
||||
}
|
||||
|
||||
if (options.IncludeRequestPipelineContextRefreshBehavior)
|
||||
{
|
||||
container.RegisterCqrsPipelineBehavior<DispatcherPipelineContextRefreshBehavior>();
|
||||
}
|
||||
|
||||
if (options.IncludeRequestPipelineOrderBehaviors)
|
||||
{
|
||||
container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderOuterBehavior>();
|
||||
container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderInnerBehavior>();
|
||||
}
|
||||
|
||||
if (options.IncludeStreamPipelineCacheBehavior)
|
||||
{
|
||||
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineCacheBehavior>();
|
||||
}
|
||||
|
||||
if (options.IncludeStreamPipelineContextRefreshBehavior)
|
||||
{
|
||||
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineContextRefreshBehavior>();
|
||||
}
|
||||
|
||||
if (options.IncludeStreamPipelineOrderBehaviors)
|
||||
{
|
||||
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineOrderOuterBehavior>();
|
||||
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineOrderInnerBehavior>();
|
||||
}
|
||||
|
||||
CqrsTestRuntime.RegisterHandlers(
|
||||
container,
|
||||
typeof(CqrsDispatcherCacheTests).Assembly,
|
||||
typeof(ArchitectureContext).Assembly);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空本测试依赖的 dispatcher 静态缓存,避免跨用例共享进程级状态导致断言漂移。
|
||||
/// </summary>
|
||||
@ -435,6 +1039,140 @@ internal sealed class CqrsDispatcherCacheTests
|
||||
return InvokeInstanceMethod(cache, "GetValueOrDefaultForTesting", primaryType, secondaryType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定 dispatcher 实例中当前保存的 request behavior presence 缓存项。
|
||||
/// </summary>
|
||||
private static object? GetRequestBehaviorPresenceCacheValue(object dispatcher, Type behaviorType)
|
||||
{
|
||||
var field = dispatcher.GetType().GetField(
|
||||
"_requestBehaviorPresenceCache",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
|
||||
Assert.That(field, Is.Not.Null, "Missing dispatcher request behavior presence cache field.");
|
||||
|
||||
var cache = field!.GetValue(dispatcher)
|
||||
?? throw new InvalidOperationException(
|
||||
"Dispatcher request behavior presence cache returned null.");
|
||||
var tryGetValueMethod = cache.GetType().GetMethod(
|
||||
"TryGetValue",
|
||||
BindingFlags.Instance | BindingFlags.Public);
|
||||
|
||||
Assert.That(tryGetValueMethod, Is.Not.Null, "Missing ConcurrentDictionary.TryGetValue accessor.");
|
||||
|
||||
object?[] arguments = [behaviorType, null];
|
||||
var found = (bool)(tryGetValueMethod!.Invoke(cache, arguments)
|
||||
?? throw new InvalidOperationException(
|
||||
"ConcurrentDictionary.TryGetValue returned null."));
|
||||
return found ? arguments[1] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定 dispatcher 实例中当前保存的 stream behavior presence 缓存项。
|
||||
/// </summary>
|
||||
private static object? GetStreamBehaviorPresenceCacheValue(object dispatcher, Type behaviorType)
|
||||
{
|
||||
var field = dispatcher.GetType().GetField(
|
||||
"_streamBehaviorPresenceCache",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
|
||||
Assert.That(field, Is.Not.Null, "Missing dispatcher stream behavior presence cache field.");
|
||||
|
||||
var cache = field!.GetValue(dispatcher)
|
||||
?? throw new InvalidOperationException(
|
||||
"Dispatcher stream behavior presence cache returned null.");
|
||||
var tryGetValueMethod = cache.GetType().GetMethod(
|
||||
"TryGetValue",
|
||||
BindingFlags.Instance | BindingFlags.Public);
|
||||
|
||||
Assert.That(tryGetValueMethod, Is.Not.Null, "Missing ConcurrentDictionary.TryGetValue accessor.");
|
||||
|
||||
object?[] arguments = [behaviorType, null];
|
||||
var found = (bool)(tryGetValueMethod!.Invoke(cache, arguments)
|
||||
?? throw new InvalidOperationException(
|
||||
"ConcurrentDictionary.TryGetValue returned null."));
|
||||
return found ? arguments[1] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断言指定 dispatcher 上某个 request behavior presence 缓存项尚未建立。
|
||||
/// </summary>
|
||||
private static void AssertRequestBehaviorPresenceIsUnset(object dispatcher, Type behaviorType)
|
||||
{
|
||||
Assert.That(GetRequestBehaviorPresenceCacheValue(dispatcher, behaviorType), Is.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断言指定 dispatcher 上某个 request behavior presence 缓存项等于预期值。
|
||||
/// </summary>
|
||||
private static void AssertRequestBehaviorPresenceEquals(object dispatcher, Type behaviorType, bool expected)
|
||||
{
|
||||
Assert.That(GetRequestBehaviorPresenceCacheValue(dispatcher, behaviorType), Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断言指定 dispatcher 上某个 stream behavior presence 缓存项尚未建立。
|
||||
/// </summary>
|
||||
private static void AssertStreamBehaviorPresenceIsUnset(object dispatcher, Type behaviorType)
|
||||
{
|
||||
Assert.That(GetStreamBehaviorPresenceCacheValue(dispatcher, behaviorType), Is.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断言指定 dispatcher 上某个 stream behavior presence 缓存项等于预期值。
|
||||
/// </summary>
|
||||
private static void AssertStreamBehaviorPresenceEquals(object dispatcher, Type behaviorType, bool expected)
|
||||
{
|
||||
Assert.That(GetStreamBehaviorPresenceCacheValue(dispatcher, behaviorType), Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断言同一容器解析出的 dispatcher 会共享实例级缓存,而另一独立容器的 dispatcher 不会提前命中。
|
||||
/// </summary>
|
||||
private static void AssertSharedDispatcherCacheState(
|
||||
object firstDispatcher,
|
||||
object secondDispatcher,
|
||||
object isolatedDispatcher,
|
||||
object? zeroPipelinePresence,
|
||||
object? onePipelinePresence)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(secondDispatcher, Is.SameAs(firstDispatcher));
|
||||
Assert.That(zeroPipelinePresence, Is.EqualTo(false));
|
||||
Assert.That(onePipelinePresence, Is.EqualTo(true));
|
||||
AssertRequestBehaviorPresenceEquals(
|
||||
secondDispatcher,
|
||||
typeof(IPipelineBehavior<DispatcherCacheRequest, int>),
|
||||
false);
|
||||
AssertRequestBehaviorPresenceIsUnset(
|
||||
isolatedDispatcher,
|
||||
typeof(IPipelineBehavior<DispatcherCacheRequest, int>));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断言同一容器解析出的 dispatcher 会共享 stream 的实例级缓存,而另一独立容器的 dispatcher 不会提前命中。
|
||||
/// </summary>
|
||||
private static void AssertSharedStreamDispatcherCacheState(
|
||||
object firstDispatcher,
|
||||
object secondDispatcher,
|
||||
object isolatedDispatcher,
|
||||
object? zeroPipelinePresence,
|
||||
object? twoPipelinePresence,
|
||||
Type zeroPipelineBehaviorType,
|
||||
bool expectedZeroPipelinePresence,
|
||||
bool expectedTwoPipelinePresence)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(secondDispatcher, Is.SameAs(firstDispatcher));
|
||||
Assert.That(zeroPipelinePresence, Is.EqualTo(expectedZeroPipelinePresence));
|
||||
Assert.That(twoPipelinePresence, Is.EqualTo(expectedTwoPipelinePresence));
|
||||
AssertStreamBehaviorPresenceEquals(secondDispatcher, zeroPipelineBehaviorType, expectedZeroPipelinePresence);
|
||||
AssertStreamBehaviorPresenceIsUnset(isolatedDispatcher, zeroPipelineBehaviorType);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 request dispatch binding 中指定行为数量的 pipeline executor 缓存项。
|
||||
/// </summary>
|
||||
@ -455,6 +1193,26 @@ internal sealed class CqrsDispatcherCacheTests
|
||||
: InvokeInstanceMethod(binding, "GetPipelineExecutorForTesting", behaviorCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 stream dispatch binding 中指定行为数量的 pipeline executor 缓存项。
|
||||
/// </summary>
|
||||
/// <param name="streamBindings">dispatcher 内部的 stream binding 缓存对象。</param>
|
||||
/// <param name="requestType">要读取的流式请求运行时类型。</param>
|
||||
/// <param name="responseType">要读取的响应元素类型。</param>
|
||||
/// <param name="behaviorCount">目标 executor 对应的行为数量。</param>
|
||||
/// <returns>已缓存的 executor;若 binding 或 executor 尚未建立则返回 <see langword="null" />。</returns>
|
||||
private static object? GetStreamPipelineExecutorValue(
|
||||
object streamBindings,
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
int behaviorCount)
|
||||
{
|
||||
var binding = GetStreamDispatchBindingValue(streamBindings, requestType, responseType);
|
||||
return binding is null
|
||||
? null
|
||||
: InvokeInstanceMethod(binding, "GetPipelineExecutorForTesting", behaviorCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调用缓存实例上的无参清理方法。
|
||||
/// </summary>
|
||||
@ -503,6 +1261,32 @@ internal sealed class CqrsDispatcherCacheTests
|
||||
.Invoke(bindingBox, Array.Empty<object>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定流式请求/响应类型对对应的强类型 stream dispatch binding。
|
||||
/// </summary>
|
||||
/// <param name="streamBindings">dispatcher 内部的 stream binding 缓存对象。</param>
|
||||
/// <param name="requestType">要读取的流式请求运行时类型。</param>
|
||||
/// <param name="responseType">要读取的响应元素类型。</param>
|
||||
/// <returns>强类型 binding;若缓存尚未建立则返回 <see langword="null" />。</returns>
|
||||
private static object? GetStreamDispatchBindingValue(object streamBindings, Type requestType, Type responseType)
|
||||
{
|
||||
var bindingBox = GetPairCacheValue(streamBindings, requestType, responseType);
|
||||
if (bindingBox is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var method = bindingBox.GetType().GetMethod(
|
||||
"Get",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
|
||||
Assert.That(method, Is.Not.Null, $"Missing stream binding accessor on {bindingBox.GetType().FullName}.");
|
||||
|
||||
return method!
|
||||
.MakeGenericMethod(responseType)
|
||||
.Invoke(bindingBox, Array.Empty<object>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 CQRS dispatcher 运行时类型。
|
||||
/// </summary>
|
||||
|
||||
@ -7,6 +7,7 @@ using GFramework.Core.Abstractions.Ioc;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using GFramework.Cqrs.Cqrs;
|
||||
using GFramework.Cqrs.Notification;
|
||||
using GFramework.Cqrs.Tests.Logging;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
@ -30,6 +31,9 @@ internal sealed class CqrsDispatcherContextValidationTests
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.Get(typeof(IRequestHandler<ContextAwareRequest, int>)))
|
||||
.Returns(new ContextAwareRequestHandler());
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.HasRegistration(typeof(IPipelineBehavior<ContextAwareRequest, int>)))
|
||||
.Returns(false);
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.GetAll(typeof(IPipelineBehavior<ContextAwareRequest, int>)))
|
||||
.Returns(Array.Empty<object>());
|
||||
@ -40,6 +44,63 @@ internal sealed class CqrsDispatcherContextValidationTests
|
||||
Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 request 上下文校验失败时,<see cref="GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime.SendAsync{TResponse}" />
|
||||
/// 不会在调用点同步抛出,而是返回一个 faulted <see cref="ValueTask{TResult}" /> 保持既有异步失败语义。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void SendAsync_Should_Return_Faulted_ValueTask_When_Context_Preparation_Fails()
|
||||
{
|
||||
var runtime = CreateRuntime(
|
||||
container =>
|
||||
{
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.Get(typeof(IRequestHandler<ContextAwareRequest, int>)))
|
||||
.Returns(new ContextAwareRequestHandler());
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.HasRegistration(typeof(IPipelineBehavior<ContextAwareRequest, int>)))
|
||||
.Returns(false);
|
||||
});
|
||||
|
||||
ValueTask<int> dispatch = default;
|
||||
Assert.That(
|
||||
() => { dispatch = runtime.SendAsync(new FakeCqrsContext(), new ContextAwareRequest()); },
|
||||
Throws.Nothing);
|
||||
Assert.That(
|
||||
async () => await dispatch.ConfigureAwait(false),
|
||||
Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 request handler 缺失时,dispatcher 仍返回 faulted <see cref="ValueTask{TResult}" />,
|
||||
/// 而不是在调用点同步抛出异常。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void SendAsync_Should_Return_Faulted_ValueTask_When_Handler_Is_Missing()
|
||||
{
|
||||
var runtime = CreateRuntime(
|
||||
container =>
|
||||
{
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.Get(typeof(IRequestHandler<ContextAwareRequest, int>)))
|
||||
.Returns((object?)null);
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.HasRegistration(typeof(IPipelineBehavior<ContextAwareRequest, int>)))
|
||||
.Returns(false);
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.GetAll(typeof(IPipelineBehavior<ContextAwareRequest, int>)))
|
||||
.Returns(Array.Empty<object>());
|
||||
});
|
||||
|
||||
ValueTask<int> dispatch = default;
|
||||
Assert.That(
|
||||
() => { dispatch = runtime.SendAsync(new FakeCqrsContext(), new ContextAwareRequest()); },
|
||||
Throws.Nothing);
|
||||
Assert.That(
|
||||
async () => await dispatch.ConfigureAwait(false),
|
||||
Throws.InvalidOperationException.With.Message.Contains("No CQRS request handler registered"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 notification handler 需要上下文注入、但当前 CQRS 上下文不实现 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContext" /> 时,
|
||||
/// dispatcher 会在发布前显式失败。
|
||||
@ -73,6 +134,39 @@ internal sealed class CqrsDispatcherContextValidationTests
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.Get(typeof(IStreamRequestHandler<ContextAwareStreamRequest, int>)))
|
||||
.Returns(new ContextAwareStreamHandler());
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.HasRegistration(typeof(IStreamPipelineBehavior<ContextAwareStreamRequest, int>)))
|
||||
.Returns(false);
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.GetAll(typeof(IStreamPipelineBehavior<ContextAwareStreamRequest, int>)))
|
||||
.Returns(Array.Empty<object>());
|
||||
});
|
||||
|
||||
Assert.That(
|
||||
() => runtime.CreateStream(new FakeCqrsContext(), new ContextAwareStreamRequest()),
|
||||
Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 stream pipeline behavior 需要上下文注入、但当前 CQRS 上下文不实现
|
||||
/// <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContext" /> 时,
|
||||
/// dispatcher 会在建流前显式失败。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void CreateStream_Should_Throw_When_Stream_Pipeline_Behavior_Context_Does_Not_Implement_IArchitectureContext()
|
||||
{
|
||||
var runtime = CreateRuntime(
|
||||
container =>
|
||||
{
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.Get(typeof(IStreamRequestHandler<ContextAwareStreamRequest, int>)))
|
||||
.Returns(new PassthroughStreamHandler());
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.HasRegistration(typeof(IStreamPipelineBehavior<ContextAwareStreamRequest, int>)))
|
||||
.Returns(true);
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.GetAll(typeof(IStreamPipelineBehavior<ContextAwareStreamRequest, int>)))
|
||||
.Returns([new ContextAwareStreamBehavior()]);
|
||||
});
|
||||
|
||||
Assert.That(
|
||||
@ -91,6 +185,11 @@ internal sealed class CqrsDispatcherContextValidationTests
|
||||
var container = new Mock<IIocContainer>(MockBehavior.Strict);
|
||||
var logger = new TestLogger("CqrsDispatcherContextValidationTests", LogLevel.Debug);
|
||||
|
||||
// PublishAsync 的默认路径会在真正发布时查询通知发布器注册;strict mock 需显式覆盖空注册分支。
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationPublisher)))
|
||||
.Returns(Array.Empty<object>());
|
||||
|
||||
configureContainer(container);
|
||||
return CqrsRuntimeFactory.CreateRuntime(container.Object, logger);
|
||||
}
|
||||
@ -174,4 +273,47 @@ internal sealed class CqrsDispatcherContextValidationTests
|
||||
await ValueTask.CompletedTask.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 stream behavior 上下文校验提供不依赖上下文注入的最小 handler。
|
||||
/// </summary>
|
||||
private sealed class PassthroughStreamHandler : IStreamRequestHandler<ContextAwareStreamRequest, int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回一个最小流;当前测试只关心 behavior 注入前的上下文校验。
|
||||
/// </summary>
|
||||
/// <param name="request">当前流请求。</param>
|
||||
/// <param name="cancellationToken">取消枚举时使用的取消令牌。</param>
|
||||
/// <returns>包含单个固定元素的异步流。</returns>
|
||||
public async IAsyncEnumerable<int> Handle(
|
||||
ContextAwareStreamRequest request,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
yield return 1;
|
||||
await ValueTask.CompletedTask.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 stream behavior 上下文校验提供需要注入架构上下文的最小 behavior。
|
||||
/// </summary>
|
||||
private sealed class ContextAwareStreamBehavior
|
||||
: CqrsContextAwareHandlerBase,
|
||||
IStreamPipelineBehavior<ContextAwareStreamRequest, int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 直接转发到下一个处理阶段;当前测试只关心调用前的上下文校验。
|
||||
/// </summary>
|
||||
/// <param name="message">当前流式请求。</param>
|
||||
/// <param name="next">下一个处理阶段。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>下游处理阶段返回的异步流。</returns>
|
||||
public IAsyncEnumerable<int> Handle(
|
||||
ContextAwareStreamRequest message,
|
||||
StreamMessageHandlerDelegate<ContextAwareStreamRequest, int> next,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return next(message, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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