diff --git a/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs index 30208feb..de3c7b92 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs @@ -16,6 +16,7 @@ using GFramework.Core.Logging; using GFramework.Cqrs.Abstractions.Cqrs; using MediatR; using Microsoft.Extensions.DependencyInjection; +using GeneratedMediator = Mediator.Mediator; namespace GFramework.Cqrs.Benchmarks.Messaging; @@ -27,8 +28,10 @@ public class NotificationBenchmarks { private MicrosoftDiContainer _container = null!; private ICqrsRuntime _runtime = null!; - private ServiceProvider _serviceProvider = null!; - private IPublisher _publisher = null!; + private ServiceProvider _mediatrServiceProvider = null!; + private ServiceProvider _mediatorServiceProvider = null!; + private IPublisher _mediatrPublisher = null!; + private GeneratedMediator _mediator = null!; private BenchmarkNotification _notification = null!; /// @@ -67,23 +70,26 @@ public class NotificationBenchmarks _container, LoggerFactoryResolver.Provider.CreateLogger(nameof(NotificationBenchmarks))); - _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider( + _mediatrServiceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider( services => services.AddSingleton, BenchmarkNotificationHandler>(), typeof(NotificationBenchmarks), static candidateType => candidateType == typeof(BenchmarkNotificationHandler), ServiceLifetime.Singleton); - _publisher = _serviceProvider.GetRequiredService(); + _mediatrPublisher = _mediatrServiceProvider.GetRequiredService(); + + _mediatorServiceProvider = BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null); + _mediator = _mediatorServiceProvider.GetRequiredService(); _notification = new BenchmarkNotification(Guid.NewGuid()); } /// - /// 释放 MediatR 对照组使用的 DI 宿主。 + /// 释放 MediatR 与 `Mediator` 对照组使用的 DI 宿主。 /// [GlobalCleanup] public void Cleanup() { - BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider); + BenchmarkCleanupHelper.DisposeAll(_container, _mediatrServiceProvider, _mediatorServiceProvider); } /// @@ -101,7 +107,16 @@ public class NotificationBenchmarks [Benchmark] public Task PublishNotification_MediatR() { - return _publisher.Publish(_notification, CancellationToken.None); + return _mediatrPublisher.Publish(_notification, CancellationToken.None); + } + + /// + /// 通过 `Mediator` source-generated concrete mediator 发布 notification,作为高性能对照组。 + /// + [Benchmark] + public ValueTask PublishNotification_Mediator() + { + return _mediator.Publish(_notification, CancellationToken.None); } /// @@ -110,6 +125,7 @@ public class NotificationBenchmarks /// 通知标识。 public sealed record BenchmarkNotification(Guid Id) : GFramework.Cqrs.Abstractions.Cqrs.INotification, + Mediator.INotification, MediatR.INotification; /// @@ -117,6 +133,7 @@ public class NotificationBenchmarks /// public sealed class BenchmarkNotificationHandler : GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler, + Mediator.INotificationHandler, MediatR.INotificationHandler { /// @@ -127,6 +144,16 @@ public class NotificationBenchmarks return ValueTask.CompletedTask; } + /// + /// 处理 NuGet `Mediator` notification。 + /// + ValueTask Mediator.INotificationHandler.Handle( + BenchmarkNotification notification, + CancellationToken cancellationToken) + { + return Handle(notification, cancellationToken); + } + /// /// 处理 MediatR notification。 /// diff --git a/GFramework.Cqrs.Benchmarks/Messaging/NotificationFanOutBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/NotificationFanOutBenchmarks.cs new file mode 100644 index 00000000..431fa359 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/NotificationFanOutBenchmarks.cs @@ -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; + +/// +/// 对比固定 4 个处理器的 notification fan-out publish 在 baseline、GFramework.CQRS、NuGet `Mediator` +/// 与 MediatR 之间的开销。 +/// +[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!; + + /// + /// 配置 notification fan-out benchmark 的公共输出格式。 + /// + 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)); + } + } + + /// + /// 构建固定 4 处理器 notification publish 所需的最小 runtime 宿主和对照对象。 + /// + [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, BenchmarkNotificationHandler1>(); + container.RegisterSingleton, BenchmarkNotificationHandler2>(); + container.RegisterSingleton, BenchmarkNotificationHandler3>(); + container.RegisterSingleton, 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, BenchmarkNotificationHandler1>(); + services.AddSingleton, BenchmarkNotificationHandler2>(); + services.AddSingleton, BenchmarkNotificationHandler3>(); + services.AddSingleton, BenchmarkNotificationHandler4>(); + }, + typeof(NotificationFanOutBenchmarks), + static candidateType => + candidateType == typeof(BenchmarkNotificationHandler1) || + candidateType == typeof(BenchmarkNotificationHandler2) || + candidateType == typeof(BenchmarkNotificationHandler3) || + candidateType == typeof(BenchmarkNotificationHandler4), + ServiceLifetime.Singleton); + _mediatrPublisher = _mediatrServiceProvider.GetRequiredService(); + + _mediatorServiceProvider = BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null); + _mediator = _mediatorServiceProvider.GetRequiredService(); + + _notification = new BenchmarkNotification(Guid.NewGuid()); + } + + /// + /// 释放 MediatR 与 `Mediator` 对照组使用的 DI 宿主。 + /// + [GlobalCleanup] + public void Cleanup() + { + BenchmarkCleanupHelper.DisposeAll(_container, _mediatrServiceProvider, _mediatorServiceProvider); + } + + /// + /// 直接依次调用 4 个处理器,作为 fan-out dispatch 额外开销的 baseline。 + /// + [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); + } + + /// + /// 通过默认顺序发布器的 GFramework.CQRS runtime 发布固定 4 处理器的 notification。 + /// + [Benchmark] + public ValueTask PublishNotification_GFrameworkCqrsSequential() + { + return _sequentialRuntime.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None); + } + + /// + /// 通过内置 Task.WhenAll(...) 发布器的 GFramework.CQRS runtime 发布固定 4 处理器的 notification。 + /// + [Benchmark] + public ValueTask PublishNotification_GFrameworkCqrsTaskWhenAll() + { + return _taskWhenAllRuntime.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None); + } + + /// + /// 通过 MediatR 发布固定 4 处理器的 notification,作为外部设计对照。 + /// + [Benchmark] + public Task PublishNotification_MediatR() + { + return _mediatrPublisher.Publish(_notification, CancellationToken.None); + } + + /// + /// 通过 `Mediator` source-generated concrete mediator 发布固定 4 处理器的 notification,作为高性能对照组。 + /// + [Benchmark] + public ValueTask PublishNotification_Mediator() + { + return _mediator.Publish(_notification, CancellationToken.None); + } + + /// + /// Benchmark notification。 + /// + /// 通知标识。 + public sealed record BenchmarkNotification(Guid Id) : + GFramework.Cqrs.Abstractions.Cqrs.INotification, + Mediator.INotification, + MediatR.INotification; + + /// + /// 为 fan-out benchmark 提供统一的 no-op 处理逻辑。 + /// + public abstract class BenchmarkNotificationHandlerBase + { + /// + /// 执行 benchmark 使用的最小处理逻辑。 + /// + /// 当前 notification。 + /// 取消令牌。 + /// 已完成的值任务。 + protected static ValueTask HandleCore(BenchmarkNotification notification, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(notification); + cancellationToken.ThrowIfCancellationRequested(); + return ValueTask.CompletedTask; + } + } + + /// + /// fan-out benchmark 的第 1 个 notification handler。 + /// + public sealed class BenchmarkNotificationHandler1 : + BenchmarkNotificationHandlerBase, + GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler, + Mediator.INotificationHandler, + MediatR.INotificationHandler + { + /// + /// 处理 GFramework.CQRS notification。 + /// + public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken) + { + return HandleCore(notification, cancellationToken); + } + + /// + /// 处理 NuGet `Mediator` notification。 + /// + ValueTask Mediator.INotificationHandler.Handle( + BenchmarkNotification notification, + CancellationToken cancellationToken) + { + return HandleCore(notification, cancellationToken); + } + + /// + /// 处理 MediatR notification。 + /// + Task MediatR.INotificationHandler.Handle( + BenchmarkNotification notification, + CancellationToken cancellationToken) + { + return HandleCore(notification, cancellationToken).AsTask(); + } + } + + /// + /// fan-out benchmark 的第 2 个 notification handler。 + /// + public sealed class BenchmarkNotificationHandler2 : + BenchmarkNotificationHandlerBase, + GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler, + Mediator.INotificationHandler, + MediatR.INotificationHandler + { + /// + /// 处理 GFramework.CQRS notification。 + /// + public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken) + { + return HandleCore(notification, cancellationToken); + } + + /// + /// 处理 NuGet `Mediator` notification。 + /// + ValueTask Mediator.INotificationHandler.Handle( + BenchmarkNotification notification, + CancellationToken cancellationToken) + { + return HandleCore(notification, cancellationToken); + } + + /// + /// 处理 MediatR notification。 + /// + Task MediatR.INotificationHandler.Handle( + BenchmarkNotification notification, + CancellationToken cancellationToken) + { + return HandleCore(notification, cancellationToken).AsTask(); + } + } + + /// + /// fan-out benchmark 的第 3 个 notification handler。 + /// + public sealed class BenchmarkNotificationHandler3 : + BenchmarkNotificationHandlerBase, + GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler, + Mediator.INotificationHandler, + MediatR.INotificationHandler + { + /// + /// 处理 GFramework.CQRS notification。 + /// + public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken) + { + return HandleCore(notification, cancellationToken); + } + + /// + /// 处理 NuGet `Mediator` notification。 + /// + ValueTask Mediator.INotificationHandler.Handle( + BenchmarkNotification notification, + CancellationToken cancellationToken) + { + return HandleCore(notification, cancellationToken); + } + + /// + /// 处理 MediatR notification。 + /// + Task MediatR.INotificationHandler.Handle( + BenchmarkNotification notification, + CancellationToken cancellationToken) + { + return HandleCore(notification, cancellationToken).AsTask(); + } + } + + /// + /// fan-out benchmark 的第 4 个 notification handler。 + /// + public sealed class BenchmarkNotificationHandler4 : + BenchmarkNotificationHandlerBase, + GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler, + Mediator.INotificationHandler, + MediatR.INotificationHandler + { + /// + /// 处理 GFramework.CQRS notification。 + /// + public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken) + { + return HandleCore(notification, cancellationToken); + } + + /// + /// 处理 NuGet `Mediator` notification。 + /// + ValueTask Mediator.INotificationHandler.Handle( + BenchmarkNotification notification, + CancellationToken cancellationToken) + { + return HandleCore(notification, cancellationToken); + } + + /// + /// 处理 MediatR notification。 + /// + Task MediatR.INotificationHandler.Handle( + BenchmarkNotification notification, + CancellationToken cancellationToken) + { + return HandleCore(notification, cancellationToken).AsTask(); + } + } +} diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md index f589a0e3..28c130a5 100644 --- a/GFramework.Cqrs.Benchmarks/README.md +++ b/GFramework.Cqrs.Benchmarks/README.md @@ -29,7 +29,9 @@ - `Messaging/StreamInvokerBenchmarks.cs` - direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 stream 完整枚举对比 - `Messaging/NotificationBenchmarks.cs` - - `GFramework.Cqrs` runtime 与 `MediatR` 的单处理器 notification publish 对比 + - `GFramework.Cqrs` runtime、NuGet `Mediator` source-generated concrete path 与 `MediatR` 的单处理器 notification publish 对比 +- `Messaging/NotificationFanOutBenchmarks.cs` + - fixed `4 handler` notification fan-out 的 baseline、`GFramework.Cqrs` 默认顺序发布器、内置 `TaskWhenAllNotificationPublisher`、NuGet `Mediator` source-generated concrete path 与 `MediatR` publish 对比 - `Messaging/StreamingBenchmarks.cs` - direct handler、已接上 handwritten generated stream invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream request 完整枚举对比 diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsNotificationPublisherTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsNotificationPublisherTests.cs index 3b4b66e6..6e674d30 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsNotificationPublisherTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsNotificationPublisherTests.cs @@ -91,6 +91,41 @@ internal sealed class CqrsNotificationPublisherTests Assert.That(handler.ObservedContext, Is.SameAs(architectureContext.Object)); } + /// + /// 验证内置 `TaskWhenAll` 发布器会继续调度所有处理器,而不是沿用默认顺序发布器的失败即停语义。 + /// + [Test] + public async Task PublishAsync_Should_Invoke_All_Handlers_When_Using_TaskWhenAll_NotificationPublisher() + { + var trailingHandler = new RecordingNotificationHandler("second", []); + var runtime = CreateRuntime( + container => + { + container + .Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler))) + .Returns( + [ + new ThrowingNotificationHandler(), + trailingHandler + ]); + }, + new TaskWhenAllNotificationPublisher()); + + var publishTask = runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).AsTask(); + + try + { + await publishTask.ConfigureAwait(false); + } + catch (Exception) + { + // 并行发布会把处理器失败收敛到返回任务;这里仅消费异常并继续验证所有处理器都已被触发。 + } + + Assert.That(trailingHandler.Invoked, Is.True); + Assert.That(publishTask.Exception, Is.Not.Null); + } + /// /// 验证默认通知发布器在零处理器场景下会保持静默完成。 /// diff --git a/GFramework.Cqrs.Tests/Cqrs/NotificationPublisherRegistrationExtensionsTests.cs b/GFramework.Cqrs.Tests/Cqrs/NotificationPublisherRegistrationExtensionsTests.cs new file mode 100644 index 00000000..f98c8833 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/NotificationPublisherRegistrationExtensionsTests.cs @@ -0,0 +1,169 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Core.Architectures; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Ioc; +using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Extensions; +using GFramework.Cqrs.Notification; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 验证 notification publisher 组合根注册扩展的关键行为。 +/// +[TestFixture] +internal sealed class NotificationPublisherRegistrationExtensionsTests +{ + /// + /// 验证显式注册内置 后, + /// 标准 runtime 基础设施会复用该策略并继续调度所有处理器。 + /// + [Test] + public async Task UseTaskWhenAllNotificationPublisher_Should_Be_Used_By_Default_Runtime_Infrastructure() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); + + var trailingHandler = new RecordingNotificationHandler(); + var container = new MicrosoftDiContainer(); + container.UseTaskWhenAllNotificationPublisher(); + container.Register>(new ThrowingNotificationHandler()); + container.Register>(trailingHandler); + CqrsTestRuntime.RegisterInfrastructure(container); + container.Freeze(); + + var context = new ArchitectureContext(container); + var publishTask = context.PublishAsync(new TestNotification()).AsTask(); + + try + { + await publishTask.ConfigureAwait(false); + } + catch (Exception) + { + // `TaskWhenAll` 策略会在所有处理器都结束后聚合失败;这里仅消费异常并继续断言第二个处理器已执行。 + } + + Assert.That(trailingHandler.WasInvoked, Is.True); + 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 实例绑定到容器。 + /// + [Test] + public void UseNotificationPublisher_Instance_Overload_Should_Register_Same_Instance() + { + var container = new MicrosoftDiContainer(); + var publisher = new TrackingNotificationPublisher(); + + var returnedContainer = container.UseNotificationPublisher(publisher); + + Assert.That(returnedContainer, Is.SameAs(container)); + Assert.That(container.Get(), Is.SameAs(publisher)); + } + + /// + /// 验证组合根扩展会阻止重复 notification publisher 注册,避免 runtime 创建阶段才暴露歧义。 + /// + [Test] + public void UseNotificationPublisher_Should_Throw_When_NotificationPublisher_Already_Registered() + { + var container = new MicrosoftDiContainer(); + container.UseTaskWhenAllNotificationPublisher(); + + Assert.That( + () => container.UseNotificationPublisher(new TrackingNotificationPublisher()), + Throws.InvalidOperationException.With.Message.Contains(nameof(INotificationPublisher))); + } + + /// + /// 为本组测试提供最小 notification 类型。 + /// + private sealed record TestNotification : INotification; + + /// + /// 记录自己是否被执行的测试处理器。 + /// + private sealed class RecordingNotificationHandler : INotificationHandler + { + /// + /// 获取当前处理器是否至少执行过一次。 + /// + public bool WasInvoked { get; private set; } + + /// + /// 记录执行痕迹并立刻完成。 + /// + public ValueTask Handle(TestNotification notification, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(notification); + cancellationToken.ThrowIfCancellationRequested(); + WasInvoked = true; + return ValueTask.CompletedTask; + } + } + + /// + /// 始终抛出异常的测试处理器,用于验证并行策略不会因为首个失败而停止其余处理器。 + /// + private sealed class ThrowingNotificationHandler : INotificationHandler + { + /// + /// 始终抛出测试异常。 + /// + public ValueTask Handle(TestNotification notification, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(notification); + cancellationToken.ThrowIfCancellationRequested(); + throw new InvalidOperationException("boom"); + } + } + + /// + /// 用于验证实例注册重载是否保留原对象身份的测试发布器。 + /// + private sealed class TrackingNotificationPublisher : INotificationPublisher + { + /// + /// 直接完成当前 publish 调用。 + /// + public ValueTask PublishAsync( + NotificationPublishContext context, + CancellationToken cancellationToken = default) + where TNotification : INotification + { + ArgumentNullException.ThrowIfNull(context); + cancellationToken.ThrowIfCancellationRequested(); + return ValueTask.CompletedTask; + } + } +} diff --git a/GFramework.Cqrs/Extensions/NotificationPublisherRegistrationExtensions.cs b/GFramework.Cqrs/Extensions/NotificationPublisherRegistrationExtensions.cs new file mode 100644 index 00000000..bbe313f9 --- /dev/null +++ b/GFramework.Cqrs/Extensions/NotificationPublisherRegistrationExtensions.cs @@ -0,0 +1,112 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Core.Abstractions.Ioc; +using GFramework.Cqrs.Notification; + +namespace GFramework.Cqrs.Extensions; + +/// +/// 为 CQRS runtime 提供 notification publisher 策略的组合根注册入口。 +/// +/// +/// 默认 runtime 只会消费一个 实例,因此该扩展类把“选择哪种策略”显式收敛到容器配置阶段。 +/// 这些入口应在 runtime 创建前调用;对于走标准 GFramework.Core 启动路径的架构,它们会被 CqrsRuntimeModule 自动复用。 +/// +public static class NotificationPublisherRegistrationExtensions +{ + /// + /// 将指定的 notification publisher 实例注册为当前容器唯一的发布策略。 + /// + /// 目标依赖注入容器。 + /// 要复用的 notification publisher 实例。 + /// 同一个 ,便于在组合根中继续链式配置。 + /// + /// 。 + /// + /// + /// 当前容器已存在 注册,无法再切换为另一个策略。 + /// + public static IIocContainer UseNotificationPublisher( + this IIocContainer container, + INotificationPublisher notificationPublisher) + { + ArgumentNullException.ThrowIfNull(container); + ArgumentNullException.ThrowIfNull(notificationPublisher); + + ThrowIfNotificationPublisherAlreadyRegistered(container); + container.Register(notificationPublisher); + return container; + } + + /// + /// 将指定类型的 notification publisher 注册为当前容器唯一的发布策略。 + /// + /// 发布策略实现类型。 + /// 目标依赖注入容器。 + /// 同一个 ,便于在组合根中继续链式配置。 + /// + /// + /// 当前容器已存在 注册,无法再切换为另一个策略。 + /// + public static IIocContainer UseNotificationPublisher(this IIocContainer container) + where TNotificationPublisher : class, INotificationPublisher + { + ArgumentNullException.ThrowIfNull(container); + + ThrowIfNotificationPublisherAlreadyRegistered(container); + container.RegisterSingleton(); + return container; + } + + /// + /// 将内置 注册为当前容器唯一的 notification publisher 策略。 + /// + /// 目标依赖注入容器。 + /// 同一个 ,便于在组合根中继续链式配置。 + /// + /// + /// 当前容器已存在 注册,无法再切换为另一个策略。 + /// + /// + /// 该策略更适合“等待所有处理器完成并统一观察失败”的语义诉求; + /// 若只是为了降低 steady-state publish 开销,应先结合实际 benchmark 结果评估是否值得切换。 + /// + public static IIocContainer UseTaskWhenAllNotificationPublisher(this IIocContainer container) + { + return UseNotificationPublisher(container, new TaskWhenAllNotificationPublisher()); + } + + /// + /// 将内置 注册为当前容器唯一的 notification publisher 策略。 + /// + /// 目标依赖注入容器。 + /// 同一个 ,便于在组合根中继续链式配置。 + /// + /// + /// 当前容器已存在 注册,无法再切换为另一个策略。 + /// + /// + /// 该策略适合处理器之间存在顺序依赖,或调用方希望在首个失败处立即停止后续分发的场景。 + /// + public static IIocContainer UseSequentialNotificationPublisher(this IIocContainer container) + { + return UseNotificationPublisher(container, new SequentialNotificationPublisher()); + } + + /// + /// 在组合根阶段阻止多个 notification publisher 策略同时注册,避免 runtime 创建时出现歧义。 + /// + /// 当前正在配置的依赖注入容器。 + /// 当前容器已存在 notification publisher 注册。 + private static void ThrowIfNotificationPublisherAlreadyRegistered(IIocContainer container) + { + if (!container.HasRegistration(typeof(INotificationPublisher))) + { + return; + } + + throw new InvalidOperationException( + $"An {typeof(INotificationPublisher).FullName} is already registered. Remove the existing notification publisher strategy before calling {nameof(UseNotificationPublisher)} again."); + } +} 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/Notification/TaskWhenAllNotificationPublisher.cs b/GFramework.Cqrs/Notification/TaskWhenAllNotificationPublisher.cs new file mode 100644 index 00000000..5ab6e53d --- /dev/null +++ b/GFramework.Cqrs/Notification/TaskWhenAllNotificationPublisher.cs @@ -0,0 +1,71 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using System.Threading.Tasks; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Notification; + +/// +/// 以内置 Task.WhenAll(...) 策略并行分发通知处理器。 +/// +/// +/// 该实现会先为当前发布调用中的每个处理器创建独立执行任务,再等待全部任务完成。 +/// 它不会保留默认顺序发布器的“首个异常立即停止”语义;如果多个处理器失败,返回任务会聚合这些异常。 +/// 适合处理器之间互不依赖,且调用方更关心总耗时而不是处理顺序的场景。 +/// +public sealed class TaskWhenAllNotificationPublisher : INotificationPublisher +{ + /// + /// 并行启动当前通知的所有处理器,并等待它们全部结束。 + /// + /// 通知类型。 + /// 当前发布调用的执行上下文。 + /// 取消令牌。 + /// 表示所有处理器都已完成的值任务。 + /// + public ValueTask PublishAsync( + NotificationPublishContext context, + CancellationToken cancellationToken = default) + where TNotification : INotification + { + ArgumentNullException.ThrowIfNull(context); + + return context.Handlers.Count switch + { + 0 => ValueTask.CompletedTask, + 1 => context.InvokeHandlerAsync(context.Handlers[0], cancellationToken), + _ => PublishCoreAsync(context, cancellationToken) + }; + } + + /// + /// 为多处理器场景建立并行等待,确保单个处理器的同步异常也会被收敛到返回任务中。 + /// + private static async ValueTask PublishCoreAsync( + NotificationPublishContext context, + CancellationToken cancellationToken) + where TNotification : INotification + { + var tasks = new Task[context.Handlers.Count]; + + for (var index = 0; index < context.Handlers.Count; index++) + { + tasks[index] = InvokeHandlerSafelyAsync(context, context.Handlers[index], cancellationToken).AsTask(); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + /// + /// 通过异步包装把同步抛出的处理器异常也转换成可聚合的任务结果。 + /// + private static async ValueTask InvokeHandlerSafelyAsync( + NotificationPublishContext context, + object handler, + CancellationToken cancellationToken) + where TNotification : INotification + { + await context.InvokeHandlerAsync(handler, cancellationToken).ConfigureAwait(false); + } +} diff --git a/GFramework.Cqrs/README.md b/GFramework.Cqrs/README.md index 1072d53c..78a051d4 100644 --- a/GFramework.Cqrs/README.md +++ b/GFramework.Cqrs/README.md @@ -50,6 +50,7 @@ - `CqrsRuntimeFactory.cs` - `Internal/CqrsDispatcher.cs` - `Notification/INotificationPublisher.cs` + - `Notification/TaskWhenAllNotificationPublisher.cs` - `Internal/CqrsHandlerRegistrar.cs` - `Internal/DefaultCqrsHandlerRegistrar.cs` - `Internal/DefaultCqrsRegistrationService.cs` @@ -124,8 +125,43 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu - 未找到处理器会抛出异常。 - 通知分发 - 通知会分发给所有已注册 `INotificationHandler<>`;零处理器时默认静默完成。 - - 默认通知发布器会按容器解析顺序逐个执行处理器,并在首个处理器抛出异常时立即停止后续分发。 - - 若容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置顺序发布器。 + - 若容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置 `SequentialNotificationPublisher`。 + - 内置 notification publisher 的推荐选择如下: + + | 策略 | 推荐场景 | 执行顺序 | 失败语义 | 备注 | + | --- | --- | --- | --- | --- | + | `SequentialNotificationPublisher` | 需要保持容器顺序,且希望首个失败立即停止后续分发 | 保证按容器解析顺序逐个执行 | 首个处理器抛出异常时立即停止 | 也是默认回退策略 | + | `TaskWhenAllNotificationPublisher` | 需要让全部处理器并行完成,并在结束后统一观察失败或取消 | 不保证顺序 | 不会在首个失败时停止其余处理器;会聚合最终异常或取消结果 | 更适合语义补齐,不是性能开关 | + | `UseNotificationPublisher(...)` 自定义实例 | 需要接入仓库外的自定义策略或第三方策略 | 取决于具体实现 | 取决于具体实现 | 仅在内置顺序 / 并行策略都不满足时使用 | + + - 若只是为了降低 fixed fan-out publish 的 steady-state 成本,当前 benchmark 并不表明 `TaskWhenAllNotificationPublisher` 会优于默认顺序发布器;它更适合你需要“等待全部处理器完成并统一观察失败”的场景。 + +如果你需要显式保留默认顺序语义,也可以在组合根里直接声明: + +```csharp +using GFramework.Cqrs.Extensions; + +container.UseSequentialNotificationPublisher(); +``` + +如果你需要切换到内置并行 notification publisher,推荐在组合根里显式声明这条策略: + +```csharp +using GFramework.Cqrs.Extensions; + +container.UseTaskWhenAllNotificationPublisher(); +``` + +如果你确实需要自定义 publisher 实例,也可以继续显式注册: + +```csharp +using GFramework.Cqrs.Extensions; +using GFramework.Cqrs.Notification; + +container.UseNotificationPublisher(new TaskWhenAllNotificationPublisher()); +``` + +对于走标准 `GFramework.Core` 启动路径的架构,这些组合根扩展会被默认基础设施自动复用;如果你直接调用 `CqrsRuntimeFactory.CreateRuntime(...)`,也仍然可以像以前一样显式传入 publisher 实例。 - 流式请求 - 通过 `IStreamRequest` 和 `IStreamRequestHandler<,>` 返回 `IAsyncEnumerable`。 - 当消费端程序集提供 generated stream invoker provider / descriptor 后,runtime 会优先消费这组 stream invoker 元数据;未命中时仍回退到既有反射 stream binding 创建路径。 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 59291782..f1f46ada 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,37 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-110` +- 恢复点编号:`CQRS-REWRITE-RP-118` - 当前阶段:`Phase 8` -- 当前 PR 锚点:`PR #341` +- 当前 PR 锚点:`PR #342` - 当前结论: + - 当前 `RP-118` 已使用 `$gframework-pr-review` 复核 `PR #342` latest-head review:CodeRabbit 当前仍成立的是 `NotificationFanOutBenchmarks` 中 MediatR 分支绕过共享 `HandleCore(...)`、`GFramework.Cqrs/README.md` 的 MD058 表格空行、以及恢复文档的 PR 锚点与 fan-out 历史值表述;Greptile 额外指出的 `UseTaskWhenAllNotificationPublisher()` 示例多余 `using GFramework.Cqrs.Notification;` 也在本轮一并收口 + - 本轮不改 `GFramework.Cqrs` runtime 语义,只让 benchmark 的 MediatR handler 与其余对照分支共用同一组空值 / 取消检查,并把 README、中文文档与 `cqrs-rewrite` 恢复文档同步到当前 PR #342 上下文 + - 本轮按 `NotificationFanOutBenchmarks` short-job 复跑确认,对称化 MediatR handler 后当前 fixed `4 handler` fan-out 结果约为 `Mediator` `3.598 ns / 0 B`、baseline `7.033 ns / 0 B`、`MediatR` `257.533 ns / 1256 B`、`GFramework.Cqrs` 顺序 `409.557 ns / 408 B`、`TaskWhenAll` `484.531 ns / 496 B` + - `RP-117` 留在“最近权威验证”中的固定 `4 handler` fan-out 数值属于早期 benchmark 记录,因此本轮选择显式补上 `历史基线(RP-112)` 标注,而不是把历史验证段落改写成新的 benchmark 结果,避免混淆恢复轨迹 + - 当前 `RP-117` 继续沿用 `$gframework-batch-boot 50`,但没有继续把 batch 推回 request dispatch 热路径:本轮先试了一刀“按运行时类型缓存 `IContextAware` 判定”的 dispatcher 微优化,随后按 `RequestBenchmarks` / `RequestLifetimeBenchmarks` 复跑确认 steady-state request 反而回落到约 `71.824 ns`,因此这组运行时代码已在同轮完全撤回,不保留负收益热点实验 + - 这一批改为只收口 notification publisher 的采用文档:`GFramework.Cqrs/README.md` 与 `docs/zh-CN/core/cqrs.md` 现在把 `Sequential` / `TaskWhenAll` / 自定义 publisher 三条策略放进同一张选择矩阵,明确 `TaskWhenAll` 的价值是“并行完成 + 聚合失败”,而不是 fixed fan-out publish 的性能升级开关 + - 当前分支相对 `origin/main`(`7ca21af9`, `2026-05-08 16:12:20 +0800`)的累计 branch diff 仍约为 `12 files`,远低于 `$gframework-batch-boot 50` 的停止阈值;因此这批继续保持 notification 采用边界内的低风险、可评审文档切片 + - 当前 `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 结论保持一致,减少使用者把它误解成默认的性能升级开关 + - 当前 `RP-114` 已继续沿用 `$gframework-batch-boot 50`,并沿着 `RP-113` 刚落地的 notification publisher 能力切片继续补 benchmark:`NotificationFanOutBenchmarks` 现同时纳入 `GFramework.Cqrs` 默认顺序发布器与新内置 `TaskWhenAllNotificationPublisher`,用于量化“能力差距收口后,固定 4 handler fan-out 的成本变化” + - `RP-114` 的 short-job 结果显示:fixed `4 handler` fan-out 下,默认顺序发布器约 `427.453 ns / 408 B`,内置 `TaskWhenAllNotificationPublisher` 约 `472.574 ns / 496 B`,`MediatR` 约 `225.940 ns / 1256 B`,NuGet `Mediator` concrete runtime 约 `3.854 ns / 0 B`;这说明当前内置并行 publisher 的主要价值是语义补齐,而不是 steady-state fan-out 性能收益 + - 这一批保持 runtime 公开 API 与 notification 语义不变,只扩 benchmark 对照口径与恢复文档;原因是 `RP-113` 已经把并行 publisher 能力落到 production path,当前更高价值的是先证明它相对默认顺序发布器、`Mediator` 与 `MediatR` 的成本位置,而不是立即继续扩第二个 publisher strategy + - 当前分支相对 `origin/main` 的累计 branch diff 启动时为 `9 files`,仍明显低于 `$gframework-batch-boot 50` 的停止阈值;这一批继续保持单模块、低风险、可直接评审的 benchmark 边界 + - 当前 `RP-113` 已继续沿用 `$gframework-batch-boot 50`,并把 notification 线从 benchmark 对照推进到实际 runtime 能力:新增公开内置 `TaskWhenAllNotificationPublisher`,让 `GFramework.Cqrs` 在保留默认顺序发布器的同时,提供与 `Mediator` `TaskWhenAllPublisher` 对齐的并行 notification publish 策略 + - `TaskWhenAllNotificationPublisher` 当前语义明确为:零处理器静默完成,单处理器直接透传,多处理器并行启动并等待全部结束;它不保留默认顺序发布器的“首个异常立即停止”语义,而是把全部处理器的失败/取消结果收敛到同一个返回任务 + - 本轮同时补齐 `CqrsNotificationPublisherTests` 对新内置策略的回归,并更新 `GFramework.Cqrs/README.md` 与 `docs/zh-CN/core/cqrs.md`,把切换方式和语义边界写回用户可见文档;当前已提交 branch diff 仍明显低于 `$gframework-batch-boot 50` 的停止阈值 + - 这一批选择真正落一个内置 publisher strategy,而不是继续加 notification benchmark 维度;原因是 `RP-111` / `RP-112` 已经把 notification gap 量化清楚,下一步更高价值的是开始收口“能力差距”而不是继续重复建立对照数据 + - 当前 `RP-112` 已继续沿用 `$gframework-batch-boot 50`,并在 `RP-111` 的单处理器 notification 对照基础上补齐固定 `4 handler` 的 fan-out publish benchmark:新增 `NotificationFanOutBenchmarks`,对比 baseline、`GFramework.Cqrs`、NuGet `Mediator` concrete runtime 与 `MediatR` + - `NotificationFanOutBenchmarks` 当前 short-job 基线约为 baseline `8.302 ns / 0 B`、`Mediator` `4.314 ns / 0 B`、`MediatR` `230.304 ns / 1256 B`、`GFramework.Cqrs` `434.413 ns / 408 B`;这说明 notification fan-out 的差距已经不只体现在单处理器 publish,而是在固定 4 处理器场景下依然保持相近量级 + - 本轮仍然只扩 benchmark 对照口径,没有直接修改 notification runtime 或 publisher 策略语义;原因是当前更高价值的事实是先量化“单处理器”和“固定 fan-out”两条 notification 路径的外部差距,再决定下一批是否值得切进 publisher strategy 或 runtime 热点 + - 当前 `RP-111` 已继续沿用 `$gframework-batch-boot 50`,并按 skill 规则重新以 `origin/main` 作为基线复核:`origin/main` = `7ca21af9`(`2026-05-08 16:12:20 +0800`),本地 `main` = `c2d22285` 已落后,当前分支 `feat/cqrs-optimization` 与 `origin/main` 的累计 branch diff 为 `0 files / 0 lines`;基于“上下文预算优先、单批可评审边界次之”的停止规则,本轮选择 `NotificationBenchmarks` 这一条仍缺 `Mediator` concrete runtime 对照的单模块 benchmark 切片,而不是为了对称性继续扩展 notification runtime seam + - `NotificationBenchmarks` 现已从双方对照扩成三方对照:新增 NuGet `Mediator` source-generated concrete runtime 宿主与 `PublishNotification_Mediator()`,`BenchmarkNotification` / `BenchmarkNotificationHandler` 也同步接上 `Mediator` 的 notification 合同;当前 short-job 基线约为 `Mediator` `1.108 ns / 0 B`、`MediatR` `97.173 ns / 416 B`、`GFramework.Cqrs` `291.582 ns / 392 B` + - 本轮只把“notification publish 的高性能外部对照”补齐到 benchmark 层,而没有直接新增 generated notification invoker/provider 或 runtime 语义调整;原因是 notification dispatch 现有反射委托本就只在首次命中时缓存,继续加一层 provider 对 steady-state publish 的收益信号不如先把 `Mediator` concrete runtime 对照补齐来得清晰 - 当前 `RP-110` 已再次使用 `$gframework-pr-review` 复核 `PR #341` latest-head review:`BenchmarkHostFactory` 的 legacy runtime alias 防守式类型检查、benchmark 宿主定向 generated registry 激活、以及 `CqrsDispatcher.SendAsync(...)` 的 faulted `ValueTask` 失败语义在当前 head 均已实质收口;本轮仅继续接受仍然成立的 CodeRabbit nitpick,为 `SendAsync_Should_Return_Faulted_ValueTask_When_Handler_Is_Missing()` 补齐 `HasRegistration(...)` / `GetAll(...)` 防御性 mock,并删除 trace 中重复 `本轮权威验证` 的 `本轮下一步` 段落 - 当前 `RP-109` 已使用 `$gframework-pr-review` 复核 `PR #341` latest-head review:benchmark 宿主改为定向激活当前场景的 generated registry,避免同一 benchmark 程序集里的其他 registry 扩大冻结服务索引与 `HasRegistration` 基线;`BenchmarkHostFactory` 为 legacy runtime alias 注册补齐防守式类型检查与 stream lifetime 运行时注释;`CqrsDispatcher.SendAsync(...)` 在保留 direct-return 热路径的同时恢复 faulted `ValueTask` 失败语义,并补齐 generated registry 定向接线与 request fault 语义回归测试;`.agents/skills/gframework-batch-boot/SKILL.md` 的 MD005 缩进也已顺手修正 - `GFramework.Cqrs` 已完成对外部 `Mediator` 的生产级替代,当前主线已从“是否可替代”转向“仓库内部收口与能力深化顺序” @@ -114,7 +141,7 @@ CQRS 迁移与收敛。 - 若后续新增 benchmark / example / tooling 项目但未同步校验发布面,solution 级 `dotnet pack` 仍可能在 tag 发布前才暴露异常包 - `RequestStartupBenchmarks` 为了量化真正的单次 cold-start,引入了 `InvocationCount=1` / `UnrollFactor=1` 的专用 job;该配置会触发 BenchmarkDotNet 的 `MinIterationTime` 提示,后续若要做稳定基线比较,还需要决定是否引入批量外层循环或自定义 cold-start harness - 当前 benchmark 宿主仍刻意保持“单根容器最小宿主”模型;若要公平比较 `Scoped` handler 生命周期,需要先引入显式 scope 创建与 scope 内首次解析的对照基线 -- 当前 `Mediator` 对照组仅先接入 steady-state request;若要把 `Transient` / `Scoped` 生命周期矩阵也纳入同一组对照,需要按 `Mediator` 官方 benchmark 的做法拆分 compile-time lifetime build config,而不是在同一编译产物里混用多个 lifetime +- 当前 `Mediator` concrete runtime 对照已覆盖 steady-state request、单处理器 notification publish 与固定 `4 handler` notification fan-out;若要把 `Transient` / `Scoped` 生命周期矩阵、stream 生命周期矩阵或更大 fan-out 矩阵也纳入同一组对照,需要按 `Mediator` 官方 benchmark 的做法拆分 compile-time lifetime / 场景配置,而不是在同一编译产物里混用多个 runtime 变量 - 当前 stream 生命周期矩阵尚未接入 `Mediator` concrete runtime;若要继续对齐 `Mediator` 官方 benchmark 的 compile-time lifetime 设计,需要为 stream 场景补专门的 build-time 配置,而不是在当前统一宿主里临时拼接 - `BenchmarkDotNet.Artifacts/` 现已加入仓库忽略规则;若后续确实需要提交新的基准报告,应显式挑选结果文件或改走文档归档,而不是直接纳入整个生成目录 - 当前 `GFramework.Cqrs` request steady-state 仍慢于 `MediatR`;在“至少超过反射版 `MediatR`”这个阶段目标达成前,任何相关改动都不能只看功能 build/test 结果,必须附带 benchmark 回归数据 @@ -128,6 +155,36 @@ CQRS 迁移与收敛。 ## 最近权威验证 +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*NotificationFanOutBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:本轮对称化 MediatR handler 后,fixed `4 handler` fan-out 对照约为 `Mediator` `3.598 ns / 0 B`、baseline `7.033 ns / 0 B`、`MediatR` `257.533 ns / 1256 B`、`GFramework.Cqrs` 顺序 `409.557 ns / 408 B`、`TaskWhenAll` `484.531 ns / 496 B` +- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/NotificationFanOutBenchmarks.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` + - 结果:通过 + - 备注:仅剩 `GFramework.sln` 的历史 CRLF 提示,无本轮新增 diff 格式问题 +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*NotificationFanOutBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:历史基线(`RP-112`)固定 `4 handler` notification fan-out 对照约为 baseline `8.302 ns / 0 B`、`Mediator` `4.314 ns / 0 B`、`MediatR` `230.304 ns / 1256 B`、`GFramework.Cqrs` `434.413 ns / 408 B` +- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/NotificationFanOutBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md` + - 结果:通过 +- `git diff --check` + - 结果:通过 + - 备注:仅剩 `GFramework.sln` 的历史 CRLF 提示,无本轮新增 diff 格式问题 +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*NotificationBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:notification publish 三方对照当前约为 `Mediator` `1.108 ns / 0 B`、`MediatR` `97.173 ns / 416 B`、`GFramework.Cqrs` `291.582 ns / 392 B` +- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md` + - 结果:通过 +- `git diff --check` + - 结果:通过 + - 备注:仅剩 `GFramework.sln` 的历史 CRLF 提示,无本轮新增 diff 格式问题 - `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` - 结果:通过,`0 warning / 0 error` - 备注:并行验证首轮曾因 `build` 与 `test` 同时写入同一输出 DLL 触发 `MSB3026` 单次复制重试;改为串行重跑同一命令后稳定通过 @@ -140,7 +197,7 @@ CQRS 迁移与收敛。 - 备注:仅剩 `GFramework.sln` 的历史 CRLF 提示,无本轮新增 diff 格式问题 - `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json` - 结果:通过 - - 备注:确认当前分支对应 `PR #341`;latest-head 当前仍显示 `CodeRabbit 2` / `Greptile 2` open thread,但其中 `BenchmarkHostFactory` / benchmark registry / faulted `ValueTask` 三类运行时 thread 已在本地失效,当前仅剩测试 mock 脆弱性与 trace 冗余仍值得继续收口 + - 备注:确认当前分支对应 `PR #342`;latest-head 当前显示 `CodeRabbit 4` / `Greptile 3` open thread,其中真正仍成立的是 benchmark handler 对称性、README / 中文文档示例与恢复文档锚点漂移,其余历史 thread 需要按当前 head 继续甄别 - `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json` - 结果:通过 - 备注:确认当前分支对应 `PR #340`;latest-head 当前显示 `CodeRabbit 2` / `Greptile 2` open thread,且 `CTRF` 报告中唯一失败测试为 `CreateStream_Should_Throw_When_Stream_Pipeline_Behavior_Context_Does_Not_Implement_IArchitectureContext` @@ -335,8 +392,8 @@ CQRS 迁移与收敛。 ## 下一推荐步骤 -1. 当前 turn 已到新的自然批次边界;本次提交后应停止,并在新的 turn 里从 `RP-108` 恢复点继续,而不是在本轮继续启动新的 benchmark 宿主或 runtime 热点切片 -2. 若下一轮继续沿用 `$gframework-batch-boot` 且优先处理性能,先看 notification publish 或更高价值的 request dispatch 常量开销热点,而不是继续堆同层级 benchmark 宿主补齐 +1. 既然 `RP-117` 已把 notification publisher 的采用路径收口成显式策略矩阵,下一轮若继续留在 notification 线,优先评估是否需要补第三种仓库内置策略或更贴近示例代码的采用文档,而不是再重复翻写同一套边界说明 +2. 当前 benchmark 仍证明 `TaskWhenAllNotificationPublisher` 的价值主要在并行完成与异常聚合语义,而不是吞吐收益;若 notification 文档已经足够,下一轮再回到 request dispatch 常量开销时,应先避开“类型级 `IContextAware` 判定缓存”这条已验证无收益的热点假设 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 d2219d2e..0e1361e3 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,209 @@ ## 2026-05-08 +### 阶段:PR #342 latest-head review 收口(CQRS-REWRITE-RP-118) + +- 使用 `$gframework-pr-review` 抓取 `feat/cqrs-optimization` 当前公开 PR,并确认当前锚点已从 `PR #341` 更新为 `PR #342` +- 本轮 latest-head review 结论: + - `CodeRabbit` 当前仍成立的是 `NotificationFanOutBenchmarks.cs` 中 MediatR 显式 `Handle(...)` 直接返回 `Task.CompletedTask`,导致该对照组绕过共享 `HandleCore(...)` 的空值 / 取消校验 + - `CodeRabbit` 对 `ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md` 的两条评论也成立:当前恢复点锚点仍写 `PR #341`,且“最近权威验证”里的 fan-out 数值属于更早轮次,需要显式标注历史来源 + - `Greptile` 额外指出 `GFramework.Cqrs/README.md` 与 `docs/zh-CN/core/cqrs.md` 里 `UseTaskWhenAllNotificationPublisher()` 示例包含多余 `using GFramework.Cqrs.Notification;`;这条在当前 head 仍成立 + - MegaLinter 仍报告 `dotnet-format` restore 失败,但这属于 CI 环境 restore 噪声,不是当前 diff 的格式违规;README 的 MD058 空行问题仍需在本地直接修复 +- 本轮主线程决策: + - 让 `NotificationFanOutBenchmarks` 的四个 MediatR handler 显式转发到 `HandleCore(notification, cancellationToken).AsTask()`,保持与 baseline、`GFramework.Cqrs` 和 NuGet `Mediator` 分支一致的前置检查 + - 在 `GFramework.Cqrs/README.md` 修复表格前后空行,并删除 README / 中文文档中 `UseTaskWhenAllNotificationPublisher()` 示例的多余 `using` + - 把 `cqrs-rewrite` tracking 当前恢复点推进到 `RP-118`,同步 `PR #342` 锚点,并把早期 fan-out 数值显式标成 `历史基线(RP-112)` +- 本轮权威验证: + - `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json` + - 结果:通过 + - 备注:确认当前分支对应 `PR #342`;CodeRabbit 当前 `4` 条 actionable comments 与 Greptile `3` 条 open thread 已作为本轮本地复核输入 + - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*NotificationFanOutBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:本轮对称化 MediatR handler 后,fixed `4 handler` fan-out 对照约为 `Mediator` `3.598 ns / 0 B`、baseline `7.033 ns / 0 B`、`MediatR` `257.533 ns / 1256 B`、`GFramework.Cqrs` 顺序 `409.557 ns / 408 B`、`TaskWhenAll` `484.531 ns / 496 B` + - `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/NotificationFanOutBenchmarks.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` + - 结果:通过 + - 备注:仅剩 `GFramework.sln` 的历史 CRLF 提示,无本轮新增 diff 格式问题 + +### 阶段:notification publisher 采用矩阵文档收口(CQRS-REWRITE-RP-117) + +- 延续 `$gframework-batch-boot 50`,本轮没有继续把自动批处理推到新的 runtime seam,而是先按 tracking 建议复核“notification 线是否还缺采用边界文档”: + - 当前分支相对 `origin/main`(`7ca21af9`, `2026-05-08 16:12:20 +0800`)的累计 branch diff 约为 `12 files`,仍明显低于 `50` 文件阈值 + - 主线程先短试了一刀 request dispatch 热路径微优化:把 dispatcher 中“运行时类型是否实现 `IContextAware`”改成弱键缓存,并按性能治理规则复跑 `RequestBenchmarks` 与 `RequestLifetimeBenchmarks` + - 复跑结果表明这条假设没有正收益:默认 steady-state request 回到约 `71.824 ns / 32 B`,`Singleton / Transient` lifetime 约为 `73.191 ns / 32 B` 与 `80.468 ns / 56 B`,因此本轮在同一提交前已完全撤回该运行时代码实验,不把负收益热点带进后续恢复点 +- 本轮主线程决策: + - 保持 `GFramework.Cqrs` runtime 与测试代码不变,只更新 `GFramework.Cqrs/README.md` 与 `docs/zh-CN/core/cqrs.md` + - 把 `SequentialNotificationPublisher`、`TaskWhenAllNotificationPublisher` 与 `UseNotificationPublisher(...)` 自定义实例三条路径收口到同一张策略矩阵 + - 在用户文档里明确 `TaskWhenAllNotificationPublisher` 是“并行完成 + 聚合失败”语义策略,而不是 fixed fan-out publish 的性能开关 +- 本轮权威验证: + - `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~CqrsDispatcherCacheTests|FullyQualifiedName~CqrsDispatcherContextValidationTests"` + - 结果:通过,`17/17` passed + - 备注:首轮与 build 并行触发时出现 `MSB3026` 单次复制重试告警,但同一命令最终稳定通过,未形成代码失败 + - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*RequestBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:用于否决本轮已撤回的热点假设;默认 steady-state request 对照约为 baseline `5.853 ns / 32 B`、`Mediator` `6.256 ns / 32 B`、`MediatR` `53.401 ns / 232 B`、`GFramework.Cqrs` `71.824 ns / 32 B` + - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:用于否决本轮已撤回的热点假设;`Singleton` 下 baseline / `MediatR` / `GFramework.Cqrs` 约 `5.259 ns / 58.415 ns / 73.191 ns`,`Transient` 下约 `4.914 ns / 57.150 ns / 80.468 ns` +- 本轮结论: + - notification publisher 公开入口现在不仅有显式顺序 / 并行 API,也有更直接的策略选择矩阵;读者不再需要从分散段落里拼装“什么时候该选哪条策略” + - request dispatch 热路径的下一轮探索应显式绕开“类型级 `IContextAware` 判定缓存”这一条已验证无收益的方向,把 context budget 留给更可能影响 steady-state 的热点 + - 当前仍可继续自动推进,但若再开一批 runtime 性能实验,应放在新的自然批次里,避免把已否决假设和新热点混在同一评审单元中 + +### 阶段:公开顺序 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` 已明确的性能/语义事实继续收口用户接入缺口: + - 当前分支相对 `origin/main`(`7ca21af9`, `2026-05-08 16:12:20 +0800`)的累计 branch diff 在启动时仍为 `9 files`,明显低于 `50` 文件阈值 + - `RP-113` / `RP-114` 已证明内置 `TaskWhenAllNotificationPublisher` 的价值主要是语义补齐,但当前用户若要采用它,仍需知道 `INotificationPublisher` 的底层注册细节 +- 本轮主线程决策: + - 新增 `GFramework.Cqrs/Extensions/NotificationPublisherRegistrationExtensions.cs`,提供 `UseNotificationPublisher(...)`、`UseNotificationPublisher()` 与 `UseTaskWhenAllNotificationPublisher()` 三个显式组合根入口 + - 在 `GFramework.Cqrs.Tests/Cqrs/NotificationPublisherRegistrationExtensionsTests.cs` 补齐回归,确认默认 runtime 基础设施会复用 `UseTaskWhenAllNotificationPublisher()`,且重复策略注册会在组合根阶段被显式阻止 + - 更新 `GFramework.Cqrs/README.md` 与 `docs/zh-CN/core/cqrs.md`,把推荐用法改成组合根扩展,并把 `RP-114` 的 benchmark 结论翻译成用户可用的采用边界 +- 本轮权威验证: + - `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"` + - 结果:通过,`9/9` passed + - `python3 scripts/license-header.py --check --paths 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 的采用路径从“理解内部 seam”收口成“在组合根里显式选择策略”,并让重复策略注册在配置阶段就得到清晰失败信号 + - 若后续仍继续 notification 线,更合理的下一刀会是补第二个内置策略或更细的采用文档,而不是继续要求用户手写容器底层注册 + +### 阶段:`TaskWhenAll` notification publisher fan-out benchmark(CQRS-REWRITE-RP-114) + +- 延续 `$gframework-batch-boot 50`,本轮不再扩新的 notification runtime 能力,而是沿着 `RP-113` 刚落地的内置并行 publisher 继续补验证口径: + - 当前分支相对 `origin/main`(`7ca21af9`, `2026-05-08 16:12:20 +0800`)的累计 branch diff 启动时为 `9 files`,明显低于 `50` 文件阈值 + - `RP-112` 只量化了默认顺序发布器的 fixed `4 handler` fan-out 成本;`RP-113` 已把 `TaskWhenAllNotificationPublisher` 引入 production runtime,但还没有 benchmark 说明“能力差距收口后,代价是多少” +- 本轮主线程决策: + - 在 `GFramework.Cqrs.Benchmarks/Messaging/NotificationFanOutBenchmarks.cs` 同时保留 `baseline`、默认顺序 `GFramework.Cqrs`、内置 `TaskWhenAllNotificationPublisher`、NuGet `Mediator` concrete runtime 与 `MediatR` 五组对照 + - 复用同一个冻结 `MicrosoftDiContainer` 创建两个 `ICqrsRuntime`,确保变量集中在 notification publisher 策略,而不是 handler 注册或容器形状差异 + - 更新 `GFramework.Cqrs.Benchmarks/README.md` 与 active tracking,使默认恢复入口直接记录新的 benchmark 口径 +- 本轮权威验证: + - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*NotificationFanOutBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:fixed `4 handler` fan-out 对照当前约为 baseline `7.424 ns / 0 B`、`Mediator` `3.854 ns / 0 B`、`MediatR` `225.940 ns / 1256 B`、`GFramework.Cqrs` 默认顺序发布器 `427.453 ns / 408 B`、内置 `TaskWhenAllNotificationPublisher` `472.574 ns / 496 B` + - `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/NotificationFanOutBenchmarks.cs GFramework.Cqrs.Benchmarks/README.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` + - 结果:通过 +- 本轮结论: + - 当前 benchmark 说明 `TaskWhenAllNotificationPublisher` 的主要价值是补齐“等待全部处理器并聚合异常”的 notification 语义,而不是在 fixed `4 handler` fan-out steady-state 下带来吞吐收益;它比默认顺序发布器额外增加了约 `45 ns` 与 `88 B` + - 这组结果足以支持后续把 notification 线的重心转回 API 配置面、使用边界与文档语义,而不是继续机械堆新的 runtime seam 或期待 `TaskWhenAll` 自带性能红利 + - 当前 turn 仍可继续自动推进,但默认停止规则仍以“上下文预算优先、单批可评审边界次之”为准 + +### 阶段:内置 `TaskWhenAll` notification publisher(CQRS-REWRITE-RP-113) + +- 延续 `$gframework-batch-boot 50`,本轮不再继续堆 notification benchmark 维度,而是直接把上一批已经量化清楚的 capability gap 收口到 runtime: + - `RP-111` / `RP-112` 已证明当前 notification publish 无论单处理器还是固定 fan-out,都和 `Mediator` 的 publish strategy 能力差距相关,而不只是“缺 benchmark” + - 当前分支相对 `origin/main` 的累计 branch diff 仍明显低于 `50` 文件阈值,因此适合用一个单模块、可回归、可文档化的能力切片继续自动推进 +- 本轮主线程决策: + - 新增 `GFramework.Cqrs/Notification/TaskWhenAllNotificationPublisher.cs`,提供公开内置并行 notification publisher,并把“同步抛出的处理器异常也收敛到返回任务中”作为实现约束 + - 在 `GFramework.Cqrs.Tests/Cqrs/CqrsNotificationPublisherTests.cs` 补齐针对新策略的回归,确认它不会像默认顺序发布器那样在首个失败处停止其余处理器 + - 更新 `GFramework.Cqrs/README.md` 与 `docs/zh-CN/core/cqrs.md`,写明切换方式,以及“不保证顺序 / 等待全部处理器完成 / 统一暴露异常或取消结果”的采用边界 +- 本轮权威验证: + - `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~CqrsNotificationPublisherTests"` + - 结果:通过 + - `python3 scripts/license-header.py --check --paths GFramework.Cqrs/Notification/TaskWhenAllNotificationPublisher.cs GFramework.Cqrs.Tests/Cqrs/CqrsNotificationPublisherTests.cs GFramework.Cqrs/README.md docs/zh-CN/core/cqrs.md` + - 结果:通过 + - `git diff --check` + - 结果:通过 +- 本轮结论: + - `GFramework.Cqrs` 现在不再只有“自定义 seam”这一种 notification publisher 扩展方式,而是先提供了一个仓库维护的内置并行策略,开始实质缩小和 `Mediator` 在 publisher strategy 上的能力差距 + - 这批改动保持默认顺序语义不变,因此风险主要落在“新策略的异常聚合和用户理解边界”,已通过测试和文档同步收口 + - 当前可以继续自动推进,但更合理的下一批应优先补新策略的 benchmark 或继续评估 notification publisher 配置面,而不是回头重复扩更多 fan-out benchmark + +### 阶段:notification fan-out publish benchmark(CQRS-REWRITE-RP-112) + +- 延续 `$gframework-batch-boot 50`,本轮没有直接切入 notification runtime 或 publisher strategy,而是先补齐固定 `4 handler` 的 fan-out publish 对照: + - `RP-111` 已量化单处理器 notification publish,但还缺“同一路径在固定多处理器 fan-out 时是否保持同级差距”的事实 + - 继续机械扩充 `HandlerCount` 参数矩阵会把 `Mediator` compile-time 处理器集合、MediatR 扫描过滤与 benchmark 注册变量混在一起;固定 `4 handler` 场景更容易保持三方对照口径稳定 +- 本轮主线程决策: + - 新增 `GFramework.Cqrs.Benchmarks/Messaging/NotificationFanOutBenchmarks.cs`,固定 4 个 handler,比对 baseline、`GFramework.Cqrs`、NuGet `Mediator` concrete runtime 与 `MediatR` 的 publish 开销 + - 让 baseline 直接顺序调用 4 个 handler,避免把 fan-out 的额外调用成本误归因为框架 dispatch 自身 + - 更新 `GFramework.Cqrs.Benchmarks/README.md`,明确 notification benchmark 现在同时覆盖单处理器与固定 4 处理器 fan-out 场景 +- 本轮权威验证: + - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*NotificationFanOutBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:历史基线(`RP-112`)固定 `4 handler` notification fan-out 对照约为 baseline `8.302 ns / 0 B`、`Mediator` `4.314 ns / 0 B`、`MediatR` `230.304 ns / 1256 B`、`GFramework.Cqrs` `434.413 ns / 408 B` + - `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/NotificationFanOutBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md` + - 结果:通过 + - `git diff --check` + - 结果:通过 + - 备注:仅剩 `GFramework.sln` 的历史 CRLF 提示,无本轮新增 diff 格式问题 +- 本轮结论: + - notification 路径现在已同时具备“单处理器 publish”与“固定 4 处理器 fan-out publish”两条三方对照基线,足以支撑后续是否值得切进 publisher strategy 或 runtime 热点 + - 当前更有价值的下一步不是继续横向堆更多 fan-out 场景,而是转向 publisher strategy / 异常语义,或回到 request dispatch 常量开销这类更可能产生真实运行时收益的切片 + - 在 branch diff 仍明显低于阈值时可以继续自动推进,但应把“上下文预算接近约 80%”继续视为优先停止信号 + +### 阶段:notification publish 补齐 `Mediator` concrete runtime 对照(CQRS-REWRITE-RP-111) + +- 延续 `$gframework-batch-boot 50`,本轮重新按 skill 规则复核 branch diff 基线与容量: + - `origin/main` = `7ca21af9`,提交时间 `2026-05-08 16:12:20 +0800` + - 本地 `main` = `c2d22285`,已落后于 remote-tracking ref,因此不作为本轮 baseline + - 当前分支 `feat/cqrs-optimization` 与 `origin/main` 的累计 branch diff 为 `0 files / 0 lines` + - 当前工作树干净,且上一个自然批次 `RP-110` 已并入 `origin/main`;因此本轮不是“续做未提交热路径”,而是基于 active topic 重新选择下一块低风险 CQRS benchmark 切片 +- 本轮接受的只读探索结论: + - `NotificationBenchmarks` 仍停留在 `GFramework.Cqrs` vs `MediatR` 的双方对照,缺少 request steady-state 已具备的 `Mediator` concrete runtime 高性能参照物 + - 对 notification 路径直接补 generated invoker/provider 的性价比不高:dispatcher 当前对 notification 反射委托已按消息类型弱缓存,steady-state publish 的主要差距不在“每次都反射” + - 因此本轮更高信号、边界更清晰的切片是先补 benchmark 对照口径,而不是为了对称性新增一层 runtime seam +- 本轮主线程决策: + - 在 `GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs` 新增 `Mediator` concrete runtime 宿主、`PublishNotification_Mediator()` benchmark 方法,以及对应的 `Mediator.INotification` / `Mediator.INotificationHandler` 合同实现 + - 保持现有 `GFramework.Cqrs` 与 `MediatR` notification publish 路径不变,只扩充对照组,确保这批仍然是单模块、低风险、可直接评审的 benchmark 收口 + - 更新 `GFramework.Cqrs.Benchmarks/README.md` 与 active tracking,使 notification 场景的公开说明和恢复入口都反映新的三方对照事实 +- 本轮权威验证: + - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*NotificationBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:notification publish 三方对照当前约为 `Mediator` `1.108 ns / 0 B`、`MediatR` `97.173 ns / 416 B`、`GFramework.Cqrs` `291.582 ns / 392 B` + - `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md` + - 结果:通过 + - `git diff --check` + - 结果:通过 + - 备注:仅剩 `GFramework.sln` 的历史 CRLF 提示,无本轮新增 diff 格式问题 +- 本轮结论: + - notification 场景现在也拥有了与 request steady-state 对称的 `Mediator` concrete runtime 参照物,后续再讨论 `notification publisher` 策略或 runtime 热点时,不再只能拿 `MediatR` 做外部对照 + - 当前最值得保留的结论不是“立刻给 notification 也上 generated invoker/provider”,而是 `GFramework.Cqrs` 单处理器 publish 相对 `Mediator` 与 `MediatR` 的量级差距已经被量化出来,可为后续是否继续压 notification 路径提供依据 + - 本轮到这里属于新的自然批次边界;下一轮若继续沿用 `$gframework-batch-boot 50`,更适合从多处理器 publish / publisher strategy 或更高价值的 request 常量开销热点里再选一块,而不是在同一 turn 里继续堆 notification 基准扩展 + ### 阶段:PR #341 latest-head review 尾声收口(CQRS-REWRITE-RP-110) - 再次使用 `$gframework-pr-review` 抓取 `PR #341` latest-head review,确认当前 open thread 已收敛到: diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 7c5d2228..33056778 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -117,7 +117,48 @@ var playerId = await architecture.Context.SendRequestAsync( - 零处理器时静默完成 - 已解析处理器按容器顺序逐个执行 - 首个处理器抛出异常时立即停止后续分发 -- 如果容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置顺序发布器 +- 如果容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置 `SequentialNotificationPublisher` + +如果你需要在组合根里明确表达“为什么选这条策略”,可以按下面的矩阵判断: + +| 策略 | 适用场景 | 顺序语义 | 失败语义 | 备注 | +| --- | --- | --- | --- | --- | +| `UseSequentialNotificationPublisher()` | 需要保持容器顺序,且希望首个失败立即停止 | 保证按容器顺序执行 | 首个处理器异常会中断后续处理器 | 这也是默认回退策略 | +| `UseTaskWhenAllNotificationPublisher()` | 需要让全部处理器并行完成,再统一观察异常或取消 | 不保证顺序 | 不会在首个失败时中断其余处理器;全部结束后统一暴露结果 | 更适合语义补齐,不是性能优化开关 | +| `UseNotificationPublisher(...)` | 需要接入自定义或第三方 publisher 策略 | 取决于实现 | 取决于实现 | 仅在内置顺序 / 并行策略都不满足时使用 | + +如果你想在组合根里显式保留默认顺序语义,也可以直接写成: + +```csharp +using GFramework.Cqrs.Extensions; + +container.UseSequentialNotificationPublisher(); +``` + +如果你需要等待所有通知处理器并行完成,而不是沿用默认顺序语义,可以显式切换到内置 +`TaskWhenAllNotificationPublisher`: + +```csharp +using GFramework.Cqrs.Extensions; + +container.UseTaskWhenAllNotificationPublisher(); +``` + +这条策略的边界也需要明确: + +- 不保证处理器执行顺序 +- 不会在首个处理器失败时立即停止其余处理器 +- 会在全部处理器结束后统一暴露异常或取消结果 +- 当前 fixed `4 handler` fan-out benchmark 中,它的 steady-state 成本也高于默认顺序发布器;因此它更适合“我要并行语义”,而不是“我要更快的 publish” + +如果你需要显式提供自定义 publisher 实例,而不是直接采用内置 `TaskWhenAll` 策略,也可以在组合根里写成: + +```csharp +using GFramework.Cqrs.Extensions; +using GFramework.Cqrs.Notification; + +container.UseNotificationPublisher(new TaskWhenAllNotificationPublisher()); +``` ## Request 与流式变体