From 96729ddcf10ba75895bb6649737042ee1e38cf24 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 6 May 2026 08:57:59 +0800 Subject: [PATCH] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E5=85=85=E5=9F=BA?= =?UTF-8?q?=E5=87=86=E4=B8=8E=E7=94=9F=E6=88=90=E5=99=A8=E5=9B=9E=E5=BD=92?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增独立的 GFramework.Cqrs.Benchmarks 项目并引入 request、notification 对比场景 - 补充 request 与 stream invoker provider 的 mixed direct/reflected 顺序回归测试 - 更新 solution、meta-package 排除规则与 CQRS ai-plan 恢复点 --- GFramework.Cqrs.Benchmarks/CustomColumn.cs | 71 +++++ .../GFramework.Cqrs.Benchmarks.csproj | 29 ++ .../Messaging/BenchmarkContext.cs | 17 + .../Messaging/Fixture.cs | 35 ++ .../Messaging/NotificationBenchmarks.cs | 136 ++++++++ .../Messaging/RequestBenchmarks.cs | 154 +++++++++ GFramework.Cqrs.Benchmarks/Program.cs | 23 ++ GFramework.Cqrs.Benchmarks/README.md | 35 ++ .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 298 ++++++++++++++++++ GFramework.csproj | 3 + GFramework.sln | 14 + .../todos/cqrs-rewrite-migration-tracking.md | 16 +- .../traces/cqrs-rewrite-migration-trace.md | 84 +++++ 13 files changed, 912 insertions(+), 3 deletions(-) create mode 100644 GFramework.Cqrs.Benchmarks/CustomColumn.cs create mode 100644 GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/BenchmarkContext.cs create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/Fixture.cs create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs create mode 100644 GFramework.Cqrs.Benchmarks/Program.cs create mode 100644 GFramework.Cqrs.Benchmarks/README.md diff --git a/GFramework.Cqrs.Benchmarks/CustomColumn.cs b/GFramework.Cqrs.Benchmarks/CustomColumn.cs new file mode 100644 index 00000000..23a73f2e --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/CustomColumn.cs @@ -0,0 +1,71 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; +using System; + +namespace GFramework.Cqrs.Benchmarks; + +/// +/// 为 CQRS benchmark 结果补充可读的场景标签列。 +/// +/// 列名。 +/// 从 benchmark case 提取列值的委托。 +public sealed class CustomColumn(string columnName, Func getValue) : IColumn +{ + /// + public string Id => $"{nameof(CustomColumn)}.{ColumnName}"; + + /// + public string ColumnName { get; } = columnName; + + /// + public bool AlwaysShow => true; + + /// + public ColumnCategory Category => ColumnCategory.Params; + + /// + public int PriorityInCategory => 0; + + /// + public bool IsNumeric => false; + + /// + public UnitType UnitType => UnitType.Dimensionless; + + /// + public string Legend => $"Custom '{ColumnName}' tag column"; + + /// + public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) + { + return false; + } + + /// + public bool IsAvailable(Summary summary) + { + return true; + } + + /// + public string GetValue(Summary summary, BenchmarkCase benchmarkCase) + { + return getValue(summary, benchmarkCase); + } + + /// + public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) + { + return GetValue(summary, benchmarkCase); + } + + /// + public override string ToString() + { + return ColumnName; + } +} diff --git a/GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj b/GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj new file mode 100644 index 00000000..6dd185ba --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj @@ -0,0 +1,29 @@ + + + + + + Exe + net10.0 + disable + enable + false + + + + + + + + + + + + + + + + diff --git a/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkContext.cs b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkContext.cs new file mode 100644 index 00000000..ff0efb46 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkContext.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Benchmarks.Messaging; + +/// +/// 为纯 runtime benchmark 提供最小 CQRS 上下文标记,避免把完整架构上下文初始化成本混入 steady-state dispatch。 +/// +internal sealed class BenchmarkContext : ICqrsContext +{ + /// + /// 共享的最小 CQRS 上下文实例。 + /// + public static BenchmarkContext Instance { get; } = new(); +} diff --git a/GFramework.Cqrs.Benchmarks/Messaging/Fixture.cs b/GFramework.Cqrs.Benchmarks/Messaging/Fixture.cs new file mode 100644 index 00000000..de42f49f --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/Fixture.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using BenchmarkDotNet.Loggers; +using System; + +namespace GFramework.Cqrs.Benchmarks.Messaging; + +/// +/// 为 CQRS benchmark 运行打印并验证当前场景配置,避免矩阵配置与实际运行环境漂移。 +/// +internal static class Fixture +{ + /// + /// 输出当前 benchmark 配置并验证关键环境变量。 + /// + /// 当前 benchmark 场景名称。 + /// 当前场景的处理器数量。 + /// 当前场景的 pipeline 行为数量。 + public static void Setup(string scenario, int handlerCount, int pipelineCount) + { + ConsoleLogger.Default.WriteLineHeader("GFramework.Cqrs benchmark config"); + ConsoleLogger.Default.WriteLineInfo($"Scenario = {scenario}"); + ConsoleLogger.Default.WriteLineInfo($"HandlerCount = {handlerCount}"); + ConsoleLogger.Default.WriteLineInfo($"PipelineCount = {pipelineCount}"); + + var environmentScenario = Environment.GetEnvironmentVariable("GFRAMEWORK_CQRS_BENCHMARK_SCENARIO"); + if (!string.IsNullOrWhiteSpace(environmentScenario) && + !string.Equals(environmentScenario, scenario, StringComparison.Ordinal)) + { + throw new InvalidOperationException( + $"Scenario mismatch. Expected '{environmentScenario}', actual '{scenario}'."); + } + } +} diff --git a/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs new file mode 100644 index 00000000..7cc98c74 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs @@ -0,0 +1,136 @@ +// 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 MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace GFramework.Cqrs.Benchmarks.Messaging; + +/// +/// 对比单处理器 notification 在 GFramework.CQRS 与 MediatR 之间的 publish 开销。 +/// +[Config(typeof(Config))] +public class NotificationBenchmarks +{ + private MicrosoftDiContainer _container = null!; + private ICqrsRuntime _runtime = null!; + private ServiceProvider _serviceProvider = null!; + private IPublisher _publisher = null!; + private BenchmarkNotification _notification = null!; + + /// + /// 配置 notification benchmark 的公共输出格式。 + /// + private sealed class Config : ManualConfig + { + public Config() + { + AddJob(Job.Default); + AddColumnProvider(DefaultColumnProviders.Instance); + AddColumn(new CustomColumn("Scenario", static (_, _) => "Notification")); + AddDiagnoser(MemoryDiagnoser.Default); + WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared)); + } + } + + /// + /// 构建 notification publish 所需的最小 runtime 宿主和对照对象。 + /// + [GlobalSetup] + public void Setup() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider + { + MinLevel = LogLevel.Fatal + }; + Fixture.Setup("Notification", handlerCount: 1, pipelineCount: 0); + + _container = new MicrosoftDiContainer(); + _container.RegisterTransient, BenchmarkNotificationHandler>(); + _runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( + _container, + LoggerFactoryResolver.Provider.CreateLogger(nameof(NotificationBenchmarks))); + + var services = new ServiceCollection(); + services.AddSingleton, BenchmarkNotificationHandler>(); + services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(NotificationBenchmarks).Assembly)); + _serviceProvider = services.BuildServiceProvider(); + _publisher = _serviceProvider.GetRequiredService(); + + _notification = new BenchmarkNotification(Guid.NewGuid()); + } + + /// + /// 释放 MediatR 对照组使用的 DI 宿主。 + /// + [GlobalCleanup] + public void Cleanup() + { + _serviceProvider.Dispose(); + } + + /// + /// 通过 GFramework.CQRS runtime 发布 notification。 + /// + [Benchmark(Baseline = true)] + public ValueTask PublishNotification_GFrameworkCqrs() + { + return _runtime.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None); + } + + /// + /// 通过 MediatR 发布 notification,作为外部设计对照。 + /// + [Benchmark] + public Task PublishNotification_MediatR() + { + return _publisher.Publish(_notification, CancellationToken.None); + } + + /// + /// Benchmark notification。 + /// + /// 通知标识。 + public sealed record BenchmarkNotification(Guid Id) : + GFramework.Cqrs.Abstractions.Cqrs.INotification, + MediatR.INotification; + + /// + /// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 notification handler。 + /// + public sealed class BenchmarkNotificationHandler : + GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler, + MediatR.INotificationHandler + { + /// + /// 处理 GFramework.CQRS notification。 + /// + public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } + + /// + /// 处理 MediatR notification。 + /// + Task MediatR.INotificationHandler.Handle( + BenchmarkNotification notification, + CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs new file mode 100644 index 00000000..e925f794 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs @@ -0,0 +1,154 @@ +// 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 MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace GFramework.Cqrs.Benchmarks.Messaging; + +/// +/// 对比单个 request 在直接调用、GFramework.CQRS runtime 与 MediatR 之间的 steady-state dispatch 开销。 +/// +[Config(typeof(Config))] +public class RequestBenchmarks +{ + private MicrosoftDiContainer _container = null!; + private ICqrsRuntime _runtime = null!; + private ServiceProvider _serviceProvider = null!; + private IMediator _mediatr = null!; + private BenchmarkRequestHandler _baselineHandler = null!; + private BenchmarkRequest _request = null!; + + /// + /// 配置 request benchmark 的公共输出格式。 + /// + private sealed class Config : ManualConfig + { + public Config() + { + AddJob(Job.Default); + AddColumnProvider(DefaultColumnProviders.Instance); + AddColumn(new CustomColumn("Scenario", static (_, _) => "Request")); + AddDiagnoser(MemoryDiagnoser.Default); + WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared)); + } + } + + /// + /// 构建 request dispatch 所需的最小 runtime 宿主和对照对象。 + /// + [GlobalSetup] + public void Setup() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider + { + MinLevel = LogLevel.Fatal + }; + Fixture.Setup("Request", handlerCount: 1, pipelineCount: 0); + + _container = new MicrosoftDiContainer(); + _baselineHandler = new BenchmarkRequestHandler(); + + _container.RegisterTransient, BenchmarkRequestHandler>(); + _runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( + _container, + LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestBenchmarks))); + + var services = new ServiceCollection(); + services.AddSingleton, BenchmarkRequestHandler>(); + services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(RequestBenchmarks).Assembly)); + _serviceProvider = services.BuildServiceProvider(); + _mediatr = _serviceProvider.GetRequiredService(); + + _request = new BenchmarkRequest(Guid.NewGuid()); + } + + /// + /// 释放 MediatR 对照组使用的 DI 宿主。 + /// + [GlobalCleanup] + public void Cleanup() + { + _serviceProvider.Dispose(); + } + + /// + /// 直接调用 handler,作为 dispatch 额外开销的 baseline。 + /// + [Benchmark(Baseline = true)] + public ValueTask SendRequest_Baseline() + { + return _baselineHandler.Handle(_request, CancellationToken.None); + } + + /// + /// 通过 GFramework.CQRS runtime 发送 request。 + /// + [Benchmark] + public ValueTask SendRequest_GFrameworkCqrs() + { + return _runtime.SendAsync(BenchmarkContext.Instance, _request, CancellationToken.None); + } + + /// + /// 通过 MediatR 发送 request,作为外部设计对照。 + /// + [Benchmark] + public Task SendRequest_MediatR() + { + return _mediatr.Send(_request, CancellationToken.None); + } + + /// + /// Benchmark request。 + /// + /// 请求标识。 + public sealed record BenchmarkRequest(Guid Id) : + GFramework.Cqrs.Abstractions.Cqrs.IRequest, + MediatR.IRequest; + + /// + /// Benchmark response。 + /// + /// 响应标识。 + public sealed record BenchmarkResponse(Guid Id); + + /// + /// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 request handler。 + /// + public sealed class BenchmarkRequestHandler : + GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler, + MediatR.IRequestHandler + { + /// + /// 处理 GFramework.CQRS request。 + /// + public ValueTask Handle(BenchmarkRequest request, CancellationToken cancellationToken) + { + return ValueTask.FromResult(new BenchmarkResponse(request.Id)); + } + + /// + /// 处理 MediatR request。 + /// + Task MediatR.IRequestHandler.Handle( + BenchmarkRequest request, + CancellationToken cancellationToken) + { + return Task.FromResult(new BenchmarkResponse(request.Id)); + } + } +} diff --git a/GFramework.Cqrs.Benchmarks/Program.cs b/GFramework.Cqrs.Benchmarks/Program.cs new file mode 100644 index 00000000..44324baa --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Program.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Running; + +namespace GFramework.Cqrs.Benchmarks; + +/// +/// 提供 GFramework.CQRS benchmark 的统一命令行入口。 +/// +internal static class Program +{ + /// + /// 运行当前程序集中的全部 benchmark。 + /// + /// 透传给 BenchmarkDotNet 的命令行参数。 + private static void Main(string[] args) + { + ConsoleLogger.Default.WriteLine("Running GFramework.Cqrs benchmarks"); + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + } +} diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md new file mode 100644 index 00000000..3ea73f65 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/README.md @@ -0,0 +1,35 @@ +# GFramework.Cqrs.Benchmarks + +该模块承载 `GFramework.Cqrs` 的独立性能基准工程,用于持续比较运行时 dispatch、publish、cold-start 与后续 generator / pipeline 收口的成本变化。 + +## 目的 + +- 为 `GFramework.Cqrs` 建立独立于 NUnit 集成测试的 BenchmarkDotNet 基线 +- 参考 `ai-libs/Mediator/benchmarks` 的场景组织方式,逐步补齐 request、notification、stream 与初始化成本对比 +- 为后续吸收 `Mediator` 的 dispatch 设计、fixture 组织和对比矩阵提供可重复验证入口 + +## 当前内容 + +- `Program.cs` + - benchmark 命令行入口 +- `Messaging/Fixture.cs` + - 运行前输出并校验场景配置 +- `Messaging/RequestBenchmarks.cs` + - direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比 +- `Messaging/NotificationBenchmarks.cs` + - `GFramework.Cqrs` runtime 与 `MediatR` 的单处理器 notification publish 对比 + +## 最小使用方式 + +```bash +dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release +``` + +也可以通过 `BenchmarkDotNet` 过滤器只运行某一类场景。 + +## 后续扩展方向 + +- pipeline behavior 数量矩阵 +- generated invoker provider 与纯反射 dispatch 对比 +- stream request benchmark +- cold-start 与 registration 成本对比 diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 545577f2..8d100aa8 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -2003,6 +2003,120 @@ public class CqrsHandlerRegistryGeneratorTests } """; + private const string MixedRequestInvokerProviderSource = """ + using System; + using System.Collections.Generic; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest + { + ValueTask Handle(TRequest request, CancellationToken cancellationToken); + } + + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + public interface ICqrsRequestInvokerProvider + { + bool TryGetDescriptor(Type requestType, Type responseType, out CqrsRequestInvokerDescriptor? descriptor); + } + + public interface IEnumeratesCqrsRequestInvokerDescriptors + { + IReadOnlyList GetDescriptors(); + } + + public sealed class CqrsRequestInvokerDescriptor + { + public CqrsRequestInvokerDescriptor(Type handlerType, MethodInfo invokerMethod) { } + } + + public sealed class CqrsRequestInvokerDescriptorEntry + { + public CqrsRequestInvokerDescriptorEntry(Type requestType, Type responseType, CqrsRequestInvokerDescriptor descriptor) + { + RequestType = requestType; + ResponseType = responseType; + Descriptor = descriptor; + } + + public Type RequestType { get; } + + public Type ResponseType { get; } + + public CqrsRequestInvokerDescriptor Descriptor { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record AlphaRequest() : IRequest; + + public sealed record BetaRequest() : IRequest; + + public sealed class AlphaHandler : IRequestHandler + { + public ValueTask Handle(AlphaRequest request, CancellationToken cancellationToken) + { + return ValueTask.FromResult("alpha"); + } + } + + public sealed class Container + { + private sealed class HiddenBetaHandler : IRequestHandler + { + public ValueTask Handle(BetaRequest request, CancellationToken cancellationToken) + { + return ValueTask.FromResult(42); + } + } + } + } + """; + private const string HiddenImplementationStreamInvokerProviderSource = """ using System; using System.Collections.Generic; @@ -2108,6 +2222,122 @@ public class CqrsHandlerRegistryGeneratorTests } """; + private const string MixedStreamInvokerProviderSource = """ + using System; + using System.Collections.Generic; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + + public interface IStreamRequestHandler where TRequest : IStreamRequest + { + IAsyncEnumerable Handle(TRequest request, CancellationToken cancellationToken); + } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + public interface ICqrsStreamInvokerProvider + { + bool TryGetDescriptor(Type requestType, Type responseType, out CqrsStreamInvokerDescriptor? descriptor); + } + + public interface IEnumeratesCqrsStreamInvokerDescriptors + { + IReadOnlyList GetDescriptors(); + } + + public sealed class CqrsStreamInvokerDescriptor + { + public CqrsStreamInvokerDescriptor(Type handlerType, MethodInfo invokerMethod) { } + } + + public sealed class CqrsStreamInvokerDescriptorEntry + { + public CqrsStreamInvokerDescriptorEntry(Type requestType, Type responseType, CqrsStreamInvokerDescriptor descriptor) + { + RequestType = requestType; + ResponseType = responseType; + Descriptor = descriptor; + } + + public Type RequestType { get; } + + public Type ResponseType { get; } + + public CqrsStreamInvokerDescriptor Descriptor { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record AlphaStream() : IStreamRequest; + + public sealed record BetaStream() : IStreamRequest; + + public sealed class AlphaStreamHandler : IStreamRequestHandler + { + public async IAsyncEnumerable Handle(AlphaStream request, CancellationToken cancellationToken) + { + yield return 1; + await Task.CompletedTask; + } + } + + public sealed class Container + { + private sealed class HiddenBetaStreamHandler : IStreamRequestHandler + { + public async IAsyncEnumerable Handle(BetaStream request, CancellationToken cancellationToken) + { + yield return "beta"; + await Task.CompletedTask; + } + } + } + } + """; + private const string PreciseReflectedRequestInvokerProviderBoundarySource = """ using System; using System.Collections.Generic; @@ -3052,6 +3282,40 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当同一轮生成同时包含 direct registration 与“隐藏实现类型 + 可见 handler interface”注册时, + /// request invoker provider 会按稳定实现排序生成连续描述符和方法编号。 + /// + [Test] + public void Emits_Request_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations() + { + var generatedSource = RunGenerator(MixedRequestInvokerProviderSource); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "new global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(typeof(global::TestApp.AlphaRequest), typeof(string), new global::GFramework.Cqrs.CqrsRequestInvokerDescriptor(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), typeof(__GFrameworkGeneratedCqrsHandlerRegistry).GetMethod(nameof(InvokeRequestHandler0), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!))")); + Assert.That( + generatedSource, + Does.Contain( + "new global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(typeof(global::TestApp.BetaRequest), typeof(int), new global::GFramework.Cqrs.CqrsRequestInvokerDescriptor(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), typeof(__GFrameworkGeneratedCqrsHandlerRegistry).GetMethod(nameof(InvokeRequestHandler1), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!))")); + Assert.That( + generatedSource, + Does.Contain( + "private static global::System.Threading.Tasks.ValueTask InvokeRequestHandler0(object handler, object request, global::System.Threading.CancellationToken cancellationToken)")); + Assert.That( + generatedSource, + Does.Contain( + "private static global::System.Threading.Tasks.ValueTask InvokeRequestHandler1(object handler, object request, global::System.Threading.CancellationToken cancellationToken)")); + Assert.That( + generatedSource, + Does.Contain( + "var typedHandler = (global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler)handler;")); + }); + } + /// /// 验证当 runtime 缺少 ICqrsRequestInvokerProvider 时, /// 生成器会整体跳过 request invoker provider 元数据发射,而不是输出半套 descriptor 成员。 @@ -3265,6 +3529,40 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当同一轮生成同时包含 direct registration 与“隐藏实现类型 + 可见 handler interface”注册时, + /// stream invoker provider 会按稳定实现排序生成连续描述符和方法编号。 + /// + [Test] + public void Emits_Stream_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations() + { + var generatedSource = RunGenerator(MixedStreamInvokerProviderSource); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "new global::GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(typeof(global::TestApp.AlphaStream), typeof(int), new global::GFramework.Cqrs.CqrsStreamInvokerDescriptor(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler), typeof(__GFrameworkGeneratedCqrsHandlerRegistry).GetMethod(nameof(InvokeStreamHandler0), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!))")); + Assert.That( + generatedSource, + Does.Contain( + "new global::GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(typeof(global::TestApp.BetaStream), typeof(string), new global::GFramework.Cqrs.CqrsStreamInvokerDescriptor(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler), typeof(__GFrameworkGeneratedCqrsHandlerRegistry).GetMethod(nameof(InvokeStreamHandler1), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!))")); + Assert.That( + generatedSource, + Does.Contain( + "private static object InvokeStreamHandler0(object handler, object request, global::System.Threading.CancellationToken cancellationToken)")); + Assert.That( + generatedSource, + Does.Contain( + "private static object InvokeStreamHandler1(object handler, object request, global::System.Threading.CancellationToken cancellationToken)")); + Assert.That( + generatedSource, + Does.Contain( + "var typedHandler = (global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler)handler;")); + }); + } + /// /// 验证当 runtime 缺少 ICqrsStreamInvokerProvider 时, /// 生成器会整体跳过 stream invoker provider 元数据发射,而不是保留孤立的 descriptor 成员。 diff --git a/GFramework.csproj b/GFramework.csproj index 296cd4bf..56e714c8 100644 --- a/GFramework.csproj +++ b/GFramework.csproj @@ -74,6 +74,7 @@ + @@ -123,6 +124,7 @@ + @@ -158,6 +160,7 @@ + diff --git a/GFramework.sln b/GFramework.sln index b2fe4c40..e5c8ffdc 100644 --- a/GFramework.sln +++ b/GFramework.sln @@ -50,6 +50,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs.SourceGener EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Game.SourceGenerators", "GFramework.Game.SourceGenerators\GFramework.Game.SourceGenerators.csproj", "{9D3AADF0-55E6-4F80-B9C5-875F63E170D8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs.Benchmarks", "GFramework.Cqrs.Benchmarks\GFramework.Cqrs.Benchmarks.csproj", "{5609D017-E481-431B-874A-7D06BFF698F9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -360,6 +362,18 @@ Global {9D3AADF0-55E6-4F80-B9C5-875F63E170D8}.Release|x64.Build.0 = Release|Any CPU {9D3AADF0-55E6-4F80-B9C5-875F63E170D8}.Release|x86.ActiveCfg = Release|Any CPU {9D3AADF0-55E6-4F80-B9C5-875F63E170D8}.Release|x86.Build.0 = Release|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Debug|x64.ActiveCfg = Debug|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Debug|x64.Build.0 = Debug|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Debug|x86.ActiveCfg = Debug|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Debug|x86.Build.0 = Debug|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Release|Any CPU.Build.0 = Release|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Release|x64.ActiveCfg = Release|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Release|x64.Build.0 = Release|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Release|x86.ActiveCfg = Release|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE 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 efc871e8..17cf7319 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,7 +7,7 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-082` +- 恢复点编号:`CQRS-REWRITE-RP-084` - 当前阶段:`Phase 8` - 当前 PR 锚点:`PR #323` - 当前结论: @@ -18,6 +18,8 @@ CQRS 迁移与收敛。 - `RP-080` 已将基础 generation gate 回归扩展到 notification handler interface、stream handler interface 与 registry attribute 缺失分支 - `RP-081` 已继续补齐基础 generation gate 的 logging 与 DI runtime contract 缺失分支 - 当前 `RP-082` 已补齐基础 generation gate 的 request handler runtime contract 缺失分支 + - `RP-083` 已补齐 mixed direct / reflected-implementation request 与 stream invoker provider 发射顺序回归 + - 当前 `RP-084` 已引入独立 `GFramework.Cqrs.Benchmarks` 项目,作为持续吸收 `Mediator` benchmark 组织方式的第一落点 - `ai-plan` active 入口现以 `PR #323` 和 `RP-082` 为唯一权威恢复锚点;`PR #307`、其他更早 PR 与阶段细节均以下方归档或说明为准 ## 当前活跃事实 @@ -45,6 +47,14 @@ CQRS 迁移与收敛。 - 备注:当前 WSL worktree 需要显式绑定 `GIT_DIR` / `GIT_WORK_TREE` 后运行 - `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` - 结果:通过,`0 warning / 0 error` +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations"` + - 结果:通过,`2/2` passed +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `GIT_DIR= GIT_WORK_TREE= python3 scripts/license-header.py --check` + - 结果:通过 +- `git diff --check` + - 结果:通过 - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Provider_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Enumerator|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Entry_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` - 结果:通过,`5/5` passed - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Descriptor_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Descriptor_Entry_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Provider_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Descriptor_Enumerator"` @@ -93,8 +103,8 @@ CQRS 迁移与收敛。 ## 下一推荐步骤 1. 继续处理 `PR #323` 的剩余 review 收尾,优先保持 `ai-plan` active 入口与 trace 的单一锚点一致 -2. 若继续推进代码切片,优先复核基础 generation gate 之外的 runtime contract 或 fallback selection 分支;基础 gate 的可安全构造缺失分支已覆盖 -3. 在进入下一批 runtime / generator 收敛前,保持最小 Release build 或 targeted test 作为权威验证 +2. 若继续推进“吸收 Mediator 设计哲学”的切片,优先扩展 benchmark 场景矩阵到 pipeline、stream、cold-start 与 generated invoker provider 对照 +3. 在进入下一批 runtime / generator 收敛前,保持最小 Release build、targeted test 或 benchmark project build 作为权威验证 ## 活跃文档 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 0dec487b..5a8f4129 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 @@ -212,3 +212,87 @@ 1. 若 review 重新触发后仍有 latest-head open thread,继续以 `PR #323` 为当前唯一 PR 恢复锚点复核 2. 后续若继续推进代码切片,优先复核基础 generation gate 之外的 runtime contract 或 fallback selection 分支 + +## 2026-05-06 + +### 阶段:mixed invoker provider 排序回归(CQRS-REWRITE-RP-083) + +- 使用 `$gframework-batch-boot 50` 继续 `feat/cqrs-optimization` 的 CQRS 收口批次 +- 批次目标:在 branch diff 相对 `origin/main` 接近 `50` 个文件前,继续补齐低风险的 generator runtime contract / emission 回归 +- 本轮基线选择: + - `origin/main a8c6c11e`,committer date `2026-05-05 13:14:24 +0800` + - `main a8c6c11e`,committer date `2026-05-05 13:14:24 +0800` + - 当前分支 `feat/cqrs-optimization a8c6c11e`,committer date `2026-05-05 13:14:24 +0800` +- 启动时 branch diff vs `origin/main` 为 `0` files / `0` lines,因此继续选择低风险测试回归切片 +- 本轮复核 `CreateGeneratedRegistrySourceShape` 与 invoker emission 路径后确认: + - 现有测试已覆盖 request / stream provider 的单一 direct 场景、单一 reflected-implementation 场景、precise reflected 跳过边界,以及各项 runtime contract 缺失分支 + - 尚未锁定“同一 registry 同时包含 direct registration 与 reflected-implementation registration”时的 descriptor 顺序与 `Invoke*HandlerN` 编号稳定性 +- 已补齐: + - `Emits_Request_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations` + - `Emits_Stream_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations` + - 两组 source fixture:`MixedRequestInvokerProviderSource`、`MixedStreamInvokerProviderSource` +- 通过新增回归,显式锁定以下约束: + - provider descriptor 条目按稳定实现排序输出 + - `InvokeRequestHandler0/1` 与 `InvokeStreamHandler0/1` 的方法编号随 emission 顺序连续增长 + - 隐藏实现类型不会破坏 direct registration 与 reflected-implementation registration 的混合发射 + +### 验证(RP-083) + +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations"` + - 结果:通过,`2/2` passed + +### 当前 stop-condition 度量(RP-083) + +- primary metric:branch diff files vs `origin/main` +- 当前说明:active batch 尚未提交时,基于 `HEAD` 的 branch diff 仍显示 `0` files / `0` lines;提交本批后再以新 `HEAD` 复算累计 branch diff + +### 当前下一步(RP-083) + +1. 提交本轮 mixed invoker provider 排序回归后,复算 branch diff vs `origin/main`,确认 `50` 文件阈值仍有充足余量 +2. 若继续推进代码切片,优先复核 invoker provider 之外的 runtime contract 或 fallback selection 分支 + +### 阶段:benchmark 基础设施引入(CQRS-REWRITE-RP-084) + +- 用户明确将当前长期分支目标上提为:系统性吸收 `ai-libs/Mediator` 的实现思路与设计哲学,并将可取部分纳入 `GFramework.Cqrs` +- 本轮据此调整批次目标,不再把关注点收缩到单个 generator 回归,而是建立能持续比较和吸收设计差异的 benchmark 基础设施 +- 参考 `ai-libs/Mediator` 的 benchmark 设计后,本轮采纳的核心结构包括: + - 独立 benchmark 项目壳,而非扩展现有 NUnit 测试项目 + - 共享 `Fixture` 输出并校验场景配置 + - `Request` / `Notification` 两个 messaging 场景作为首批最小落点 + - 自定义列 `CustomColumn`,为后续矩阵扩展保留可读结果标签 +- 本轮新增: + - `GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj` + - `GFramework.Cqrs.Benchmarks/Program.cs` + - `GFramework.Cqrs.Benchmarks/CustomColumn.cs` + - `GFramework.Cqrs.Benchmarks/Messaging/Fixture.cs` + - `GFramework.Cqrs.Benchmarks/Messaging/BenchmarkContext.cs` + - `GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs` + - `GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs` + - `GFramework.Cqrs.Benchmarks/README.md` +- 设计取舍: + - 使用最小 `ICqrsContext` marker,避免把完整 `ArchitectureContext` 初始化成本混入 steady-state dispatch + - 直接复用 `GFramework.Cqrs.CqrsRuntimeFactory` 与 `MicrosoftDiContainer`,让基准聚焦于 runtime dispatch / publish + - 外部对照组先接入 `MediatR`,保持与 `Mediator` benchmark 的对照哲学一致;但本轮仍只做最小 request / notification 场景 + - 暂不把 source generator benchmark、cold-start 独立工程或完整 pipeline / stream 矩阵一起引入,避免首批 scope 失控 +- 兼容性修正: + - 在根 `GFramework.csproj` 中显式排除 `GFramework.Cqrs.Benchmarks/**`,避免 meta-package 意外编译 benchmark 源码 + - 将 benchmark 项目加入 `GFramework.sln`,保持仓库级工作流完整 + +### 验证(RP-084) + +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `GIT_DIR= GIT_WORK_TREE= python3 scripts/license-header.py --check` + - 结果:通过 +- `git diff --check` + - 结果:通过 + +### 当前 stop-condition 度量(RP-084) + +- primary metric:branch diff files vs `origin/main` +- 当前说明:本轮仍在 `50` 文件阈值以内,可继续按 benchmark 场景或 CQRS runtime 对照能力分批推进 + +### 当前下一步(RP-084) + +1. 继续扩展 `GFramework.Cqrs.Benchmarks`,优先补齐 pipeline、stream、cold-start 与 generated invoker provider 对照场景 +2. 当后续有具体 runtime 优化切片时,用该 benchmark 项目验证是否真正吸收到了 `Mediator` 的低开销 dispatch 设计收益