fix(cqrs): 收口PR审查遗留问题

- 修复 benchmark 宿主误激活同程序集其他 generated registry 的接线路径,收窄服务索引与 descriptor 基线

- 恢复 CqrsDispatcher.SendAsync 的 faulted ValueTask 失败语义,并补充相关回归测试

- 补充 legacy runtime alias 的防守式类型检查、stream lifetime 注释与 cqrs-rewrite 恢复文档验证记录
This commit is contained in:
gewuyou 2026-05-08 14:10:06 +08:00
parent 39ac61c095
commit 9bd8c34693
15 changed files with 224 additions and 31 deletions

View File

@ -147,7 +147,7 @@ When shorthand is used:
6. After each completed batch:
- integrate or verify the result
- rerun the required validation
- recompute the primary stop-condition metric
- recompute the primary stop-condition metric
- reassess whether one more batch would likely push the agent near or beyond roughly 80% context usage
- decide immediately whether to continue or stop
7. Do not require the user to manually trigger every round unless:

View File

@ -6,6 +6,7 @@ using System.Linq;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Ioc;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Internal;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime;
@ -58,11 +59,11 @@ internal static class BenchmarkHostFactory
var notificationPublisher = container.Get<GFramework.Cqrs.Notification.INotificationPublisher>();
var runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, runtimeLogger, notificationPublisher);
container.Register(runtime);
container.Register<LegacyICqrsRuntime>((LegacyICqrsRuntime)runtime);
RegisterLegacyRuntimeAlias(container, runtime);
}
else if (container.Get<LegacyICqrsRuntime>() is null)
{
container.Register<LegacyICqrsRuntime>((LegacyICqrsRuntime)container.GetRequired<ICqrsRuntime>());
RegisterLegacyRuntimeAlias(container, container.GetRequired<ICqrsRuntime>());
}
if (container.Get<ICqrsHandlerRegistrar>() is null)
@ -81,6 +82,43 @@ internal static class BenchmarkHostFactory
}
}
/// <summary>
/// 只激活当前 benchmark 场景明确拥有的 generated registry避免同一程序集里的其他 benchmark registry
/// 扩大冻结后服务索引与 dispatcher descriptor 基线。
/// </summary>
/// <typeparam name="TRegistry">当前 benchmark 需要接入的 generated registry 类型。</typeparam>
/// <param name="container">承载 generated registry 注册结果的 GFramework benchmark 容器。</param>
internal static void RegisterGeneratedBenchmarkRegistry<TRegistry>(MicrosoftDiContainer container)
where TRegistry : class, GFramework.Cqrs.ICqrsHandlerRegistry
{
ArgumentNullException.ThrowIfNull(container);
var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar");
CqrsHandlerRegistrar.RegisterGeneratedRegistry(container, typeof(TRegistry), registrarLogger);
}
/// <summary>
/// 为旧命名空间下的 CQRS runtime 契约注册兼容别名。
/// </summary>
/// <param name="container">承载 runtime 别名的 benchmark 容器。</param>
/// <param name="runtime">当前正式 CQRS runtime 实例。</param>
/// <exception cref="InvalidOperationException">
/// <paramref name="runtime" /> 未同时实现 legacy CQRS runtime 契约。
/// </exception>
private static void RegisterLegacyRuntimeAlias(MicrosoftDiContainer container, ICqrsRuntime runtime)
{
ArgumentNullException.ThrowIfNull(container);
ArgumentNullException.ThrowIfNull(runtime);
if (runtime is not LegacyICqrsRuntime legacyRuntime)
{
throw new InvalidOperationException(
$"The registered {typeof(ICqrsRuntime).FullName} must also implement {typeof(LegacyICqrsRuntime).FullName}. Actual runtime type: {runtime.GetType().FullName}.");
}
container.Register<LegacyICqrsRuntime>(legacyRuntime);
}
/// <summary>
/// 创建只承载当前 benchmark handler 集合的最小 MediatR 宿主。
/// </summary>

View File

@ -66,7 +66,7 @@ public class RequestBenchmarks
_baselineHandler = new BenchmarkRequestHandler();
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
container.RegisterCqrsHandlersFromAssembly(typeof(RequestBenchmarks).Assembly);
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedDefaultRequestBenchmarkRegistry>(container);
});
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,

View File

@ -83,7 +83,7 @@ public class RequestInvokerBenchmarks
_generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
container.RegisterCqrsHandlersFromAssembly(typeof(RequestInvokerBenchmarks).Assembly);
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedRequestInvokerBenchmarkRegistry>(container);
});
_generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_generatedContainer,

View File

