Compare commits

..

94 Commits
v0.3.1 ... main

Author SHA1 Message Date
gewuyou
699d0b4896
Merge pull request #345 from GeWuYou/feat/cqrs-optimization
Feat/cqrs optimization
2026-05-09 18:39:38 +08:00
gewuyou
6d5d4be20b docs(cqrs): 收口PR345评审反馈
- 更新 AGENTS 多智能体预算术语说明,明确 worker 波次停止边界

- 修复 StreamLifetimeBenchmarks 的取消传播、观测维度注释与枚举器特性写法

- 调整 benchmark README 与 CQRS ai-plan 恢复文档,移除过期 PR 锚点和旧 diff 统计
2026-05-09 17:10:54 +08:00
gewuyou
9ffe3ba237 test(cqrs-benchmarks): 补充stream lifetime双观测口径
- 新增 StreamLifetimeBenchmarks 的 FirstItem 与 DrainAll 观测模式,用于拆分建流瞬时成本与完整枚举成本

- 更新 cqrs-rewrite 恢复文档与 benchmark README,同步 RP-127 的验证结果、branch diff 与下一恢复点
2026-05-09 16:19:14 +08:00
gewuyou
b7fa3eee29 perf(cqrs): 优化 stream 建流常量成本
- 优化 generated stream dispatch binding 为按响应类型缓存强类型 invoker 与 pipeline executor,压缩 CreateStream 热路径桥接开销

- 保持 stream 异常契约与行为缓存语义不变,并补齐相关 XML 注释与必要内联说明

- 补充 generated stream binding 与 pipeline executor 复用回归,覆盖 generated invoker 与 stream pipeline 组合场景
2026-05-09 16:14:16 +08:00
gewuyou
228e954d2d docs(ai-workflow): 补充多agent协作治理入口
- 新增 gframework-multi-agent-batch skill 及其公开入口说明

- 更新 AGENTS.md 中主 Agent 协调多 worker 的职责与停机约束

- 补充 ai-plan-governance 主题的 public recovery 入口与验证记录
2026-05-09 15:56:15 +08:00
gewuyou
d4735aec25 docs(cqrs-benchmarks): 更新基准工程说明
- 更新 RequestLifetimeBenchmarks 的 generated-provider 宿主说明

- 更新 StreamLifetimeBenchmarks 的 reflection/generated/MediatR 对照口径说明

- 补充 benchmark 工程的最小构建与过滤运行示例
2026-05-09 14:53:30 +08:00
gewuyou
a07d1c4076 docs(cqrs-rewrite): 同步 benchmark 恢复文档入口
- 更新 active tracking 的恢复点、branch diff 与 benchmark 口径。

- 补充 request lifetime 与 stream lifetime 的最新权威验证结果。

- 推进 trace 到 RP-125 与 RP-126,并记录下一推荐步骤。
2026-05-09 14:49:32 +08:00
gewuyou
9107e23268 test(cqrs-benchmarks): 补齐 stream lifetime 三方对照口径
- 拆分 GFramework stream lifetime benchmark 的 reflection、generated 与 MediatR 独立请求和 handler 类型

- 调整 generated stream registry 仅绑定 generated 口径,避免静态 dispatcher 缓存污染对照结果

- 验证 StreamLifetimeBenchmarks 在 Singleton 与 Transient 下均产出完整四方对照结果
2026-05-09 12:56:27 +08:00
gewuyou
f9c9561f40 fix(cqrs-benchmarks): 对齐 request lifetime 宿主路径
- 新增 request lifetime benchmark 的 generated registry,提供最小 generated request descriptor。

- 更新 RequestLifetimeBenchmarks 使用 generated-provider 宿主,并保留 Singleton/Transient 生命周期矩阵控制。

- 补充 dispatcher 缓存清理,避免生命周期矩阵之间互相污染 benchmark 结果。
2026-05-09 12:54:19 +08:00
gewuyou
f9dd105bcc perf(cqrs): 缓存 stream pipeline 存在性判定
- 优化 CqrsDispatcher 的 CreateStream 热路径,按 dispatcher 实例缓存 stream pipeline behavior 的服务可见性

- 新增 stream presence cache 回归与最小测试桩,锁住同容器共享、跨容器隔离的缓存语义

- 更新 cqrs-rewrite 恢复文档并补充本轮 stream benchmark 验证结果
2026-05-09 12:42:03 +08:00
gewuyou
d85828c533
Merge pull request #344 from GeWuYou/feat/cqrs-optimization
Feat/cqrs optimization
2026-05-09 12:25:41 +08:00
gewuyou
17e7f64e71 fix(cqrs): 收口PR344通知发布评审问题
- 修复 CqrsDispatcher 默认通知发布器热路径的重复解析与默认实例重复分配

- 补充 strict IIocContainer 测试装配与通知发布器唯一注册断言

- 重构 CqrsDispatcherCacheTests 共享容器装配并更新 cqrs-rewrite 恢复文档
2026-05-09 11:14:32 +08:00
gewuyou
56dc4fd343 fix(cqrs): 缓存零管道请求的行为判定
- 新增 dispatcher 实例级 request behavior presence cache,减少零管道请求 steady-state 的容器查询开销

- 补充 dispatcher cache 回归并更新 cqrs-rewrite active tracking 与 trace,记录 request benchmark 和 lifetime benchmark 结果
2026-05-09 09:25:08 +08:00
gewuyou
3fbc563d59
Merge pull request #343 from GeWuYou/feat/ai-first-config
Feat/添加数组形状关键字的验证与拒绝机制
2026-05-09 09:17:55 +08:00
gewuyou
4ccc36aac9 fix(cqrs): 补齐架构启动的 notification publisher 回归
- 新增标准 Architecture 启动路径下自定义 notification publisher 的集成回归,验证 Configurator 注册的策略会被真实 publish 路径复用

- 更新 cqrs-rewrite active tracking 与 trace,推进恢复点到 RP-121 并记录当前验证结果与下一步边界
2026-05-09 08:54:14 +08:00
gewuyou
a36b5978c4 fix(ai-first-config): 补齐PR审查遗漏的文档与追踪记录
- 补充两个参数化测试方法缺失的 XML param 注释
- 修复 trace 中触发 MD024 的重复三级标题
- 更新 ai-plan 跟踪与 PR #343 follow-up 验证记录
2026-05-09 08:51:51 +08:00
gewuyou
000c3e4c45 fix(cqrs): 修复 notification publisher 默认接线
- 修复默认 CQRS runtime 在工厂层过早固化顺序 publisher 的问题

- 更新 dispatcher 与基础设施接线,确保组合根注册的 publisher 能在标准 publish 路径生效

- 补充 notification publisher 回归并更新 cqrs-rewrite 的 RP-120 恢复点
2026-05-09 08:41:44 +08:00
gewuyou
6af600d7b9 test(cqrs): 补齐 notification publisher 泛型注册回归
- 补充 UseNotificationPublisher<TPublisher>() 的单例解析与重复注册回归覆盖

- 更新 notification publisher 组合根文档,说明实例重载与泛型重载的生命周期边界

- 更新 cqrs-rewrite 跟踪与追踪文档,记录 RP-119 的验证结果与下一恢复点
2026-05-09 08:26:43 +08:00
gewuyou
d389eb36c1
Merge pull request #342 from GeWuYou/feat/cqrs-optimization
Feat/cqrs optimization
2026-05-08 20:08:33 +08:00
gewuyou
59ceb06f2d fix(cqrs): 收口 PR342 审查遗留问题
- 修复 NotificationFanOutBenchmarks 中 MediatR handler 绕过 HandleCore 的对照偏差

- 更新 README 与中文文档中的 notification publisher 示例和表格格式

- 同步 cqrs-rewrite tracking 与 trace 到 PR #342 审查恢复点和最新验证结果
2026-05-08 19:29:45 +08:00
gewuyou
4121e12909 docs(cqrs): 收口 notification publisher 采用说明
- 更新 notification publisher 的策略选择矩阵,明确顺序、并行与自定义 publisher 的适用边界

- 补充 CQRS 重写 tracking 与 trace,记录已撤回的无收益 request 热路径实验和当前恢复点
2026-05-08 18:56:37 +08:00
gewuyou
59ec255878 feat(cqrs): 公开顺序 notification publisher 策略
- 新增公开 SequentialNotificationPublisher,并让默认 runtime 回退复用该策略

- 增加顺序 notification publisher 组合根注册入口,并更新测试文档与恢复点
2026-05-08 17:57:57 +08:00
gewuyou
310791db5a feat(cqrs): 收口 notification publisher 配置入口
- 新增 notification publisher 组合根注册扩展,提供 TaskWhenAll 与自定义策略入口

- 补充通知发布策略配置回归测试,并更新 CQRS 文档与恢复点记录
2026-05-08 17:53:27 +08:00
gewuyou
b0102b5206 test(cqrs): 补充 notification publisher fan-out 基准对照
- 新增默认顺序发布器与 TaskWhenAllNotificationPublisher 的 fixed 4 handler fan-out benchmark 对照

- 更新 benchmark README 与 cqrs-rewrite 恢复文档,记录 RP-114 的性能结论与下一步
2026-05-08 17:42:48 +08:00
gewuyou
7ff4b628a1 feat(cqrs): 新增并行通知发布策略
- 新增 TaskWhenAllNotificationPublisher 内置并行通知发布器并保留默认顺序语义

- 补充通知发布策略回归测试与采用边界文档

- 更新 cqrs-rewrite 跟踪与执行追踪恢复点
2026-05-08 17:25:42 +08:00
gewuyou
c7af175f2e fix(cqrs): 补充通知扇出基准
- 新增 NotificationFanOutBenchmarks,量化固定四处理器 notification publish 对照

- 更新 benchmark README,补充 notification fan-out 场景说明

- 更新 cqrs-rewrite active tracking 与 trace,记录 RP-112 的基线、验证结果与下一恢复建议
2026-05-08 17:09:08 +08:00
gewuyou
98c5b14bd5 fix(cqrs): 补齐通知基准对照
- 新增 NotificationBenchmarks 的 Mediator concrete runtime 对照与对应通知合同实现

- 更新 benchmark README,明确 notification publish 已扩成三方对照

- 更新 cqrs-rewrite active tracking 与 trace,记录 RP-111 的基线、验证结果与下一恢复建议
2026-05-08 16:27:26 +08:00
gewuyou
7ca21af92d
Merge pull request #341 from GeWuYou/feat/cqrs-optimization
Feat/cqrs optimization
2026-05-08 16:12:20 +08:00
gewuyou
769d036434 fix(cqrs): 收口PR341剩余review尾项
- 修复 request faulted ValueTask 回归测试对 pipeline 探测顺序的隐式依赖,补齐 HasRegistration 与 GetAll 的防御性 mock

- 更新 cqrs-rewrite tracking 与 trace,记录 PR #341 latest-head review 的 stale thread 复核结论与本轮验证结果
2026-05-08 15:06:24 +08:00
gewuyou
9bd8c34693 fix(cqrs): 收口PR审查遗留问题
- 修复 benchmark 宿主误激活同程序集其他 generated registry 的接线路径,收窄服务索引与 descriptor 基线

- 恢复 CqrsDispatcher.SendAsync 的 faulted ValueTask 失败语义,并补充相关回归测试

- 补充 legacy runtime alias 的防守式类型检查、stream lifetime 注释与 cqrs-rewrite 恢复文档验证记录
2026-05-08 14:10:06 +08:00
gewuyou
39ac61c095 fix(cqrs): 补齐流式生命周期基准矩阵
- 新增 stream handler 的 Singleton 和 Transient 生命周期 benchmark,并沿用 generated-provider 宿主接线

- 更新 CQRS benchmark README 与 active ai-plan 恢复点,记录 RP-108 的验证结果和下一步建议
2026-05-08 13:03:00 +08:00
gewuyou
24462b0035 perf(cqrs): 收口默认流式基准宿主
- 新增默认 stream benchmark 的 handwritten generated registry,并通过真实程序集注册路径接上 generated stream invoker provider

- 更新 StreamingBenchmarks 宿主接线、README 与 RP-107 recovery 文档,统一 request、pipeline、stream 默认宿主口径

- 更新 gframework-boot 与 gframework-batch-boot 技能,改为以上下文预算接近约 80% 为默认优先停止信号
2026-05-08 12:47:24 +08:00
gewuyou
c82e981b7e perf(cqrs): 收口请求管线基准宿主
- 新增 request pipeline benchmark 的 handwritten generated request registry,并通过真实程序集注册路径接上 generated invoker provider

- 更新 RequestPipelineBenchmarks 宿主接线与 benchmark README,统一默认 request 与 pipeline 场景的 generated-provider 口径

- 更新 CQRS 迁移 tracking 与 trace,记录 RP-106 的基线、验证结果与下一恢复点
2026-05-08 12:38:18 +08:00
gewuyou
d9547dae4b perf(cqrs): 收口默认请求基准宿主
- 新增 handwritten generated request registry,并让默认 RequestBenchmarks 通过真实程序集注册路径接上 generated invoker provider

- 补齐 benchmark 最小宿主所需的 CQRS runtime、registrar 与 registration service 基础设施接线

- 更新 CQRS 迁移 tracking 与 trace,记录 RP-105 的 benchmark 结论和当前恢复点
2026-05-08 12:23:05 +08:00
gewuyou
120a1487f5 perf(cqrs): 收口请求热路径常量开销
- 优化 CqrsDispatcher.SendAsync 的 direct-return ValueTask 路径,移除 dispatcher 自身的异步状态机开销

- 引入 MicrosoftDiContainer 冻结后服务键索引,收敛 HasRegistration(Type) 的重复描述符扫描

- 更新 cqrs-rewrite active tracking 与 trace,记录 RP-104 的基线、验证结果与下一批建议
2026-05-08 11:38:27 +08:00
gewuyou
4d6dbba6a0
Merge pull request #340 from GeWuYou/feat/cqrs-optimization
Feat/cqrs optimization
2026-05-08 11:13:33 +08:00
gewuyou
32eeb41f29 fix(cqrs): 修复 HasRegistration 评审回归
- 修复 HasRegistration(Type) 的服务键判定,避免将仅按具体类型注册的行为误判为接口已注册

- 补充 strict mock 场景与 HasRegistration 回归测试,并修复 PR #340 暴露的 stream context validation 失败

- 更新 IoC 与 benchmark 文档注释,同步 cqrs-rewrite tracking/trace 到 PR #340 / RP-103
2026-05-08 10:54:37 +08:00
gewuyou
5da4a5893b perf(cqrs): 收紧性能回归门槛并忽略基准产物
- 更新 BenchmarkDotNet 生成目录忽略规则,避免本地基准产物污染工作树

- 补充 CQRS benchmark 回归要求与性能目标,要求相关改动后复跑 request 基准

- 更新 cqrs-rewrite 跟踪文档并记录最新 request 基准结果
2026-05-08 10:30:24 +08:00
gewuyou
18018966f9 perf(cqrs): 优化请求分发热路径并补充 Mediator 对照基准
- 优化 dispatcher 在零 pipeline 场景下跳过空行为解析,减少请求热路径分配

- 修复 MicrosoftDiContainer 热路径的无效 debug 字符串构造,并新增非激活注册检测回归测试

- 新增基于 NuGet 的 Mediator 对照基准并更新 CQRS 重写跟踪文档
2026-05-08 09:41:27 +08:00
gewuyou
6a582d0b0b fix(game-config): 收紧数组形状关键字边界
- 修复 Runtime、Generator 与 Tooling 对 prefixItems、additionalItems、unevaluatedItems 的静默接受边界

- 补充 GFramework.Game 与 SourceGenerators 的数组形状关键字回归测试和诊断契约

- 更新 reader-facing 文档与 ai-plan 恢复点,记录新的数组 schema 支持边界
2026-05-08 09:40:14 +08:00
gewuyou
5dc2dd25b9
Merge pull request #339 from GeWuYou/feat/cqrs-optimization
feat(cqrs): 补齐流式管道行为接缝
2026-05-08 09:08:37 +08:00
gewuyou
e44c56fb46 fix(cqrs): 收口 PR339 流式管道评审问题
- 修复 MicrosoftDiContainer 中 request 与 stream 行为注册逻辑的重复实现并统一校验路径

- 补充流式管道注册入口与 continuation 缓存的 XML 契约说明,明确并发与冻结前调用约束

- 更新 cqrs-rewrite 跟踪文档并修正 ICqrsRequestInvokerProvider 的 XML 缩进格式问题
2026-05-08 08:49:19 +08:00
gewuyou
aebf1e974d feat(cqrs): 补齐流式管道行为接缝
- 新增 stream pipeline 契约、dispatcher executor 缓存与 generated invoker 兼容路径

- 补充 Architecture 与 IOC 的流式管道注册入口及对应回归测试

- 更新 CQRS 文档和 cqrs-rewrite 的 active tracking/trace
2026-05-08 08:20:48 +08:00
gewuyou
3e1ce089af docs(config-tool): 对齐对象数组嵌套编辑说明
- 更新 VS Code 配置工具 README,收紧 raw YAML 回退条件并同步实际对象数组编辑边界

- 修复中文配置工具文档对嵌套对象数组能力的过时描述,明确仅在超出共享子集时回退

- 补充 ai-plan tracking 与 trace,记录本轮文档对齐、验证结果与批处理停止条件
2026-05-08 07:37:03 +08:00
gewuyou
02a60df718
Merge pull request #335 from GeWuYou/dependabot/nuget/Meziantou.Analyzer-3.0.72
Bump Meziantou.Analyzer from 3.0.60 to 3.0.72
2026-05-07 22:00:58 +08:00
gewuyou
77820da820
Merge pull request #336 from GeWuYou/dependabot/nuget/Meziantou.Polyfill-1.0.123
Bump Meziantou.Polyfill from 1.0.121 to 1.0.123
2026-05-07 22:00:48 +08:00
gewuyou
55639c559c
Merge pull request #337 from GeWuYou/dependabot/nuget/GFramework.Cqrs.Benchmarks/Microsoft.Extensions.Logging-10.0.7
Bump Microsoft.Extensions.Logging from 10.0.0 to 10.0.7
2026-05-07 22:00:35 +08:00
gewuyou
042b74473f
Merge pull request #338 from GeWuYou/dependabot/nuget/GFramework.Core.Tests/multi-6f1e76e95e
Bump NUnit from 4.5.1 to 4.6.0
2026-05-07 21:59:59 +08:00
dependabot[bot]
55c2a1ae69
Bump NUnit from 4.5.1 to 4.6.0
---
updated-dependencies:
- dependency-name: NUnit
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: NUnit
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: NUnit
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: NUnit
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: NUnit
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: NUnit
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: NUnit
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-07 13:14:14 +00:00
dependabot[bot]
debc9f27ac
Bump Microsoft.Extensions.Logging from 10.0.0 to 10.0.7
---
updated-dependencies:
- dependency-name: Microsoft.Extensions.Logging
  dependency-version: 10.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-07 13:10:38 +00:00
dependabot[bot]
8f6e6e121e
Bump Meziantou.Polyfill from 1.0.121 to 1.0.123
---
updated-dependencies:
- dependency-name: Meziantou.Polyfill
  dependency-version: 1.0.123
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-07 13:09:51 +00:00
dependabot[bot]
d010026448
Bump Meziantou.Analyzer from 3.0.60 to 3.0.72
---
updated-dependencies:
- dependency-name: Meziantou.Analyzer
  dependency-version: 3.0.72
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-07 13:08:44 +00:00
gewuyou
54b79d99d3
Merge pull request #334 from GeWuYou/feat/cqrs-optimization
Feat/Implement CQRS runtime integration for legacy compatibility
2026-05-07 20:47:08 +08:00
gewuyou
ffb0a8aff5 fix(core): 收窄 legacy bridge 上下文回退异常边界
- 修复 LegacyCqrsDispatchHelper 仅在上下文缺失时回退,避免吞掉真实 InvalidOperationException

- 补充 CommandExecutor 与 QueryExecutor 相关回归测试,覆盖 fallback 与异常冒泡语义

- 更新 cqrs-rewrite 跟踪与追踪文档,记录 PR #334 本轮复核与验证结果
2026-05-07 20:35:47 +08:00
gewuyou
44d1a89a0b test(core): 补强 legacy bridge 上下文断言
- 补充 AsyncQueryExecutor 与 CommandExecutor bridge 测试的上下文保留断言

- 优化 RecordingCqrsRuntime 的 bridge 执行模拟与响应类型诊断

- 更新 cqrs-rewrite active tracking 与 trace 的 RP-097 验证记录
2026-05-07 20:17:46 +08:00
gewuyou
cca413042f chore(cqrs-rewrite): 同步PR334评审复核状态
- 更新 active tracking 与 trace 到 RP-096,记录 latest-head review 的最新权威结论

- 补充 PR #334 当前 stale open thread、CI 测试与 MegaLinter 噪音的本地复核结果
2026-05-07 19:52:14 +08:00
gewuyou
dc3bd3744e fix(core): 收口 legacy bridge 同步评审问题
- 修复 legacy 同步 bridge 的 runtime 等待方式,统一通过共享 helper 隔离同步上下文并收口重复 dispatch-context 解析逻辑

- 补充 legacy async command bridge 的取消可见性,并更新 ICqrsRuntime 与相关入口的契约说明

- 新增 bridge 回归测试并更新 cqrs-rewrite active tracking,覆盖同步上下文隔离、测试容器释放与取消语义
2026-05-07 19:00:49 +08:00
gewuyou
6056159866 fix(core): 收口 legacy cqrs bridge 评审问题
- 修复 legacy bridge 测试装配与清理流程,改用 InternalsVisibleTo 和显式 handler 注册,补齐共享计数器重置与生命周期说明

- 优化 CommandExecutor、QueryExecutor 与相关模块的 runtime 契约,补充 XML 文档、nullable 注解和显式依赖解析

- 更新 legacy 异步 bridge 的取消语义、兼容文档回退边界以及 cqrs-rewrite active tracking/trace
2026-05-07 17:54:05 +08:00
gewuyou
d7293aa475 refactor(core): 统一旧版命令查询到Cqrs运行时
- 重构 Core 兼容命令查询入口,使 legacy SendCommand/SendQuery 通过内部 bridge request 复用统一 CQRS runtime

- 新增 legacy bridge handler 与真实启动路径回归测试,验证默认架构初始化会自动接入统一 pipeline

- 更新 Core 与 CQRS 文档及 cqrs-rewrite 跟踪,记录 Mediator 尚未吸收的能力差距与后续收口方向
2026-05-07 17:20:14 +08:00
gewuyou
017e689abd feat(cqrs): 补齐请求生命周期基准矩阵
- 新增 request handler Singleton 与 Transient 生命周期 benchmark,并说明 Scoped 对照的宿主前置条件

- 更新 benchmark README,补充当前覆盖范围与后续扩展方向

- 更新 cqrs-rewrite active tracking 与 trace,记录 RP-092 验证结果和沙箱外 benchmark 权威结论
2026-05-07 14:20:50 +08:00
gewuyou
2c58d8b69e
Merge pull request #333 from GeWuYou/refactor/single-context-priority
docs(ai-plan): 归档 single-context-priority 主题
2026-05-07 13:24:46 +08:00
GeWuYou
14cd1fc9a0 chore(benchmark): 删除错误的任务 2026-05-07 13:08:55 +08:00
GeWuYou
577c89fdf3 chore(benchmark): 归档已完成任务,删除错误的任务 2026-05-07 12:44:57 +08:00
gewuyou
a692190a77 docs(ai-plan): 归档 single-context-priority 主题
- 更新 public index,只保留仍处于活跃状态的 topic 与分支映射

- 归档 single-context-priority 主题目录到 public archive

- 补充 ai-plan-governance 跟踪与 trace,记录本次归档校正与验证结果
2026-05-07 12:23:18 +08:00
gewuyou
c3df2b2c96
Merge pull request #332 from GeWuYou/refactor/single-context-priority
Refactor/single context priority
2026-05-07 11:34:50 +08:00
gewuyou
ee8b6a4deb fix(core): 修复上下文销毁解绑与并发一致性
- 修复 GameContext 的别名字典与当前活动上下文同步边界,避免解绑与读取路径出现状态漂移
- 修复 Architecture.Destroy() 缺少全局解绑的问题,并补充相关生命周期 XML 文档
- 更新回归测试、CQRS 注册断言与 single-context-priority 跟踪记录
2026-05-07 10:43:07 +08:00
gewuyou
ff04a4fbad fix(core): 补齐架构销毁后的上下文解绑
- 修复 Architecture 销毁后 GameContext 仍保留活动上下文的问题

- 补充生命周期回归测试并验证失败初始化后的解绑路径

- 收口生成器文档中的多架构表述并更新 ai-plan 追踪
2026-05-07 10:03:16 +08:00
gewuyou
e3fa0db992 refactor(core): 收敛单活动上下文与预冻结查询
- 收敛 GameContext 为单活动上下文模型并保留类型别名兼容查找

- 统一 MicrosoftDiContainer 预冻结实例读取路径并补充 CQRS 注册阶段提示

- 更新 Core 测试、上下文文档与 ai-plan 追踪记录
2026-05-07 08:58:09 +08:00
gewuyou
c2d22285ed
Merge pull request #331 from GeWuYou/fix/package-validation-guard
fix(release): 前移发布包清单校验
2026-05-06 21:34:59 +08:00
gewuyou
e3d6aa5111 fix(release): 修复发布校验链路的审查遗留问题
- 修复 PR workflow 中 dotnet pack 重复构建整个 solution 的问题

- 优化 packed modules 校验脚本的 find 实现以兼容 BSD 环境

- 更新 cqrs-rewrite 活跃跟踪与追踪文档中的当前 PR 锚点和审查结论
2026-05-06 21:27:21 +08:00
gewuyou
30ddb841a9 fix(release): 前移发布包清单校验
- 修复 benchmark 项目误入发布面的风险,明确 GFramework.Cqrs.Benchmarks 保持不可打包。

- 新增共享 packed modules 校验脚本,并让 publish 与 CI 工作流复用同一份发布包名单规则。

- 更新 CQRS active tracking 与 trace,记录本轮发布校验前移的恢复点与验证结果。
2026-05-06 21:12:42 +08:00
gewuyou
c65c131d6a
Merge pull request #330 from GeWuYou/fix/microsoft-di-container-disposal
fix(core): 修复容器释放与基准资源泄漏
2026-05-06 20:47:32 +08:00
gewuyou
f0a2978882 fix(core): 修复容器并发释放重复销毁锁
- 修复 MicrosoftDiContainer 在并发 Dispose 场景下可能重复执行底层读写锁销毁的问题

- 补充 IocContainerLifetimeTests 回归用例以覆盖并发释放时的单次锁销毁约束

- 更新 microsoft-di-container-disposal 追踪文档记录剩余 PR review 处理结果
2026-05-06 20:39:38 +08:00
gewuyou
3233151207 fix(ioc): 修复容器释放竞态与清理路径
- 修复 MicrosoftDiContainer 在等待线程与并发 Dispose 场景下泄露底层锁异常的问题
- 更新 IIocContainer 释放契约文档并移除 Clear 中不可达的 provider 释放逻辑
- 新增 benchmark cleanup helper、并发释放回归测试与 ai-plan 恢复入口
2026-05-06 20:23:16 +08:00
gewuyou
0ec8aa076b fix(core): 修复容器释放与基准资源泄漏
- 修复 MicrosoftDiContainer 的 IDisposable 释放逻辑、根 ServiceProvider 清理与释放后访问保护
- 更新 CQRS benchmarks 的容器 cleanup,并补齐 RequestStartupBenchmarks 的冷启动容器释放路径
- 补充 Core 容器生命周期回归测试并归档 issue 327 的 ai-plan topic
2026-05-06 19:08:48 +08:00
gewuyou
588800bb7b
Merge pull request #329 from GeWuYou/chore/archive-completed-ai-plan-topics
chore(ai-plan): 归档已完成专题
2026-05-06 17:22:16 +08:00
gewuyou
ee41206965 chore(ai-plan): 归档已完成专题
- 更新 ai-plan 公共索引,移除 semantic-release-versioning、runtime-generator-boundary 和 github-issue-review-skill 的活跃入口与分支映射
- 归档 三个已完成 topic 的 tracking 与 trace 文档到 ai-plan/public/archive/ 下
2026-05-06 16:59:35 +08:00
gewuyou
db89918333
Merge pull request #328 from GeWuYou/feat/github-issue-review-skill
feat(skills): 新增 GitHub issue 分诊 skill
2026-05-06 16:51:02 +08:00
gewuyou
f25ccccad2 fix(skills): 修复 issue review skill 评审问题
- 修复 issue-review 脚本的代理回退、GitHub Token 认证与 JSON 输出契约

- 调整非 bug issue 的澄清判定并补充 docs、feature 分诊回归测试

- 更新 skill 示例占位符与 ai-plan 跟踪记录,收敛 PR #328 follow-up
2026-05-06 16:25:29 +08:00
gewuyou
ab9829044f feat(skills): 新增 GitHub issue 分诊 skill
- 新增 gframework-issue-review skill,支持抓取 issue 元数据、评论、timeline 与分诊摘要。

- 补充 JSON 输出、唯一 open issue 自动解析与 WSL Linux git 绑定兼容处理。

- 更新 ai-plan 恢复入口并增加脚本级测试与验证记录。
2026-05-06 15:40:48 +08:00
gewuyou
109bce6e9e
Merge pull request #326 from GeWuYou/feat/cqrs-optimization
Test/Add comprehensive CQRS benchmarking suite with reflection and generated invoker paths
2026-05-06 14:29:06 +08:00
gewuyou
6d619b9a1f fix(cqrs): 收敛 benchmark review 收尾问题
- 修复 benchmark workflow 过滤器输入的 shell 注入风险

- 统一 request 与 stream invoker 基准中 MediatR handler 的生命周期基线

- 更新 request pipeline benchmark 的缓存清理与空行为类型声明

- 压缩 cqrs-rewrite active 跟踪与 trace,记录本轮 PR review 收尾结论
2026-05-06 12:57:56 +08:00
gewuyou
2cb6216d05 fix(cqrs): 修复 benchmark 对照宿主与冷启动基线
- 新增 BenchmarkHostFactory 统一 benchmark 最小宿主构建,并限制 MediatR 扫描到当前场景所需类型

- 修复 GFramework benchmark 容器未冻结导致的首次 handler 解析缺口,恢复 RequestStartupBenchmarks 冷启动结果

- 优化 request、pipeline、notification、stream 与 invoker benchmark 的生命周期对齐,减少无关程序集扫描噪音

- 更新 cqrs-rewrite 跟踪与追踪文档,记录 PR #326 benchmark review 收敛、根因和验证结果
2026-05-06 12:09:20 +08:00
gewuyou
f71791ae98 ci(cqrs): 新增手动 benchmark 工作流
- 新增仅支持 workflow_dispatch 的 Benchmark workflow,默认只验证 benchmark 项目 Release build
- 补充可选 benchmark_filter 输入与 BenchmarkDotNet 工件上传,支持按场景手动执行基准测试
- 更新 cqrs-rewrite 跟踪与 trace,记录手动 benchmark workflow 的用途与当前 startup benchmark 残留风险
2026-05-06 11:48:15 +08:00
gewuyou
2ac02c1a6f fix(cqrs): 收敛 benchmark review 修复
- 修复 RequestStartupBenchmarks 的 baseline 分组、初始化阶段对齐与 MediatR 重复注册问题
- 新增共享 dispatcher cache helper,并统一 benchmark 宿主的 MediatR logging/license 过滤配置
- 更新 cqrs-rewrite 跟踪与 trace,记录 PR #326 锚点、验证去重和 startup benchmark 的残留运行风险
2026-05-06 11:07:33 +08:00
gewuyou
449eeb9606 feat(cqrs): 补齐 stream invoker 基准对照
- 新增 stream generated invoker benchmark 与手写 registry,对照 reflection runtime、generated runtime 和 MediatR 的完整枚举开销

- 更新 benchmark README,补充 generated stream invoker provider 的场景说明与后续扩展方向

- 更新 cqrs-rewrite 跟踪与 trace,记录 RP-089 的基线、验证结果和下一批建议
2026-05-06 09:46:52 +08:00
gewuyou
c01abac06e
Merge pull request #325 from GeWuYou/feat/ai-first-config
fix(game-config): 收紧开放对象关键字边界
2026-05-06 09:40:08 +08:00
gewuyou
6e1eaf8f5c test(cqrs): 补充请求调用器生成路径基准
- 新增 request reflection 与 generated invoker provider 的 steady-state 对照基准

- 引入 handwritten generated registry/provider 以走通真实 registrar 与 dispatcher 预热链路

- 更新 benchmark README 与 cqrs-rewrite RP-088 跟踪记录
2026-05-06 09:36:48 +08:00
gewuyou
e0bbf13d88 test(cqrs): 补充请求启动阶段基准
- 新增 request initialization 与 cold-start 基准并对齐当前 runtime 启动口径

- 通过清理 dispatcher 静态缓存隔离 GFramework.Cqrs 首次分发测量结果

- 更新 benchmark README 与 cqrs-rewrite RP-087 跟踪记录
2026-05-06 09:30:17 +08:00
gewuyou
f776d09f68 fix(ai-first-config): 收口开放对象评审跟进
- 修复 Runtime、Generator 与 Tooling 中开放对象关键字校验的不可达 additionalProperties 分支

- 补充 Tooling 对 additionalProperties false 的正向回归测试

- 更新游戏配置接入文档与 ai-plan 跟踪,记录 PR #325 的核验结论和验证结果
2026-05-06 09:25:59 +08:00
gewuyou
a8f98e467d test(cqrs): 补充请求管道数量矩阵基准
- 新增 request pipeline 0/1/4 数量矩阵基准并保持 GFramework.Cqrs 与 MediatR 对照

- 更新 benchmark README 说明当前场景覆盖与后续扩展方向

