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 设计收益