From 4e98b63e9ccad4f4a4e20ecb2ab47bc49e348fbf Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Mon, 11 May 2026 14:52:39 +0800 Subject: [PATCH] =?UTF-8?q?fix(cqrs):=20=E6=94=B6=E5=8F=A3=20PR=20review?= =?UTF-8?q?=20=E5=89=A9=E4=BD=99=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 NotificationLifetimeBenchmarks 的 scoped 容器释放与公开 XML 契约缺口 - 修复 generated descriptor 预热阶段先去重后校验导致的有效后继条目丢失问题 - 更新 generated descriptor 的 MethodInfo 比较方式并补充 request/stream 回归测试 - 同步 cqrs-rewrite active tracking 与 trace 的当前 PR 锚点到 PR #348 --- .../NotificationLifetimeBenchmarks.cs | 8 +- ...qrsGeneratedRequestInvokerProviderTests.cs | 187 ++++++++++++++++++ .../Internal/CqrsHandlerRegistrar.cs | 18 +- .../todos/cqrs-rewrite-migration-tracking.md | 13 +- .../traces/cqrs-rewrite-migration-trace.md | 23 +++ 5 files changed, 236 insertions(+), 13 deletions(-) diff --git a/GFramework.Cqrs.Benchmarks/Messaging/NotificationLifetimeBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/NotificationLifetimeBenchmarks.cs index ac83127a..f684f35e 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/NotificationLifetimeBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/NotificationLifetimeBenchmarks.cs @@ -133,7 +133,7 @@ public class NotificationLifetimeBenchmarks { try { - BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider); + BenchmarkCleanupHelper.DisposeAll(_scopedContainer, _container, _serviceProvider); } finally { @@ -144,6 +144,7 @@ public class NotificationLifetimeBenchmarks /// /// 直接调用 handler,作为不同生命周期矩阵下的 publish 额外开销 baseline。 /// + /// 代表基线 handler 完成当前 notification 处理的值任务。 [Benchmark(Baseline = true)] public ValueTask PublishNotification_Baseline() { @@ -153,6 +154,7 @@ public class NotificationLifetimeBenchmarks /// /// 通过 GFramework.CQRS runtime 发布 notification。 /// + /// 代表当前 GFramework.CQRS publish 完成的值任务。 [Benchmark] public ValueTask PublishNotification_GFrameworkCqrs() { @@ -171,6 +173,7 @@ public class NotificationLifetimeBenchmarks /// /// 通过 MediatR 发布 notification,作为外部对照。 /// + /// 代表当前 MediatR publish 完成的任务。 [Benchmark] public Task PublishNotification_MediatR() { @@ -293,6 +296,9 @@ public class NotificationLifetimeBenchmarks /// /// 处理 GFramework.CQRS notification。 /// + /// 当前要处理的 notification。 + /// 取消令牌。 + /// 代表当前 notification 处理完成的值任务。 public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(notification); diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs index 4530e537..227f101d 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs @@ -548,6 +548,26 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Assert.That(response, Is.EqualTo("runtime:payload")); } + /// + /// 验证当首个 request descriptor 无效、后续同键 descriptor 有效时, + /// registrar 不会因为过早去重而丢掉本可注册的 generated descriptor。 + /// + [Test] + public async Task SendAsync_Should_Use_Later_Valid_Generated_Request_Descriptor_When_First_Duplicate_Is_Invalid() + { + var generatedAssembly = CreateGeneratedAssembly( + typeof(InvalidThenValidDuplicateRequestInvokerProviderRegistry), + "GFramework.Cqrs.Tests.Cqrs.InvalidThenValidDuplicateRequestInvokerAssembly, Version=1.0.0.0"); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + container.Freeze(); + + var context = new ArchitectureContext(container); + var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false); + Assert.That(response, Is.EqualTo("generated:payload")); + } + /// /// 验证当 stream descriptor 枚举项与 provider 的 TryGetDescriptor 结果不一致时, /// registrar 会忽略该坏 descriptor,并继续回退到反射建流路径。 @@ -568,6 +588,26 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Assert.That(results, Is.EqualTo([3, 4])); } + /// + /// 验证当首个 stream descriptor 无效、后续同键 descriptor 有效时, + /// registrar 不会因为过早去重而丢掉本可注册的 generated descriptor。 + /// + [Test] + public async Task CreateStream_Should_Use_Later_Valid_Generated_Stream_Descriptor_When_First_Duplicate_Is_Invalid() + { + var generatedAssembly = CreateGeneratedAssembly( + typeof(InvalidThenValidDuplicateStreamInvokerProviderRegistry), + "GFramework.Cqrs.Tests.Cqrs.InvalidThenValidDuplicateStreamInvokerAssembly, Version=1.0.0.0"); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + container.Freeze(); + + var context = new ArchitectureContext(container); + var results = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false); + Assert.That(results, Is.EqualTo([30, 31])); + } + /// /// 模拟返回实例 request invoker 方法的 generated registry。 /// @@ -1288,6 +1328,81 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests } } + /// + /// 模拟首个 request descriptor 无效、后续同键 descriptor 有效的 generated registry。 + /// + private sealed class InvalidThenValidDuplicateRequestInvokerProviderRegistry : + ICqrsHandlerRegistry, + ICqrsRequestInvokerProvider, + IEnumeratesCqrsRequestInvokerDescriptors + { + private static readonly CqrsRequestInvokerDescriptor InvalidDescriptor = new( + typeof(IRequestHandler), + typeof(InvalidThenValidDuplicateRequestInvokerProviderRegistry).GetMethod( + nameof(InvokeAlternativeGenerated), + BindingFlags.NonPublic | BindingFlags.Static)!); + + private static readonly CqrsRequestInvokerDescriptor ValidDescriptor = new( + typeof(IRequestHandler), + typeof(GeneratedRequestInvokerProviderRegistry).GetMethod( + "InvokeGenerated", + BindingFlags.NonPublic | BindingFlags.Static)!); + + /// + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(IRequestHandler), + typeof(GeneratedRequestInvokerRequestHandler)); + } + + /// + public bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsRequestInvokerDescriptor? descriptor) + { + ArgumentNullException.ThrowIfNull(requestType); + ArgumentNullException.ThrowIfNull(responseType); + + if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string)) + { + descriptor = ValidDescriptor; + return true; + } + + descriptor = null; + return false; + } + + /// + public IReadOnlyList GetDescriptors() + { + return + [ + new CqrsRequestInvokerDescriptorEntry( + typeof(GeneratedRequestInvokerRequest), + typeof(string), + InvalidDescriptor), + new CqrsRequestInvokerDescriptorEntry( + typeof(GeneratedRequestInvokerRequest), + typeof(string), + ValidDescriptor) + ]; + } + + private static ValueTask InvokeAlternativeGenerated( + object handler, + object request, + CancellationToken cancellationToken) + { + return ValueTask.FromResult("invalid-first:payload"); + } + } + /// /// 模拟枚举出的 stream descriptor 与 provider 显式查询结果不一致的 generated registry。 /// @@ -1356,6 +1471,78 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests } } + /// + /// 模拟首个 stream descriptor 无效、后续同键 descriptor 有效的 generated registry。 + /// + private sealed class InvalidThenValidDuplicateStreamInvokerProviderRegistry : + ICqrsHandlerRegistry, + ICqrsStreamInvokerProvider, + IEnumeratesCqrsStreamInvokerDescriptors + { + private static readonly CqrsStreamInvokerDescriptor InvalidDescriptor = new( + typeof(IStreamRequestHandler), + typeof(InvalidThenValidDuplicateStreamInvokerProviderRegistry).GetMethod( + nameof(InvokeAlternativeGenerated), + BindingFlags.NonPublic | BindingFlags.Static)!); + + private static readonly CqrsStreamInvokerDescriptor ValidDescriptor = new( + typeof(IStreamRequestHandler), + typeof(GeneratedStreamInvokerProviderRegistry).GetMethod( + "InvokeGenerated", + BindingFlags.NonPublic | BindingFlags.Static)!); + + /// + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(IStreamRequestHandler), + typeof(GeneratedStreamInvokerRequestHandler)); + } + + /// + public bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsStreamInvokerDescriptor? descriptor) + { + ArgumentNullException.ThrowIfNull(requestType); + ArgumentNullException.ThrowIfNull(responseType); + + if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int)) + { + descriptor = ValidDescriptor; + return true; + } + + descriptor = null; + return false; + } + + /// + public IReadOnlyList GetDescriptors() + { + return + [ + new CqrsStreamInvokerDescriptorEntry( + typeof(GeneratedStreamInvokerRequest), + typeof(int), + InvalidDescriptor), + new CqrsStreamInvokerDescriptorEntry( + typeof(GeneratedStreamInvokerRequest), + typeof(int), + ValidDescriptor) + ]; + } + + private static object InvokeAlternativeGenerated(object handler, object request, CancellationToken cancellationToken) + { + return new[] { 800, 801 }.ToAsyncEnumerable(); + } + } + /// /// 创建带有 generated request invoker registry 元数据的程序集替身。 /// diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index 1b90edb8..740d85a8 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -360,6 +360,10 @@ internal static class CqrsHandlerRegistrar var descriptorKey = new InvokerDescriptorKey( descriptorEntry.RequestType, descriptorEntry.ResponseType); + + if (!TryValidateEnumeratedRequestInvokerDescriptor(provider, descriptorEntry, assemblyName, logger)) + continue; + if (!registeredKeys.Add(descriptorKey)) { logger.Warn( @@ -367,9 +371,6 @@ internal static class CqrsHandlerRegistrar continue; } - if (!TryValidateEnumeratedRequestInvokerDescriptor(provider, descriptorEntry, assemblyName, logger)) - continue; - CqrsDispatcher.RegisterGeneratedRequestInvokerDescriptor( descriptorEntry.RequestType, descriptorEntry.ResponseType, @@ -455,6 +456,10 @@ internal static class CqrsHandlerRegistrar var descriptorKey = new InvokerDescriptorKey( descriptorEntry.RequestType, descriptorEntry.ResponseType); + + if (!TryValidateEnumeratedStreamInvokerDescriptor(provider, descriptorEntry, assemblyName, logger)) + continue; + if (!registeredKeys.Add(descriptorKey)) { logger.Warn( @@ -462,9 +467,6 @@ internal static class CqrsHandlerRegistrar continue; } - if (!TryValidateEnumeratedStreamInvokerDescriptor(provider, descriptorEntry, assemblyName, logger)) - continue; - CqrsDispatcher.RegisterGeneratedStreamInvokerDescriptor( descriptorEntry.RequestType, descriptorEntry.ResponseType, @@ -501,7 +503,7 @@ internal static class CqrsHandlerRegistrar return false; } - if (!ReferenceEquals(resolvedDescriptor.InvokerMethod, descriptorEntry.Descriptor.InvokerMethod) || + if (!resolvedDescriptor.InvokerMethod.Equals(descriptorEntry.Descriptor.InvokerMethod) || resolvedDescriptor.HandlerType != descriptorEntry.Descriptor.HandlerType) { logger.Warn( @@ -546,7 +548,7 @@ internal static class CqrsHandlerRegistrar return false; } - if (!ReferenceEquals(resolvedDescriptor.InvokerMethod, descriptorEntry.Descriptor.InvokerMethod) || + if (!resolvedDescriptor.InvokerMethod.Equals(descriptorEntry.Descriptor.InvokerMethod) || resolvedDescriptor.HandlerType != descriptorEntry.Descriptor.HandlerType) { logger.Warn( 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 9a8beb55..a019a586 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,9 +12,9 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-133` +- 恢复点编号:`CQRS-REWRITE-RP-134` - 当前阶段:`Phase 8` -- 当前 PR 锚点:`PR #347` +- 当前 PR 锚点:`PR #348` - 当前结论: - 本轮按 `$gframework-batch-boot` 协调多波 non-conflicting subagent,基线固定为 `origin/main @ 3b2e6899d5ffdcfb634b28f3846f57528fbf9196 (2026-05-11T12:25:00+08:00)`。 @@ -35,6 +35,11 @@ CQRS 迁移与收敛。 - `docs/zh-CN/core/cqrs.md` - `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md` - `ai-plan/public/cqrs-rewrite/archive/**` 顶部导航与跳转约定 + - 当前 `PR #348` latest-head review 再次复核后: + - 跳过 `NotificationLifetimeBenchmarks.HandlerLifetime` 的 `[GenerateEnumExtensions]` 建议,原因是仓库没有“所有枚举统一生成扩展”的约定,且 benchmark 局部枚举不在该能力的强制范围内 + - 接受并修复 `NotificationLifetimeBenchmarks` 的 scoped 容器释放与公开 XML 文档缺口 + - 接受并修复 `CqrsHandlerRegistrar` 对 generated descriptor 的“先去重后校验”缺陷,并补回归测试锁定“首条无效、后条有效”的同键场景 + - 接受并修复 generated descriptor 校验对 `MethodInfo` 使用 `ReferenceEquals` 的过严比较,改为按方法语义等价匹配 - 当前尚未提交的收尾切片仅剩: - `GFramework.Cqrs.Benchmarks/Messaging/NotificationLifetimeBenchmarks.cs` - `GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs` @@ -46,7 +51,7 @@ CQRS 迁移与收敛。 ## 当前活跃事实 - 当前分支:`feat/cqrs-optimization` -- 当前 PR:`PR #347` +- 当前 PR:`PR #348` - 当前写面: - `GFramework.Cqrs.Benchmarks/README.md` - `GFramework.Cqrs.Benchmarks/Messaging/NotificationLifetimeBenchmarks.cs` @@ -110,7 +115,7 @@ CQRS 迁移与收敛。 ## 下一推荐步骤 1. 先提交当前未提交的 `NotificationLifetime + registration fallback tests + CQRS/legacy docs` 收尾切片,回收工作树到干净状态。 -2. 再次运行 `$gframework-pr-review`,复核 `PR #347` latest-head open thread 是否已随着本轮多波 head 收敛。 +2. 再次运行 `$gframework-pr-review`,复核 `PR #348` latest-head open thread 是否已随着本轮多波 head 收敛。 3. 若继续扩 benchmark,优先从 `GFramework.Cqrs.Benchmarks/README.md` 已明确列出的 gap 中选下一个单文件切片,而不是继续扩大 shared infra 改动面。 ## 活跃文档 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 444ffc87..0afbabfb 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,29 @@ SPDX-License-Identifier: Apache-2.0 ## 2026-05-11 +### 阶段:PR #348 latest-head review 再收口(CQRS-REWRITE-RP-134) + +- 重新执行 `$gframework-pr-review` 抓取当前分支 `feat/cqrs-optimization` 对应的 `PR #348` +- 本轮 latest-head open AI thread 复核结论: + - `NotificationLifetimeBenchmarks.HandlerLifetime` 补 `[GenerateEnumExtensions]` 仍判定为泛化误报 + - 仓库没有“产品/benchmark 枚举默认都启用该特性”的现行约定 + - benchmark 项目也未接入 `GFramework.Core.SourceGenerators.Abstractions`,不应为局部对照枚举平白扩大 generator 依赖面 + - `NotificationLifetimeBenchmarks` 的 `_scopedContainer` 释放缺口与公开 benchmark API 的 XML 契约缺口仍成立,接受修复 + - `CqrsHandlerRegistrar` 中 generated descriptor 的“先去重后校验”缺陷仍成立,接受修复并补测试 + - `CqrsHandlerRegistrar` 对 `MethodInfo` 使用 `ReferenceEquals` 的过严比较仍成立,接受修复 + - active tracking / trace 的当前 PR 锚点仍停留在 `PR #347`,接受同步到 `PR #348` +- 本轮主线程实施: + - `NotificationLifetimeBenchmarks` + - `Cleanup()` 将 `_scopedContainer` 一并交给 `BenchmarkCleanupHelper.DisposeAll(...)` + - 为公开 benchmark 方法与公开 handler 方法补齐缺失的 `` / `` XML 契约 + - `CqrsHandlerRegistrar` + - request / stream generated descriptor 预热路径改为“先 `TryValidate...`,后写入 `registeredKeys`” + - descriptor 对齐判断从 `ReferenceEquals(resolvedDescriptor.InvokerMethod, ...)` 调整为 `resolvedDescriptor.InvokerMethod.Equals(...)` + - `CqrsGeneratedRequestInvokerProviderTests` + - 新增 request / stream 两个回归用例,锁定“首条同键 descriptor 无效、后条有效时,仍应接受后条有效 generated descriptor” + - `ai-plan/public/cqrs-rewrite/**` + - 将 active tracking / trace 的当前 PR 锚点同步到 `PR #348` + ### 阶段:PR #347 latest-head review 收口(CQRS-REWRITE-RP-132) - 使用 `$gframework-pr-review` 重新抓取当前分支 `feat/cqrs-optimization` 对应的 `PR #347`