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