From 59ec2558785f121bbcb7931f0047d82b107b7408 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 8 May 2026 17:57:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(cqrs):=20=E5=85=AC=E5=BC=80=E9=A1=BA?= =?UTF-8?q?=E5=BA=8F=20notification=20publisher=20=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增公开 SequentialNotificationPublisher,并让默认 runtime 回退复用该策略 - 增加顺序 notification publisher 组合根注册入口,并更新测试文档与恢复点 --- ...ionPublisherRegistrationExtensionsTests.cs | 26 +++++++++++++++++++ ...ficationPublisherRegistrationExtensions.cs | 17 ++++++++++++ .../SequentialNotificationPublisher.cs | 11 ++++---- GFramework.Cqrs/README.md | 10 ++++++- .../todos/cqrs-rewrite-migration-tracking.md | 7 +++-- .../traces/cqrs-rewrite-migration-trace.md | 22 ++++++++++++++++ docs/zh-CN/core/cqrs.md | 10 ++++++- 7 files changed, 93 insertions(+), 10 deletions(-) rename GFramework.Cqrs/{Internal => Notification}/SequentialNotificationPublisher.cs (70%) diff --git a/GFramework.Cqrs.Tests/Cqrs/NotificationPublisherRegistrationExtensionsTests.cs b/GFramework.Cqrs.Tests/Cqrs/NotificationPublisherRegistrationExtensionsTests.cs index 7657754a..f98c8833 100644 --- a/GFramework.Cqrs.Tests/Cqrs/NotificationPublisherRegistrationExtensionsTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/NotificationPublisherRegistrationExtensionsTests.cs @@ -50,6 +50,32 @@ internal sealed class NotificationPublisherRegistrationExtensionsTests Assert.That(publishTask.Exception, Is.Not.Null); } + /// + /// 验证显式注册内置 后, + /// 默认 runtime 基础设施会保留“首个失败立即停止后续处理器”的顺序语义。 + /// + [Test] + public void UseSequentialNotificationPublisher_Should_Preserve_Stop_On_First_Failure_Semantics() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); + + var trailingHandler = new RecordingNotificationHandler(); + var container = new MicrosoftDiContainer(); + container.UseSequentialNotificationPublisher(); + container.Register>(new ThrowingNotificationHandler()); + container.Register>(trailingHandler); + CqrsTestRuntime.RegisterInfrastructure(container); + container.Freeze(); + + var context = new ArchitectureContext(container); + + Assert.That( + async () => await context.PublishAsync(new TestNotification()).ConfigureAwait(false), + Throws.InvalidOperationException.With.Message.EqualTo("boom")); + Assert.That(trailingHandler.WasInvoked, Is.False); + Assert.That(container.GetRequired(), Is.TypeOf()); + } + /// /// 验证显式传入实例的组合根注册入口会把同一个 publisher 实例绑定到容器。 /// diff --git a/GFramework.Cqrs/Extensions/NotificationPublisherRegistrationExtensions.cs b/GFramework.Cqrs/Extensions/NotificationPublisherRegistrationExtensions.cs index 034e878b..bbe313f9 100644 --- a/GFramework.Cqrs/Extensions/NotificationPublisherRegistrationExtensions.cs +++ b/GFramework.Cqrs/Extensions/NotificationPublisherRegistrationExtensions.cs @@ -77,6 +77,23 @@ public static class NotificationPublisherRegistrationExtensions return UseNotificationPublisher(container, new TaskWhenAllNotificationPublisher()); } + /// + /// 将内置 注册为当前容器唯一的 notification publisher 策略。 + /// + /// 目标依赖注入容器。 + /// 同一个 ,便于在组合根中继续链式配置。 + /// + /// + /// 当前容器已存在 注册,无法再切换为另一个策略。 + /// + /// + /// 该策略适合处理器之间存在顺序依赖,或调用方希望在首个失败处立即停止后续分发的场景。 + /// + public static IIocContainer UseSequentialNotificationPublisher(this IIocContainer container) + { + return UseNotificationPublisher(container, new SequentialNotificationPublisher()); + } + /// /// 在组合根阶段阻止多个 notification publisher 策略同时注册,避免 runtime 创建时出现歧义。 /// diff --git a/GFramework.Cqrs/Internal/SequentialNotificationPublisher.cs b/GFramework.Cqrs/Notification/SequentialNotificationPublisher.cs similarity index 70% rename from GFramework.Cqrs/Internal/SequentialNotificationPublisher.cs rename to GFramework.Cqrs/Notification/SequentialNotificationPublisher.cs index 534ecaf0..63a097a7 100644 --- a/GFramework.Cqrs/Internal/SequentialNotificationPublisher.cs +++ b/GFramework.Cqrs/Notification/SequentialNotificationPublisher.cs @@ -2,18 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 using GFramework.Cqrs.Abstractions.Cqrs; -using GFramework.Cqrs.Notification; -namespace GFramework.Cqrs.Internal; +namespace GFramework.Cqrs.Notification; /// -/// 默认的通知发布器实现。 +/// 以内置顺序策略逐个分发通知处理器。 /// /// -/// 该实现完整保留当前 CQRS runtime 的既有通知语义:按已解析顺序逐个执行处理器, -/// 并在首个处理器抛出异常时立即停止后续发布。 +/// 该实现完整保留默认 CQRS runtime 的既有通知语义:按已解析顺序逐个执行处理器。 +/// 当任意处理器抛出异常时,后续处理器不会继续执行,因此更适合存在顺序依赖或希望尽早暴露首个失败的场景。 /// -internal sealed class SequentialNotificationPublisher : INotificationPublisher +public sealed class SequentialNotificationPublisher : INotificationPublisher { /// /// 按既定顺序逐个执行当前通知的处理器。 diff --git a/GFramework.Cqrs/README.md b/GFramework.Cqrs/README.md index 6d5b5835..0e4c0891 100644 --- a/GFramework.Cqrs/README.md +++ b/GFramework.Cqrs/README.md @@ -127,9 +127,17 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu - 通知会分发给所有已注册 `INotificationHandler<>`;零处理器时默认静默完成。 - 默认通知发布器会按容器解析顺序逐个执行处理器,并在首个处理器抛出异常时立即停止后续分发。 - 若需要等待所有处理器并行完成,可以在创建 runtime 时显式传入 `TaskWhenAllNotificationPublisher`;该策略不保证执行顺序,并会在全部处理器结束后聚合异常或取消结果。 - - 若容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置顺序发布器。 + - 若容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置 `SequentialNotificationPublisher`。 - 若只是为了降低 fixed fan-out publish 的 steady-state 成本,当前 benchmark 并不表明 `TaskWhenAllNotificationPublisher` 会优于默认顺序发布器;它更适合你需要“等待全部处理器完成并统一观察失败”的场景。 +如果你需要显式保留默认顺序语义,也可以在组合根里直接声明: + +```csharp +using GFramework.Cqrs.Extensions; + +container.UseSequentialNotificationPublisher(); +``` + 如果你需要切换到内置并行 notification publisher,推荐在组合根里显式声明这条策略: ```csharp diff --git a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md index f0b458af..424b8444 100644 --- a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md +++ b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md @@ -7,10 +7,13 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-115` +- 恢复点编号:`CQRS-REWRITE-RP-116` - 当前阶段:`Phase 8` - 当前 PR 锚点:`PR #341` - 当前结论: + - 当前 `RP-116` 已继续沿用 `$gframework-batch-boot 50`,并把刚收口的 notification publisher 配置面补成对称的内置策略集合:`SequentialNotificationPublisher` 现在作为公开类型提供,组合根新增 `UseSequentialNotificationPublisher()`,不再只存在“一个显式并行策略 + 一个隐式默认回退” + - 这一批让用户能够在文档、配置和测试里显式表达“我要顺序失败即停”与“我要并行等待全部完成”这两条内置语义,而不需要把默认顺序策略理解成 runtime 内部细节;这进一步降低了 notification publisher seam 的心智负担 + - 当前分支相对 `origin/main` 的累计 branch diff 提交 `RP-115` 后约为 `11 files`,仍明显低于 `$gframework-batch-boot 50` 的停止阈值;因此这批继续保持 notification 配置面内的低风险、可评审切片 - 当前 `RP-115` 已继续沿用 `$gframework-batch-boot 50`,并把 notification publisher 线从“已具备 seam + benchmark 事实”继续收口到组合根配置面:新增 `GFramework.Cqrs.Extensions.NotificationPublisherRegistrationExtensions`,提供 `UseNotificationPublisher(...)` / `UseNotificationPublisher()` / `UseTaskWhenAllNotificationPublisher()` 三个显式入口,避免用户再手写 `Register(new ...)` - 这一批同时把重复策略注册前移到组合根阶段显式阻止,并在回归里确认 `UseTaskWhenAllNotificationPublisher()` 经过默认 runtime 基础设施后仍会命中“失败不阻断其余 handler”的并行语义;这让 notification publisher 的采用路径从“知道内部 seam 如何接线”收口为“知道该在容器里选哪条策略” - 用户文档现同步写明 `TaskWhenAllNotificationPublisher` 更适合“并行完成 + 统一观察失败”的语义诉求,而不是 fixed fan-out steady-state publish 优化;这与 `RP-114` 的 benchmark 结论保持一致,减少使用者把它误解成默认的性能升级开关 @@ -372,7 +375,7 @@ CQRS 迁移与收敛。 ## 下一推荐步骤 -1. 既然 `RP-115` 已把 notification publisher 选择面收口到显式组合根扩展,下一轮若继续留在 notification 线,优先评估是否需要补第二个内置策略或更细的配置文档,而不是再让用户直接依赖裸 `INotificationPublisher` 注册细节 +1. 既然 `RP-116` 已把顺序 / 并行两条内置策略都收口成显式组合根入口,下一轮若继续留在 notification 线,优先评估是否需要补更细的采用文档或第三种策略,而不是再让用户直接依赖裸 `INotificationPublisher` 注册细节 2. 当前 benchmark 已证明 `TaskWhenAllNotificationPublisher` 的价值主要在并行完成与异常聚合语义,而不是吞吐收益;若 notification 配置面已经足够,下一轮优先回到 request dispatch 常量开销,而不是新增 generated notification invoker/provider 这类 steady-state 收益信号偏弱的 runtime seam 3. 若 benchmark 对照需要继续贴近 `Mediator` 官方设计,再评估 `Mediator` 的 compile-time lifetime / stream 对照矩阵,或给 stream 引入 scoped host 基线,而不是回头重试已被 benchmark 否决的 `GetAll(Type)` 零行为探测方案 diff --git a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md index c23bfdb6..d0c4f41e 100644 --- a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md +++ b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md @@ -2,6 +2,28 @@ ## 2026-05-08 +### 阶段:公开顺序 notification publisher 策略(CQRS-REWRITE-RP-116) + +- 延续 `$gframework-batch-boot 50`,本轮继续留在 notification publisher 配置面,但不再新增第三方 benchmark 或 runtime seam: + - 当前分支相对 `origin/main`(`7ca21af9`, `2026-05-08 16:12:20 +0800`)的累计 branch diff 在 `RP-115` 提交后约为 `11 files`,明显低于 `50` 文件阈值 + - `RP-115` 已把采用路径收口到显式组合根扩展,但当前仍只有 `TaskWhenAllNotificationPublisher` 是公开内置策略;默认顺序语义仍主要靠“未注册时的隐式回退”表达 +- 本轮主线程决策: + - 新增公开 `GFramework.Cqrs/Notification/SequentialNotificationPublisher.cs`,并让 `CqrsRuntimeFactory` 默认回退直接使用这条公开顺序策略 + - 删除 `GFramework.Cqrs/Internal/SequentialNotificationPublisher.cs` 的内部副本,避免默认顺序语义同时存在“内部实现”和“公开实现”两套类型来源 + - 为 `NotificationPublisherRegistrationExtensions` 增加 `UseSequentialNotificationPublisher()`,并在回归与用户文档中把“显式顺序策略”与“显式并行策略”作为对称选择面呈现 +- 本轮权威验证: + - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~NotificationPublisherRegistrationExtensionsTests|FullyQualifiedName~CqrsNotificationPublisherTests"` + - 结果:通过,`10/10` passed + - `python3 scripts/license-header.py --check --paths GFramework.Cqrs/Notification/SequentialNotificationPublisher.cs GFramework.Cqrs/CqrsRuntimeFactory.cs GFramework.Cqrs/Extensions/NotificationPublisherRegistrationExtensions.cs GFramework.Cqrs.Tests/Cqrs/NotificationPublisherRegistrationExtensionsTests.cs GFramework.Cqrs/README.md docs/zh-CN/core/cqrs.md ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md` + - 结果:通过 + - `git diff --check` + - 结果:通过 +- 本轮结论: + - notification publisher 的公开配置面现已从“一个显式策略 + 一个隐式默认回退”收口成两条对称的内置策略选择:`UseSequentialNotificationPublisher()` 与 `UseTaskWhenAllNotificationPublisher()` + - 若后续继续 notification 线,更合理的下一刀会是补更细的采用文档或新的策略语义,而不是继续让顺序 / 并行这两条基础选择停留在隐式约定上 + ### 阶段:notification publisher 组合根配置面(CQRS-REWRITE-RP-115) - 延续 `$gframework-batch-boot 50`,本轮不再回到 benchmark 宿主,而是沿着 `RP-114` 已明确的性能/语义事实继续收口用户接入缺口: diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 82331786..866fc706 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -117,7 +117,15 @@ var playerId = await architecture.Context.SendRequestAsync( - 零处理器时静默完成 - 已解析处理器按容器顺序逐个执行 - 首个处理器抛出异常时立即停止后续分发 -- 如果容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置顺序发布器 +- 如果容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置 `SequentialNotificationPublisher` + +如果你想在组合根里显式保留默认顺序语义,也可以直接写成: + +```csharp +using GFramework.Cqrs.Extensions; + +container.UseSequentialNotificationPublisher(); +``` 如果你需要等待所有通知处理器并行完成,而不是沿用默认顺序语义,可以显式切换到内置 `TaskWhenAllNotificationPublisher`: