fix(cqrs): 收口 PR review 剩余问题

- 修复 NotificationLifetimeBenchmarks 的 scoped 容器释放与公开 XML 契约缺口

- 修复 generated descriptor 预热阶段先去重后校验导致的有效后继条目丢失问题

- 更新 generated descriptor 的 MethodInfo 比较方式并补充 request/stream 回归测试

- 同步 cqrs-rewrite active tracking 与 trace 的当前 PR 锚点到 PR #348
This commit is contained in:
gewuyou 2026-05-11 14:52:39 +08:00
parent babd132e81
commit 4e98b63e9c
5 changed files with 236 additions and 13 deletions

View File

@ -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
/// <summary>
/// 直接调用 handler作为不同生命周期矩阵下的 publish 额外开销 baseline。
/// </summary>
/// <returns>代表基线 handler 完成当前 notification 处理的值任务。</returns>
[Benchmark(Baseline = true)]
public ValueTask PublishNotification_Baseline()
{
@ -153,6 +154,7 @@ public class NotificationLifetimeBenchmarks
/// <summary>
/// 通过 GFramework.CQRS runtime 发布 notification。
/// </summary>
/// <returns>代表当前 GFramework.CQRS publish 完成的值任务。</returns>
[Benchmark]
public ValueTask PublishNotification_GFrameworkCqrs()
{
@ -171,6 +173,7 @@ public class NotificationLifetimeBenchmarks
/// <summary>
/// 通过 MediatR 发布 notification作为外部对照。
/// </summary>
/// <returns>代表当前 MediatR publish 完成的任务。</returns>
[Benchmark]
public Task PublishNotification_MediatR()
{
@ -293,6 +296,9 @@ public class NotificationLifetimeBenchmarks
/// <summary>
/// 处理 GFramework.CQRS notification。
/// </summary>
/// <param name="notification">当前要处理的 notification。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>代表当前 notification 处理完成的值任务。</returns>
public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(notification);

View File

@ -548,6 +548,26 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
Assert.That(response, Is.EqualTo("runtime:payload"));
}
/// <summary>
/// 验证当首个 request descriptor 无效、后续同键 descriptor 有效时,
/// registrar 不会因为过早去重而丢掉本可注册的 generated descriptor。
/// </summary>
[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"));
}
/// <summary>
/// 验证当 stream descriptor 枚举项与 provider 的 TryGetDescriptor 结果不一致时,
/// registrar 会忽略该坏 descriptor并继续回退到反射建流路径。
@ -568,6 +588,26 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
Assert.That(results, Is.EqualTo([3, 4]));
}
/// <summary>
/// 验证当首个 stream descriptor 无效、后续同键 descriptor 有效时,
/// registrar 不会因为过早去重而丢掉本可注册的 generated descriptor。
/// </summary>
[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]));
}
/// <summary>
/// 模拟返回实例 request invoker 方法的 generated registry。
/// </summary>
@ -1288,6 +1328,81 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
}
}
/// <summary>
/// 模拟首个 request descriptor 无效、后续同键 descriptor 有效的 generated registry。
/// </summary>
private sealed class InvalidThenValidDuplicateRequestInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsRequestInvokerProvider,
IEnumeratesCqrsRequestInvokerDescriptors
{
private static readonly CqrsRequestInvokerDescriptor InvalidDescriptor = new(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(InvalidThenValidDuplicateRequestInvokerProviderRegistry).GetMethod(
nameof(InvokeAlternativeGenerated),
BindingFlags.NonPublic | BindingFlags.Static)!);
private static readonly CqrsRequestInvokerDescriptor ValidDescriptor = new(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(GeneratedRequestInvokerProviderRegistry).GetMethod(
"InvokeGenerated",
BindingFlags.NonPublic | BindingFlags.Static)!);
/// <inheritdoc />
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(GeneratedRequestInvokerRequestHandler));
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors()
{
return
[
new CqrsRequestInvokerDescriptorEntry(
typeof(GeneratedRequestInvokerRequest),
typeof(string),
InvalidDescriptor),
new CqrsRequestInvokerDescriptorEntry(
typeof(GeneratedRequestInvokerRequest),
typeof(string),
ValidDescriptor)
];
}
private static ValueTask<string> InvokeAlternativeGenerated(
object handler,
object request,
CancellationToken cancellationToken)
{
return ValueTask.FromResult("invalid-first:payload");
}
}
/// <summary>
/// 模拟枚举出的 stream descriptor 与 provider 显式查询结果不一致的 generated registry。
/// </summary>
@ -1356,6 +1471,78 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
}
}
/// <summary>
/// 模拟首个 stream descriptor 无效、后续同键 descriptor 有效的 generated registry。
/// </summary>
private sealed class InvalidThenValidDuplicateStreamInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsStreamInvokerProvider,
IEnumeratesCqrsStreamInvokerDescriptors
{
private static readonly CqrsStreamInvokerDescriptor InvalidDescriptor = new(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(InvalidThenValidDuplicateStreamInvokerProviderRegistry).GetMethod(
nameof(InvokeAlternativeGenerated),
BindingFlags.NonPublic | BindingFlags.Static)!);
private static readonly CqrsStreamInvokerDescriptor ValidDescriptor = new(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(GeneratedStreamInvokerProviderRegistry).GetMethod(
"InvokeGenerated",
BindingFlags.NonPublic | BindingFlags.Static)!);
/// <inheritdoc />
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(GeneratedStreamInvokerRequestHandler));
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public IReadOnlyList<CqrsStreamInvokerDescriptorEntry> 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();
}
}
/// <summary>
/// 创建带有 generated request invoker registry 元数据的程序集替身。
/// </summary>

View File

@ -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(

View File

@ -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 改动面。
## 活跃文档

View File

@ -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 方法补齐缺失的 `<returns>` / `<param>` 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`