diff --git a/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs index 80ce9b88..d7bbdfc5 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs @@ -21,7 +21,7 @@ using GeneratedMediator = Mediator.Mediator; namespace GFramework.Cqrs.Benchmarks.Messaging; /// -/// 对比单处理器 notification 在 GFramework.CQRS 与 MediatR 之间的 publish 开销。 +/// 对比单处理器 notification 在 GFramework.CQRS、NuGet `Mediator` 与 MediatR 之间的 publish 开销。 /// [Config(typeof(Config))] public class NotificationBenchmarks @@ -132,7 +132,7 @@ public class NotificationBenchmarks MediatR.INotification; /// - /// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 notification handler。 + /// 同时实现 GFramework.CQRS、NuGet `Mediator` 与 MediatR 契约的最小 notification handler。 /// public sealed class BenchmarkNotificationHandler : GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler, diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs index 6e23cfe8..ff5ca2c4 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs @@ -157,7 +157,7 @@ public class RequestBenchmarks public sealed record BenchmarkResponse(Guid Id); /// - /// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 request handler。 + /// 同时实现 GFramework.CQRS、NuGet `Mediator` 与 MediatR 契约的最小 request handler。 /// public sealed class BenchmarkRequestHandler : GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler, diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.cs index f96deeef..0e4affc2 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.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; @@ -35,7 +36,9 @@ public class RequestLifetimeBenchmarks private ScopedBenchmarkContainer? _scopedContainer; private ICqrsRuntime? _scopedRuntime; private ServiceProvider _serviceProvider = null!; + private ServiceProvider _mediatorServiceProvider = null!; private IMediator? _mediatr; + private GeneratedMediator? _mediator; private BenchmarkRequestHandler _baselineHandler = null!; private BenchmarkRequest _request = null!; private ILogger _runtimeLogger = null!; @@ -83,7 +86,7 @@ public class RequestLifetimeBenchmarks } /// - /// 构建当前生命周期下的 GFramework 与 MediatR request 对照宿主。 + /// 构建当前生命周期下的 GFramework、NuGet `Mediator` 与 MediatR request 对照宿主。 /// [GlobalSetup] public void Setup() @@ -130,6 +133,12 @@ public class RequestLifetimeBenchmarks { _mediatr = _serviceProvider.GetRequiredService(); } + + _mediatorServiceProvider = CreateMediatorServiceProvider(Lifetime); + if (Lifetime != HandlerLifetime.Scoped) + { + _mediator = _mediatorServiceProvider.GetRequiredService(); + } } /// @@ -140,7 +149,7 @@ public class RequestLifetimeBenchmarks { try { - BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider); + BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider, _mediatorServiceProvider); } finally { @@ -196,6 +205,24 @@ public class RequestLifetimeBenchmarks return _mediatr!.Send(_request, CancellationToken.None); } + /// + /// 通过 `Mediator` source-generated concrete mediator 发送 request,作为 compile-time 对照。 + /// + /// 代表当前 `Mediator` request dispatch 完成的值任务。 + [Benchmark] + public ValueTask SendRequest_Mediator() + { + if (Lifetime == HandlerLifetime.Scoped) + { + return SendScopedMediatorRequestAsync( + _mediatorServiceProvider, + _request, + CancellationToken.None); + } + + return _mediator!.Send(_request, CancellationToken.None); + } + /// /// 按生命周期把 benchmark request handler 注册到 GFramework 容器。 /// @@ -243,12 +270,83 @@ public class RequestLifetimeBenchmarks }; } + /// + /// 构建只承载当前 benchmark request handler 的最小 `Mediator` 对照宿主,并按生命周期切换生成器注册形状。 + /// + /// 待比较的 handler 生命周期。 + /// 可直接解析 generated `Mediator.Mediator` 的 DI 宿主。 + private static ServiceProvider CreateMediatorServiceProvider(HandlerLifetime lifetime) + { + return lifetime switch + { + HandlerLifetime.Singleton => CreateSingletonMediatorServiceProvider(), + HandlerLifetime.Scoped => CreateScopedMediatorServiceProvider(), + HandlerLifetime.Transient => CreateTransientMediatorServiceProvider(), + _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.") + }; + } + + /// + /// 在真实的 request 级作用域内执行一次 `Mediator` request 分发。 + /// + /// 请求响应类型。 + /// 当前 benchmark 的根 。 + /// 要发送的 request。 + /// 取消令牌。 + /// 当前 request 的响应结果。 + private static async ValueTask SendScopedMediatorRequestAsync( + ServiceProvider rootServiceProvider, + Mediator.IRequest request, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(rootServiceProvider); + ArgumentNullException.ThrowIfNull(request); + + using var scope = rootServiceProvider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + return await mediator.Send(request, cancellationToken).ConfigureAwait(false); + } + + /// + /// 构建 singleton 生命周期的 `Mediator` 对照宿主。 + /// + /// 按 singleton 形状生成 DI 注册的 `Mediator` 宿主。 + private static ServiceProvider CreateSingletonMediatorServiceProvider() + { + var services = new ServiceCollection(); + services.AddMediator(static options => options.ServiceLifetime = ServiceLifetime.Singleton); + return services.BuildServiceProvider(); + } + + /// + /// 构建 scoped 生命周期的 `Mediator` 对照宿主。 + /// + /// 按 scoped 形状生成 DI 注册的 `Mediator` 宿主。 + private static ServiceProvider CreateScopedMediatorServiceProvider() + { + var services = new ServiceCollection(); + services.AddMediator(static options => options.ServiceLifetime = ServiceLifetime.Scoped); + return services.BuildServiceProvider(); + } + + /// + /// 构建 transient 生命周期的 `Mediator` 对照宿主。 + /// + /// 按 transient 形状生成 DI 注册的 `Mediator` 宿主。 + private static ServiceProvider CreateTransientMediatorServiceProvider() + { + var services = new ServiceCollection(); + services.AddMediator(static options => options.ServiceLifetime = ServiceLifetime.Transient); + return services.BuildServiceProvider(); + } + /// /// Benchmark request。 /// /// 请求标识。 public sealed record BenchmarkRequest(Guid Id) : GFramework.Cqrs.Abstractions.Cqrs.IRequest, + Mediator.IRequest, MediatR.IRequest; /// @@ -258,10 +356,11 @@ public class RequestLifetimeBenchmarks public sealed record BenchmarkResponse(Guid Id); /// - /// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 request handler。 + /// 同时实现 GFramework.CQRS、NuGet `Mediator` 与 MediatR 契约的最小 request handler。 /// public sealed class BenchmarkRequestHandler : GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler, + Mediator.IRequestHandler, MediatR.IRequestHandler { /// @@ -272,6 +371,16 @@ public class RequestLifetimeBenchmarks return ValueTask.FromResult(new BenchmarkResponse(request.Id)); } + /// + /// 处理 NuGet `Mediator` request。 + /// + ValueTask Mediator.IRequestHandler.Handle( + BenchmarkRequest request, + CancellationToken cancellationToken) + { + return Handle(request, cancellationToken); + } + /// /// 处理 MediatR request。 /// diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md index dd943daa..c50f93b2 100644 --- a/GFramework.Cqrs.Benchmarks/README.md +++ b/GFramework.Cqrs.Benchmarks/README.md @@ -16,7 +16,7 @@ - `Messaging/RequestBenchmarks.cs` - direct handler、默认 `GFramework.Cqrs` runtime、NuGet `Mediator` source-generated concrete path、`MediatR` - `Messaging/RequestLifetimeBenchmarks.cs` - - `Singleton / Scoped / Transient` 三类 handler 生命周期下,baseline、默认 generated-provider 宿主接线的 `GFramework.Cqrs` runtime 与 `MediatR` + - `Singleton / Scoped / Transient` 三类 handler 生命周期下,baseline、默认 generated-provider 宿主接线的 `GFramework.Cqrs` runtime、NuGet `Mediator` source-generated concrete path 与 `MediatR` - `Messaging/RequestPipelineBenchmarks.cs` - `0 / 1 / 4` 个 pipeline 行为下,baseline、默认 generated-provider 宿主接线的 `GFramework.Cqrs` runtime 与 `MediatR` - `Messaging/RequestInvokerBenchmarks.cs` @@ -104,5 +104,4 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro ## 当前缺口 - 当前没有 stream 生命周期版的 NuGet `Mediator` source-generated concrete path 对照;`StreamLifetimeBenchmarks` 现在只覆盖 `GFramework.Cqrs` 与 `MediatR` -- 当前没有 request 生命周期下的 NuGet `Mediator` compile-time lifetime 矩阵;`RequestLifetimeBenchmarks` 只覆盖 `GFramework.Cqrs` 与 `MediatR` - 当前没有 notification fan-out 的生命周期矩阵;`NotificationFanOutBenchmarks` 只覆盖固定 `4 handler` 的已装配宿主 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 10260065..9ded9794 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 @@ -12,7 +12,7 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-139` +- 恢复点编号:`CQRS-REWRITE-RP-140` - 当前阶段:`Phase 8` - 当前 PR 锚点:`PR #349(已于 2026-05-12 合并到 origin/main)` - 当前结论: @@ -23,14 +23,14 @@ CQRS 迁移与收敛。 - README 一致性盘点成立,`GFramework.Cqrs.Benchmarks/README.md` 的 startup coverage / 解释边界仍可收紧 - benchmark XML 缺口盘点存在明显误报;代表文件中的 class / benchmark 方法 `` 与 `` 已实际存在 - 因此本轮不接受新的大范围 XML 收口波次,避免把上下文预算消耗在错误候选上 - - 本轮 accepted delegated scope 收敛为单文件 docs-only worker: - - `GFramework.Cqrs.Benchmarks/README.md` - - 明确 `StreamStartupBenchmarks` 现已覆盖 `MediatR`、`GFramework.Cqrs` reflection、 - `GFramework.Cqrs` generated、NuGet `Mediator` 四组 initialization / cold-start 对照 - - 补充 `RequestStartupBenchmarks` 与 `NotificationStartupBenchmarks` 的 - `GFramework.Cqrs` 路径是“单 handler 最小宿主 + 手工注册”的 startup / cold-start 模型,不外推到程序集扫描、 - 完整注册协调器、fan-out 或发布策略变体 - - 当前决定在该 docs-only 收口后停在自然边界: + - 本轮主线程将 `RequestLifetimeBenchmarks` 的 request lifetime 矩阵扩展为 baseline、`GFramework.Cqrs`、NuGet `Mediator` 与 `MediatR` 四组对照: + - `BenchmarkRequest` 与 `BenchmarkRequestHandler` 增补 `Mediator` 契约实现 + - `Mediator` 宿主按 `Singleton / Scoped / Transient` 三种编译期常量分支生成,满足 source generator 的配置约束 + - `Scoped` 路径通过显式 `CreateScope()` 解析 generated `Mediator.Mediator`,保持与现有 request scoped 对照相同的作用域边界 + - 本轮同时收口两处 benchmark XML 文档漂移: + - `RequestBenchmarks` 的 handler 说明补齐 NuGet `Mediator` 契约事实 + - `NotificationBenchmarks` 的类说明与 handler 说明补齐 NuGet `Mediator` 对照事实 + - 当前决定在该 parity + docs 收口后停在自然边界: - branch-size 仍低于 `50 files` - 但下一批低风险候选已不再清晰;继续开波次的收益低于评审与上下文成本 - tests 侧此前已补齐并提交: @@ -50,6 +50,9 @@ CQRS 迁移与收敛。 - 当前分支:`feat/cqrs-optimization` - 当前 PR:`PR #349(已合并;当前分支暂无新的公开 PR)` - 当前写面: + - `GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.cs` + - `GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs` + - `GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.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` @@ -57,7 +60,7 @@ CQRS 迁移与收敛。 - `origin/main @ 2b2bec65 (2026-05-12 11:49:39 +0800)` - 当前已提交 branch diff:`14 files` - 当前分支比 `origin/main` 多 `5` 个提交:`f346110a`、`a016e3d4`、`ab422b05`、`555c7c07`、`c32a1ec4` - - 当前未提交面由 benchmark README 的 startup 边界同步与 `ai-plan` 恢复点更新构成 + - 当前工作面已收口为 request lifetime parity、两处 benchmark XML 文档修正与对应 `README` / `ai-plan` 同步 - 本轮提交: - `f346110a` `feat(cqrs-benchmarks): 补齐 stream startup 的 Mediator 对照路径` - `ab422b05` `docs(cqrs-benchmarks): 补齐 request benchmark 返回值注释` @@ -67,28 +70,28 @@ CQRS 迁移与收敛。 ## 当前风险 - `StreamStartupBenchmarks` 的 `Mediator` parity 目前只做了编译验证,尚未单独执行 benchmark 作业确认 startup 矩阵运行结果。 -- `StreamLifetimeBenchmarks` 仍缺 `Mediator` parity;该项涉及 `BenchmarkHostFactory` 与 compile-time lifetime 形状,不再是本轮低风险切片。 +- `StreamLifetimeBenchmarks` 仍缺 `Mediator` parity;虽然 `RequestLifetimeBenchmarks` 已证明 `Mediator` 的 compile-time lifetime 可通过常量分支接入,但 stream 侧仍需要额外处理 `IAsyncEnumerable` 与作用域覆盖整个枚举周期的组合边界。 - benchmark XML 盘点若再次依赖粗糙脚本或只读 inventory,仍有把已存在文档误记为缺口的风险;后续若再开 XML 波次,必须先用主线程抽样核对代表文件。 -- 本轮已在 README 精度同步后主动停批次;若后续恢复,优先先做 `StreamStartupBenchmarks` smoke run 或更明确的 parity / docs 候选,而不是继续机械扩张 XML 批次。 +- 本轮已在 request lifetime parity 与文档漂移收口后主动停批次;若后续恢复,优先先做 `StreamStartupBenchmarks` smoke run 或更明确的 parity / docs 候选,而不是继续机械扩张 XML 批次。 ## 最近权威验证 - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` - 结果:通过,`0 warning / 0 error` + - 备注:确认 `RequestLifetimeBenchmarks` 的 NuGet `Mediator` lifetime parity 可在当前生成器约束下编译通过 - `python3 scripts/license-header.py --check` - 结果:通过 - `$gframework-pr-review` - 结果:`PR #349` 已关闭;latest-head review open thread 经本地核对仅剩 `StreamingBenchmarks.Stream_MediatR()` 的 XML 文档缺口仍成立 -- `git --git-dir=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/.git/worktrees/GFramework-cqrs --work-tree=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework-WorkTree/GFramework-cqrs diff -- GFramework.Cqrs.Benchmarks/README.md` +- `git --git-dir=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/.git/worktrees/GFramework-cqrs --work-tree=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework-WorkTree/GFramework-cqrs diff -- GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.cs GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs` - 结果:通过 - - 备注:确认本轮 worker 仅修改 README 的 startup coverage / 边界文案 + - 备注:确认本轮代码面收敛在 request lifetime parity 与两处 XML 文档漂移修正 ## 下一推荐步骤 -1. 串行运行 `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`、`python3 scripts/license-header.py --check --paths ...` 与 `git diff --check`,作为本轮 docs-only 收尾的权威验证。 -2. 提交当前 README 与 `ai-plan` 更新,回到干净工作树。 -3. 若后续继续 benchmark 波次,优先单独执行 `StreamStartupBenchmarks` 的最小 smoke run,验证新加 `Mediator` startup 路径可运行。 -4. 若后续再开文档批次,先用主线程核对代表文件,再决定是否存在真实 XML 缺口;不要直接沿用误报 inventory 扩批。 +1. 若后续继续 benchmark 波次,优先单独执行 `StreamStartupBenchmarks` 的最小 smoke run,验证新加 `Mediator` startup 路径可运行。 +2. 若后续评估 `StreamLifetimeBenchmarks` 的 `Mediator` parity,先复用本轮 request lifetime 的“编译期常量 lifetime 分支”经验,再判断是否值得引入新的 scoped stream helper。 +3. 若后续再开 XML / docs 批次,先由主线程逐文件核对代表样本,不要直接沿用误报 inventory 扩批。 ## 活跃文档 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 30c89650..9848ad42 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 @@ -7,6 +7,43 @@ SPDX-License-Identifier: Apache-2.0 ## 2026-05-12 +### 阶段:request lifetime 的 Mediator parity 与文档漂移收口(CQRS-REWRITE-RP-140) + +- 继续按 `$gframework-batch-boot 50` 推进,基线保持为 `origin/main @ 2b2bec65 (2026-05-12 11:49:39 +0800)`。 +- 本轮启动时重新测得当前已提交 branch diff 仍为 `14 files / 324 lines`,远低于 `50 files` 阈值;停止与否继续由 context-budget / reviewability 主导。 +- 主线程结合两个 explorer 子代理的只读盘点后,接受以下结论: + - 不再继续按 benchmark XML `` inventory 机械扩批;粗糙脚本会把“注释位于 `[Benchmark]` 之前”的现有文档误判为缺口 + - `RequestLifetimeBenchmarks` 的 NuGet `Mediator` lifetime parity 是当前仍然真实、且能保持 reviewable 的实现候选 + - `NotificationBenchmarks.cs` 与 `RequestBenchmarks.cs` 仍有两处低风险 XML 文档漂移,均只涉及 NuGet `Mediator` 事实同步 +- 本轮主线程实施: + - `RequestLifetimeBenchmarks.cs` + - 新增 `GeneratedMediator` 宿主字段、`SendRequest_Mediator()` benchmark 方法与 scoped `Mediator` request helper + - 将 `BenchmarkRequest` / `BenchmarkRequestHandler` 扩为同时实现 `Mediator` 契约 + - 为 `Mediator` 宿主改用 `Singleton / Scoped / Transient` 三个编译期常量分支,规避 `MSG0007` 对运行时 lifetime 赋值的生成器限制 + - `RequestBenchmarks.cs` + - 将 handler XML 文档说明补齐为同时实现 `GFramework.CQRS`、NuGet `Mediator` 与 `MediatR` + - `NotificationBenchmarks.cs` + - 将类说明与 handler XML 文档说明补齐为同时覆盖 `GFramework.CQRS`、NuGet `Mediator` 与 `MediatR` + - `GFramework.Cqrs.Benchmarks/README.md` + - 将 `RequestLifetimeBenchmarks` 的 coverage 更新为包含 NuGet `Mediator` source-generated concrete path + - 删除“当前没有 request 生命周期下的 NuGet `Mediator` compile-time lifetime 矩阵”这一已过时缺口 +- 验证里程碑: + - 第一次 `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:失败 + - 原因:`RequestLifetimeBenchmarks.cs` 中基于运行时变量写入 `MediatorOptions.ServiceLifetime`,触发 `MSG0007` + - 主线程修正: + - 将 `CreateMediatorServiceProvider(HandlerLifetime lifetime)` 收口为 3 个常量分支工厂: + `CreateSingletonMediatorServiceProvider()`、`CreateScopedMediatorServiceProvider()`、`CreateTransientMediatorServiceProvider()` + - 第二次 `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- 当前 stop decision: + - 不继续开启新的实现波次 + - 原因不是 branch-size 阈值耗尽;当前分支仍只有 `14 files` + - 停止原因是本轮已经完成一条真实 parity 收口和两处文档漂移修正,继续扩到 `StreamLifetimeBenchmarks` 会显著提高作用域与 review 成本 +- 当前下一步: + - 主线程补跑 `python3 scripts/license-header.py --check --paths ...` 与 `git diff --check` + - 更新 active tracking / trace 后提交当前 benchmark 代码、README 与 `ai-plan` + ### 阶段:README startup coverage 精度同步并停在自然边界(CQRS-REWRITE-RP-139) - 继续按 `$gframework-batch-boot 50` 推进,基线保持为 `origin/main @ 2b2bec65 (2026-05-12 11:49:39 +0800)`。