mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-09 01:54:30 +08:00
feat(cqrs): 公开顺序 notification publisher 策略
- 新增公开 SequentialNotificationPublisher,并让默认 runtime 回退复用该策略 - 增加顺序 notification publisher 组合根注册入口,并更新测试文档与恢复点
This commit is contained in:
parent
310791db5a
commit
59ec255878
@ -50,6 +50,32 @@ internal sealed class NotificationPublisherRegistrationExtensionsTests
|
||||
Assert.That(publishTask.Exception, Is.Not.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证显式注册内置 <see cref="SequentialNotificationPublisher" /> 后,
|
||||
/// 默认 runtime 基础设施会保留“首个失败立即停止后续处理器”的顺序语义。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void UseSequentialNotificationPublisher_Should_Preserve_Stop_On_First_Failure_Semantics()
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
|
||||
|
||||
var trailingHandler = new RecordingNotificationHandler();
|
||||
var container = new MicrosoftDiContainer();
|
||||
container.UseSequentialNotificationPublisher();
|
||||
container.Register<INotificationHandler<TestNotification>>(new ThrowingNotificationHandler());
|
||||
container.Register<INotificationHandler<TestNotification>>(trailingHandler);
|
||||
CqrsTestRuntime.RegisterInfrastructure(container);
|
||||
container.Freeze();
|
||||
|
||||
var context = new ArchitectureContext(container);
|
||||
|
||||
Assert.That(
|
||||
async () => await context.PublishAsync(new TestNotification()).ConfigureAwait(false),
|
||||
Throws.InvalidOperationException.With.Message.EqualTo("boom"));
|
||||
Assert.That(trailingHandler.WasInvoked, Is.False);
|
||||
Assert.That(container.GetRequired<INotificationPublisher>(), Is.TypeOf<SequentialNotificationPublisher>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证显式传入实例的组合根注册入口会把同一个 publisher 实例绑定到容器。
|
||||
/// </summary>
|
||||
|
||||
@ -77,6 +77,23 @@ public static class NotificationPublisherRegistrationExtensions
|
||||
return UseNotificationPublisher(container, new TaskWhenAllNotificationPublisher());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将内置 <see cref="SequentialNotificationPublisher" /> 注册为当前容器唯一的 notification publisher 策略。
|
||||
/// </summary>
|
||||
/// <param name="container">目标依赖注入容器。</param>
|
||||
/// <returns>同一个 <paramref name="container" />,便于在组合根中继续链式配置。</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 当前容器已存在 <see cref="INotificationPublisher" /> 注册,无法再切换为另一个策略。
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// 该策略适合处理器之间存在顺序依赖,或调用方希望在首个失败处立即停止后续分发的场景。
|
||||
/// </remarks>
|
||||
public static IIocContainer UseSequentialNotificationPublisher(this IIocContainer container)
|
||||
{
|
||||
return UseNotificationPublisher(container, new SequentialNotificationPublisher());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在组合根阶段阻止多个 notification publisher 策略同时注册,避免 runtime 创建时出现歧义。
|
||||
/// </summary>
|
||||
|
||||
@ -2,18 +2,17 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using GFramework.Cqrs.Notification;
|
||||
|
||||
namespace GFramework.Cqrs.Internal;
|
||||
namespace GFramework.Cqrs.Notification;
|
||||
|
||||
/// <summary>
|
||||
/// 默认的通知发布器实现。
|
||||
/// 以内置顺序策略逐个分发通知处理器。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该实现完整保留当前 CQRS runtime 的既有通知语义:按已解析顺序逐个执行处理器,
|
||||
/// 并在首个处理器抛出异常时立即停止后续发布。
|
||||
/// <para>该实现完整保留默认 CQRS runtime 的既有通知语义:按已解析顺序逐个执行处理器。</para>
|
||||
/// <para>当任意处理器抛出异常时,后续处理器不会继续执行,因此更适合存在顺序依赖或希望尽早暴露首个失败的场景。</para>
|
||||
/// </remarks>
|
||||
internal sealed class SequentialNotificationPublisher : INotificationPublisher
|
||||
public sealed class SequentialNotificationPublisher : INotificationPublisher
|
||||
{
|
||||
/// <summary>
|
||||
/// 按既定顺序逐个执行当前通知的处理器。
|
||||
@ -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
|
||||
|
||||
@ -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<TPublisher>()` / `UseTaskWhenAllNotificationPublisher()` 三个显式入口,避免用户再手写 `Register<INotificationPublisher>(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)` 零行为探测方案
|
||||
|
||||
|
||||
@ -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` 已明确的性能/语义事实继续收口用户接入缺口:
|
||||
|
||||
@ -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`:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user