@ -69,7 +69,7 @@ public class RequestPipelineBenchmarks
_baselineHandler = new BenchmarkRequestHandler();
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
container.RegisterCqrsHandlersFromAssembly(typeof(RequestPipelineBenchmarks).Assembly);
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedRequestPipelineBenchmarkRegistry>(container);
RegisterGFrameworkPipelineBehaviors(container, PipelineCount);
});
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(

View File

@ -83,7 +83,7 @@ public class StreamInvokerBenchmarks
_generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
container.RegisterCqrsHandlersFromAssembly(typeof(StreamInvokerBenchmarks).Assembly);
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedStreamInvokerBenchmarkRegistry>(container);
});
_generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_generatedContainer,

View File

@ -93,9 +93,11 @@ public class StreamLifetimeBenchmarks
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
container.RegisterCqrsHandlersFromAssembly(typeof(StreamLifetimeBenchmarks).Assembly);
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedStreamLifetimeBenchmarkRegistry>(container);
RegisterGFrameworkHandler(container, Lifetime);
});
// 容器内已提前保留默认 runtime 以支撑 generated registry 接线;
// 这里额外创建带生命周期后缀的 runtime只是为了区分不同 benchmark 矩阵的 dispatcher 日志。
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,
LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + "." + Lifetime));

View File

@ -67,7 +67,7 @@ public class StreamingBenchmarks
_baselineHandler = new BenchmarkStreamHandler();
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
container.RegisterCqrsHandlersFromAssembly(typeof(StreamingBenchmarks).Assembly);
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedDefaultStreamingBenchmarkRegistry>(container);
});
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,

View File

@ -43,6 +43,57 @@ internal sealed class CqrsDispatcherContextValidationTests
Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext"));
}
/// <summary>
/// 验证 request 上下文校验失败时,<see cref="GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime.SendAsync{TResponse}" />
/// 不会在调用点同步抛出,而是返回一个 faulted <see cref="ValueTask{TResult}" /> 保持既有异步失败语义。
/// </summary>
[Test]
public void SendAsync_Should_Return_Faulted_ValueTask_When_Context_Preparation_Fails()
{
var runtime = CreateRuntime(
container =>
{
container
.Setup(currentContainer => currentContainer.Get(typeof(IRequestHandler<ContextAwareRequest, int>)))
.Returns(new ContextAwareRequestHandler());
container
.Setup(currentContainer => currentContainer.HasRegistration(typeof(IPipelineBehavior<ContextAwareRequest, int>)))
.Returns(false);
});
ValueTask<int> dispatch = default;
Assert.That(
() => { dispatch = runtime.SendAsync(new FakeCqrsContext(), new ContextAwareRequest()); },
Throws.Nothing);
Assert.That(
async () => await dispatch.ConfigureAwait(false),
Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext"));
}
/// <summary>
/// 验证 request handler 缺失时dispatcher 仍返回 faulted <see cref="ValueTask{TResult}" />
/// 而不是在调用点同步抛出异常。
/// </summary>
[Test]
public void SendAsync_Should_Return_Faulted_ValueTask_When_Handler_Is_Missing()
{
var runtime = CreateRuntime(
container =>
{
container
.Setup(currentContainer => currentContainer.Get(typeof(IRequestHandler<ContextAwareRequest, int>)))
.Returns((object?)null);
});
ValueTask<int> dispatch = default;
Assert.That(
() => { dispatch = runtime.SendAsync(new FakeCqrsContext(), new ContextAwareRequest()); },
Throws.Nothing);
Assert.That(
async () => await dispatch.ConfigureAwait(false),
Throws.InvalidOperationException.With.Message.Contains("No CQRS request handler registered"));
}
/// <summary>
/// 验证当 notification handler 需要上下文注入、但当前 CQRS 上下文不实现 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContext" /> 时,
/// dispatcher 会在发布前显式失败。

View File

@ -7,6 +7,7 @@ using GFramework.Core.Architectures;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Internal;
namespace GFramework.Cqrs.Tests.Cqrs;
@ -99,6 +100,32 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
Is.EqualTo([typeof(GeneratedStreamInvokerProviderRegistry)]));
}
/// <summary>
/// 验证 direct generated-registry 激活入口只会接入指定 registry而不会顺手把同一测试程序集里的其他 registry 一并注册。
/// </summary>
[Test]
public void RegisterGeneratedRegistry_Should_Register_Only_The_Selected_Provider()
{
var container = new MicrosoftDiContainer();
var logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsGeneratedRequestInvokerProviderTests));
CqrsHandlerRegistrar.RegisterGeneratedRegistry(
container,
typeof(GeneratedRequestInvokerProviderRegistry),
logger);
var requestProviders = container.GetAll<ICqrsRequestInvokerProvider>();
var streamProviders = container.GetAll<ICqrsStreamInvokerProvider>();
Assert.Multiple(() =>
{
Assert.That(
requestProviders.Select(static provider => provider.GetType()),
Is.EqualTo([typeof(GeneratedRequestInvokerProviderRegistry)]));
Assert.That(streamProviders, Is.Empty);
});
}
/// <summary>
/// 验证当实现类型隐藏、但 stream handler interface 仍可直接表达时,
/// registrar 仍会把 generated stream invoker provider 注册到容器中。

