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`