docs(cqrs): 收紧生成器与通知策略说明

- 更新 CQRS 栏目中的 generated invoker、fallback 分层与 notification publisher 选择边界说明

- 对齐生成器专题页与当前 runtime 注册和分发实现的实际行为
This commit is contained in:
gewuyou 2026-05-11 12:37:39 +08:00
parent 3b2e6899d5
commit d9e47abdb6
2 changed files with 17 additions and 10 deletions

View File

@ -118,6 +118,7 @@ var playerId = await architecture.Context.SendRequestAsync(
- 已解析处理器按容器顺序逐个执行
- 首个处理器抛出异常时立即停止后续分发
- 如果容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置 `SequentialNotificationPublisher`
- 默认 runtime 只消费一个 `INotificationPublisher`;如果容器里已经存在该注册,再调用 `UseNotificationPublisher*` 系列扩展会直接报错,而不是按“后注册覆盖前注册”处理
如果你需要在组合根里明确表达“为什么选这条策略”,可以按下面的矩阵判断:
@ -125,7 +126,7 @@ var playerId = await architecture.Context.SendRequestAsync(
| --- | --- | --- | --- | --- |
| `UseSequentialNotificationPublisher()` | 需要保持容器顺序,且希望首个失败立即停止 | 保证按容器顺序执行 | 首个处理器异常会中断后续处理器 | 这也是默认回退策略 |
| `UseTaskWhenAllNotificationPublisher()` | 需要让全部处理器并行完成,再统一观察异常或取消 | 不保证顺序 | 不会在首个失败时中断其余处理器;全部结束后统一暴露结果 | 更适合语义补齐,不是性能优化开关 |
| `UseNotificationPublisher(...)` / `UseNotificationPublisher<TPublisher>()` | 需要接入自定义或第三方 publisher 策略 | 取决于实现 | 取决于实现 | 前者复用现成实例,后者让容器负责单例生命周期 |
| `UseNotificationPublisher(...)` / `UseNotificationPublisher<TPublisher>()` | 需要接入自定义或第三方 publisher 策略 | 取决于实现 | 取决于实现 | 前者复用现成实例,后者让容器负责单例生命周期;两者都要求容器此前尚未注册 `INotificationPublisher` |
如果你想在组合根里显式保留默认顺序语义,也可以直接写成:
@ -216,11 +217,12 @@ protected override void OnInitialize()
2. 存在生成注册器时优先使用 `ICqrsHandlerRegistry`
3. 当生成注册器同时暴露 generated request invoker provider 时runtime 会把 request/response 类型对对应的 descriptor 预先接线到 dispatcher 缓存,后续请求分发优先消费这些 generated request invoker 元数据
4. 当生成注册器同时暴露 generated stream invoker provider 时runtime 会以同样方式优先消费 stream request 对应的 generated stream invoker descriptor只有当前类型对未命中时才回退到既有反射 stream binding
5. 生成注册器不可用时记录告警并回退到反射路径;只有“未命中 generated descriptor”才会走反射绑定已命中的不兼容元数据会直接抛出异常
6. 当生成注册器携带 `CqrsReflectionFallbackAttribute` 元数据时,运行时会先完成生成注册器注册,再补剩余 handler
7. `CqrsReflectionFallbackAttribute` 可以同时携带 `Type[]``string[]` 两类清单;运行时会优先复用直接 `Type` 条目,只对名称条目做定向 `Assembly.GetType(...)` 查找
8. 只有旧版空 marker 或生成注册器不可用时,才会回到整程序集反射扫描
9. 同一程序集按稳定键去重,避免重复注册
5. generated invoker 只覆盖 request 与 stream 两类单次分发元数据notification handler 仍通过已注册的 `INotificationHandler<>` 集合和选定的 `INotificationPublisher` 参与分发,不存在对应的 generated notification invoker 通道
6. 生成注册器不可用时记录告警并回退到反射路径;只有“未命中 generated descriptor”才会走反射 binding 创建,已成功登记到缓存的类型对不会再回退到另一条 generated 通道
7. 当生成注册器携带 `CqrsReflectionFallbackAttribute` 元数据时,运行时会先完成生成注册器注册,再补剩余 handler
8. `CqrsReflectionFallbackAttribute` 可以同时携带 `Type[]``string[]` 两类清单;运行时会优先复用直接 `Type` 条目,只对名称条目做定向 `Assembly.GetType(...)` 查找
9. 只有 fallback 元数据为空、仍是旧版空 marker 语义,或生成注册器整体不可用时,才会回到整程序集反射扫描
10. 同一程序集按稳定键去重,避免重复注册
换句话说,声明 fallback 特性本身不等于“整包反射扫描”。当前推荐理解是生成注册器负责能静态表达的部分fallback 只补它覆盖不到的 handler。
@ -231,7 +233,7 @@ protected override void OnInitialize()
- stream invoker provider / descriptor
- 面向 `CreateStream(...)` 触发的流式请求分发
两者的共同点都是“优先消费 generated invoker 元数据,未命中时保留既有反射绑定作为兜底”,而不是要求业务侧切换到另一套 runtime 入口。
两者的共同点都是“优先消费 generated invoker 元数据,未命中时保留既有反射绑定作为兜底”,而不是要求业务侧切换到另一套 runtime 入口。通知发布不在这组 generated invoker 能力里;它始终沿用 runtime 解析出的 handler 集合与当前 publisher 策略。
对接入方来说,更关键的 reader-facing 语义是:安装 `Cqrs.SourceGenerators` 后,不要求“所有 handler 都能被生成代码直接引用”才有收益。
即使仍有 fallbackruntime 也会先消费 generated registry再只对剩余 handler 做定向补扫;只有旧版 marker 语义或空 fallback 元数据才会退回整程序集扫描。

View File

@ -40,6 +40,7 @@ runtime 在注册 handlers 时优先走静态注册表;当运行时合同允
这意味着运行时会先使用生成注册器完成可静态表达的映射;对 request 与 stream 分发来说,也会优先消费 generated invoker
descriptor。只有当前类型对没有 generated metadata或 registry / fallback 无法覆盖时,才继续回到既有反射 binding 或补扫路径,而不是退回整程序集盲扫。
如果这些 fallback handlers 本身仍可直接引用,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据;当 runtime 允许同一程序集声明多个 fallback 特性实例时mixed 场景也会拆成 `Type` 元数据和字符串元数据两段,进一步减少 runtime 再做字符串类型名回查的成本。
这里的 generated invoker 只覆盖 `IRequestHandler<,>``IStreamRequestHandler<,>``INotificationHandler<>` 仍然只参与 registry / fallback 注册;通知分发本身继续由 runtime 解析出的 handler 集合和 `INotificationPublisher` 策略决定。
## 最小接入路径
@ -87,9 +88,10 @@ RegisterCqrsHandlersFromAssemblies(
2. 优先激活生成的 `ICqrsHandlerRegistry`
3. 若生成注册器同时提供 request invoker provider / descriptorregistrar 会把这些 request invoker 元数据预先登记到 dispatcher 缓存
4. 若生成注册器同时提供 stream invoker provider / descriptorruntime 也会优先消费对应的 generated stream invoker 元数据;未命中时仍回退到既有反射 stream binding
5. 若生成元数据损坏、registry 不可激活,记录告警并回退到反射路径
6. 若存在 `CqrsReflectionFallbackAttribute`,优先按其中携带的 `Type` 或类型名补扫剩余 handler若元数据为空或只保留 marker 语义,则退回整程序集补扫
7. 同一程序集按稳定键去重,避免重复注册
5. generated invoker provider 不是独立入口;它只是让 dispatcher 在已知 `requestType + responseType` 类型对时优先命中编译期 descriptor未命中时仍保持原有 runtime 分发入口
6. 若生成元数据损坏、registry 不可激活,记录告警并回退到反射路径
7. 若存在 `CqrsReflectionFallbackAttribute`,优先按其中携带的 `Type` 或类型名补扫剩余 handler只有元数据为空、只保留 marker 语义,或 registry 整体不可用时,才退回整程序集补扫
8. 同一程序集按稳定键去重,避免重复注册
这个行为由
[运行时注册流程测试](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs)
@ -127,6 +129,8 @@ RegisterCqrsHandlersFromAssemblies(
- 其余场景统一回退到字符串元数据,避免 mixed 场景漏注册
- 只有在 runtime 提供 `CqrsReflectionFallbackAttribute` 合同时,才允许发射依赖 fallback 的结果
`fallback` 在这里表示“补齐生成注册器没有直接接线的剩余 handler”不是“生成器一出现就重新扫描整个程序集”。只要 attribute 里已经带了明确 `Type` 或类型名runtime 就会先走这份定向清单。
## 生成策略层级
把这个生成器理解成“静态注册 or 整程序集扫描”的二选一,会低估它的收益。当前策略实际上分成四层:
@ -141,6 +145,7 @@ RegisterCqrsHandlersFromAssemblies(
- 只有前面几层都无法覆盖的剩余 handler才交给 `CqrsReflectionFallbackAttribute`
这意味着安装生成器后,并不要求“所有 handler 都可直接引用”才有收益。很多只能部分静态表达的项目,仍然可以把大部分注册路径前移到编译期,再对少数复杂类型做定向补扫。
其中 request / stream 的 generated invoker descriptor 只在前两类 runtime seam 同时存在、且当前 handler 能安全生成静态 invoker 时才会出现;否则对应请求仍然走已存在的反射 binding 创建路径,不会影响 registry 本身继续工作。
## 哪些场景通常不会直接退回整程序集扫描