View File

@ -110,30 +110,38 @@ internal sealed class CqrsDispatcher(
IRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(request);
var requestType = request.GetType();
var dispatchBinding = GetRequestDispatchBinding<TResponse>(requestType);
var handler = container.Get(dispatchBinding.HandlerType)
?? throw new InvalidOperationException(
$"No CQRS request handler registered for {requestType.FullName}.");
PrepareHandler(handler, context);
if (!container.HasRegistration(dispatchBinding.BehaviorType))
try
{
return dispatchBinding.RequestInvoker(handler, request, cancellationToken);
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(request);
var requestType = request.GetType();
var dispatchBinding = GetRequestDispatchBinding<TResponse>(requestType);
var handler = container.Get(dispatchBinding.HandlerType)
?? throw new InvalidOperationException(
$"No CQRS request handler registered for {requestType.FullName}.");
PrepareHandler(handler, context);
if (!container.HasRegistration(dispatchBinding.BehaviorType))
{
return dispatchBinding.RequestInvoker(handler, request, cancellationToken);
}
var behaviors = container.GetAll(dispatchBinding.BehaviorType);
foreach (var behavior in behaviors)
{
PrepareHandler(behavior, context);
}
return dispatchBinding.GetPipelineExecutor(behaviors.Count)
.Invoke(handler, behaviors, request, cancellationToken);
}
var behaviors = container.GetAll(dispatchBinding.BehaviorType);
foreach (var behavior in behaviors)
catch (Exception exception)
{
PrepareHandler(behavior, context);
// 保留旧 async 实现的 faulted-ValueTask 失败语义,同时继续复用 direct-return 的热路径。
return ValueTask.FromException<TResponse>(exception);
}
return dispatchBinding.GetPipelineExecutor(behaviors.Count)
.Invoke(handler, behaviors, request, cancellationToken);
}
/// <summary>

View File

@ -68,6 +68,36 @@ internal static class CqrsHandlerRegistrar
}
}
/// <summary>
/// 直接激活并注册单个 generated registry避免调用方为了只接入一个 benchmark registry
/// 而额外扫描同一程序集里的其他 registry / handler。
/// </summary>
/// <param name="container">承载 generated registry 注册结果的目标容器。</param>
/// <param name="registryType">要直接激活的 generated registry 类型。</param>
/// <param name="logger">当前注册过程使用的日志记录器。</param>
/// <exception cref="ArgumentNullException">
/// <paramref name="container" />、<paramref name="registryType" /> 或 <paramref name="logger" /> 为 <see langword="null" />。
/// </exception>
/// <exception cref="InvalidOperationException">指定 registry 类型不满足 generated registry 运行时契约。</exception>
internal static void RegisterGeneratedRegistry(
IIocContainer container,
Type registryType,
ILogger logger)
{
ArgumentNullException.ThrowIfNull(container);
ArgumentNullException.ThrowIfNull(registryType);
ArgumentNullException.ThrowIfNull(logger);
var assemblyName = GetAssemblySortKey(registryType.Assembly);
if (!TryCreateGeneratedRegistry(registryType, assemblyName, logger, out var registry))
{
throw new InvalidOperationException(
$"Unable to activate generated CQRS handler registry {registryType.FullName} in assembly {assemblyName}.");
}
RegisterGeneratedRegistries(container.GetServicesUnsafe, [registry], assemblyName, logger);
}
/// <summary>
/// 优先使用程序集级源码生成注册器完成 CQRS 映射注册。
/// </summary>

View File

@ -0,0 +1,7 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("GFramework.Cqrs.Tests")]
[assembly: InternalsVisibleTo("GFramework.Cqrs.Benchmarks")]

View File