- 补充 cqrs-rewrite 跟踪与 trace 的 RP-086 恢复点和验证记录
2026-05-06 09:23:07 +08:00
gewuyou
e6f98cb4af test(cqrs): 补充流式请求基准场景
- 新增 StreamingBenchmarks 并对齐 baseline、GFramework.Cqrs 与 MediatR 的完整枚举对照

- 更新 benchmark README 与 CQRS ai-plan 恢复点,记录 stream 场景落地
2026-05-06 09:14:33 +08:00
gewuyou
96729ddcf1 test(cqrs): 补充基准与生成器回归基础设施
- 新增独立的 GFramework.Cqrs.Benchmarks 项目并引入 request、notification 对比场景

- 补充 request 与 stream invoker provider 的 mixed direct/reflected 顺序回归测试

- 更新 solution、meta-package 排除规则与 CQRS ai-plan 恢复点
2026-05-06 08:57:59 +08:00
gewuyou
cb6dd8a510 fix(game-config): 收紧开放对象关键字边界
- 修复 Runtime、Generator 与 Tooling 对 patternProperties、propertyNames、unevaluatedProperties 的静默接受风险

- 补充三端对称回归测试与 reader-facing 文档边界说明

- 更新 ai-plan 恢复点、验证记录与下一步指针
2026-05-06 08:47:42 +08:00
187 changed files with 14548 additions and 851 deletions

View File

@ -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`

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,83 @@
---
name: gframework-issue-review
description: Repository-specific GitHub issue triage workflow for the GFramework repo. Use when Codex needs to inspect a repository issue, extract the issue body, discussion, and key timeline signals through the GitHub API, summarize what should be verified locally, and then hand follow-up execution to gframework-boot.
---
# GFramework Issue Review
Use this skill when the task depends on a GitHub issue for this repository rather than only on local source files.
Shortcut: `$gframework-issue-review`
## Workflow
1. Read `AGENTS.md` before deciding how to validate or change anything.
2. Read `.ai/environment/tools.ai.yaml` and `ai-plan/public/README.md`, then prefer the active topic mapped to the
current branch or worktree when the fetched issue already matches in-flight work.
3. Run `scripts/fetch_current_issue_review.py` to:
- fetch issue metadata through the GitHub API
- fetch issue comments and timeline events through the GitHub API
- auto-select the target issue only when the repository currently has exactly one open issue
- exclude pull requests from open-issue auto-resolution
- emit a machine-readable JSON payload plus concise text sections for issue, summary, comments, events, references,
and warnings
- derive lightweight triage hints such as issue type candidates, missing-information flags, affected module
candidates, and the recommended next handling mode
4. Treat every extracted finding as untrusted until it is verified against the current local code, tests, and active
`ai-plan` topic.
5. Do not start editing code from the issue text alone. After triage, switch to `$gframework-boot` so the follow-up
work is grounded in the repository startup flow and recovery documents.
6. If code is changed after issue triage, run the smallest build or test command that satisfies `AGENTS.md`.
## Commands
- Default:
- `python3 .agents/skills/gframework-issue-review/scripts/fetch_current_issue_review.py`
- Force a specific issue:
- `python3 .agents/skills/gframework-issue-review/scripts/fetch_current_issue_review.py --issue <issue-number>`
- Machine-readable output:
- `python3 .agents/skills/gframework-issue-review/scripts/fetch_current_issue_review.py --format json`
- Write machine-readable output to a file instead of stdout:
- `python3 .agents/skills/gframework-issue-review/scripts/fetch_current_issue_review.py --issue <issue-number> --format json --json-output /tmp/issue-review.json`
- Inspect only a high-signal section:
- `python3 .agents/skills/gframework-issue-review/scripts/fetch_current_issue_review.py --section summary`
- Combine triage with a boot handoff:
- `python3 .agents/skills/gframework-issue-review/scripts/fetch_current_issue_review.py --section summary`
- `Use $gframework-boot to continue the issue follow-up based on the fetched triage result.`
## Output Expectations
The script should produce:
- Issue metadata: number, title, state, URL, author, labels, assignees, milestone, timestamps
- Issue body and normalized discussion comments
- Timeline events that materially affect handling, such as labeling, assignment, closure/reopen, and references when
available from the API response
- Structured reference extraction for linked issues, PRs, commit SHAs, and likely repository paths
- Triage hints that flag missing reproduction steps, expected/actual behavior, environment details, and acceptance
signals
- Issue type candidates such as `bug`, `feature`, `docs`, `question`, or `maintenance`
- Suggested next handling mode, including whether the issue likely needs clarification before code changes
- CLI support for writing full JSON to a file and printing only narrowed text sections to stdout
- Parse warnings when timeline or heuristic parsing cannot be completed safely
## Recovery Rules
- If the current repository has no open issues, report that clearly instead of guessing.
- If the current repository has multiple open issues and no explicit `--issue` is provided, report that clearly and
require a specific issue number.
- If GitHub access fails because of proxy configuration, rerun the fetch with proxy variables removed.
- Prefer GitHub API results over HTML scraping.
- Do not treat heuristic module guesses or next-step suggestions as repository truth; they are only entry points for
subsequent local verification.
- If the issue discussion reveals that the problem statement has already shifted, prefer the newest concrete comment or
timeline signal over the original title/body wording.
- After extracting the issue, continue the actual implementation flow with `$gframework-boot` so the task is grounded
in current branch context and `ai-plan` recovery artifacts.
## Example Triggers
- `Use $gframework-issue-review on the current repository issue`
- `Check the open GitHub issue and summarize what should be verified locally`
- `Inspect issue <issue-number> and tell me whether this looks like bug triage or a feature request`
- `先用 $gframework-issue-review 看当前 open issue再用 $gframework-boot 继续`

View File

@ -0,0 +1,4 @@
interface:
display_name: "GFramework Issue Review"
short_description: "Inspect the current repository issue and triage next steps"
default_prompt: "Use $gframework-issue-review to inspect the current repository issue through the GitHub API, summarize the issue body, discussion, and key timeline signals, highlight what must be verified locally, and then hand follow-up execution to $gframework-boot."

View File

@ -0,0 +1,858 @@
#!/usr/bin/env python3
# Copyright (c) 2025-2026 GeWuYou
# SPDX-License-Identifier: Apache-2.0
"""
Fetch the current GFramework GitHub issue and extract the signals needed for
local follow-up work without relying on gh CLI.
"""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import urllib.error
import urllib.request
from typing import Any
OWNER = "GeWuYou"
REPO = "GFramework"
WORKTREE_ROOT_DIRECTORY_NAME = "GFramework-WorkTree"
GIT_ENVIRONMENT_KEY = "GFRAMEWORK_WINDOWS_GIT"
GIT_DIR_ENVIRONMENT_KEY = "GFRAMEWORK_GIT_DIR"
WORK_TREE_ENVIRONMENT_KEY = "GFRAMEWORK_WORK_TREE"
REQUEST_TIMEOUT_ENVIRONMENT_KEY = "GFRAMEWORK_ISSUE_REVIEW_TIMEOUT_SECONDS"
GITHUB_TOKEN_ENVIRONMENT_KEYS = ("GFRAMEWORK_GITHUB_TOKEN", "GITHUB_TOKEN", "GH_TOKEN")
PROXY_ENVIRONMENT_KEYS = ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "all_proxy")
DEFAULT_REQUEST_TIMEOUT_SECONDS = 60
USER_AGENT = "codex-gframework-issue-review"
DISPLAY_SECTION_CHOICES = (
"issue",
"summary",
"comments",
"events",
"references",
"warnings",
)
ISSUE_TYPE_CANDIDATES = ("bug", "feature", "docs", "question", "maintenance")
ACTIVE_TOPIC_KEYWORDS: dict[str, tuple[str, ...]] = {
"ai-first-config-system": ("config", "configuration", "gameconfig", "settings"),
"coroutine-optimization": ("coroutine", "yield", "await", "scheduler"),
"cqrs-rewrite": ("cqrs", "command", "query", "eventbus", "event bus"),
"data-repository-persistence": ("repository", "serialization", "persistence", "data", "settings"),
"runtime-generator-boundary": ("source generator", "generator", "attribute", "packaging"),
"semantic-release-versioning": ("release", "version", "semantic-release", "tag", "publish"),
"documentation-full-coverage-governance": ("docs", "documentation", "readme", "vitepress", "api reference"),
}
ACTUAL_BEHAVIOR_PATTERNS = (
"actual",
"currently",
"instead",
"but",
"error",
"exception",
"fails",
"failed",
"wrong",
)
EXPECTED_BEHAVIOR_PATTERNS = (
"expected",
"should",
"want",
"would like",
"needs to",
)
REPRODUCTION_PATTERNS = (
"steps to reproduce",
"reproduce",
"reproduction",
"how to reproduce",
"minimal example",
"sample",
"demo",
)
ENVIRONMENT_PATTERNS = (
"windows",
"linux",
"macos",
"wsl",
"godot",
".net",
"sdk",
"version",
"environment",
)
ACCEPTANCE_PATTERNS = (
"acceptance",
"done when",
"definition of done",
"verified by",
"test plan",
)
FILE_PATH_PATTERN = re.compile(r"\b(?:[A-Za-z0-9_.-]+/)+[A-Za-z0-9_.-]+\b")
ISSUE_REFERENCE_PATTERN = re.compile(r"(?:^|\s)#(\d+)\b")
COMMIT_REFERENCE_PATTERN = re.compile(r"\b[0-9a-f]{7,40}\b")
LINE_BREAK_NORMALIZER = re.compile(r"\n{3,}")
def resolve_git_command() -> str:
"""Resolve the git executable to use for this repository."""
candidates = [
os.environ.get(GIT_ENVIRONMENT_KEY),
"git.exe",
"git",
]
for candidate in candidates:
if not candidate:
continue
if os.path.isabs(candidate):
if os.path.exists(candidate):
return candidate
continue
resolved_candidate = shutil.which(candidate)
if resolved_candidate:
return resolved_candidate
raise RuntimeError(f"No usable git executable found. Set {GIT_ENVIRONMENT_KEY} to override it.")
def find_repository_root(start_path: Path) -> Path | None:
"""Locate the repository root by walking parent directories for repo markers."""
for candidate in (start_path, *start_path.parents):
if (candidate / "AGENTS.md").exists() and (candidate / ".ai/environment/tools.ai.yaml").exists():
return candidate
return None
def resolve_worktree_git_dir(repository_root: Path) -> Path | None:
"""Resolve the main-repository worktree gitdir for this WSL worktree layout."""
if repository_root.parent.name != WORKTREE_ROOT_DIRECTORY_NAME:
return None
primary_repository_root = repository_root.parent.parent / REPO
candidate_git_dir = primary_repository_root / ".git" / "worktrees" / repository_root.name
return candidate_git_dir if candidate_git_dir.exists() else None
def resolve_git_invocation() -> list[str]:
"""Resolve the git command arguments, preferring explicit WSL worktree binding."""
configured_git_dir = os.environ.get(GIT_DIR_ENVIRONMENT_KEY)
configured_work_tree = os.environ.get(WORK_TREE_ENVIRONMENT_KEY)
linux_git = shutil.which("git")
if configured_git_dir and configured_work_tree and linux_git:
return [linux_git, f"--git-dir={configured_git_dir}", f"--work-tree={configured_work_tree}"]
repository_root = find_repository_root(Path.cwd())
if repository_root is not None and linux_git:
worktree_git_dir = resolve_worktree_git_dir(repository_root)
if worktree_git_dir is not None:
return [linux_git, f"--git-dir={worktree_git_dir}", f"--work-tree={repository_root}"]
root_git_dir = repository_root / ".git"
if root_git_dir.exists():
return [linux_git, f"--git-dir={root_git_dir}", f"--work-tree={repository_root}"]
return [resolve_git_command()]
def resolve_request_timeout_seconds() -> int:
"""Return the GitHub request timeout in seconds."""
configured_timeout = os.environ.get(REQUEST_TIMEOUT_ENVIRONMENT_KEY)
if not configured_timeout:
return DEFAULT_REQUEST_TIMEOUT_SECONDS
try:
parsed_timeout = int(configured_timeout)
except ValueError as error:
raise RuntimeError(
f"{REQUEST_TIMEOUT_ENVIRONMENT_KEY} must be an integer number of seconds."
) from error
if parsed_timeout <= 0:
raise RuntimeError(f"{REQUEST_TIMEOUT_ENVIRONMENT_KEY} must be greater than zero.")
return parsed_timeout
def run_command(args: list[str]) -> str:
"""Run a command and return stdout, raising on failure."""
process = subprocess.run(args, capture_output=True, text=True, check=False)
if process.returncode != 0:
stderr = process.stderr.strip()
raise RuntimeError(f"Command failed: {' '.join(args)}\n{stderr}")
return process.stdout.strip()
def get_current_branch() -> str:
"""Return the current git branch name."""
return run_command([*resolve_git_invocation(), "rev-parse", "--abbrev-ref", "HEAD"])
def resolve_github_token() -> str | None:
"""Return the first configured GitHub token for authenticated API requests."""
for environment_key in GITHUB_TOKEN_ENVIRONMENT_KEYS:
token = os.environ.get(environment_key)
if token:
return token
return None
def build_request_headers(accept: str) -> dict[str, str]:
"""Build GitHub request headers and include auth when a token is available."""
headers = {"Accept": accept, "User-Agent": USER_AGENT}
token = resolve_github_token()
if token:
headers["Authorization"] = f"Bearer {token}"
return headers
def has_proxy_environment() -> bool:
"""Return whether the current process is configured to use an outbound proxy."""
return any(os.environ.get(environment_key) for environment_key in PROXY_ENVIRONMENT_KEYS)
def perform_request(url: str, headers: dict[str, str], *, disable_proxy: bool) -> tuple[str, Any]:
"""Execute a single HTTP request and return decoded text plus response headers."""
opener = (
urllib.request.build_opener(urllib.request.ProxyHandler({}))
if disable_proxy
else urllib.request.build_opener()
)
request = urllib.request.Request(url, headers=headers)
with opener.open(request, timeout=resolve_request_timeout_seconds()) as response:
return response.read().decode("utf-8", "replace"), response.headers
def open_url(url: str, accept: str) -> tuple[str, Any]:
"""Open a URL, retrying without proxies only when the configured proxy path fails."""
headers = build_request_headers(accept)
try:
return perform_request(url, headers, disable_proxy=False)
except urllib.error.HTTPError:
raise
except (urllib.error.URLError, TimeoutError, OSError):
if not has_proxy_environment():
raise
return perform_request(url, headers, disable_proxy=True)
def fetch_json(url: str, accept: str = "application/vnd.github+json") -> tuple[Any, Any]:
"""Fetch a JSON payload and its response headers from GitHub."""
text, headers = open_url(url, accept=accept)
return json.loads(text), headers
def extract_next_link(headers: Any) -> str | None:
"""Extract the next-page link from GitHub pagination headers."""
link_header = headers.get("Link")
if not link_header:
return None
match = re.search(r'<([^>]+)>;\s*rel="next"', link_header)
return match.group(1) if match else None
def fetch_paged_json(url: str, accept: str = "application/vnd.github+json") -> list[dict[str, Any]]:
"""Fetch every page from a paginated GitHub API endpoint."""
items: list[dict[str, Any]] = []
next_url: str | None = url
while next_url:
payload, headers = fetch_json(next_url, accept=accept)
if not isinstance(payload, list):
raise RuntimeError(f"Expected list payload from GitHub API, got {type(payload).__name__}.")
items.extend(payload)
next_url = extract_next_link(headers)
return items
def collapse_whitespace(text: str) -> str:
"""Collapse repeated whitespace into single spaces while preserving paragraph intent."""
normalized = text.replace("\r\n", "\n").replace("\r", "\n")
normalized = LINE_BREAK_NORMALIZER.sub("\n\n", normalized)
normalized = re.sub(r"[ \t]+", " ", normalized)
normalized = re.sub(r" *\n *", "\n", normalized)
return normalized.strip()
def truncate_text(text: str, max_length: int) -> str:
"""Collapse whitespace and truncate long text for CLI display."""
collapsed = collapse_whitespace(text)
if max_length <= 0 or len(collapsed) <= max_length:
return collapsed
return collapsed[: max_length - 3].rstrip() + "..."
def filter_open_issue_candidates(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Filter GitHub issue list responses down to non-PR issue items."""
return [item for item in items if not item.get("pull_request")]
def select_single_open_issue_number(items: list[dict[str, Any]]) -> int:
"""Resolve the target issue number when the repository has exactly one open issue."""
issues = filter_open_issue_candidates(items)
if not issues:
raise RuntimeError("No open GitHub issues found for this repository. Pass --issue <number> to inspect one.")
if len(issues) > 1:
numbers = ", ".join(str(item.get("number")) for item in issues[:5])
suffix = "" if len(issues) <= 5 else ", ..."
raise RuntimeError(
"Multiple open GitHub issues found for this repository "
f"({len(issues)} total: {numbers}{suffix}). Pass --issue <number> to inspect one."
)
return int(issues[0]["number"])
def resolve_issue_number(issue_number: int | None) -> tuple[int, str]:
"""Resolve the issue number, auto-selecting only when exactly one open issue exists."""
if issue_number is not None:
return issue_number, "explicit"
open_items = fetch_paged_json(f"https://api.github.com/repos/{OWNER}/{REPO}/issues?state=open&per_page=100")
return select_single_open_issue_number(open_items), "auto-single-open-issue"
def fetch_issue_metadata(issue_number: int) -> dict[str, Any]:
"""Fetch normalized metadata for a GitHub issue."""
payload, _ = fetch_json(f"https://api.github.com/repos/{OWNER}/{REPO}/issues/{issue_number}")
if not isinstance(payload, dict):
raise RuntimeError("Failed to fetch GitHub issue metadata.")
if payload.get("pull_request"):
raise RuntimeError(f"Item #{issue_number} is a pull request, not a plain issue.")
labels = []
for label in payload.get("labels", []):
if isinstance(label, dict) and label.get("name"):
labels.append(str(label["name"]))
assignees = []
for assignee in payload.get("assignees", []):
login = assignee.get("login")
if login:
assignees.append(str(login))
milestone_title = None
milestone = payload.get("milestone")
if isinstance(milestone, dict) and milestone.get("title"):
milestone_title = str(milestone["title"])
return {
"number": int(payload["number"]),
"title": str(payload["title"]),
"state": str(payload["state"]).upper(),
"url": str(payload["html_url"]),
"author": str(payload.get("user", {}).get("login") or ""),
"created_at": str(payload.get("created_at") or ""),
"updated_at": str(payload.get("updated_at") or ""),
"labels": labels,
"assignees": assignees,
"milestone": milestone_title,
"body": str(payload.get("body") or ""),
}
def fetch_issue_comments(issue_number: int) -> list[dict[str, Any]]:
"""Fetch issue comments for the selected issue."""
return fetch_paged_json(f"https://api.github.com/repos/{OWNER}/{REPO}/issues/{issue_number}/comments?per_page=100")
def fetch_issue_timeline(issue_number: int) -> list[dict[str, Any]]:
"""Fetch issue timeline events when GitHub exposes them to the current client."""
return fetch_paged_json(
f"https://api.github.com/repos/{OWNER}/{REPO}/issues/{issue_number}/timeline?per_page=100",
accept="application/vnd.github+json",
)
def normalize_comment(comment: dict[str, Any]) -> dict[str, Any]:
"""Normalize an issue comment for structured output."""
return {
"id": int(comment.get("id") or 0),
"author": str(comment.get("user", {}).get("login") or ""),
"created_at": str(comment.get("created_at") or ""),
"updated_at": str(comment.get("updated_at") or ""),
"body": str(comment.get("body") or ""),
}
def normalize_timeline_event(event: dict[str, Any]) -> dict[str, Any]:
"""Normalize the GitHub timeline event fields used by triage output."""
actor = str(event.get("actor", {}).get("login") or "")
created_at = str(event.get("created_at") or event.get("submitted_at") or "")
event_type = str(event.get("event") or event.get("__typename") or "unknown")
label_name = ""
assignee = ""
source_issue_number: int | None = None
source_issue_url = ""
commit_id = ""
label = event.get("label")
if isinstance(label, dict) and label.get("name"):
label_name = str(label["name"])
assignee_payload = event.get("assignee")
if isinstance(assignee_payload, dict) and assignee_payload.get("login"):
assignee = str(assignee_payload["login"])
source = event.get("source")
if isinstance(source, dict):
issue_payload = source.get("issue")
if isinstance(issue_payload, dict):
if issue_payload.get("number"):
source_issue_number = int(issue_payload["number"])
if issue_payload.get("html_url"):
source_issue_url = str(issue_payload["html_url"])
commit_id_value = event.get("commit_id")
if isinstance(commit_id_value, str):
commit_id = commit_id_value
return {
"event": event_type,
"actor": actor,
"created_at": created_at,
"label": label_name,
"assignee": assignee,
"commit_id": commit_id,
"source_issue_number": source_issue_number,
"source_issue_url": source_issue_url,
}
def gather_text_blocks(issue: dict[str, Any], comments: list[dict[str, Any]]) -> list[str]:
"""Return the issue body plus discussion comment bodies for heuristic parsing."""
blocks = [issue.get("body", "")]
blocks.extend(comment.get("body", "") for comment in comments)
return [block for block in blocks if block]
def has_any_pattern(text_blocks: list[str], patterns: tuple[str, ...]) -> bool:
"""Return whether any normalized text block contains any requested pattern."""
lowered_blocks = [collapse_whitespace(block).lower() for block in text_blocks]
return any(pattern in block for block in lowered_blocks for pattern in patterns)
def choose_issue_type_candidates(issue: dict[str, Any], text_blocks: list[str]) -> list[str]:
"""Infer lightweight issue-type candidates from labels and discussion text."""
labels = [label.lower() for label in issue.get("labels", [])]
text = "\n".join(text_blocks).lower()
candidates: list[str] = []
if any(label in {"bug", "regression"} for label in labels) or "bug" in text or "error" in text or "fails" in text:
candidates.append("bug")
if any(label in {"feature", "enhancement"} for label in labels) or "feature" in text or "support" in text:
candidates.append("feature")
if any(label in {"documentation", "docs"} for label in labels) or "documentation" in text or "readme" in text:
candidates.append("docs")
if any(label in {"question", "help wanted"} for label in labels) or "?" in issue.get("title", ""):
candidates.append("question")
if any(label in {"chore", "maintenance", "refactor"} for label in labels) or "cleanup" in text or "refactor" in text:
candidates.append("maintenance")
if not candidates:
candidates.append("question" if issue.get("body", "").strip().endswith("?") else "bug")
ordered_candidates: list[str] = []
for candidate in ISSUE_TYPE_CANDIDATES:
if candidate in candidates:
ordered_candidates.append(candidate)
return ordered_candidates
def extract_references_from_text(text: str) -> dict[str, list[str]]:
"""Extract issue, commit, and file-path references from one text block."""
issue_numbers = sorted({match.group(1) for match in ISSUE_REFERENCE_PATTERN.finditer(text)}, key=int)
commit_shas = sorted({match.group(0) for match in COMMIT_REFERENCE_PATTERN.finditer(text)})
file_paths = sorted({match.group(0) for match in FILE_PATH_PATTERN.finditer(text)})
return {
"issues": [f"#{number}" for number in issue_numbers],
"commit_shas": commit_shas,
"file_paths": file_paths,
}
def merge_reference_values(values: list[dict[str, list[str]]]) -> dict[str, list[str]]:
"""Merge extracted reference lists while preserving sorted unique output."""
merged: dict[str, set[str]] = {"issues": set(), "commit_shas": set(), "file_paths": set()}
for value in values:
for key in merged:
merged[key].update(value.get(key, []))
return {
"issues": sorted(merged["issues"], key=lambda item: int(item[1:])),
"commit_shas": sorted(merged["commit_shas"]),
"file_paths": sorted(merged["file_paths"]),
}
def build_references(issue: dict[str, Any], comments: list[dict[str, Any]], events: list[dict[str, Any]]) -> dict[str, Any]:
"""Build structured references from issue text and timeline context."""
extracted = [extract_references_from_text(issue.get("body", ""))]
extracted.extend(extract_references_from_text(comment.get("body", "")) for comment in comments)
merged = merge_reference_values(extracted)
referenced_by_timeline = sorted(
{
f"#{event['source_issue_number']}"
for event in events
if event.get("source_issue_number") is not None
},
key=lambda item: int(item[1:]),
)
pull_request_references = sorted(
{
issue_reference
for issue_reference in merged["issues"]
if issue_reference != f"#{issue['number']}"
},
key=lambda item: int(item[1:]),
)
return {
"issues": merged["issues"],
"pull_requests_or_issues": pull_request_references,
"commit_shas": merged["commit_shas"],
"file_paths": merged["file_paths"],
"timeline_cross_references": referenced_by_timeline,
}
def build_information_flags(
issue: dict[str, Any],
comments: list[dict[str, Any]],
issue_type_candidates: list[str],
) -> dict[str, bool]:
"""Derive missing-information and readiness flags with issue-type-aware heuristics."""
text_blocks = gather_text_blocks(issue, comments)
has_reproduction_steps = has_any_pattern(text_blocks, REPRODUCTION_PATTERNS)
has_expected_behavior = has_any_pattern(text_blocks, EXPECTED_BEHAVIOR_PATTERNS)
has_actual_behavior = has_any_pattern(text_blocks, ACTUAL_BEHAVIOR_PATTERNS)
has_environment_details = has_any_pattern(text_blocks, ENVIRONMENT_PATTERNS)
has_acceptance_signals = has_any_pattern(text_blocks, ACCEPTANCE_PATTERNS)
primary_issue_type = issue_type_candidates[0] if issue_type_candidates else "bug"
if primary_issue_type == "bug":
needs_clarification = not (
(has_actual_behavior and (has_reproduction_steps or has_environment_details))
or has_acceptance_signals
)
elif primary_issue_type in {"feature", "docs"}:
needs_clarification = not (has_expected_behavior or has_acceptance_signals)
elif primary_issue_type == "maintenance":
needs_clarification = not (has_expected_behavior or has_actual_behavior or has_acceptance_signals)
else:
needs_clarification = not (has_expected_behavior or has_actual_behavior or has_acceptance_signals)
return {
"has_reproduction_steps": has_reproduction_steps,
"has_expected_behavior": has_expected_behavior,
"has_actual_behavior": has_actual_behavior,
"has_environment_details": has_environment_details,
"has_acceptance_signals": has_acceptance_signals,
"needs_clarification": needs_clarification,
}
def choose_affected_topics(issue: dict[str, Any], comments: list[dict[str, Any]]) -> list[str]:
"""Map the issue discussion to likely active topics when obvious keyword matches exist."""
text = "\n".join(gather_text_blocks(issue, comments)).lower()
matches: list[str] = []
for topic, keywords in ACTIVE_TOPIC_KEYWORDS.items():
if any(keyword in text for keyword in keywords):
matches.append(topic)
return matches
def choose_next_action(
information_flags: dict[str, bool],
issue_type_candidates: list[str],
affected_topics: list[str],
) -> str:
"""Choose the next handling mode for boot handoff."""
if information_flags["needs_clarification"]:
return "clarify-issue-before-code"
if affected_topics:
return "resume-existing-topic-with-boot"
if "docs" in issue_type_candidates and issue_type_candidates[0] == "docs":
return "start-new-docs-topic-with-boot"
return "start-new-topic-with-boot"
def build_triage_hints(issue: dict[str, Any], comments: list[dict[str, Any]]) -> dict[str, Any]:
"""Build lightweight, reviewable triage hints for boot follow-up."""
text_blocks = gather_text_blocks(issue, comments)
issue_type_candidates = choose_issue_type_candidates(issue, text_blocks)
information_flags = build_information_flags(issue, comments, issue_type_candidates)
affected_topics = choose_affected_topics(issue, comments)
next_action = choose_next_action(information_flags, issue_type_candidates, affected_topics)
return {
"issue_type_candidates": issue_type_candidates,
"information_flags": information_flags,
"affected_active_topics": affected_topics,
"next_action": next_action,
"boot_handoff": {
"recommended_skill": "gframework-boot",
"mode": "resume" if affected_topics else "new",
"notes": (
"Use gframework-boot to verify the issue against local code and active ai-plan topics."
if not information_flags["needs_clarification"]
else "Use gframework-boot to record a clarification-first task before changing code."
),
},
}
def build_result(issue_number: int, branch: str, resolution_mode: str) -> dict[str, Any]:
"""Build the full issue review payload for the selected issue."""
parse_warnings: list[str] = []
issue = fetch_issue_metadata(issue_number)
raw_comments = fetch_issue_comments(issue_number)
comments = [normalize_comment(comment) for comment in raw_comments]
events: list[dict[str, Any]] = []
try:
raw_events = fetch_issue_timeline(issue_number)
events = [normalize_timeline_event(event) for event in raw_events]
except Exception as error: # noqa: BLE001
parse_warnings.append(f"Issue timeline could not be fetched or parsed: {error}")
references = build_references(issue, comments, events)
triage_hints = build_triage_hints(issue, comments)
return {
"issue": {
**issue,
"resolved_from_branch": branch,
"resolution_mode": resolution_mode,
},
"discussion": {
"comment_count": len(comments),
"comments": comments,
},
"events": {
"count": len(events),
"items": events,
},
"references": references,
"triage_hints": triage_hints,
"parse_warnings": parse_warnings,
}
def write_json_output(result: dict[str, Any], output_path: str) -> str:
"""Write the full JSON result to disk and return the destination path."""
destination_path = Path(output_path).expanduser()
destination_path.parent.mkdir(parents=True, exist_ok=True)
destination_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
return str(destination_path)
def summarize_events(events: list[dict[str, Any]]) -> list[str]:
"""Convert normalized events into concise text lines."""
lines: list[str] = []
for event in events:
summary = f"- {event['event']}"
details: list[str] = []
if event.get("actor"):
details.append(f"actor={event['actor']}")
if event.get("label"):
details.append(f"label={event['label']}")
if event.get("assignee"):
details.append(f"assignee={event['assignee']}")
if event.get("source_issue_number") is not None:
details.append(f"source_issue=#{event['source_issue_number']}")
if event.get("commit_id"):
details.append(f"commit={event['commit_id'][:12]}")
if event.get("created_at"):
details.append(f"at={event['created_at']}")
if details:
summary += " (" + ", ".join(details) + ")"
lines.append(summary)
return lines
def format_text(
result: dict[str, Any],
*,
sections: list[str] | None = None,
max_description_length: int = 400,
json_output_path: str | None = None,
) -> str:
"""Format the result payload into concise text output."""
lines: list[str] = []
selected_sections = set(sections or DISPLAY_SECTION_CHOICES)
issue = result["issue"]
triage_hints = result["triage_hints"]
discussion = result["discussion"]
events = result["events"]
references = result["references"]
if "issue" in selected_sections:
lines.append(f"Issue #{issue['number']}: {issue['title']}")
lines.append(f"State: {issue['state']}")
lines.append(f"Author: {issue['author']}")
lines.append(f"Labels: {', '.join(issue['labels']) if issue['labels'] else '(none)'}")
lines.append(f"Assignees: {', '.join(issue['assignees']) if issue['assignees'] else '(none)'}")
lines.append(f"Milestone: {issue['milestone'] or '(none)'}")
lines.append(f"Created: {issue['created_at']}")
lines.append(f"Updated: {issue['updated_at']}")
lines.append(f"Resolved from branch: {issue['resolved_from_branch'] or '(not branch-based)'}")
lines.append(f"Resolution mode: {issue['resolution_mode']}")
lines.append(f"URL: {issue['url']}")
if issue["body"]:
lines.append("Body:")
lines.append(truncate_text(issue["body"], max_description_length))
if "summary" in selected_sections:
lines.append("")
lines.append("Triage summary:")
lines.append("- Issue type candidates: " + ", ".join(triage_hints["issue_type_candidates"]))
information_flags = triage_hints["information_flags"]
lines.append(
"- Information flags: "
+ ", ".join(
[
f"repro={'yes' if information_flags['has_reproduction_steps'] else 'no'}",
f"expected={'yes' if information_flags['has_expected_behavior'] else 'no'}",
f"actual={'yes' if information_flags['has_actual_behavior'] else 'no'}",
f"environment={'yes' if information_flags['has_environment_details'] else 'no'}",
f"acceptance={'yes' if information_flags['has_acceptance_signals'] else 'no'}",
f"needs_clarification={'yes' if information_flags['needs_clarification'] else 'no'}",
]
)
)
lines.append(
"- Affected active topics: "
+ (", ".join(triage_hints["affected_active_topics"]) if triage_hints["affected_active_topics"] else "(none)")
)
lines.append(f"- Next action: {triage_hints['next_action']}")
lines.append(f"- Boot handoff: {triage_hints['boot_handoff']['notes']}")
if "comments" in selected_sections:
lines.append("")
lines.append(f"Discussion comments: {discussion['comment_count']}")
for comment in discussion["comments"]:
lines.append(f"- {comment['author']} at {comment['created_at']}")
lines.append(f" {truncate_text(comment['body'], max_description_length)}")
if "events" in selected_sections:
lines.append("")
lines.append(f"Timeline events: {events['count']}")
lines.extend(summarize_events(events["items"]))
if "references" in selected_sections:
lines.append("")
lines.append("References:")
lines.append("- Mentioned issues: " + (", ".join(references["issues"]) if references["issues"] else "(none)"))
lines.append(
"- Cross references: "
+ (
", ".join(references["timeline_cross_references"])
if references["timeline_cross_references"]
else "(none)"
)
)
lines.append(
"- Related issue/PR mentions: "
+ (
", ".join(references["pull_requests_or_issues"])
if references["pull_requests_or_issues"]
else "(none)"
)
)
lines.append("- Commit SHAs: " + (", ".join(references["commit_shas"]) if references["commit_shas"] else "(none)"))
lines.append("- File paths: " + (", ".join(references["file_paths"]) if references["file_paths"] else "(none)"))
if result["parse_warnings"] and "warnings" in selected_sections:
lines.append("")
lines.append("Warnings:")
for warning in result["parse_warnings"]:
lines.append(f"- {truncate_text(warning, max_description_length)}")
if json_output_path:
lines.append("")
lines.append(f"Full JSON written to: {json_output_path}")
return "\n".join(lines)
def parse_args() -> argparse.Namespace:
"""Parse CLI arguments."""
parser = argparse.ArgumentParser()
parser.add_argument("--branch", help="Override the current branch name.")
parser.add_argument("--issue", type=int, help="Fetch a specific issue number instead of auto-selecting one.")
parser.add_argument("--format", choices=("text", "json"), default="text")
parser.add_argument(
"--json-output",
help="Write the full JSON result to a file. When used with --format text, stdout stays concise and points to the file.",
)
parser.add_argument(
"--section",
action="append",
choices=DISPLAY_SECTION_CHOICES,
help="Limit text output to specific sections. Can be passed multiple times.",
)
parser.add_argument(
"--max-description-length",
type=int,
default=400,
help="Truncate long text bodies in text output to this many characters.",
)
return parser.parse_args()
def main() -> None:
"""Run the CLI entry point."""
args = parse_args()
branch = args.branch or get_current_branch()
issue_number, resolution_mode = resolve_issue_number(args.issue)
result = build_result(issue_number, branch, resolution_mode)
json_output_path: str | None = None
if args.json_output:
json_output_path = write_json_output(result, args.json_output)
if args.format == "json":
print(json.dumps(result, ensure_ascii=False, indent=2))
return
print(
format_text(
result,
sections=args.section,
max_description_length=args.max_description_length,
json_output_path=json_output_path,
)
)
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(str(error), file=sys.stderr)
sys.exit(1)

View File

@ -0,0 +1,94 @@
#!/usr/bin/env python3
# Copyright (c) 2025-2026 GeWuYou
# SPDX-License-Identifier: Apache-2.0
"""Regression tests for the GFramework issue review fetch helper."""
from __future__ import annotations
import importlib.util
from pathlib import Path
import unittest
SCRIPT_PATH = Path(__file__).with_name("fetch_current_issue_review.py")
MODULE_SPEC = importlib.util.spec_from_file_location("fetch_current_issue_review", SCRIPT_PATH)
if MODULE_SPEC is None or MODULE_SPEC.loader is None:
raise RuntimeError(f"Unable to load module from {SCRIPT_PATH}.")
MODULE = importlib.util.module_from_spec(MODULE_SPEC)
MODULE_SPEC.loader.exec_module(MODULE)
class SelectSingleOpenIssueNumberTests(unittest.TestCase):
"""Cover auto-resolution rules for open GitHub issues."""
def test_select_single_open_issue_number_filters_pull_requests(self) -> None:
"""Pull requests in the issues API must not block the single-open-issue path."""
selected = MODULE.select_single_open_issue_number(
[
{"number": 10, "pull_request": {"url": "https://example.test/pr/10"}},
{"number": 11},
]
)
self.assertEqual(selected, 11)
def test_select_single_open_issue_number_rejects_multiple_plain_issues(self) -> None:
"""Auto-resolution must stop when more than one plain issue is open."""
with self.assertRaisesRegex(RuntimeError, "Multiple open GitHub issues found"):
MODULE.select_single_open_issue_number([{"number": 11}, {"number": 12}])
class ExtractReferencesFromTextTests(unittest.TestCase):
"""Cover lightweight reference extraction used by the text and JSON output."""
def test_extract_references_from_text_finds_issue_commit_and_path_mentions(self) -> None:
"""The helper should retain the high-signal references needed for follow-up triage."""
references = MODULE.extract_references_from_text(
"See #123, commit abcdef1234567890, and GFramework.Core/Systems/Runner.cs for the failing path."
)
self.assertEqual(references["issues"], ["#123"])
self.assertEqual(references["commit_shas"], ["abcdef1234567890"])
self.assertEqual(references["file_paths"], ["GFramework.Core/Systems/Runner.cs"])
class BuildTriageHintsTests(unittest.TestCase):
"""Cover next-action classification for non-bug issue flows."""
def test_build_triage_hints_routes_docs_issue_to_docs_topic_without_bug_style_clarification(self) -> None:
"""Docs issues with a clear requested change should not be forced through bug-style clarification."""
triage_hints = MODULE.build_triage_hints(
{
"title": "Update documentation landing page",
"labels": ["docs"],
"body": "The guide should explain the landing-page layout for new contributors.",
},
[],
)
self.assertEqual(triage_hints["issue_type_candidates"][0], "docs")
self.assertEqual(triage_hints["affected_active_topics"], [])
self.assertFalse(triage_hints["information_flags"]["needs_clarification"])
self.assertEqual(triage_hints["next_action"], "start-new-docs-topic-with-boot")
def test_build_triage_hints_routes_feature_issue_to_new_topic_when_request_is_clear(self) -> None:
"""Feature requests with explicit desired behavior should stay actionable without fake bug repro gates."""
triage_hints = MODULE.build_triage_hints(
{
"title": "Support release note previews",
"labels": ["enhancement"],
"body": "The workflow should support previewing generated notes before completion.",
},
[],
)
self.assertEqual(triage_hints["issue_type_candidates"][0], "feature")
self.assertEqual(triage_hints["affected_active_topics"], [])
self.assertFalse(triage_hints["information_flags"]["needs_clarification"])
self.assertEqual(triage_hints["next_action"], "start-new-topic-with-boot")
if __name__ == "__main__":
unittest.main()

View 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.`

View File

@ -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."

71
.github/workflows/benchmark.yml vendored Normal file
View File

@ -0,0 +1,71 @@
# Copyright (c) 2025-2026 GeWuYou
# SPDX-License-Identifier: Apache-2.0
name: Benchmark
on:
workflow_dispatch:
inputs:
benchmark_filter:
description: '可选的 BenchmarkDotNet 过滤器;留空时仅执行 benchmark 项目 Release build'
required: false
default: ''
type: string
permissions:
contents: read
jobs:
benchmark:
name: Benchmark Build Or Run
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Cache NuGet packages
uses: actions/cache@v5
with:
path: |
~/.nuget/packages
~/.local/share/NuGet
key: ${{ runner.os }}-nuget-benchmarks-${{ hashFiles('GFramework.Cqrs.Benchmarks/*.csproj', 'GFramework.Cqrs/*.csproj', 'GFramework.Cqrs.Abstractions/*.csproj', 'GFramework.Core/*.csproj', 'GFramework.Core.Abstractions/*.csproj', '**/nuget.config') }}
- name: Restore benchmark project
run: dotnet restore GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj
- name: Build benchmark project
run: dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-restore
- name: Report build-only mode
if: ${{ inputs.benchmark_filter == '' }}
run: |
echo "No benchmark filter provided."
echo "Workflow completed after validating the benchmark project build."
- name: Run filtered benchmarks
if: ${{ inputs.benchmark_filter != '' }}
env:
BENCHMARK_FILTER: ${{ inputs.benchmark_filter }}
run: |
set -euo pipefail
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- \
--filter "$BENCHMARK_FILTER"
- name: Upload BenchmarkDotNet artifacts
if: ${{ always() && inputs.benchmark_filter != '' }}
uses: actions/upload-artifact@v7
with:
name: benchmark-artifacts
path: |
BenchmarkDotNet.Artifacts/**
GFramework.Cqrs.Benchmarks/bin/Release/net10.0/BenchmarkDotNet.Artifacts/**
if-no-files-found: ignore

View File

@ -155,6 +155,19 @@ jobs:
- name: Build
run: dotnet build GFramework.sln -c Release --no-restore
- name: Pack published modules
run: |
rm -rf ./packages
dotnet pack GFramework.sln \
-c Release \
--no-build \
--no-restore \
-o ./packages \
-p:IncludeSymbols=false
- name: Validate packed modules
run: bash scripts/validate-packed-modules.sh ./packages
# 运行单元测试输出TRX格式结果到TestResults目录
# 顺序执行各测试项目,避免并发 dotnet test 进程导致“TRX 全绿但 step 仍返回失败”的假红状态
- name: Test All Projects

View File

@ -82,41 +82,7 @@ jobs:
-p:IncludeSymbols=false
- name: Validate packed modules
run: |
set -euo pipefail
expected_packages=(
"GeWuYou.GFramework"
"GeWuYou.GFramework.Core"
"GeWuYou.GFramework.Core.Abstractions"
"GeWuYou.GFramework.Core.SourceGenerators"
"GeWuYou.GFramework.Cqrs"
"GeWuYou.GFramework.Cqrs.Abstractions"
"GeWuYou.GFramework.Cqrs.SourceGenerators"
"GeWuYou.GFramework.Ecs.Arch"
"GeWuYou.GFramework.Ecs.Arch.Abstractions"
"GeWuYou.GFramework.Game"
"GeWuYou.GFramework.Game.Abstractions"
"GeWuYou.GFramework.Game.SourceGenerators"
"GeWuYou.GFramework.Godot"
"GeWuYou.GFramework.Godot.SourceGenerators"
)
mapfile -t actual_packages < <(
find ./packages -maxdepth 1 -type f -name '*.nupkg' -printf '%f\n' \
| sed -E 's/\.[0-9][0-9A-Za-z.-]*\.nupkg$//' \
| sort -u
)
printf '%s\n' "${expected_packages[@]}" | sort > expected-packages.txt
printf '%s\n' "${actual_packages[@]}" | sort > actual-packages.txt
echo "Expected packages:"
cat expected-packages.txt
echo "Actual packages:"
cat actual-packages.txt
diff -u expected-packages.txt actual-packages.txt
run: bash scripts/validate-packed-modules.sh ./packages
- name: Validate runtime-generator package boundaries
run: python3 scripts/validate-runtime-generator-boundaries.py --package-dir ./packages

1
.gitignore vendored
View File

@ -26,3 +26,4 @@ ai-libs/
.codex
# tool
.venv/
BenchmarkDotNet.Artifacts/

View File

@ -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.

View File

@ -6,11 +6,11 @@
<Project>
<!-- Keep repository-wide analyzer behavior consistent while allowing only selected projects to opt into polyfills. -->
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="3.0.60">
<PackageReference Include="Meziantou.Analyzer" Version="3.0.72">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Update="Meziantou.Polyfill" Version="1.0.121">
<PackageReference Update="Meziantou.Polyfill" Version="1.0.123">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

View File

@ -84,6 +84,20 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia
void RegisterCqrsPipelineBehavior<TBehavior>()
where TBehavior : class;
/// <summary>
/// 注册 CQRS 流式请求管道行为。
/// 既支持实现 <c>IStreamPipelineBehavior&lt;,&gt;</c> 的开放泛型行为类型,
/// 也支持绑定到单一流式请求/响应对的封闭行为类型。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
/// <exception cref="InvalidOperationException">当前架构的底层容器已冻结,无法继续注册流式管道行为。</exception>
/// <exception cref="ObjectDisposedException">当前架构的底层容器已释放,无法继续注册流式管道行为。</exception>
/// <remarks>
/// 该入口应在架构初始化冻结容器之前调用;具体开放泛型或封闭行为类型的校验逻辑由底层容器负责。
/// </remarks>
void RegisterCqrsStreamPipelineBehavior<TBehavior>()
where TBehavior : class;
/// <summary>
/// 从指定程序集显式注册 CQRS 处理器。
/// 当处理器位于默认架构程序集之外的模块或扩展程序集中时,可在初始化阶段调用该入口接入对应程序集。

View File

@ -8,9 +8,15 @@ using GFramework.Core.Abstractions.Systems;
namespace GFramework.Core.Abstractions.Ioc;
/// <summary>
/// 依赖注入容器接口,定义了服务注册、解析和管理的基本操作
/// 依赖注入容器接口,定义服务注册、解析与生命周期管理的统一入口。
/// </summary>
public interface IIocContainer : IContextAware
/// <remarks>
/// 实现者必须在 <see cref="IDisposable.Dispose" /> 中释放容器拥有的根 <see cref="IServiceProvider" /> 及其
/// 关联同步资源,并保证释放操作幂等。
/// 容器一旦释放,后续任何注册、解析、查询或作用域创建调用都必须抛出
/// <see cref="ObjectDisposedException" />,避免消费者继续访问失效的运行时状态。
/// </remarks>
public interface IIocContainer : IContextAware, IDisposable
{
#region Register Methods
@ -99,6 +105,20 @@ public interface IIocContainer : IContextAware
void RegisterCqrsPipelineBehavior<TBehavior>()
where TBehavior : class;
/// <summary>
/// 注册 CQRS 流式请求管道行为。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
/// <exception cref="InvalidOperationException">容器已冻结,无法继续注册流式管道行为。</exception>
/// <exception cref="ObjectDisposedException">容器已释放,无法继续注册流式管道行为。</exception>
/// <remarks>
/// 该入口既支持实现 <c>IStreamPipelineBehavior&lt;,&gt;</c> 的开放泛型行为类型,
/// 也支持绑定到单一流式请求/响应对的封闭行为类型。
/// 应在容器冻结前的注册阶段调用;具体可注册形态由实现容器负责校验。
/// </remarks>
void RegisterCqrsStreamPipelineBehavior<TBehavior>()
where TBehavior : class;
/// <summary>
/// 从指定程序集显式注册 CQRS 处理器。
/// 该入口适用于处理器不位于默认架构程序集中的场景,例如扩展包、模块程序集或拆分后的业务程序集。
@ -135,6 +155,10 @@ public interface IIocContainer : IContextAware
/// </summary>
/// <typeparam name="T">期望获取的实例类型</typeparam>
/// <returns>找到的第一个实例;如果未找到则返回 null</returns>
/// <remarks>
/// 在 <see cref="Freeze" /> 之前,该查询只保证返回已经物化为实例绑定的服务。
/// 仅通过工厂或实现类型注册的服务在预冻结阶段可能不可见;若需要完整激活语义,请先冻结容器。
/// </remarks>
T? Get<T>() where T : class;
/// <summary>
@ -143,6 +167,10 @@ public interface IIocContainer : IContextAware
/// </summary>
/// <param name="type">期望获取的实例类型</param>
/// <returns>找到的第一个实例;如果未找到则返回 null</returns>
/// <remarks>
/// 在 <see cref="Freeze" /> 之前,该查询只保证返回已经物化为实例绑定的服务。
/// 仅通过工厂或实现类型注册的服务在预冻结阶段可能不可见;若需要完整激活语义,请先冻结容器。
/// </remarks>
object? Get(Type type);
@ -168,6 +196,9 @@ public interface IIocContainer : IContextAware
/// </summary>
/// <typeparam name="T">期望获取的实例类型</typeparam>
/// <returns>所有符合条件的实例列表;如果没有则返回空数组</returns>
/// <remarks>
/// 在 <see cref="Freeze" /> 之前,该查询只会枚举当前已经可见的实例绑定,不会主动执行工厂或创建实现类型。
/// </remarks>
IReadOnlyList<T> GetAll<T>() where T : class;
/// <summary>
@ -175,6 +206,9 @@ public interface IIocContainer : IContextAware
/// </summary>
/// <param name="type">期望获取的实例类型</param>
/// <returns>所有符合条件的实例列表;如果没有则返回空数组</returns>
/// <remarks>
/// 在 <see cref="Freeze" /> 之前,该查询只会枚举当前已经可见的实例绑定,不会主动执行工厂或创建实现类型。
/// </remarks>
IReadOnlyList<object> GetAll(Type type);
@ -213,8 +247,26 @@ public interface IIocContainer : IContextAware
/// </summary>
/// <typeparam name="T">要检查的类型</typeparam>
/// <returns>如果容器中包含指定类型的实例则返回true否则返回false</returns>
/// <remarks>
/// 在 <see cref="Freeze" /> 之前,该方法更接近“是否存在对应注册”的检查,而不是完整的 DI 可解析性判断。
/// </remarks>
bool Contains<T>() where T : class;
/// <summary>
/// 检查容器中是否存在可赋值给指定服务类型的注册项,而不要求解析出实例。
/// </summary>
/// <param name="type">要检查的服务类型。</param>
/// <returns>若存在显式注册或开放泛型映射可满足该服务类型,则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
/// <exception cref="ArgumentNullException">当 <paramref name="type" /> 为 <see langword="null" /> 时抛出。</exception>
/// <exception cref="ObjectDisposedException">当调用 <see cref="HasRegistration(Type)" /> 时容器已被释放时抛出。</exception>
/// <remarks>
/// 该入口面向“先判断是否值得解析实例”的热路径优化场景。
/// 与 <see cref="Contains{T}" /> 不同,它不会为了判断结果而激活服务实例,因此可避免把瞬态对象创建、
/// 多服务枚举或日志分配混入仅需存在性判断的调用链中。
/// 该方法按服务键与开放泛型映射判断可见性,不会把“仅以实现类型自身注册”的实例误判成其所有可赋值接口都已注册。
/// </remarks>
bool HasRegistration(Type type);
/// <summary>
/// 判断容器中是否包含某个具体的实例对象
/// </summary>

View File

@ -10,11 +10,13 @@ using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Architectures;
using GFramework.Core.Command;
using GFramework.Core.Cqrs;
using GFramework.Core.Environment;
using GFramework.Core.Events;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Core.Query;
using GFramework.Core.Services.Modules;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Tests.Architectures;
@ -41,9 +43,13 @@ namespace GFramework.Core.Tests.Architectures;
/// - GetUtility方法 - 获取未注册工具时抛出异常
/// - GetEnvironment方法 - 获取环境对象
/// </summary>
[NonParallelizable]
[TestFixture]
public class ArchitectureContextTests
{
/// <summary>
/// 初始化测试所需的容器与默认服务实例。
/// </summary>
[SetUp]
public void SetUp()
{
@ -71,10 +77,22 @@ public class ArchitectureContextTests
_container.RegisterPlurality(_queryBus);
_container.RegisterPlurality(_asyncQueryBus);
_container.RegisterPlurality(_environment);
new CqrsRuntimeModule().Register(_container);
RegisterLegacyBridgeHandlers(_container);
_context = new ArchitectureContext(_container);
}
/// <summary>
/// 释放当前测试创建的容器,并清理 legacy bridge 共享计数状态。
/// </summary>
[TearDown]
public void TearDown()
{
LegacyBridgePipelineTracker.Reset();
_container?.Dispose();
}
private AsyncQueryExecutor? _asyncQueryBus;
private CommandExecutor? _commandBus;
private MicrosoftDiContainer? _container;
@ -124,6 +142,31 @@ public class ArchitectureContextTests
Assert.That(result, Is.EqualTo(42));
}
/// <summary>
/// 测试 legacy 查询通过 <see cref="ArchitectureContext" /> 发送时会进入统一 CQRS pipeline
/// 并把当前架构上下文注入到查询对象。
/// </summary>
[Test]
public void SendQuery_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context()
{
LegacyBridgePipelineTracker.Reset();
var testQuery = new LegacyArchitectureBridgeQuery();
var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
try
{
var result = bridgeContext.SendQuery(testQuery);
Assert.That(result, Is.EqualTo(24));
Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
}
finally
{
bridgeContainer.Dispose();
}
}
/// <summary>
/// 测试SendQuery方法在查询为null时应抛出ArgumentNullException
/// </summary>
@ -146,6 +189,31 @@ public class ArchitectureContextTests
Assert.That(testCommand.Executed, Is.True);
}
/// <summary>
/// 测试 legacy 命令通过 <see cref="ArchitectureContext" /> 发送时会进入统一 CQRS pipeline
/// 并把当前架构上下文注入到命令对象。
/// </summary>
[Test]
public void SendCommand_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context()
{
LegacyBridgePipelineTracker.Reset();
var testCommand = new LegacyArchitectureBridgeCommand();
var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
try
{
bridgeContext.SendCommand(testCommand);
Assert.That(testCommand.Executed, Is.True);
Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
}
finally
{
bridgeContainer.Dispose();
}
}
/// <summary>
/// 测试SendCommand方法在命令为null时应抛出ArgumentNullException
/// </summary>
@ -168,6 +236,87 @@ public class ArchitectureContextTests
Assert.That(result, Is.EqualTo(123));
}
/// <summary>
/// 测试 legacy 带返回值命令通过 <see cref="ArchitectureContext" /> 发送时会进入统一 CQRS pipeline
/// 并保持原始返回值语义。
/// </summary>
[Test]
public void SendCommand_WithResult_Should_Bridge_Through_CqrsRuntime()
{
LegacyBridgePipelineTracker.Reset();
var testCommand = new LegacyArchitectureBridgeCommandWithResult();
var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
try
{
var result = bridgeContext.SendCommand(testCommand);
Assert.That(result, Is.EqualTo(42));
Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
}
finally
{
bridgeContainer.Dispose();
}
}
/// <summary>
/// 测试 legacy 异步查询通过 <see cref="ArchitectureContext" /> 发送时也会进入统一 CQRS pipeline。
/// </summary>
[Test]
public async Task SendQueryAsync_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context()
{
LegacyBridgePipelineTracker.Reset();
var testQuery = new LegacyArchitectureBridgeAsyncQuery();
var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
try
{
var result = await bridgeContext.SendQueryAsync(testQuery).ConfigureAwait(false);
Assert.That(result, Is.EqualTo(64));
Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
}
finally
{
bridgeContainer.Dispose();
}
}
/// <summary>
/// 为需要验证统一 CQRS pipeline 的用例创建一个已冻结的最小 bridge 上下文。
/// </summary>
/// <param name="container">返回承载当前 bridge 上下文的冻结容器,供测试在 finally 中显式释放。</param>
/// <returns>能够执行 legacy bridge request 且会 materialize open-generic pipeline behavior 的上下文。</returns>
private static ArchitectureContext CreateFrozenBridgeContext(out MicrosoftDiContainer container)
{
container = new MicrosoftDiContainer();
RegisterLegacyBridgeHandlers(container);
new CqrsRuntimeModule().Register(container);
container.ExecuteServicesHook(services =>
services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LegacyBridgeTrackingPipelineBehavior<,>)));
container.Freeze();
return new ArchitectureContext(container);
}
/// <summary>
/// 把 GFramework.Core 内部的 legacy bridge handler 实例预先注册成可见的实例绑定。
/// </summary>
/// <param name="container">目标测试容器。</param>
private static void RegisterLegacyBridgeHandlers(MicrosoftDiContainer container)
{
ArgumentNullException.ThrowIfNull(container);
container.RegisterPlurality(new LegacyCommandDispatchRequestHandler());
container.RegisterPlurality(new LegacyCommandResultDispatchRequestHandler());
container.RegisterPlurality(new LegacyAsyncCommandDispatchRequestHandler());
container.RegisterPlurality(new LegacyAsyncCommandResultDispatchRequestHandler());
container.RegisterPlurality(new LegacyQueryDispatchRequestHandler());
container.RegisterPlurality(new LegacyAsyncQueryDispatchRequestHandler());
}
/// <summary>
/// 测试SendCommand方法带返回值在命令为null时应抛出ArgumentNullException
/// </summary>

View File

@ -6,11 +6,13 @@ using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Lifecycle;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Architectures;
using GFramework.Core.Logging;
using GFramework.Core.Rule;
namespace GFramework.Core.Tests.Architectures;
@ -181,6 +183,80 @@ public class ArchitectureLifecycleBehaviorTests
}));
}
/// <summary>
/// 验证架构销毁后会解除全局 GameContext 绑定。
/// 该回归测试用于防止已销毁架构继续充当默认上下文回退入口。
/// </summary>
[Test]
public async Task DestroyAsync_Should_Unbind_Context_From_GameContext()
{
var architecture = new PhaseTrackingArchitecture();
await architecture.InitializeAsync();
Assert.That(GameContext.GetByType(architecture.GetType()), Is.SameAs(architecture.Context));
await architecture.DestroyAsync();
Assert.Throws<InvalidOperationException>(() => GameContext.GetByType(architecture.GetType()));
Assert.Throws<InvalidOperationException>(() => GameContext.GetFirstArchitectureContext());
}
/// <summary>
/// 验证失败初始化后的销毁同样会解除全局上下文绑定。
/// </summary>
[Test]
public async Task DestroyAsync_After_FailedInitialization_Should_Unbind_Context_From_GameContext()
{
var destroyOrder = new List<string>();
var architecture = new FailingInitializationArchitecture(destroyOrder);
var exception = Assert.ThrowsAsync<InvalidOperationException>(() => architecture.InitializeAsync());
Assert.That(exception, Is.Not.Null);
Assert.That(GameContext.GetByType(architecture.GetType()), Is.SameAs(architecture.Context));
await architecture.DestroyAsync();
Assert.Throws<InvalidOperationException>(() => GameContext.GetByType(architecture.GetType()));
Assert.Throws<InvalidOperationException>(() => GameContext.GetFirstArchitectureContext());
}
/// <summary>
/// 验证销毁后的新 ContextAware 实例不会再通过全局回退命中过期上下文。
/// </summary>
[Test]
public async Task DestroyAsync_Should_Prevent_New_ContextAware_Fallback_From_Using_Destroyed_Context()
{
var architecture = new PhaseTrackingArchitecture();
await architecture.InitializeAsync();
await architecture.DestroyAsync();
IContextAware probe = new LifecycleContextAwareProbe();
Assert.Throws<InvalidOperationException>(() => probe.GetContext());
}
/// <summary>
/// 验证同步兼容销毁入口同样会解除全局 GameContext 绑定。
/// </summary>
[Test]
public async Task Destroy_Should_Unbind_Context_From_GameContext()
{
var architecture = new PhaseTrackingArchitecture();
await architecture.InitializeAsync();
Assert.That(GameContext.GetByType(architecture.GetType()), Is.SameAs(architecture.Context));
#pragma warning disable CS0618
architecture.Destroy();
#pragma warning restore CS0618
Assert.Throws<InvalidOperationException>(() => GameContext.GetByType(architecture.GetType()));
Assert.Throws<InvalidOperationException>(() => GameContext.GetFirstArchitectureContext());
}
/// <summary>
/// 验证启用 AllowLateRegistration 时,生命周期层会立即初始化后注册的组件,而不是继续沿用初始化期的拒绝策略。
/// 由于公共架构 API 在 Ready 之后会先触发容器限制,此回归测试直接覆盖生命周期协作者的对齐逻辑。
@ -232,6 +308,13 @@ public class ArchitectureLifecycleBehaviorTests
}
}
/// <summary>
/// 仅用于验证销毁后全局上下文回退是否仍然泄漏的最小 ContextAware 探针。
/// </summary>
private sealed class LifecycleContextAwareProbe : ContextAwareBase
{
}
/// <summary>
/// 在初始化时注册可销毁组件的测试架构。
/// </summary>

View File

@ -1,11 +1,16 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using System.Reflection;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Architectures;
using GFramework.Core.Logging;
using GFramework.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Notification;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Tests.Architectures;
@ -13,6 +18,7 @@ namespace GFramework.Core.Tests.Architectures;
/// 验证 Architecture 通过 <c>ArchitectureModules</c> 暴露出的模块安装与 CQRS 行为注册能力。
/// 这些测试覆盖模块安装回调和请求管道行为接入,确保模块管理器仍然保持可观察行为不变。
/// </summary>
[NonParallelizable]
[TestFixture]
public class ArchitectureModulesBehaviorTests
{
@ -24,7 +30,9 @@ public class ArchitectureModulesBehaviorTests
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
GameContext.Clear();
AdditionalAssemblyNotificationHandlerState.Reset();
TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount = 0;
TrackingStreamPipelineBehavior<ModuleStreamBehaviorRequest, int>.InvocationCount = 0;
}
/// <summary>
@ -33,8 +41,11 @@ public class ArchitectureModulesBehaviorTests
[TearDown]
public void TearDown()
{
AdditionalAssemblyNotificationHandlerState.Reset();
GameContext.Clear();
TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount = 0;
TrackingStreamPipelineBehavior<ModuleStreamBehaviorRequest, int>.InvocationCount = 0;
LegacyBridgePipelineTracker.Reset();
}
/// <summary>
@ -47,15 +58,19 @@ public class ArchitectureModulesBehaviorTests
var architecture = new ModuleTestArchitecture(target => target.InstallModule(module));
await architecture.InitializeAsync();
Assert.Multiple(() =>
try
{
Assert.That(module.InstalledArchitecture, Is.SameAs(architecture));
Assert.That(module.InstallCallCount, Is.EqualTo(1));
Assert.That(architecture.Context.GetUtility<InstalledByModuleUtility>(), Is.Not.Null);
});
await architecture.DestroyAsync();
Assert.Multiple(() =>
{
Assert.That(module.InstalledArchitecture, Is.SameAs(architecture));
Assert.That(module.InstallCallCount, Is.EqualTo(1));
Assert.That(architecture.Context.GetUtility<InstalledByModuleUtility>(), Is.Not.Null);
});
}
finally
{
await architecture.DestroyAsync();
}
}
/// <summary>
@ -68,16 +83,111 @@ public class ArchitectureModulesBehaviorTests
target.RegisterCqrsPipelineBehavior<TrackingPipelineBehavior<ModuleBehaviorRequest, string>>());
await architecture.InitializeAsync();
var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest());
Assert.Multiple(() =>
try
{
Assert.That(response, Is.EqualTo("handled"));
Assert.That(TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount, Is.EqualTo(1));
});
var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest());
await architecture.DestroyAsync();
Assert.Multiple(() =>
{
Assert.That(response, Is.EqualTo("handled"));
Assert.That(TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount, Is.EqualTo(1));
});
}
finally
{
await architecture.DestroyAsync();
}
}
/// <summary>
/// 验证注册的 CQRS stream 行为会参与建流处理流程。
/// </summary>
[Test]
public async Task RegisterCqrsStreamPipelineBehavior_Should_Apply_Pipeline_Behavior_To_Stream_Request()
{
var architecture = new ModuleTestArchitecture(target =>
target.RegisterCqrsStreamPipelineBehavior<TrackingStreamPipelineBehavior<ModuleStreamBehaviorRequest, int>>());
await architecture.InitializeAsync();
try
{
var response = await DrainAsync(architecture.Context.CreateStream(new ModuleStreamBehaviorRequest()));
Assert.Multiple(() =>
{
Assert.That(response, Is.EqualTo([7]));
Assert.That(
TrackingStreamPipelineBehavior<ModuleStreamBehaviorRequest, int>.InvocationCount,
Is.EqualTo(1));
});
}
finally
{
await architecture.DestroyAsync();
}
}
/// <summary>
/// 验证默认架构初始化路径会自动扫描 Core 程序集里的 legacy bridge handler
/// 使旧 <c>SendCommand</c> / <c>SendQuery</c> 入口也能进入统一 CQRS pipeline。
/// </summary>
[Test]
public async Task InitializeAsync_Should_AutoRegister_LegacyBridgeHandlers_For_Default_Core_Assemblies()
{
LegacyBridgePipelineTracker.Reset();
var architecture = new LegacyBridgeArchitecture();
await architecture.InitializeAsync();
try
{
var query = new LegacyArchitectureBridgeQuery();
var command = new LegacyArchitectureBridgeCommand();
var queryResult = architecture.Context.SendQuery(query);
architecture.Context.SendCommand(command);
Assert.Multiple(() =>
{
Assert.That(queryResult, Is.EqualTo(24));
Assert.That(query.ObservedContext, Is.SameAs(architecture.Context));
Assert.That(command.Executed, Is.True);
Assert.That(command.ObservedContext, Is.SameAs(architecture.Context));
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(2));
});
}
finally
{
await architecture.DestroyAsync();
}
}
/// <summary>
/// 验证标准架构启动路径会复用通过 <see cref="Architecture.Configurator" /> 声明的自定义 notification publisher
/// 而不是在 <see cref="GFramework.Core.Services.Modules.CqrsRuntimeModule" /> 创建 runtime 时提前固化默认顺序策略。
/// </summary>
[Test]
public async Task InitializeAsync_Should_Reuse_Custom_NotificationPublisher_From_Configurator()
{
var generatedAssembly = CreateGeneratedHandlerAssembly();
var architecture = new ConfiguredNotificationPublisherArchitecture(generatedAssembly.Object);
await architecture.InitializeAsync();
try
{
var probe = architecture.Context.GetService<ArchitectureNotificationPublisherProbe>();
await architecture.Context.PublishAsync(new AdditionalAssemblyNotification());
Assert.Multiple(() =>
{
Assert.That(probe.WasCalled, Is.True);
Assert.That(AdditionalAssemblyNotificationHandlerState.InvocationCount, Is.EqualTo(1));
});
}
finally
{
await architecture.DestroyAsync();
}
}
/// <summary>
@ -94,6 +204,52 @@ public class ArchitectureModulesBehaviorTests
}
}
/// <summary>
/// 通过公开初始化入口注册测试 pipeline behavior 的最小架构,
/// 用于验证默认 Core 程序集扫描是否会自动接入 legacy bridge handler。
/// </summary>
private sealed class LegacyBridgeArchitecture : Architecture
{
/// <summary>
/// 在容器钩子阶段注册 open-generic pipeline behavior
/// 以便 bridge request 走真实的架构初始化与 handler 自动扫描链路。
/// </summary>
public override Action<IServiceCollection>? Configurator => services =>
services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LegacyBridgeTrackingPipelineBehavior<,>));
/// <summary>
/// 保持空初始化,让测试只聚焦默认 CQRS 接线与 legacy bridge handler 自动发现。
/// </summary>
protected override void OnInitialize()
{
}
}
/// <summary>
/// 通过标准架构启动路径声明自定义 notification publisher 的最小架构。
/// </summary>
private sealed class ConfiguredNotificationPublisherArchitecture(Assembly generatedAssembly) : Architecture
{
/// <summary>
/// 在服务钩子阶段注册 probe 与自定义 publisher
/// 以模拟真实项目在组合根里通过 <see cref="IServiceCollection" /> 覆盖默认策略的路径。
/// </summary>
public override Action<IServiceCollection>? Configurator => services =>
{
services.AddSingleton<ArchitectureNotificationPublisherProbe>();
services.AddSingleton<INotificationPublisher, ArchitectureTrackingNotificationPublisher>();
};
/// <summary>
/// 在用户初始化阶段显式接入额外程序集里的 notification handler
/// 让测试聚焦“publisher 是否被复用”,而不是依赖当前测试文件自己的 handler 扫描形状。
/// </summary>
protected override void OnInitialize()
{
RegisterCqrsHandlersFromAssembly(generatedAssembly);
}
}
/// <summary>
/// 记录模块安装调用情况的测试模块。
/// </summary>
@ -127,4 +283,85 @@ public class ArchitectureModulesBehaviorTests
private sealed class InstalledByModuleUtility : IUtility
{
}
/// <summary>
/// 创建一个仅暴露程序集级 CQRS registry 元数据的 mocked Assembly。
/// 该测试替身模拟扩展程序集已经提供 notification handler registry而架构只需在初始化时显式接入该程序集。
/// </summary>
/// <returns>包含程序集级 notification handler registry 元数据的 mocked Assembly。</returns>
private static Mock<Assembly> CreateGeneratedHandlerAssembly()
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Architectures.ExplicitAdditionalHandlers, Version=1.0.0.0");
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(AdditionalAssemblyNotificationHandlerRegistry))]);
return generatedAssembly;
}
/// <summary>
/// 记录自定义 notification publisher 是否真正参与了标准架构启动路径下的 publish 调用。
/// </summary>
private sealed class ArchitectureNotificationPublisherProbe
{
/// <summary>
/// 获取 probe 是否已被 publisher 标记为执行过。
/// </summary>
public bool WasCalled { get; private set; }
/// <summary>
/// 记录当前 publish 调用已经命中了自定义 publisher。
/// </summary>
public void MarkCalled()
{
WasCalled = true;
}
}
/// <summary>
/// 依赖容器内 probe 的自定义 notification publisher。
/// 该类型通过显式标记 + 正常转发处理器执行,验证标准架构启动路径不会把自定义策略短路成默认顺序发布器。
/// </summary>
private sealed class ArchitectureTrackingNotificationPublisher(
ArchitectureNotificationPublisherProbe probe) : INotificationPublisher
{
/// <summary>
/// 记录自定义 publisher 已参与当前发布调用,并继续按处理器解析顺序转发执行。
/// </summary>
public async ValueTask PublishAsync<TNotification>(
NotificationPublishContext<TNotification> context,
CancellationToken cancellationToken = default)
where TNotification : INotification
{
ArgumentNullException.ThrowIfNull(context);
cancellationToken.ThrowIfCancellationRequested();
probe.MarkCalled();
foreach (var handler in context.Handlers)
{
await context.InvokeHandlerAsync(handler, cancellationToken).ConfigureAwait(false);
}
}
}
/// <summary>
/// 物化异步流为只读列表,便于断言 stream pipeline 行为的最终可观察结果。
/// </summary>
/// <typeparam name="T">流元素类型。</typeparam>
/// <param name="stream">要物化的异步流。</param>
/// <returns>按枚举顺序收集的元素列表。</returns>
private static async Task<IReadOnlyList<T>> DrainAsync<T>(IAsyncEnumerable<T> stream)
{
var results = new List<T>();
await foreach (var item in stream.ConfigureAwait(false))
{
results.Add(item);
}
return results;
}
}

View File

@ -9,7 +9,7 @@ namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// ContextProvider 相关类的单元测试
/// 测试内容包括:
/// - GameContextProvider 获取第一个架构上下文
/// - GameContextProvider 获取当前活动架构上下文
/// - GameContextProvider 尝试获取指定类型的上下文
/// - ScopedContextProvider 获取绑定的上下文
/// - ScopedContextProvider 尝试获取指定类型的上下文
@ -37,10 +37,10 @@ public class ContextProviderTests
}
/// <summary>
/// 测试 GameContextProvider 是否能正确获取第一个架构上下文
/// 测试 GameContextProvider 是否能正确获取当前活动架构上下文
/// </summary>
[Test]
public void GameContextProvider_GetContext_Should_Return_First_Context()
public void GameContextProvider_GetContext_Should_Return_Current_Context()
{
var context = new TestArchitectureContext();
GameContext.Bind(typeof(TestArchitecture), context);
@ -63,13 +63,13 @@ public class ContextProviderTests
}
/// <summary>
/// 测试 GameContextProvider 的 TryGetContext 方法在找到上下文时返回 true
/// 测试 GameContextProvider 的 TryGetContext 方法在仅绑定架构类型时也能返回 true
/// </summary>
[Test]
public void GameContextProvider_TryGetContext_Should_Return_True_When_Found()
public void GameContextProvider_TryGetContext_Should_Return_True_When_Current_Context_Matches()
{
var context = new TestArchitectureContext();
GameContext.Bind(typeof(TestArchitectureContext), context);
GameContext.Bind(typeof(TestArchitecture), context);
var provider = new GameContextProvider();
var result = provider.TryGetContext<TestArchitectureContext>(out var foundContext);

View File

@ -6,20 +6,12 @@ using GFramework.Core.Architectures;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// GameContext类的单元测试
/// GameContext 类的单元测试
/// 测试内容包括:
/// - ArchitectureReadOnlyDictionary在启动时为空
/// - Bind方法添加上下文到字典
/// - Bind重复类型时抛出异常
/// - GetByType返回正确的上下文
/// - GetByType未找到时抛出异常
/// - Get泛型方法返回正确的上下文
/// - TryGet方法在找到时返回true
/// - TryGet方法在未找到时返回false
/// - GetFirstArchitectureContext在存在时返回
/// - GetFirstArchitectureContext为空时抛出异常
/// - Unbind移除上下文
/// - Clear移除所有上下文
/// - 初始状态为空
/// - 绑定后可通过架构类型和上下文类型回查
/// - 不允许并存绑定两个不同上下文实例
/// - 清理和解绑会同步更新当前活动上下文
/// </summary>
[TestFixture]
public class GameContextTests
@ -81,6 +73,21 @@ public class GameContextTests
GameContext.Bind(typeof(TestArchitecture), context2));
}
/// <summary>
/// 测试绑定第二个不同的上下文实例时会被拒绝。
/// </summary>
[Test]
public void Bind_WithDifferentContextInstance_Should_ThrowInvalidOperationException()
{
var firstContext = new TestArchitectureContext();
var secondContext = new TestArchitectureContext();
GameContext.Bind(typeof(TestArchitecture), firstContext);
Assert.Throws<InvalidOperationException>(() =>
GameContext.Bind(typeof(AnotherTestArchitectureContext), secondContext));
}
/// <summary>
/// 测试GetByType方法是否返回正确的上下文
/// </summary>
@ -106,13 +113,27 @@ public class GameContextTests
}
/// <summary>
/// 测试Get泛型方法是否返回正确的上下文
/// 测试 GetByType 支持按当前活动上下文的具体类型回查。
/// </summary>
[Test]
public void GetGeneric_Should_Return_Correct_Context()
public void GetByType_Should_Return_Current_Context_When_Requested_By_Context_Type()
{
var context = new TestArchitectureContext();
GameContext.Bind(typeof(TestArchitectureContext), context);
GameContext.Bind(typeof(TestArchitecture), context);
var result = GameContext.GetByType(typeof(TestArchitectureContext));
Assert.That(result, Is.SameAs(context));
}
/// <summary>
/// 测试 Get 泛型方法在仅绑定架构类型时也能返回当前上下文
/// </summary>
[Test]
public void GetGeneric_Should_Return_Current_Context_When_Bound_By_Architecture_Type()
{
var context = new TestArchitectureContext();
GameContext.Bind(typeof(TestArchitecture), context);
var result = GameContext.Get<TestArchitectureContext>();
@ -120,13 +141,13 @@ public class GameContextTests
}
/// <summary>
/// 测试TryGet方法在找到上下文时是否返回true并正确设置输出参数
/// 测试 TryGet 方法在仅绑定架构类型时也能找到当前上下文
/// </summary>
[Test]
public void TryGet_Should_ReturnTrue_When_Found()
public void TryGet_Should_ReturnTrue_When_Bound_By_Architecture_Type()
{
var context = new TestArchitectureContext();
GameContext.Bind(typeof(TestArchitectureContext), context);
GameContext.Bind(typeof(TestArchitecture), context);
var result = GameContext.TryGet(out TestArchitectureContext? foundContext);
@ -135,7 +156,7 @@ public class GameContextTests
}
/// <summary>
/// 测试TryGet方法在未找到上下文时是否返回false且输出参数为null
/// 测试 TryGet 方法在未找到上下文时是否返回 false 且输出参数为 null
/// </summary>
[Test]
public void TryGet_Should_ReturnFalse_When_Not_Found()
@ -171,10 +192,10 @@ public class GameContextTests
}
/// <summary>
/// 测试Unbind方法是否正确移除指定类型的上下文
/// 测试 Unbind 方法在移除最后一个别名时会清空当前活动上下文
/// </summary>
[Test]
public void Unbind_Should_Remove_Context()
public void Unbind_Should_Remove_Context_When_Last_Alias_Is_Removed()
{
var context = new TestArchitectureContext();
GameContext.Bind(typeof(TestArchitecture), context);
@ -185,16 +206,34 @@ public class GameContextTests
}
/// <summary>
/// 测试Clear方法是否正确移除所有上下文
/// 测试 Unbind 方法在仍有其他别名时保留当前活动上下文
/// </summary>
[Test]
public void Unbind_Should_Keep_Current_Context_When_Another_Alias_Remains()
{
var context = new TestArchitectureContext();
GameContext.Bind(typeof(TestArchitecture), context);
GameContext.Bind(typeof(TestArchitectureContext), context);
GameContext.Unbind(typeof(TestArchitecture));
Assert.That(GameContext.GetFirstArchitectureContext(), Is.SameAs(context));
Assert.That(GameContext.ArchitectureReadOnlyDictionary.Count, Is.EqualTo(1));
}
/// <summary>
/// 测试 Clear 方法是否正确移除所有上下文
/// </summary>
[Test]
public void Clear_Should_Remove_All_Contexts()
{
GameContext.Bind(typeof(TestArchitecture), new TestArchitectureContext());
GameContext.Bind(typeof(TestArchitectureContext), new TestArchitectureContext());
var context = new TestArchitectureContext();
GameContext.Bind(typeof(TestArchitecture), context);
GameContext.Bind(typeof(TestArchitectureContext), context);
GameContext.Clear();
Assert.That(GameContext.ArchitectureReadOnlyDictionary.Count, Is.EqualTo(0));
Assert.Throws<InvalidOperationException>(() => GameContext.GetFirstArchitectureContext());
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Rule;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 用于验证 legacy 异步查询桥接时也会显式注入当前架构上下文。
/// </summary>
public sealed class LegacyArchitectureBridgeAsyncQuery : ContextAwareBase, IAsyncQuery<int>
{
/// <summary>
/// 获取执行期间观察到的上下文实例。
/// </summary>
public IArchitectureContext? ObservedContext { get; private set; }
/// <summary>
/// 执行异步查询并返回测试结果。
/// </summary>
public Task<int> DoAsync()
{
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
return Task.FromResult(64);
}
}

View File

@ -0,0 +1,33 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Rule;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 用于验证 legacy 命令桥接时会把当前 <see cref="IArchitectureContext" /> 注入到命令对象。
/// </summary>
public sealed class LegacyArchitectureBridgeCommand : ContextAwareBase, ICommand
{
/// <summary>
/// 获取执行期间观察到的上下文实例。
/// </summary>
public IArchitectureContext? ObservedContext { get; private set; }
/// <summary>
/// 获取当前命令是否已经执行。
/// </summary>
public bool Executed { get; private set; }
/// <summary>
/// 执行命令并记录 bridge handler 注入的上下文。
/// </summary>
public void Execute()
{
Executed = true;
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Rule;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 用于验证 legacy 带返回值命令桥接时会沿用统一 runtime。
/// </summary>
public sealed class LegacyArchitectureBridgeCommandWithResult : ContextAwareBase, ICommand<int>
{
/// <summary>
/// 获取执行期间观察到的上下文实例。
/// </summary>
public IArchitectureContext? ObservedContext { get; private set; }
/// <summary>
/// 执行命令并返回测试结果。
/// </summary>
public int Execute()
{
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
return 42;
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Rule;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 用于验证 legacy 查询桥接时会把当前 <see cref="IArchitectureContext" /> 注入到查询对象。
/// </summary>
public sealed class LegacyArchitectureBridgeQuery : ContextAwareBase, IQuery<int>
{
/// <summary>
/// 获取执行期间观察到的上下文实例。
/// </summary>
public IArchitectureContext? ObservedContext { get; private set; }
/// <summary>
/// 执行查询并返回测试结果。
/// </summary>
public int Do()
{
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
return 24;
}
}

View File

@ -0,0 +1,47 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using System.Threading;
using GFramework.Core.Cqrs;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 为 legacy bridge pipeline 回归测试保存跨泛型闭包共享的计数状态。
/// </summary>
/// <remarks>
/// 该计数器通过 <see cref="Interlocked.Increment(ref int)" /> 原子递增,并使用
/// <see cref="Volatile" /> 读写,因此单次读写操作本身是线程安全的。
/// 由于状态在同一进程内跨 fixture 共享,所有使用它的测试都必须在清理阶段调用 <see cref="Reset" />
/// 以避免并行或失败测试把旧计数泄露给后续断言。
/// </remarks>
public static class LegacyBridgePipelineTracker
{
private static int _invocationCount;
/// <summary>
/// 获取当前进程内被识别为 legacy bridge request 的 pipeline 命中次数。
/// </summary>
public static int InvocationCount => Volatile.Read(ref _invocationCount);
/// <summary>
/// 重置计数器。
/// </summary>
public static void Reset()
{
Volatile.Write(ref _invocationCount, 0);
}
/// <summary>
/// 若当前请求类型属于 Core legacy bridge request则记录一次命中。
/// </summary>
public static void Record(Type requestType)
{
ArgumentNullException.ThrowIfNull(requestType);
if (typeof(LegacyCqrsDispatchRequestBase).IsAssignableFrom(requestType))
{
Interlocked.Increment(ref _invocationCount);
}
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using System.Threading;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 记录 legacy Core CQRS bridge request 是否经过统一 CQRS pipeline 的测试行为。
/// </summary>
public sealed class LegacyBridgeTrackingPipelineBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
/// <inheritdoc />
public async ValueTask<TResponse> Handle(
TRequest message,
MessageHandlerDelegate<TRequest, TResponse> next,
CancellationToken cancellationToken)
{
LegacyBridgePipelineTracker.Record(typeof(TRequest));
return await next(message, cancellationToken).ConfigureAwait(false);
}
}

View File

@ -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>
{
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
}

View File

@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Command;
using GFramework.Core.Rule;
using GFramework.Core.Tests.Architectures;
namespace GFramework.Core.Tests.Command;
@ -75,6 +77,95 @@ public class CommandExecutorTests
Assert.Throws<ArgumentNullException>(() => _commandExecutor.Send<int>(null!));
}
/// <summary>
/// 验证当 legacy 命令没有可用上下文时,会安全回退到本地直接执行路径。
/// </summary>
[Test]
public void Send_Should_Fall_Back_To_Legacy_Execution_When_Context_IsMissing()
{
var runtime = new RecordingCqrsRuntime();
var executor = new CommandExecutor(runtime);
var command = new MissingContextLegacyCommand();
executor.Send(command);
Assert.Multiple(() =>
{
Assert.That(command.Executed, Is.True);
Assert.That(runtime.LastRequest, Is.Null);
});
}
/// <summary>
/// 验证非“缺上下文”类型的 <see cref="InvalidOperationException" /> 不会被 bridge 回退逻辑误吞掉。
/// </summary>
[Test]
public void Send_Should_Propagate_InvalidOperationException_When_ContextAware_Target_Throws_Unexpected_Error()
{
var runtime = new RecordingCqrsRuntime();
var executor = new CommandExecutor(runtime);
var command = new ThrowingLegacyCommand();
Assert.That(
() => executor.Send(command),
Throws.InvalidOperationException.With.Message.EqualTo(ThrowingLegacyCommand.ExceptionMessage));
Assert.That(runtime.LastRequest, Is.Null);
}
/// <summary>
/// 验证 legacy 同步命令桥接会在线程池上等待 runtime
/// 避免直接继承调用方当前的同步上下文。
/// </summary>
[Test]
public void Send_Should_Bridge_Through_Runtime_Without_Reusing_Caller_SynchronizationContext()
{
var runtime = new RecordingCqrsRuntime();
var executor = new CommandExecutor(runtime);
var command = new ContextAwareLegacyCommand();
var expectedContext = new TestArchitectureContextBaseStub();
((GFramework.Core.Abstractions.Rule.IContextAware)command).SetContext(expectedContext);
var originalContext = SynchronizationContext.Current;
try
{
SynchronizationContext.SetSynchronizationContext(new TestLegacySynchronizationContext());
executor.Send(command);
Assert.Multiple(() =>
{
Assert.That(runtime.LastRequest, Is.TypeOf<GFramework.Core.Cqrs.LegacyCommandDispatchRequest>());
Assert.That(runtime.ObservedSynchronizationContextType, Is.Null);
});
}
finally
{
SynchronizationContext.SetSynchronizationContext(originalContext);
}
}
/// <summary>
/// 验证 legacy 带返回值命令桥接也会保留上下文注入与返回值语义。
/// </summary>
[Test]
public void Send_WithResult_Should_Bridge_Through_Runtime_And_Preserve_Context()
{
var runtime = new RecordingCqrsRuntime(static _ => 123);
var executor = new CommandExecutor(runtime);
var command = new ContextAwareLegacyCommandWithResult(123);
var expectedContext = new TestArchitectureContextBaseStub();
((GFramework.Core.Abstractions.Rule.IContextAware)command).SetContext(expectedContext);
var result = executor.Send(command);
Assert.Multiple(() =>
{
Assert.That(result, Is.EqualTo(123));
Assert.That(runtime.LastRequest, Is.TypeOf<GFramework.Core.Cqrs.LegacyCommandResultDispatchRequest>());
Assert.That(command.ObservedContext, Is.SameAs(expectedContext));
});
}
/// <summary>
/// 测试SendAsync方法执行异步命令
/// </summary>
@ -122,4 +213,65 @@ public class CommandExecutorTests
{
Assert.ThrowsAsync<ArgumentNullException>(() => _commandExecutor.SendAsync<int>(null!));
}
/// <summary>
/// 为同步 bridge 测试提供最小架构上下文替身。
/// </summary>
private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
{
}
/// <summary>
/// 用于验证缺少上下文时仍会走本地 fallback 的测试命令。
/// </summary>
private sealed class MissingContextLegacyCommand : GFramework.Core.Abstractions.Rule.IContextAware, GFramework.Core.Abstractions.Command.ICommand
{
/// <summary>
/// 获取命令是否已经执行。
/// </summary>
public bool Executed { get; private set; }
/// <inheritdoc />
public void SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
{
ArgumentNullException.ThrowIfNull(context);
}
/// <inheritdoc />
public GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext()
{
throw new InvalidOperationException("Architecture context has not been set. Call SetContext before accessing the context.");
}
/// <inheritdoc />
public void Execute()
{
Executed = true;
}
}
/// <summary>
/// 用于验证 bridge 上下文解析不会吞掉意外运行时错误的测试命令。
/// </summary>
private sealed class ThrowingLegacyCommand : GFramework.Core.Abstractions.Rule.IContextAware, GFramework.Core.Abstractions.Command.ICommand
{
internal const string ExceptionMessage = "Unexpected context failure.";
/// <inheritdoc />
public void SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
{
ArgumentNullException.ThrowIfNull(context);
}
/// <inheritdoc />
public GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext()
{
throw new InvalidOperationException(ExceptionMessage);
}
/// <inheritdoc />
public void Execute()
{
}
}
}

View File

@ -0,0 +1,31 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Rule;
namespace GFramework.Core.Tests.Command;
/// <summary>
/// 为 <see cref="CommandExecutorTests" /> 提供可观察上下文注入的 legacy 命令。
/// </summary>
internal sealed class ContextAwareLegacyCommand : ContextAwareBase, ICommand
{
/// <summary>
/// 获取执行期间观察到的架构上下文。
/// </summary>
public IArchitectureContext? ObservedContext { get; private set; }
/// <summary>
/// 获取命令是否已经执行。
/// </summary>
public bool Executed { get; private set; }
/// <inheritdoc />
public void Execute()
{
Executed = true;
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Rule;
namespace GFramework.Core.Tests.Command;
/// <summary>
/// 为 <see cref="CommandExecutorTests" /> 提供可观察上下文注入的带返回值 legacy 命令。
/// </summary>
internal sealed class ContextAwareLegacyCommandWithResult(int result) : ContextAwareBase, ICommand<int>
{
/// <summary>
/// 获取执行期间观察到的架构上下文。
/// </summary>
public IArchitectureContext? ObservedContext { get; private set; }
/// <inheritdoc />
public int Execute()
{
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
return result;
}
}

View File

@ -0,0 +1,207 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Tests.Command;
/// <summary>
/// 记录 bridge 执行线程与收到请求的最小 CQRS runtime 测试替身。
/// </summary>
internal sealed class RecordingCqrsRuntime(Func<object?, object?>? responseFactory = null) : ICqrsRuntime
{
private static readonly Func<object?, object?> DefaultResponseFactory = _ => null;
private readonly Func<object?, object?> _responseFactory = responseFactory ?? DefaultResponseFactory;
/// <summary>
/// 获取最近一次 <see cref="SendAsync{TResponse}" /> 观察到的同步上下文类型。
/// </summary>
public Type? ObservedSynchronizationContextType { get; private set; }
/// <summary>
/// 获取最近一次收到的请求实例。
/// </summary>
public object? LastRequest { get; private set; }
/// <inheritdoc />
public async ValueTask<TResponse> SendAsync<TResponse>(
ICqrsContext context,
IRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(request);
ObservedSynchronizationContextType = SynchronizationContext.Current?.GetType();
LastRequest = request;
object? response = request switch
{
LegacyCommandDispatchRequest legacyCommandDispatchRequest => ExecuteLegacyCommand(context, legacyCommandDispatchRequest),
LegacyCommandResultDispatchRequest legacyCommandResultDispatchRequest => ExecuteContextAwareRequest(
context,
legacyCommandResultDispatchRequest.Target,
legacyCommandResultDispatchRequest.Execute),
LegacyQueryDispatchRequest legacyQueryDispatchRequest => ExecuteContextAwareRequest(
context,
legacyQueryDispatchRequest.Target,
legacyQueryDispatchRequest.Execute),
LegacyAsyncCommandDispatchRequest legacyAsyncCommandDispatchRequest => await ExecuteLegacyAsyncCommandAsync(
context,
legacyAsyncCommandDispatchRequest,
cancellationToken).ConfigureAwait(false),
LegacyAsyncCommandResultDispatchRequest legacyAsyncCommandResultDispatchRequest => await ExecuteContextAwareRequestAsync(
context,
legacyAsyncCommandResultDispatchRequest.Target,
legacyAsyncCommandResultDispatchRequest.ExecuteAsync,
cancellationToken).ConfigureAwait(false),
LegacyAsyncQueryDispatchRequest legacyAsyncQueryDispatchRequest => await ExecuteContextAwareRequestAsync(
context,
legacyAsyncQueryDispatchRequest.Target,
legacyAsyncQueryDispatchRequest.ExecuteAsync,
cancellationToken).ConfigureAwait(false),
IRequest<Unit> => Unit.Value,
_ => _responseFactory(request)
};
return ConvertResponse<TResponse>(request, response);
}
/// <inheritdoc />
public ValueTask PublishAsync<TNotification>(
ICqrsContext context,
TNotification notification,
CancellationToken cancellationToken = default)
where TNotification : INotification
{
throw new NotSupportedException();
}
/// <inheritdoc />
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(
ICqrsContext context,
IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
throw new NotSupportedException();
}
/// <summary>
/// 将测试替身工厂返回的装箱结果显式还原为目标类型,并在类型不匹配时给出可诊断异常。
/// </summary>
/// <typeparam name="TResponse">当前请求声明的响应类型。</typeparam>
/// <param name="request">触发响应工厂的请求实例。</param>
/// <param name="response">响应工厂返回的装箱结果。</param>
/// <returns>还原后的目标类型响应。</returns>
/// <exception cref="InvalidOperationException">
/// 响应工厂返回 <see langword="null" /> 或错误类型,导致无法还原为 <typeparamref name="TResponse" />。
/// </exception>
private static TResponse ConvertResponse<TResponse>(IRequest<TResponse> request, object? response)
{
if (response is TResponse typedResponse)
{
return typedResponse;
}
if (response is null && !typeof(TResponse).IsValueType)
{
return (TResponse)response!;
}
string actualType = response?.GetType().FullName ?? "null";
throw new InvalidOperationException(
$"RecordingCqrsRuntime 无法将响应类型从 '{actualType}' 转换为 '{typeof(TResponse).FullName}'。"
+ $" 请求类型:'{request.GetType().FullName}'。");
}
/// <summary>
/// 按 bridge handler 语义为 legacy 无返回值命令注入上下文并执行。
/// </summary>
/// <param name="context">当前运行时接收到的架构上下文。</param>
/// <param name="request">待执行的 legacy 命令桥接请求。</param>
/// <returns>桥接后的 <see cref="Unit" /> 响应。</returns>
private static Unit ExecuteLegacyCommand(
ICqrsContext context,
LegacyCommandDispatchRequest request)
{
PrepareTarget(context, request.Command);
request.Command.Execute();
return Unit.Value;
}
/// <summary>
/// 按 bridge handler 语义为 legacy 异步无返回值命令注入上下文并执行。
/// </summary>
/// <param name="context">当前运行时接收到的架构上下文。</param>
/// <param name="request">待执行的 legacy 异步命令桥接请求。</param>
/// <param name="cancellationToken">调用方传入的取消令牌。</param>
/// <returns>表示 bridge 执行完成的异步结果。</returns>
private static async Task<Unit> ExecuteLegacyAsyncCommandAsync(
ICqrsContext context,
LegacyAsyncCommandDispatchRequest request,
CancellationToken cancellationToken)
{
PrepareTarget(context, request.Command);
await request.Command.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
return Unit.Value;
}
/// <summary>
/// 按 bridge handler 语义为带返回值 legacy 请求注入上下文并执行同步委托。
/// </summary>
/// <param name="context">当前运行时接收到的架构上下文。</param>
/// <param name="target">需要接收上下文注入的 legacy 目标对象。</param>
/// <param name="execute">实际执行 legacy 目标逻辑的同步委托。</param>
/// <returns>同步执行结果。</returns>
private static object? ExecuteContextAwareRequest(
ICqrsContext context,
object target,
Func<object?> execute)
{
PrepareTarget(context, target);
return execute();
}
/// <summary>
/// 按 bridge handler 语义为带返回值 legacy 请求注入上下文并执行异步委托。
/// </summary>
/// <param name="context">当前运行时接收到的架构上下文。</param>
/// <param name="target">需要接收上下文注入的 legacy 目标对象。</param>
/// <param name="executeAsync">实际执行 legacy 目标逻辑的异步委托。</param>
/// <param name="cancellationToken">调用方传入的取消令牌。</param>
/// <returns>异步执行结果。</returns>
private static async Task<object?> ExecuteContextAwareRequestAsync(
ICqrsContext context,
object target,
Func<Task<object?>> executeAsync,
CancellationToken cancellationToken)
{
PrepareTarget(context, target);
return await executeAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// 模拟 legacy bridge handler 的上下文注入语义,使测试替身与生产桥接行为保持一致。
/// </summary>
/// <param name="context">当前运行时接收到的架构上下文。</param>
/// <param name="target">即将执行的 legacy 目标对象。</param>
private static void PrepareTarget(ICqrsContext context, object target)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(target);
if (context is not GFramework.Core.Abstractions.Architectures.IArchitectureContext architectureContext)
{
throw new InvalidOperationException(
$"RecordingCqrsRuntime 期望收到 IArchitectureContext但实际为 '{context.GetType().FullName}'。");
}
if (target is IContextAware contextAware)
{
contextAware.SetContext(architectureContext);
}
}
}

View File

@ -0,0 +1,11 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
namespace GFramework.Core.Tests.Command;
/// <summary>
/// 为 legacy 同步 bridge 回归测试提供可识别的同步上下文占位类型。
/// </summary>
internal sealed class TestLegacySynchronizationContext : SynchronizationContext
{
}

View File

@ -0,0 +1,87 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Cqrs;
using GFramework.Core.Rule;
using GFramework.Core.Tests.Architectures;
namespace GFramework.Core.Tests.Cqrs;
/// <summary>
/// 验证 legacy 异步无返回值命令 bridge handler 的取消语义。
/// </summary>
[TestFixture]
public class LegacyAsyncCommandDispatchRequestHandlerTests
{
/// <summary>
/// 验证当取消令牌在执行前已触发时handler 不会启动底层 legacy 命令。
/// </summary>
[Test]
public void Handle_Should_Throw_Without_Executing_Command_When_Cancellation_Is_Already_Requested()
{
var handler = new LegacyAsyncCommandDispatchRequestHandler();
var command = new ProbeAsyncCommand(Task.CompletedTask);
using var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Cancel();
Assert.ThrowsAsync<OperationCanceledException>(
async () => await handler.Handle(
new LegacyAsyncCommandDispatchRequest(command),
cancellationTokenSource.Token)
.AsTask()
.ConfigureAwait(false));
Assert.That(command.ExecutionCount, Is.Zero);
}
/// <summary>
/// 验证当底层 legacy 命令正在运行时handler 会通过 <c>WaitAsync</c> 及时向调用方暴露取消。
/// </summary>
[Test]
public async Task Handle_Should_Observe_Cancellation_While_Command_Is_Running()
{
var completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var handler = new LegacyAsyncCommandDispatchRequestHandler();
var command = new ProbeAsyncCommand(completionSource.Task);
using var cancellationTokenSource = new CancellationTokenSource();
((IContextAware)handler).SetContext(new TestArchitectureContextBaseStub());
var handleTask = handler.Handle(
new LegacyAsyncCommandDispatchRequest(command),
cancellationTokenSource.Token)
.AsTask();
cancellationTokenSource.Cancel();
Assert.That(
async () => await handleTask.ConfigureAwait(false),
Throws.InstanceOf<OperationCanceledException>());
Assert.That(command.ExecutionCount, Is.EqualTo(1));
}
/// <summary>
/// 为 handler 取消测试提供可控完成时机的异步命令替身。
/// </summary>
private sealed class ProbeAsyncCommand(Task executionTask) : ContextAwareBase, IAsyncCommand
{
/// <summary>
/// 获取底层命令逻辑的触发次数。
/// </summary>
public int ExecutionCount { get; private set; }
/// <inheritdoc />
public Task ExecuteAsync()
{
ExecutionCount++;
return executionTask;
}
}
/// <summary>
/// 为 handler 取消测试提供最小架构上下文替身。
/// </summary>
private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
{
}
}

View File

@ -16,7 +16,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1"/>
<PackageReference Include="Moq" Version="4.20.72"/>
<PackageReference Include="NUnit" Version="4.5.1"/>
<PackageReference Include="NUnit" Version="4.6.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0"/>
</ItemGroup>

View File

@ -3,6 +3,9 @@
using GFramework.Core.Ioc;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace GFramework.Core.Tests.Ioc;
@ -22,6 +25,18 @@ public class IocContainerLifetimeTests
public Guid Id { get; } = Guid.NewGuid();
}
private sealed class DisposableTestService : ITestService, IDisposable
{
public Guid Id { get; } = Guid.NewGuid();
public bool IsDisposed { get; private set; }
public void Dispose()
{
IsDisposed = true;
}
}
[Test]
public void RegisterSingleton_Should_Return_Same_Instance()
{
@ -207,4 +222,112 @@ public class IocContainerLifetimeTests
scope2.Dispose();
scope3.Dispose();
}
}
[Test]
public void Dispose_Should_Dispose_Resolved_Singleton_And_Block_Further_Use()
{
// Arrange
var container = new MicrosoftDiContainer();
container.RegisterSingleton<DisposableTestService, DisposableTestService>();
container.Freeze();
var service = container.GetRequired<DisposableTestService>();
// Act
container.Dispose();
// Assert
Assert.That(service.IsDisposed, Is.True);
Assert.Throws<ObjectDisposedException>(() => container.Get<DisposableTestService>());
Assert.Throws<ObjectDisposedException>(() => container.CreateScope());
}
[Test]
public void Dispose_Should_Be_Idempotent()
{
var container = new MicrosoftDiContainer();
Assert.DoesNotThrow(container.Dispose);
Assert.DoesNotThrow(container.Dispose);
}
[Test]
public void Dispose_Should_Be_Idempotent_When_Called_Concurrently()
{
var container = new MicrosoftDiContainer();
var containerLock = GetContainerLock(container);
var releasedGate = false;
containerLock.EnterWriteLock();
try
{
var firstDisposeTask = Task.Run(container.Dispose);
Thread.Sleep(50);
var secondDisposeTask = Task.Run(container.Dispose);
Thread.Sleep(50);
containerLock.ExitWriteLock();
releasedGate = true;
Assert.That(async () => await Task.WhenAll(firstDisposeTask, secondDisposeTask).ConfigureAwait(false), Throws.Nothing);
}
finally
{
if (!releasedGate)
{
containerLock.ExitWriteLock();
}
}
}
[Test]
public void Dispose_Should_Only_Attempt_Lock_Disposal_Once_When_Called_Concurrently()
{
var container = new MicrosoftDiContainer();
var containerLock = GetContainerLock(container);
var releasedGate = false;
containerLock.EnterWriteLock();
try
{
var firstDisposeTask = Task.Run(container.Dispose);
Thread.Sleep(50);
var secondDisposeTask = Task.Run(container.Dispose);
Thread.Sleep(50);
containerLock.ExitWriteLock();
releasedGate = true;
Assert.That(async () => await Task.WhenAll(firstDisposeTask, secondDisposeTask).ConfigureAwait(false), Throws.Nothing);
Assert.That(GetLockDisposalStarted(container), Is.EqualTo(1));
}
finally
{
if (!releasedGate)
{
containerLock.ExitWriteLock();
}
}
}
/// <summary>
/// 通过反射获取容器内部锁,用于构造可重复的并发释放竞态回归。
/// </summary>
private static ReaderWriterLockSlim GetContainerLock(MicrosoftDiContainer container)
{
ArgumentNullException.ThrowIfNull(container);
var lockField = typeof(MicrosoftDiContainer).GetField("_lock", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.That(lockField, Is.Not.Null);
return (ReaderWriterLockSlim)lockField!.GetValue(container)!;
}
/// <summary>
/// 读取锁销毁启动标记,验证并发释放路径不会重复执行底层锁销毁。
/// </summary>
private static int GetLockDisposalStarted(MicrosoftDiContainer container)
{
ArgumentNullException.ThrowIfNull(container);
var flagField = typeof(MicrosoftDiContainer).GetField("_lockDisposalStarted", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.That(flagField, Is.Not.Null);
return (int)flagField!.GetValue(container)!;
}
}

View File

@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
@ -156,6 +158,21 @@ public class MicrosoftDiContainerTests
Assert.That(result, Is.SameAs(instance));
}
/// <summary>
/// 测试预冻结阶段通过 RegisterPlurality 注册的接口别名仍可通过 Get 解析到同一实例。
/// </summary>
[Test]
public void Get_Should_Return_RegisterPlurality_Interface_Instance_Before_Freeze()
{
var instance = new TestService();
_container.RegisterPlurality(instance);
var result = _container.Get<IService>();
Assert.That(result, Is.SameAs(instance));
}
/// <summary>
/// 测试当 CQRS 基础设施已手动接线后,再调用处理器注册入口不会重复注册 runtime seam。
/// </summary>
@ -276,6 +293,32 @@ public class MicrosoftDiContainerTests
Assert.That(results.Count, Is.EqualTo(0));
}
/// <summary>
/// 测试预冻结阶段通过实现类型注册的服务不会被当作已物化实例返回。
/// </summary>
[Test]
public void Get_Should_Return_Null_PreFreeze_For_ImplementationType_Registration()
{
_container.RegisterSingleton<IService, TestService>();
var result = _container.Get<IService>();
Assert.That(result, Is.Null);
}
/// <summary>
/// 测试预冻结阶段通过实现类型注册的服务在 GetAll 中同样不可见。
/// </summary>
[Test]
public void GetAll_Should_Return_Empty_PreFreeze_For_ImplementationType_Registration()
{
_container.RegisterSingleton<IService, TestService>();
var results = _container.GetAll<IService>();
Assert.That(results, Is.Empty);
}
/// <summary>
/// 测试容器未冻结时,会折叠“不同服务类型指向同一实例”的兼容别名重复,
/// 但会保留同一服务类型的重复显式注册。
@ -351,6 +394,21 @@ public class MicrosoftDiContainerTests
Assert.That(_container.Get<TestService>(), Is.SameAs(instance));
}
/// <summary>
/// 测试预冻结阶段通过 RegisterPlurality 注册的接口别名对 Contains 与 GetAll 都可见。
/// </summary>
[Test]
public void Contains_Should_Return_True_For_RegisterPlurality_Interface_Alias_Before_Freeze()
{
var instance = new TestService();
_container.RegisterPlurality(instance);
var services = _container.GetAll<IService>();
Assert.That(services, Has.Count.EqualTo(1));
Assert.That(_container.Contains<IService>(), Is.True);
}
/// <summary>
/// 测试当不存在实例时检查包含关系应返回 false 的功能
@ -361,6 +419,47 @@ public class MicrosoftDiContainerTests
Assert.That(_container.Contains<TestService>(), Is.False);
}
/// <summary>
/// 测试显式服务不存在时HasRegistration 应返回 false且不会要求先冻结或解析实例。
/// </summary>
[Test]
public void HasRegistration_WithNoMatchingService_Should_ReturnFalse()
{
Assert.That(_container.HasRegistration(typeof(IPipelineBehavior<HasRegistrationRequest, int>)), Is.False);
}
/// <summary>
/// 测试 HasRegistration 能识别开放泛型 CQRS pipeline 行为对闭合请求/响应对的可见性。
/// </summary>
[Test]
public void HasRegistration_Should_ReturnTrue_For_Closed_Service_Satisfied_By_Open_Generic_Registration()
{
_container.GetServicesUnsafe.AddSingleton(
typeof(IPipelineBehavior<,>),
typeof(OpenGenericHasRegistrationBehavior<,>));
Assert.That(_container.HasRegistration(typeof(IPipelineBehavior<HasRegistrationRequest, int>)), Is.True);
_container.Freeze();
Assert.That(_container.HasRegistration(typeof(IPipelineBehavior<HasRegistrationRequest, int>)), Is.True);
}
/// <summary>
/// 测试 HasRegistration 不会把仅以具体实现类型自注册的服务误判成其接口服务键也已注册。
/// </summary>
[Test]
public void HasRegistration_Should_ReturnFalse_For_Interface_When_Only_Concrete_Service_Key_Is_Registered()
{
_container.GetServicesUnsafe.AddSingleton(typeof(SelfRegisteredConcreteBehavior), typeof(SelfRegisteredConcreteBehavior));
Assert.That(_container.HasRegistration(typeof(IPipelineBehavior<HasRegistrationRequest, int>)), Is.False);
_container.Freeze();
Assert.That(_container.HasRegistration(typeof(IPipelineBehavior<HasRegistrationRequest, int>)), Is.False);
}
/// <summary>
/// 测试当实例存在时检查实例包含关系应返回 true 的功能
/// </summary>
@ -428,6 +527,21 @@ public class MicrosoftDiContainerTests
Is.True);
}
/// <summary>
/// 测试 RegisterCqrsHandlersFromAssemblies 会通过注册阶段可见实例解析 CQRS 注册服务。
/// </summary>
[Test]
public void RegisterCqrsHandlersFromAssemblies_Should_Resolve_Registration_Service_When_Registered_As_Instance()
{
Assert.DoesNotThrow(() =>
_container.RegisterCqrsHandlersFromAssemblies([typeof(DeterministicOrderNotification).Assembly]));
Assert.That(
_container.GetServicesUnsafe.Any(static descriptor =>
descriptor.ServiceType == typeof(INotificationHandler<DeterministicOrderNotification>)),
Is.True);
}
/// <summary>
/// 测试当程序集集合中包含空元素时CQRS handler 注册入口会在委托给注册服务前直接失败。
/// </summary>
@ -760,4 +874,118 @@ public class MicrosoftDiContainerTests
Assert.That(((IPrioritizedService)services[0]).Priority, Is.EqualTo(10));
Assert.That(((IPrioritizedService)services[1]).Priority, Is.EqualTo(30));
}
/// <summary>
/// 测试容器释放后会阻止后续注册与解析,避免 benchmark 或短生命周期宿主继续使用已回收状态。
/// </summary>
[Test]
public void Dispose_Should_Block_Subsequent_Registration_And_Query_Operations()
{
_container.Dispose();
Assert.Throws<ObjectDisposedException>(() => _container.Register(new TestService()));
Assert.Throws<ObjectDisposedException>(() => _container.Contains<TestService>());
Assert.Throws<ObjectDisposedException>(() => _container.GetAll<TestService>());
}
/// <summary>
/// 测试等待中的读取线程在容器释放后也会收到稳定的容器级释放异常,而不是底层锁异常。
/// </summary>
[Test]
public async Task Dispose_Should_Translate_Waiting_Readers_To_Container_ObjectDisposedException()
{
_container.RegisterSingleton(new TestService());
_container.Freeze();
var containerLock = GetContainerLock(_container);
var releasedGate = false;
using var queryStarted = new ManualResetEventSlim(false);
containerLock.EnterWriteLock();
try
{
var queryTask = Task.Run(() =>
{
queryStarted.Set();
return _container.Get<TestService>();
});
Assert.That(queryStarted.Wait(TimeSpan.FromSeconds(1)), Is.True);
var disposeTask = Task.Run(_container.Dispose);
Thread.Sleep(50);
containerLock.ExitWriteLock();
releasedGate = true;
await disposeTask.ConfigureAwait(false);
var exception = Assert.ThrowsAsync<ObjectDisposedException>(async () => await queryTask.ConfigureAwait(false));
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.ObjectName, Is.EqualTo(nameof(MicrosoftDiContainer)));
}
finally
{
if (!releasedGate)
{
containerLock.ExitWriteLock();
}
}
}
/// <summary>
/// 通过反射获取容器内部锁,用于构造可重复的并发释放竞态回归。
/// </summary>
private static ReaderWriterLockSlim GetContainerLock(MicrosoftDiContainer container)
{
ArgumentNullException.ThrowIfNull(container);
var lockField = typeof(MicrosoftDiContainer).GetField("_lock", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.That(lockField, Is.Not.Null);
return (ReaderWriterLockSlim)lockField!.GetValue(container)!;
}
/// <summary>
/// 供 HasRegistration 回归使用的最小请求类型。
/// </summary>
private sealed class HasRegistrationRequest : IRequest<int>
{
}
/// <summary>
/// 供 HasRegistration 回归使用的开放泛型 pipeline 行为。
/// </summary>
/// <typeparam name="TRequest">请求类型。</typeparam>
/// <typeparam name="TResponse">响应类型。</typeparam>
private sealed class OpenGenericHasRegistrationBehavior<TRequest, TResponse> :
IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
/// <summary>
/// 透传到下一个 pipeline 节点,不额外改变请求语义。
/// </summary>
public ValueTask<TResponse> Handle(
TRequest request,
MessageHandlerDelegate<TRequest, TResponse> next,
CancellationToken cancellationToken)
{
return next(request, cancellationToken);
}
}
/// <summary>
/// 供 HasRegistration 服务键判定回归使用的最小封闭 pipeline 行为。
/// </summary>
private sealed class SelfRegisteredConcreteBehavior : IPipelineBehavior<HasRegistrationRequest, int>
{
/// <summary>
/// 透传到下一个 pipeline 节点,不额外改变请求语义。
/// </summary>
public ValueTask<int> Handle(
HasRegistrationRequest request,
MessageHandlerDelegate<HasRegistrationRequest, int> next,
CancellationToken cancellationToken)
{
return next(request, cancellationToken);
}
}
}

View File

@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Query;
using GFramework.Core.Tests.Architectures;
using GFramework.Core.Tests.Command;
namespace GFramework.Core.Tests.Query;
@ -138,4 +140,33 @@ public class AsyncQueryExecutorTests
Assert.That(result1, Is.EqualTo(20));
Assert.That(result2, Is.EqualTo(40));
}
/// <summary>
/// 验证 legacy 异步查询桥接会保留上下文注入,并通过 runtime 返回结果。
/// </summary>
[Test]
public async Task SendAsync_Should_Bridge_Through_Runtime_And_Preserve_Context()
{
var runtime = new RecordingCqrsRuntime(static _ => 64);
var executor = new AsyncQueryExecutor(runtime);
var query = new ContextAwareLegacyAsyncQuery(64);
var expectedContext = new TestArchitectureContextBaseStub();
((GFramework.Core.Abstractions.Rule.IContextAware)query).SetContext(expectedContext);
var result = await executor.SendAsync(query);
Assert.Multiple(() =>
{
Assert.That(result, Is.EqualTo(64));
Assert.That(runtime.LastRequest, Is.TypeOf<GFramework.Core.Cqrs.LegacyAsyncQueryDispatchRequest>());
Assert.That(query.ObservedContext, Is.SameAs(expectedContext));
});
}
/// <summary>
/// 为异步 bridge 测试提供最小架构上下文替身。
/// </summary>
private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
{
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Rule;
namespace GFramework.Core.Tests.Query;
/// <summary>
/// 为 <see cref="AsyncQueryExecutorTests" /> 提供可观察上下文注入的 legacy 异步查询。
/// </summary>
internal sealed class ContextAwareLegacyAsyncQuery(int result) : ContextAwareBase, IAsyncQuery<int>
{
/// <summary>
/// 获取执行期间观察到的架构上下文。
/// </summary>
public IArchitectureContext? ObservedContext { get; private set; }
/// <inheritdoc />
public Task<int> DoAsync()
{
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
return Task.FromResult(result);
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Rule;
namespace GFramework.Core.Tests.Query;
/// <summary>
/// 为 <see cref="QueryExecutorTests" /> 提供可观察上下文注入的 legacy 查询。
/// </summary>
internal sealed class ContextAwareLegacyQuery(int result) : ContextAwareBase, IQuery<int>
{
/// <summary>
/// 获取执行期间观察到的架构上下文。
/// </summary>
public IArchitectureContext? ObservedContext { get; private set; }
/// <inheritdoc />
public int Do()
{
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
return result;
}
}

View File

@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Query;
using GFramework.Core.Tests.Architectures;
using GFramework.Core.Tests.Command;
namespace GFramework.Core.Tests.Query;
@ -61,4 +63,44 @@ public class QueryExecutorTests
Assert.That(result, Is.EqualTo("Result: 10"));
}
/// <summary>
/// 验证 legacy 同步查询桥接会在线程池上等待 runtime
/// 避免直接复用调用方的同步上下文。
/// </summary>
[Test]
public void Send_Should_Bridge_Through_Runtime_Without_Reusing_Caller_SynchronizationContext()
{
var runtime = new RecordingCqrsRuntime(static _ => 24);
var executor = new QueryExecutor(runtime);
var query = new ContextAwareLegacyQuery(24);
var expectedContext = new TestArchitectureContextBaseStub();
((GFramework.Core.Abstractions.Rule.IContextAware)query).SetContext(expectedContext);
var originalContext = SynchronizationContext.Current;
try
{
SynchronizationContext.SetSynchronizationContext(new TestLegacySynchronizationContext());
var result = executor.Send(query);
Assert.Multiple(() =>
{
Assert.That(result, Is.EqualTo(24));
Assert.That(runtime.LastRequest, Is.TypeOf<GFramework.Core.Cqrs.LegacyQueryDispatchRequest>());
Assert.That(runtime.ObservedSynchronizationContextType, Is.Null);
});
}
finally
{
SynchronizationContext.SetSynchronizationContext(originalContext);
}
}
/// <summary>
/// 为同步 bridge 测试提供最小架构上下文替身。
/// </summary>
private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
{
}
}

View File

@ -83,22 +83,15 @@ public class ContextAwareTests
}
/// <summary>
/// 测试 GetContext 方法在未设置上下文时的行为
/// 验证当内部 Context 为 null 时GetContext 方法不会抛出异常
/// 此时应返回第一个架构上下文(在测试环境中验证不抛出异常即可)
/// 测试 GetContext 方法在未设置上下文时会回退到当前活动上下文
/// </summary>
[Test]
public void GetContext_Should_Return_FirstArchitectureContext_When_Not_Set()
public void GetContext_Should_Return_CurrentArchitectureContext_When_Not_Set()
{
// Arrange - 暂时不调用 SetContext让 Context 为 null
IContextAware aware = _contextAware;
// Act - 当 Context 为 null 时,应该返回第一个 Architecture Context
// 由于测试环境中没有实际的 Architecture Context这里只测试调用不会抛出异常
// 在实际使用中,当 Context 为 null 时会调用 GameContext.GetFirstArchitectureContext()
var result = aware.GetContext();
// Assert - 验证在没有设置 Context 时的行为
// 注意:由于测试环境中可能没有 Architecture Context这里我们只测试不抛出异常
Assert.DoesNotThrow(() => aware.GetContext());
Assert.That(result, Is.SameAs(_mockContext));
}
}

View File

@ -177,6 +177,21 @@ public abstract class Architecture : IArchitecture
_modules.RegisterCqrsPipelineBehavior<TBehavior>();
}
/// <summary>
/// 注册 CQRS 流式请求管道行为。
/// 可以传入开放泛型行为类型,也可以传入绑定到特定流式请求的封闭行为类型。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
/// <exception cref="InvalidOperationException">当前架构的底层容器已冻结,无法继续注册流式管道行为。</exception>
/// <exception cref="ObjectDisposedException">当前架构的底层容器已释放,无法继续注册流式管道行为。</exception>
/// <remarks>
/// 该调用会委托到底层容器完成校验与注册,因此应在初始化冻结前完成所有流式行为接线。
/// </remarks>
public void RegisterCqrsStreamPipelineBehavior<TBehavior>() where TBehavior : class
{
_modules.RegisterCqrsStreamPipelineBehavior<TBehavior>();
}
/// <summary>
/// 从指定程序集显式注册 CQRS 处理器。
/// 该入口适用于把拆分到其他模块或扩展包程序集中的 handlers 接入当前架构。
@ -359,18 +374,44 @@ public abstract class Architecture : IArchitecture
/// <summary>
/// 异步销毁架构及所有组件
/// </summary>
/// <remarks>
/// 无论 <c>_lifecycle.DestroyAsync()</c> 是否抛出异常,该方法都会在 <see langword="finally" /> 中调用
/// <see cref="GameContext.Unbind" />(<see cref="object.GetType" />()),移除当前架构类型在全局上下文表中的绑定。
/// 这样可以阻止新的惰性上下文回退命中已销毁实例;但已经缓存上下文的对象不会被自动重置。
/// </remarks>
public virtual async ValueTask DestroyAsync()
{
await _lifecycle.DestroyAsync().ConfigureAwait(false);
try
{
await _lifecycle.DestroyAsync().ConfigureAwait(false);
}
finally
{
// 架构初始化时会把当前实例绑定到 GameContext销毁后必须解除该全局回退入口
// 避免后续惰性 ContextAware 调用继续命中过期的运行时上下文。
GameContext.Unbind(GetType());
}
}
/// <summary>
/// 销毁架构并清理所有组件资源(同步方法,保留用于向后兼容)
/// </summary>
/// <remarks>
/// 该同步兼容入口会与 <see cref="DestroyAsync" /> 保持相同的全局解绑语义;即使销毁过程抛出异常,
/// 也会在 <see langword="finally" /> 中调用 <see cref="GameContext.Unbind" />(<see cref="object.GetType" />())。
/// </remarks>
[Obsolete("建议使用 DestroyAsync() 以支持异步清理")]
public virtual void Destroy()
{
_lifecycle.Destroy();
try
{
_lifecycle.Destroy();
}
finally
{
// 同步销毁同样需要解除全局回退入口,避免兼容调用路径保留过期上下文。
GameContext.Unbind(GetType());
}
}
#endregion

View File

@ -11,6 +11,7 @@ using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
@ -111,7 +112,8 @@ public class ArchitectureContext : IArchitectureContext
/// <returns>响应结果</returns>
public TResponse SendRequest<TResponse>(IRequest<TResponse> request)
{
return SendRequestAsync(request).AsTask().GetAwaiter().GetResult();
ArgumentNullException.ThrowIfNull(request);
return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, request);
}
/// <summary>
@ -180,10 +182,12 @@ public class ArchitectureContext : IArchitectureContext
/// <returns>查询结果</returns>
public TResult SendQuery<TResult>(IQuery<TResult> query)
{
if (query == null) throw new ArgumentNullException(nameof(query));
var queryBus = GetOrCache<IQueryExecutor>();
if (queryBus == null) throw new InvalidOperationException("IQueryExecutor not registered");
return queryBus.Send(query);
ArgumentNullException.ThrowIfNull(query);
var boxedResult = SendRequest(
new LegacyQueryDispatchRequest(
query,
() => query.Do()));
return (TResult)boxedResult!;
}
/// <summary>
@ -192,9 +196,10 @@ public class ArchitectureContext : IArchitectureContext
/// <typeparam name="TResponse">查询响应类型</typeparam>
/// <param name="query">要发送的查询对象</param>
/// <returns>查询结果</returns>
public TResponse SendQuery<TResponse>(Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
public TResponse SendQuery<TResponse>(global::GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
{
return SendQueryAsync(query).AsTask().GetAwaiter().GetResult();
ArgumentNullException.ThrowIfNull(query);
return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, query);
}
/// <summary>
@ -205,10 +210,13 @@ public class ArchitectureContext : IArchitectureContext
/// <returns>查询结果</returns>
public async Task<TResult> SendQueryAsync<TResult>(IAsyncQuery<TResult> query)
{
if (query == null) throw new ArgumentNullException(nameof(query));
var asyncQueryBus = GetOrCache<IAsyncQueryExecutor>();
if (asyncQueryBus == null) throw new InvalidOperationException("IAsyncQueryExecutor not registered");
return await asyncQueryBus.SendAsync(query).ConfigureAwait(false);
ArgumentNullException.ThrowIfNull(query);
var boxedResult = await SendRequestAsync(
new LegacyAsyncQueryDispatchRequest(
query,
async () => await query.DoAsync().ConfigureAwait(false)))
.ConfigureAwait(false);
return (TResult)boxedResult!;
}
/// <summary>
@ -218,7 +226,7 @@ public class ArchitectureContext : IArchitectureContext
/// <param name="query">要发送的查询对象</param>
/// <param name="cancellationToken">取消令牌,用于取消操作</param>
/// <returns>包含查询结果的ValueTask</returns>
public async ValueTask<TResponse> SendQueryAsync<TResponse>(Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
public async ValueTask<TResponse> SendQueryAsync<TResponse>(global::GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(query);
@ -355,7 +363,7 @@ public class ArchitectureContext : IArchitectureContext
/// <param name="cancellationToken">取消令牌,用于取消操作</param>
/// <returns>包含命令执行结果的ValueTask</returns>
public async ValueTask<TResponse> SendCommandAsync<TResponse>(
Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
global::GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(command);
@ -369,9 +377,7 @@ public class ArchitectureContext : IArchitectureContext
public async Task SendCommandAsync(IAsyncCommand command)
{
ArgumentNullException.ThrowIfNull(command);
var commandBus = GetOrCache<ICommandExecutor>();
if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered");
await commandBus.SendAsync(command).ConfigureAwait(false);
await SendRequestAsync(new LegacyAsyncCommandDispatchRequest(command)).ConfigureAwait(false);
}
/// <summary>
@ -383,9 +389,12 @@ public class ArchitectureContext : IArchitectureContext
public async Task<TResult> SendCommandAsync<TResult>(IAsyncCommand<TResult> command)
{
ArgumentNullException.ThrowIfNull(command);
var commandBus = GetOrCache<ICommandExecutor>();
if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered");
return await commandBus.SendAsync(command).ConfigureAwait(false);
var boxedResult = await SendRequestAsync(
new LegacyAsyncCommandResultDispatchRequest(
command,
async () => await command.ExecuteAsync().ConfigureAwait(false)))
.ConfigureAwait(false);
return (TResult)boxedResult!;
}
/// <summary>
@ -394,9 +403,10 @@ public class ArchitectureContext : IArchitectureContext
/// <typeparam name="TResponse">命令响应类型</typeparam>
/// <param name="command">要发送的命令对象</param>
/// <returns>命令执行结果</returns>
public TResponse SendCommand<TResponse>(Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
public TResponse SendCommand<TResponse>(global::GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
{
return SendCommandAsync(command).AsTask().GetAwaiter().GetResult();
ArgumentNullException.ThrowIfNull(command);
return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, command);
}
/// <summary>
@ -406,8 +416,7 @@ public class ArchitectureContext : IArchitectureContext
public void SendCommand(ICommand command)
{
ArgumentNullException.ThrowIfNull(command);
var commandBus = GetOrCache<ICommandExecutor>();
commandBus.Send(command);
SendRequest(new LegacyCommandDispatchRequest(command));
}
/// <summary>
@ -419,9 +428,11 @@ public class ArchitectureContext : IArchitectureContext
public TResult SendCommand<TResult>(ICommand<TResult> command)
{
ArgumentNullException.ThrowIfNull(command);
var commandBus = GetOrCache<ICommandExecutor>();
if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered");
return commandBus.Send(command);
var boxedResult = SendRequest(
new LegacyCommandResultDispatchRequest(
command,
() => command.Execute()));
return (TResult)boxedResult!;
}
#endregion

View File

@ -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 处理器。
/// 该入口用于把默认架构程序集之外的扩展处理器接入当前架构容器。

View File

@ -2,58 +2,101 @@
// SPDX-License-Identifier: Apache-2.0
using System.Collections.Concurrent;
using System.Threading;
using GFramework.Core.Abstractions.Architectures;
namespace GFramework.Core.Architectures;
/// <summary>
/// 游戏上下文管理类,用于管理当前的架构上下文实例
/// 游戏上下文管理类,用于管理当前活动的架构上下文实例及其兼容类型别名。
/// </summary>
public static class GameContext
{
// ConcurrentDictionary 负责向外暴露安全的实时视图;该锁负责维护“别名字典 + 当前活动上下文”之间的组合不变式。
#if NET9_0_OR_GREATER
private static readonly Lock SyncRoot = new();
#else
private static readonly object SyncRoot = new();
#endif
private static readonly ConcurrentDictionary<Type, IArchitectureContext> ArchitectureDictionary
= new();
private static IArchitectureContext? _currentArchitectureContext;
/// <summary>
/// 获取所有已注册的架构上下文的只读字典
/// 获取所有已注册的架构上下文类型别名映射。
/// 该只读视图会反映当前并发状态,不保证是稳定快照。
/// </summary>
public static IReadOnlyDictionary<Type, IArchitectureContext> ArchitectureReadOnlyDictionary =>
ArchitectureDictionary;
/// <summary>
/// 绑定指定类型的架构上下文到管理器中
/// 绑定指定类型的架构上下文到管理器中。
/// 同一时刻只允许存在一个活动上下文实例,但可以为其绑定多个兼容类型别名。
/// </summary>
/// <param name="architectureType">架构类型</param>
/// <param name="context">架构上下文实例</param>
/// <exception cref="InvalidOperationException">当指定类型的架构上下文已存在时抛出</exception>
/// <exception cref="ArgumentNullException">
/// <paramref name="architectureType" /> 或 <paramref name="context" /> 为 <see langword="null" />。
/// </exception>
/// <exception cref="InvalidOperationException">当指定类型的架构上下文已存在,或尝试绑定第二个不同上下文实例时抛出。</exception>
public static void Bind(Type architectureType, IArchitectureContext context)
{
if (!ArchitectureDictionary.TryAdd(architectureType, context))
throw new InvalidOperationException(
$"Architecture context for '{architectureType.Name}' already exists");
ArgumentNullException.ThrowIfNull(architectureType);
ArgumentNullException.ThrowIfNull(context);
lock (SyncRoot)
{
if (_currentArchitectureContext != null && !ReferenceEquals(_currentArchitectureContext, context))
throw new InvalidOperationException(
$"GameContext already tracks active context '{_currentArchitectureContext.GetType().Name}'. " +
$"Cannot bind a different context '{context.GetType().Name}'.");
if (!ArchitectureDictionary.TryAdd(architectureType, context))
throw new InvalidOperationException(
$"Architecture context for '{architectureType.Name}' already exists");
_currentArchitectureContext ??= context;
}
}
/// <summary>
/// 获取字典中的第一个架构上下文
/// 获取当前活动的架构上下文。
/// 该方法保留原有名称以兼容存量调用方,但语义已经收敛为“当前上下文”,而不是任意字典首项。
/// </summary>
/// <returns>返回字典中的第一个架构上下文实例</returns>
/// <exception cref="InvalidOperationException">当字典为空时抛出</exception>
/// <returns>当前活动的架构上下文实例。</returns>
/// <exception cref="InvalidOperationException">当当前没有活动上下文时抛出。</exception>
public static IArchitectureContext GetFirstArchitectureContext()
{
return ArchitectureDictionary.Values.First();
lock (SyncRoot)
{
if (_currentArchitectureContext is { } context)
return context;
}
throw new InvalidOperationException("No active architecture context is currently bound.");
}
/// <summary>
/// 根据类型获取对应的架构上下文
/// 根据类型获取对应的架构上下文。
/// 兼容层会优先查找显式绑定的类型别名,然后回退到当前上下文的类型兼容判断。
/// </summary>
/// <param name="type">要查找的架构类型</param>
/// <returns>返回指定类型的架构上下文实例</returns>
/// <exception cref="ArgumentNullException"><paramref name="type" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">当指定类型的架构上下文不存在时抛出</exception>
public static IArchitectureContext GetByType(Type type)
{
if (ArchitectureDictionary.TryGetValue(type, out var context))
return context;
ArgumentNullException.ThrowIfNull(type);
lock (SyncRoot)
{
if (ArchitectureDictionary.TryGetValue(type, out var context))
return context;
if (_currentArchitectureContext != null && type.IsInstanceOfType(_currentArchitectureContext))
return _currentArchitectureContext;
}
throw new InvalidOperationException(
$"Architecture context for '{type.Name}' not found");
@ -61,22 +104,30 @@ public static class GameContext
/// <summary>
/// 获取指定类型的架构上下文实例
/// 获取指定类型的架构上下文实例。
/// 该方法会优先复用当前活动上下文,再回退到显式注册的类型别名。
/// </summary>
/// <typeparam name="T">架构上下文类型必须实现IArchitectureContext接口</typeparam>
/// <returns>指定类型的架构上下文实例</returns>
/// <exception cref="InvalidOperationException">当指定类型的架构上下文不存在时抛出</exception>
public static T Get<T>() where T : class, IArchitectureContext
{
if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx))
return (T)ctx;
lock (SyncRoot)
{
if (_currentArchitectureContext is T currentContext)
return currentContext;
if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx))
return (T)ctx;
}
throw new InvalidOperationException(
$"Architecture context '{typeof(T).Name}' not found");
}
/// <summary>
/// 尝试获取指定类型的架构上下文实例
/// 尝试获取指定类型的架构上下文实例。
/// 该方法会优先检查当前活动上下文是否兼容目标类型,再回退到显式注册的类型别名。
/// </summary>
/// <typeparam name="T">架构上下文类型必须实现IArchitectureContext接口</typeparam>
/// <param name="context">输出参数如果找到则返回对应的架构上下文实例否则返回null</param>
@ -84,10 +135,19 @@ public static class GameContext
public static bool TryGet<T>(out T? context)
where T : class, IArchitectureContext
{
if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx))
lock (SyncRoot)
{
context = (T)ctx;
return true;
if (_currentArchitectureContext is T currentContext)
{
context = currentContext;
return true;
}
if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx))
{
context = (T)ctx;
return true;
}
}
context = null;
@ -95,20 +155,54 @@ public static class GameContext
}
/// <summary>
/// 移除指定类型的架构上下文绑定
/// 移除指定类型的架构上下文绑定。
/// 当最后一个指向当前活动上下文的别名被移除时,也会同步清空当前活动上下文指针。
/// </summary>
/// <param name="architectureType">要移除的架构类型</param>
/// <exception cref="ArgumentNullException"><paramref name="architectureType" /> 为 <see langword="null" />。</exception>
public static void Unbind(Type architectureType)
{
ArchitectureDictionary.TryRemove(architectureType, out _);
ArgumentNullException.ThrowIfNull(architectureType);
lock (SyncRoot)
{
if (!ArchitectureDictionary.TryRemove(architectureType, out var removedContext))
return;
if (_currentArchitectureContext == null || !ReferenceEquals(_currentArchitectureContext, removedContext))
return;
if (!HasAliasForContext(removedContext))
_currentArchitectureContext = null;
}
}
/// <summary>
/// 清空所有架构上下文绑定
/// 清空所有架构上下文绑定,并重置当前活动上下文。
/// </summary>
public static void Clear()
{
ArchitectureDictionary.Clear();
lock (SyncRoot)
{
ArchitectureDictionary.Clear();
_currentArchitectureContext = null;
}
}
}
/// <summary>
/// 判断当前是否仍存在指向同一上下文实例的其他类型别名。
/// </summary>
/// <param name="context">被移除绑定原本指向的上下文实例。</param>
/// <returns>如果还有其他别名指向同一实例则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
private static bool HasAliasForContext(IArchitectureContext context)
{
foreach (var current in ArchitectureDictionary.Values)
{
if (ReferenceEquals(current, context))
return true;
}
return false;
}
}

View File

@ -6,21 +6,24 @@ using GFramework.Core.Abstractions.Architectures;
namespace GFramework.Core.Architectures;
/// <summary>
/// 基于 GameContext 的默认上下文提供者
/// 基于 GameContext 的默认上下文提供者。
/// 默认只面向当前活动上下文工作,而不是维护多个并存的全局上下文。
/// </summary>
public sealed class GameContextProvider : IArchitectureContextProvider
{
/// <summary>
/// 获取当前的架构上下文(返回第一个注册的架构上下文)
/// 获取当前的架构上下文
/// </summary>
/// <returns>架构上下文实例</returns>
/// <exception cref="InvalidOperationException">当前没有已绑定的活动架构上下文时抛出。</exception>
public IArchitectureContext GetContext()
{
return GameContext.GetFirstArchitectureContext();
}
/// <summary>
/// 尝试获取指定类型的架构上下文
/// 尝试获取指定类型的架构上下文。
/// 若当前活动上下文本身兼容 <typeparamref name="T" />,则无需显式类型别名也会返回成功。
/// </summary>
/// <typeparam name="T">架构上下文类型</typeparam>
/// <param name="context">输出的上下文实例</param>
@ -29,4 +32,4 @@ public sealed class GameContextProvider : IArchitectureContextProvider
{
return GameContext.TryGet(out context);
}
}
}

View File

@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
using IAsyncCommand = GFramework.Core.Abstractions.Command.IAsyncCommand;
namespace GFramework.Core.Command;
@ -10,8 +12,20 @@ namespace GFramework.Core.Command;
/// 表示一个命令执行器,用于执行命令操作。
/// 该类实现了 ICommandExecutor 接口,提供命令执行的核心功能。
/// </summary>
public sealed class CommandExecutor : ICommandExecutor
public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExecutor
{
private readonly ICqrsRuntime? _runtime = runtime;
/// <summary>
/// 获取当前执行器是否已接入统一 CQRS runtime。
/// </summary>
/// <remarks>
/// 当调用方只是直接 new 一个执行器做纯单元测试时,这里允许为空,并回退到 legacy 直接执行路径;
/// 当执行器由架构容器提供给 <see cref="Architectures.ArchitectureContext" /> 使用时,应始终传入 runtime
/// 以便旧入口也复用统一 pipeline 与 handler 调度链路。
/// </remarks>
public bool UsesCqrsRuntime => _runtime is not null;
/// <summary>
/// 发送并执行无返回值的命令
/// </summary>
@ -21,6 +35,11 @@ public sealed class CommandExecutor : ICommandExecutor
{
ArgumentNullException.ThrowIfNull(command);
if (TryExecuteThroughCqrsRuntime(command, static currentCommand => new LegacyCommandDispatchRequest(currentCommand)))
{
return;
}
command.Execute();
}
@ -35,6 +54,16 @@ public sealed class CommandExecutor : ICommandExecutor
{
ArgumentNullException.ThrowIfNull(command);
if (TryExecuteThroughCqrsRuntime(
command,
static currentCommand => new LegacyCommandResultDispatchRequest(
currentCommand,
() => currentCommand.Execute()),
out TResult? result))
{
return result!;
}
return command.Execute();
}
@ -47,6 +76,13 @@ public sealed class CommandExecutor : ICommandExecutor
{
ArgumentNullException.ThrowIfNull(command);
var cqrsRuntime = _runtime;
if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, command, out var context))
{
return cqrsRuntime.SendAsync(context, new LegacyAsyncCommandDispatchRequest(command)).AsTask();
}
return command.ExecuteAsync();
}
@ -61,6 +97,90 @@ public sealed class CommandExecutor : ICommandExecutor
{
ArgumentNullException.ThrowIfNull(command);
var cqrsRuntime = _runtime;
if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, command, out var context))
{
return BridgeAsyncCommandWithResultAsync(cqrsRuntime, context, command);
}
return command.ExecuteAsync();
}
}
/// <summary>
/// 尝试通过统一 CQRS runtime 执行当前 legacy 请求。
/// </summary>
/// <typeparam name="TTarget">legacy 目标对象类型。</typeparam>
/// <typeparam name="TRequest">bridge request 类型。</typeparam>
/// <param name="target">即将执行的 legacy 目标对象。</param>
/// <param name="requestFactory">用于创建 bridge request 的工厂。</param>
/// <returns>若成功切入 CQRS runtime 则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
private bool TryExecuteThroughCqrsRuntime<TTarget, TRequest>(
TTarget target,
Func<TTarget, TRequest> requestFactory)
where TTarget : class
where TRequest : IRequest<Unit>
{
var cqrsRuntime = _runtime;
if (!LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, target, out var context))
{
return false;
}
LegacyCqrsDispatchHelper.SendSynchronously(cqrsRuntime, context, requestFactory(target));
return true;
}
/// <summary>
/// 尝试通过统一 CQRS runtime 执行当前 legacy 请求,并返回装箱结果。
/// </summary>
/// <typeparam name="TTarget">legacy 目标对象类型。</typeparam>
/// <typeparam name="TResult">预期结果类型。</typeparam>
/// <typeparam name="TRequest">bridge request 类型。</typeparam>
/// <param name="target">即将执行的 legacy 目标对象。</param>
/// <param name="requestFactory">用于创建 bridge request 的工厂。</param>
/// <param name="result">若命中 bridge则返回执行结果否则返回默认值。</param>
/// <returns>若成功切入 CQRS runtime 则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
private bool TryExecuteThroughCqrsRuntime<TTarget, TResult, TRequest>(
TTarget target,
Func<TTarget, TRequest> requestFactory,
out TResult? result)
where TTarget : class
where TRequest : IRequest<object?>
{
var cqrsRuntime = _runtime;
if (!LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, target, out var context))
{
result = default;
return false;
}
var boxedResult = LegacyCqrsDispatchHelper.SendSynchronously(cqrsRuntime, context, requestFactory(target));
result = (TResult)boxedResult!;
return true;
}
/// <summary>
/// 通过统一 CQRS runtime 异步执行 legacy 带返回值命令,并把装箱结果还原为目标类型。
/// </summary>
/// <typeparam name="TResult">命令返回值类型。</typeparam>
/// <param name="runtime">负责调度当前 bridge request 的统一 CQRS runtime。</param>
/// <param name="context">当前架构上下文。</param>
/// <param name="command">要桥接的 legacy 命令。</param>
/// <returns>命令执行结果。</returns>
private static async Task<TResult> BridgeAsyncCommandWithResultAsync<TResult>(
ICqrsRuntime runtime,
GFramework.Core.Abstractions.Architectures.IArchitectureContext context,
IAsyncCommand<TResult> command)
{
var boxedResult = await runtime.SendAsync(
context,
new LegacyAsyncCommandResultDispatchRequest(
command,
async () => await command.ExecuteAsync().ConfigureAwait(false)))
.ConfigureAwait(false);
return (TResult)boxedResult!;
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using CoreCommand = GFramework.Core.Abstractions.Command;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 包装 legacy 异步无返回值命令,使其能够通过自有 CQRS runtime 调度。
/// </summary>
/// <param name="command">当前 bridge request 代理的 legacy 异步命令实例。</param>
internal sealed class LegacyAsyncCommandDispatchRequest(CoreCommand.IAsyncCommand command)
: LegacyCqrsDispatchRequestBase(ValidateCommand(command)), IRequest<Unit>
{
/// <summary>
/// 获取当前 bridge request 代理的异步命令实例。
/// </summary>
public CoreCommand.IAsyncCommand Command { get; } = command;
private static CoreCommand.IAsyncCommand ValidateCommand(CoreCommand.IAsyncCommand command)
{
return command ?? throw new ArgumentNullException(nameof(command));
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 处理 legacy 异步无返回值命令的 bridge handler。
/// </summary>
internal sealed class LegacyAsyncCommandDispatchRequestHandler
: LegacyCqrsDispatchHandlerBase, IRequestHandler<LegacyAsyncCommandDispatchRequest, Unit>
{
/// <inheritdoc />
public async ValueTask<Unit> Handle(
LegacyAsyncCommandDispatchRequest request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
// Legacy ExecuteAsync contract does not accept CancellationToken; use WaitAsync so the caller can still observe cancellation promptly.
cancellationToken.ThrowIfCancellationRequested();
PrepareTarget(request.Command);
await request.Command.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
return Unit.Value;
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 包装 legacy 异步带返回值命令,使其能够通过自有 CQRS runtime 调度。
/// </summary>
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 命令目标实例。</param>
/// <param name="executeAsync">封装 legacy 异步命令执行逻辑并返回装箱结果的委托。</param>
internal sealed class LegacyAsyncCommandResultDispatchRequest(object target, Func<Task<object?>> executeAsync)
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
{
private readonly Func<Task<object?>> _executeAsync = executeAsync ?? throw new ArgumentNullException(nameof(executeAsync));
/// <summary>
/// 异步执行底层 legacy 命令并返回装箱后的结果。
/// </summary>
/// <returns>表示异步执行结果的任务;任务结果为底层 legacy 命令返回的装箱值。</returns>
public Task<object?> ExecuteAsync() => _executeAsync();
}

View File

@ -0,0 +1,25 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 处理 legacy 异步带返回值命令的 bridge handler。
/// </summary>
internal sealed class LegacyAsyncCommandResultDispatchRequestHandler
: LegacyCqrsDispatchHandlerBase, IRequestHandler<LegacyAsyncCommandResultDispatchRequest, object?>
{
/// <inheritdoc />
public async ValueTask<object?> Handle(
LegacyAsyncCommandResultDispatchRequest request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
// Legacy ExecuteAsync contract does not accept CancellationToken; use WaitAsync so the caller can observe cancellation promptly.
cancellationToken.ThrowIfCancellationRequested();
PrepareTarget(request.Target);
return await request.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 包装 legacy 异步查询,使其能够通过自有 CQRS runtime 调度。
/// </summary>
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 查询目标实例。</param>
/// <param name="executeAsync">封装 legacy 异步查询执行逻辑并返回装箱结果的委托。</param>
internal sealed class LegacyAsyncQueryDispatchRequest(object target, Func<Task<object?>> executeAsync)
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
{
private readonly Func<Task<object?>> _executeAsync = executeAsync ?? throw new ArgumentNullException(nameof(executeAsync));
/// <summary>
/// 异步执行底层 legacy 查询并返回装箱后的结果。
/// </summary>
/// <returns>表示异步执行结果的任务;任务结果为底层 legacy 查询返回的装箱值。</returns>
public Task<object?> ExecuteAsync() => _executeAsync();
}

View File

@ -0,0 +1,25 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 处理 legacy 异步查询的 bridge handler。
/// </summary>
internal sealed class LegacyAsyncQueryDispatchRequestHandler
: LegacyCqrsDispatchHandlerBase, IRequestHandler<LegacyAsyncQueryDispatchRequest, object?>
{
/// <inheritdoc />
public async ValueTask<object?> Handle(
LegacyAsyncQueryDispatchRequest request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
// Legacy DoAsync contract does not accept CancellationToken; use WaitAsync so the caller can observe cancellation promptly.
cancellationToken.ThrowIfCancellationRequested();
PrepareTarget(request.Target);
return await request.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using CoreCommand = GFramework.Core.Abstractions.Command;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 包装 legacy 无返回值命令,使其能够通过自有 CQRS runtime 调度。
/// </summary>
/// <param name="command">当前 bridge request 代理的 legacy 命令实例。</param>
internal sealed class LegacyCommandDispatchRequest(CoreCommand.ICommand command)
: LegacyCqrsDispatchRequestBase(ValidateCommand(command)), IRequest<Unit>
{
/// <summary>
/// 获取当前 bridge request 代理的命令实例。
/// </summary>
public CoreCommand.ICommand Command { get; } = command;
private static CoreCommand.ICommand ValidateCommand(CoreCommand.ICommand command)
{
return command ?? throw new ArgumentNullException(nameof(command));
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 处理 legacy 无返回值命令的 bridge handler。
/// </summary>
internal sealed class LegacyCommandDispatchRequestHandler
: LegacyCqrsDispatchHandlerBase, IRequestHandler<LegacyCommandDispatchRequest, Unit>
{
/// <inheritdoc />
public ValueTask<Unit> Handle(LegacyCommandDispatchRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
PrepareTarget(request.Command);
request.Command.Execute();
return ValueTask.FromResult(Unit.Value);
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 包装 legacy 带返回值命令,使其能够通过自有 CQRS runtime 调度。
/// </summary>
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 命令目标实例。</param>
/// <param name="execute">封装 legacy 命令执行逻辑并返回装箱结果的委托。</param>
internal sealed class LegacyCommandResultDispatchRequest(object target, Func<object?> execute)
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
{
private readonly Func<object?> _execute = execute ?? throw new ArgumentNullException(nameof(execute));
/// <summary>
/// 执行底层 legacy 命令并返回装箱后的结果。
/// </summary>
/// <returns>底层 legacy 命令执行后的装箱结果;若命令语义无返回值则为 <see langword="null" />。</returns>
public object? Execute() => _execute();
}

View File

@ -0,0 +1,21 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 处理 legacy 带返回值命令的 bridge handler。
/// </summary>
internal sealed class LegacyCommandResultDispatchRequestHandler
: LegacyCqrsDispatchHandlerBase, IRequestHandler<LegacyCommandResultDispatchRequest, object?>
{
/// <inheritdoc />
public ValueTask<object?> Handle(LegacyCommandResultDispatchRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
PrepareTarget(request.Target);
return ValueTask.FromResult(request.Execute());
}
}

View File

@ -0,0 +1,33 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Rule;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 为 legacy Core CQRS bridge handler 提供共享的上下文注入辅助逻辑。
/// </summary>
internal abstract class LegacyCqrsDispatchHandlerBase : ContextAwareBase
{
/// <summary>
/// 在执行 legacy 命令或查询前,把当前架构上下文显式注入给支持 <see cref="IContextAware" /> 的目标对象。
/// </summary>
/// <param name="target">即将执行的 legacy 目标对象。</param>
/// <exception cref="ArgumentNullException"><paramref name="target" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">
/// 目标对象实现了 <see cref="IContextAware" />,但当前 handler 还没有可用的架构上下文。
/// </exception>
protected void PrepareTarget(object target)
{
ArgumentNullException.ThrowIfNull(target);
if (target is IContextAware contextAware)
{
var context = Context ?? throw new InvalidOperationException(
"Legacy CQRS bridge handler requires an active architecture context before executing a context-aware target.");
contextAware.SetContext(context);
}
}
}

View File

@ -0,0 +1,116 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Rule;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 为 legacy Core CQRS bridge 提供共享的上下文解析与同步兼容辅助逻辑。
/// </summary>
/// <remarks>
/// 旧的同步 Command/Query 入口仍需要阻塞等待统一 <see cref="ICqrsRuntime" /> 返回结果。
/// 这里统一通过 <see cref="Task.Run(System.Func{System.Threading.Tasks.Task})" /> 把等待动作切换到线程池,
/// 避免直接占用调用方的 <see cref="SynchronizationContext" /> 导致 legacy 同步入口与异步 pipeline 互相卡死。
/// </remarks>
internal static class LegacyCqrsDispatchHelper
{
/// <summary>
/// 解析当前 legacy 目标对象是否能够绑定到统一 CQRS runtime 的架构上下文。
/// </summary>
/// <param name="runtime">当前执行器可用的统一 CQRS runtime。</param>
/// <param name="target">即将执行的 legacy 目标对象。</param>
/// <param name="context">命中时返回可用于 CQRS runtime 的架构上下文。</param>
/// <returns>
/// 当 <paramref name="runtime" /> 可用且 <paramref name="target" /> 能稳定提供
/// <see cref="IArchitectureContext" /> 时返回 <see langword="true" />;否则返回 <see langword="false" />。
/// </returns>
internal static bool TryResolveDispatchContext(
[NotNullWhen(true)] ICqrsRuntime? runtime,
object target,
out IArchitectureContext context)
{
ArgumentNullException.ThrowIfNull(target);
context = null!;
if (runtime is null || target is not IContextAware contextAware)
{
return false;
}
try
{
context = contextAware.GetContext();
return true;
}
catch (InvalidOperationException exception) when (IsMissingContextException(exception))
{
return false;
}
}
/// <summary>
/// 判断当前 <see cref="InvalidOperationException" /> 是否表示 legacy 目标尚未具备可桥接的架构上下文。
/// </summary>
/// <param name="exception">由 <see cref="IContextAware.GetContext" /> 抛出的异常。</param>
/// <returns>
/// 仅当异常明确表示“上下文尚未设置”或“当前没有活动上下文”时返回 <see langword="true" />
/// 其他运行时错误必须继续向上传播,避免把真实故障误判为可安全回退。
/// </returns>
private static bool IsMissingContextException(InvalidOperationException exception)
{
ArgumentNullException.ThrowIfNull(exception);
return string.Equals(
exception.Message,
"Architecture context has not been set. Call SetContext before accessing the context.",
StringComparison.Ordinal)
|| string.Equals(
exception.Message,
"No active architecture context is currently bound.",
StringComparison.Ordinal);
}
/// <summary>
/// 同步等待统一 CQRS runtime 完成无返回值请求。
/// </summary>
/// <param name="runtime">负责分发当前请求的统一 CQRS runtime。</param>
/// <param name="context">当前架构上下文。</param>
/// <param name="request">要同步等待的请求。</param>
internal static void SendSynchronously(
ICqrsRuntime runtime,
IArchitectureContext context,
IRequest<Unit> request)
{
ArgumentNullException.ThrowIfNull(runtime);
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(request);
Task.Run(() => runtime.SendAsync(context, request).AsTask()).GetAwaiter().GetResult();
}
/// <summary>
/// 同步等待统一 CQRS runtime 完成带返回值请求,并返回实际响应。
/// </summary>
/// <typeparam name="TResponse">请求响应类型。</typeparam>
/// <param name="runtime">负责分发当前请求的统一 CQRS runtime。</param>
/// <param name="context">当前架构上下文。</param>
/// <param name="request">要同步等待的请求。</param>
/// <returns>统一 CQRS runtime 返回的响应结果。</returns>
internal static TResponse SendSynchronously<TResponse>(
ICqrsRuntime runtime,
IArchitectureContext context,
IRequest<TResponse> request)
{
ArgumentNullException.ThrowIfNull(runtime);
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(request);
return Task.Run(() => runtime.SendAsync(context, request).AsTask()).GetAwaiter().GetResult();
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
namespace GFramework.Core.Cqrs;
/// <summary>
/// 为 legacy Command / Query 到自有 CQRS runtime 的桥接请求提供共享的目标对象封装。
/// </summary>
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 目标对象。</param>
internal abstract class LegacyCqrsDispatchRequestBase(object target)
{
/// <summary>
/// 获取当前 bridge request 代理的 legacy 目标对象。
/// </summary>
public object Target { get; } = target ?? throw new ArgumentNullException(nameof(target));
}

View File

@ -0,0 +1,23 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 包装 legacy 同步查询,使其能够通过自有 CQRS runtime 调度。
/// </summary>
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 查询目标实例。</param>
/// <param name="execute">封装 legacy 查询执行逻辑并返回装箱结果的委托。</param>
internal sealed class LegacyQueryDispatchRequest(object target, Func<object?> execute)
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
{
private readonly Func<object?> _execute = execute ?? throw new ArgumentNullException(nameof(execute));
/// <summary>
/// 执行底层 legacy 查询并返回装箱后的结果。
/// </summary>
/// <returns>底层 legacy 查询执行后的装箱结果;若查询无返回值则为 <see langword="null" />。</returns>
public object? Execute() => _execute();
}

View File

@ -0,0 +1,21 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 处理 legacy 同步查询的 bridge handler。
/// </summary>
internal sealed class LegacyQueryDispatchRequestHandler
: LegacyCqrsDispatchHandlerBase, IRequestHandler<LegacyQueryDispatchRequest, object?>
{
/// <inheritdoc />
public ValueTask<object?> Handle(LegacyQueryDispatchRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
PrepareTarget(request.Target);
return ValueTask.FromResult(request.Execute());
}
}

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
using System.Reflection;
using System.Threading;
using GFramework.Core.Abstractions.Bases;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Logging;
@ -17,11 +18,143 @@ namespace GFramework.Core.Ioc;
/// 将 Microsoft DI 包装为 IIocContainer 接口实现
/// 提供线程安全的依赖注入容器功能
/// </summary>
/// <remarks>
/// 该适配器负责维护服务注册表、冻结后的根 <see cref="IServiceProvider" /> 以及并发访问控制。
/// 容器释放后会阻止任何进一步访问,并统一抛出 <see cref="ObjectDisposedException" />
/// 以避免 benchmark、测试宿主或短生命周期架构误用失效的 DI 状态。
/// </remarks>
/// <param name="serviceCollection">可选的IServiceCollection实例默认创建新的ServiceCollection</param>
public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) : ContextAwareBase, IIocContainer
{
#region Helper Methods
/// <summary>
/// 抛出统一的容器释放异常,避免并发路径泄露底层锁类型的实现细节。
/// </summary>
/// <exception cref="ObjectDisposedException">始终抛出,且对象名固定为当前容器类型。</exception>
private void ThrowDisposedException()
{
const string objectName = nameof(MicrosoftDiContainer);
_logger.Warn("Attempted to use a disposed MicrosoftDiContainer.");
throw new ObjectDisposedException(objectName);
}
/// <summary>
/// 检查容器是否已释放,避免访问已经失效的服务提供者与同步原语。
/// </summary>
/// <exception cref="ObjectDisposedException">当容器已释放时抛出。</exception>
private void ThrowIfDisposed()
{
if (!_disposed) return;
ThrowDisposedException();
}
/// <summary>
/// 进入读锁,并在获取锁前后都复核释放状态,确保等待中的线程也能稳定得到容器级异常。
/// </summary>
/// <exception cref="ObjectDisposedException">当容器已释放,或等待期间被其他线程释放时抛出。</exception>
private void EnterReadLockOrThrowDisposed()
{
var lockTaken = false;
try
{
_lock.EnterReadLock();
lockTaken = true;
}
catch (ObjectDisposedException) when (_disposed)
{
ThrowDisposedException();
}
if (!_disposed)
{
return;
}
if (lockTaken)
{
_lock.ExitReadLock();
}
ThrowDisposedException();
}
/// <summary>
/// 进入写锁,并在获取锁前后都复核释放状态,确保等待中的线程不会泄露底层锁异常。
/// </summary>
/// <exception cref="ObjectDisposedException">当容器已释放,或等待期间被其他线程释放时抛出。</exception>
private void EnterWriteLockOrThrowDisposed()
{
var lockTaken = false;
try
{
_lock.EnterWriteLock();
lockTaken = true;
}
catch (ObjectDisposedException) when (_disposed)
{
ThrowDisposedException();
}
if (!_disposed)
{
return;
}
if (lockTaken)
{
_lock.ExitWriteLock();
}
ThrowDisposedException();
}
/// <summary>
/// 在释放标志已经对外可见后,等待遗留 waiter 退场,再尝试释放底层锁。
/// </summary>
/// <remarks>
/// 容器会先把 <see cref="_disposed" /> 置为 <see langword="true" /> 并退出写锁,
/// 这样所有已在等待队列中的线程都能醒来并通过统一路径抛出容器级
/// <see cref="ObjectDisposedException" />。只有当这些线程退场后,底层锁才可安全释放。
/// 该步骤只允许一个释放调用者执行,避免并发 <see cref="Dispose" /> 重复销毁同一个
/// <see cref="ReaderWriterLockSlim" /> 并破坏幂等契约。
/// </remarks>
private void DisposeLockWhenQuiescent()
{
if (Interlocked.CompareExchange(ref _lockDisposalStarted, 1, 0) != 0)
{
return;
}
const int maxDisposeSpinAttempts = 512;
var spinWait = new SpinWait();
for (var attempt = 0; attempt < maxDisposeSpinAttempts; attempt++)
{
if (_lock.CurrentReadCount == 0 &&
_lock.WaitingReadCount == 0 &&
_lock.WaitingWriteCount == 0 &&
_lock.WaitingUpgradeCount == 0)
{
try
{
_lock.Dispose();
return;
}
catch (SynchronizationLockException)
{
// 等待中的线程刚好在本轮检查后切换状态;继续自旋直到锁真正静默。
}
}
spinWait.SpinOnce();
}
_logger.Warn("MicrosoftDiContainer lock disposal was skipped because waiters did not quiesce in time.");
}
/// <summary>
/// 检查容器是否已冻结,如果已冻结则抛出异常
/// 用于保护注册操作的安全性
@ -52,11 +185,27 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// </summary>
private IServiceProvider? _provider;
/// <summary>
/// 冻结后可复用的服务类型可见性索引。
/// 容器冻结后注册集合不再变化,因此 <see cref="HasRegistration(Type)" /> 可以安全复用该索引。
/// </summary>
private FrozenServiceTypeIndex? _frozenServiceTypeIndex;
/// <summary>
/// 容器冻结状态标志true表示容器已冻结不可修改
/// </summary>
private volatile bool _frozen;
/// <summary>
/// 容器释放状态标志true 表示容器已释放,不允许继续访问。
/// </summary>
private volatile bool _disposed;
/// <summary>
/// 标记底层读写锁的销毁流程是否已经启动,确保并发释放时最多只有一个线程尝试销毁锁实例。
/// </summary>
private int _lockDisposalStarted;
/// <summary>
/// 读写锁,确保多线程环境下的线程安全操作
/// </summary>
@ -85,8 +234,9 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <exception cref="InvalidOperationException">当容器已冻结或类型已被注册时抛出</exception>
public void RegisterSingleton<T>(T instance)
{
ThrowIfDisposed();
var type = typeof(T);
_lock.EnterWriteLock();
EnterWriteLockOrThrowDisposed();
try
{
ThrowIfFrozen();
@ -119,7 +269,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
where TImpl : class, TService
where TService : class
{
_lock.EnterWriteLock();
ThrowIfDisposed();
EnterWriteLockOrThrowDisposed();
try
{
ThrowIfFrozen();
@ -142,7 +293,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
where TImpl : class, TService
where TService : class
{
_lock.EnterWriteLock();
ThrowIfDisposed();
EnterWriteLockOrThrowDisposed();
try
{
ThrowIfFrozen();
@ -165,7 +317,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
where TImpl : class, TService
where TService : class
{
_lock.EnterWriteLock();
ThrowIfDisposed();
EnterWriteLockOrThrowDisposed();
try
{
ThrowIfFrozen();
@ -187,10 +340,11 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <exception cref="InvalidOperationException">当容器已冻结时抛出</exception>
public void RegisterPlurality(object instance)
{
ThrowIfDisposed();
var concreteType = instance.GetType();
var interfaces = concreteType.GetInterfaces();
_lock.EnterWriteLock();
EnterWriteLockOrThrowDisposed();
try
{
ThrowIfFrozen();
@ -219,7 +373,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// </summary>
public void RegisterPlurality<T>() where T : class
{
_lock.EnterWriteLock();
ThrowIfDisposed();
EnterWriteLockOrThrowDisposed();
try
{
ThrowIfFrozen();
@ -262,7 +417,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <exception cref="InvalidOperationException">当容器已冻结时抛出</exception>
public void Register<T>(T instance)
{
_lock.EnterWriteLock();
ThrowIfDisposed();
EnterWriteLockOrThrowDisposed();
try
{
ThrowIfFrozen();
@ -284,7 +440,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <exception cref="InvalidOperationException">当容器已冻结时抛出</exception>
public void Register(Type type, object instance)
{
_lock.EnterWriteLock();
ThrowIfDisposed();
EnterWriteLockOrThrowDisposed();
try
{
ThrowIfFrozen();
@ -307,7 +464,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
public void RegisterFactory<TService>(
Func<IServiceProvider, TService> factory) where TService : class
{
_lock.EnterWriteLock();
ThrowIfDisposed();
EnterWriteLockOrThrowDisposed();
try
{
ThrowIfFrozen();
@ -328,40 +486,16 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
public void RegisterCqrsPipelineBehavior<TBehavior>() where TBehavior : class
{
_lock.EnterWriteLock();
ThrowIfDisposed();
EnterWriteLockOrThrowDisposed();
try
{
ThrowIfFrozen();
var behaviorType = typeof(TBehavior);
if (behaviorType.IsGenericTypeDefinition)
{
GetServicesUnsafe.AddSingleton(typeof(IPipelineBehavior<,>), behaviorType);
}
else
{
var pipelineInterfaces = behaviorType
.GetInterfaces()
.Where(type => type.IsGenericType &&
type.GetGenericTypeDefinition() == typeof(IPipelineBehavior<,>))
.ToList();
if (pipelineInterfaces.Count == 0)
{
var errorMessage = $"{behaviorType.Name} does not implement IPipelineBehavior<,>";
_logger.Error(errorMessage);
throw new InvalidOperationException(errorMessage);
}
// 为每个已闭合的管道接口建立显式映射,支持针对特定请求/响应的专用行为。
foreach (var pipelineInterface in pipelineInterfaces)
{
GetServicesUnsafe.AddSingleton(pipelineInterface, behaviorType);
}
}
_logger.Debug($"CQRS pipeline behavior registered: {behaviorType.Name}");
RegisterCqrsPipelineBehaviorCore(
typeof(TBehavior),
typeof(IPipelineBehavior<,>),
"IPipelineBehavior<,>",
"pipeline behavior");
}
finally
{
@ -369,6 +503,75 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
}
/// <summary>
/// 注册 CQRS 流式请求管道行为。
/// 同时支持开放泛型行为类型和已闭合的具体行为类型,
/// 以兼容通用行为和针对单一流式请求的专用行为两种注册方式。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
public void RegisterCqrsStreamPipelineBehavior<TBehavior>() where TBehavior : class
{
ThrowIfDisposed();
EnterWriteLockOrThrowDisposed();
try
{
ThrowIfFrozen();
RegisterCqrsPipelineBehaviorCore(
typeof(TBehavior),
typeof(IStreamPipelineBehavior<,>),
"IStreamPipelineBehavior<,>",
"stream pipeline behavior");
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// 复用 CQRS 行为注册的开放泛型/封闭接口校验逻辑,
/// 让 request 与 stream 两条入口保持一致的容器注册语义。
/// </summary>
/// <param name="behaviorType">待注册的行为运行时类型。</param>
/// <param name="openGenericInterfaceType">行为必须实现的开放泛型接口类型。</param>
/// <param name="interfaceTypeDisplayName">用于日志与异常的接口显示名称。</param>
/// <param name="registrationLabel">用于日志的注册类别名称。</param>
/// <exception cref="InvalidOperationException"><paramref name="behaviorType" /> 未实现目标行为接口。</exception>
private void RegisterCqrsPipelineBehaviorCore(
Type behaviorType,
Type openGenericInterfaceType,
string interfaceTypeDisplayName,
string registrationLabel)
{
if (behaviorType.IsGenericTypeDefinition)
{
GetServicesUnsafe.AddSingleton(openGenericInterfaceType, behaviorType);
}
else
{
var pipelineInterfaces = behaviorType
.GetInterfaces()
.Where(type => type.IsGenericType &&
type.GetGenericTypeDefinition() == openGenericInterfaceType)
.ToList();
if (pipelineInterfaces.Count == 0)
{
var errorMessage = $"{behaviorType.Name} does not implement {interfaceTypeDisplayName}";
_logger.Error(errorMessage);
throw new InvalidOperationException(errorMessage);
}
// 为每个已闭合的行为接口建立显式映射,支持针对特定请求/响应对的专用行为。
foreach (var pipelineInterface in pipelineInterfaces)
{
GetServicesUnsafe.AddSingleton(pipelineInterface, behaviorType);
}
}
_logger.Debug($"CQRS {registrationLabel} registered: {behaviorType.Name}");
}
/// <summary>
/// 从指定程序集显式注册 CQRS 处理器。
/// </summary>
@ -392,6 +595,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
{
ArgumentNullException.ThrowIfNull(assemblies);
ThrowIfDisposed();
var assemblyArray = assemblies.ToArray();
foreach (var assembly in assemblyArray)
{
@ -401,7 +605,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
}
_lock.EnterWriteLock();
EnterWriteLockOrThrowDisposed();
try
{
ThrowIfFrozen();
@ -419,7 +623,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <param name="configurator">服务配置委托</param>
public void ExecuteServicesHook(Action<IServiceCollection>? configurator = null)
{
_lock.EnterWriteLock();
ThrowIfDisposed();
EnterWriteLockOrThrowDisposed();
try
{
ThrowIfFrozen();
@ -444,14 +649,17 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <exception cref="InvalidOperationException">未找到可用的 CQRS 程序集注册协调器实例时抛出。</exception>
private ICqrsRegistrationService ResolveCqrsRegistrationService()
{
var descriptor = GetServicesUnsafe.LastOrDefault(static service =>
service.ServiceType == typeof(ICqrsRegistrationService));
var registrationService = CollectRegisteredImplementationInstances(typeof(ICqrsRegistrationService))
.OfType<ICqrsRegistrationService>()
.LastOrDefault();
if (descriptor?.ImplementationInstance is ICqrsRegistrationService registrationService)
if (registrationService != null)
return registrationService;
const string errorMessage =
"ICqrsRegistrationService not registered. Ensure the CQRS runtime module has been installed before registering handlers.";
"ICqrsRegistrationService is not visible during the registration stage. Ensure the CQRS runtime module " +
"has been installed and that the registration service is pre-materialized as an instance binding before " +
"registering handlers.";
_logger.Error(errorMessage);
throw new InvalidOperationException(errorMessage);
}
@ -464,23 +672,13 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <returns>服务实例或null</returns>
public T? Get<T>() where T : class
{
_lock.EnterReadLock();
ThrowIfDisposed();
EnterReadLockOrThrowDisposed();
try
{
if (_provider == null)
{
// 如果容器未冻结,从服务集合中查找已注册的实例
var serviceType = typeof(T);
var descriptor = GetServicesUnsafe.FirstOrDefault(s =>
s.ServiceType == serviceType || serviceType.IsAssignableFrom(s.ServiceType));
if (descriptor?.ImplementationInstance is T instance)
{
return instance;
}
// 在未冻结状态下无法调用工厂方法或创建实例返回null
return null;
return CollectRegisteredImplementationInstances(typeof(T)).OfType<T>().FirstOrDefault();
}
var result = _provider!.GetService<T>();
@ -503,23 +701,23 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <returns>服务实例或null</returns>
public object? Get(Type type)
{
_lock.EnterReadLock();
ArgumentNullException.ThrowIfNull(type);
ThrowIfDisposed();
EnterReadLockOrThrowDisposed();
try
{
if (_provider == null)
{
// 如果容器未冻结,从服务集合中查找已注册的实例
var descriptor =
GetServicesUnsafe.FirstOrDefault(s =>
s.ServiceType == type || type.IsAssignableFrom(s.ServiceType));
return descriptor?.ImplementationInstance;
return CollectRegisteredImplementationInstances(type).FirstOrDefault();
}
var result = _provider!.GetService(type);
_logger.Debug(result != null
? $"Retrieved instance: {type.Name}"
: $"No instance found for type: {type.Name}");
if (_logger.IsDebugEnabled())
{
_logger.Debug(result != null
? $"Retrieved instance: {type.Name}"
: $"No instance found for type: {type.Name}");
}
return result;
}
finally
@ -593,7 +791,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <returns>只读的服务实例列表</returns>
public IReadOnlyList<T> GetAll<T>() where T : class
{
_lock.EnterReadLock();
ThrowIfDisposed();
EnterReadLockOrThrowDisposed();
try
{
if (_provider == null)
@ -602,7 +801,10 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
var services = _provider!.GetServices<T>().ToList();
_logger.Debug($"Retrieved {services.Count} instances of {typeof(T).Name}");
if (_logger.IsDebugEnabled())
{
_logger.Debug($"Retrieved {services.Count} instances of {typeof(T).Name}");
}
return services;
}
finally
@ -620,8 +822,9 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
public IReadOnlyList<object> GetAll(Type type)
{
ArgumentNullException.ThrowIfNull(type);
ThrowIfDisposed();
_lock.EnterReadLock();
EnterReadLockOrThrowDisposed();
try
{
if (_provider == null)
@ -630,7 +833,10 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
var services = _provider!.GetServices(type).ToList();
_logger.Debug($"Retrieved {services.Count} instances of {type.Name}");
if (_logger.IsDebugEnabled())
{
_logger.Debug($"Retrieved {services.Count} instances of {type.Name}");
}
return services.Where(o => o != null).Cast<object>().ToList();
}
finally
@ -750,6 +956,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <returns>排序后的只读服务实例列表</returns>
public IReadOnlyList<T> GetAllSorted<T>(Comparison<T> comparison) where T : class
{
ThrowIfDisposed();
var list = GetAll<T>().ToList();
list.Sort(comparison);
return list;
@ -816,7 +1023,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <returns>true表示包含该类型实例false表示不包含</returns>
public bool Contains<T>() where T : class
{
_lock.EnterReadLock();
ThrowIfDisposed();
EnterReadLockOrThrowDisposed();
try
{
if (_provider == null)
@ -830,6 +1038,31 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
}
/// <summary>
/// 检查容器中是否存在可赋值给指定服务类型的注册项,而不要求先解析实例。
/// </summary>
/// <param name="type">要检查的服务类型。</param>
/// <returns>若存在显式注册或开放泛型映射可满足该服务类型,则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public bool HasRegistration(Type type)
{
ArgumentNullException.ThrowIfNull(type);
ThrowIfDisposed();
EnterReadLockOrThrowDisposed();
try
{
if (_frozenServiceTypeIndex is not null)
{
return _frozenServiceTypeIndex.Contains(type);
}
return HasRegistrationCore(type);
}
finally
{
_lock.ExitReadLock();
}
}
/// <summary>
/// 判断容器中是否包含某个具体的实例对象
/// 通过已注册实例集合进行快速查找
@ -838,7 +1071,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <returns>true表示包含该实例false表示不包含</returns>
public bool ContainsInstance(object instance)
{
_lock.EnterReadLock();
ThrowIfDisposed();
EnterReadLockOrThrowDisposed();
try
{
return _registeredInstances.Contains(instance);
@ -849,13 +1083,60 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
}
/// <summary>
/// 在当前容器状态下检查指定服务类型是否存在可见注册。
/// </summary>
/// <param name="requestedType">要检查的服务类型。</param>
/// <returns>存在可满足该类型的注册时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
/// <remarks>
/// 该检查只回答“是否可能解析到服务”,不会为了判断结果而激活实例。
/// 预冻结阶段只基于当前服务描述符推断;冻结后则同样只观察描述符,
/// 避免把瞬态/多实例解析成本混入热路径中的存在性判断。
/// </remarks>
private bool HasRegistrationCore(Type requestedType)
{
foreach (var descriptor in GetServicesUnsafe)
{
if (CanSatisfyServiceType(descriptor.ServiceType, requestedType))
{
return true;
}
}
return false;
}
/// <summary>
/// 判断某个服务描述符声明的服务类型是否能满足当前请求类型。
/// </summary>
/// <param name="registeredServiceType">注册时声明的服务类型。</param>
/// <param name="requestedType">调用方请求的服务类型。</param>
/// <returns>若当前注册可用于解析 <paramref name="requestedType" />,则返回 <see langword="true" />。</returns>
private static bool CanSatisfyServiceType(Type registeredServiceType, Type requestedType)
{
// 这里刻意与 Get/GetAll 的“按服务键解析”语义保持一致:
// 只有注册时声明的服务类型本身命中,或开放泛型服务键能闭合到请求类型时,才视为存在可见注册。
if (registeredServiceType == requestedType)
{
return true;
}
if (requestedType.IsConstructedGenericType && registeredServiceType.IsGenericTypeDefinition)
{
return requestedType.GetGenericTypeDefinition() == registeredServiceType;
}
return false;
}
/// <summary>
/// 清空容器中的所有实例和服务注册
/// 只有在容器未冻结状态下才能执行清空操作
/// </summary>
public void Clear()
{
_lock.EnterWriteLock();
ThrowIfDisposed();
EnterWriteLockOrThrowDisposed();
try
{
// 冻结的容器不允许清空操作
@ -865,9 +1146,12 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
return;
}
// 未冻结的容器不会构建根 ServiceProvider因此这里仅重置注册状态即可。
GetServicesUnsafe.Clear();
_registeredInstances.Clear();
_provider = null;
_frozenServiceTypeIndex = null;
_frozen = false;
_logger.Info("Container cleared");
}
finally
@ -882,7 +1166,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// </summary>
public void Freeze()
{
_lock.EnterWriteLock();
ThrowIfDisposed();
EnterWriteLockOrThrowDisposed();
try
{
// 防止重复冻结
@ -893,6 +1178,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
_provider = GetServicesUnsafe.BuildServiceProvider();
_frozenServiceTypeIndex = FrozenServiceTypeIndex.Create(GetServicesUnsafe);
_frozen = true;
_logger.Info("IOC Container frozen - ServiceProvider built");
}
@ -902,6 +1188,59 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
}
/// <summary>
/// 保存冻结后按服务键可见的精确服务类型与开放泛型定义集合。
/// </summary>
/// <remarks>
/// 该索引只回答“按当前服务键语义是否可见”,因此与 <see cref="Get(Type)" /> /
/// <see cref="GetAll(Type)" /> 一样不会退化为更宽松的可赋值匹配。
/// </remarks>
private sealed class FrozenServiceTypeIndex(HashSet<Type> exactServiceTypes, HashSet<Type> openGenericServiceTypes)
{
private readonly HashSet<Type> _exactServiceTypes = exactServiceTypes;
private readonly HashSet<Type> _openGenericServiceTypes = openGenericServiceTypes;
/// <summary>
/// 基于冻结时最终确定的服务描述符集合创建索引。
/// </summary>
/// <param name="descriptors">冻结时的服务描述符序列。</param>
/// <returns>供存在性判断热路径复用的服务键索引。</returns>
public static FrozenServiceTypeIndex Create(IEnumerable<ServiceDescriptor> descriptors)
{
ArgumentNullException.ThrowIfNull(descriptors);
var exactServiceTypes = new HashSet<Type>();
var openGenericServiceTypes = new HashSet<Type>();
foreach (var descriptor in descriptors)
{
var serviceType = descriptor.ServiceType;
exactServiceTypes.Add(serviceType);
if (serviceType.IsGenericTypeDefinition)
{
openGenericServiceTypes.Add(serviceType);
}
}
return new FrozenServiceTypeIndex(exactServiceTypes, openGenericServiceTypes);
}
/// <summary>
/// 判断当前索引是否声明了目标服务键。
/// </summary>
/// <param name="requestedType">要检查的服务类型。</param>
/// <returns>命中精确服务键或可闭合的开放泛型服务键时返回 <see langword="true" />。</returns>
public bool Contains(Type requestedType)
{
ArgumentNullException.ThrowIfNull(requestedType);
return _exactServiceTypes.Contains(requestedType) ||
requestedType.IsConstructedGenericType &&
_openGenericServiceTypes.Contains(requestedType.GetGenericTypeDefinition());
}
}
/// <summary>
/// 获取底层的服务集合
/// 提供对内部IServiceCollection的访问权限用于高级配置和自定义操作
@ -917,7 +1256,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <exception cref="InvalidOperationException">当容器未冻结时抛出</exception>
public IServiceScope CreateScope()
{
_lock.EnterReadLock();
ThrowIfDisposed();
EnterReadLockOrThrowDisposed();
try
{
// 在锁内检查,避免竞态条件
@ -938,5 +1278,59 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
}
/// <summary>
/// 释放容器持有的服务提供者、注册状态和同步原语。
/// </summary>
/// <remarks>
/// 冻结后的根 <see cref="IServiceProvider" /> 会拥有 DI 创建的单例与作用域根缓存,因此 benchmark、
/// 测试宿主或短生命周期架构在结束时需要显式释放容器,避免这些对象与内部
/// <see cref="ReaderWriterLockSlim" /> 一起滞留。
/// 释放是幂等的;首次释放后所有后续访问都会抛出 <see cref="ObjectDisposedException" />。
/// </remarks>
public void Dispose()
{
if (_disposed)
{
return;
}
var lockTaken = false;
try
{
try
{
_lock.EnterWriteLock();
lockTaken = true;
}
catch (ObjectDisposedException) when (_disposed)
{
return;
}
if (_disposed)
{
return;
}
_disposed = true;
(_provider as IDisposable)?.Dispose();
_provider = null;
_frozenServiceTypeIndex = null;
GetServicesUnsafe.Clear();
_registeredInstances.Clear();
_frozen = false;
_logger.Info("IOC Container disposed");
}
finally
{
if (lockTaken)
{
_lock.ExitWriteLock();
DisposeLockWhenQuiescent();
}
}
}
#endregion
}

View File

@ -0,0 +1,6 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("GFramework.Core.Tests")]

View File

@ -2,14 +2,23 @@
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Query;
/// <summary>
/// 异步查询总线实现,用于处理异步查询请求
/// </summary>
public sealed class AsyncQueryExecutor : IAsyncQueryExecutor
public sealed class AsyncQueryExecutor(ICqrsRuntime? runtime = null) : IAsyncQueryExecutor
{
private readonly ICqrsRuntime? _runtime = runtime;
/// <summary>
/// 获取当前执行器是否已接入统一 CQRS runtime。
/// </summary>
public bool UsesCqrsRuntime => _runtime is not null;
/// <summary>
/// 异步发送查询请求并返回结果
/// </summary>
@ -18,8 +27,38 @@ public sealed class AsyncQueryExecutor : IAsyncQueryExecutor
/// <returns>包含查询结果的异步任务</returns>
public Task<TResult> SendAsync<TResult>(IAsyncQuery<TResult> query)
{
// 验证查询参数不为空
ArgumentNullException.ThrowIfNull(query);
var cqrsRuntime = _runtime;
if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, query, out var context))
{
return BridgeAsyncQueryAsync(cqrsRuntime, context, query);
}
return query.DoAsync();
}
}
/// <summary>
/// 通过统一 CQRS runtime 异步执行 legacy 查询,并把装箱结果还原为目标类型。
/// </summary>
/// <typeparam name="TResult">查询结果类型。</typeparam>
/// <param name="runtime">负责调度当前 bridge request 的统一 CQRS runtime。</param>
/// <param name="context">当前架构上下文。</param>
/// <param name="query">要桥接的 legacy 查询。</param>
/// <returns>查询执行结果。</returns>
private static async Task<TResult> BridgeAsyncQueryAsync<TResult>(
ICqrsRuntime runtime,
GFramework.Core.Abstractions.Architectures.IArchitectureContext context,
IAsyncQuery<TResult> query)
{
var boxedResult = await runtime.SendAsync(
context,
new LegacyAsyncQueryDispatchRequest(
query,
async () => await query.DoAsync().ConfigureAwait(false)))
.ConfigureAwait(false);
return (TResult)boxedResult!;
}
}

View File

@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Query;
@ -10,21 +12,47 @@ namespace GFramework.Core.Query;
/// QueryExecutor 类负责执行查询操作,实现 IQueryExecutor 接口。
/// 该类是密封的,防止被继承。
/// </summary>
public sealed class QueryExecutor : IQueryExecutor
public sealed class QueryExecutor(ICqrsRuntime? runtime = null) : IQueryExecutor
{
private readonly ICqrsRuntime? _runtime = runtime;
/// <summary>
/// 获取当前执行器是否已接入统一 CQRS runtime。
/// </summary>
public bool UsesCqrsRuntime => _runtime is not null;
/// <summary>
/// 执行指定的查询并返回结果。
/// 该方法通过调用查询对象的 Do 方法来获取结果。
/// 当查询对象携带可用的架构上下文且执行器已接入统一 runtime 时,
/// 该方法会先把 legacy 查询包装成内部 request 并交给 <see cref="ICqrsRuntime" />
/// 以复用统一的 dispatch / pipeline 入口;否则回退到 legacy 直接执行。
/// </summary>
/// <typeparam name="TResult">查询结果的类型。</typeparam>
/// <param name="query">要执行的查询对象,必须实现 IQuery&lt;TResult&gt; 接口。</param>
/// <returns>查询执行的结果,类型为 TResult。</returns>
/// <returns>查询执行成功后还原出的 <typeparamref name="TResult" /> 结果。</returns>
/// <exception cref="NullReferenceException">
/// 统一 CQRS runtime 返回 <see langword="null" />,但 <typeparamref name="TResult" /> 为值类型。
/// </exception>
/// <exception cref="InvalidCastException">
/// 统一 CQRS runtime 返回的装箱结果无法转换为 <typeparamref name="TResult" />。
/// </exception>
public TResult Send<TResult>(IQuery<TResult> query)
{
// 验证查询参数不为 null如果为 null 则抛出 ArgumentNullException 异常
ArgumentNullException.ThrowIfNull(query);
// 调用查询对象的 Do 方法执行查询并返回结果
var cqrsRuntime = _runtime;
if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, query, out var context))
{
var boxedResult = LegacyCqrsDispatchHelper.SendSynchronously(
cqrsRuntime,
context,
new LegacyQueryDispatchRequest(
query,
() => query.Do()));
return (TResult)boxedResult!;
}
return query.Do();
}
}

View File

@ -15,6 +15,9 @@
- 资源、对象池、日志、协程、并发、环境、配置与本地化
- 服务模块管理、时间提供器与默认的 IoC 容器适配
标准架构启动路径下,旧 `Command` / `Query` 兼容入口现在会继续保持原有使用方式,
但底层会通过 `GFramework.Cqrs` 的统一 runtime、pipeline 与上下文注入链路执行。
它不负责:
- 游戏内容配置、Scene / UI / Storage 等游戏层能力

View File

@ -46,8 +46,10 @@ public abstract class ContextAwareBase : IContextAware
/// </summary>
/// <returns>当前架构上下文对象。</returns>
/// <remarks>
/// 当 <see cref="Context" /> 为空时,该实现会直接回退到 <see cref="GameContext.GetFirstArchitectureContext" />
/// 当 <see cref="Context" /> 为空时,该实现会直接回退到 <see cref="GameContext.GetFirstArchitectureContext" /> 返回的当前活动上下文
/// 该回退过程不执行额外同步,也不支持替换 provider如需这些能力请改用生成的 ContextAware 实现。
/// 一旦回退结果被写入 <see cref="Context" />,后续即使关联架构解除 <see cref="GameContext" /> 绑定,
/// 该实例仍会保留原引用,调用方需要自行约束其生命周期或改用支持 provider 协调的生成实现。
/// </remarks>
IArchitectureContext IContextAware.GetContext()
{

View File

@ -4,6 +4,7 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Query;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Services.Modules;
@ -32,10 +33,19 @@ public sealed class AsyncQueryExecutorModule : IServiceModule
/// 注册异步查询执行器到依赖注入容器。
/// 创建异步查询执行器实例并将其注册为多例服务。
/// </summary>
/// <param name="container">依赖注入容器实例。</param>
/// <param name="container">承载异步查询执行器与 CQRS runtime 的依赖注入容器实例。</param>
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">
/// 容器中尚未注册唯一的 <see cref="ICqrsRuntime" /> 实例,无法构建统一 runtime 版本的异步查询执行器。
/// </exception>
/// <remarks>
/// 该模块会在注册阶段立即解析 <see cref="ICqrsRuntime" />,因此
/// <see cref="CqrsRuntimeModule" /> 必须先于当前模块完成注册。
/// </remarks>
public void Register(IIocContainer container)
{
container.RegisterPlurality(new AsyncQueryExecutor());
ArgumentNullException.ThrowIfNull(container);
container.RegisterPlurality(new AsyncQueryExecutor(container.GetRequired<ICqrsRuntime>()));
}
/// <summary>
@ -55,4 +65,4 @@ public sealed class AsyncQueryExecutorModule : IServiceModule
{
return ValueTask.CompletedTask;
}
}
}

View File

@ -4,6 +4,7 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Command;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Services.Modules;
@ -32,10 +33,19 @@ public sealed class CommandExecutorModule : IServiceModule
/// 注册命令执行器到依赖注入容器。
/// 创建命令执行器实例并将其注册为多例服务。
/// </summary>
/// <param name="container">依赖注入容器实例。</param>
/// <param name="container">承载命令执行器与 CQRS runtime 的依赖注入容器实例。</param>
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">
/// 容器中尚未注册唯一的 <see cref="ICqrsRuntime" /> 实例,无法构建统一 runtime 版本的命令执行器。
/// </exception>
/// <remarks>
/// 该模块会在注册阶段立即解析 <see cref="ICqrsRuntime" />,因此
/// <see cref="CqrsRuntimeModule" /> 必须先于当前模块完成注册。
/// </remarks>
public void Register(IIocContainer container)
{
container.RegisterPlurality(new CommandExecutor());
ArgumentNullException.ThrowIfNull(container);
container.RegisterPlurality(new CommandExecutor(container.GetRequired<ICqrsRuntime>()));
}
/// <summary>
@ -55,4 +65,4 @@ public sealed class CommandExecutorModule : IServiceModule
{
return ValueTask.CompletedTask;
}
}
}

View File

@ -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);

View File

@ -4,6 +4,7 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Query;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Services.Modules;
@ -32,10 +33,19 @@ public sealed class QueryExecutorModule : IServiceModule
/// 注册查询执行器到依赖注入容器。
/// 创建查询执行器实例并将其注册为多例服务。
/// </summary>
/// <param name="container">依赖注入容器实例。</param>
/// <param name="container">承载查询执行器与 CQRS runtime 的依赖注入容器实例。</param>
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">
/// 容器中尚未注册唯一的 <see cref="ICqrsRuntime" /> 实例,无法构建统一 runtime 版本的查询执行器。
/// </exception>
/// <remarks>
/// 该模块会在注册阶段立即解析 <see cref="ICqrsRuntime" />,因此
/// <see cref="CqrsRuntimeModule" /> 必须先于当前模块完成注册。
/// </remarks>
public void Register(IIocContainer container)
{
container.RegisterPlurality(new QueryExecutor());
ArgumentNullException.ThrowIfNull(container);
container.RegisterPlurality(new QueryExecutor(container.GetRequired<ICqrsRuntime>()));
}
/// <summary>
@ -55,4 +65,4 @@ public sealed class QueryExecutorModule : IServiceModule
{
return ValueTask.CompletedTask;
}
}
}

View File

@ -28,6 +28,9 @@ public interface ICqrsRuntime
/// <remarks>
/// 该契约允许调用方传入任意 <see cref="ICqrsContext" />
/// 但默认运行时在需要向处理器或行为注入框架上下文时,仍要求该上下文同时实现 <c>IArchitectureContext</c>。
/// 为了兼容 legacy 同步入口,<c>ArchitectureContext</c>、<c>QueryExecutor</c> 与 <c>CommandExecutor</c>
/// 可能会在后台线程上同步等待该异步结果;实现者与 pipeline 行为不应依赖调用方的
/// <see cref="SynchronizationContext" />,并应优先在内部异步链路上使用 <c>ConfigureAwait(false)</c>。
/// </remarks>
ValueTask<TResponse> SendAsync<TResponse>(
ICqrsContext context,

View 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);
}

View File

@ -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>;

View File

@ -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`
## 最小接入路径

View File

@ -0,0 +1,71 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;
using System;
namespace GFramework.Cqrs.Benchmarks;
/// <summary>
/// 为 CQRS benchmark 结果补充可读的场景标签列。
/// </summary>
/// <param name="columnName">列名。</param>
/// <param name="getValue">从 benchmark case 提取列值的委托。</param>
public sealed class CustomColumn(string columnName, Func<Summary, BenchmarkCase, string> getValue) : IColumn
{
/// <inheritdoc />
public string Id => $"{nameof(CustomColumn)}.{ColumnName}";
/// <inheritdoc />
public string ColumnName { get; } = columnName;
/// <inheritdoc />
public bool AlwaysShow => true;
/// <inheritdoc />
public ColumnCategory Category => ColumnCategory.Params;
/// <inheritdoc />
public int PriorityInCategory => 0;
/// <inheritdoc />
public bool IsNumeric => false;
/// <inheritdoc />
public UnitType UnitType => UnitType.Dimensionless;
/// <inheritdoc />
public string Legend => $"Custom '{ColumnName}' tag column";
/// <inheritdoc />
public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase)
{
return false;
}
/// <inheritdoc />
public bool IsAvailable(Summary summary)
{
return true;
}
/// <inheritdoc />
public string GetValue(Summary summary, BenchmarkCase benchmarkCase)
{
return getValue(summary, benchmarkCase);
}
/// <inheritdoc />
public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style)
{
return GetValue(summary, benchmarkCase);
}
/// <inheritdoc />
public override string ToString()
{
return ColumnName;
}
}

View File

@ -0,0 +1,38 @@
<!--
Copyright (c) 2025-2026 GeWuYou
SPDX-License-Identifier: Apache-2.0
-->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<!-- Keep benchmark infrastructure out of the published NuGet package set. -->
<IsPackable>false</IsPackable>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<PackageReference Include="Mediator.Abstractions" Version="3.0.2" />
<PackageReference Include="Mediator.SourceGenerator" Version="3.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="MediatR" Version="13.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.7" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GFramework.Cqrs.Abstractions\GFramework.Cqrs.Abstractions.csproj" />
<ProjectReference Include="..\GFramework.Cqrs\GFramework.Cqrs.csproj" />
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj" />
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,57 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using System;
using System.Collections.Generic;
using System.Runtime.ExceptionServices;
namespace GFramework.Cqrs.Benchmarks.Messaging;
/// <summary>
/// 统一处理 benchmark 宿主的资源释放,避免前一个 <see cref="IDisposable" /> 抛错后中断后续清理。
/// </summary>
internal static class BenchmarkCleanupHelper
{
/// <summary>
/// 按顺序释放一组 benchmark 资源,并在全部资源都尝试释放后再回抛异常。
/// </summary>
/// <param name="disposables">当前 benchmark 宿主拥有并负责释放的资源。</param>
/// <exception cref="Exception">
/// 当且仅当至少一个资源释放失败时抛出。
/// 单个失败会回抛原始异常,多个失败会聚合为 <see cref="AggregateException" />。
/// </exception>
public static void DisposeAll(params IDisposable?[] disposables)
{
List<Exception>? exceptions = null;
foreach (var disposable in disposables)
{
if (disposable is null)
{
continue;
}
try
{
disposable.Dispose();
}
catch (Exception exception)
{
exceptions ??= [];
exceptions.Add(exception);
}
}
if (exceptions is null)
{
return;
}
if (exceptions.Count == 1)
{
ExceptionDispatchInfo.Capture(exceptions[0]).Throw();
}
throw new AggregateException("One or more benchmark resources failed to dispose cleanly.", exceptions);
}
}

View File

@ -0,0 +1,17 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Benchmarks.Messaging;
/// <summary>
/// 为纯 runtime benchmark 提供最小 CQRS 上下文标记,避免把完整架构上下文初始化成本混入 steady-state dispatch。
/// </summary>
internal sealed class BenchmarkContext : ICqrsContext
{
/// <summary>
/// 共享的最小 CQRS 上下文实例。
/// </summary>
public static BenchmarkContext Instance { get; } = new();
}

View File

@ -0,0 +1,49 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using System;
using System.Reflection;
namespace GFramework.Cqrs.Benchmarks.Messaging;
/// <summary>
/// 提供 benchmark 共享的 dispatcher 静态缓存清理入口。
/// </summary>
/// <remarks>
/// `GFramework.Cqrs` runtime 会把反射绑定与 generated invoker 元数据缓存在静态字段中。
/// benchmark 需要在同一进程内重复比较 cold-start、reflection 与 generated 路径时,
/// 显式清空这些缓存,避免前一组 benchmark 污染后续结果。
/// </remarks>
internal static class BenchmarkDispatcherCacheHelper
{
/// <summary>
/// 清空 dispatcher 上与 benchmark 对照相关的全部静态缓存。
/// </summary>
public static void ClearDispatcherCaches()
{
ClearDispatcherCache("NotificationDispatchBindings");
ClearDispatcherCache("RequestDispatchBindings");
ClearDispatcherCache("StreamDispatchBindings");
ClearDispatcherCache("GeneratedRequestInvokers");
ClearDispatcherCache("GeneratedStreamInvokers");
}
/// <summary>
/// 通过反射定位并清空 dispatcher 的指定缓存字段。
/// </summary>
/// <param name="fieldName">要清理的静态缓存字段名。</param>
/// <exception cref="InvalidOperationException">指定缓存字段不存在、返回空值或未暴露清理方法。</exception>
internal static void ClearDispatcherCache(string fieldName)
{
var field = typeof(GFramework.Cqrs.CqrsRuntimeFactory).Assembly
.GetType("GFramework.Cqrs.Internal.CqrsDispatcher", throwOnError: true)!
.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException($"Missing dispatcher cache field {fieldName}.");
var cache = field.GetValue(null)
?? throw new InvalidOperationException($"Dispatcher cache field {fieldName} returned null.");
var clearMethod = cache.GetType().GetMethod("Clear", BindingFlags.Public | BindingFlags.Instance)
?? throw new InvalidOperationException(
$"Dispatcher cache field {fieldName} does not expose a Clear method.");
_ = clearMethod.Invoke(cache, null);
}
}

View File

@ -0,0 +1,194 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using System;
using System.Linq;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Ioc;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Internal;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime;
namespace GFramework.Cqrs.Benchmarks.Messaging;
/// <summary>
/// 为 benchmark 场景构建最小且可重复的 GFramework / MediatR 对照宿主。
/// </summary>
/// <remarks>
/// 基准工程里的对照目标是“相同消息合同下的调度差异”,而不是程序集扫描量或容器生命周期差异。
/// 因此这里统一封装两类宿主的最小注册形状,确保:
/// 1. GFramework 容器在首次发送前已经冻结,可真实解析按类型注册的 handler
/// 2. MediatR 只扫描当前 benchmark 明确拥有的 handler / behavior 类型,避免整个程序集的额外注册污染结果。
/// </remarks>
internal static class BenchmarkHostFactory
{
/// <summary>
/// 创建一个已经冻结的 GFramework benchmark 容器。
/// </summary>
/// <param name="configure">向容器写入 benchmark 所需 handler / pipeline 的注册动作。</param>
/// <returns>已冻结、可立即用于 runtime 分发的容器。</returns>
internal static MicrosoftDiContainer CreateFrozenGFrameworkContainer(Action<MicrosoftDiContainer> configure)
{
ArgumentNullException.ThrowIfNull(configure);
var container = new MicrosoftDiContainer();
RegisterCqrsInfrastructure(container);
configure(container);
container.Freeze();
return container;
}
/// <summary>
/// 为 benchmark 宿主补齐默认 CQRS runtime seam确保它既能手工注册 handler也能走真实的程序集注册入口。
/// </summary>
/// <param name="container">当前 benchmark 拥有的 GFramework 容器。</param>
/// <remarks>
/// `RegisterCqrsHandlersFromAssembly(...)` 依赖预先可见的 runtime / registrar / registration service 实例绑定。
/// benchmark 宿主直接使用裸 <see cref="MicrosoftDiContainer" />,因此需要在配置阶段先补齐这组基础设施,
/// 避免各个 benchmark 用例各自复制同一段前置接线逻辑。
/// </remarks>
private static void RegisterCqrsInfrastructure(MicrosoftDiContainer container)
{
ArgumentNullException.ThrowIfNull(container);
if (container.Get<ICqrsRuntime>() is null)
{
var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher");
var notificationPublisher = container.Get<GFramework.Cqrs.Notification.INotificationPublisher>();
var runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, runtimeLogger, notificationPublisher);
container.Register(runtime);
RegisterLegacyRuntimeAlias(container, runtime);
}
else if (container.Get<LegacyICqrsRuntime>() is null)
{
RegisterLegacyRuntimeAlias(container, container.GetRequired<ICqrsRuntime>());
}
if (container.Get<ICqrsHandlerRegistrar>() is null)
{
var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar");
var registrar = GFramework.Cqrs.CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger);
container.Register<ICqrsHandlerRegistrar>(registrar);
}
if (container.Get<ICqrsRegistrationService>() is null)
{
var registrationLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsRegistrationService");
var registrar = container.GetRequired<ICqrsHandlerRegistrar>();
var registrationService = GFramework.Cqrs.CqrsRuntimeFactory.CreateRegistrationService(registrar, registrationLogger);
container.Register<ICqrsRegistrationService>(registrationService);
}
}
/// <summary>
/// 只激活当前 benchmark 场景明确拥有的 generated registry避免同一程序集里的其他 benchmark registry
/// 扩大冻结后服务索引与 dispatcher descriptor 基线。
/// </summary>
/// <typeparam name="TRegistry">当前 benchmark 需要接入的 generated registry 类型。</typeparam>
/// <param name="container">承载 generated registry 注册结果的 GFramework benchmark 容器。</param>
internal static void RegisterGeneratedBenchmarkRegistry<TRegistry>(MicrosoftDiContainer container)
where TRegistry : class, GFramework.Cqrs.ICqrsHandlerRegistry
{
ArgumentNullException.ThrowIfNull(container);
var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar");
CqrsHandlerRegistrar.RegisterGeneratedRegistry(container, typeof(TRegistry), registrarLogger);
}
/// <summary>
/// 为旧命名空间下的 CQRS runtime 契约注册兼容别名。
/// </summary>
/// <param name="container">承载 runtime 别名的 benchmark 容器。</param>
/// <param name="runtime">当前正式 CQRS runtime 实例。</param>
/// <exception cref="InvalidOperationException">
/// <paramref name="runtime" /> 未同时实现 legacy CQRS runtime 契约。
/// </exception>
private static void RegisterLegacyRuntimeAlias(MicrosoftDiContainer container, ICqrsRuntime runtime)
{
ArgumentNullException.ThrowIfNull(container);
ArgumentNullException.ThrowIfNull(runtime);
if (runtime is not LegacyICqrsRuntime legacyRuntime)
{
throw new InvalidOperationException(
$"The registered {typeof(ICqrsRuntime).FullName} must also implement {typeof(LegacyICqrsRuntime).FullName}. Actual runtime type: {runtime.GetType().FullName}.");
}
container.Register<LegacyICqrsRuntime>(legacyRuntime);
}
/// <summary>
/// 创建只承载当前 benchmark handler 集合的最小 MediatR 宿主。
/// </summary>
/// <param name="configure">补充当前场景的显式服务注册,例如手工单例 handler 或 pipeline 行为。</param>
/// <param name="handlerAssemblyMarkerType">用于限定扫描程序集的标记类型。</param>
/// <param name="handlerTypeFilter">
/// 仅允许当前 benchmark 场景需要的 handler / behavior 类型通过扫描;
/// 这样可保留 `AddMediatR` 的正常装配路径,同时避免整个基准程序集里的其他 handler 被一并注册。
/// </param>
/// <param name="lifetime">当前 benchmark 希望 MediatR 使用的默认注册生命周期。</param>
/// <returns>只承载当前 benchmark 场景所需服务的 DI 宿主。</returns>
internal static ServiceProvider CreateMediatRServiceProvider(
Action<IServiceCollection>? configure,
Type handlerAssemblyMarkerType,
Func<Type, bool> handlerTypeFilter,
ServiceLifetime lifetime = ServiceLifetime.Transient)
{
ArgumentNullException.ThrowIfNull(handlerAssemblyMarkerType);
ArgumentNullException.ThrowIfNull(handlerTypeFilter);
var services = new ServiceCollection();
services.AddLogging(static builder =>
Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter(
builder,
"LuckyPennySoftware.MediatR.License",
Microsoft.Extensions.Logging.LogLevel.None));
configure?.Invoke(services);
services.AddMediatR(options =>
{
options.Lifetime = lifetime;
options.TypeEvaluator = handlerTypeFilter;
options.RegisterServicesFromAssembly(handlerAssemblyMarkerType.Assembly);
});
return services.BuildServiceProvider();
}
/// <summary>
/// 创建承载 NuGet `Mediator` source-generated concrete mediator 的最小对照宿主。
/// </summary>
/// <param name="configure">补充当前场景的显式服务注册。</param>
/// <returns>可直接解析 generated `Mediator.Mediator` 的 DI 宿主。</returns>
/// <remarks>
/// 当前 benchmark 只把 `Mediator` 作为单例 steady-state 对照组接入,
/// 因为它的 lifetime 由 source generator 在编译期塑形;若后续需要 `Transient` / `Scoped` 矩阵,
/// 应按 `Mediator` 官方 benchmark 的做法拆成独立 build config而不是在同一编译产物里混用多个 lifetime。
/// </remarks>
internal static ServiceProvider CreateMediatorServiceProvider(Action<IServiceCollection>? configure)
{
var services = new ServiceCollection();
configure?.Invoke(services);
services.AddMediator();
return services.BuildServiceProvider();
}
/// <summary>
/// 判断某个类型是否正好实现了指定的闭合或开放 MediatR 合同。
/// </summary>
/// <param name="candidateType">待判断类型。</param>
/// <param name="openGenericContract">目标开放泛型合同,例如 <see cref="MediatR.IRequestHandler{TRequest,TResponse}" />。</param>
/// <returns>命中任一实现接口时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
internal static bool ImplementsOpenGenericContract(Type candidateType, Type openGenericContract)
{
ArgumentNullException.ThrowIfNull(candidateType);
ArgumentNullException.ThrowIfNull(openGenericContract);
return candidateType.GetInterfaces().Any(interfaceType =>
interfaceType.IsGenericType &&
interfaceType.GetGenericTypeDefinition() == openGenericContract);
}
}

View File

@ -0,0 +1,35 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using BenchmarkDotNet.Loggers;
using System;
namespace GFramework.Cqrs.Benchmarks.Messaging;
/// <summary>
/// 为 CQRS benchmark 运行打印并验证当前场景配置,避免矩阵配置与实际运行环境漂移。
/// </summary>
internal static class Fixture
{
/// <summary>
/// 输出当前 benchmark 配置并验证关键环境变量。
/// </summary>
/// <param name="scenario">当前 benchmark 场景名称。</param>
/// <param name="handlerCount">当前场景的处理器数量。</param>
/// <param name="pipelineCount">当前场景的 pipeline 行为数量。</param>
public static void Setup(string scenario, int handlerCount, int pipelineCount)
{
ConsoleLogger.Default.WriteLineHeader("GFramework.Cqrs benchmark config");
ConsoleLogger.Default.WriteLineInfo($"Scenario = {scenario}");
ConsoleLogger.Default.WriteLineInfo($"HandlerCount = {handlerCount}");
ConsoleLogger.Default.WriteLineInfo($"PipelineCount = {pipelineCount}");
var environmentScenario = Environment.GetEnvironmentVariable("GFRAMEWORK_CQRS_BENCHMARK_SCENARIO");
if (!string.IsNullOrWhiteSpace(environmentScenario) &&
!string.Equals(environmentScenario, scenario, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"Scenario mismatch. Expected '{environmentScenario}', actual '{scenario}'.");
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,98 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using GFramework.Core.Abstractions.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Cqrs.Benchmarks.Messaging;
/// <summary>
/// 为 benchmark 手写一个“生成后等价物” registry用于驱动真实的 generated invoker provider 运行时接线路径。
/// </summary>
public sealed class GeneratedRequestInvokerBenchmarkRegistry :
GFramework.Cqrs.ICqrsHandlerRegistry,
GFramework.Cqrs.ICqrsRequestInvokerProvider,
GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors
{
private static readonly GFramework.Cqrs.CqrsRequestInvokerDescriptor Descriptor =
new(
typeof(IRequestHandler<
RequestInvokerBenchmarks.GeneratedBenchmarkRequest,
RequestInvokerBenchmarks.GeneratedBenchmarkResponse>),
typeof(GeneratedRequestInvokerBenchmarkRegistry).GetMethod(
nameof(InvokeGeneratedRequestHandler),
BindingFlags.Public | BindingFlags.Static)
?? throw new InvalidOperationException("Missing generated request invoker benchmark method."));
private static readonly IReadOnlyList<GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> Descriptors =
[
new GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(
typeof(RequestInvokerBenchmarks.GeneratedBenchmarkRequest),
typeof(RequestInvokerBenchmarks.GeneratedBenchmarkResponse),
Descriptor)
];
/// <summary>
/// 将 generated benchmark request handler 注册到目标服务集合。
/// </summary>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IRequestHandler<
RequestInvokerBenchmarks.GeneratedBenchmarkRequest,
RequestInvokerBenchmarks.GeneratedBenchmarkResponse>),
typeof(RequestInvokerBenchmarks.GeneratedBenchmarkRequestHandler));
logger.Debug("Registered generated request invoker benchmark handler.");
}
/// <summary>
/// 返回当前 provider 暴露的全部 generated request invoker 描述符。
/// </summary>
public IReadOnlyList<GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> GetDescriptors()
{
return Descriptors;
}
/// <summary>
/// 为目标请求/响应类型对返回 generated request invoker 描述符。
/// </summary>
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out GFramework.Cqrs.CqrsRequestInvokerDescriptor? descriptor)
{
if (requestType == typeof(RequestInvokerBenchmarks.GeneratedBenchmarkRequest) &&
responseType == typeof(RequestInvokerBenchmarks.GeneratedBenchmarkResponse))
{
descriptor = Descriptor;
return true;
}
descriptor = null;
return false;
}
/// <summary>
/// 模拟 generated invoker provider 产出的开放静态调用入口。
/// </summary>
public static ValueTask<RequestInvokerBenchmarks.GeneratedBenchmarkResponse> InvokeGeneratedRequestHandler(
object handler,
object request,
CancellationToken cancellationToken)
{
var typedHandler = (IRequestHandler<
RequestInvokerBenchmarks.GeneratedBenchmarkRequest,
RequestInvokerBenchmarks.GeneratedBenchmarkResponse>)handler;
var typedRequest = (RequestInvokerBenchmarks.GeneratedBenchmarkRequest)request;
return typedHandler.Handle(typedRequest, cancellationToken);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,94 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using GFramework.Core.Abstractions.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Cqrs.Benchmarks.Messaging;
/// <summary>
/// 为 benchmark 手写一个“生成后等价物” stream registry用于驱动真实的 generated stream invoker provider 运行时接线路径。
/// </summary>
public sealed class GeneratedStreamInvokerBenchmarkRegistry :
GFramework.Cqrs.ICqrsHandlerRegistry,
GFramework.Cqrs.ICqrsStreamInvokerProvider,
GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors
{
private static readonly GFramework.Cqrs.CqrsStreamInvokerDescriptor Descriptor =
new(
typeof(IStreamRequestHandler<
StreamInvokerBenchmarks.GeneratedBenchmarkStreamRequest,
StreamInvokerBenchmarks.GeneratedBenchmarkResponse>),
typeof(GeneratedStreamInvokerBenchmarkRegistry).GetMethod(
nameof(InvokeGeneratedStreamHandler),
BindingFlags.Public | BindingFlags.Static)
?? throw new InvalidOperationException("Missing generated stream invoker benchmark method."));
private static readonly IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> Descriptors =
[
new GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(
typeof(StreamInvokerBenchmarks.GeneratedBenchmarkStreamRequest),
typeof(StreamInvokerBenchmarks.GeneratedBenchmarkResponse),
Descriptor)
];
/// <summary>
/// 将 generated benchmark stream handler 注册到目标服务集合。
/// </summary>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IStreamRequestHandler<
StreamInvokerBenchmarks.GeneratedBenchmarkStreamRequest,
StreamInvokerBenchmarks.GeneratedBenchmarkResponse>),
typeof(StreamInvokerBenchmarks.GeneratedBenchmarkStreamHandler));
logger.Debug("Registered generated stream invoker benchmark handler.");
}
/// <summary>
/// 返回当前 provider 暴露的全部 generated stream invoker 描述符。
/// </summary>
public IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> GetDescriptors()
{
return Descriptors;
}
/// <summary>
/// 为目标流式请求/响应类型对返回 generated stream invoker 描述符。
/// </summary>
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out GFramework.Cqrs.CqrsStreamInvokerDescriptor? descriptor)
{
if (requestType == typeof(StreamInvokerBenchmarks.GeneratedBenchmarkStreamRequest) &&
responseType == typeof(StreamInvokerBenchmarks.GeneratedBenchmarkResponse))
{
descriptor = Descriptor;
return true;
}
descriptor = null;
return false;
}
/// <summary>
/// 模拟 generated stream invoker provider 产出的开放静态调用入口。
/// </summary>
public static object InvokeGeneratedStreamHandler(object handler, object request, CancellationToken cancellationToken)
{
var typedHandler = (IStreamRequestHandler<
StreamInvokerBenchmarks.GeneratedBenchmarkStreamRequest,
StreamInvokerBenchmarks.GeneratedBenchmarkResponse>)handler;
var typedRequest = (StreamInvokerBenchmarks.GeneratedBenchmarkStreamRequest)request;
return typedHandler.Handle(typedRequest, cancellationToken);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,167 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
using System;
using System.Threading;
using System.Threading.Tasks;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using GeneratedMediator = Mediator.Mediator;
namespace GFramework.Cqrs.Benchmarks.Messaging;
/// <summary>
/// 对比单处理器 notification 在 GFramework.CQRS 与 MediatR 之间的 publish 开销。
/// </summary>
[Config(typeof(Config))]
public class NotificationBenchmarks
{
private MicrosoftDiContainer _container = null!;
private ICqrsRuntime _runtime = null!;
private ServiceProvider _mediatrServiceProvider = null!;
private ServiceProvider _mediatorServiceProvider = null!;
private IPublisher _mediatrPublisher = null!;
private GeneratedMediator _mediator = null!;
private BenchmarkNotification _notification = null!;
/// <summary>
/// 配置 notification benchmark 的公共输出格式。
/// </summary>
private sealed class Config : ManualConfig
{
public Config()
{
AddJob(Job.Default);
AddColumnProvider(DefaultColumnProviders.Instance);
AddColumn(new CustomColumn("Scenario", static (_, _) => "Notification"));
AddDiagnoser(MemoryDiagnoser.Default);
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
}
}
/// <summary>
/// 构建 notification publish 所需的最小 runtime 宿主和对照对象。
/// </summary>
[GlobalSetup]
public void Setup()
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
{
MinLevel = LogLevel.Fatal
};
Fixture.Setup("Notification", handlerCount: 1, pipelineCount: 0);
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>>(
new BenchmarkNotificationHandler());
});
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,
LoggerFactoryResolver.Provider.CreateLogger(nameof(NotificationBenchmarks)));
_mediatrServiceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
services => services.AddSingleton<MediatR.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler>(),
typeof(NotificationBenchmarks),
static candidateType => candidateType == typeof(BenchmarkNotificationHandler),
ServiceLifetime.Singleton);
_mediatrPublisher = _mediatrServiceProvider.GetRequiredService<IPublisher>();
_mediatorServiceProvider = BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null);
_mediator = _mediatorServiceProvider.GetRequiredService<GeneratedMediator>();
_notification = new BenchmarkNotification(Guid.NewGuid());
}
/// <summary>
/// 释放 MediatR 与 `Mediator` 对照组使用的 DI 宿主。
/// </summary>
[GlobalCleanup]
public void Cleanup()
{
BenchmarkCleanupHelper.DisposeAll(_container, _mediatrServiceProvider, _mediatorServiceProvider);
}
/// <summary>
/// 通过 GFramework.CQRS runtime 发布 notification。
/// </summary>
[Benchmark(Baseline = true)]
public ValueTask PublishNotification_GFrameworkCqrs()
{
return _runtime.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None);
}
/// <summary>
/// 通过 MediatR 发布 notification作为外部设计对照。
/// </summary>
[Benchmark]
public Task PublishNotification_MediatR()
{
return _mediatrPublisher.Publish(_notification, CancellationToken.None);
}
/// <summary>
/// 通过 `Mediator` source-generated concrete mediator 发布 notification作为高性能对照组。
/// </summary>
[Benchmark]
public ValueTask PublishNotification_Mediator()
{
return _mediator.Publish(_notification, CancellationToken.None);
}
/// <summary>
/// Benchmark notification。
/// </summary>
/// <param name="Id">通知标识。</param>
public sealed record BenchmarkNotification(Guid Id) :
GFramework.Cqrs.Abstractions.Cqrs.INotification,
Mediator.INotification,
MediatR.INotification;
/// <summary>
/// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 notification handler。
/// </summary>
public sealed class BenchmarkNotificationHandler :
GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>,
Mediator.INotificationHandler<BenchmarkNotification>,
MediatR.INotificationHandler<BenchmarkNotification>
{
/// <summary>
/// 处理 GFramework.CQRS notification。
/// </summary>
public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
/// <summary>
/// 处理 NuGet `Mediator` notification。
/// </summary>
ValueTask Mediator.INotificationHandler<BenchmarkNotification>.Handle(
BenchmarkNotification notification,
CancellationToken cancellationToken)
{
return Handle(notification, cancellationToken);
}
/// <summary>
/// 处理 MediatR notification。
/// </summary>
Task MediatR.INotificationHandler<BenchmarkNotification>.Handle(
BenchmarkNotification notification,
CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,350 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
using System;
using System.Threading;
using System.Threading.Tasks;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Notification;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using GeneratedMediator = Mediator.Mediator;
namespace GFramework.Cqrs.Benchmarks.Messaging;
/// <summary>
/// 对比固定 4 个处理器的 notification fan-out publish 在 baseline、GFramework.CQRS、NuGet `Mediator`
/// 与 MediatR 之间的开销。
/// </summary>
[Config(typeof(Config))]
public class NotificationFanOutBenchmarks
{
private MicrosoftDiContainer _container = null!;
private ICqrsRuntime _sequentialRuntime = null!;
private ICqrsRuntime _taskWhenAllRuntime = null!;
private ServiceProvider _mediatrServiceProvider = null!;
private ServiceProvider _mediatorServiceProvider = null!;
private IPublisher _mediatrPublisher = null!;
private GeneratedMediator _mediator = null!;
private BenchmarkNotification _notification = null!;
private BenchmarkNotificationHandler1 _baselineHandler1 = null!;
private BenchmarkNotificationHandler2 _baselineHandler2 = null!;
private BenchmarkNotificationHandler3 _baselineHandler3 = null!;
private BenchmarkNotificationHandler4 _baselineHandler4 = null!;
/// <summary>
/// 配置 notification fan-out benchmark 的公共输出格式。
/// </summary>
private sealed class Config : ManualConfig
{
public Config()
{
AddJob(Job.Default);
AddColumnProvider(DefaultColumnProviders.Instance);
AddColumn(new CustomColumn("Scenario", static (_, _) => "NotificationFanOut"));
AddDiagnoser(MemoryDiagnoser.Default);
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
}
}
/// <summary>
/// 构建固定 4 处理器 notification publish 所需的最小 runtime 宿主和对照对象。
/// </summary>
[GlobalSetup]
public void Setup()
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
{
MinLevel = LogLevel.Fatal
};
Fixture.Setup("NotificationFanOut", handlerCount: 4, pipelineCount: 0);
_baselineHandler1 = new BenchmarkNotificationHandler1();
_baselineHandler2 = new BenchmarkNotificationHandler2();
_baselineHandler3 = new BenchmarkNotificationHandler3();
_baselineHandler4 = new BenchmarkNotificationHandler4();
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler1>();
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler2>();
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler3>();
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler4>();
});
_sequentialRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,
LoggerFactoryResolver.Provider.CreateLogger(nameof(NotificationFanOutBenchmarks)));
_taskWhenAllRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,
LoggerFactoryResolver.Provider.CreateLogger($"{nameof(NotificationFanOutBenchmarks)}.{nameof(TaskWhenAllNotificationPublisher)}"),
new TaskWhenAllNotificationPublisher());
_mediatrServiceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
services =>
{
services.AddSingleton<MediatR.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler1>();
services.AddSingleton<MediatR.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler2>();
services.AddSingleton<MediatR.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler3>();
services.AddSingleton<MediatR.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler4>();
},
typeof(NotificationFanOutBenchmarks),
static candidateType =>
candidateType == typeof(BenchmarkNotificationHandler1) ||
candidateType == typeof(BenchmarkNotificationHandler2) ||
candidateType == typeof(BenchmarkNotificationHandler3) ||
candidateType == typeof(BenchmarkNotificationHandler4),
ServiceLifetime.Singleton);
_mediatrPublisher = _mediatrServiceProvider.GetRequiredService<IPublisher>();
_mediatorServiceProvider = BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null);
_mediator = _mediatorServiceProvider.GetRequiredService<GeneratedMediator>();
_notification = new BenchmarkNotification(Guid.NewGuid());
}
/// <summary>
/// 释放 MediatR 与 `Mediator` 对照组使用的 DI 宿主。
/// </summary>
[GlobalCleanup]
public void Cleanup()
{
BenchmarkCleanupHelper.DisposeAll(_container, _mediatrServiceProvider, _mediatorServiceProvider);
}
/// <summary>
/// 直接依次调用 4 个处理器,作为 fan-out dispatch 额外开销的 baseline。
/// </summary>
[Benchmark(Baseline = true)]
public async ValueTask PublishNotification_Baseline()
{
await _baselineHandler1.Handle(_notification, CancellationToken.None).ConfigureAwait(false);
await _baselineHandler2.Handle(_notification, CancellationToken.None).ConfigureAwait(false);
await _baselineHandler3.Handle(_notification, CancellationToken.None).ConfigureAwait(false);
await _baselineHandler4.Handle(_notification, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
/// 通过默认顺序发布器的 GFramework.CQRS runtime 发布固定 4 处理器的 notification。
/// </summary>
[Benchmark]
public ValueTask PublishNotification_GFrameworkCqrsSequential()
{
return _sequentialRuntime.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None);
}
/// <summary>
/// 通过内置 <c>Task.WhenAll(...)</c> 发布器的 GFramework.CQRS runtime 发布固定 4 处理器的 notification。
/// </summary>
[Benchmark]
public ValueTask PublishNotification_GFrameworkCqrsTaskWhenAll()
{
return _taskWhenAllRuntime.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None);
}
/// <summary>
/// 通过 MediatR 发布固定 4 处理器的 notification作为外部设计对照。
/// </summary>
[Benchmark]
public Task PublishNotification_MediatR()
{
return _mediatrPublisher.Publish(_notification, CancellationToken.None);
}
/// <summary>
/// 通过 `Mediator` source-generated concrete mediator 发布固定 4 处理器的 notification作为高性能对照组。
/// </summary>
[Benchmark]
public ValueTask PublishNotification_Mediator()
{
return _mediator.Publish(_notification, CancellationToken.None);
}
/// <summary>
/// Benchmark notification。
/// </summary>
/// <param name="Id">通知标识。</param>
public sealed record BenchmarkNotification(Guid Id) :
GFramework.Cqrs.Abstractions.Cqrs.INotification,
Mediator.INotification,
MediatR.INotification;
/// <summary>
/// 为 fan-out benchmark 提供统一的 no-op 处理逻辑。
/// </summary>
public abstract class BenchmarkNotificationHandlerBase
{
/// <summary>
/// 执行 benchmark 使用的最小处理逻辑。
/// </summary>
/// <param name="notification">当前 notification。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>已完成的值任务。</returns>
protected static ValueTask HandleCore(BenchmarkNotification notification, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(notification);
cancellationToken.ThrowIfCancellationRequested();
return ValueTask.CompletedTask;
}
}
/// <summary>
/// fan-out benchmark 的第 1 个 notification handler。
/// </summary>
public sealed class BenchmarkNotificationHandler1 :
BenchmarkNotificationHandlerBase,
GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>,
Mediator.INotificationHandler<BenchmarkNotification>,
MediatR.INotificationHandler<BenchmarkNotification>
{
/// <summary>
/// 处理 GFramework.CQRS notification。
/// </summary>
public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken)
{
return HandleCore(notification, cancellationToken);
}
/// <summary>
/// 处理 NuGet `Mediator` notification。
/// </summary>
ValueTask Mediator.INotificationHandler<BenchmarkNotification>.Handle(
BenchmarkNotification notification,
CancellationToken cancellationToken)
{
return HandleCore(notification, cancellationToken);
}
/// <summary>
/// 处理 MediatR notification。
/// </summary>
Task MediatR.INotificationHandler<BenchmarkNotification>.Handle(
BenchmarkNotification notification,
CancellationToken cancellationToken)
{
return HandleCore(notification, cancellationToken).AsTask();
}
}
/// <summary>
/// fan-out benchmark 的第 2 个 notification handler。
/// </summary>
public sealed class BenchmarkNotificationHandler2 :
BenchmarkNotificationHandlerBase,
GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>,
Mediator.INotificationHandler<BenchmarkNotification>,
MediatR.INotificationHandler<BenchmarkNotification>
{
/// <summary>
/// 处理 GFramework.CQRS notification。
/// </summary>
public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken)
{
return HandleCore(notification, cancellationToken);
}
/// <summary>
/// 处理 NuGet `Mediator` notification。
/// </summary>
ValueTask Mediator.INotificationHandler<BenchmarkNotification>.Handle(
BenchmarkNotification notification,
CancellationToken cancellationToken)
{
return HandleCore(notification, cancellationToken);
}
/// <summary>
/// 处理 MediatR notification。
/// </summary>
Task MediatR.INotificationHandler<BenchmarkNotification>.Handle(
BenchmarkNotification notification,
CancellationToken cancellationToken)
{
return HandleCore(notification, cancellationToken).AsTask();
}
}
/// <summary>
/// fan-out benchmark 的第 3 个 notification handler。
/// </summary>
public sealed class BenchmarkNotificationHandler3 :
BenchmarkNotificationHandlerBase,
GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>,
Mediator.INotificationHandler<BenchmarkNotification>,
MediatR.INotificationHandler<BenchmarkNotification>
{
/// <summary>
/// 处理 GFramework.CQRS notification。
/// </summary>
public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken)
{
return HandleCore(notification, cancellationToken);
}
/// <summary>
/// 处理 NuGet `Mediator` notification。
/// </summary>
ValueTask Mediator.INotificationHandler<BenchmarkNotification>.Handle(
BenchmarkNotification notification,
CancellationToken cancellationToken)
{
return HandleCore(notification, cancellationToken);
}
/// <summary>
/// 处理 MediatR notification。
/// </summary>
Task MediatR.INotificationHandler<BenchmarkNotification>.Handle(
BenchmarkNotification notification,
CancellationToken cancellationToken)
{
return HandleCore(notification, cancellationToken).AsTask();
}
}
/// <summary>
/// fan-out benchmark 的第 4 个 notification handler。
/// </summary>
public sealed class BenchmarkNotificationHandler4 :
BenchmarkNotificationHandlerBase,
GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>,
Mediator.INotificationHandler<BenchmarkNotification>,
MediatR.INotificationHandler<BenchmarkNotification>
{
/// <summary>
/// 处理 GFramework.CQRS notification。
/// </summary>
public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken)
{
return HandleCore(notification, cancellationToken);
}
/// <summary>
/// 处理 NuGet `Mediator` notification。
/// </summary>
ValueTask Mediator.INotificationHandler<BenchmarkNotification>.Handle(
BenchmarkNotification notification,
CancellationToken cancellationToken)
{
return HandleCore(notification, cancellationToken);
}
/// <summary>
/// 处理 MediatR notification。
/// </summary>
Task MediatR.INotificationHandler<BenchmarkNotification>.Handle(
BenchmarkNotification notification,
CancellationToken cancellationToken)
{
return HandleCore(notification, cancellationToken).AsTask();
}
}
}

View File

@ -0,0 +1,191 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
using System;
using System.Threading;
using System.Threading.Tasks;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using GeneratedMediator = Mediator.Mediator;
namespace GFramework.Cqrs.Benchmarks.Messaging;
/// <summary>
/// 对比单个 request 在直接调用、GFramework.CQRS runtime、NuGet `Mediator` 与 MediatR 之间的 steady-state dispatch 开销。
/// </summary>
[Config(typeof(Config))]
public class RequestBenchmarks
{
private MicrosoftDiContainer _container = null!;
private ICqrsRuntime _runtime = null!;
private ServiceProvider _mediatrServiceProvider = null!;
private ServiceProvider _mediatorServiceProvider = null!;
private IMediator _mediatr = null!;
private GeneratedMediator _mediator = null!;
private BenchmarkRequestHandler _baselineHandler = null!;
private BenchmarkRequest _request = null!;
/// <summary>
/// 配置 request benchmark 的公共输出格式。
/// </summary>
private sealed class Config : ManualConfig
{
public Config()
{
AddJob(Job.Default);
AddColumnProvider(DefaultColumnProviders.Instance);
AddColumn(new CustomColumn("Scenario", static (_, _) => "Request"));
AddDiagnoser(MemoryDiagnoser.Default);
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
}
}
/// <summary>
/// 构建 request dispatch 所需的最小 runtime 宿主和对照对象。
/// </summary>
[GlobalSetup]
public void Setup()
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
{
MinLevel = LogLevel.Fatal
};
Fixture.Setup("Request", handlerCount: 1, pipelineCount: 0);
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
_baselineHandler = new BenchmarkRequestHandler();
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedDefaultRequestBenchmarkRegistry>(container);
});
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,
LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestBenchmarks)));
_mediatrServiceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
configure: null,
typeof(RequestBenchmarks),
static candidateType => candidateType == typeof(BenchmarkRequestHandler),
ServiceLifetime.Singleton);
_mediatr = _mediatrServiceProvider.GetRequiredService<IMediator>();
_mediatorServiceProvider = BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null);
_mediator = _mediatorServiceProvider.GetRequiredService<GeneratedMediator>();
_request = new BenchmarkRequest(Guid.NewGuid());
}
/// <summary>
/// 释放 MediatR 与 `Mediator` 对照组使用的 DI 宿主。
/// </summary>
[GlobalCleanup]
public void Cleanup()
{
try
{
BenchmarkCleanupHelper.DisposeAll(_container, _mediatrServiceProvider, _mediatorServiceProvider);
}
finally
{
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
}
}
/// <summary>
/// 直接调用 handler作为 dispatch 额外开销的 baseline。
/// </summary>
[Benchmark(Baseline = true)]
public ValueTask<BenchmarkResponse> SendRequest_Baseline()
{
return _baselineHandler.Handle(_request, CancellationToken.None);
}
/// <summary>
/// 通过 GFramework.CQRS runtime 发送 request。
/// </summary>
[Benchmark]
public ValueTask<BenchmarkResponse> SendRequest_GFrameworkCqrs()
{
return _runtime.SendAsync(BenchmarkContext.Instance, _request, CancellationToken.None);
}
/// <summary>
/// 通过 MediatR 发送 request作为外部设计对照。
/// </summary>
[Benchmark]
public Task<BenchmarkResponse> SendRequest_MediatR()
{
return _mediatr.Send(_request, CancellationToken.None);
}
/// <summary>
/// 通过 `ai-libs/Mediator` 的 source-generated concrete mediator 发送 request作为高性能对照组。
/// </summary>
[Benchmark]
public ValueTask<BenchmarkResponse> SendRequest_Mediator()
{
return _mediator.Send(_request, CancellationToken.None);
}
/// <summary>
/// Benchmark request。
/// </summary>
/// <param name="Id">请求标识。</param>
public sealed record BenchmarkRequest(Guid Id) :
GFramework.Cqrs.Abstractions.Cqrs.IRequest<BenchmarkResponse>,
Mediator.IRequest<BenchmarkResponse>,
MediatR.IRequest<BenchmarkResponse>;
/// <summary>
/// Benchmark response。
/// </summary>
/// <param name="Id">响应标识。</param>
public sealed record BenchmarkResponse(Guid Id);
/// <summary>
/// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 request handler。
/// </summary>
public sealed class BenchmarkRequestHandler :
GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>,
Mediator.IRequestHandler<BenchmarkRequest, BenchmarkResponse>,
MediatR.IRequestHandler<BenchmarkRequest, BenchmarkResponse>
{
/// <summary>
/// 处理 GFramework.CQRS request。
/// </summary>
public ValueTask<BenchmarkResponse> Handle(BenchmarkRequest request, CancellationToken cancellationToken)
{
return ValueTask.FromResult(new BenchmarkResponse(request.Id));
}
/// <summary>
/// 处理 NuGet `Mediator` request。
/// </summary>
ValueTask<BenchmarkResponse> Mediator.IRequestHandler<BenchmarkRequest, BenchmarkResponse>.Handle(
BenchmarkRequest request,
CancellationToken cancellationToken)
{
return Handle(request, cancellationToken);
}
/// <summary>
/// 处理 MediatR request。
/// </summary>
Task<BenchmarkResponse> MediatR.IRequestHandler<BenchmarkRequest, BenchmarkResponse>.Handle(
BenchmarkRequest request,
CancellationToken cancellationToken)
{
return Task.FromResult(new BenchmarkResponse(request.Id));
}
}
}

View File

@ -0,0 +1,240 @@
// 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.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.GeneratedRequestInvokerBenchmarkRegistry))]
namespace GFramework.Cqrs.Benchmarks.Messaging;
/// <summary>
/// 对比 request steady-state dispatch 在 direct handler、GFramework 反射路径、GFramework generated invoker 路径与 MediatR 之间的开销差异。
/// </summary>
[Config(typeof(Config))]
public class RequestInvokerBenchmarks
{
private MicrosoftDiContainer _reflectionContainer = null!;
private ICqrsRuntime _reflectionRuntime = null!;
private MicrosoftDiContainer _generatedContainer = null!;
private ICqrsRuntime _generatedRuntime = null!;
private ServiceProvider _serviceProvider = null!;
private IMediator _mediatr = null!;
private ReflectionBenchmarkRequestHandler _baselineHandler = null!;
private ReflectionBenchmarkRequest _reflectionRequest = null!;
private GeneratedBenchmarkRequest _generatedRequest = null!;
private MediatRBenchmarkRequest _mediatrRequest = null!;
/// <summary>
/// 配置 request invoker benchmark 的公共输出格式。
/// </summary>
private sealed class Config : ManualConfig
{
public Config()
{
AddJob(Job.Default);
AddColumnProvider(DefaultColumnProviders.Instance);
AddColumn(new CustomColumn("Scenario", static (_, _) => "RequestInvoker"));
AddDiagnoser(MemoryDiagnoser.Default);
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
}
}
/// <summary>
/// 构建 reflection / generated / MediatR 三组 request dispatch 对照宿主。
/// </summary>
[GlobalSetup]
public void Setup()
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
{
MinLevel = LogLevel.Fatal
};
Fixture.Setup("RequestInvoker", handlerCount: 1, pipelineCount: 0);
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
_baselineHandler = new ReflectionBenchmarkRequestHandler();
_reflectionRequest = new ReflectionBenchmarkRequest(Guid.NewGuid());
_generatedRequest = new GeneratedBenchmarkRequest(Guid.NewGuid());
_mediatrRequest = new MediatRBenchmarkRequest(Guid.NewGuid());
_reflectionContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static container =>
{
container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<ReflectionBenchmarkRequest, ReflectionBenchmarkResponse>, ReflectionBenchmarkRequestHandler>();
});
_reflectionRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_reflectionContainer,
LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestInvokerBenchmarks) + ".Reflection"));
_generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedRequestInvokerBenchmarkRegistry>(container);
});
_generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_generatedContainer,
LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestInvokerBenchmarks) + ".Generated"));
_serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
configure: null,
typeof(RequestInvokerBenchmarks),
static candidateType => candidateType == typeof(MediatRBenchmarkRequestHandler),
ServiceLifetime.Transient);
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
}
/// <summary>
/// 释放 MediatR 对照组使用的 DI 宿主,并清理静态 dispatcher 缓存。
/// </summary>
[GlobalCleanup]
public void Cleanup()
{
try
{
BenchmarkCleanupHelper.DisposeAll(_reflectionContainer, _generatedContainer, _serviceProvider);
}
finally
{
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
}
}
/// <summary>
/// 直接调用最小 request handler作为 dispatch 额外开销 baseline。
/// </summary>
[Benchmark(Baseline = true)]
public ValueTask<ReflectionBenchmarkResponse> SendRequest_Baseline()
{
return _baselineHandler.Handle(_reflectionRequest, CancellationToken.None);
}
/// <summary>
/// 通过 GFramework.CQRS 反射 request binding 路径发送 request。
/// </summary>
[Benchmark]
public ValueTask<ReflectionBenchmarkResponse> SendRequest_GFrameworkReflection()
{
return _reflectionRuntime.SendAsync(BenchmarkContext.Instance, _reflectionRequest, CancellationToken.None);
}
/// <summary>
/// 通过 generated request invoker provider 预热后的 GFramework.CQRS runtime 发送 request。
/// </summary>
[Benchmark]
public ValueTask<GeneratedBenchmarkResponse> SendRequest_GFrameworkGenerated()
{
return _generatedRuntime.SendAsync(BenchmarkContext.Instance, _generatedRequest, CancellationToken.None);
}
/// <summary>
/// 通过 MediatR 发送 request作为外部对照。
/// </summary>
[Benchmark]
public Task<MediatRBenchmarkResponse> SendRequest_MediatR()
{
return _mediatr.Send(_mediatrRequest, CancellationToken.None);
}
/// <summary>
/// Reflection runtime request。
/// </summary>
/// <param name="Id">请求标识。</param>
public sealed record ReflectionBenchmarkRequest(Guid Id) :
GFramework.Cqrs.Abstractions.Cqrs.IRequest<ReflectionBenchmarkResponse>;
/// <summary>
/// Reflection runtime response。
/// </summary>
/// <param name="Id">响应标识。</param>
public sealed record ReflectionBenchmarkResponse(Guid Id);
/// <summary>
/// Generated runtime request。
/// </summary>
/// <param name="Id">请求标识。</param>
public sealed record GeneratedBenchmarkRequest(Guid Id) :
GFramework.Cqrs.Abstractions.Cqrs.IRequest<GeneratedBenchmarkResponse>;
/// <summary>
/// Generated runtime response。
/// </summary>
/// <param name="Id">响应标识。</param>
public sealed record GeneratedBenchmarkResponse(Guid Id);
/// <summary>
/// MediatR request。
/// </summary>
/// <param name="Id">请求标识。</param>
public sealed record MediatRBenchmarkRequest(Guid Id) : MediatR.IRequest<MediatRBenchmarkResponse>;
/// <summary>
/// MediatR response。
/// </summary>
/// <param name="Id">响应标识。</param>
public sealed record MediatRBenchmarkResponse(Guid Id);
/// <summary>
/// Reflection runtime 的最小 request handler。
/// </summary>
public sealed class ReflectionBenchmarkRequestHandler :
GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<ReflectionBenchmarkRequest, ReflectionBenchmarkResponse>
{
/// <summary>
/// 处理 reflection benchmark request。
/// </summary>
public ValueTask<ReflectionBenchmarkResponse> Handle(
ReflectionBenchmarkRequest request,
CancellationToken cancellationToken)
{
return ValueTask.FromResult(new ReflectionBenchmarkResponse(request.Id));
}
}
/// <summary>
/// Generated runtime 的最小 request handler。
/// </summary>
public sealed class GeneratedBenchmarkRequestHandler :
GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<GeneratedBenchmarkRequest, GeneratedBenchmarkResponse>
{
/// <summary>
/// 处理 generated benchmark request。
/// </summary>
public ValueTask<GeneratedBenchmarkResponse> Handle(
GeneratedBenchmarkRequest request,
CancellationToken cancellationToken)
{
return ValueTask.FromResult(new GeneratedBenchmarkResponse(request.Id));
}
}
/// <summary>
/// MediatR 对照组的最小 request handler。
/// </summary>
public sealed class MediatRBenchmarkRequestHandler :
MediatR.IRequestHandler<MediatRBenchmarkRequest, MediatRBenchmarkResponse>
{
/// <summary>
/// 处理 MediatR benchmark request。
/// </summary>
public Task<MediatRBenchmarkResponse> Handle(
MediatRBenchmarkRequest request,
CancellationToken cancellationToken)
{
return Task.FromResult(new MediatRBenchmarkResponse(request.Id));
}
}
}

Some files were not shown because too many files have changed in this diff Show More