mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-12 22:03:30 +08:00
Compare commits
84 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 |
@ -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.
|
||||
|
||||
@ -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 处理器。
|
||||
/// 该入口适用于处理器不位于默认架构程序集中的场景,例如扩展包、模块程序集或拆分后的业务程序集。
|
||||
@ -238,6 +252,21 @@ public interface IIocContainer : IContextAware, IDisposable
|
||||
/// </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>
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
// 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;
|
||||
@ -27,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>
|
||||
@ -36,8 +41,10 @@ public class ArchitectureModulesBehaviorTests
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
AdditionalAssemblyNotificationHandlerState.Reset();
|
||||
GameContext.Clear();
|
||||
TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount = 0;
|
||||
TrackingStreamPipelineBehavior<ModuleStreamBehaviorRequest, int>.InvocationCount = 0;
|
||||
LegacyBridgePipelineTracker.Reset();
|
||||
}
|
||||
|
||||
@ -92,6 +99,34 @@ public class ArchitectureModulesBehaviorTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <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。
|
||||
@ -126,6 +161,35 @@ public class ArchitectureModulesBehaviorTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 用于测试模块行为的最小架构实现。
|
||||
/// </summary>
|
||||
@ -161,6 +225,31 @@ public class ArchitectureModulesBehaviorTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@ -194,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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -419,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>
|
||||
@ -902,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 接入当前架构。
|
||||
|
||||
@ -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 处理器。
|
||||
/// 该入口用于把默认架构程序集之外的扩展处理器接入当前架构容器。
|
||||
|
||||
@ -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>
|
||||
@ -662,9 +712,12 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
}
|
||||
|
||||
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
|
||||
@ -748,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
|
||||
@ -777,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
|
||||
@ -979,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>
|
||||
/// 判断容器中是否包含某个具体的实例对象
|
||||
/// 通过已注册实例集合进行快速查找
|
||||
@ -999,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>
|
||||
/// 清空容器中的所有实例和服务注册
|
||||
/// 只有在容器未冻结状态下才能执行清空操作
|
||||
@ -1020,6 +1150,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
GetServicesUnsafe.Clear();
|
||||
_registeredInstances.Clear();
|
||||
_provider = null;
|
||||
_frozenServiceTypeIndex = null;
|
||||
_frozen = false;
|
||||
_logger.Info("Container cleared");
|
||||
}
|
||||
@ -1047,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");
|
||||
}
|
||||
@ -1056,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的访问权限,用于高级配置和自定义操作
|
||||
@ -1131,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,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);
|
||||
|
||||
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,6 +18,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
|
||||
<PackageReference Include="Mediator.Abstractions" Version="3.0.2" />
|
||||
<PackageReference Include="Mediator.SourceGenerator" Version="3.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MediatR" Version="13.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.7" />
|
||||
|
||||
@ -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()
|
||||
{
|
||||
|
||||
@ -23,24 +23,31 @@ namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
/// 对比 request steady-state dispatch 在不同 handler 生命周期下的额外开销。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 当前矩阵只覆盖 `Singleton` 与 `Transient`。
|
||||
/// `Scoped` 在两个 runtime 中都依赖显式作用域边界,而当前 benchmark 宿主故意保持“单根容器最小宿主”模型,
|
||||
/// 直接把 scoped 解析压到根作用域会让对照语义失真,因此留到未来有真实 scoped host 基线时再扩展。
|
||||
/// 当前矩阵覆盖 `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 = null!;
|
||||
private ICqrsRuntime? _runtime;
|
||||
private ScopedBenchmarkContainer? _scopedContainer;
|
||||
private ICqrsRuntime? _scopedRuntime;
|
||||
private ServiceProvider _serviceProvider = null!;
|
||||
private IMediator _mediatr = null!;
|
||||
private IMediator? _mediatr;
|
||||
private BenchmarkRequestHandler _baselineHandler = null!;
|
||||
private BenchmarkRequest _request = null!;
|
||||
private ILogger _runtimeLogger = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 控制当前 benchmark 使用的 handler 生命周期。
|
||||
/// </summary>
|
||||
[Params(HandlerLifetime.Singleton, HandlerLifetime.Transient)]
|
||||
[Params(HandlerLifetime.Singleton, HandlerLifetime.Scoped, HandlerLifetime.Transient)]
|
||||
public HandlerLifetime Lifetime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@ -53,6 +60,11 @@ public class RequestLifetimeBenchmarks
|
||||
/// </summary>
|
||||
Singleton,
|
||||
|
||||
/// <summary>
|
||||
/// 每次 request 在显式作用域内解析并复用 handler 实例。
|
||||
/// </summary>
|
||||
Scoped,
|
||||
|
||||
/// <summary>
|
||||
/// 每次分发都重新解析新的 handler 实例。
|
||||
/// </summary>
|
||||
@ -85,24 +97,43 @@ public class RequestLifetimeBenchmarks
|
||||
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 = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_container,
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestLifetimeBenchmarks) + "." + 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));
|
||||
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
|
||||
if (Lifetime != HandlerLifetime.Scoped)
|
||||
{
|
||||
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -111,12 +142,20 @@ public class RequestLifetimeBenchmarks
|
||||
[GlobalCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
|
||||
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()
|
||||
{
|
||||
@ -126,26 +165,50 @@ public class RequestLifetimeBenchmarks
|
||||
/// <summary>
|
||||
/// 通过 GFramework.CQRS runtime 发送 request。
|
||||
/// </summary>
|
||||
/// <returns>代表当前 GFramework.CQRS request dispatch 完成的值任务。</returns>
|
||||
[Benchmark]
|
||||
public ValueTask<BenchmarkResponse> SendRequest_GFrameworkCqrs()
|
||||
{
|
||||
return _runtime.SendAsync(BenchmarkContext.Instance, _request, CancellationToken.None);
|
||||
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()
|
||||
{
|
||||
return _mediatr.Send(_request, CancellationToken.None);
|
||||
if (Lifetime == HandlerLifetime.Scoped)
|
||||
{
|
||||
return BenchmarkHostFactory.SendScopedMediatRRequestAsync(
|
||||
_serviceProvider,
|
||||
_request,
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
return _mediatr!.Send(_request, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按生命周期把 benchmark request handler 注册到 GFramework 容器。
|
||||
/// 按生命周期把 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);
|
||||
@ -156,6 +219,10 @@ public class RequestLifetimeBenchmarks
|
||||
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;
|
||||
@ -174,13 +241,14 @@ public class RequestLifetimeBenchmarks
|
||||
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。
|
||||
/// Benchmark request。
|
||||
/// </summary>
|
||||
/// <param name="Id">请求标识。</param>
|
||||
public sealed record BenchmarkRequest(Guid 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,47 +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/RequestLifetimeBenchmarks.cs`
|
||||
- `Singleton / Transient` 两类 handler 生命周期下,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_*"
|
||||
```
|
||||
|
||||
- request / stream 的真实 source-generator 产物与 handwritten generated provider 对照
|
||||
- stream handler 生命周期矩阵
|
||||
- 带真实显式作用域边界的 scoped host 对照
|
||||
- generated invoker provider 与纯反射 dispatch / 建流对比继续扩展到更多场景
|
||||
## 并发运行约束
|
||||
|
||||
当两个 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ using GFramework.Core.Architectures;
|
||||
using GFramework.Core.Ioc;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using GFramework.Cqrs.Internal;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
@ -27,6 +28,7 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
||||
{
|
||||
_previousLoggerFactoryProvider = LoggerFactoryResolver.Provider;
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
|
||||
GeneratedStreamPipelineTrackingBehavior.InvocationCount = 0;
|
||||
ClearRegistrarCaches();
|
||||
ClearDispatcherCaches();
|
||||
}
|
||||
@ -38,6 +40,7 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
||||
public void TearDown()
|
||||
{
|
||||
LoggerFactoryResolver.Provider = _previousLoggerFactoryProvider ?? new ConsoleLoggerFactoryProvider();
|
||||
GeneratedStreamPipelineTrackingBehavior.InvocationCount = 0;
|
||||
ClearRegistrarCaches();
|
||||
ClearDispatcherCaches();
|
||||
}
|
||||
@ -97,6 +100,32 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
||||
Is.EqualTo([typeof(GeneratedStreamInvokerProviderRegistry)]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 direct generated-registry 激活入口只会接入指定 registry,而不会顺手把同一测试程序集里的其他 registry 一并注册。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterGeneratedRegistry_Should_Register_Only_The_Selected_Provider()
|
||||
{
|
||||
var container = new MicrosoftDiContainer();
|
||||
var logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsGeneratedRequestInvokerProviderTests));
|
||||
|
||||
CqrsHandlerRegistrar.RegisterGeneratedRegistry(
|
||||
container,
|
||||
typeof(GeneratedRequestInvokerProviderRegistry),
|
||||
logger);
|
||||
|
||||
var requestProviders = container.GetAll<ICqrsRequestInvokerProvider>();
|
||||
var streamProviders = container.GetAll<ICqrsStreamInvokerProvider>();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(
|
||||
requestProviders.Select(static provider => provider.GetType()),
|
||||
Is.EqualTo([typeof(GeneratedRequestInvokerProviderRegistry)]));
|
||||
Assert.That(streamProviders, Is.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当实现类型隐藏、但 stream handler interface 仍可直接表达时,
|
||||
/// registrar 仍会把 generated stream invoker provider 注册到容器中。
|
||||
@ -169,6 +198,73 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
||||
Assert.That(results, Is.EqualTo([30, 31]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 generated stream invoker 与 stream pipeline 行为同时存在时,
|
||||
/// dispatcher 仍会保持 generated invoker 优先,并正确包裹到行为链内。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task CreateStream_Should_Use_Generated_Stream_Invoker_Inside_Stream_Pipeline()
|
||||
{
|
||||
var generatedAssembly = CreateGeneratedStreamInvokerAssembly();
|
||||
var container = new MicrosoftDiContainer();
|
||||
container.RegisterCqrsStreamPipelineBehavior<GeneratedStreamPipelineTrackingBehavior>();
|
||||
|
||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||
container.Freeze();
|
||||
|
||||
var context = new ArchitectureContext(container);
|
||||
var results = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(results, Is.EqualTo([30, 31]));
|
||||
Assert.That(GeneratedStreamPipelineTrackingBehavior.InvocationCount, Is.EqualTo(1));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 generated stream binding 与对应的 pipeline executor 在首次建流后会被缓存并复用,
|
||||
/// 同时保持 generated invoker 的结果与行为执行语义不变。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task CreateStream_Should_Reuse_Cached_Generated_Stream_Binding_And_Pipeline_Executor()
|
||||
{
|
||||
var generatedAssembly = CreateGeneratedStreamInvokerAssembly();
|
||||
var container = new MicrosoftDiContainer();
|
||||
container.RegisterCqrsStreamPipelineBehavior<GeneratedStreamPipelineTrackingBehavior>();
|
||||
|
||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||
container.Freeze();
|
||||
|
||||
var streamBindings = GetDispatcherCacheField("StreamDispatchBindings");
|
||||
var requestType = typeof(GeneratedStreamInvokerRequest);
|
||||
var responseType = typeof(int);
|
||||
|
||||
Assert.That(
|
||||
GetStreamPipelineExecutorValue(streamBindings, requestType, responseType, 1),
|
||||
Is.Null);
|
||||
|
||||
var context = new ArchitectureContext(container);
|
||||
var firstResults = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false);
|
||||
var bindingAfterFirstDispatch = GetPairCacheValue(streamBindings, requestType, responseType);
|
||||
var executorAfterFirstDispatch = GetStreamPipelineExecutorValue(streamBindings, requestType, responseType, 1);
|
||||
|
||||
var secondResults = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false);
|
||||
var bindingAfterSecondDispatch = GetPairCacheValue(streamBindings, requestType, responseType);
|
||||
var executorAfterSecondDispatch = GetStreamPipelineExecutorValue(streamBindings, requestType, responseType, 1);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(firstResults, Is.EqualTo([30, 31]));
|
||||
Assert.That(secondResults, Is.EqualTo([30, 31]));
|
||||
Assert.That(bindingAfterFirstDispatch, Is.Not.Null);
|
||||
Assert.That(bindingAfterSecondDispatch, Is.SameAs(bindingAfterFirstDispatch));
|
||||
Assert.That(executorAfterFirstDispatch, Is.Not.Null);
|
||||
Assert.That(executorAfterSecondDispatch, Is.SameAs(executorAfterFirstDispatch));
|
||||
Assert.That(GeneratedStreamPipelineTrackingBehavior.InvocationCount, Is.EqualTo(2));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当实现类型隐藏、但 stream handler interface 仍可直接表达时,
|
||||
/// dispatcher 仍会消费 generated stream invoker descriptor。
|
||||
@ -352,6 +448,166 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
||||
Assert.That(results, Is.EqualTo([3, 4]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 generated request invoker provider 的 descriptor 枚举抛出异常时,
|
||||
/// registrar 会跳过 generated descriptor 预热并回退到反射路径。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task SendAsync_Should_Fall_Back_To_Runtime_Path_When_Request_Descriptor_Enumeration_Throws()
|
||||
{
|
||||
var generatedAssembly = CreateGeneratedAssembly(
|
||||
typeof(ThrowingEnumeratingRequestInvokerProviderRegistry),
|
||||
"GFramework.Cqrs.Tests.Cqrs.ThrowingEnumeratingRequestInvokerAssembly, Version=1.0.0.0");
|
||||
var container = new MicrosoftDiContainer();
|
||||
|
||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||
container.Freeze();
|
||||
|
||||
var context = new ArchitectureContext(container);
|
||||
var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false);
|
||||
Assert.That(response, Is.EqualTo("runtime:payload"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 generated stream invoker provider 的 descriptor 枚举抛出异常时,
|
||||
/// registrar 会跳过 generated descriptor 预热并回退到反射建流路径。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task CreateStream_Should_Fall_Back_To_Runtime_Path_When_Stream_Descriptor_Enumeration_Throws()
|
||||
{
|
||||
var generatedAssembly = CreateGeneratedAssembly(
|
||||
typeof(ThrowingEnumeratingStreamInvokerProviderRegistry),
|
||||
"GFramework.Cqrs.Tests.Cqrs.ThrowingEnumeratingStreamInvokerAssembly, Version=1.0.0.0");
|
||||
var container = new MicrosoftDiContainer();
|
||||
|
||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||
container.Freeze();
|
||||
|
||||
var context = new ArchitectureContext(container);
|
||||
var results = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false);
|
||||
Assert.That(results, Is.EqualTo([3, 4]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 request descriptor 枚举返回重复 request-response pair 时,
|
||||
/// registrar 会稳定保留首个有效描述符,并忽略后续重复项。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task SendAsync_Should_Use_First_Generated_Request_Descriptor_When_Duplicates_Are_Enumerated()
|
||||
{
|
||||
var generatedAssembly = CreateGeneratedAssembly(
|
||||
typeof(DuplicateEnumeratingRequestInvokerProviderRegistry),
|
||||
"GFramework.Cqrs.Tests.Cqrs.DuplicateEnumeratingRequestInvokerAssembly, Version=1.0.0.0");
|
||||
var container = new MicrosoftDiContainer();
|
||||
|
||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||
container.Freeze();
|
||||
|
||||
var context = new ArchitectureContext(container);
|
||||
var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false);
|
||||
Assert.That(response, Is.EqualTo("generated:payload"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 stream descriptor 枚举返回重复 request-response pair 时,
|
||||
/// registrar 会稳定保留首个有效描述符,并忽略后续重复项。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task CreateStream_Should_Use_First_Generated_Stream_Descriptor_When_Duplicates_Are_Enumerated()
|
||||
{
|
||||
var generatedAssembly = CreateGeneratedAssembly(
|
||||
typeof(DuplicateEnumeratingStreamInvokerProviderRegistry),
|
||||
"GFramework.Cqrs.Tests.Cqrs.DuplicateEnumeratingStreamInvokerAssembly, Version=1.0.0.0");
|
||||
var container = new MicrosoftDiContainer();
|
||||
|
||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||
container.Freeze();
|
||||
|
||||
var context = new ArchitectureContext(container);
|
||||
var results = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false);
|
||||
Assert.That(results, Is.EqualTo([30, 31]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 request descriptor 枚举项与 provider 的 TryGetDescriptor 结果不一致时,
|
||||
/// registrar 会忽略该坏 descriptor,并继续回退到反射路径。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task SendAsync_Should_Fall_Back_To_Runtime_Path_When_Enumerated_Request_Descriptor_Does_Not_Match_Provider()
|
||||
{
|
||||
var generatedAssembly = CreateGeneratedAssembly(
|
||||
typeof(MismatchedEnumeratingRequestInvokerProviderRegistry),
|
||||
"GFramework.Cqrs.Tests.Cqrs.MismatchedEnumeratingRequestInvokerAssembly, Version=1.0.0.0");
|
||||
var container = new MicrosoftDiContainer();
|
||||
|
||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||
container.Freeze();
|
||||
|
||||
var context = new ArchitectureContext(container);
|
||||
var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false);
|
||||
Assert.That(response, Is.EqualTo("runtime:payload"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当首个 request descriptor 无效、后续同键 descriptor 有效时,
|
||||
/// registrar 不会因为过早去重而丢掉本可注册的 generated descriptor。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task SendAsync_Should_Use_Later_Valid_Generated_Request_Descriptor_When_First_Duplicate_Is_Invalid()
|
||||
{
|
||||
var generatedAssembly = CreateGeneratedAssembly(
|
||||
typeof(InvalidThenValidDuplicateRequestInvokerProviderRegistry),
|
||||
"GFramework.Cqrs.Tests.Cqrs.InvalidThenValidDuplicateRequestInvokerAssembly, Version=1.0.0.0");
|
||||
var container = new MicrosoftDiContainer();
|
||||
|
||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||
container.Freeze();
|
||||
|
||||
var context = new ArchitectureContext(container);
|
||||
var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false);
|
||||
Assert.That(response, Is.EqualTo("generated:payload"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 stream descriptor 枚举项与 provider 的 TryGetDescriptor 结果不一致时,
|
||||
/// registrar 会忽略该坏 descriptor,并继续回退到反射建流路径。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task CreateStream_Should_Fall_Back_To_Runtime_Path_When_Enumerated_Stream_Descriptor_Does_Not_Match_Provider()
|
||||
{
|
||||
var generatedAssembly = CreateGeneratedAssembly(
|
||||
typeof(MismatchedEnumeratingStreamInvokerProviderRegistry),
|
||||
"GFramework.Cqrs.Tests.Cqrs.MismatchedEnumeratingStreamInvokerAssembly, Version=1.0.0.0");
|
||||
var container = new MicrosoftDiContainer();
|
||||
|
||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||
container.Freeze();
|
||||
|
||||
var context = new ArchitectureContext(container);
|
||||
var results = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false);
|
||||
Assert.That(results, Is.EqualTo([3, 4]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当首个 stream descriptor 无效、后续同键 descriptor 有效时,
|
||||
/// registrar 不会因为过早去重而丢掉本可注册的 generated descriptor。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task CreateStream_Should_Use_Later_Valid_Generated_Stream_Descriptor_When_First_Duplicate_Is_Invalid()
|
||||
{
|
||||
var generatedAssembly = CreateGeneratedAssembly(
|
||||
typeof(InvalidThenValidDuplicateStreamInvokerProviderRegistry),
|
||||
"GFramework.Cqrs.Tests.Cqrs.InvalidThenValidDuplicateStreamInvokerAssembly, Version=1.0.0.0");
|
||||
var container = new MicrosoftDiContainer();
|
||||
|
||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||
container.Freeze();
|
||||
|
||||
var context = new ArchitectureContext(container);
|
||||
var results = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false);
|
||||
Assert.That(results, Is.EqualTo([30, 31]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟返回实例 request invoker 方法的 generated registry。
|
||||
/// </summary>
|
||||
@ -764,6 +1020,529 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 descriptor 枚举阶段抛出异常的 request invoker provider。
|
||||
/// </summary>
|
||||
private sealed class ThrowingEnumeratingRequestInvokerProviderRegistry :
|
||||
ICqrsHandlerRegistry,
|
||||
ICqrsRequestInvokerProvider,
|
||||
IEnumeratesCqrsRequestInvokerDescriptors
|
||||
{
|
||||
private static readonly CqrsRequestInvokerDescriptor Descriptor = new(
|
||||
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||
typeof(GeneratedRequestInvokerProviderRegistry).GetMethod(
|
||||
"InvokeGenerated",
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddTransient(
|
||||
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||
typeof(GeneratedRequestInvokerRequestHandler));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out CqrsRequestInvokerDescriptor? descriptor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(requestType);
|
||||
ArgumentNullException.ThrowIfNull(responseType);
|
||||
|
||||
if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
|
||||
{
|
||||
descriptor = Descriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
throw new InvalidOperationException("request descriptors failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 descriptor 枚举阶段抛出异常的 stream invoker provider。
|
||||
/// </summary>
|
||||
private sealed class ThrowingEnumeratingStreamInvokerProviderRegistry :
|
||||
ICqrsHandlerRegistry,
|
||||
ICqrsStreamInvokerProvider,
|
||||
IEnumeratesCqrsStreamInvokerDescriptors
|
||||
{
|
||||
private static readonly CqrsStreamInvokerDescriptor Descriptor = new(
|
||||
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||
typeof(GeneratedStreamInvokerProviderRegistry).GetMethod(
|
||||
"InvokeGenerated",
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddTransient(
|
||||
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||
typeof(GeneratedStreamInvokerRequestHandler));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out CqrsStreamInvokerDescriptor? descriptor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(requestType);
|
||||
ArgumentNullException.ThrowIfNull(responseType);
|
||||
|
||||
if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
|
||||
{
|
||||
descriptor = Descriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
throw new InvalidOperationException("stream descriptors failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟返回重复 request descriptor 条目的 generated registry。
|
||||
/// </summary>
|
||||
private sealed class DuplicateEnumeratingRequestInvokerProviderRegistry :
|
||||
ICqrsHandlerRegistry,
|
||||
ICqrsRequestInvokerProvider,
|
||||
IEnumeratesCqrsRequestInvokerDescriptors
|
||||
{
|
||||
private static readonly CqrsRequestInvokerDescriptor PrimaryDescriptor = new(
|
||||
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||
typeof(GeneratedRequestInvokerProviderRegistry).GetMethod(
|
||||
"InvokeGenerated",
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||
|
||||
private static readonly CqrsRequestInvokerDescriptor SecondaryDescriptor = new(
|
||||
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||
typeof(DuplicateEnumeratingRequestInvokerProviderRegistry).GetMethod(
|
||||
nameof(InvokeAlternativeGenerated),
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddTransient(
|
||||
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||
typeof(GeneratedRequestInvokerRequestHandler));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out CqrsRequestInvokerDescriptor? descriptor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(requestType);
|
||||
ArgumentNullException.ThrowIfNull(responseType);
|
||||
|
||||
if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
|
||||
{
|
||||
descriptor = PrimaryDescriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return
|
||||
[
|
||||
new CqrsRequestInvokerDescriptorEntry(typeof(GeneratedRequestInvokerRequest), typeof(string), PrimaryDescriptor),
|
||||
new CqrsRequestInvokerDescriptorEntry(typeof(GeneratedRequestInvokerRequest), typeof(string), SecondaryDescriptor)
|
||||
];
|
||||
}
|
||||
|
||||
private static ValueTask<string> InvokeAlternativeGenerated(
|
||||
object handler,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return ValueTask.FromResult("duplicate:payload");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟返回重复 stream descriptor 条目的 generated registry。
|
||||
/// </summary>
|
||||
private sealed class DuplicateEnumeratingStreamInvokerProviderRegistry :
|
||||
ICqrsHandlerRegistry,
|
||||
ICqrsStreamInvokerProvider,
|
||||
IEnumeratesCqrsStreamInvokerDescriptors
|
||||
{
|
||||
private static readonly CqrsStreamInvokerDescriptor PrimaryDescriptor = new(
|
||||
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||
typeof(GeneratedStreamInvokerProviderRegistry).GetMethod(
|
||||
"InvokeGenerated",
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||
|
||||
private static readonly CqrsStreamInvokerDescriptor SecondaryDescriptor = new(
|
||||
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||
typeof(DuplicateEnumeratingStreamInvokerProviderRegistry).GetMethod(
|
||||
nameof(InvokeAlternativeGenerated),
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddTransient(
|
||||
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||
typeof(GeneratedStreamInvokerRequestHandler));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out CqrsStreamInvokerDescriptor? descriptor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(requestType);
|
||||
ArgumentNullException.ThrowIfNull(responseType);
|
||||
|
||||
if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
|
||||
{
|
||||
descriptor = PrimaryDescriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return
|
||||
[
|
||||
new CqrsStreamInvokerDescriptorEntry(typeof(GeneratedStreamInvokerRequest), typeof(int), PrimaryDescriptor),
|
||||
new CqrsStreamInvokerDescriptorEntry(typeof(GeneratedStreamInvokerRequest), typeof(int), SecondaryDescriptor)
|
||||
];
|
||||
}
|
||||
|
||||
private static object InvokeAlternativeGenerated(object handler, object request, CancellationToken cancellationToken)
|
||||
{
|
||||
return new[] { 900, 901 }.ToAsyncEnumerable();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟枚举出的 request descriptor 与 provider 显式查询结果不一致的 generated registry。
|
||||
/// </summary>
|
||||
private sealed class MismatchedEnumeratingRequestInvokerProviderRegistry :
|
||||
ICqrsHandlerRegistry,
|
||||
ICqrsRequestInvokerProvider,
|
||||
IEnumeratesCqrsRequestInvokerDescriptors
|
||||
{
|
||||
private static readonly CqrsRequestInvokerDescriptor ProviderDescriptor = new(
|
||||
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||
typeof(GeneratedRequestInvokerProviderRegistry).GetMethod(
|
||||
"InvokeGenerated",
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||
|
||||
private static readonly CqrsRequestInvokerDescriptor EnumeratedDescriptor = new(
|
||||
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||
typeof(MismatchedEnumeratingRequestInvokerProviderRegistry).GetMethod(
|
||||
nameof(InvokeAlternativeGenerated),
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddTransient(
|
||||
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||
typeof(GeneratedRequestInvokerRequestHandler));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out CqrsRequestInvokerDescriptor? descriptor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(requestType);
|
||||
ArgumentNullException.ThrowIfNull(responseType);
|
||||
|
||||
if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
|
||||
{
|
||||
descriptor = ProviderDescriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return
|
||||
[
|
||||
new CqrsRequestInvokerDescriptorEntry(
|
||||
typeof(GeneratedRequestInvokerRequest),
|
||||
typeof(string),
|
||||
EnumeratedDescriptor)
|
||||
];
|
||||
}
|
||||
|
||||
private static ValueTask<string> InvokeAlternativeGenerated(
|
||||
object handler,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return ValueTask.FromResult("mismatched:payload");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟首个 request descriptor 无效、后续同键 descriptor 有效的 generated registry。
|
||||
/// </summary>
|
||||
private sealed class InvalidThenValidDuplicateRequestInvokerProviderRegistry :
|
||||
ICqrsHandlerRegistry,
|
||||
ICqrsRequestInvokerProvider,
|
||||
IEnumeratesCqrsRequestInvokerDescriptors
|
||||
{
|
||||
private static readonly CqrsRequestInvokerDescriptor InvalidDescriptor = new(
|
||||
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||
typeof(InvalidThenValidDuplicateRequestInvokerProviderRegistry).GetMethod(
|
||||
nameof(InvokeAlternativeGenerated),
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||
|
||||
private static readonly CqrsRequestInvokerDescriptor ValidDescriptor = new(
|
||||
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||
typeof(GeneratedRequestInvokerProviderRegistry).GetMethod(
|
||||
"InvokeGenerated",
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddTransient(
|
||||
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||
typeof(GeneratedRequestInvokerRequestHandler));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out CqrsRequestInvokerDescriptor? descriptor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(requestType);
|
||||
ArgumentNullException.ThrowIfNull(responseType);
|
||||
|
||||
if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
|
||||
{
|
||||
descriptor = ValidDescriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return
|
||||
[
|
||||
new CqrsRequestInvokerDescriptorEntry(
|
||||
typeof(GeneratedRequestInvokerRequest),
|
||||
typeof(string),
|
||||
InvalidDescriptor),
|
||||
new CqrsRequestInvokerDescriptorEntry(
|
||||
typeof(GeneratedRequestInvokerRequest),
|
||||
typeof(string),
|
||||
ValidDescriptor)
|
||||
];
|
||||
}
|
||||
|
||||
private static ValueTask<string> InvokeAlternativeGenerated(
|
||||
object handler,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return ValueTask.FromResult("invalid-first:payload");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟枚举出的 stream descriptor 与 provider 显式查询结果不一致的 generated registry。
|
||||
/// </summary>
|
||||
private sealed class MismatchedEnumeratingStreamInvokerProviderRegistry :
|
||||
ICqrsHandlerRegistry,
|
||||
ICqrsStreamInvokerProvider,
|
||||
IEnumeratesCqrsStreamInvokerDescriptors
|
||||
{
|
||||
private static readonly CqrsStreamInvokerDescriptor ProviderDescriptor = new(
|
||||
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||
typeof(GeneratedStreamInvokerProviderRegistry).GetMethod(
|
||||
"InvokeGenerated",
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||
|
||||
private static readonly CqrsStreamInvokerDescriptor EnumeratedDescriptor = new(
|
||||
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||
typeof(MismatchedEnumeratingStreamInvokerProviderRegistry).GetMethod(
|
||||
nameof(InvokeAlternativeGenerated),
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddTransient(
|
||||
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||
typeof(GeneratedStreamInvokerRequestHandler));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out CqrsStreamInvokerDescriptor? descriptor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(requestType);
|
||||
ArgumentNullException.ThrowIfNull(responseType);
|
||||
|
||||
if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
|
||||
{
|
||||
descriptor = ProviderDescriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return
|
||||
[
|
||||
new CqrsStreamInvokerDescriptorEntry(
|
||||
typeof(GeneratedStreamInvokerRequest),
|
||||
typeof(int),
|
||||
EnumeratedDescriptor)
|
||||
];
|
||||
}
|
||||
|
||||
private static object InvokeAlternativeGenerated(object handler, object request, CancellationToken cancellationToken)
|
||||
{
|
||||
return new[] { 700, 701 }.ToAsyncEnumerable();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟首个 stream descriptor 无效、后续同键 descriptor 有效的 generated registry。
|
||||
/// </summary>
|
||||
private sealed class InvalidThenValidDuplicateStreamInvokerProviderRegistry :
|
||||
ICqrsHandlerRegistry,
|
||||
ICqrsStreamInvokerProvider,
|
||||
IEnumeratesCqrsStreamInvokerDescriptors
|
||||
{
|
||||
private static readonly CqrsStreamInvokerDescriptor InvalidDescriptor = new(
|
||||
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||
typeof(InvalidThenValidDuplicateStreamInvokerProviderRegistry).GetMethod(
|
||||
nameof(InvokeAlternativeGenerated),
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||
|
||||
private static readonly CqrsStreamInvokerDescriptor ValidDescriptor = new(
|
||||
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||
typeof(GeneratedStreamInvokerProviderRegistry).GetMethod(
|
||||
"InvokeGenerated",
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddTransient(
|
||||
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||
typeof(GeneratedStreamInvokerRequestHandler));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out CqrsStreamInvokerDescriptor? descriptor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(requestType);
|
||||
ArgumentNullException.ThrowIfNull(responseType);
|
||||
|
||||
if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
|
||||
{
|
||||
descriptor = ValidDescriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return
|
||||
[
|
||||
new CqrsStreamInvokerDescriptorEntry(
|
||||
typeof(GeneratedStreamInvokerRequest),
|
||||
typeof(int),
|
||||
InvalidDescriptor),
|
||||
new CqrsStreamInvokerDescriptorEntry(
|
||||
typeof(GeneratedStreamInvokerRequest),
|
||||
typeof(int),
|
||||
ValidDescriptor)
|
||||
];
|
||||
}
|
||||
|
||||
private static object InvokeAlternativeGenerated(object handler, object request, CancellationToken cancellationToken)
|
||||
{
|
||||
return new[] { 800, 801 }.ToAsyncEnumerable();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建带有 generated request invoker registry 元数据的程序集替身。
|
||||
/// </summary>
|
||||
@ -906,6 +1685,65 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
||||
.Invoke(cache, Array.Empty<object>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取双键缓存中当前保存的对象。
|
||||
/// </summary>
|
||||
private static object? GetPairCacheValue(object cache, Type primaryType, Type secondaryType)
|
||||
{
|
||||
return InvokeInstanceMethod(cache, "GetValueOrDefaultForTesting", primaryType, secondaryType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定 stream dispatch binding 中当前缓存的 pipeline executor。
|
||||
/// </summary>
|
||||
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>
|
||||
/// 读取指定流式请求/响应类型对对应的强类型 stream dispatch binding。
|
||||
/// </summary>
|
||||
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>
|
||||
/// 调用目标对象上的实例方法。
|
||||
/// </summary>
|
||||
private static object? InvokeInstanceMethod(object target, string methodName, params object[] arguments)
|
||||
{
|
||||
var method = target.GetType().GetMethod(
|
||||
methodName,
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
|
||||
Assert.That(method, Is.Not.Null, $"Missing method {target.GetType().FullName}.{methodName}.");
|
||||
|
||||
return method!.Invoke(target, arguments);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 枚举并收集当前异步流中的全部元素,便于断言 generated stream invoker 的输出。
|
||||
/// </summary>
|
||||
|
||||
@ -138,6 +138,68 @@ internal sealed class CqrsHandlerRegistrarFallbackFailureTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 generated registry 是抽象类型时,registrar 会记录告警并回退到反射扫描。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterHandlers_Should_Fall_Back_To_Reflection_When_Generated_Registry_Is_Abstract()
|
||||
{
|
||||
var generatedAssembly = CreateGeneratedRegistryAssembly(
|
||||
"GFramework.Cqrs.Tests.Cqrs.AbstractGeneratedRegistryAssembly, Version=1.0.0.0",
|
||||
typeof(AbstractGeneratedNotificationHandlerRegistry));
|
||||
generatedAssembly
|
||||
.Setup(static assembly => assembly.GetTypes())
|
||||
.Returns([typeof(GeneratedRegistryNotificationHandler)]);
|
||||
|
||||
var container = new MicrosoftDiContainer();
|
||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(
|
||||
GetGeneratedRegistryNotificationHandlerTypes(container),
|
||||
Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)]));
|
||||
Assert.That(
|
||||
GetWarningLogs().Any(log =>
|
||||
log.Message.Contains("because it is abstract", StringComparison.Ordinal)),
|
||||
Is.True);
|
||||
});
|
||||
|
||||
generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 generated registry 不暴露可访问无参构造器时,registrar 会记录告警并回退到反射扫描。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterHandlers_Should_Fall_Back_To_Reflection_When_Generated_Registry_Has_No_Parameterless_Constructor()
|
||||
{
|
||||
var generatedAssembly = CreateGeneratedRegistryAssembly(
|
||||
"GFramework.Cqrs.Tests.Cqrs.NoParameterlessGeneratedRegistryAssembly, Version=1.0.0.0",
|
||||
typeof(ConstructorArgumentNotificationHandlerRegistry));
|
||||
generatedAssembly
|
||||
.Setup(static assembly => assembly.GetTypes())
|
||||
.Returns([typeof(GeneratedRegistryNotificationHandler)]);
|
||||
|
||||
var container = new MicrosoftDiContainer();
|
||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(
|
||||
GetGeneratedRegistryNotificationHandlerTypes(container),
|
||||
Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)]));
|
||||
Assert.That(
|
||||
GetWarningLogs().Any(log =>
|
||||
log.Message.Contains(
|
||||
"does not expose an accessible parameterless constructor",
|
||||
StringComparison.Ordinal)),
|
||||
Is.True);
|
||||
});
|
||||
|
||||
generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个仅通过 generated registry 注册主 handler、并附带指定 fallback 元数据的程序集替身。
|
||||
/// </summary>
|
||||
@ -161,6 +223,24 @@ internal sealed class CqrsHandlerRegistrarFallbackFailureTests
|
||||
return generatedAssembly;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个只声明 generated registry attribute 的程序集替身,用于验证 registry 激活失败后的回退行为。
|
||||
/// </summary>
|
||||
/// <param name="assemblyName">用于日志与缓存键的程序集名。</param>
|
||||
/// <param name="registryType">要暴露给 registrar 的 generated registry 类型。</param>
|
||||
/// <returns>已完成基础接线的程序集 mock。</returns>
|
||||
private static Mock<Assembly> CreateGeneratedRegistryAssembly(string assemblyName, Type registryType)
|
||||
{
|
||||
var generatedAssembly = new Mock<Assembly>();
|
||||
generatedAssembly
|
||||
.SetupGet(static assembly => assembly.FullName)
|
||||
.Returns(assemblyName);
|
||||
generatedAssembly
|
||||
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
|
||||
.Returns([new CqrsHandlerRegistryAttribute(registryType)]);
|
||||
return generatedAssembly;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提取容器中针对 generated notification 注册的处理器实现类型。
|
||||
/// </summary>
|
||||
@ -259,4 +339,55 @@ internal sealed class CqrsHandlerRegistrarFallbackFailureTests
|
||||
.Where(static log => log.Level == LogLevel.Warning)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 generated registry 被错误声明为抽象类型时的激活失败场景。
|
||||
/// </summary>
|
||||
private abstract class AbstractGeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// 抽象 registry 即便具备注册逻辑,也不应被运行时实例化。
|
||||
/// </summary>
|
||||
/// <param name="services">承载处理器映射的服务集合。</param>
|
||||
/// <param name="logger">记录注册诊断的日志器。</param>
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddTransient(
|
||||
typeof(INotificationHandler<GeneratedRegistryNotification>),
|
||||
typeof(GeneratedRegistryNotificationHandler));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 generated registry 缺少可访问无参构造器时的激活失败场景。
|
||||
/// </summary>
|
||||
private sealed class ConstructorArgumentNotificationHandlerRegistry : ICqrsHandlerRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一个只能通过额外参数构造的测试 registry。
|
||||
/// </summary>
|
||||
/// <param name="marker">用于区分测试场景的占位参数。</param>
|
||||
public ConstructorArgumentNotificationHandlerRegistry(string marker)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(marker);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 此实现仅用于满足接口契约;本用例关注的是实例化失败前的回退行为。
|
||||
/// </summary>
|
||||
/// <param name="services">承载处理器映射的服务集合。</param>
|
||||
/// <param name="logger">记录注册诊断的日志器。</param>
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddTransient(
|
||||
typeof(INotificationHandler<GeneratedRegistryNotification>),
|
||||
typeof(GeneratedRegistryNotificationHandler));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ using GFramework.Core.Architectures;
|
||||
using GFramework.Core.Ioc;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using GFramework.Cqrs.Internal;
|
||||
using GFramework.Cqrs.Tests.Logging;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
@ -170,6 +171,74 @@ internal sealed class CqrsHandlerRegistrarTests
|
||||
Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 direct generated-registry 激活入口在 registry 为抽象类型时会抛出异常,并保留契约告警。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterGeneratedRegistry_Should_Throw_When_Generated_Registry_Is_Abstract()
|
||||
{
|
||||
var capturingProvider = new CapturingLoggerFactoryProvider(LogLevel.Warning);
|
||||
var logger = capturingProvider.CreateLogger(nameof(CqrsHandlerRegistrarTests));
|
||||
var container = new MicrosoftDiContainer();
|
||||
|
||||
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||
CqrsHandlerRegistrar.RegisterGeneratedRegistry(
|
||||
container,
|
||||
typeof(AbstractGeneratedNotificationHandlerRegistry),
|
||||
logger));
|
||||
|
||||
var warningLogs = capturingProvider.Loggers
|
||||
.SelectMany(static createdLogger => createdLogger.Logs)
|
||||
.Where(static log => log.Level == LogLevel.Warning)
|
||||
.ToArray();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(exception, Is.Not.Null);
|
||||
Assert.That(exception!.Message, Does.Contain(typeof(AbstractGeneratedNotificationHandlerRegistry).FullName));
|
||||
Assert.That(
|
||||
warningLogs.Any(log =>
|
||||
log.Message.Contains("because it is abstract", StringComparison.Ordinal)),
|
||||
Is.True);
|
||||
Assert.That(container.GetServicesUnsafe, Is.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 direct generated-registry 激活入口在 registry 缺少无参构造器时会抛出异常,并保留契约告警。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterGeneratedRegistry_Should_Throw_When_Generated_Registry_Has_No_Parameterless_Constructor()
|
||||
{
|
||||
var capturingProvider = new CapturingLoggerFactoryProvider(LogLevel.Warning);
|
||||
var logger = capturingProvider.CreateLogger(nameof(CqrsHandlerRegistrarTests));
|
||||
var container = new MicrosoftDiContainer();
|
||||
|
||||
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||
CqrsHandlerRegistrar.RegisterGeneratedRegistry(
|
||||
container,
|
||||
typeof(ConstructorArgumentNotificationHandlerRegistry),
|
||||
logger));
|
||||
|
||||
var warningLogs = capturingProvider.Loggers
|
||||
.SelectMany(static createdLogger => createdLogger.Logs)
|
||||
.Where(static log => log.Level == LogLevel.Warning)
|
||||
.ToArray();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(exception, Is.Not.Null);
|
||||
Assert.That(exception!.Message, Does.Contain(typeof(ConstructorArgumentNotificationHandlerRegistry).FullName));
|
||||
Assert.That(
|
||||
warningLogs.Any(log =>
|
||||
log.Message.Contains(
|
||||
"does not expose an accessible parameterless constructor",
|
||||
StringComparison.Ordinal)),
|
||||
Is.True);
|
||||
Assert.That(container.GetServicesUnsafe, Is.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当生成注册器元数据损坏时,运行时会记录告警并回退到反射扫描路径。
|
||||
/// </summary>
|
||||
@ -695,4 +764,55 @@ internal sealed class CqrsHandlerRegistrarTests
|
||||
return typeof(CqrsReflectionFallbackAttribute).Assembly
|
||||
.GetType("GFramework.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟被错误声明为抽象类型的 generated registry。
|
||||
/// </summary>
|
||||
private abstract class AbstractGeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// 抽象 registry 即便具备注册逻辑,也不应被 direct 激活入口实例化。
|
||||
/// </summary>
|
||||
/// <param name="services">承载处理器映射的服务集合。</param>
|
||||
/// <param name="logger">记录注册诊断的日志器。</param>
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddTransient(
|
||||
typeof(INotificationHandler<GeneratedRegistryNotification>),
|
||||
typeof(GeneratedRegistryNotificationHandler));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟缺少无参构造器的 generated registry。
|
||||
/// </summary>
|
||||
private sealed class ConstructorArgumentNotificationHandlerRegistry : ICqrsHandlerRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一个只能通过额外参数构造的测试 registry。
|
||||
/// </summary>
|
||||
/// <param name="marker">用于区分测试场景的占位参数。</param>
|
||||
public ConstructorArgumentNotificationHandlerRegistry(string marker)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(marker);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 此实现仅用于满足接口契约;本用例关注的是构造阶段失败后的异常语义。
|
||||
/// </summary>
|
||||
/// <param name="services">承载处理器映射的服务集合。</param>
|
||||
/// <param name="logger">记录注册诊断的日志器。</param>
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddTransient(
|
||||
typeof(INotificationHandler<GeneratedRegistryNotification>),
|
||||
typeof(GeneratedRegistryNotificationHandler));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,6 +91,145 @@ internal sealed class CqrsNotificationPublisherTests
|
||||
Assert.That(handler.ObservedContext, Is.SameAs(architectureContext.Object));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当容器里可见多个通知发布策略时,dispatcher 会拒绝在歧义状态下继续发布。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void PublishAsync_Should_Throw_When_Multiple_NotificationPublishers_Are_Registered()
|
||||
{
|
||||
var runtime = CreateRuntime(
|
||||
container =>
|
||||
{
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler<PublisherNotification>)))
|
||||
.Returns([new RecordingNotificationHandler("only", [])]);
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationPublisher)))
|
||||
.Returns(
|
||||
[
|
||||
new TrackingNotificationPublisher(),
|
||||
new TrackingNotificationPublisher()
|
||||
]);
|
||||
});
|
||||
|
||||
Assert.That(
|
||||
async () => await runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).ConfigureAwait(false),
|
||||
Throws.InvalidOperationException.With.Message.EqualTo(
|
||||
$"Multiple {typeof(INotificationPublisher).FullName} instances are registered. Remove duplicate notification publisher strategies before publishing notifications."));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 dispatcher 在首次发布时解析通知发布器后,会复用同一实例并停止继续查询容器。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task PublishAsync_Should_Cache_Resolved_NotificationPublisher_After_First_Publish()
|
||||
{
|
||||
var firstPublisher = new TrackingNotificationPublisher();
|
||||
var secondPublisher = new TrackingNotificationPublisher();
|
||||
var notificationPublisherLookupCount = 0;
|
||||
var runtime = CreateRuntime(
|
||||
container =>
|
||||
{
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler<PublisherNotification>)))
|
||||
.Returns([new RecordingNotificationHandler("only", [])]);
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationPublisher)))
|
||||
.Returns(() =>
|
||||
{
|
||||
notificationPublisherLookupCount++;
|
||||
return notificationPublisherLookupCount switch
|
||||
{
|
||||
1 => [firstPublisher],
|
||||
2 => [secondPublisher],
|
||||
_ => throw new AssertionException("Notification publisher should be resolved at most once.")
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
await runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).ConfigureAwait(false);
|
||||
await runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).ConfigureAwait(false);
|
||||
|
||||
Assert.That(notificationPublisherLookupCount, Is.EqualTo(1));
|
||||
Assert.That(firstPublisher.PublishCallCount, Is.EqualTo(2));
|
||||
Assert.That(secondPublisher.PublishCallCount, Is.Zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当容器里没有任何通知发布器时,dispatcher 会回退到内置顺序发布器,
|
||||
/// 并在首次解析后缓存该 fallback 结果而不是在后续发布时重新查询容器。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task PublishAsync_Should_Fallback_To_SequentialNotificationPublisher_And_Cache_It_When_None_Is_Registered()
|
||||
{
|
||||
var invocationOrder = new List<string>();
|
||||
var notificationPublisherLookupCount = 0;
|
||||
var runtime = CreateRuntime(
|
||||
container =>
|
||||
{
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler<PublisherNotification>)))
|
||||
.Returns(
|
||||
[
|
||||
new RecordingNotificationHandler("first", invocationOrder),
|
||||
new RecordingNotificationHandler("second", invocationOrder)
|
||||
]);
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationPublisher)))
|
||||
.Returns(() =>
|
||||
{
|
||||
notificationPublisherLookupCount++;
|
||||
if (notificationPublisherLookupCount == 1)
|
||||
{
|
||||
return Array.Empty<object>();
|
||||
}
|
||||
|
||||
throw new AssertionException("Notification publisher should not be resolved more than once.");
|
||||
});
|
||||
});
|
||||
|
||||
await runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).ConfigureAwait(false);
|
||||
await runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).ConfigureAwait(false);
|
||||
|
||||
Assert.That(notificationPublisherLookupCount, Is.EqualTo(1));
|
||||
Assert.That(invocationOrder, Is.EqualTo(["first", "second", "first", "second"]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证内置 `TaskWhenAll` 发布器会继续调度所有处理器,而不是沿用默认顺序发布器的失败即停语义。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task PublishAsync_Should_Invoke_All_Handlers_When_Using_TaskWhenAll_NotificationPublisher()
|
||||
{
|
||||
var trailingHandler = new RecordingNotificationHandler("second", []);
|
||||
var runtime = CreateRuntime(
|
||||
container =>
|
||||
{
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler<PublisherNotification>)))
|
||||
.Returns(
|
||||
[
|
||||
new ThrowingNotificationHandler(),
|
||||
trailingHandler
|
||||
]);
|
||||
},
|
||||
new TaskWhenAllNotificationPublisher());
|
||||
|
||||
var publishTask = runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).AsTask();
|
||||
|
||||
try
|
||||
{
|
||||
await publishTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 并行发布会把处理器失败收敛到返回任务;这里仅消费异常并继续验证所有处理器都已被触发。
|
||||
}
|
||||
|
||||
Assert.That(trailingHandler.Invoked, Is.True);
|
||||
Assert.That(publishTask.Exception, Is.Not.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证默认通知发布器在零处理器场景下会保持静默完成。
|
||||
/// </summary>
|
||||
@ -148,6 +287,11 @@ internal sealed class CqrsNotificationPublisherTests
|
||||
var container = new Mock<IIocContainer>(MockBehavior.Strict);
|
||||
var logger = new TestLogger(nameof(CqrsNotificationPublisherTests), LogLevel.Debug);
|
||||
|
||||
// 默认 runtime 会延迟解析通知发布器;strict mock 需要声明“未注册自定义 publisher”的空集合返回。
|
||||
container
|
||||
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationPublisher)))
|
||||
.Returns(Array.Empty<object>());
|
||||
|
||||
configureContainer(container);
|
||||
return CqrsRuntimeFactory.CreateRuntime(container.Object, logger, notificationPublisher);
|
||||
}
|
||||
@ -222,6 +366,11 @@ internal sealed class CqrsNotificationPublisherTests
|
||||
/// </summary>
|
||||
public bool WasCalled { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前发布器累计执行发布的次数。
|
||||
/// </summary>
|
||||
public int PublishCallCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 记录当前发布器已被调用,并继续按当前顺序执行所有处理器。
|
||||
/// </summary>
|
||||
@ -235,6 +384,7 @@ internal sealed class CqrsNotificationPublisherTests
|
||||
where TNotification : INotification
|
||||
{
|
||||
WasCalled = true;
|
||||
PublishCallCount++;
|
||||
|
||||
foreach (var handler in context.Handlers)
|
||||
{
|
||||
|
||||
@ -13,6 +13,24 @@ namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
[TestFixture]
|
||||
internal sealed class CqrsRegistrationServiceTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证空程序集输入不会触发底层注册,也不会产生重复跳过日志。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterHandlers_Should_Not_Invoke_Registrar_When_Assemblies_Are_Empty()
|
||||
{
|
||||
var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug);
|
||||
var registrar = new Mock<ICqrsHandlerRegistrar>(MockBehavior.Strict);
|
||||
var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger);
|
||||
|
||||
service.RegisterHandlers([]);
|
||||
|
||||
registrar.Verify(
|
||||
static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()),
|
||||
Times.Never);
|
||||
Assert.That(logger.Logs, Has.Count.EqualTo(0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证同一次调用内出现重复程序集键时,底层注册器只会接收到一次注册请求。
|
||||
/// </summary>
|
||||
@ -87,17 +105,201 @@ internal sealed class CqrsRegistrationServiceTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证协调器会忽略空项,并按稳定程序集键排序后仅注册当前调用内的唯一程序集。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterHandlers_Should_Ignore_Null_Entries_And_Register_Unique_Assemblies_In_Stable_Key_Order()
|
||||
{
|
||||
var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug);
|
||||
var registrar = new Mock<ICqrsHandlerRegistrar>(MockBehavior.Strict);
|
||||
var assemblyC = CreateAssembly("GFramework.Cqrs.Tests.Sorting.C, Version=1.0.0.0");
|
||||
var assemblyA = CreateAssembly("GFramework.Cqrs.Tests.Sorting.A, Version=1.0.0.0");
|
||||
var duplicateAssemblyA = CreateAssembly("GFramework.Cqrs.Tests.Sorting.A, Version=1.0.0.0");
|
||||
var assemblyB = CreateAssembly("GFramework.Cqrs.Tests.Sorting.B, Version=1.0.0.0");
|
||||
var registeredAssemblies = new List<Assembly>();
|
||||
|
||||
registrar
|
||||
.Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()))
|
||||
.Callback<IEnumerable<Assembly>>(assemblies => registeredAssemblies.AddRange(assemblies));
|
||||
|
||||
var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger);
|
||||
|
||||
service.RegisterHandlers([assemblyC.Object, null!, assemblyA.Object, duplicateAssemblyA.Object, assemblyB.Object, null!]);
|
||||
|
||||
registrar.Verify(
|
||||
static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()),
|
||||
Times.Exactly(3));
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(
|
||||
registeredAssemblies,
|
||||
Is.EqualTo([assemblyA.Object, assemblyB.Object, assemblyC.Object]));
|
||||
Assert.That(logger.Logs, Has.Count.EqualTo(0));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证跨调用遇到已注册程序集键时,协调器会跳过重复项,同时继续按稳定程序集键顺序处理剩余新程序集。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterHandlers_Should_Skip_Previously_Registered_Keys_And_Keep_Stable_Order_For_Remaining_Assemblies()
|
||||
{
|
||||
var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug);
|
||||
var registrar = new Mock<ICqrsHandlerRegistrar>(MockBehavior.Strict);
|
||||
var firstAssembly = CreateAssembly("GFramework.Cqrs.Tests.Sorting.B, Version=1.0.0.0");
|
||||
var duplicateAssembly = CreateAssembly("GFramework.Cqrs.Tests.Sorting.B, Version=1.0.0.0");
|
||||
var assemblyC = CreateAssembly("GFramework.Cqrs.Tests.Sorting.C, Version=1.0.0.0");
|
||||
var assemblyA = CreateAssembly("GFramework.Cqrs.Tests.Sorting.A, Version=1.0.0.0");
|
||||
var registeredAssemblies = new List<Assembly>();
|
||||
|
||||
registrar
|
||||
.Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()))
|
||||
.Callback<IEnumerable<Assembly>>(assemblies => registeredAssemblies.AddRange(assemblies));
|
||||
|
||||
var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger);
|
||||
|
||||
service.RegisterHandlers([firstAssembly.Object]);
|
||||
service.RegisterHandlers([assemblyC.Object, duplicateAssembly.Object, assemblyA.Object]);
|
||||
|
||||
registrar.Verify(
|
||||
static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()),
|
||||
Times.Exactly(3));
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(
|
||||
registeredAssemblies,
|
||||
Is.EqualTo([firstAssembly.Object, assemblyA.Object, assemblyC.Object]));
|
||||
var debugMessages = logger.Logs
|
||||
.Where(static log => log.Level == LogLevel.Debug)
|
||||
.Select(static log => log.Message)
|
||||
.ToArray();
|
||||
Assert.That(debugMessages, Has.Length.EqualTo(1));
|
||||
Assert.That(
|
||||
debugMessages[0],
|
||||
Does.Contain("GFramework.Cqrs.Tests.Sorting.B, Version=1.0.0.0"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 <see cref="Assembly.FullName" /> 缺失时,协调器会退化到 <see cref="AssemblyName.Name" /> 作为稳定程序集键。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterHandlers_Should_Fallback_To_Simple_Name_When_Full_Name_Is_Missing()
|
||||
{
|
||||
var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug);
|
||||
var registrar = new Mock<ICqrsHandlerRegistrar>(MockBehavior.Strict);
|
||||
var firstAssembly = CreateAssembly(
|
||||
assemblyFullName: null,
|
||||
assemblySimpleName: "GFramework.Cqrs.Tests.SimpleNameFallback",
|
||||
assemblyDisplayName: "DisplayName-A");
|
||||
var secondAssembly = CreateAssembly(
|
||||
assemblyFullName: null,
|
||||
assemblySimpleName: "GFramework.Cqrs.Tests.SimpleNameFallback",
|
||||
assemblyDisplayName: "DisplayName-B");
|
||||
IEnumerable<Assembly>? registeredAssemblies = null;
|
||||
|
||||
registrar
|
||||
.Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()))
|
||||
.Callback<IEnumerable<Assembly>>(assemblies => registeredAssemblies = assemblies.ToArray());
|
||||
|
||||
var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger);
|
||||
|
||||
service.RegisterHandlers([firstAssembly.Object]);
|
||||
service.RegisterHandlers([secondAssembly.Object]);
|
||||
|
||||
registrar.Verify(
|
||||
static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()),
|
||||
Times.Once);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(registeredAssemblies, Is.EqualTo([firstAssembly.Object]));
|
||||
var debugMessages = logger.Logs
|
||||
.Where(static log => log.Level == LogLevel.Debug)
|
||||
.Select(static log => log.Message)
|
||||
.ToArray();
|
||||
Assert.That(debugMessages, Has.Length.EqualTo(1));
|
||||
Assert.That(debugMessages[0], Does.Contain("GFramework.Cqrs.Tests.SimpleNameFallback"));
|
||||
Assert.That(debugMessages[0], Does.Not.Contain("DisplayName-B"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 <see cref="Assembly.FullName" /> 与 <see cref="AssemblyName.Name" /> 均缺失时,
|
||||
/// 协调器会退化到 <see cref="object.ToString" /> 结果作为稳定程序集键。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterHandlers_Should_Fallback_To_ToString_When_Full_Name_And_Simple_Name_Are_Missing()
|
||||
{
|
||||
var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug);
|
||||
var registrar = new Mock<ICqrsHandlerRegistrar>(MockBehavior.Strict);
|
||||
const string assemblyDisplayName = "GFramework.Cqrs.Tests.ToStringFallback";
|
||||
var firstAssembly = CreateAssembly(
|
||||
assemblyFullName: null,
|
||||
assemblySimpleName: null,
|
||||
assemblyDisplayName: assemblyDisplayName);
|
||||
var secondAssembly = CreateAssembly(
|
||||
assemblyFullName: null,
|
||||
assemblySimpleName: null,
|
||||
assemblyDisplayName: assemblyDisplayName);
|
||||
IEnumerable<Assembly>? registeredAssemblies = null;
|
||||
|
||||
registrar
|
||||
.Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()))
|
||||
.Callback<IEnumerable<Assembly>>(assemblies => registeredAssemblies = assemblies.ToArray());
|
||||
|
||||
var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger);
|
||||
|
||||
service.RegisterHandlers([firstAssembly.Object]);
|
||||
service.RegisterHandlers([secondAssembly.Object]);
|
||||
|
||||
registrar.Verify(
|
||||
static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()),
|
||||
Times.Once);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(registeredAssemblies, Is.EqualTo([firstAssembly.Object]));
|
||||
var debugMessages = logger.Logs
|
||||
.Where(static log => log.Level == LogLevel.Debug)
|
||||
.Select(static log => log.Message)
|
||||
.ToArray();
|
||||
Assert.That(debugMessages, Has.Length.EqualTo(1));
|
||||
Assert.That(debugMessages[0], Does.Contain(assemblyDisplayName));
|
||||
Assert.That(debugMessages[0], Does.Contain("already registered"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个带稳定程序集键的程序集 mock,用于模拟不同 <see cref="Assembly" /> 实例表示同一程序集的场景。
|
||||
/// </summary>
|
||||
/// <param name="assemblyFullName">要返回的程序集完整名称。</param>
|
||||
/// <returns>配置好完整名称的程序集 mock。</returns>
|
||||
private static Mock<Assembly> CreateAssembly(string assemblyFullName)
|
||||
{
|
||||
return CreateAssembly(assemblyFullName, assemblySimpleName: null, assemblyDisplayName: assemblyFullName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个可配置程序集元数据退化路径的程序集 mock,用于验证稳定程序集键的回退顺序。
|
||||
/// </summary>
|
||||
/// <param name="assemblyFullName">要返回的程序集完整名称;为 <see langword="null" /> 时模拟缺失完整名称。</param>
|
||||
/// <param name="assemblySimpleName">要返回的程序集简单名称;为 <see langword="null" /> 时模拟缺失简单名称。</param>
|
||||
/// <param name="assemblyDisplayName">当需要退化到 <see cref="object.ToString" /> 时返回的显示名称。</param>
|
||||
/// <returns>配置好程序集元数据的程序集 mock。</returns>
|
||||
private static Mock<Assembly> CreateAssembly(string? assemblyFullName, string? assemblySimpleName, string assemblyDisplayName)
|
||||
{
|
||||
var assembly = new Mock<Assembly>();
|
||||
var assemblyName = new AssemblyName();
|
||||
assembly
|
||||
.SetupGet(static currentAssembly => currentAssembly.FullName)
|
||||
.Returns(assemblyFullName);
|
||||
assemblyName.Name = assemblySimpleName;
|
||||
assembly
|
||||
.Setup(static currentAssembly => currentAssembly.GetName())
|
||||
.Returns(assemblyName);
|
||||
assembly
|
||||
.Setup(static currentAssembly => currentAssembly.ToString())
|
||||
.Returns(assemblyDisplayName);
|
||||
|
||||
return assembly;
|
||||
}
|
||||
|
||||
@ -12,9 +12,27 @@ namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
internal static class DispatcherStreamContextRefreshState
|
||||
{
|
||||
private static readonly Lock _syncRoot = new();
|
||||
private static int _nextBehaviorInstanceId;
|
||||
private static int _nextHandlerInstanceId;
|
||||
private static readonly List<DispatcherPipelineContextSnapshot> _behaviorSnapshots = [];
|
||||
private static readonly List<DispatcherPipelineContextSnapshot> _handlerSnapshots = [];
|
||||
|
||||
/// <summary>
|
||||
/// 获取每次建流时记录的 behavior 快照副本。
|
||||
/// </summary>
|
||||
/// <returns>当前已记录的 behavior 上下文快照副本。</returns>
|
||||
/// <remarks>共享状态通过 <c>_syncRoot</c> 串行化,避免并行测试写入抖动。</remarks>
|
||||
public static IReadOnlyList<DispatcherPipelineContextSnapshot> BehaviorSnapshots
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return _behaviorSnapshots.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取每次建流时记录的快照副本。
|
||||
/// </summary>
|
||||
@ -31,6 +49,15 @@ internal static class DispatcherStreamContextRefreshState
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为新的 behavior 测试实例分配稳定编号。
|
||||
/// </summary>
|
||||
/// <returns>单调递增的 behavior 实例编号。</returns>
|
||||
public static int AllocateBehaviorInstanceId()
|
||||
{
|
||||
return Interlocked.Increment(ref _nextBehaviorInstanceId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为新的 handler 测试实例分配稳定编号。
|
||||
/// </summary>
|
||||
@ -40,6 +67,21 @@ internal static class DispatcherStreamContextRefreshState
|
||||
return Interlocked.Increment(ref _nextHandlerInstanceId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录 behavior 在当前建流中观察到的上下文。
|
||||
/// </summary>
|
||||
/// <param name="dispatchId">触发本次记录的稳定分发标识。</param>
|
||||
/// <param name="instanceId">观察到该上下文的 behavior 实例编号。</param>
|
||||
/// <param name="context">当前分发注入到 behavior 的架构上下文。</param>
|
||||
/// <remarks>写入过程通过 <c>_syncRoot</c> 串行化,确保快照列表保持稳定顺序。</remarks>
|
||||
public static void RecordBehavior(string dispatchId, int instanceId, IArchitectureContext context)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_behaviorSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录 handler 在当前建流中观察到的上下文。
|
||||
/// </summary>
|
||||
@ -63,7 +105,9 @@ internal static class DispatcherStreamContextRefreshState
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_nextBehaviorInstanceId = 0;
|
||||
_nextHandlerInstanceId = 0;
|
||||
_behaviorSnapshots.Clear();
|
||||
_handlerSnapshots.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Threading;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 为 <see cref="DispatcherCacheStreamRequest" /> 提供最小 stream pipeline 行为,
|
||||
/// 用于命中 dispatcher 的 stream pipeline invoker 缓存分支。
|
||||
/// </summary>
|
||||
internal sealed class DispatcherStreamPipelineCacheBehavior : IStreamPipelineBehavior<DispatcherCacheStreamRequest, int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 直接转发到下一个处理阶段。
|
||||
/// </summary>
|
||||
/// <param name="request">当前流式请求。</param>
|
||||
/// <param name="next">下一个处理阶段。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>下游处理阶段返回的异步流。</returns>
|
||||
public IAsyncEnumerable<int> Handle(
|
||||
DispatcherCacheStreamRequest request,
|
||||
StreamMessageHandlerDelegate<DispatcherCacheStreamRequest, int> next,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return next(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using GFramework.Cqrs.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 记录缓存 stream pipeline executor 复用场景下每次建流注入到 behavior 的上下文与实例身份。
|
||||
/// </summary>
|
||||
internal sealed class DispatcherStreamPipelineContextRefreshBehavior
|
||||
: CqrsContextAwareHandlerBase,
|
||||
IStreamPipelineBehavior<DispatcherStreamContextRefreshRequest, int>
|
||||
{
|
||||
private readonly int _instanceId = DispatcherStreamContextRefreshState.AllocateBehaviorInstanceId();
|
||||
|
||||
/// <summary>
|
||||
/// 记录当前 behavior 实例实际收到的上下文,然后继续执行下游处理阶段。
|
||||
/// </summary>
|
||||
/// <param name="request">当前流式请求。</param>
|
||||
/// <param name="next">下一个处理阶段。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>下游处理阶段返回的异步流。</returns>
|
||||
public async IAsyncEnumerable<int> Handle(
|
||||
DispatcherStreamContextRefreshRequest request,
|
||||
StreamMessageHandlerDelegate<DispatcherStreamContextRefreshRequest, int> next,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
DispatcherStreamContextRefreshState.RecordBehavior(request.DispatchId, _instanceId, Context);
|
||||
|
||||
await foreach (var item in next(request, cancellationToken)
|
||||
.WithCancellation(cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 处理 <see cref="DispatcherStreamPipelineOrderRequest" /> 并记录最终 handler 执行位置。
|
||||
/// </summary>
|
||||
internal sealed class DispatcherStreamPipelineOrderHandler : IStreamRequestHandler<DispatcherStreamPipelineOrderRequest, int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 记录 handler 执行步骤并返回稳定元素。
|
||||
/// </summary>
|
||||
/// <param name="request">当前流式请求。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>包含一个固定元素的异步流。</returns>
|
||||
public async IAsyncEnumerable<int> Handle(
|
||||
DispatcherStreamPipelineOrderRequest request,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
DispatcherStreamPipelineOrderState.Record("Handler");
|
||||
yield return 21;
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 记录双 stream pipeline 的内层行为顺序。
|
||||
/// </summary>
|
||||
internal sealed class DispatcherStreamPipelineOrderInnerBehavior : IStreamPipelineBehavior<DispatcherStreamPipelineOrderRequest, int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 在进入和离开下游阶段时记录顺序。
|
||||
/// </summary>
|
||||
/// <param name="request">当前流式请求。</param>
|
||||
/// <param name="next">下一个处理阶段。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>下游处理阶段返回的异步流。</returns>
|
||||
public async IAsyncEnumerable<int> Handle(
|
||||
DispatcherStreamPipelineOrderRequest request,
|
||||
StreamMessageHandlerDelegate<DispatcherStreamPipelineOrderRequest, int> next,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
DispatcherStreamPipelineOrderState.Record("Inner:Before");
|
||||
|
||||
await foreach (var item in next(request, cancellationToken)
|
||||
.WithCancellation(cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
|
||||
DispatcherStreamPipelineOrderState.Record("Inner:After");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 记录双 stream pipeline 的外层行为顺序。
|
||||
/// </summary>
|
||||
internal sealed class DispatcherStreamPipelineOrderOuterBehavior : IStreamPipelineBehavior<DispatcherStreamPipelineOrderRequest, int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 在进入和离开下游阶段时记录顺序。
|
||||
/// </summary>
|
||||
/// <param name="request">当前流式请求。</param>
|
||||
/// <param name="next">下一个处理阶段。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>下游处理阶段返回的异步流。</returns>
|
||||
public async IAsyncEnumerable<int> Handle(
|
||||
DispatcherStreamPipelineOrderRequest request,
|
||||
StreamMessageHandlerDelegate<DispatcherStreamPipelineOrderRequest, int> next,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
DispatcherStreamPipelineOrderState.Record("Outer:Before");
|
||||
|
||||
await foreach (var item in next(request, cancellationToken)
|
||||
.WithCancellation(cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
|
||||
DispatcherStreamPipelineOrderState.Record("Outer:After");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证双 stream pipeline 行为执行顺序的最小流式请求。
|
||||
/// </summary>
|
||||
internal sealed record DispatcherStreamPipelineOrderRequest : IStreamRequest<int>;
|
||||
@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Threading;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 记录双 stream pipeline 行为的实际执行顺序。
|
||||
/// </summary>
|
||||
internal static class DispatcherStreamPipelineOrderState
|
||||
{
|
||||
private static readonly Lock SyncRoot = new();
|
||||
private static readonly List<string> _steps = [];
|
||||
|
||||
/// <summary>
|
||||
/// 获取按执行顺序追加的步骤快照。
|
||||
/// 共享状态通过 <c>SyncRoot</c> 串行化,避免并行 stream 行为测试互相污染步骤列表。
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> Steps
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return _steps.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录一个新的 stream pipeline 执行步骤。
|
||||
/// </summary>
|
||||
/// <param name="step">要追加的步骤名称。</param>
|
||||
public static void Record(string step)
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
_steps.Add(step);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空当前记录,供下一次断言使用。
|
||||
/// </summary>
|
||||
public static void Reset()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
_steps.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 处理 <see cref="DispatcherZeroPipelineStreamRequest" />,用于验证零管道 stream 的 dispatcher 缓存路径。
|
||||
/// </summary>
|
||||
internal sealed class DispatcherZeroPipelineStreamHandler : IStreamRequestHandler<DispatcherZeroPipelineStreamRequest, int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回一个单元素异步流,便于在缓存测试中最小化处理噪音。
|
||||
/// </summary>
|
||||
/// <param name="request">当前零管道 stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于终止异步枚举的取消令牌。</param>
|
||||
/// <returns>只包含一个元素的异步响应流。</returns>
|
||||
public async IAsyncEnumerable<int> Handle(
|
||||
DispatcherZeroPipelineStreamRequest request,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
yield return 1;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 表示未注册任何 stream pipeline behavior 的最小缓存验证请求。
|
||||
/// </summary>
|
||||
internal sealed record DispatcherZeroPipelineStreamRequest : IStreamRequest<int>;
|
||||
@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Threading;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 记录 generated stream invoker 与 stream pipeline 行为组合时的命中次数。
|
||||
/// </summary>
|
||||
internal sealed class GeneratedStreamPipelineTrackingBehavior
|
||||
: IStreamPipelineBehavior<GeneratedStreamInvokerRequest, int>
|
||||
{
|
||||
private static int _invocationCount;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或重置当前测试进程中的行为触发次数。
|
||||
/// </summary>
|
||||
public static int InvocationCount
|
||||
{
|
||||
get => Volatile.Read(ref _invocationCount);
|
||||
set => Volatile.Write(ref _invocationCount, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录一次行为执行,然后继续执行 generated stream invoker。
|
||||
/// </summary>
|
||||
/// <param name="message">当前流式请求消息。</param>
|
||||
/// <param name="next">下一个处理阶段。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>下游处理阶段返回的异步流。</returns>
|
||||
public IAsyncEnumerable<int> Handle(
|
||||
GeneratedStreamInvokerRequest message,
|
||||
StreamMessageHandlerDelegate<GeneratedStreamInvokerRequest, int> next,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Interlocked.Increment(ref _invocationCount);
|
||||
return next(message, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,277 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Ioc;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using GFramework.Cqrs.Extensions;
|
||||
using GFramework.Cqrs.Notification;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 验证 notification publisher 组合根注册扩展的关键行为。
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
internal sealed class NotificationPublisherRegistrationExtensionsTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证显式注册内置 <see cref="TaskWhenAllNotificationPublisher" /> 后,
|
||||
/// 标准 runtime 基础设施会复用该策略并继续调度所有处理器。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task UseTaskWhenAllNotificationPublisher_Should_Be_Used_By_Default_Runtime_Infrastructure()
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
|
||||
|
||||
var trailingHandler = new RecordingNotificationHandler();
|
||||
var container = new MicrosoftDiContainer();
|
||||
container.UseTaskWhenAllNotificationPublisher();
|
||||
container.Register<INotificationHandler<TestNotification>>(new ThrowingNotificationHandler());
|
||||
container.Register<INotificationHandler<TestNotification>>(trailingHandler);
|
||||
CqrsTestRuntime.RegisterInfrastructure(container);
|
||||
container.Freeze();
|
||||
Assert.That(container.GetAll(typeof(INotificationHandler<TestNotification>)), Has.Count.EqualTo(2));
|
||||
Assert.That(container.GetAll(typeof(INotificationPublisher)), Has.Count.EqualTo(1));
|
||||
|
||||
var context = new ArchitectureContext(container);
|
||||
var publishTask = context.PublishAsync(new TestNotification()).AsTask();
|
||||
|
||||
try
|
||||
{
|
||||
await publishTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// `TaskWhenAll` 策略会在所有处理器都结束后聚合失败;这里仅消费异常并继续断言第二个处理器已执行。
|
||||
}
|
||||
|
||||
Assert.That(trailingHandler.WasInvoked, Is.True);
|
||||
Assert.That(publishTask.Exception, Is.Not.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证显式注册内置 <see cref="SequentialNotificationPublisher" /> 后,
|
||||
/// 默认 runtime 基础设施会保留“首个失败立即停止后续处理器”的顺序语义。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void UseSequentialNotificationPublisher_Should_Preserve_Stop_On_First_Failure_Semantics()
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
|
||||
|
||||
var trailingHandler = new RecordingNotificationHandler();
|
||||
var container = new MicrosoftDiContainer();
|
||||
container.UseSequentialNotificationPublisher();
|
||||
container.Register<INotificationHandler<TestNotification>>(new ThrowingNotificationHandler());
|
||||
container.Register<INotificationHandler<TestNotification>>(trailingHandler);
|
||||
CqrsTestRuntime.RegisterInfrastructure(container);
|
||||
container.Freeze();
|
||||
Assert.That(container.GetAll(typeof(INotificationHandler<TestNotification>)), Has.Count.EqualTo(2));
|
||||
Assert.That(container.GetAll(typeof(INotificationPublisher)), Has.Count.EqualTo(1));
|
||||
|
||||
var context = new ArchitectureContext(container);
|
||||
|
||||
Assert.That(
|
||||
async () => await context.PublishAsync(new TestNotification()).ConfigureAwait(false),
|
||||
Throws.InvalidOperationException.With.Message.EqualTo("boom"));
|
||||
Assert.That(trailingHandler.WasInvoked, Is.False);
|
||||
Assert.That(container.GetRequired<INotificationPublisher>(), Is.TypeOf<SequentialNotificationPublisher>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证显式传入实例的组合根注册入口会把同一个 publisher 实例绑定到容器。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void UseNotificationPublisher_Instance_Overload_Should_Register_Same_Instance()
|
||||
{
|
||||
var container = new MicrosoftDiContainer();
|
||||
var publisher = new TrackingNotificationPublisher();
|
||||
|
||||
var returnedContainer = container.UseNotificationPublisher(publisher);
|
||||
|
||||
Assert.That(returnedContainer, Is.SameAs(container));
|
||||
Assert.That(container.Get<INotificationPublisher>(), Is.SameAs(publisher));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证泛型组合根注册入口会把指定的 publisher 类型注册为容器内唯一的单例策略。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void UseNotificationPublisher_Generic_Overload_Should_Register_Configured_Type()
|
||||
{
|
||||
var container = new MicrosoftDiContainer();
|
||||
|
||||
var returnedContainer = container.UseNotificationPublisher<TrackingNotificationPublisher>();
|
||||
container.Freeze();
|
||||
|
||||
Assert.That(returnedContainer, Is.SameAs(container));
|
||||
Assert.That(container.HasRegistration(typeof(INotificationPublisher)), Is.True);
|
||||
Assert.That(container.GetAll(typeof(INotificationPublisher)), Has.Count.EqualTo(1));
|
||||
Assert.That(container.GetRequired<INotificationPublisher>(), Is.TypeOf<TrackingNotificationPublisher>());
|
||||
Assert.That(container.GetRequired<INotificationPublisher>(), Is.SameAs(container.GetRequired<INotificationPublisher>()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当自定义 publisher 依赖其他容器服务时,泛型组合根入口仍会被默认 runtime 基础设施正确复用。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task UseNotificationPublisher_Generic_Overload_Should_Be_Used_By_Default_Runtime_Infrastructure()
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
|
||||
|
||||
var probe = new NotificationPublisherProbe();
|
||||
var handler = new RecordingNotificationHandler();
|
||||
var container = new MicrosoftDiContainer();
|
||||
container.Register(probe);
|
||||
container.UseNotificationPublisher<DependencyAwareNotificationPublisher>();
|
||||
container.Register<INotificationHandler<TestNotification>>(handler);
|
||||
CqrsTestRuntime.RegisterInfrastructure(container);
|
||||
container.Freeze();
|
||||
Assert.That(container.GetAll(typeof(INotificationHandler<TestNotification>)), Has.Count.EqualTo(1));
|
||||
Assert.That(container.GetAll(typeof(INotificationPublisher)), Has.Count.EqualTo(1));
|
||||
|
||||
var context = new ArchitectureContext(container);
|
||||
|
||||
await context.PublishAsync(new TestNotification()).ConfigureAwait(false);
|
||||
|
||||
Assert.That(probe.WasCalled, Is.True);
|
||||
Assert.That(handler.WasInvoked, Is.True);
|
||||
Assert.That(container.GetRequired<INotificationPublisher>(), Is.TypeOf<DependencyAwareNotificationPublisher>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证组合根扩展会阻止重复 notification publisher 注册,避免 runtime 创建阶段才暴露歧义。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void UseNotificationPublisher_Should_Throw_When_NotificationPublisher_Already_Registered()
|
||||
{
|
||||
var container = new MicrosoftDiContainer();
|
||||
container.UseTaskWhenAllNotificationPublisher();
|
||||
|
||||
Assert.That(
|
||||
() => container.UseNotificationPublisher(new TrackingNotificationPublisher()),
|
||||
Throws.InvalidOperationException.With.Message.Contains(nameof(INotificationPublisher)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当容器已存在 notification publisher 注册时,泛型组合根入口也会拒绝重复策略声明。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void UseNotificationPublisher_Generic_Overload_Should_Throw_When_NotificationPublisher_Already_Registered()
|
||||
{
|
||||
var container = new MicrosoftDiContainer();
|
||||
container.UseSequentialNotificationPublisher();
|
||||
|
||||
Assert.That(
|
||||
() => container.UseNotificationPublisher<TrackingNotificationPublisher>(),
|
||||
Throws.InvalidOperationException.With.Message.Contains(nameof(INotificationPublisher)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为本组测试提供最小 notification 类型。
|
||||
/// </summary>
|
||||
private sealed record TestNotification : INotification;
|
||||
|
||||
/// <summary>
|
||||
/// 记录自己是否被执行的测试处理器。
|
||||
/// </summary>
|
||||
private sealed class RecordingNotificationHandler : INotificationHandler<TestNotification>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前处理器是否至少执行过一次。
|
||||
/// </summary>
|
||||
public bool WasInvoked { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 记录执行痕迹并立刻完成。
|
||||
/// </summary>
|
||||
public ValueTask Handle(TestNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(notification);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
WasInvoked = true;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 始终抛出异常的测试处理器,用于验证并行策略不会因为首个失败而停止其余处理器。
|
||||
/// </summary>
|
||||
private sealed class ThrowingNotificationHandler : INotificationHandler<TestNotification>
|
||||
{
|
||||
/// <summary>
|
||||
/// 始终抛出测试异常。
|
||||
/// </summary>
|
||||
public ValueTask Handle(TestNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(notification);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
throw new InvalidOperationException("boom");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证实例注册重载是否保留原对象身份的测试发布器。
|
||||
/// </summary>
|
||||
private sealed class TrackingNotificationPublisher : INotificationPublisher
|
||||
{
|
||||
/// <summary>
|
||||
/// 直接完成当前 publish 调用。
|
||||
/// </summary>
|
||||
public ValueTask PublishAsync<TNotification>(
|
||||
NotificationPublishContext<TNotification> context,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TNotification : INotification
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录泛型 publisher 是否真正参与了 publish 调用的探针。
|
||||
/// </summary>
|
||||
private sealed class NotificationPublisherProbe
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取探针是否已被自定义 publisher 标记为执行过。
|
||||
/// </summary>
|
||||
public bool WasCalled { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 记录当前自定义 publisher 已接管本次通知发布。
|
||||
/// </summary>
|
||||
public void MarkCalled()
|
||||
{
|
||||
WasCalled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 依赖容器内探针服务的自定义 publisher,用于验证泛型重载确实走过了 provider 构造路径。
|
||||
/// </summary>
|
||||
private sealed class DependencyAwareNotificationPublisher(NotificationPublisherProbe 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -24,6 +24,10 @@ public static class CqrsRuntimeFactory
|
||||
/// <param name="container">目标依赖注入容器。</param>
|
||||
/// <param name="logger">用于 runtime 诊断的日志器。</param>
|
||||
/// <returns>默认 CQRS runtime。</returns>
|
||||
/// <remarks>
|
||||
/// 若调用方未显式传入 notification publisher,runtime 会在真正发布通知时优先复用容器里声明的
|
||||
/// <see cref="INotificationPublisher" />;若仍未声明,则回退到默认顺序发布器。
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <paramref name="container" /> 或 <paramref name="logger" /> 为 <see langword="null" />。
|
||||
/// </exception>
|
||||
@ -37,7 +41,10 @@ public static class CqrsRuntimeFactory
|
||||
/// </summary>
|
||||
/// <param name="container">目标依赖注入容器。</param>
|
||||
/// <param name="logger">用于 runtime 诊断的日志器。</param>
|
||||
/// <param name="notificationPublisher">可选的通知发布策略;若为 <see langword="null" /> 则使用默认顺序发布器。</param>
|
||||
/// <param name="notificationPublisher">
|
||||
/// 可选的通知发布策略;若为 <see langword="null" />,runtime 会在发布时优先尝试解析容器中已声明的
|
||||
/// <see cref="INotificationPublisher" />,否则再回退到默认顺序发布器。
|
||||
/// </param>
|
||||
/// <returns>默认 CQRS runtime。</returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <paramref name="container" /> 或 <paramref name="logger" /> 为 <see langword="null" />。
|
||||
@ -53,7 +60,7 @@ public static class CqrsRuntimeFactory
|
||||
return new CqrsDispatcher(
|
||||
container,
|
||||
logger,
|
||||
notificationPublisher ?? new SequentialNotificationPublisher());
|
||||
notificationPublisher);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -0,0 +1,112 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Ioc;
|
||||
using GFramework.Cqrs.Notification;
|
||||
|
||||
namespace GFramework.Cqrs.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// 为 CQRS runtime 提供 notification publisher 策略的组合根注册入口。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>默认 runtime 只会消费一个 <see cref="INotificationPublisher" /> 实例,因此该扩展类把“选择哪种策略”显式收敛到容器配置阶段。</para>
|
||||
/// <para>这些入口应在 runtime 创建前调用;对于走标准 <c>GFramework.Core</c> 启动路径的架构,它们会被 <c>CqrsRuntimeModule</c> 自动复用。</para>
|
||||
/// </remarks>
|
||||
public static class NotificationPublisherRegistrationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 将指定的 notification publisher 实例注册为当前容器唯一的发布策略。
|
||||
/// </summary>
|
||||
/// <param name="container">目标依赖注入容器。</param>
|
||||
/// <param name="notificationPublisher">要复用的 notification publisher 实例。</param>
|
||||
/// <returns>同一个 <paramref name="container" />,便于在组合根中继续链式配置。</returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <paramref name="container" /> 或 <paramref name="notificationPublisher" /> 为 <see langword="null" />。
|
||||
/// </exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 当前容器已存在 <see cref="INotificationPublisher" /> 注册,无法再切换为另一个策略。
|
||||
/// </exception>
|
||||
public static IIocContainer UseNotificationPublisher(
|
||||
this IIocContainer container,
|
||||
INotificationPublisher notificationPublisher)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
ArgumentNullException.ThrowIfNull(notificationPublisher);
|
||||
|
||||
ThrowIfNotificationPublisherAlreadyRegistered(container);
|
||||
container.Register(notificationPublisher);
|
||||
return container;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将指定类型的 notification publisher 注册为当前容器唯一的发布策略。
|
||||
/// </summary>
|
||||
/// <typeparam name="TNotificationPublisher">发布策略实现类型。</typeparam>
|
||||
/// <param name="container">目标依赖注入容器。</param>
|
||||
/// <returns>同一个 <paramref name="container" />,便于在组合根中继续链式配置。</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 当前容器已存在 <see cref="INotificationPublisher" /> 注册,无法再切换为另一个策略。
|
||||
/// </exception>
|
||||
public static IIocContainer UseNotificationPublisher<TNotificationPublisher>(this IIocContainer container)
|
||||
where TNotificationPublisher : class, INotificationPublisher
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
ThrowIfNotificationPublisherAlreadyRegistered(container);
|
||||
container.RegisterSingleton<INotificationPublisher, TNotificationPublisher>();
|
||||
return container;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将内置 <see cref="TaskWhenAllNotificationPublisher" /> 注册为当前容器唯一的 notification publisher 策略。
|
||||
/// </summary>
|
||||
/// <param name="container">目标依赖注入容器。</param>
|
||||
/// <returns>同一个 <paramref name="container" />,便于在组合根中继续链式配置。</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 当前容器已存在 <see cref="INotificationPublisher" /> 注册,无法再切换为另一个策略。
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// 该策略更适合“等待所有处理器完成并统一观察失败”的语义诉求;
|
||||
/// 若只是为了降低 steady-state publish 开销,应先结合实际 benchmark 结果评估是否值得切换。
|
||||
/// </remarks>
|
||||
public static IIocContainer UseTaskWhenAllNotificationPublisher(this IIocContainer container)
|
||||
{
|
||||
return UseNotificationPublisher(container, new TaskWhenAllNotificationPublisher());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将内置 <see cref="SequentialNotificationPublisher" /> 注册为当前容器唯一的 notification publisher 策略。
|
||||
/// </summary>
|
||||
/// <param name="container">目标依赖注入容器。</param>
|
||||
/// <returns>同一个 <paramref name="container" />,便于在组合根中继续链式配置。</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 当前容器已存在 <see cref="INotificationPublisher" /> 注册,无法再切换为另一个策略。
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// 该策略适合处理器之间存在顺序依赖,或调用方希望在首个失败处立即停止后续分发的场景。
|
||||
/// </remarks>
|
||||
public static IIocContainer UseSequentialNotificationPublisher(this IIocContainer container)
|
||||
{
|
||||
return UseNotificationPublisher(container, new SequentialNotificationPublisher());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在组合根阶段阻止多个 notification publisher 策略同时注册,避免 runtime 创建时出现歧义。
|
||||
/// </summary>
|
||||
/// <param name="container">当前正在配置的依赖注入容器。</param>
|
||||
/// <exception cref="InvalidOperationException">当前容器已存在 notification publisher 注册。</exception>
|
||||
private static void ThrowIfNotificationPublisherAlreadyRegistered(IIocContainer container)
|
||||
{
|
||||
if (!container.HasRegistration(typeof(INotificationPublisher)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"An {typeof(INotificationPublisher).FullName} is already registered. Remove the existing notification publisher strategy before calling {nameof(UseNotificationPublisher)} again.");
|
||||
}
|
||||
}
|
||||
@ -19,8 +19,8 @@ namespace GFramework.Cqrs;
|
||||
public interface ICqrsRequestInvokerProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 尝试为指定请求/响应类型对提供运行时元数据。
|
||||
/// </summary>
|
||||
/// 尝试为指定请求/响应类型对提供运行时元数据。
|
||||
/// </summary>
|
||||
/// <param name="requestType">请求运行时类型。</param>
|
||||
/// <param name="responseType">响应运行时类型。</param>
|
||||
/// <param name="descriptor">命中时返回的 request invoker 元数据。</param>
|
||||
|
||||
@ -19,8 +19,17 @@ namespace GFramework.Cqrs.Internal;
|
||||
internal sealed class CqrsDispatcher(
|
||||
IIocContainer container,
|
||||
ILogger logger,
|
||||
INotificationPublisher notificationPublisher) : ICqrsRuntime
|
||||
INotificationPublisher? notificationPublisher) : ICqrsRuntime
|
||||
{
|
||||
// 实例级热路径缓存:默认 runtime 在容器冻结前创建,但请求/stream 行为注册在架构生命周期内保持稳定。
|
||||
// 因此这里按 behavior service type 记住“当前 dispatcher 对应容器里是否存在该行为”,避免 0-pipeline steady-state
|
||||
// 每次 SendAsync 都重复询问容器。缓存值只反映当前 dispatcher 持有容器的注册可见性,不跨 runtime 共享。
|
||||
private readonly ConcurrentDictionary<Type, bool> _requestBehaviorPresenceCache = new();
|
||||
|
||||
// 与 request 路径相同,stream 的 behavior 注册可见性在当前 dispatcher 生命周期内保持稳定。
|
||||
// 这里缓存 “CreateStream(...) 对应 behaviorType 是否存在注册”,避免零管道 stream 每次建流都重复询问容器。
|
||||
private readonly ConcurrentDictionary<Type, bool> _streamBehaviorPresenceCache = new();
|
||||
|
||||
// 卸载安全的进程级缓存:当 generated registry 提供 request invoker 元数据时,
|
||||
// registrar 会按请求/响应类型对把它们写入这里;若类型被卸载,条目会自然失效。
|
||||
private static readonly WeakTypePairCache<GeneratedRequestInvokerMetadata>
|
||||
@ -36,8 +45,9 @@ internal sealed class CqrsDispatcher(
|
||||
private static readonly WeakKeyCache<Type, NotificationDispatchBinding>
|
||||
NotificationDispatchBindings = new();
|
||||
|
||||
// 卸载安全的进程级缓存:请求/响应类型对采用弱键缓存,避免流式消息类型被静态字典永久保留。
|
||||
private static readonly WeakTypePairCache<StreamDispatchBinding>
|
||||
// 卸载安全的进程级缓存:流式请求/响应类型对命中后复用强类型 dispatch binding 盒子,
|
||||
// 避免 stream 响应元素在热路径上退化为 object 桥接,同时仍保持弱键卸载安全语义。
|
||||
private static readonly WeakTypePairCache<StreamDispatchBindingBox>
|
||||
StreamDispatchBindings = new();
|
||||
|
||||
// 卸载安全的进程级缓存:请求/响应类型对命中后复用强类型 dispatch binding;
|
||||
@ -58,9 +68,17 @@ internal sealed class CqrsDispatcher(
|
||||
private static readonly MethodInfo StreamHandlerInvokerMethodDefinition = typeof(CqrsDispatcher)
|
||||
.GetMethod(nameof(InvokeStreamHandler), BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
|
||||
private readonly INotificationPublisher _notificationPublisher = notificationPublisher
|
||||
?? throw new ArgumentNullException(
|
||||
nameof(notificationPublisher));
|
||||
private static readonly MethodInfo StreamPipelineInvokerMethodDefinition = typeof(CqrsDispatcher)
|
||||
.GetMethod(nameof(InvokeStreamPipelineExecutor), BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
|
||||
// runtime 通常会在容器冻结前创建;此时通过实现类型注册的 notification publisher
|
||||
// 还没有被底层 provider 物化,因此不能只在构造阶段抓取一次。
|
||||
// 显式传入实例时仍优先复用该实例;否则在真正 publish 时再尝试从容器解析。
|
||||
private readonly INotificationPublisher? _notificationPublisher = notificationPublisher;
|
||||
|
||||
// 容器冻结后 notification publisher 解析结果在当前 dispatcher 生命周期内保持稳定;
|
||||
// 因此首次 publish 后缓存最终策略实例,避免后续热路径重复查容器和重复分配默认 publisher。
|
||||
private INotificationPublisher? _resolvedNotificationPublisher;
|
||||
|
||||
/// <summary>
|
||||
/// 发布通知到所有已注册处理器。
|
||||
@ -91,7 +109,7 @@ internal sealed class CqrsDispatcher(
|
||||
}
|
||||
|
||||
var publishContext = CreateNotificationPublishContext(notification, handlers, context, dispatchBinding.Invoker);
|
||||
await _notificationPublisher.PublishAsync(publishContext, cancellationToken).ConfigureAwait(false);
|
||||
await ResolveNotificationPublisher().PublishAsync(publishContext, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -102,32 +120,58 @@ internal sealed class CqrsDispatcher(
|
||||
/// <param name="request">请求对象。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>请求响应。</returns>
|
||||
public async ValueTask<TResponse> SendAsync<TResponse>(
|
||||
public ValueTask<TResponse> SendAsync<TResponse>(
|
||||
ICqrsContext context,
|
||||
IRequest<TResponse> request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
try
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var requestType = request.GetType();
|
||||
var dispatchBinding = GetRequestDispatchBinding<TResponse>(requestType);
|
||||
var handler = container.Get(dispatchBinding.HandlerType)
|
||||
?? throw new InvalidOperationException(
|
||||
$"No CQRS request handler registered for {requestType.FullName}.");
|
||||
var requestType = request.GetType();
|
||||
var dispatchBinding = GetRequestDispatchBinding<TResponse>(requestType);
|
||||
var handler = container.Get(dispatchBinding.HandlerType)
|
||||
?? throw new InvalidOperationException(
|
||||
$"No CQRS request handler registered for {requestType.FullName}.");
|
||||
|
||||
PrepareHandler(handler, context);
|
||||
var behaviors = container.GetAll(dispatchBinding.BehaviorType);
|
||||
PrepareHandler(handler, context);
|
||||
if (!HasRequestBehaviorRegistration(dispatchBinding.BehaviorType))
|
||||
{
|
||||
return dispatchBinding.RequestInvoker(handler, request, cancellationToken);
|
||||
}
|
||||
|
||||
foreach (var behavior in behaviors)
|
||||
PrepareHandler(behavior, context);
|
||||
var behaviors = container.GetAll(dispatchBinding.BehaviorType);
|
||||
|
||||
if (behaviors.Count == 0)
|
||||
return await dispatchBinding.RequestInvoker(handler, request, cancellationToken).ConfigureAwait(false);
|
||||
foreach (var behavior in behaviors)
|
||||
{
|
||||
PrepareHandler(behavior, context);
|
||||
}
|
||||
|
||||
return await dispatchBinding.GetPipelineExecutor(behaviors.Count)
|
||||
.Invoke(handler, behaviors, request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return dispatchBinding.GetPipelineExecutor(behaviors.Count)
|
||||
.Invoke(handler, behaviors, request, cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// 保留旧 async 实现的 faulted-ValueTask 失败语义,同时继续复用 direct-return 的热路径。
|
||||
return ValueTask.FromException<TResponse>(exception);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取当前 dispatcher 容器里是否存在指定 request pipeline 行为注册,并在首次命中后缓存结果。
|
||||
/// </summary>
|
||||
/// <param name="behaviorType">目标 pipeline 行为服务类型。</param>
|
||||
/// <returns>存在注册时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
private bool HasRequestBehaviorRegistration(Type behaviorType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(behaviorType);
|
||||
|
||||
return _requestBehaviorPresenceCache.GetOrAdd(
|
||||
behaviorType,
|
||||
static (cachedBehaviorType, currentContainer) => currentContainer.HasRegistration(cachedBehaviorType),
|
||||
container);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -147,17 +191,41 @@ internal sealed class CqrsDispatcher(
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var requestType = request.GetType();
|
||||
var dispatchBinding = StreamDispatchBindings.GetOrAdd(
|
||||
requestType,
|
||||
typeof(TResponse),
|
||||
static (requestType, responseType) => CreateStreamDispatchBinding(requestType, responseType));
|
||||
var dispatchBinding = GetStreamDispatchBinding<TResponse>(requestType);
|
||||
var handler = container.Get(dispatchBinding.HandlerType)
|
||||
?? throw new InvalidOperationException(
|
||||
$"No CQRS stream handler registered for {requestType.FullName}.");
|
||||
|
||||
PrepareHandler(handler, context);
|
||||
if (!HasStreamBehaviorRegistration(dispatchBinding.BehaviorType))
|
||||
{
|
||||
return dispatchBinding.StreamInvoker(handler, request, cancellationToken);
|
||||
}
|
||||
|
||||
return (IAsyncEnumerable<TResponse>)dispatchBinding.Invoker(handler, request, cancellationToken);
|
||||
var behaviors = container.GetAll(dispatchBinding.BehaviorType);
|
||||
|
||||
foreach (var behavior in behaviors)
|
||||
{
|
||||
PrepareHandler(behavior, context);
|
||||
}
|
||||
|
||||
return dispatchBinding.GetPipelineExecutor(behaviors.Count)
|
||||
.Invoke(handler, behaviors, dispatchBinding.StreamInvoker, request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取当前 dispatcher 容器里是否存在指定 stream pipeline 行为注册,并在首次命中后缓存结果。
|
||||
/// </summary>
|
||||
/// <param name="behaviorType">目标 stream pipeline 行为服务类型。</param>
|
||||
/// <returns>存在注册时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
private bool HasStreamBehaviorRegistration(Type behaviorType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(behaviorType);
|
||||
|
||||
return _streamBehaviorPresenceCache.GetOrAdd(
|
||||
behaviorType,
|
||||
static (cachedBehaviorType, currentContainer) => currentContainer.HasRegistration(cachedBehaviorType),
|
||||
container);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -177,6 +245,44 @@ internal sealed class CqrsDispatcher(
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析当前 publish 调用应使用的 notification publisher。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 显式传入实例的路径优先;若调用方只在组合根里声明了 <see cref="INotificationPublisher" /> 类型映射,
|
||||
/// 则在容器冻结后的首次 publish 才能拿到底层 provider 构造出来的实例。
|
||||
/// 若容器中仍未声明任何策略,则回退到默认顺序发布器。
|
||||
/// </remarks>
|
||||
private INotificationPublisher ResolveNotificationPublisher()
|
||||
{
|
||||
if (_notificationPublisher is not null)
|
||||
{
|
||||
return _notificationPublisher;
|
||||
}
|
||||
|
||||
var resolvedNotificationPublisher = _resolvedNotificationPublisher;
|
||||
if (resolvedNotificationPublisher is not null)
|
||||
{
|
||||
return resolvedNotificationPublisher;
|
||||
}
|
||||
|
||||
var registeredPublishers = container.GetAll(typeof(INotificationPublisher));
|
||||
resolvedNotificationPublisher = registeredPublishers.Count switch
|
||||
{
|
||||
0 => new SequentialNotificationPublisher(),
|
||||
1 => (INotificationPublisher)registeredPublishers[0],
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Multiple {typeof(INotificationPublisher).FullName} instances are registered. Remove duplicate notification publisher strategies before publishing notifications.")
|
||||
};
|
||||
|
||||
Interlocked.CompareExchange(
|
||||
ref _resolvedNotificationPublisher,
|
||||
resolvedNotificationPublisher,
|
||||
comparand: null);
|
||||
|
||||
return _resolvedNotificationPublisher;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为指定请求类型构造完整分发绑定,把服务类型与强类型调用委托一次性收敛到同一缓存项。
|
||||
/// </summary>
|
||||
@ -291,69 +397,114 @@ internal sealed class CqrsDispatcher(
|
||||
/// <summary>
|
||||
/// 为指定流式请求类型构造完整分发绑定,把服务类型与调用委托聚合到同一缓存项。
|
||||
/// </summary>
|
||||
private static StreamDispatchBinding CreateStreamDispatchBinding(Type requestType, Type responseType)
|
||||
private static StreamDispatchBinding<TResponse> CreateStreamDispatchBinding<TResponse>(Type requestType)
|
||||
{
|
||||
var generatedDescriptor = TryGetGeneratedStreamInvokerDescriptor(requestType, responseType);
|
||||
var generatedDescriptor = TryGetGeneratedStreamInvokerDescriptor<TResponse>(requestType);
|
||||
if (generatedDescriptor is not null)
|
||||
{
|
||||
var resolvedGeneratedDescriptor = generatedDescriptor.Value;
|
||||
return new StreamDispatchBinding(
|
||||
return new StreamDispatchBinding<TResponse>(
|
||||
resolvedGeneratedDescriptor.HandlerType,
|
||||
typeof(IStreamPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)),
|
||||
requestType,
|
||||
resolvedGeneratedDescriptor.Invoker);
|
||||
}
|
||||
|
||||
return new StreamDispatchBinding(
|
||||
typeof(IStreamRequestHandler<,>).MakeGenericType(requestType, responseType),
|
||||
CreateStreamInvoker(requestType, responseType));
|
||||
return new StreamDispatchBinding<TResponse>(
|
||||
typeof(IStreamRequestHandler<,>).MakeGenericType(requestType, typeof(TResponse)),
|
||||
typeof(IStreamPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)),
|
||||
requestType,
|
||||
CreateStreamInvoker<TResponse>(requestType));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定流式请求/响应类型对的 dispatch binding;若缓存未命中则按当前加载状态创建。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
|
||||
/// <param name="requestType">流式请求运行时类型。</param>
|
||||
/// <returns>当前请求/响应类型对对应的强类型 stream dispatch binding。</returns>
|
||||
private static StreamDispatchBinding<TResponse> GetStreamDispatchBinding<TResponse>(Type requestType)
|
||||
{
|
||||
var bindingBox = StreamDispatchBindings.GetOrAdd(
|
||||
requestType,
|
||||
typeof(TResponse),
|
||||
static (cachedRequestType, cachedResponseType) =>
|
||||
CreateStreamDispatchBindingBox<TResponse>(cachedRequestType, cachedResponseType));
|
||||
return bindingBox.Get<TResponse>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为弱键流式请求缓存创建强类型 binding 盒子,避免响应元素走 object 结果桥接。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
|
||||
/// <param name="requestType">流式请求运行时类型。</param>
|
||||
/// <param name="responseType">缓存命中的响应运行时类型。</param>
|
||||
/// <returns>可放入弱键缓存的强类型 binding 盒子。</returns>
|
||||
private static StreamDispatchBindingBox CreateStreamDispatchBindingBox<TResponse>(
|
||||
Type requestType,
|
||||
Type responseType)
|
||||
{
|
||||
if (responseType != typeof(TResponse))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Stream dispatch binding cache expected response type {typeof(TResponse).FullName}, but received {responseType.FullName}.");
|
||||
}
|
||||
|
||||
return StreamDispatchBindingBox.Create(CreateStreamDispatchBinding<TResponse>(requestType));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试从容器已注册的 generated stream invoker provider 中获取指定流式请求/响应类型对的元数据。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
|
||||
/// <param name="requestType">流式请求运行时类型。</param>
|
||||
/// <param name="responseType">流式响应元素类型。</param>
|
||||
/// <returns>命中时返回强类型化后的描述符;否则返回 <see langword="null" />。</returns>
|
||||
private static StreamInvokerDescriptor? TryGetGeneratedStreamInvokerDescriptor(Type requestType, Type responseType)
|
||||
private static StreamInvokerDescriptor<TResponse>? TryGetGeneratedStreamInvokerDescriptor<TResponse>(Type requestType)
|
||||
{
|
||||
return GeneratedStreamInvokers.TryGetValue(requestType, responseType, out var metadata) &&
|
||||
return GeneratedStreamInvokers.TryGetValue(requestType, typeof(TResponse), out var metadata) &&
|
||||
metadata is not null
|
||||
? CreateStreamInvokerDescriptor(requestType, responseType, metadata)
|
||||
? CreateStreamInvokerDescriptor<TResponse>(requestType, metadata)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把 provider 返回的弱类型描述符转换为 dispatcher 内部使用的 stream invoker 描述符。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
|
||||
/// <param name="requestType">流式请求运行时类型。</param>
|
||||
/// <param name="responseType">流式响应元素类型。</param>
|
||||
/// <param name="descriptor">provider 返回的弱类型描述符。</param>
|
||||
/// <returns>可直接用于创建 stream dispatch binding 的描述符。</returns>
|
||||
/// <exception cref="InvalidOperationException">当 provider 返回的委托签名与当前流式请求/响应类型对不匹配时抛出。</exception>
|
||||
private static StreamInvokerDescriptor CreateStreamInvokerDescriptor(
|
||||
private static StreamInvokerDescriptor<TResponse> CreateStreamInvokerDescriptor<TResponse>(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
GeneratedStreamInvokerMetadata descriptor)
|
||||
{
|
||||
if (!descriptor.InvokerMethod.IsStatic)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Generated CQRS stream invoker provider returned a non-static invoker method for request type {requestType.FullName} and response type {responseType.FullName}.");
|
||||
$"Generated CQRS stream invoker provider returned a non-static invoker method for request type {requestType.FullName} and response type {typeof(TResponse).FullName}.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (Delegate.CreateDelegate(typeof(StreamInvoker), descriptor.InvokerMethod) is not StreamInvoker invoker)
|
||||
if (Delegate.CreateDelegate(typeof(WeakStreamInvoker), descriptor.InvokerMethod) is not
|
||||
WeakStreamInvoker weakInvoker)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Generated CQRS stream invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {responseType.FullName}.");
|
||||
$"Generated CQRS stream invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {typeof(TResponse).FullName}.");
|
||||
}
|
||||
|
||||
return new StreamInvokerDescriptor(descriptor.HandlerType, invoker);
|
||||
// generated stream descriptor 的公开契约仍以 object 返回值暴露异步流;
|
||||
// 这里在 binding 创建时只做一次适配,把后续 CreateStream 热路径保持为强类型调用。
|
||||
var adapter = new GeneratedStreamInvokerAdapter<TResponse>(weakInvoker);
|
||||
StreamInvoker<TResponse> invoker = (handler, request, cancellationToken) =>
|
||||
adapter.Invoke(handler, request, cancellationToken);
|
||||
return new StreamInvokerDescriptor<TResponse>(descriptor.HandlerType, invoker);
|
||||
}
|
||||
catch (ArgumentException exception)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Generated CQRS stream invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {responseType.FullName}.",
|
||||
$"Generated CQRS stream invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {typeof(TResponse).FullName}.",
|
||||
exception);
|
||||
}
|
||||
}
|
||||
@ -425,11 +576,11 @@ internal sealed class CqrsDispatcher(
|
||||
/// <summary>
|
||||
/// 生成流式处理器调用委托,避免每次创建流都重复反射。
|
||||
/// </summary>
|
||||
private static StreamInvoker CreateStreamInvoker(Type requestType, Type responseType)
|
||||
private static StreamInvoker<TResponse> CreateStreamInvoker<TResponse>(Type requestType)
|
||||
{
|
||||
var method = StreamHandlerInvokerMethodDefinition
|
||||
.MakeGenericMethod(requestType, responseType);
|
||||
return (StreamInvoker)Delegate.CreateDelegate(typeof(StreamInvoker), method);
|
||||
.MakeGenericMethod(requestType, typeof(TResponse));
|
||||
return (StreamInvoker<TResponse>)Delegate.CreateDelegate(typeof(StreamInvoker<TResponse>), method);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -480,7 +631,7 @@ internal sealed class CqrsDispatcher(
|
||||
/// <summary>
|
||||
/// 执行已强类型化的流式处理器调用。
|
||||
/// </summary>
|
||||
private static object InvokeStreamHandler<TRequest, TResponse>(
|
||||
private static IAsyncEnumerable<TResponse> InvokeStreamHandler<TRequest, TResponse>(
|
||||
object handler,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
@ -491,6 +642,25 @@ internal sealed class CqrsDispatcher(
|
||||
return typedHandler.Handle(typedRequest, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行指定行为数量的强类型 stream pipeline executor。
|
||||
/// 该入口本身是缓存的固定 executor 形状;每次建流只绑定当前 handler 与 behaviors 实例。
|
||||
/// </summary>
|
||||
private static IAsyncEnumerable<TResponse> InvokeStreamPipelineExecutor<TRequest, TResponse>(
|
||||
object handler,
|
||||
IReadOnlyList<object> behaviors,
|
||||
StreamInvoker<TResponse> streamInvoker,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
where TRequest : IStreamRequest<TResponse>
|
||||
{
|
||||
var invocation = new StreamPipelineInvocation<TRequest, TResponse>(
|
||||
(IStreamRequestHandler<TRequest, TResponse>)handler,
|
||||
streamInvoker,
|
||||
behaviors);
|
||||
return invocation.Invoke((TRequest)request, cancellationToken);
|
||||
}
|
||||
|
||||
private delegate ValueTask<TResponse> RequestInvoker<TResponse>(
|
||||
object handler,
|
||||
object request,
|
||||
@ -505,7 +675,19 @@ internal sealed class CqrsDispatcher(
|
||||
private delegate ValueTask NotificationInvoker(object handler, object notification,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
private delegate object StreamInvoker(object handler, object request, CancellationToken cancellationToken);
|
||||
private delegate IAsyncEnumerable<TResponse> StreamInvoker<TResponse>(
|
||||
object handler,
|
||||
object request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
private delegate object WeakStreamInvoker(object handler, object request, CancellationToken cancellationToken);
|
||||
|
||||
private delegate IAsyncEnumerable<TResponse> StreamPipelineInvoker<TResponse>(
|
||||
object handler,
|
||||
IReadOnlyList<object> behaviors,
|
||||
StreamInvoker<TResponse> streamInvoker,
|
||||
object request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 将不同响应类型的 request dispatch binding 包装到统一弱缓存值中,
|
||||
@ -552,6 +734,72 @@ internal sealed class CqrsDispatcher(
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将不同响应类型的 stream dispatch binding 包装到统一弱缓存值中,
|
||||
/// 同时保留强类型流式委托,避免响应元素退化为 object 桥接。
|
||||
/// </summary>
|
||||
private abstract class StreamDispatchBindingBox
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建一个新的强类型 stream dispatch binding 盒子。
|
||||
/// </summary>
|
||||
public static StreamDispatchBindingBox Create<TResponse>(StreamDispatchBinding<TResponse> binding)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(binding);
|
||||
return new StreamDispatchBindingBox<TResponse>(binding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定响应类型的 stream dispatch binding。
|
||||
/// </summary>
|
||||
public abstract StreamDispatchBinding<TResponse> Get<TResponse>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存特定响应类型的 stream dispatch binding。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
|
||||
private sealed class StreamDispatchBindingBox<TResponse>(StreamDispatchBinding<TResponse> binding)
|
||||
: StreamDispatchBindingBox
|
||||
{
|
||||
private readonly StreamDispatchBinding<TResponse> _binding = binding;
|
||||
|
||||
/// <summary>
|
||||
/// 以原始强类型返回当前 binding;若请求的响应类型不匹配则抛出异常。
|
||||
/// </summary>
|
||||
public override StreamDispatchBinding<TRequestedResponse> Get<TRequestedResponse>()
|
||||
{
|
||||
if (typeof(TRequestedResponse) != typeof(TResponse))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Cached stream dispatch binding for {typeof(TResponse).FullName} cannot be used as {typeof(TRequestedResponse).FullName}.");
|
||||
}
|
||||
|
||||
return (StreamDispatchBinding<TRequestedResponse>)(object)_binding;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 generated stream provider 的弱类型开放静态入口适配为 dispatcher 内部的强类型流式委托。
|
||||
/// 适配对象与 binding 同生命周期缓存,避免在每次建流时重复创建桥接闭包。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
|
||||
private sealed class GeneratedStreamInvokerAdapter<TResponse>(WeakStreamInvoker invoker)
|
||||
{
|
||||
private readonly WeakStreamInvoker _invoker = invoker;
|
||||
|
||||
/// <summary>
|
||||
/// 调用 generated provider 暴露的弱类型入口,并把返回结果物化为当前响应类型的异步流。
|
||||
/// </summary>
|
||||
public IAsyncEnumerable<TResponse> Invoke(
|
||||
object handler,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return (IAsyncEnumerable<TResponse>)_invoker(handler, request, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存通知分发路径所需的服务类型与强类型调用委托。
|
||||
/// 该绑定把“容器解析哪个服务类型”与“如何调用处理器”聚合到同一缓存项中。
|
||||
@ -582,17 +830,53 @@ internal sealed class CqrsDispatcher(
|
||||
/// 保存流式请求分发路径所需的服务类型与调用委托。
|
||||
/// 该绑定让建流热路径只需一次缓存命中即可获得解析与调用所需元数据。
|
||||
/// </summary>
|
||||
private sealed class StreamDispatchBinding(Type handlerType, StreamInvoker invoker)
|
||||
private sealed class StreamDispatchBinding<TResponse>(
|
||||
Type handlerType,
|
||||
Type behaviorType,
|
||||
Type requestType,
|
||||
StreamInvoker<TResponse> streamInvoker)
|
||||
{
|
||||
// 线程安全:该缓存按 behaviorCount 复用 stream pipeline executor 形状,缓存项只保存委托与数量信息,
|
||||
// 不会跨建流缓存 handler 或 behavior 实例。若不同请求持续出现新的行为数量组合,字典会随之增长。
|
||||
private readonly ConcurrentDictionary<int, StreamPipelineExecutor<TResponse>> _pipelineExecutors = new();
|
||||
private readonly StreamPipelineInvoker<TResponse> _pipelineInvoker = CreateStreamPipelineInvoker<TResponse>(requestType);
|
||||
|
||||
/// <summary>
|
||||
/// 获取流式请求处理器在容器中的服务类型。
|
||||
/// </summary>
|
||||
public Type HandlerType { get; } = handlerType;
|
||||
|
||||
/// <summary>
|
||||
/// 获取 stream pipeline 行为在容器中的服务类型。
|
||||
/// </summary>
|
||||
public Type BehaviorType { get; } = behaviorType;
|
||||
|
||||
/// <summary>
|
||||
/// 获取执行流式请求处理器的调用委托。
|
||||
/// </summary>
|
||||
public StreamInvoker Invoker { get; } = invoker;
|
||||
public StreamInvoker<TResponse> StreamInvoker { get; } = streamInvoker;
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定行为数量对应的 stream pipeline executor。
|
||||
/// executor 形状会按行为数量缓存,但不会缓存 handler 或 behavior 实例。
|
||||
/// </summary>
|
||||
public StreamPipelineExecutor<TResponse> GetPipelineExecutor(int behaviorCount)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(behaviorCount);
|
||||
return _pipelineExecutors.GetOrAdd<StreamPipelineExecutorFactoryState<TResponse>>(
|
||||
behaviorCount,
|
||||
static (count, state) => CreateStreamPipelineExecutor(count, state.PipelineInvoker),
|
||||
new StreamPipelineExecutorFactoryState<TResponse>(_pipelineInvoker));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅供测试读取指定行为数量是否已存在缓存 executor。
|
||||
/// </summary>
|
||||
public object? GetPipelineExecutorForTesting(int behaviorCount)
|
||||
{
|
||||
_pipelineExecutors.TryGetValue(behaviorCount, out var executor);
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -745,9 +1029,72 @@ internal sealed class CqrsDispatcher(
|
||||
/// </summary>
|
||||
/// <param name="HandlerType">流式请求处理器在容器中的服务类型。</param>
|
||||
/// <param name="Invoker">执行流式请求处理器的调用委托。</param>
|
||||
private readonly record struct StreamInvokerDescriptor(
|
||||
private readonly record struct StreamInvokerDescriptor<TResponse>(
|
||||
Type HandlerType,
|
||||
StreamInvoker Invoker);
|
||||
StreamInvoker<TResponse> Invoker);
|
||||
|
||||
/// <summary>
|
||||
/// 为指定流式请求类型创建可跨多个 behaviorCount 复用的 typed pipeline invoker。
|
||||
/// </summary>
|
||||
private static StreamPipelineInvoker<TResponse> CreateStreamPipelineInvoker<TResponse>(Type requestType)
|
||||
{
|
||||
var method = StreamPipelineInvokerMethodDefinition
|
||||
.MakeGenericMethod(requestType, typeof(TResponse));
|
||||
return (StreamPipelineInvoker<TResponse>)Delegate.CreateDelegate(
|
||||
typeof(StreamPipelineInvoker<TResponse>),
|
||||
method);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为指定流式请求/响应类型与固定行为数量创建 pipeline executor。
|
||||
/// 行为数量用于表达缓存形状,实际建流仍会消费本次容器解析出的 handler 与 behaviors 实例。
|
||||
/// </summary>
|
||||
private static StreamPipelineExecutor<TResponse> CreateStreamPipelineExecutor<TResponse>(
|
||||
int behaviorCount,
|
||||
StreamPipelineInvoker<TResponse> invoker)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(behaviorCount);
|
||||
return new StreamPipelineExecutor<TResponse>(behaviorCount, invoker);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存固定行为数量下的 typed stream pipeline executor 形状。
|
||||
/// 该对象自身可跨建流复用,但每次调用都只绑定当前 handler 与 behavior 实例。
|
||||
/// </summary>
|
||||
private sealed class StreamPipelineExecutor<TResponse>(
|
||||
int behaviorCount,
|
||||
StreamPipelineInvoker<TResponse> invoker)
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取此 executor 预期处理的行为数量。
|
||||
/// </summary>
|
||||
public int BehaviorCount { get; } = behaviorCount;
|
||||
|
||||
/// <summary>
|
||||
/// 使用当前 handler / behaviors / request 执行缓存的 pipeline 形状。
|
||||
/// </summary>
|
||||
public IAsyncEnumerable<TResponse> Invoke(
|
||||
object handler,
|
||||
IReadOnlyList<object> behaviors,
|
||||
StreamInvoker<TResponse> streamInvoker,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (behaviors.Count != BehaviorCount)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Cached stream pipeline executor expected {BehaviorCount} behaviors, but received {behaviors.Count}.");
|
||||
}
|
||||
|
||||
return invoker(handler, behaviors, streamInvoker, request, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 stream pipeline executor 缓存携带 typed pipeline invoker,避免按行为数量建缓存时创建闭包。
|
||||
/// </summary>
|
||||
private readonly record struct StreamPipelineExecutorFactoryState<TResponse>(
|
||||
StreamPipelineInvoker<TResponse> PipelineInvoker);
|
||||
|
||||
/// <summary>
|
||||
/// 供 registrar 在 generated registry 激活后登记 request invoker 元数据。
|
||||
@ -878,4 +1225,90 @@ internal sealed class CqrsDispatcher(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存单次 stream pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。
|
||||
/// 该对象只存在于本次建流,不会跨请求保留容器解析出的实例。
|
||||
/// </summary>
|
||||
private sealed class StreamPipelineInvocation<TRequest, TResponse>(
|
||||
IStreamRequestHandler<TRequest, TResponse> handler,
|
||||
StreamInvoker<TResponse> streamInvoker,
|
||||
IReadOnlyList<object> behaviors)
|
||||
where TRequest : IStreamRequest<TResponse>
|
||||
{
|
||||
private readonly IStreamRequestHandler<TRequest, TResponse> _handler = handler;
|
||||
private readonly StreamInvoker<TResponse> _streamInvoker = streamInvoker;
|
||||
private readonly IReadOnlyList<object> _behaviors = behaviors;
|
||||
private readonly StreamMessageHandlerDelegate<TRequest, TResponse>?[] _continuations =
|
||||
new StreamMessageHandlerDelegate<TRequest, TResponse>?[behaviors.Count + 1];
|
||||
|
||||
/// <summary>
|
||||
/// 从 stream pipeline 起点开始创建异步响应序列。
|
||||
/// </summary>
|
||||
public IAsyncEnumerable<TResponse> Invoke(TRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return GetContinuation(0)(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定阶段的 continuation,并在首次请求时为该阶段绑定一次不可变调用入口。
|
||||
/// 同一行为多次调用 <c>next</c> 时会命中相同 continuation,保持与 request pipeline 一致的链式语义。
|
||||
/// 线程模型上,该缓存仅假定单次建流链按顺序推进;若某个 behavior 并发调用多个 <c>next</c>,
|
||||
/// 这里可能重复创建等价 continuation,但不会跨建流共享,也不会缓存容器解析出的实例。
|
||||
/// </summary>
|
||||
private StreamMessageHandlerDelegate<TRequest, TResponse> GetContinuation(int index)
|
||||
{
|
||||
var continuation = _continuations[index];
|
||||
if (continuation is not null)
|
||||
{
|
||||
return continuation;
|
||||
}
|
||||
|
||||
continuation = index == _behaviors.Count
|
||||
? InvokeHandler
|
||||
: new StreamPipelineContinuation<TRequest, TResponse>(this, index).Invoke;
|
||||
_continuations[index] = continuation;
|
||||
return continuation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行指定索引的 stream pipeline behavior。
|
||||
/// </summary>
|
||||
private IAsyncEnumerable<TResponse> InvokeBehavior(
|
||||
int index,
|
||||
TRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var behavior = (IStreamPipelineBehavior<TRequest, TResponse>)_behaviors[index];
|
||||
return behavior.Handle(request, GetContinuation(index + 1), cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调用最终流式请求处理器。
|
||||
/// </summary>
|
||||
private IAsyncEnumerable<TResponse> InvokeHandler(TRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return _streamInvoker(_handler, request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将固定阶段索引绑定为标准 <see cref="StreamMessageHandlerDelegate{TRequest,TResponse}" />。
|
||||
/// 该包装只在单次建流生命周期内存在,用于把缓存 shape 套入当前实例。
|
||||
/// </summary>
|
||||
private sealed class StreamPipelineContinuation<TCurrentRequest, TCurrentResponse>(
|
||||
StreamPipelineInvocation<TCurrentRequest, TCurrentResponse> invocation,
|
||||
int index)
|
||||
where TCurrentRequest : IStreamRequest<TCurrentResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行当前阶段并跳转到下一个 continuation。
|
||||
/// </summary>
|
||||
public IAsyncEnumerable<TCurrentResponse> Invoke(
|
||||
TCurrentRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return invocation.InvokeBehavior(index, request, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,13 @@ namespace GFramework.Cqrs.Internal;
|
||||
/// </summary>
|
||||
internal static class CqrsHandlerRegistrar
|
||||
{
|
||||
/// <summary>
|
||||
/// 描述 generated invoker descriptor 在 registrar 预热阶段使用的 request/response 类型对键。
|
||||
/// </summary>
|
||||
/// <param name="RequestType">请求运行时类型。</param>
|
||||
/// <param name="ResponseType">响应运行时类型。</param>
|
||||
private readonly record struct InvokerDescriptorKey(Type RequestType, Type ResponseType);
|
||||
|
||||
// 卸载安全的进程级缓存:程序集元数据只按弱键复用。
|
||||
// 若程序集来自 collectible AssemblyLoadContext,被回收后会重新分析,而不会被静态缓存永久钉住。
|
||||
private static readonly WeakKeyCache<Assembly, AssemblyRegistrationMetadata> AssemblyMetadataCache =
|
||||
@ -68,6 +75,36 @@ internal static class CqrsHandlerRegistrar
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接激活并注册单个 generated registry,避免调用方为了只接入一个 benchmark registry
|
||||
/// 而额外扫描同一程序集里的其他 registry / handler。
|
||||
/// </summary>
|
||||
/// <param name="container">承载 generated registry 注册结果的目标容器。</param>
|
||||
/// <param name="registryType">要直接激活的 generated registry 类型。</param>
|
||||
/// <param name="logger">当前注册过程使用的日志记录器。</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <paramref name="container" />、<paramref name="registryType" /> 或 <paramref name="logger" /> 为 <see langword="null" />。
|
||||
/// </exception>
|
||||
/// <exception cref="InvalidOperationException">指定 registry 类型不满足 generated registry 运行时契约。</exception>
|
||||
internal static void RegisterGeneratedRegistry(
|
||||
IIocContainer container,
|
||||
Type registryType,
|
||||
ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
ArgumentNullException.ThrowIfNull(registryType);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
var assemblyName = GetAssemblySortKey(registryType.Assembly);
|
||||
if (!TryCreateGeneratedRegistry(registryType, assemblyName, logger, out var registry))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Unable to activate generated CQRS handler registry {registryType.FullName} in assembly {assemblyName}.");
|
||||
}
|
||||
|
||||
RegisterGeneratedRegistries(container.GetServicesUnsafe, [registry], assemblyName, logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 优先使用程序集级源码生成注册器完成 CQRS 映射注册。
|
||||
/// </summary>
|
||||
@ -291,8 +328,49 @@ internal static class CqrsHandlerRegistrar
|
||||
if (provider is not IEnumeratesCqrsRequestInvokerDescriptors descriptorSource)
|
||||
return;
|
||||
|
||||
foreach (var descriptorEntry in descriptorSource.GetDescriptors())
|
||||
IReadOnlyList<CqrsRequestInvokerDescriptorEntry>? descriptors;
|
||||
try
|
||||
{
|
||||
descriptors = descriptorSource.GetDescriptors();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.Warn(
|
||||
$"Failed to enumerate generated CQRS request invoker descriptors from provider {provider.GetType().FullName} in assembly {assemblyName}. Falling back to runtime reflection for request invokers: {exception.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (descriptors is null)
|
||||
{
|
||||
logger.Warn(
|
||||
$"Ignoring generated CQRS request invoker descriptors from provider {provider.GetType().FullName} in assembly {assemblyName} because GetDescriptors() returned null.");
|
||||
return;
|
||||
}
|
||||
|
||||
var registeredKeys = new HashSet<InvokerDescriptorKey>();
|
||||
foreach (var descriptorEntry in descriptors)
|
||||
{
|
||||
if (descriptorEntry is null)
|
||||
{
|
||||
logger.Warn(
|
||||
$"Ignoring null generated CQRS request invoker descriptor entry from provider {provider.GetType().FullName} in assembly {assemblyName}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var descriptorKey = new InvokerDescriptorKey(
|
||||
descriptorEntry.RequestType,
|
||||
descriptorEntry.ResponseType);
|
||||
|
||||
if (!TryValidateEnumeratedRequestInvokerDescriptor(provider, descriptorEntry, assemblyName, logger))
|
||||
continue;
|
||||
|
||||
if (!registeredKeys.Add(descriptorKey))
|
||||
{
|
||||
logger.Warn(
|
||||
$"Ignoring duplicate generated CQRS request invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
CqrsDispatcher.RegisterGeneratedRequestInvokerDescriptor(
|
||||
descriptorEntry.RequestType,
|
||||
descriptorEntry.ResponseType,
|
||||
@ -346,8 +424,49 @@ internal static class CqrsHandlerRegistrar
|
||||
if (provider is not IEnumeratesCqrsStreamInvokerDescriptors descriptorSource)
|
||||
return;
|
||||
|
||||
foreach (var descriptorEntry in descriptorSource.GetDescriptors())
|
||||
IReadOnlyList<CqrsStreamInvokerDescriptorEntry>? descriptors;
|
||||
try
|
||||
{
|
||||
descriptors = descriptorSource.GetDescriptors();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.Warn(
|
||||
$"Failed to enumerate generated CQRS stream invoker descriptors from provider {provider.GetType().FullName} in assembly {assemblyName}. Falling back to runtime reflection for stream invokers: {exception.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (descriptors is null)
|
||||
{
|
||||
logger.Warn(
|
||||
$"Ignoring generated CQRS stream invoker descriptors from provider {provider.GetType().FullName} in assembly {assemblyName} because GetDescriptors() returned null.");
|
||||
return;
|
||||
}
|
||||
|
||||
var registeredKeys = new HashSet<InvokerDescriptorKey>();
|
||||
foreach (var descriptorEntry in descriptors)
|
||||
{
|
||||
if (descriptorEntry is null)
|
||||
{
|
||||
logger.Warn(
|
||||
$"Ignoring null generated CQRS stream invoker descriptor entry from provider {provider.GetType().FullName} in assembly {assemblyName}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var descriptorKey = new InvokerDescriptorKey(
|
||||
descriptorEntry.RequestType,
|
||||
descriptorEntry.ResponseType);
|
||||
|
||||
if (!TryValidateEnumeratedStreamInvokerDescriptor(provider, descriptorEntry, assemblyName, logger))
|
||||
continue;
|
||||
|
||||
if (!registeredKeys.Add(descriptorKey))
|
||||
{
|
||||
logger.Warn(
|
||||
$"Ignoring duplicate generated CQRS stream invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
CqrsDispatcher.RegisterGeneratedStreamInvokerDescriptor(
|
||||
descriptorEntry.RequestType,
|
||||
descriptorEntry.ResponseType,
|
||||
@ -357,6 +476,96 @@ internal static class CqrsHandlerRegistrar
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验 request descriptor 枚举项是否与 provider 的显式查询结果保持一致。
|
||||
/// </summary>
|
||||
/// <param name="provider">当前正在预热的 request invoker provider。</param>
|
||||
/// <param name="descriptorEntry">当前枚举到的描述符条目。</param>
|
||||
/// <param name="assemblyName">当前程序集的稳定名称。</param>
|
||||
/// <param name="logger">日志记录器。</param>
|
||||
/// <returns>当该枚举项可安全写入 dispatcher 缓存时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
private static bool TryValidateEnumeratedRequestInvokerDescriptor(
|
||||
ICqrsRequestInvokerProvider provider,
|
||||
CqrsRequestInvokerDescriptorEntry descriptorEntry,
|
||||
string assemblyName,
|
||||
ILogger logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!provider.TryGetDescriptor(
|
||||
descriptorEntry.RequestType,
|
||||
descriptorEntry.ResponseType,
|
||||
out var resolvedDescriptor) ||
|
||||
resolvedDescriptor is null)
|
||||
{
|
||||
logger.Warn(
|
||||
$"Ignoring generated CQRS request invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName} because TryGetDescriptor did not return a matching descriptor.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!resolvedDescriptor.InvokerMethod.Equals(descriptorEntry.Descriptor.InvokerMethod) ||
|
||||
resolvedDescriptor.HandlerType != descriptorEntry.Descriptor.HandlerType)
|
||||
{
|
||||
logger.Warn(
|
||||
$"Ignoring generated CQRS request invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName} because the enumerated descriptor does not match TryGetDescriptor.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.Warn(
|
||||
$"Ignoring generated CQRS request invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName} because TryGetDescriptor threw: {exception.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验 stream descriptor 枚举项是否与 provider 的显式查询结果保持一致。
|
||||
/// </summary>
|
||||
/// <param name="provider">当前正在预热的 stream invoker provider。</param>
|
||||
/// <param name="descriptorEntry">当前枚举到的描述符条目。</param>
|
||||
/// <param name="assemblyName">当前程序集的稳定名称。</param>
|
||||
/// <param name="logger">日志记录器。</param>
|
||||
/// <returns>当该枚举项可安全写入 dispatcher 缓存时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
private static bool TryValidateEnumeratedStreamInvokerDescriptor(
|
||||
ICqrsStreamInvokerProvider provider,
|
||||
CqrsStreamInvokerDescriptorEntry descriptorEntry,
|
||||
string assemblyName,
|
||||
ILogger logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!provider.TryGetDescriptor(
|
||||
descriptorEntry.RequestType,
|
||||
descriptorEntry.ResponseType,
|
||||
out var resolvedDescriptor) ||
|
||||
resolvedDescriptor is null)
|
||||
{
|
||||
logger.Warn(
|
||||
$"Ignoring generated CQRS stream invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName} because TryGetDescriptor did not return a matching descriptor.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!resolvedDescriptor.InvokerMethod.Equals(descriptorEntry.Descriptor.InvokerMethod) ||
|
||||
resolvedDescriptor.HandlerType != descriptorEntry.Descriptor.HandlerType)
|
||||
{
|
||||
logger.Warn(
|
||||
$"Ignoring generated CQRS stream invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName} because the enumerated descriptor does not match TryGetDescriptor.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.Warn(
|
||||
$"Ignoring generated CQRS stream invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName} because TryGetDescriptor threw: {exception.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 generated registry 的 fallback 元数据转换为统一的注册结果,并记录下一阶段是定向补扫还是整程序集扫描。
|
||||
/// </summary>
|
||||
|
||||
@ -2,18 +2,17 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using GFramework.Cqrs.Notification;
|
||||
|
||||
namespace GFramework.Cqrs.Internal;
|
||||
namespace GFramework.Cqrs.Notification;
|
||||
|
||||
/// <summary>
|
||||
/// 默认的通知发布器实现。
|
||||
/// 以内置顺序策略逐个分发通知处理器。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该实现完整保留当前 CQRS runtime 的既有通知语义:按已解析顺序逐个执行处理器,
|
||||
/// 并在首个处理器抛出异常时立即停止后续发布。
|
||||
/// <para>该实现完整保留默认 CQRS runtime 的既有通知语义:按已解析顺序逐个执行处理器。</para>
|
||||
/// <para>当任意处理器抛出异常时,后续处理器不会继续执行,因此更适合存在顺序依赖或希望尽早暴露首个失败的场景。</para>
|
||||
/// </remarks>
|
||||
internal sealed class SequentialNotificationPublisher : INotificationPublisher
|
||||
public sealed class SequentialNotificationPublisher : INotificationPublisher
|
||||
{
|
||||
/// <summary>
|
||||
/// 按既定顺序逐个执行当前通知的处理器。
|
||||
@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Notification;
|
||||
|
||||
/// <summary>
|
||||
/// 以内置 <c>Task.WhenAll(...)</c> 策略并行分发通知处理器。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>该实现会先为当前发布调用中的每个处理器创建独立执行任务,再等待全部任务完成。</para>
|
||||
/// <para>它不会保留默认顺序发布器的“首个异常立即停止”语义;如果多个处理器失败,返回任务会聚合这些异常。</para>
|
||||
/// <para>适合处理器之间互不依赖,且调用方更关心总耗时而不是处理顺序的场景。</para>
|
||||
/// </remarks>
|
||||
public sealed class TaskWhenAllNotificationPublisher : INotificationPublisher
|
||||
{
|
||||
/// <summary>
|
||||
/// 并行启动当前通知的所有处理器,并等待它们全部结束。
|
||||
/// </summary>
|
||||
/// <typeparam name="TNotification">通知类型。</typeparam>
|
||||
/// <param name="context">当前发布调用的执行上下文。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>表示所有处理器都已完成的值任务。</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="context" /> 为 <see langword="null" />。</exception>
|
||||
public ValueTask PublishAsync<TNotification>(
|
||||
NotificationPublishContext<TNotification> context,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TNotification : INotification
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
return context.Handlers.Count switch
|
||||
{
|
||||
0 => ValueTask.CompletedTask,
|
||||
1 => context.InvokeHandlerAsync(context.Handlers[0], cancellationToken),
|
||||
_ => PublishCoreAsync(context, cancellationToken)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为多处理器场景建立并行等待,确保单个处理器的同步异常也会被收敛到返回任务中。
|
||||
/// </summary>
|
||||
private static async ValueTask PublishCoreAsync<TNotification>(
|
||||
NotificationPublishContext<TNotification> context,
|
||||
CancellationToken cancellationToken)
|
||||
where TNotification : INotification
|
||||
{
|
||||
var tasks = new Task[context.Handlers.Count];
|
||||
|
||||
for (var index = 0; index < context.Handlers.Count; index++)
|
||||
{
|
||||
tasks[index] = InvokeHandlerSafelyAsync(context, context.Handlers[index], cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过异步包装把同步抛出的处理器异常也转换成可聚合的任务结果。
|
||||
/// </summary>
|
||||
private static async ValueTask InvokeHandlerSafelyAsync<TNotification>(
|
||||
NotificationPublishContext<TNotification> context,
|
||||
object handler,
|
||||
CancellationToken cancellationToken)
|
||||
where TNotification : INotification
|
||||
{
|
||||
await context.InvokeHandlerAsync(handler, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
7
GFramework.Cqrs/Properties/AssemblyInfo.cs
Normal file
7
GFramework.Cqrs/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,7 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("GFramework.Cqrs.Tests")]
|
||||
[assembly: InternalsVisibleTo("GFramework.Cqrs.Benchmarks")]
|
||||
@ -50,6 +50,7 @@
|
||||
- `CqrsRuntimeFactory.cs`
|
||||
- `Internal/CqrsDispatcher.cs`
|
||||
- `Notification/INotificationPublisher.cs`
|
||||
- `Notification/TaskWhenAllNotificationPublisher.cs`
|
||||
- `Internal/CqrsHandlerRegistrar.cs`
|
||||
- `Internal/DefaultCqrsHandlerRegistrar.cs`
|
||||
- `Internal/DefaultCqrsRegistrationService.cs`
|
||||
@ -71,7 +72,7 @@ dotnet add package GeWuYou.GFramework.Cqrs
|
||||
dotnet add package GeWuYou.GFramework.Cqrs.Abstractions
|
||||
```
|
||||
|
||||
如果你希望减少处理器注册时的反射扫描,再额外安装:
|
||||
如果你希望把可静态表达的 handler 注册与 request / stream invoker 元数据前移到编译期,再额外安装:
|
||||
|
||||
```bash
|
||||
dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
|
||||
@ -115,7 +116,9 @@ using GFramework.Cqrs.Extensions;
|
||||
var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInput("Alice")));
|
||||
```
|
||||
|
||||
在 `ArchitectureContext` 上也可以直接使用统一 CQRS 入口,例如 `SendRequestAsync`、`SendQueryAsync`、`PublishAsync` 和 `CreateStream`。
|
||||
在 `ArchitectureContext` 上也可以直接使用统一 CQRS 入口,例如 `SendRequestAsync`、`SendAsync`、`SendQueryAsync`、`PublishAsync` 和 `CreateStream`。
|
||||
|
||||
如果你走标准 `GFramework.Core` 架构启动路径,`CqrsRuntimeModule` 会自动创建 runtime 并接线默认注册流程;只有在裸容器、测试宿主或自定义组合根里,才需要显式补齐 runtime、publisher 策略或额外程序集注册。
|
||||
|
||||
## 运行时行为
|
||||
|
||||
@ -124,17 +127,65 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu
|
||||
- 未找到处理器会抛出异常。
|
||||
- 通知分发
|
||||
- 通知会分发给所有已注册 `INotificationHandler<>`;零处理器时默认静默完成。
|
||||
- 默认通知发布器会按容器解析顺序逐个执行处理器,并在首个处理器抛出异常时立即停止后续分发。
|
||||
- 若容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置顺序发布器。
|
||||
- 若容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置 `SequentialNotificationPublisher`。
|
||||
- notification publish 不存在 generated invoker 通道;它始终基于当前已注册的 `INotificationHandler<>` 集合和选定的 `INotificationPublisher` 策略执行。
|
||||
- 默认 runtime 只消费一个 `INotificationPublisher`;如果容器里已经存在该注册,再调用 `UseNotificationPublisher(...)`、`UseNotificationPublisher<TPublisher>()`、`UseSequentialNotificationPublisher()` 或 `UseTaskWhenAllNotificationPublisher()` 会直接报错,而不是按“后注册覆盖前注册”处理。
|
||||
- 内置 notification publisher 的推荐选择如下:
|
||||
|
||||
| 策略 | 推荐场景 | 执行顺序 | 失败语义 | 备注 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `SequentialNotificationPublisher` | 需要保持容器顺序,且希望首个失败立即停止后续分发 | 保证按容器解析顺序逐个执行 | 首个处理器抛出异常时立即停止 | 也是默认回退策略 |
|
||||
| `TaskWhenAllNotificationPublisher` | 需要让全部处理器并行完成,并在结束后统一观察失败或取消 | 不保证顺序 | 不会在首个失败时停止其余处理器;会聚合最终异常或取消结果 | 更适合语义补齐,不是性能开关 |
|
||||
| `UseNotificationPublisher(...)` / `UseNotificationPublisher<TPublisher>()` | 需要接入仓库外的自定义策略或第三方策略 | 取决于具体实现 | 取决于具体实现 | 前者复用现成实例,后者让容器负责单例生命周期 |
|
||||
|
||||
- 若只是为了降低 fixed fan-out publish 的 steady-state 成本,当前 benchmark 并不表明 `TaskWhenAllNotificationPublisher` 会优于默认顺序发布器;它更适合你需要“等待全部处理器完成并统一观察失败”的场景,而不是把 publish 切成另一条 generated 或更快的分发通道。
|
||||
|
||||
如果你需要显式保留默认顺序语义,也可以在组合根里直接声明:
|
||||
|
||||
```csharp
|
||||
using GFramework.Cqrs.Extensions;
|
||||
|
||||
container.UseSequentialNotificationPublisher();
|
||||
```
|
||||
|
||||
如果你需要切换到内置并行 notification publisher,推荐在组合根里显式声明这条策略:
|
||||
|
||||
```csharp
|
||||
using GFramework.Cqrs.Extensions;
|
||||
|
||||
container.UseTaskWhenAllNotificationPublisher();
|
||||
```
|
||||
|
||||
如果你确实需要自定义 publisher 实例,也可以继续显式注册:
|
||||
|
||||
```csharp
|
||||
using GFramework.Cqrs.Extensions;
|
||||
using GFramework.Cqrs.Notification;
|
||||
|
||||
container.UseNotificationPublisher(new TaskWhenAllNotificationPublisher());
|
||||
```
|
||||
|
||||
如果你希望由容器负责创建并长期复用自定义 publisher,也可以改用泛型重载:
|
||||
|
||||
```csharp
|
||||
using GFramework.Cqrs.Extensions;
|
||||
|
||||
container.UseNotificationPublisher<MyCustomNotificationPublisher>();
|
||||
```
|
||||
|
||||
对于走标准 `GFramework.Core` 启动路径的架构,这些组合根扩展会被默认基础设施自动复用;如果你直接调用 `CqrsRuntimeFactory.CreateRuntime(...)`,也仍然可以像以前一样显式传入 publisher 实例。
|
||||
- 流式请求
|
||||
- 通过 `IStreamRequest<TResponse>` 和 `IStreamRequestHandler<,>` 返回 `IAsyncEnumerable<TResponse>`。
|
||||
- 当消费端程序集提供 generated stream invoker provider / descriptor 后,runtime 会优先消费这组 stream invoker 元数据;未命中时仍回退到既有反射 stream binding 创建路径。
|
||||
- 所有已注册 `IStreamPipelineBehavior<TRequest, TResponse>` 会在建流阶段包裹对应 stream handler;默认实现不拦截每个元素,而是围绕单次 `CreateStream(...)` 调用编排行为链。
|
||||
- 上下文注入
|
||||
- 处理器基类继承 `CqrsContextAwareHandlerBase`,runtime 会在分发前注入当前 `IArchitectureContext`。
|
||||
- 如果处理器或行为需要上下文注入,而当前 `ICqrsContext` 不是 `IArchitectureContext`,默认实现会抛出异常。
|
||||
- 管道行为
|
||||
- 所有已注册 `IPipelineBehavior<TRequest, TResponse>` 会包裹请求处理器执行。
|
||||
- 当前包内提供了 `LoggingBehavior` 和 `PerformanceBehavior` 两个可复用行为。
|
||||
- 所有已注册 `IStreamPipelineBehavior<TRequest, TResponse>` 会包裹流式请求处理器执行。
|
||||
- 注册入口分别为 `RegisterCqrsPipelineBehavior<TBehavior>()` 与 `RegisterCqrsStreamPipelineBehavior<TBehavior>()`。
|
||||
- 当前包内提供了 `LoggingBehavior` 和 `PerformanceBehavior` 两个可复用 request 行为;stream 行为需要按业务需求自行实现。
|
||||
|
||||
## 处理器注册与程序集接入
|
||||
|
||||
@ -144,18 +195,20 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu
|
||||
- 优先尝试消费端程序集上的 `ICqrsHandlerRegistry` 生成注册器。
|
||||
- 当生成注册器同时暴露 generated request invoker provider 或 generated stream invoker provider 时,registrar 会把对应 descriptor 元数据接线到 runtime 缓存。
|
||||
- 生成注册器不可用或元数据损坏时,记录告警并回退到反射扫描。
|
||||
- generated invoker 只覆盖 request 与 stream 两类单次分发元数据;`INotificationHandler<>` 仍然只参与 registry / fallback 注册,通知分发本身继续由 runtime 解析出的 handler 集合和 `INotificationPublisher` 策略决定。
|
||||
- 当程序集声明了 `CqrsReflectionFallbackAttribute` 时,运行时会先执行生成注册器,再只补它未覆盖的 handler。
|
||||
- `CqrsReflectionFallbackAttribute` 现在可以多次声明,并同时承载 `Type[]` 与 `string[]` 两类 fallback 清单。
|
||||
- 运行时会优先复用 fallback 特性里直接提供的 `Type` 条目,只对字符串条目执行定向 `Assembly.GetType(...)` 查找;只有旧版空 marker 才会退回整程序集扫描。
|
||||
- `CqrsReflectionFallbackAttribute` 可以多次声明,并同时承载 `Type[]` 与 `string[]` 两类 fallback 清单。
|
||||
- 运行时会优先复用 fallback 特性里直接提供的 `Type` 条目,只对字符串条目执行定向 `Assembly.GetType(...)` 查找;只有旧版空 marker、空 fallback 元数据,或生成注册器整体不可用时,才会退回整程序集扫描。
|
||||
- 处理器以 transient 方式注册,避免上下文感知处理器在并发请求间共享可变上下文。
|
||||
|
||||
如果你走标准 `GFramework.Core` 架构初始化路径,这些步骤通常由框架自动完成;裸容器或测试环境则需要显式补齐 runtime 与注册入口。
|
||||
|
||||
## 适用边界
|
||||
|
||||
- 这个包是默认实现,不是“纯契约包”。
|
||||
- 这个包是默认实现,不是“纯契约包”;如果你只需要共享请求/处理器契约,请停在 `GeWuYou.GFramework.Cqrs.Abstractions`。
|
||||
- 处理器基类依赖 runtime 在分发前注入上下文,不适合脱离 dispatcher 直接手动实例化后调用。
|
||||
- README 中的消息基类和 handler 基类位于 `GFramework.Cqrs`,接口契约位于 `GFramework.Cqrs.Abstractions`;最小示例通常需要同时引入这两个命名空间层级。
|
||||
- 如果你的目标只是“先用起来”,优先沿用 `ArchitectureContext` / `IContextAware` 的统一入口;只有在需要更换通知策略、接入额外程序集或搭裸容器测试时,再显式配置组合根。
|
||||
|
||||
## 文档入口
|
||||
|
||||
|
||||
52
GFramework.Game.Abstractions/Input/IInputBindingStore.cs
Normal file
52
GFramework.Game.Abstractions/Input/IInputBindingStore.cs
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
namespace GFramework.Game.Abstractions.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 定义逻辑动作绑定的查询、修改与快照导入导出契约。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该接口承担框架输入系统的持久化与重绑定边界。
|
||||
/// 宿主层可以把自己的原生输入系统适配到这里,上层业务则只依赖动作名和绑定描述,不直接接触宿主输入事件。
|
||||
/// </remarks>
|
||||
public interface IInputBindingStore
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取指定动作的当前绑定。
|
||||
/// </summary>
|
||||
/// <param name="actionName">动作名称。</param>
|
||||
/// <returns>动作绑定快照。</returns>
|
||||
InputActionBinding GetBindings(string actionName);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有动作的当前绑定快照。
|
||||
/// </summary>
|
||||
/// <returns>全量输入绑定快照。</returns>
|
||||
InputBindingSnapshot ExportSnapshot();
|
||||
|
||||
/// <summary>
|
||||
/// 使用给定快照替换当前绑定。
|
||||
/// </summary>
|
||||
/// <param name="snapshot">要导入的快照。</param>
|
||||
void ImportSnapshot(InputBindingSnapshot snapshot);
|
||||
|
||||
/// <summary>
|
||||
/// 把指定绑定设置为动作的主绑定。
|
||||
/// </summary>
|
||||
/// <param name="actionName">动作名称。</param>
|
||||
/// <param name="binding">新绑定。</param>
|
||||
/// <param name="swapIfTaken">是否在冲突时交换已占用绑定。</param>
|
||||
void SetPrimaryBinding(string actionName, InputBindingDescriptor binding, bool swapIfTaken = true);
|
||||
|
||||
/// <summary>
|
||||
/// 将指定动作恢复为默认绑定。
|
||||
/// </summary>
|
||||
/// <param name="actionName">动作名称。</param>
|
||||
void ResetAction(string actionName);
|
||||
|
||||
/// <summary>
|
||||
/// 将所有动作恢复为默认绑定。
|
||||
/// </summary>
|
||||
void ResetAll();
|
||||
}
|
||||
15
GFramework.Game.Abstractions/Input/IInputDeviceTracker.cs
Normal file
15
GFramework.Game.Abstractions/Input/IInputDeviceTracker.cs
Normal file
@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
namespace GFramework.Game.Abstractions.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 定义当前活跃输入设备上下文的查询入口。
|
||||
/// </summary>
|
||||
public interface IInputDeviceTracker
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前输入设备上下文。
|
||||
/// </summary>
|
||||
InputDeviceContext CurrentDevice { get; }
|
||||
}
|
||||
20
GFramework.Game.Abstractions/Input/IUiInputActionMap.cs
Normal file
20
GFramework.Game.Abstractions/Input/IUiInputActionMap.cs
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Game.Abstractions.UI;
|
||||
|
||||
namespace GFramework.Game.Abstractions.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 定义逻辑动作名到 UI 语义动作的映射规则。
|
||||
/// </summary>
|
||||
public interface IUiInputActionMap
|
||||
{
|
||||
/// <summary>
|
||||
/// 尝试把逻辑动作映射为 UI 语义动作。
|
||||
/// </summary>
|
||||
/// <param name="actionName">逻辑动作名称。</param>
|
||||
/// <param name="action">映射出的 UI 语义动作。</param>
|
||||
/// <returns>如果映射成功则返回 <see langword="true" />。</returns>
|
||||
bool TryMap(string actionName, out UiInputAction action);
|
||||
}
|
||||
17
GFramework.Game.Abstractions/Input/IUiInputDispatcher.cs
Normal file
17
GFramework.Game.Abstractions/Input/IUiInputDispatcher.cs
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
namespace GFramework.Game.Abstractions.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 定义面向 UI 语义动作的输入分发入口。
|
||||
/// </summary>
|
||||
public interface IUiInputDispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// 尝试把逻辑动作分发到当前 UI 路由。
|
||||
/// </summary>
|
||||
/// <param name="actionName">逻辑动作名称。</param>
|
||||
/// <returns>如果该动作被映射为 UI 动作并成功分发,则返回 <see langword="true" />。</returns>
|
||||
bool TryDispatch(string actionName);
|
||||
}
|
||||
37
GFramework.Game.Abstractions/Input/InputActionBinding.cs
Normal file
37
GFramework.Game.Abstractions/Input/InputActionBinding.cs
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
namespace GFramework.Game.Abstractions.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 描述一个逻辑动作当前持有的绑定集合。
|
||||
/// </summary>
|
||||
public sealed class InputActionBinding
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一个动作绑定快照。
|
||||
/// </summary>
|
||||
/// <param name="actionName">动作名称。</param>
|
||||
/// <param name="bindings">当前绑定列表。</param>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="actionName" /> 为空时抛出。</exception>
|
||||
public InputActionBinding(string actionName, IReadOnlyList<InputBindingDescriptor> bindings)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(actionName))
|
||||
{
|
||||
throw new ArgumentException("Action name cannot be null or whitespace.", nameof(actionName));
|
||||
}
|
||||
|
||||
ActionName = actionName;
|
||||
Bindings = bindings ?? Array.Empty<InputBindingDescriptor>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取动作名称。
|
||||
/// </summary>
|
||||
public string ActionName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前绑定列表。
|
||||
/// </summary>
|
||||
public IReadOnlyList<InputBindingDescriptor> Bindings { get; }
|
||||
}
|
||||
67
GFramework.Game.Abstractions/Input/InputBindingDescriptor.cs
Normal file
67
GFramework.Game.Abstractions/Input/InputBindingDescriptor.cs
Normal file
@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
namespace GFramework.Game.Abstractions.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 描述一个框架无关的动作绑定。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该模型是运行时输入系统与宿主适配层之间的稳定交换格式。
|
||||
/// 宿主层负责把原生输入事件转成此描述,抽象层和默认运行时只根据这些字段做查询、冲突检测和持久化。
|
||||
/// </remarks>
|
||||
public sealed class InputBindingDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一个动作绑定描述。
|
||||
/// </summary>
|
||||
/// <param name="deviceKind">设备族。</param>
|
||||
/// <param name="bindingKind">绑定类型。</param>
|
||||
/// <param name="code">宿主无关的物理码值。</param>
|
||||
/// <param name="displayName">用于设置界面展示的名称。</param>
|
||||
/// <param name="axisDirection">轴向方向;非轴向绑定时为 <see langword="null" />。</param>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="code" /> 为空时抛出。</exception>
|
||||
public InputBindingDescriptor(
|
||||
InputDeviceKind deviceKind,
|
||||
InputBindingKind bindingKind,
|
||||
string code,
|
||||
string displayName,
|
||||
float? axisDirection = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code))
|
||||
{
|
||||
throw new ArgumentException("Binding code cannot be null or whitespace.", nameof(code));
|
||||
}
|
||||
|
||||
DeviceKind = deviceKind;
|
||||
BindingKind = bindingKind;
|
||||
Code = code;
|
||||
DisplayName = displayName ?? string.Empty;
|
||||
AxisDirection = axisDirection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取设备族。
|
||||
/// </summary>
|
||||
public InputDeviceKind DeviceKind { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取绑定类型。
|
||||
/// </summary>
|
||||
public InputBindingKind BindingKind { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取宿主无关的物理码值。
|
||||
/// </summary>
|
||||
public string Code { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于展示的标签。
|
||||
/// </summary>
|
||||
public string DisplayName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取轴向方向。
|
||||
/// </summary>
|
||||
public float? AxisDirection { get; }
|
||||
}
|
||||
35
GFramework.Game.Abstractions/Input/InputBindingKind.cs
Normal file
35
GFramework.Game.Abstractions/Input/InputBindingKind.cs
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
namespace GFramework.Game.Abstractions.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 描述一个逻辑绑定使用的物理输入类型。
|
||||
/// </summary>
|
||||
public enum InputBindingKind
|
||||
{
|
||||
/// <summary>
|
||||
/// 未指定。
|
||||
/// </summary>
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 键盘按键。
|
||||
/// </summary>
|
||||
Key = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 鼠标按钮。
|
||||
/// </summary>
|
||||
MouseButton = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 手柄按钮。
|
||||
/// </summary>
|
||||
GamepadButton = 3,
|
||||
|
||||
/// <summary>
|
||||
/// 手柄轴向。
|
||||
/// </summary>
|
||||
GamepadAxis = 4
|
||||
}
|
||||
24
GFramework.Game.Abstractions/Input/InputBindingSnapshot.cs
Normal file
24
GFramework.Game.Abstractions/Input/InputBindingSnapshot.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
namespace GFramework.Game.Abstractions.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 描述一组动作绑定的可持久化快照。
|
||||
/// </summary>
|
||||
public sealed class InputBindingSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一个输入绑定快照。
|
||||
/// </summary>
|
||||
/// <param name="actions">动作绑定集合。</param>
|
||||
public InputBindingSnapshot(IReadOnlyList<InputActionBinding> actions)
|
||||
{
|
||||
Actions = actions ?? Array.Empty<InputActionBinding>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取动作绑定集合。
|
||||
/// </summary>
|
||||
public IReadOnlyList<InputActionBinding> Actions { get; }
|
||||
}
|
||||
41
GFramework.Game.Abstractions/Input/InputDeviceContext.cs
Normal file
41
GFramework.Game.Abstractions/Input/InputDeviceContext.cs
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
namespace GFramework.Game.Abstractions.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 描述当前活跃输入设备上下文。
|
||||
/// </summary>
|
||||
public sealed class InputDeviceContext
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一个输入设备上下文。
|
||||
/// </summary>
|
||||
/// <param name="deviceKind">当前设备族。</param>
|
||||
/// <param name="deviceIndex">设备索引;未知时为 <see langword="null" />。</param>
|
||||
/// <param name="deviceName">宿主归一化后的设备名称。</param>
|
||||
public InputDeviceContext(
|
||||
InputDeviceKind deviceKind,
|
||||
int? deviceIndex = null,
|
||||
string? deviceName = null)
|
||||
{
|
||||
DeviceKind = deviceKind;
|
||||
DeviceIndex = deviceIndex;
|
||||
DeviceName = deviceName ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前设备族。
|
||||
/// </summary>
|
||||
public InputDeviceKind DeviceKind { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前设备索引。
|
||||
/// </summary>
|
||||
public int? DeviceIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取宿主归一化后的设备名称。
|
||||
/// </summary>
|
||||
public string DeviceName { get; }
|
||||
}
|
||||
34
GFramework.Game.Abstractions/Input/InputDeviceKind.cs
Normal file
34
GFramework.Game.Abstractions/Input/InputDeviceKind.cs
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
namespace GFramework.Game.Abstractions.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 描述框架级输入设备族。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该枚举用于跨宿主共享“当前输入来自哪一类设备”的语义。
|
||||
/// 它故意避免暴露 Godot、Unity 或平台 SDK 的原生事件类型,确保上层业务只依赖稳定的设备族判断。
|
||||
/// </remarks>
|
||||
public enum InputDeviceKind
|
||||
{
|
||||
/// <summary>
|
||||
/// 未识别或尚未产生任何输入。
|
||||
/// </summary>
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 键盘与鼠标输入。
|
||||
/// </summary>
|
||||
KeyboardMouse = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 游戏手柄输入。
|
||||
/// </summary>
|
||||
Gamepad = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 触摸输入。
|
||||
/// </summary>
|
||||
Touch = 3
|
||||
}
|
||||
@ -13,6 +13,7 @@
|
||||
- 典型使用场景:
|
||||
- 定义 `IScene`、`IUiPage`、`ISettingsData`、`IData` 等业务对象
|
||||
- 让 feature 包只感知 `IConfigRegistry`、`ISaveRepository<T>`、`ISettingsModel`、`IUiRouter`、`ISceneRouter`
|
||||
- 在输入层共享动作绑定、设备上下文和 UI 语义桥接契约
|
||||
- 在引擎适配层之外共享设置、场景参数、UI 参数、存档数据类型
|
||||
|
||||
## 与相邻包的关系
|
||||
@ -131,6 +132,18 @@ UI 页面与路由契约。
|
||||
|
||||
`IUiRouter` 不只覆盖页面栈,还覆盖 Overlay / Modal / Toast / Topmost 等层级 UI 语义。
|
||||
|
||||
### `Input/`
|
||||
|
||||
- `InputBindingDescriptor`
|
||||
- `InputActionBinding`
|
||||
- `InputBindingSnapshot`
|
||||
- `IInputBindingStore`
|
||||
- `IInputDeviceTracker`
|
||||
- `IUiInputActionMap`
|
||||
- `IUiInputDispatcher`
|
||||
|
||||
这一层定义的是统一输入抽象、绑定快照与 UI 语义桥接契约。
|
||||
|
||||
### `Routing/`
|
||||
|
||||
- `IRoute`
|
||||
@ -164,6 +177,7 @@ Scene 与 UI 路由共享这套基础约定。
|
||||
| `Setting/` | `ISettingsData`、`ISettingsModel`、`ISettingsSystem`、`LocalizationSettings` | 看设置数据、应用语义、迁移接口和内置设置对象 |
|
||||
| `Scene/` | `IScene`、`ISceneRouter`、`ISceneFactory`、`SceneTransitionEvent` | 看场景行为、路由、工厂 / root 边界与转场事件模型 |
|
||||
| `UI/` | `IUiPage`、`IUiRouter`、`IUiFactory`、`UiInteractionProfile`、`UiTransitionHandlerOptions` | 看页面栈、层级 UI、输入动作与 UI 转场契约 |
|
||||
| `Input/` | `InputBindingDescriptor`、`IInputBindingStore`、`IInputDeviceTracker`、`IUiInputDispatcher` | 看动作绑定、设备上下文和 UI 输入桥接契约 |
|
||||
| `Routing/` `Storage/` `Asset/` `Enums/` | `IRoute`、`IRouteContext`、`IFileStorage`、`IAssetRegistry<T>`、`UiLayer`、`SceneTransitionType` | 看公共路由上下文、存储角色、资源注册表与跨层共享枚举 |
|
||||
|
||||
## 最小接入路径
|
||||
@ -267,6 +281,7 @@ public sealed class ContinueGameCommandHandler
|
||||
- 序列化系统:[序列化系统](../docs/zh-CN/game/serialization.md)
|
||||
- 场景系统:[场景系统](../docs/zh-CN/game/scene.md)
|
||||
- UI 系统:[UI 系统](../docs/zh-CN/game/ui.md)
|
||||
- 输入系统:[输入系统](../docs/zh-CN/game/input.md)
|
||||
|
||||
## 选择建议
|
||||
|
||||
|
||||
@ -21,3 +21,4 @@
|
||||
GF_ConfigSchema_014 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
GF_ConfigSchema_015 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
GF_ConfigSchema_016 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
GF_ConfigSchema_017 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
|
||||
@ -237,6 +237,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
||||
return TryValidateStringFormatMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||
TryValidateUnsupportedCombinatorKeywordsRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||
TryValidateUnsupportedOpenObjectKeywordsRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||
TryValidateUnsupportedArrayShapeKeywordsRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||
TryValidateDependentRequiredMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||
TryValidateDependentSchemasMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||
TryValidateAllOfMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||
@ -916,6 +917,40 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
||||
out diagnostic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归拒绝当前共享子集尚未支持的数组形状关键字。
|
||||
/// 当前配置系统只接受单个 object-typed <c>items</c> schema,
|
||||
/// 并继续拒绝 tuple / open-array 关键字,避免生成器对数组元素形状
|
||||
/// 比运行时与工具链更宽松。
|
||||
/// </summary>
|
||||
/// <param name="filePath">Schema 文件路径。</param>
|
||||
/// <param name="displayPath">逻辑字段路径。</param>
|
||||
/// <param name="element">当前 schema 节点。</param>
|
||||
/// <param name="diagnostic">失败时返回的诊断。</param>
|
||||
/// <returns>当前节点树是否未声明不支持的数组形状关键字。</returns>
|
||||
private static bool TryValidateUnsupportedArrayShapeKeywordsRecursively(
|
||||
string filePath,
|
||||
string displayPath,
|
||||
JsonElement element,
|
||||
out Diagnostic? diagnostic)
|
||||
{
|
||||
return TryTraverseSchemaRecursively(
|
||||
filePath,
|
||||
displayPath,
|
||||
element,
|
||||
static (currentFilePath, currentDisplayPath, currentElement, _) =>
|
||||
{
|
||||
return TryValidateUnsupportedArrayShapeKeywords(
|
||||
currentFilePath,
|
||||
currentDisplayPath,
|
||||
currentElement,
|
||||
out var currentDiagnostic)
|
||||
? (true, (Diagnostic?)null)
|
||||
: (false, currentDiagnostic);
|
||||
},
|
||||
out diagnostic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当前节点是否声明了会改变生成类型形状的未支持组合关键字。
|
||||
/// </summary>
|
||||
@ -976,6 +1011,36 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当前节点是否声明了当前共享子集尚未支持的数组形状关键字。
|
||||
/// </summary>
|
||||
/// <param name="filePath">Schema 文件路径。</param>
|
||||
/// <param name="displayPath">逻辑字段路径。</param>
|
||||
/// <param name="element">当前 schema 节点。</param>
|
||||
/// <param name="diagnostic">失败时返回的诊断。</param>
|
||||
/// <returns>未声明不支持关键字时返回 <see langword="true" />。</returns>
|
||||
private static bool TryValidateUnsupportedArrayShapeKeywords(
|
||||
string filePath,
|
||||
string displayPath,
|
||||
JsonElement element,
|
||||
out Diagnostic? diagnostic)
|
||||
{
|
||||
diagnostic = null;
|
||||
if (TryGetUnsupportedArrayShapeKeywordName(element) is not { } keywordName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
diagnostic = Diagnostic.Create(
|
||||
ConfigSchemaDiagnostics.UnsupportedArrayShapeKeyword,
|
||||
CreateFileLocation(filePath),
|
||||
Path.GetFileName(filePath),
|
||||
displayPath,
|
||||
keywordName,
|
||||
"The current config schema subset only accepts one object-valued 'items' schema and rejects tuple or open-array keywords that can change item shape across Runtime, Generator, and Tooling.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前节点声明的首个未支持组合关键字。
|
||||
/// </summary>
|
||||
@ -1007,6 +1072,19 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
||||
null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前节点声明的首个未支持数组形状关键字。
|
||||
/// </summary>
|
||||
/// <param name="element">当前 schema 节点。</param>
|
||||
/// <returns>命中的关键字名称;未声明时返回空。</returns>
|
||||
private static string? TryGetUnsupportedArrayShapeKeywordName(JsonElement element)
|
||||
{
|
||||
return element.TryGetProperty("prefixItems", out _) ? "prefixItems" :
|
||||
element.TryGetProperty("additionalItems", out _) ? "additionalItems" :
|
||||
element.TryGetProperty("unevaluatedItems", out _) ? "unevaluatedItems" :
|
||||
null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以统一顺序递归遍历 schema 树,并把每个节点交给调用方提供的校验逻辑。
|
||||
/// 该遍历覆盖对象属性、<c>dependentSchemas</c> / <c>allOf</c> /
|
||||
|
||||
@ -187,4 +187,15 @@ public static class ConfigSchemaDiagnostics
|
||||
SourceGeneratorsConfigCategory,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
|
||||
/// <summary>
|
||||
/// schema 节点声明了当前共享子集尚未支持的数组形状关键字。
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor UnsupportedArrayShapeKeyword = new(
|
||||
"GF_ConfigSchema_017",
|
||||
"Config schema uses an unsupported array-shape keyword",
|
||||
"Property '{1}' in schema file '{0}' uses unsupported array-shape keyword '{2}': {3}",
|
||||
SourceGeneratorsConfigCategory,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
}
|
||||
|
||||
@ -1636,6 +1636,68 @@ public class YamlConfigLoaderTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证数组字段声明 tuple / open-array 关键字时,会在 schema 解析阶段被显式拒绝。
|
||||
/// </summary>
|
||||
/// <param name="keywordName">待验证的数组形状关键字名称。</param>
|
||||
/// <param name="keywordValueJson">用于拼接测试 schema 的关键字值 JSON 片段。</param>
|
||||
[TestCase("prefixItems", """
|
||||
[
|
||||
{ "type": "integer" }
|
||||
]
|
||||
""")]
|
||||
[TestCase("additionalItems", "false")]
|
||||
[TestCase("unevaluatedItems", "false")]
|
||||
public void LoadAsync_Should_Throw_When_Array_Schema_Declares_Unsupported_ArrayShape_Keyword(
|
||||
string keywordName,
|
||||
string keywordValueJson)
|
||||
{
|
||||
CreateConfigFile(
|
||||
"monster/slime.yaml",
|
||||
"""
|
||||
id: 1
|
||||
name: Slime
|
||||
dropRates:
|
||||
- 5
|
||||
""");
|
||||
CreateSchemaFile(
|
||||
"schemas/monster.schema.json",
|
||||
$$"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name", "dropRates"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": { "type": "string" },
|
||||
"dropRates": {
|
||||
"type": "array",
|
||||
"{{keywordName}}": {{keywordValueJson}},
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var loader = new YamlConfigLoader(_rootPath)
|
||||
.RegisterTable<int, MonsterConfigIntegerArrayStub>("monster", "monster", "schemas/monster.schema.json",
|
||||
static config => config.Id);
|
||||
var registry = new ConfigRegistry();
|
||||
|
||||
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(exception, Is.Not.Null);
|
||||
Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.SchemaUnsupported));
|
||||
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("dropRates"));
|
||||
Assert.That(exception.Message, Does.Contain($"unsupported '{keywordName}' metadata"));
|
||||
Assert.That(exception.Message, Does.Contain("only accepts one object-valued 'items' schema"));
|
||||
Assert.That(registry.Count, Is.EqualTo(0));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证对象数组的 <c>contains</c> 试匹配会按声明属性子集工作,而不会因额外字段误判为不匹配。
|
||||
/// </summary>
|
||||
|
||||
95
GFramework.Game.Tests/Input/InputBindingStoreTests.cs
Normal file
95
GFramework.Game.Tests/Input/InputBindingStoreTests.cs
Normal file
@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Game.Abstractions.Input;
|
||||
using GFramework.Game.Input;
|
||||
|
||||
namespace GFramework.Game.Tests.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 验证默认输入绑定存储的重绑定、冲突交换与默认恢复行为。
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public sealed class InputBindingStoreTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证主绑定冲突时,会把原绑定交换回被占用动作。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void SetPrimaryBinding_WhenBindingOwnedByAnotherAction_SwapsBindings()
|
||||
{
|
||||
var store = CreateStore();
|
||||
var replacement = new InputBindingDescriptor(
|
||||
InputDeviceKind.KeyboardMouse,
|
||||
InputBindingKind.Key,
|
||||
"key:68",
|
||||
"D");
|
||||
|
||||
store.SetPrimaryBinding("move_left", replacement);
|
||||
|
||||
var moveLeft = store.GetBindings("move_left");
|
||||
var moveRight = store.GetBindings("move_right");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(moveLeft.Bindings[0].Code, Is.EqualTo("key:68"));
|
||||
Assert.That(moveRight.Bindings[0].Code, Is.EqualTo("key:65"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证重置全部绑定时,会回退到初始化默认快照。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ResetAll_Should_Restore_DefaultSnapshot()
|
||||
{
|
||||
var store = CreateStore();
|
||||
store.SetPrimaryBinding(
|
||||
"move_left",
|
||||
new InputBindingDescriptor(InputDeviceKind.KeyboardMouse, InputBindingKind.Key, "key:81", "Q"));
|
||||
|
||||
store.ResetAll();
|
||||
var snapshot = store.ExportSnapshot();
|
||||
|
||||
Assert.That(
|
||||
snapshot.Actions.Single(action => string.Equals(action.ActionName, "move_left", StringComparison.Ordinal)).Bindings[0].Code,
|
||||
Is.EqualTo("key:65"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证查询不存在的动作时,不会把空条目写回当前快照。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void GetBindings_WhenActionMissing_Should_NotMutateSnapshot()
|
||||
{
|
||||
var store = CreateStore();
|
||||
|
||||
var missingBindings = store.GetBindings("jump");
|
||||
var snapshot = store.ExportSnapshot();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(missingBindings.ActionName, Is.EqualTo("jump"));
|
||||
Assert.That(missingBindings.Bindings, Is.Empty);
|
||||
Assert.That(snapshot.Actions.Any(action => string.Equals(action.ActionName, "jump", StringComparison.Ordinal)), Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
private static InputBindingStore CreateStore()
|
||||
{
|
||||
return new InputBindingStore(
|
||||
new InputBindingSnapshot(
|
||||
[
|
||||
new InputActionBinding(
|
||||
"move_left",
|
||||
[
|
||||
new InputBindingDescriptor(InputDeviceKind.KeyboardMouse, InputBindingKind.Key, "key:65", "A")
|
||||
]),
|
||||
new InputActionBinding(
|
||||
"move_right",
|
||||
[
|
||||
new InputBindingDescriptor(InputDeviceKind.KeyboardMouse, InputBindingKind.Key, "key:68", "D")
|
||||
])
|
||||
]));
|
||||
}
|
||||
}
|
||||
52
GFramework.Game.Tests/Input/UiInputDispatcherTests.cs
Normal file
52
GFramework.Game.Tests/Input/UiInputDispatcherTests.cs
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Game.Abstractions.Input;
|
||||
using GFramework.Game.Input;
|
||||
|
||||
namespace GFramework.Game.Tests.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 验证逻辑动作到 UI 路由分发的默认桥接行为。
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public sealed class UiInputDispatcherTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证 `ui_cancel` 会被映射为 `UiInputAction.Cancel` 并继续分发给路由器。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TryDispatch_WhenActionCanMapToUiAction_ForwardsToRouter()
|
||||
{
|
||||
var router = new Mock<IUiRouter>();
|
||||
router.Setup(mock => mock.TryDispatchUiAction(UiInputAction.Cancel)).Returns(true);
|
||||
|
||||
var dispatcher = new UiInputDispatcher(new UiInputActionMap(), router.Object);
|
||||
|
||||
var dispatched = dispatcher.TryDispatch("ui_cancel");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(dispatched, Is.True);
|
||||
router.Verify(mock => mock.TryDispatchUiAction(UiInputAction.Cancel), Times.Once);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证未映射的逻辑动作不会触发 UI 路由。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TryDispatch_WhenActionIsUnknown_ReturnsFalseWithoutRouting()
|
||||
{
|
||||
var router = new Mock<IUiRouter>();
|
||||
var dispatcher = new UiInputDispatcher(new UiInputActionMap(), router.Object);
|
||||
|
||||
var dispatched = dispatcher.TryDispatch("inventory_toggle");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(dispatched, Is.False);
|
||||
router.Verify(mock => mock.TryDispatchUiAction(It.IsAny<UiInputAction>()), Times.Never);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -326,6 +326,7 @@ internal static partial class YamlConfigSchemaValidator
|
||||
{
|
||||
ValidateUnsupportedCombinatorKeywords(tableName, schemaPath, propertyPath, element);
|
||||
ValidateUnsupportedOpenObjectKeywords(tableName, schemaPath, propertyPath, element);
|
||||
ValidateUnsupportedArrayShapeKeywords(tableName, schemaPath, propertyPath, element);
|
||||
var typeName = ResolveNodeTypeName(tableName, schemaPath, propertyPath, element);
|
||||
var referenceTableName = TryGetReferenceTableName(tableName, schemaPath, propertyPath, element);
|
||||
ValidateObjectOnlyKeywords(tableName, schemaPath, propertyPath, element, typeName);
|
||||
@ -401,6 +402,36 @@ internal static partial class YamlConfigSchemaValidator
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显式拒绝当前共享子集中尚未支持的数组形状关键字。
|
||||
/// 当前配置系统只接受单个 object-valued <c>items</c> schema,
|
||||
/// 并继续拒绝 tuple / open-array 关键字,避免 Runtime / Generator / Tooling
|
||||
/// 对数组元素形状产生静默漂移。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">当前节点的逻辑属性路径。</param>
|
||||
/// <param name="element">当前 schema 节点。</param>
|
||||
private static void ValidateUnsupportedArrayShapeKeywords(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
JsonElement element)
|
||||
{
|
||||
if (TryGetUnsupportedArrayShapeKeywordName(element) is not { } keywordName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported '{keywordName}' metadata. " +
|
||||
"The current config schema subset only accepts one object-valued 'items' schema and rejects tuple or open-array keywords that can change item shape across Runtime, Generator, and Tooling.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前节点声明的首个未支持组合关键字。
|
||||
/// </summary>
|
||||
@ -432,6 +463,19 @@ internal static partial class YamlConfigSchemaValidator
|
||||
null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前节点声明的首个未支持数组形状关键字。
|
||||
/// </summary>
|
||||
/// <param name="element">当前 schema 节点。</param>
|
||||
/// <returns>命中的关键字名称;未声明时返回空。</returns>
|
||||
private static string? TryGetUnsupportedArrayShapeKeywordName(JsonElement element)
|
||||
{
|
||||
return element.TryGetProperty("prefixItems", out _) ? "prefixItems" :
|
||||
element.TryGetProperty("additionalItems", out _) ? "additionalItems" :
|
||||
element.TryGetProperty("unevaluatedItems", out _) ? "unevaluatedItems" :
|
||||
null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 schema 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。
|
||||
/// </summary>
|
||||
|
||||
192
GFramework.Game/Input/InputBindingStore.cs
Normal file
192
GFramework.Game/Input/InputBindingStore.cs
Normal file
@ -0,0 +1,192 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Game.Abstractions.Input;
|
||||
|
||||
namespace GFramework.Game.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 提供基于内存快照的默认输入绑定存储实现。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该实现聚焦于框架级动作绑定管理语义:默认值恢复、主绑定替换、冲突交换与快照导入导出。
|
||||
/// 它不依赖具体宿主输入事件,适合作为 `Game` 层默认运行时与单元测试基线。
|
||||
/// 该类型内部使用普通 `Dictionary` / `List` 保存可变状态,不提供额外同步原语。
|
||||
/// 宿主应在同一输入线程或受控的串行配置阶段访问它;如果存在跨线程读写需求,应由外层协调同步。
|
||||
/// </remarks>
|
||||
public sealed class InputBindingStore : IInputBindingStore
|
||||
{
|
||||
private readonly Dictionary<string, List<InputBindingDescriptor>> _defaultBindings;
|
||||
private readonly Dictionary<string, List<InputBindingDescriptor>> _currentBindings;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化输入绑定存储。
|
||||
/// </summary>
|
||||
/// <param name="defaultSnapshot">默认绑定快照。</param>
|
||||
public InputBindingStore(InputBindingSnapshot defaultSnapshot)
|
||||
{
|
||||
_defaultBindings = ToDictionary(defaultSnapshot);
|
||||
_currentBindings = CloneDictionary(_defaultBindings);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public InputActionBinding GetBindings(string actionName)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(actionName);
|
||||
|
||||
return _currentBindings.TryGetValue(actionName, out var bindings)
|
||||
? new InputActionBinding(actionName, bindings.ToArray())
|
||||
: new InputActionBinding(actionName, Array.Empty<InputBindingDescriptor>());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public InputBindingSnapshot ExportSnapshot()
|
||||
{
|
||||
var actions = _currentBindings
|
||||
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
|
||||
.Select(static pair => new InputActionBinding(pair.Key, pair.Value.ToArray()))
|
||||
.ToArray();
|
||||
|
||||
return new InputBindingSnapshot(actions);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ImportSnapshot(InputBindingSnapshot snapshot)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(snapshot);
|
||||
|
||||
_currentBindings.Clear();
|
||||
foreach (var action in snapshot.Actions)
|
||||
{
|
||||
_currentBindings[action.ActionName] = [..action.Bindings];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetPrimaryBinding(string actionName, InputBindingDescriptor binding, bool swapIfTaken = true)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(actionName);
|
||||
ArgumentNullException.ThrowIfNull(binding);
|
||||
|
||||
var targetBindings = GetOrCreateBindings(actionName);
|
||||
var existingOwner = FindOwner(actionName, binding);
|
||||
|
||||
if (existingOwner is not null)
|
||||
{
|
||||
if (!swapIfTaken)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var previousPrimary = targetBindings.Count > 0 ? targetBindings[0] : null;
|
||||
var ownerBindings = GetOrCreateBindings(existingOwner);
|
||||
ReplaceBinding(ownerBindings, binding, previousPrimary);
|
||||
}
|
||||
|
||||
RemoveBinding(targetBindings, binding);
|
||||
targetBindings.Insert(0, binding);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ResetAction(string actionName)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(actionName);
|
||||
|
||||
if (_defaultBindings.TryGetValue(actionName, out var bindings))
|
||||
{
|
||||
_currentBindings[actionName] = [..bindings];
|
||||
return;
|
||||
}
|
||||
|
||||
_currentBindings.Remove(actionName);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ResetAll()
|
||||
{
|
||||
_currentBindings.Clear();
|
||||
foreach (var pair in _defaultBindings)
|
||||
{
|
||||
_currentBindings[pair.Key] = [..pair.Value];
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, List<InputBindingDescriptor>> ToDictionary(InputBindingSnapshot snapshot)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(snapshot);
|
||||
|
||||
return snapshot.Actions.ToDictionary(
|
||||
static action => action.ActionName,
|
||||
static action => action.Bindings.ToList(),
|
||||
StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private static Dictionary<string, List<InputBindingDescriptor>> CloneDictionary(
|
||||
IReadOnlyDictionary<string, List<InputBindingDescriptor>> source)
|
||||
{
|
||||
return source.ToDictionary(
|
||||
static pair => pair.Key,
|
||||
static pair => pair.Value.ToList(),
|
||||
StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private static void RemoveBinding(List<InputBindingDescriptor> bindings, InputBindingDescriptor binding)
|
||||
{
|
||||
bindings.RemoveAll(existing => AreEquivalent(existing, binding));
|
||||
}
|
||||
|
||||
private static void ReplaceBinding(
|
||||
List<InputBindingDescriptor> bindings,
|
||||
InputBindingDescriptor bindingToReplace,
|
||||
InputBindingDescriptor? replacement)
|
||||
{
|
||||
var index = bindings.FindIndex(existing => AreEquivalent(existing, bindingToReplace));
|
||||
if (index < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bindings.RemoveAt(index);
|
||||
if (replacement is not null)
|
||||
{
|
||||
bindings.Insert(index, replacement);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AreEquivalent(InputBindingDescriptor left, InputBindingDescriptor right)
|
||||
{
|
||||
return left.DeviceKind == right.DeviceKind
|
||||
&& left.BindingKind == right.BindingKind
|
||||
&& string.Equals(left.Code, right.Code, StringComparison.Ordinal)
|
||||
&& Nullable.Equals(left.AxisDirection, right.AxisDirection);
|
||||
}
|
||||
|
||||
private List<InputBindingDescriptor> GetOrCreateBindings(string actionName)
|
||||
{
|
||||
if (!_currentBindings.TryGetValue(actionName, out var bindings))
|
||||
{
|
||||
bindings = [];
|
||||
_currentBindings[actionName] = bindings;
|
||||
}
|
||||
|
||||
return bindings;
|
||||
}
|
||||
|
||||
private string? FindOwner(string excludedActionName, InputBindingDescriptor binding)
|
||||
{
|
||||
foreach (var pair in _currentBindings)
|
||||
{
|
||||
if (string.Equals(pair.Key, excludedActionName, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pair.Value.Any(existing => AreEquivalent(existing, binding)))
|
||||
{
|
||||
return pair.Key;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
41
GFramework.Game/Input/InputDeviceTracker.cs
Normal file
41
GFramework.Game/Input/InputDeviceTracker.cs
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Game.Abstractions.Input;
|
||||
|
||||
namespace GFramework.Game.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 提供可由宿主侧更新的默认输入设备跟踪器。
|
||||
/// </summary>
|
||||
public sealed class InputDeviceTracker : IInputDeviceTracker
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化输入设备跟踪器。
|
||||
/// </summary>
|
||||
public InputDeviceTracker()
|
||||
{
|
||||
CurrentDevice = new InputDeviceContext(InputDeviceKind.Unknown);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// 该属性不提供额外同步原语。
|
||||
/// 宿主应在同一输入线程内调用 <see cref="Update" /> 并读取当前值,例如 Godot 的主线程或输入事件线程。
|
||||
/// </remarks>
|
||||
public InputDeviceContext CurrentDevice { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 使用新的宿主设备上下文覆盖当前状态。
|
||||
/// </summary>
|
||||
/// <param name="context">新的设备上下文。</param>
|
||||
/// <remarks>
|
||||
/// 该方法设计给宿主输入线程串行调用。
|
||||
/// 如果宿主需要跨线程读取设备上下文,应在外层提供自己的同步策略,而不是依赖此类型完成可见性保证。
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="context" /> 为 <see langword="null" /> 时抛出。</exception>
|
||||
public void Update(InputDeviceContext context)
|
||||
{
|
||||
CurrentDevice = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
}
|
||||
39
GFramework.Game/Input/UiInputActionMap.cs
Normal file
39
GFramework.Game/Input/UiInputActionMap.cs
Normal file
@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Game.Abstractions.Input;
|
||||
using GFramework.Game.Abstractions.UI;
|
||||
|
||||
namespace GFramework.Game.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 提供动作名称到 UI 语义动作的默认映射实现。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 默认映射只负责桥接现有 `UiInputAction` 语义,并通过字符串别名兼容 Godot 常见 `ui_*` 动作命名。
|
||||
/// 更复杂的项目级 action map 可以通过自定义实现覆盖该行为。
|
||||
/// </remarks>
|
||||
public sealed class UiInputActionMap : IUiInputActionMap
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, UiInputAction> DefaultMappings =
|
||||
new Dictionary<string, UiInputAction>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["cancel"] = UiInputAction.Cancel,
|
||||
["ui_cancel"] = UiInputAction.Cancel,
|
||||
["confirm"] = UiInputAction.Confirm,
|
||||
["ui_accept"] = UiInputAction.Confirm,
|
||||
["submit"] = UiInputAction.Confirm
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryMap(string actionName, out UiInputAction action)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(actionName))
|
||||
{
|
||||
action = UiInputAction.None;
|
||||
return false;
|
||||
}
|
||||
|
||||
return DefaultMappings.TryGetValue(actionName, out action);
|
||||
}
|
||||
}
|
||||
40
GFramework.Game/Input/UiInputDispatcher.cs
Normal file
40
GFramework.Game/Input/UiInputDispatcher.cs
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Game.Abstractions.Input;
|
||||
using GFramework.Game.Abstractions.UI;
|
||||
|
||||
namespace GFramework.Game.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 提供逻辑动作到 UI 路由语义分发的默认桥接。
|
||||
/// </summary>
|
||||
public sealed class UiInputDispatcher : IUiInputDispatcher
|
||||
{
|
||||
private readonly IUiInputActionMap _actionMap;
|
||||
private readonly IUiRouter _router;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 UI 输入分发器。
|
||||
/// </summary>
|
||||
/// <param name="actionMap">动作映射表。</param>
|
||||
/// <param name="router">目标 UI 路由器。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="actionMap" /> 为 <see langword="null" /> 时抛出。</exception>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="router" /> 为 <see langword="null" /> 时抛出。</exception>
|
||||
public UiInputDispatcher(IUiInputActionMap actionMap, IUiRouter router)
|
||||
{
|
||||
_actionMap = actionMap ?? throw new ArgumentNullException(nameof(actionMap));
|
||||
_router = router ?? throw new ArgumentNullException(nameof(router));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryDispatch(string actionName)
|
||||
{
|
||||
if (!_actionMap.TryMap(actionName, out var action))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _router.TryDispatchUiAction(action);
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,7 @@
|
||||
- 数据与存档:`Data/`
|
||||
- 设置系统:`Setting/`
|
||||
- 场景与 UI 路由基类:`Scene/`、`UI/`
|
||||
- 动作绑定与 UI 输入桥接:`Input/`
|
||||
- 序列化与文件存储:`Serializer/`、`Storage/`
|
||||
|
||||
## 与相邻包的关系
|
||||
@ -158,6 +159,22 @@
|
||||
- [场景系统](../docs/zh-CN/game/scene.md)
|
||||
- [UI 系统](../docs/zh-CN/game/ui.md)
|
||||
|
||||
### `Input/`
|
||||
|
||||
- `InputBindingStore`
|
||||
- 纯托管动作绑定存储
|
||||
- 提供默认快照恢复、主绑定替换、冲突交换与快照导入导出
|
||||
- `InputDeviceTracker`
|
||||
- 提供当前活跃设备上下文的默认持有者
|
||||
- `UiInputActionMap`
|
||||
- 把 `ui_accept` / `ui_cancel` 等逻辑动作桥接到 `UiInputAction`
|
||||
- `UiInputDispatcher`
|
||||
- 把逻辑动作名继续分发给 `IUiRouter`
|
||||
|
||||
对应文档:
|
||||
|
||||
- [输入系统](../docs/zh-CN/game/input.md)
|
||||
|
||||
### `Routing/` 与 `State/`
|
||||
|
||||
- `Routing/RouterBase<TRoute, TContext>`
|
||||
@ -176,6 +193,7 @@
|
||||
| `Config/` | `YamlConfigLoader`、`ConfigRegistry`、`GameConfigBootstrap`、`YamlConfigSchemaValidator` | 看 YAML 加载、schema 校验、模块接入与热重载边界 |
|
||||
| `Data/` `Storage/` `Serializer/` | `DataRepository`、`SaveRepository<TSaveData>`、`UnifiedSettingsDataRepository`、`FileStorage`、`JsonSerializer` | 看持久化布局、槽位存档、统一设置文件和底层序列化 / 存储实现 |
|
||||
| `Setting/` | `SettingsModel<TRepository>`、`SettingsSystem`、`SettingsAppliedEvent<T>` | 看初始化、应用、保存、重置等设置生命周期编排 |
|
||||
| `Input/` | `InputBindingStore`、`InputDeviceTracker`、`UiInputActionMap`、`UiInputDispatcher` | 看动作绑定、设备上下文和 UI 输入桥接的默认运行时实现 |
|
||||
| `Scene/` `UI/` `Routing/` | `SceneRouterBase`、`UiRouterBase`、`SceneTransitionPipeline`、`UiTransitionPipeline`、`RouterBase<TRoute, TContext>` | 看路由基类、转换处理器和项目层需要自己提供的 factory / root 边界 |
|
||||
| `Extensions/` `Internal/` `State/` | `DataLocationExtensions`、`VersionedMigrationRunner`、`GameStateMachineSystem` | 看辅助扩展、内部迁移执行逻辑和游戏态状态机封装 |
|
||||
|
||||
@ -354,6 +372,7 @@ public sealed class MyUiRouter : UiRouterBase
|
||||
- 序列化系统:[序列化系统](../docs/zh-CN/game/serialization.md)
|
||||
- 场景系统:[场景系统](../docs/zh-CN/game/scene.md)
|
||||
- UI 系统:[UI 系统](../docs/zh-CN/game/ui.md)
|
||||
- 输入系统:[输入系统](../docs/zh-CN/game/input.md)
|
||||
|
||||
## 什么时候不该直接依赖本包
|
||||
|
||||
|
||||
316
GFramework.Godot.Tests/Input/GodotInputBindingStoreTests.cs
Normal file
316
GFramework.Godot.Tests/Input/GodotInputBindingStoreTests.cs
Normal file
@ -0,0 +1,316 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Game.Abstractions.Input;
|
||||
using GFramework.Godot.Input;
|
||||
|
||||
namespace GFramework.Godot.Tests.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 验证 Godot 输入绑定存储在纯托管后端上的动作快照、导入与冲突交换语义。
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public sealed class GodotInputBindingStoreTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证导出快照会反映后端提供的框架绑定描述。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ExportSnapshot_Should_ReturnBackendBindings()
|
||||
{
|
||||
var backend = new FakeInputMapBackend(
|
||||
new InputBindingSnapshot(
|
||||
[
|
||||
new InputActionBinding(
|
||||
"ui_accept",
|
||||
[
|
||||
new InputBindingDescriptor(
|
||||
InputDeviceKind.KeyboardMouse,
|
||||
InputBindingKind.Key,
|
||||
"key:13",
|
||||
"Enter")
|
||||
])
|
||||
]));
|
||||
|
||||
var store = new GodotInputBindingStore(backend);
|
||||
var snapshot = store.ExportSnapshot();
|
||||
var acceptBindings = snapshot.Actions.Single(
|
||||
action => string.Equals(action.ActionName, "ui_accept", StringComparison.Ordinal));
|
||||
|
||||
Assert.That(acceptBindings.Bindings[0].Code, Is.EqualTo("key:13"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证导入快照后会把新绑定回写到后端,并能重新导出。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ImportSnapshot_Should_UpdateBackendBindings()
|
||||
{
|
||||
var backend = new FakeInputMapBackend(
|
||||
new InputBindingSnapshot(
|
||||
[
|
||||
new InputActionBinding(
|
||||
"ui_accept",
|
||||
[
|
||||
new InputBindingDescriptor(
|
||||
InputDeviceKind.KeyboardMouse,
|
||||
InputBindingKind.Key,
|
||||
"key:13",
|
||||
"Enter")
|
||||
])
|
||||
]));
|
||||
|
||||
var store = new GodotInputBindingStore(backend);
|
||||
store.ImportSnapshot(
|
||||
new InputBindingSnapshot(
|
||||
[
|
||||
new InputActionBinding(
|
||||
"ui_accept",
|
||||
[
|
||||
new InputBindingDescriptor(
|
||||
InputDeviceKind.KeyboardMouse,
|
||||
InputBindingKind.Key,
|
||||
"key:32",
|
||||
"Space")
|
||||
])
|
||||
]));
|
||||
|
||||
var snapshot = store.ExportSnapshot();
|
||||
var acceptBindings = snapshot.Actions.Single(
|
||||
action => string.Equals(action.ActionName, "ui_accept", StringComparison.Ordinal));
|
||||
|
||||
Assert.That(acceptBindings.Bindings[0].Code, Is.EqualTo("key:32"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证导入快照时,会清空快照中未出现动作的后端绑定。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ImportSnapshot_WhenActionMissingFromSnapshot_Should_ClearBackendBindings()
|
||||
{
|
||||
var backend = new FakeInputMapBackend(
|
||||
new InputBindingSnapshot(
|
||||
[
|
||||
new InputActionBinding(
|
||||
"ui_accept",
|
||||
[
|
||||
new InputBindingDescriptor(
|
||||
InputDeviceKind.KeyboardMouse,
|
||||
InputBindingKind.Key,
|
||||
"key:13",
|
||||
"Enter")
|
||||
]),
|
||||
new InputActionBinding(
|
||||
"ui_cancel",
|
||||
[
|
||||
new InputBindingDescriptor(
|
||||
InputDeviceKind.KeyboardMouse,
|
||||
InputBindingKind.Key,
|
||||
"key:27",
|
||||
"Escape")
|
||||
])
|
||||
]));
|
||||
|
||||
var store = new GodotInputBindingStore(backend);
|
||||
store.ImportSnapshot(
|
||||
new InputBindingSnapshot(
|
||||
[
|
||||
new InputActionBinding(
|
||||
"ui_accept",
|
||||
[
|
||||
new InputBindingDescriptor(
|
||||
InputDeviceKind.KeyboardMouse,
|
||||
InputBindingKind.Key,
|
||||
"key:32",
|
||||
"Space")
|
||||
])
|
||||
]));
|
||||
|
||||
var snapshot = store.ExportSnapshot();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(
|
||||
snapshot.Actions.Single(action => string.Equals(action.ActionName, "ui_accept", StringComparison.Ordinal)).Bindings[0].Code,
|
||||
Is.EqualTo("key:32"));
|
||||
Assert.That(
|
||||
snapshot.Actions.Single(action => string.Equals(action.ActionName, "ui_cancel", StringComparison.Ordinal)).Bindings,
|
||||
Is.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证从纯托管绑定设置主绑定时,会保留 `Game` 层冲突交换语义。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void SetPrimaryBinding_WhenBindingTaken_SwapsBackendBindings()
|
||||
{
|
||||
var backend = new FakeInputMapBackend(
|
||||
new InputBindingSnapshot(
|
||||
[
|
||||
new InputActionBinding(
|
||||
"move_left",
|
||||
[
|
||||
new InputBindingDescriptor(
|
||||
InputDeviceKind.KeyboardMouse,
|
||||
InputBindingKind.Key,
|
||||
"key:65",
|
||||
"A")
|
||||
]),
|
||||
new InputActionBinding(
|
||||
"move_right",
|
||||
[
|
||||
new InputBindingDescriptor(
|
||||
InputDeviceKind.KeyboardMouse,
|
||||
InputBindingKind.Key,
|
||||
"key:68",
|
||||
"D")
|
||||
])
|
||||
]));
|
||||
|
||||
var store = new GodotInputBindingStore(backend);
|
||||
store.SetPrimaryBinding(
|
||||
"move_left",
|
||||
new InputBindingDescriptor(
|
||||
InputDeviceKind.KeyboardMouse,
|
||||
InputBindingKind.Key,
|
||||
"key:68",
|
||||
"D"));
|
||||
|
||||
var snapshot = store.ExportSnapshot();
|
||||
var moveLeft = snapshot.Actions.Single(
|
||||
action => string.Equals(action.ActionName, "move_left", StringComparison.Ordinal));
|
||||
var moveRight = snapshot.Actions.Single(
|
||||
action => string.Equals(action.ActionName, "move_right", StringComparison.Ordinal));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(moveLeft.Bindings[0].Code, Is.EqualTo("key:68"));
|
||||
Assert.That(moveRight.Bindings[0].Code, Is.EqualTo("key:65"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证重置全部绑定时,会移除运行时新增且默认快照中不存在的动作。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ResetAll_WhenRuntimeActionIsNotInDefaults_Should_RemoveAction()
|
||||
{
|
||||
var backend = new FakeInputMapBackend(
|
||||
new InputBindingSnapshot(
|
||||
[
|
||||
new InputActionBinding(
|
||||
"ui_accept",
|
||||
[
|
||||
new InputBindingDescriptor(
|
||||
InputDeviceKind.KeyboardMouse,
|
||||
InputBindingKind.Key,
|
||||
"key:13",
|
||||
"Enter")
|
||||
])
|
||||
]));
|
||||
|
||||
var store = new GodotInputBindingStore(backend);
|
||||
store.ImportSnapshot(
|
||||
new InputBindingSnapshot(
|
||||
[
|
||||
new InputActionBinding(
|
||||
"ui_accept",
|
||||
[
|
||||
new InputBindingDescriptor(
|
||||
InputDeviceKind.KeyboardMouse,
|
||||
InputBindingKind.Key,
|
||||
"key:13",
|
||||
"Enter")
|
||||
]),
|
||||
new InputActionBinding(
|
||||
"debug_toggle",
|
||||
[
|
||||
new InputBindingDescriptor(
|
||||
InputDeviceKind.KeyboardMouse,
|
||||
InputBindingKind.Key,
|
||||
"key:192",
|
||||
"QuoteLeft")
|
||||
])
|
||||
]));
|
||||
|
||||
store.ResetAll();
|
||||
|
||||
var snapshot = store.ExportSnapshot();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(
|
||||
snapshot.Actions.Any(action => string.Equals(action.ActionName, "ui_accept", StringComparison.Ordinal)),
|
||||
Is.True);
|
||||
Assert.That(
|
||||
snapshot.Actions.Any(action => string.Equals(action.ActionName, "debug_toggle", StringComparison.Ordinal)),
|
||||
Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试用的纯托管 InputMap 后端。
|
||||
/// </summary>
|
||||
private sealed class FakeInputMapBackend : IGodotInputMapBackend
|
||||
{
|
||||
private readonly Dictionary<string, List<InputBindingDescriptor>> _defaults;
|
||||
private readonly Dictionary<string, List<InputBindingDescriptor>> _current;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化测试后端。
|
||||
/// </summary>
|
||||
/// <param name="snapshot">初始快照。</param>
|
||||
public FakeInputMapBackend(InputBindingSnapshot snapshot)
|
||||
{
|
||||
_defaults = snapshot.Actions.ToDictionary(
|
||||
static action => action.ActionName,
|
||||
static action => action.Bindings.ToList(),
|
||||
StringComparer.Ordinal);
|
||||
_current = snapshot.Actions.ToDictionary(
|
||||
static action => action.ActionName,
|
||||
static action => action.Bindings.ToList(),
|
||||
StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> GetActionNames()
|
||||
{
|
||||
return [.._current.Keys.OrderBy(static key => key, StringComparer.Ordinal)];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<InputBindingDescriptor> GetBindings(string actionName)
|
||||
{
|
||||
return _current.TryGetValue(actionName, out var bindings) ? [..bindings] : Array.Empty<InputBindingDescriptor>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetBindings(string actionName, IReadOnlyList<InputBindingDescriptor> bindings)
|
||||
{
|
||||
_current[actionName] = [..bindings];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ResetAction(string actionName)
|
||||
{
|
||||
if (_defaults.TryGetValue(actionName, out var bindings))
|
||||
{
|
||||
_current[actionName] = [..bindings];
|
||||
return;
|
||||
}
|
||||
|
||||
_current.Remove(actionName);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ResetAll()
|
||||
{
|
||||
_current.Clear();
|
||||
foreach (var pair in _defaults)
|
||||
{
|
||||
_current[pair.Key] = [..pair.Value];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
191
GFramework.Godot/Input/GodotInputBindingCodec.cs
Normal file
191
GFramework.Godot/Input/GodotInputBindingCodec.cs
Normal file
@ -0,0 +1,191 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Globalization;
|
||||
using GFramework.Game.Abstractions.Input;
|
||||
|
||||
namespace GFramework.Godot.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 负责在 Godot 原生输入事件与框架绑定描述之间做双向转换。
|
||||
/// </summary>
|
||||
internal static class GodotInputBindingCodec
|
||||
{
|
||||
/// <summary>
|
||||
/// 尝试把原生输入事件转换成框架绑定描述。
|
||||
/// </summary>
|
||||
/// <param name="inputEvent">原生输入事件。</param>
|
||||
/// <param name="binding">转换后的绑定描述。</param>
|
||||
/// <returns>如果转换成功则返回 <see langword="true" />。</returns>
|
||||
public static bool TryCreateBinding(InputEvent inputEvent, out InputBindingDescriptor binding)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(inputEvent);
|
||||
|
||||
switch (inputEvent)
|
||||
{
|
||||
case InputEventKey keyEvent:
|
||||
var keyCode = GetKeyCode(keyEvent);
|
||||
binding = new InputBindingDescriptor(
|
||||
InputDeviceKind.KeyboardMouse,
|
||||
InputBindingKind.Key,
|
||||
FormattableString.Invariant($"key:{(int)keyCode}"),
|
||||
keyCode.ToString());
|
||||
return true;
|
||||
case InputEventMouseButton mouseButtonEvent:
|
||||
binding = new InputBindingDescriptor(
|
||||
InputDeviceKind.KeyboardMouse,
|
||||
InputBindingKind.MouseButton,
|
||||
FormattableString.Invariant($"mouse:{(int)mouseButtonEvent.ButtonIndex}"),
|
||||
mouseButtonEvent.ButtonIndex.ToString());
|
||||
return true;
|
||||
case InputEventJoypadButton joypadButtonEvent:
|
||||
binding = new InputBindingDescriptor(
|
||||
InputDeviceKind.Gamepad,
|
||||
InputBindingKind.GamepadButton,
|
||||
FormattableString.Invariant($"joy-button:{(int)joypadButtonEvent.ButtonIndex}"),
|
||||
joypadButtonEvent.ButtonIndex.ToString());
|
||||
return true;
|
||||
case InputEventJoypadMotion joypadMotionEvent:
|
||||
var direction = joypadMotionEvent.AxisValue >= 0f ? 1f : -1f;
|
||||
binding = new InputBindingDescriptor(
|
||||
InputDeviceKind.Gamepad,
|
||||
InputBindingKind.GamepadAxis,
|
||||
FormattableString.Invariant($"joy-axis:{(int)joypadMotionEvent.Axis}:{direction.ToString(CultureInfo.InvariantCulture)}"),
|
||||
GetAxisDisplayName(joypadMotionEvent.Axis, direction),
|
||||
direction);
|
||||
return true;
|
||||
default:
|
||||
binding = null!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把框架绑定描述还原为 Godot 输入事件。
|
||||
/// </summary>
|
||||
/// <param name="binding">绑定描述。</param>
|
||||
/// <returns>可写回 `InputMap` 的输入事件。</returns>
|
||||
/// <exception cref="ArgumentException">当绑定描述无法转换时抛出。</exception>
|
||||
public static InputEvent CreateInputEvent(InputBindingDescriptor binding)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(binding);
|
||||
|
||||
return binding.BindingKind switch
|
||||
{
|
||||
InputBindingKind.Key => CreateKeyEvent(binding),
|
||||
InputBindingKind.MouseButton => CreateMouseButtonEvent(binding),
|
||||
InputBindingKind.GamepadButton => CreateGamepadButtonEvent(binding),
|
||||
InputBindingKind.GamepadAxis => CreateGamepadAxisEvent(binding),
|
||||
_ => throw new ArgumentException($"Unsupported binding kind '{binding.BindingKind}'.", nameof(binding))
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从原生输入事件推断当前设备上下文。
|
||||
/// </summary>
|
||||
/// <param name="inputEvent">原生输入事件。</param>
|
||||
/// <returns>推断出的设备上下文。</returns>
|
||||
public static InputDeviceContext GetDeviceContext(InputEvent inputEvent)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(inputEvent);
|
||||
|
||||
return inputEvent switch
|
||||
{
|
||||
InputEventKey => new InputDeviceContext(InputDeviceKind.KeyboardMouse),
|
||||
InputEventMouse => new InputDeviceContext(InputDeviceKind.KeyboardMouse),
|
||||
InputEventJoypadButton joypadButtonEvent => CreateGamepadContext(joypadButtonEvent.Device),
|
||||
InputEventJoypadMotion joypadMotionEvent => CreateGamepadContext(joypadMotionEvent.Device),
|
||||
InputEventScreenTouch => new InputDeviceContext(InputDeviceKind.Touch),
|
||||
_ => new InputDeviceContext(InputDeviceKind.Unknown)
|
||||
};
|
||||
}
|
||||
|
||||
private static InputDeviceContext CreateGamepadContext(int deviceIndex)
|
||||
{
|
||||
return new InputDeviceContext(
|
||||
InputDeviceKind.Gamepad,
|
||||
deviceIndex,
|
||||
"gamepad");
|
||||
}
|
||||
|
||||
private static InputEventKey CreateKeyEvent(InputBindingDescriptor binding)
|
||||
{
|
||||
var code = ParseSingleSegment(binding.Code, "key");
|
||||
return new InputEventKey
|
||||
{
|
||||
Keycode = (Key)code,
|
||||
PhysicalKeycode = (Key)code
|
||||
};
|
||||
}
|
||||
|
||||
private static InputEventMouseButton CreateMouseButtonEvent(InputBindingDescriptor binding)
|
||||
{
|
||||
var buttonIndex = ParseSingleSegment(binding.Code, "mouse");
|
||||
return new InputEventMouseButton
|
||||
{
|
||||
ButtonIndex = (MouseButton)buttonIndex
|
||||
};
|
||||
}
|
||||
|
||||
private static InputEventJoypadButton CreateGamepadButtonEvent(InputBindingDescriptor binding)
|
||||
{
|
||||
var buttonIndex = ParseSingleSegment(binding.Code, "joy-button");
|
||||
return new InputEventJoypadButton
|
||||
{
|
||||
ButtonIndex = (JoyButton)buttonIndex
|
||||
};
|
||||
}
|
||||
|
||||
private static InputEventJoypadMotion CreateGamepadAxisEvent(InputBindingDescriptor binding)
|
||||
{
|
||||
var parts = binding.Code.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length != 3 || !string.Equals(parts[0], "joy-axis", StringComparison.Ordinal))
|
||||
{
|
||||
throw new ArgumentException($"Binding code '{binding.Code}' is not a valid joy-axis code.", nameof(binding));
|
||||
}
|
||||
|
||||
if (!int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var axis))
|
||||
{
|
||||
throw new ArgumentException($"Binding code '{binding.Code}' does not contain a valid axis index.", nameof(binding));
|
||||
}
|
||||
|
||||
if (!float.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out var direction))
|
||||
{
|
||||
throw new ArgumentException($"Binding code '{binding.Code}' does not contain a valid axis direction.", nameof(binding));
|
||||
}
|
||||
|
||||
return new InputEventJoypadMotion
|
||||
{
|
||||
Axis = (JoyAxis)axis,
|
||||
AxisValue = direction
|
||||
};
|
||||
}
|
||||
|
||||
private static int ParseSingleSegment(string code, string prefix)
|
||||
{
|
||||
var parts = code.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length != 2 || !string.Equals(parts[0], prefix, StringComparison.Ordinal))
|
||||
{
|
||||
throw new ArgumentException($"Binding code '{code}' is not a valid {prefix} code.", nameof(code));
|
||||
}
|
||||
|
||||
if (!int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
throw new ArgumentException($"Binding code '{code}' does not contain a valid numeric value.", nameof(code));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static Key GetKeyCode(InputEventKey keyEvent)
|
||||
{
|
||||
return keyEvent.PhysicalKeycode != Key.None ? keyEvent.PhysicalKeycode : keyEvent.Keycode;
|
||||
}
|
||||
|
||||
private static string GetAxisDisplayName(JoyAxis axis, float direction)
|
||||
{
|
||||
return direction >= 0f
|
||||
? FormattableString.Invariant($"{axis} Positive")
|
||||
: FormattableString.Invariant($"{axis} Negative");
|
||||
}
|
||||
}
|
||||
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