@ -7,10 +7,11 @@ CQRS 迁移与收敛。
## 当前恢复点
- 恢复点编号:`CQRS-REWRITE-RP-108`
- 恢复点编号:`CQRS-REWRITE-RP-109`
- 当前阶段:`Phase 8`
- 当前 PR 锚点:`PR #340`
- 当前 PR 锚点:`PR #341`
- 当前结论:
- 当前 `RP-109` 已使用 `$gframework-pr-review` 复核 `PR #341` latest-head reviewbenchmark 宿主改为定向激活当前场景的 generated registry避免同一 benchmark 程序集里的其他 registry 扩大冻结服务索引与 `HasRegistration` 基线;`BenchmarkHostFactory` 为 legacy runtime alias 注册补齐防守式类型检查与 stream lifetime 运行时注释;`CqrsDispatcher.SendAsync(...)` 在保留 direct-return 热路径的同时恢复 faulted `ValueTask` 失败语义,并补齐 generated registry 定向接线与 request fault 语义回归测试;`.agents/skills/gframework-batch-boot/SKILL.md` 的 MD005 缩进也已顺手修正
- `GFramework.Cqrs` 已完成对外部 `Mediator` 的生产级替代,当前主线已从“是否可替代”转向“仓库内部收口与能力深化顺序”
- `dispatch/invoker` 生成前移已扩展到 request / stream 路径,`RP-077` 已补齐 request invoker provider gate 与 stream gate 对称的 descriptor / descriptor entry runtime 合同回归
- `RP-078` 已补齐 mixed fallback metadata 在 runtime 不允许多个 fallback attribute 实例时的单字符串 attribute 回退回归

View File

@ -2,6 +2,35 @@
## 2026-05-08
### 阶段PR #341 latest-head review 收口CQRS-REWRITE-RP-109
- 使用 `$gframework-pr-review` 抓取 `feat/cqrs-optimization` 当前公开 PR并确认当前锚点已从 `PR #340` 更新为 `PR #341`
- 本轮 latest-head review 结论:
- `CodeRabbit` 仍有 `BenchmarkHostFactory.cs` 的 legacy runtime 硬转型、`StreamLifetimeBenchmarks.cs` 的注释缺口,以及 `.agents/skills/gframework-batch-boot/SKILL.md``MD005` 缩进问题
- `Greptile` 指出的两条仍然成立benchmark 项目里通过 `RegisterCqrsHandlersFromAssembly(typeof(...).Assembly)` 会把同程序集的其他 generated registry 一并激活,扩大 benchmark 宿主的服务索引基线;`CqrsDispatcher.SendAsync(...)` 直接去掉 `async/await` 后也把原本的 faulted-`ValueTask` 失败语义改成了同步抛出
- 本轮主线程决策:
- 在 `GFramework.Cqrs.Internal.CqrsHandlerRegistrar` 新增 direct generated-registry 激活入口,并通过 `InternalsVisibleTo` 暴露给 `GFramework.Cqrs.Benchmarks`,让 benchmark 宿主只激活当前场景的 generated registry
- 把 `RequestBenchmarks``RequestPipelineBenchmarks``StreamingBenchmarks``StreamLifetimeBenchmarks` 以及 request/stream invoker benchmark 的 generated 宿主全部切到定向 registry 接线,避免同程序集其他 registry 扩大冻结索引和 descriptor 预热基线
- 在 `BenchmarkHostFactory` 里用防守式类型检查注册 legacy runtime alias并补充 stream lifetime runtime 二次创建的注释
- 让 `CqrsDispatcher.SendAsync(...)` 通过 `ValueTask.FromException<TResponse>(...)` 恢复旧的 faulted-`ValueTask` 失败语义,同时保留成功路径的 direct-return 热路径
- 补齐 `CqrsGeneratedRequestInvokerProviderTests``CqrsDispatcherContextValidationTests` 的 targeted 回归,并顺手修正 batch boot skill 的 markdown 缩进
- 本轮权威验证:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过,`1 warning / 0 error`
- 备注:仅出现 `MSB3026` 单次复制重试告警,随后成功产出 `net10.0` 目标;未出现编译失败或新增代码警告
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsDispatcherContextValidationTests|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"`
- 结果:通过,`24/24` passed
- 备注:首轮并行验证时因与 build 同时运行触发 MSBuild 输出文件锁竞争;改为串行重跑同一命令后稳定通过
- `python3 scripts/license-header.py --check --paths GFramework.Cqrs/Properties/AssemblyInfo.cs GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs GFramework.Cqrs/Internal/CqrsDispatcher.cs GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs`
- 结果:通过
- 备注:仓库脚本默认内部调用未绑定 worktree 的 `git ls-files`,因此本轮按修改文件列表显式 `--paths` 校验
- `git diff --check`
- 结果:通过
- 本轮下一步:
- 运行 `GFramework.Cqrs` / `GFramework.Cqrs.Benchmarks` 的 Release build、相关 targeted tests、license header check 与 `git diff --check`
### 阶段stream handler 生命周期矩阵 benchmarkCQRS-REWRITE-RP-108
- 延续 `$gframework-batch-boot 50`,本轮继续使用 `origin/main` 作为 branch diff 基线,并先复核: