From 3b4eb3e40a62c3fbd49ac93e20d199385f5ef836 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:45:07 +0800 Subject: [PATCH 01/15] =?UTF-8?q?docs(cqrs):=20=E6=9B=B4=E6=96=B0=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=E4=B8=8E=E5=9B=9E=E9=80=80=E8=AF=AD=E4=B9=89=E8=AF=B4?= =?UTF-8?q?=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 CQRS 入口文档,明确 generated registry 优先与 targeted fallback 的注册顺序 - 修正 README 对 CqrsReflectionFallbackAttribute 的过时描述,补充多实例与 Type 或字符串双合同语义 - 优化 API 参考中的 CQRS 阅读关注点,突出 generated registry 与 targeted fallback contract --- GFramework.Cqrs/README.md | 7 +++++-- docs/zh-CN/api-reference/index.md | 2 +- docs/zh-CN/core/cqrs.md | 14 +++++++++----- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/GFramework.Cqrs/README.md b/GFramework.Cqrs/README.md index 65c3cd99..62547a39 100644 --- a/GFramework.Cqrs/README.md +++ b/GFramework.Cqrs/README.md @@ -20,7 +20,7 @@ - `GeWuYou.GFramework.Cqrs` - 默认 runtime 与业务侧常用基类。 - `GeWuYou.GFramework.Cqrs.SourceGenerators` - - 可选。为消费端程序集生成 `ICqrsHandlerRegistry`,运行时优先走生成注册表;缺失或不适用时,回退到反射扫描。 + - 可选。为消费端程序集生成 `ICqrsHandlerRegistry`,运行时优先走生成注册表;只有缺失、不适用,或 fallback 仍需补齐剩余 handler 时,才继续进入反射路径。 - `GFramework.Core` - 架构上下文中实际调用 `ICqrsRuntime`,并在模块初始化时注册 CQRS 基础设施。 @@ -137,7 +137,10 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu - 同一程序集按稳定键去重,避免重复注册。 - 优先尝试消费端程序集上的 `ICqrsHandlerRegistry` 生成注册器。 -- 生成注册器不可用,或声明了 `CqrsReflectionFallbackAttribute` 时,回退到反射扫描。 +- 生成注册器不可用或元数据损坏时,记录告警并回退到反射扫描。 +- 当程序集声明了 `CqrsReflectionFallbackAttribute` 时,运行时会先执行生成注册器,再只补它未覆盖的 handler。 +- `CqrsReflectionFallbackAttribute` 现在可以多次声明,并同时承载 `Type[]` 与 `string[]` 两类 fallback 清单。 +- 运行时会优先复用 fallback 特性里直接提供的 `Type` 条目,只对字符串条目执行定向 `Assembly.GetType(...)` 查找;只有旧版空 marker 才会退回整程序集扫描。 - 处理器以 transient 方式注册,避免上下文感知处理器在并发请求间共享可变上下文。 如果你走标准 `GFramework.Core` 架构初始化路径,这些步骤通常由框架自动完成;裸容器或测试环境则需要显式补齐 runtime 与注册入口。 diff --git a/docs/zh-CN/api-reference/index.md b/docs/zh-CN/api-reference/index.md index e6fac1dc..7a496c5b 100644 --- a/docs/zh-CN/api-reference/index.md +++ b/docs/zh-CN/api-reference/index.md @@ -29,7 +29,7 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题 | 模块族 | 先看什么 | 继续深入 | XML 文档关注点 | | --- | --- | --- | --- | | `Core` / `Core.Abstractions` | [Core 模块](../core/index.md) | [Core 抽象层说明](../abstractions/core-abstractions.md)、[快速开始](../getting-started/quick-start.md) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 | -| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [CQRS 运行时](../core/cqrs.md) | [CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)、[协程系统](../core/coroutine.md) | request / notification / handler / pipeline / registry / fallback contract | +| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [CQRS 运行时](../core/cqrs.md) | [CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)、[协程系统](../core/coroutine.md) | request / notification / handler / pipeline / generated registry / targeted fallback contract | | `Game` / `Game.Abstractions` / `Game.SourceGenerators` | [Game 模块总览](../game/index.md) | [Game 抽象层说明](../abstractions/game-abstractions.md)、[配置系统](../game/config-system.md) | 配置、数据、设置、场景、UI、存储、序列化契约 | | `Godot` / `Godot.SourceGenerators` | [Godot 模块总览](../godot/index.md) | [Godot 项目生成器](../source-generators/godot-project-generator.md)、[GetNode 生成器](../source-generators/get-node-generator.md)、[BindNodeSignal 生成器](../source-generators/bind-node-signal-generator.md) | 节点扩展、场景 / UI 适配、配置 / 存储 / 设置接线、Godot 生成器入口 | | `Ecs.Arch` / `Ecs.Arch.Abstractions` | [ECS 模块总览](../ecs/index.md) | [Arch ECS 集成](../ecs/arch.md)、[Ecs.Arch 抽象层说明](../abstractions/ecs-arch-abstractions.md) | ECS 模块契约、系统适配、配置对象和运行时装配边界 | diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 7bde2c3a..655f0b66 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -19,7 +19,7 @@ description: Cqrs 模块族的运行时、契约层、生成器入口,以及 | --- | --- | --- | | `GeWuYou.GFramework.Cqrs.Abstractions` | 纯契约层,定义 request、notification、stream、handler、pipeline、runtime seam | 需要把消息契约放到更稳定的共享层,或只依赖接口做解耦 | | `GeWuYou.GFramework.Cqrs` | 默认 runtime,提供 dispatcher、handler 基类、上下文扩展和程序集注册流程 | 大多数直接消费 CQRS 的业务模块 | -| `GeWuYou.GFramework.Cqrs.SourceGenerators` | 编译期生成 `ICqrsHandlerRegistry`,缩小运行时反射扫描范围 | handler 较多,想把注册映射前移到编译期 | +| `GeWuYou.GFramework.Cqrs.SourceGenerators` | 编译期生成 `ICqrsHandlerRegistry`,让运行时先走生成注册器,再只对剩余 handler 做定向 fallback | handler 较多,想把注册映射前移到编译期 | ## 最小接入路径 @@ -156,8 +156,12 @@ protected override void OnInitialize() 1. 优先读取消费端程序集上的 `CqrsHandlerRegistryAttribute` 2. 存在生成注册器时优先使用 `ICqrsHandlerRegistry` 3. 生成注册器不可用或元数据异常时记录告警并回退到反射路径 -4. 如果程序集带有 `CqrsReflectionFallbackAttribute`,只补扫剩余 handler -5. 同一程序集按稳定键去重,避免重复注册 +4. 当生成注册器携带 `CqrsReflectionFallbackAttribute` 元数据时,运行时会先完成生成注册器注册,再补剩余 handler +5. `CqrsReflectionFallbackAttribute` 可以同时携带 `Type[]` 和 `string[]` 两类清单;运行时会优先复用直接 `Type` 条目,只对名称条目做定向 `Assembly.GetType(...)` 查找 +6. 只有旧版空 marker 或生成注册器不可用时,才会回到整程序集反射扫描 +7. 同一程序集按稳定键去重,避免重复注册 + +换句话说,声明 fallback 特性本身不等于“整包反射扫描”。当前推荐理解是:生成注册器负责能静态表达的部分,fallback 只补它覆盖不到的 handler。 `Cqrs.SourceGenerators` 的专题入口见[CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)。 @@ -205,8 +209,8 @@ RegisterCqrsPipelineBehavior>(); | `GFramework.Cqrs.Abstractions/Cqrs/` | `ICqrsRuntime`、`ICqrsHandlerRegistrar`、`IPipelineBehavior<,>`、`IRequestHandler<,>`、`Unit` | 请求、处理器和 runtime seam 的最小契约 | | `GFramework.Cqrs/Command` `Query` `Notification` `Request` `Extensions` | `CommandBase`、`QueryBase`、`NotificationBase`、`RequestBase`、`ContextAwareCqrsExtensions` | 业务侧常用基类和上下文发送入口 | | `GFramework.Cqrs/Cqrs/` | `AbstractCommandHandler<,>`、`AbstractQueryHandler<,>`、`AbstractRequestHandler<,>`、`AbstractStreamCommandHandler<,>`、`AbstractStreamQueryHandler<,>`、`LoggingBehavior<,>` | 默认处理器基类、上下文注入、流式处理与行为管道 | -| `GFramework.Cqrs` 根入口与 `Internal/` | `CqrsRuntimeFactory`、`ICqrsHandlerRegistry`、`CqrsHandlerRegistryAttribute`、`CqrsReflectionFallbackAttribute`、`DefaultCqrsRegistrationService` | runtime 创建入口、registry 协议、fallback 语义和程序集去重规则 | -| `GFramework.Cqrs.SourceGenerators/Cqrs/` | `CqrsHandlerRegistryGenerator`、`RuntimeTypeReferenceSpec`、`OrderedRegistrationKind` | 生成注册器、精确 type lookup 和 fallback 诊断边界 | +| `GFramework.Cqrs` 根入口与 `Internal/` | `CqrsRuntimeFactory`、`ICqrsHandlerRegistry`、`CqrsHandlerRegistryAttribute`、`CqrsReflectionFallbackAttribute`、`DefaultCqrsRegistrationService` | runtime 创建入口、generated-registry 优先级、targeted fallback 语义和程序集去重规则 | +| `GFramework.Cqrs.SourceGenerators/Cqrs/` | `CqrsHandlerRegistryGenerator`、`RuntimeTypeReferenceSpec`、`OrderedRegistrationKind` | 生成注册器、可直接引用类型判定、mixed fallback 发射与诊断边界 | ## 继续阅读 From e81a43680d344e29bf15c4079782b388b7e474c2 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:49:08 +0800 Subject: [PATCH 02/15] =?UTF-8?q?fix(cqrs):=20=E7=BC=93=E5=AD=98=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E7=AE=A1=E9=81=93=E6=89=A7=E8=A1=8C=E5=BD=A2=E7=8A=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化 CqrsDispatcher 的 request pipeline 路径,按请求类型与行为数量缓存 typed executor 形状并在单次分发中绑定当前 handler 与 behaviors - 补充 dispatcher 缓存回归测试,覆盖 pipeline executor 的首次创建、后续复用与行为顺序稳定 --- .../Cqrs/CqrsDispatcherCacheTests.cs | 136 ++++++++++++ .../DispatcherPipelineOrderCacheRequest.cs | 8 + ...patcherPipelineOrderCacheRequestHandler.cs | 21 ++ .../DispatcherPipelineOrderInnerBehavior.cs | 27 +++ .../DispatcherPipelineOrderOuterBehavior.cs | 27 +++ .../Cqrs/DispatcherPipelineOrderState.cs | 20 ++ GFramework.Cqrs/Internal/CqrsDispatcher.cs | 202 +++++++++++++++--- 7 files changed, 406 insertions(+), 35 deletions(-) create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequest.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequestHandler.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderInnerBehavior.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderOuterBehavior.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderState.cs diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs index ee8c549a..13ffb761 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs @@ -24,6 +24,8 @@ internal sealed class CqrsDispatcherCacheTests LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); _container = new MicrosoftDiContainer(); _container.RegisterCqrsPipelineBehavior(); + _container.RegisterCqrsPipelineBehavior(); + _container.RegisterCqrsPipelineBehavior(); CqrsTestRuntime.RegisterHandlers( _container, @@ -145,6 +147,103 @@ internal sealed class CqrsDispatcherCacheTests }); } + /// + /// 验证 request pipeline executor 会按行为数量在 binding 内首次创建并在后续分发中复用。 + /// + [Test] + public async Task Dispatcher_Should_Cache_Request_Pipeline_Executors_Per_Behavior_Count() + { + var requestBindings = GetCacheField("RequestDispatchBindings"); + + Assert.Multiple(() => + { + Assert.That( + GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineCacheRequest), + typeof(int), + 1), + Is.Null); + Assert.That( + GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineOrderCacheRequest), + typeof(int), + 2), + Is.Null); + }); + + await _context!.SendRequestAsync(new DispatcherPipelineCacheRequest()); + await _context.SendRequestAsync(new DispatcherPipelineOrderCacheRequest()); + + var singleBehaviorExecutor = GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineCacheRequest), + typeof(int), + 1); + var twoBehaviorExecutor = GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineOrderCacheRequest), + typeof(int), + 2); + + await _context.SendRequestAsync(new DispatcherPipelineCacheRequest()); + await _context.SendRequestAsync(new DispatcherPipelineOrderCacheRequest()); + + Assert.Multiple(() => + { + Assert.That(singleBehaviorExecutor, Is.Not.Null); + Assert.That(twoBehaviorExecutor, Is.Not.Null); + Assert.That(singleBehaviorExecutor, Is.Not.SameAs(twoBehaviorExecutor)); + Assert.That( + GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineCacheRequest), + typeof(int), + 1), + Is.SameAs(singleBehaviorExecutor)); + Assert.That( + GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineOrderCacheRequest), + typeof(int), + 2), + Is.SameAs(twoBehaviorExecutor)); + }); + } + + /// + /// 验证复用缓存的 request pipeline executor 后,行为顺序和最终处理器顺序保持不变。 + /// + [Test] + public async Task Dispatcher_Should_Preserve_Request_Pipeline_Order_When_Reusing_Cached_Executor() + { + DispatcherPipelineOrderState.Reset(); + + await _context!.SendRequestAsync(new DispatcherPipelineOrderCacheRequest()); + var firstInvocation = DispatcherPipelineOrderState.Steps.ToArray(); + + DispatcherPipelineOrderState.Reset(); + + await _context.SendRequestAsync(new DispatcherPipelineOrderCacheRequest()); + var secondInvocation = DispatcherPipelineOrderState.Steps.ToArray(); + + var expectedOrder = new[] + { + "Outer:Before", + "Inner:Before", + "Handler", + "Inner:After", + "Outer:After" + }; + + Assert.Multiple(() => + { + Assert.That(firstInvocation, Is.EqualTo(expectedOrder)); + Assert.That(secondInvocation, Is.EqualTo(expectedOrder)); + }); + } + /// /// 通过反射读取 dispatcher 的静态缓存对象。 /// @@ -188,6 +287,21 @@ internal sealed class CqrsDispatcherCacheTests return InvokeInstanceMethod(cache, "GetValueOrDefaultForTesting", primaryType, secondaryType); } + /// + /// 读取 request dispatch binding 中指定行为数量的 pipeline executor 缓存项。 + /// + private static object? GetRequestPipelineExecutorValue( + object requestBindings, + Type requestType, + Type responseType, + int behaviorCount) + { + var binding = GetRequestDispatchBindingValue(requestBindings, requestType, responseType); + return binding is null + ? null + : InvokeInstanceMethod(binding, "GetPipelineExecutorForTesting", behaviorCount); + } + /// /// 调用缓存实例上的无参清理方法。 /// @@ -210,6 +324,28 @@ internal sealed class CqrsDispatcherCacheTests return method!.Invoke(target, arguments); } + /// + /// 读取指定请求/响应类型对对应的强类型 request dispatch binding。 + /// + private static object? GetRequestDispatchBindingValue(object requestBindings, Type requestType, Type responseType) + { + var bindingBox = GetPairCacheValue(requestBindings, requestType, responseType); + if (bindingBox is null) + { + return null; + } + + var method = bindingBox.GetType().GetMethod( + "Get", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + Assert.That(method, Is.Not.Null, $"Missing request binding accessor on {bindingBox.GetType().FullName}."); + + return method! + .MakeGenericMethod(responseType) + .Invoke(bindingBox, Array.Empty()); + } + /// /// 获取 CQRS dispatcher 运行时类型。 /// diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequest.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequest.cs new file mode 100644 index 00000000..9528a060 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequest.cs @@ -0,0 +1,8 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 为双行为 pipeline 顺序回归提供最小请求。 +/// +internal sealed record DispatcherPipelineOrderCacheRequest : IRequest; diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequestHandler.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequestHandler.cs new file mode 100644 index 00000000..00224ff8 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequestHandler.cs @@ -0,0 +1,21 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 为双行为顺序回归提供最终请求处理器。 +/// +internal sealed class DispatcherPipelineOrderCacheRequestHandler : IRequestHandler +{ + /// + /// 记录处理器执行并返回固定结果。 + /// + /// 当前请求。 + /// 取消令牌。 + /// 固定整数结果。 + public ValueTask Handle(DispatcherPipelineOrderCacheRequest request, CancellationToken cancellationToken) + { + DispatcherPipelineOrderState.Steps.Add("Handler"); + return ValueTask.FromResult(3); + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderInnerBehavior.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderInnerBehavior.cs new file mode 100644 index 00000000..a4b7def6 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderInnerBehavior.cs @@ -0,0 +1,27 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 作为内层行为验证缓存 executor 复用后仍保持注册顺序。 +/// +internal sealed class DispatcherPipelineOrderInnerBehavior : IPipelineBehavior +{ + /// + /// 记录内层行为的前后执行节点。 + /// + /// 当前请求。 + /// 下一个处理阶段。 + /// 取消令牌。 + /// 下游处理器结果。 + public async ValueTask Handle( + DispatcherPipelineOrderCacheRequest request, + MessageHandlerDelegate next, + CancellationToken cancellationToken) + { + DispatcherPipelineOrderState.Steps.Add("Inner:Before"); + var result = await next(request, cancellationToken).ConfigureAwait(false); + DispatcherPipelineOrderState.Steps.Add("Inner:After"); + return result; + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderOuterBehavior.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderOuterBehavior.cs new file mode 100644 index 00000000..b9ba2315 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderOuterBehavior.cs @@ -0,0 +1,27 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 作为外层行为验证缓存 executor 复用后仍保持注册顺序。 +/// +internal sealed class DispatcherPipelineOrderOuterBehavior : IPipelineBehavior +{ + /// + /// 记录外层行为的前后执行节点。 + /// + /// 当前请求。 + /// 下一个处理阶段。 + /// 取消令牌。 + /// 下游处理器结果。 + public async ValueTask Handle( + DispatcherPipelineOrderCacheRequest request, + MessageHandlerDelegate next, + CancellationToken cancellationToken) + { + DispatcherPipelineOrderState.Steps.Add("Outer:Before"); + var result = await next(request, cancellationToken).ConfigureAwait(false); + DispatcherPipelineOrderState.Steps.Add("Outer:After"); + return result; + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderState.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderState.cs new file mode 100644 index 00000000..2673371e --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderState.cs @@ -0,0 +1,20 @@ +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 记录双行为 pipeline 的实际执行顺序。 +/// +internal static class DispatcherPipelineOrderState +{ + /// + /// 获取按执行顺序追加的步骤名称。 + /// + public static List Steps { get; } = []; + + /// + /// 清空当前记录,供下一次断言使用。 + /// + public static void Reset() + { + Steps.Clear(); + } +} diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index 108f6c03..505299c5 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; @@ -34,7 +35,7 @@ internal sealed class CqrsDispatcher( .GetMethod(nameof(InvokeRequestHandlerAsync), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo RequestPipelineInvokerMethodDefinition = typeof(CqrsDispatcher) - .GetMethod(nameof(InvokeRequestPipelineAsync), BindingFlags.NonPublic | BindingFlags.Static)!; + .GetMethod(nameof(InvokeRequestPipelineExecutorAsync), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo NotificationHandlerInvokerMethodDefinition = typeof(CqrsDispatcher) .GetMethod(nameof(InvokeNotificationHandlerAsync), BindingFlags.NonPublic | BindingFlags.Static)!; @@ -108,7 +109,8 @@ internal sealed class CqrsDispatcher( if (behaviors.Count == 0) return await dispatchBinding.RequestInvoker(handler, request, cancellationToken).ConfigureAwait(false); - return await dispatchBinding.PipelineInvoker(handler, behaviors, request, cancellationToken) + return await dispatchBinding.GetPipelineExecutor(behaviors.Count) + .Invoke(handler, behaviors, request, cancellationToken) .ConfigureAwait(false); } @@ -168,7 +170,7 @@ internal sealed class CqrsDispatcher( typeof(IRequestHandler<,>).MakeGenericType(requestType, typeof(TResponse)), typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)), CreateRequestInvoker(requestType), - CreateRequestPipelineInvoker(requestType)); + requestType); } /// @@ -227,18 +229,6 @@ internal sealed class CqrsDispatcher( return (RequestInvoker)Delegate.CreateDelegate(typeof(RequestInvoker), method); } - /// - /// 生成带管道行为的请求处理委托,避免每次发送都重复反射。 - /// - private static RequestPipelineInvoker CreateRequestPipelineInvoker(Type requestType) - { - var method = RequestPipelineInvokerMethodDefinition - .MakeGenericMethod(requestType, typeof(TResponse)); - return (RequestPipelineInvoker)Delegate.CreateDelegate( - typeof(RequestPipelineInvoker), - method); - } - /// /// 生成通知处理器调用委托,避免每次发布都重复反射。 /// @@ -274,29 +264,20 @@ internal sealed class CqrsDispatcher( } /// - /// 执行包含管道行为链的请求处理。 + /// 执行指定行为数量的强类型 request pipeline executor。 + /// 该入口本身是缓存的固定 executor 形状;每次分发只绑定当前 handler 与 behaviors 实例。 /// - private static ValueTask InvokeRequestPipelineAsync( + private static ValueTask InvokeRequestPipelineExecutorAsync( object handler, IReadOnlyList behaviors, object request, CancellationToken cancellationToken) where TRequest : IRequest { - var typedHandler = (IRequestHandler)handler; - var typedRequest = (TRequest)request; - - MessageHandlerDelegate next = - (message, token) => typedHandler.Handle(message, token); - - for (var i = behaviors.Count - 1; i >= 0; i--) - { - var behavior = (IPipelineBehavior)behaviors[i]; - var currentNext = next; - next = (message, token) => behavior.Handle(message, currentNext, token); - } - - return next(typedRequest, cancellationToken); + var invocation = new RequestPipelineInvocation( + (IRequestHandler)handler, + behaviors); + return invocation.InvokeAsync((TRequest)request, cancellationToken); } /// @@ -424,15 +405,17 @@ internal sealed class CqrsDispatcher( /// /// 保存普通请求分发路径所需的 handler 服务类型、pipeline 服务类型与强类型调用委托。 - /// 该绑定同时覆盖“直接请求处理”和“带 pipeline 的请求处理”两条路径。 + /// 该绑定同时覆盖“直接请求处理”和“按行为数量缓存 pipeline executor 形状”的两条路径。 /// /// 请求响应类型。 private sealed class RequestDispatchBinding( Type handlerType, Type behaviorType, RequestInvoker requestInvoker, - RequestPipelineInvoker pipelineInvoker) + Type requestType) { + private readonly ConcurrentDictionary> _pipelineExecutors = new(); + /// /// 获取请求处理器在容器中的服务类型。 /// @@ -449,8 +432,157 @@ internal sealed class CqrsDispatcher( public RequestInvoker RequestInvoker { get; } = requestInvoker; /// - /// 获取执行 pipeline 行为链的强类型委托。 + /// 获取指定行为数量对应的 pipeline executor。 + /// executor 形状会按请求/响应类型与行为数量缓存,但不会缓存 handler 或 behavior 实例。 /// - public RequestPipelineInvoker PipelineInvoker { get; } = pipelineInvoker; + public RequestPipelineExecutor GetPipelineExecutor(int behaviorCount) + { + ArgumentOutOfRangeException.ThrowIfNegative(behaviorCount); + return _pipelineExecutors.GetOrAdd( + behaviorCount, + count => CreateRequestPipelineExecutor(requestType, count)); + } + + /// + /// 仅供测试读取指定行为数量是否已存在缓存 executor。 + /// + public object? GetPipelineExecutorForTesting(int behaviorCount) + { + _pipelineExecutors.TryGetValue(behaviorCount, out var executor); + return executor; + } + } + + /// + /// 为指定请求/响应类型与固定行为数量创建 pipeline executor。 + /// 行为数量用于表达缓存形状,实际分发仍会消费本次容器解析出的 handler 与 behaviors 实例。 + /// + private static RequestPipelineExecutor CreateRequestPipelineExecutor( + Type requestType, + int behaviorCount) + { + ArgumentOutOfRangeException.ThrowIfNegative(behaviorCount); + + var method = RequestPipelineInvokerMethodDefinition + .MakeGenericMethod(requestType, typeof(TResponse)); + var invoker = (RequestPipelineInvoker)Delegate.CreateDelegate( + typeof(RequestPipelineInvoker), + method); + return new RequestPipelineExecutor(behaviorCount, invoker); + } + + /// + /// 保存固定行为数量下的 typed pipeline executor 形状。 + /// 该对象自身可跨分发复用,但每次调用都只绑定当前 handler 与 behavior 实例。 + /// + /// 请求响应类型。 + private sealed class RequestPipelineExecutor( + int behaviorCount, + RequestPipelineInvoker invoker) + { + /// + /// 获取此 executor 预期处理的行为数量。 + /// + public int BehaviorCount { get; } = behaviorCount; + + /// + /// 使用当前 handler / behaviors / request 执行缓存的 pipeline 形状。 + /// + public ValueTask Invoke( + object handler, + IReadOnlyList behaviors, + object request, + CancellationToken cancellationToken) + { + if (behaviors.Count != BehaviorCount) + { + throw new InvalidOperationException( + $"Cached request pipeline executor expected {BehaviorCount} behaviors, but received {behaviors.Count}."); + } + + return invoker(handler, behaviors, request, cancellationToken); + } + } + + /// + /// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。 + /// 该对象只存在于本次分发,不会跨请求保留容器解析出的实例。 + /// + private sealed class RequestPipelineInvocation( + IRequestHandler handler, + IReadOnlyList behaviors) + where TRequest : IRequest + { + private readonly IRequestHandler _handler = handler; + private readonly IReadOnlyList _behaviors = behaviors; + private readonly MessageHandlerDelegate?[] _continuations = + new MessageHandlerDelegate?[behaviors.Count + 1]; + + /// + /// 从 pipeline 起点执行当前请求。 + /// + public ValueTask InvokeAsync(TRequest request, CancellationToken cancellationToken) + { + return GetContinuation(0)(request, cancellationToken); + } + + /// + /// 获取指定阶段的 continuation,并在首次请求时为该阶段绑定一次不可变调用入口。 + /// 同一行为多次调用 next 时会命中相同 continuation,保持与传统链式委托一致的语义。 + /// + private MessageHandlerDelegate GetContinuation(int index) + { + var continuation = _continuations[index]; + if (continuation is not null) + { + return continuation; + } + + continuation = index == _behaviors.Count + ? InvokeHandlerAsync + : new RequestPipelineContinuation(this, index).InvokeAsync; + _continuations[index] = continuation; + return continuation; + } + + /// + /// 执行指定索引的 pipeline behavior。 + /// + private ValueTask InvokeBehaviorAsync( + int index, + TRequest request, + CancellationToken cancellationToken) + { + var behavior = (IPipelineBehavior)_behaviors[index]; + return behavior.Handle(request, GetContinuation(index + 1), cancellationToken); + } + + /// + /// 调用最终请求处理器。 + /// + private ValueTask InvokeHandlerAsync(TRequest request, CancellationToken cancellationToken) + { + return _handler.Handle(request, cancellationToken); + } + + /// + /// 将固定阶段索引绑定为标准 。 + /// 该包装只在单次分发生命周期内存在,用于把缓存 shape 套入当前实例。 + /// + private sealed class RequestPipelineContinuation( + RequestPipelineInvocation invocation, + int index) + where TCurrentRequest : IRequest + { + /// + /// 执行当前阶段并跳转到下一个 continuation。 + /// + public ValueTask InvokeAsync( + TCurrentRequest request, + CancellationToken cancellationToken) + { + return invocation.InvokeBehaviorAsync(index, request, cancellationToken); + } + } } } From 7b5efde3bdb6de18fd1f88ed4b91ceb5379e1756 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:56:05 +0800 Subject: [PATCH 03/15] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E5=BC=BA=E6=95=B0?= =?UTF-8?q?=E7=BB=84=E7=B1=BB=E5=9E=8B=E7=94=9F=E6=88=90=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增多维数组、交错数组与外部程序集隐藏元素类型的 precise runtime type lookup 回归 - 更新 cqrs-rewrite 跟踪与追踪,记录 RP-053 到 RP-054 的并行批次收口与验证结果 --- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 219 ++++++++++++++++++ .../todos/cqrs-rewrite-migration-tracking.md | 28 ++- .../traces/cqrs-rewrite-migration-trace.md | 44 ++++ 3 files changed, 288 insertions(+), 3 deletions(-) diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 78441c5b..e8cc2378 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -314,6 +314,128 @@ public class CqrsHandlerRegistryGeneratorTests """; + private const string HiddenMultiDimensionalArrayResponseSource = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed class Container + { + private sealed record HiddenResponse(); + + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenHandler : IRequestHandler { } + } + } + """; + + private const string HiddenJaggedArrayResponseSource = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed class Container + { + private sealed record HiddenResponse(); + + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenHandler : IRequestHandler { } + } + } + """; + private const string HiddenGenericEnvelopeResponseSource = """ using System; @@ -963,6 +1085,24 @@ public class CqrsHandlerRegistryGeneratorTests } """; + private const string ExternalProtectedMultiDimensionalTypeDependencySource = """ + using GFramework.Cqrs.Abstractions.Cqrs; + + namespace Dep; + + public abstract class VisibilityScope + { + protected internal sealed record ProtectedResponse(); + + protected internal sealed record ProtectedRequest() : IRequest; + } + + public abstract class HandlerBase : + IRequestHandler + { + } + """; + private const string LegacyFallbackMarkerHiddenHandlerSource = """ using System; @@ -1590,6 +1730,52 @@ public class CqrsHandlerRegistryGeneratorTests ("CqrsHandlerRegistry.g.cs", HiddenGenericEnvelopeResponseExpected)); } + /// + /// 验证精确重建路径会保留隐藏元素类型的多维数组秩信息, + /// 使生成注册器继续走定向运行时类型重建,而不是退回宽松接口发现。 + /// + [Test] + public void Generates_Precise_Service_Type_For_Hidden_MultiDimensional_Array_Type_Arguments() + { + var generatedSource = RunGenerator(HiddenMultiDimensionalArrayResponseSource); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "var serviceType0_0Argument1Element = registryAssembly.GetType(\"TestApp.Container+HiddenResponse\", throwOnError: false, ignoreCase: false);")); + Assert.That( + generatedSource, + Does.Contain( + "var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1Element.MakeArrayType(2));")); + Assert.That(generatedSource, Does.Not.Contain("RegisterRemainingReflectedHandlerInterfaces(")); + }); + } + + /// + /// 验证精确重建路径会递归覆盖交错数组, + /// 确保隐藏元素类型的每一层数组都继续通过数组发射分支稳定重建。 + /// + [Test] + public void Generates_Precise_Service_Type_For_Hidden_Jagged_Array_Type_Arguments() + { + var generatedSource = RunGenerator(HiddenJaggedArrayResponseSource); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "var serviceType0_0Argument1ElementElement = registryAssembly.GetType(\"TestApp.Container+HiddenResponse\", throwOnError: false, ignoreCase: false);")); + Assert.That( + generatedSource, + Does.Contain( + "var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1ElementElement.MakeArrayType().MakeArrayType());")); + Assert.That(generatedSource, Does.Not.Contain("RegisterRemainingReflectedHandlerInterfaces(")); + }); + } + /// /// 验证当 handler 合同把 pointer 响应类型放进 CQRS 泛型参数时, /// 生成器会保守回退而不是继续发射不可构造的精确注册代码。 @@ -1682,6 +1868,39 @@ public class CqrsHandlerRegistryGeneratorTests Is.EqualTo(ExternalAssemblyPreciseLookupExpected)); } + /// + /// 验证当外部程序集隐藏元素类型以多维数组形式参与 CQRS 合同时, + /// 生成器仍会保留外部程序集定向查找与数组秩信息,而不是退回 fallback 元数据。 + /// + [Test] + public void Generates_Precise_Assembly_Type_Lookups_For_Inaccessible_External_MultiDimensional_Array_Elements() + { + var contractsReference = MetadataReferenceTestBuilder.CreateFromSource( + "Contracts", + ExternalProtectedTypeContractsSource); + var dependencyReference = MetadataReferenceTestBuilder.CreateFromSource( + "Dependency", + ExternalProtectedMultiDimensionalTypeDependencySource, + contractsReference); + var generatedSource = RunGenerator( + ExternalProtectedTypeLookupSource, + contractsReference, + dependencyReference); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "var serviceType0_0Argument1Element = ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedResponse\");")); + Assert.That( + generatedSource, + Does.Contain( + "var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1Element.MakeArrayType(2));")); + Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute(")); + }); + } + /// /// 验证即使 runtime 仍暴露旧版无参 fallback marker,生成器也会优先在生成注册器内部处理隐藏 handler, /// 不再输出 fallback marker。 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 64365852..7390846d 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 @@ -7,7 +7,7 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-052` +- 恢复点编号:`CQRS-REWRITE-RP-054` - 当前阶段:`Phase 8` - 当前焦点: - 当前功能历史已归档,active 跟踪仅保留 `Phase 8` 主线的恢复入口 @@ -15,6 +15,8 @@ CQRS 迁移与收敛。 - `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出 - 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据 - 当 runtime 不支持多实例 fallback 特性或缺少对应构造函数时,mixed fallback 场景仍会整体保守回退到字符串元数据,避免仅部分 handler 走 `Type[]` 时漏掉剩余需按名称恢复的 handlers + - 已完成 request pipeline executor 形状缓存:`CqrsDispatcher` 现会在单个 request binding 内按 `behaviorCount` 复用强类型 pipeline executor,而不是每次 `SendAsync` 都重建整条 `next` 委托链 + - 已补充 dispatcher pipeline executor 缓存与双行为顺序回归,锁定缓存复用后仍保持现有行为执行顺序 - 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke` - 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物 - 已修正 pointer / function pointer 泛型合同的错误覆盖:生成器不再为这两类类型发射 precise runtime type 重建代码 @@ -71,6 +73,17 @@ CQRS 迁移与收敛。 - latest reviewed commit 当前剩余 `3` 条 open AI review threads:`2` 条 Greptile、`1` 条 CodeRabbit - 本地核对后确认 `dotnet-format` 仍只有 `Restore operation failed` 噪音,没有附带当前仍成立的文件级格式诊断 - 已按 review triage 修正 generator source preamble 的多实例 fallback 特性排版、移除死参数,并补强 mixed/direct fallback 发射回归断言与 XML 文档 +- `2026-04-29` 已完成一轮 precise runtime type lookup 的数组回归补强: + - `GFramework.SourceGenerators.Tests` 已新增多维数组、交错数组、外部程序集隐藏元素类型三类回归 + - 当前生成器在 precise runtime type lookup 下已稳定保留数组秩信息,并递归发射交错数组的 `MakeArrayType()` 链 + - 本轮定向测试未暴露数组发射缺陷,因此未改动 fallback 合同选择逻辑,也未调整 direct / named / mixed fallback 排版路径 +- `2026-04-29` 已完成一轮 request pipeline executor 形状缓存: + - `CqrsDispatcher` 现会继续按 `requestType + responseType` 缓存 request dispatch binding,并在 binding 内按 `behaviorCount` 缓存强类型 pipeline executor + - 每次分发只绑定当前 handler / behaviors 实例,不缓存容器解析结果,因此不改变 transient 生命周期与上下文注入语义 + - `GFramework.Cqrs.Tests` 已补充 executor 首次创建 / 后续复用与双行为顺序回归 +- `2026-04-29` 已完成一轮 CQRS 入口文档对齐: + - `GFramework.Cqrs/README.md`、`docs/zh-CN/core/cqrs.md` 与 `docs/zh-CN/api-reference/index.md` 现已明确 generated registry 优先、targeted fallback 补齐剩余 handler 的当前语义 + - 当前工作区相对 `origin/main` 的累计 diff 已达到 `13 files / 709 lines`,仍低于本轮 `gframework-batch-boot 50` 的主要 stop condition - 当前主线优先级: - generator 覆盖面继续扩大 - dispatch/invoker 反射占比继续下降 @@ -133,9 +146,18 @@ CQRS 迁移与收敛。 - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` - 结果:通过 - 备注:`13/13` 测试通过;本轮确认 mixed fallback metadata 的 registrar 消费路径未回归 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`21/21` 测试通过;本轮新增多维数组、交错数组与外部程序集隐藏元素类型的 precise runtime type lookup 回归 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` + - 结果:通过 + - 备注:`4/4` 测试通过;本轮覆盖 request pipeline executor 的首次创建、复用与双行为顺序回归 +- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;本轮确认 dispatcher request pipeline 形状缓存未破坏 `net8.0` / `net9.0` / `net10.0` 目标构建 ## 下一步 -1. 继续 `Phase 8` 主线,优先再找一个收益明确的 generator 覆盖缺口,继续减少仍必须依赖字符串 fallback 元数据的 handler 类型形态 -2. 若继续文档主线,优先再扫 `docs/zh-CN/api-reference` 与教程入口页,补齐仍过时的 CQRS API / 命名空间表述 +1. 继续 `Phase 8` 主线,优先再找一个收益明确且写集独立的 generator 或 dispatch 热点;当前累计 diff 为 `13 files / 709 lines`,距离 `50 files` stop condition 仍有余量 +2. 若继续文档主线,优先再扫教程入口页与 API 参考中的 CQRS 采用说明,确认是否还有旧 Command / Query 迁移口径残留 3. 若后续再出现新的 PR review 或 review thread 变化,再重新执行 `$gframework-pr-review` 作为独立验证步骤 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 cb008db1..d01396a5 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 @@ -2,6 +2,50 @@ ## 2026-04-29 +### 阶段:低风险并行批次收口(CQRS-REWRITE-RP-054) + +- 继续按 `gframework-batch-boot 50` 推进 `Phase 8`,本轮先完成批次评估后再并行拆分写集,避免把 generator、runtime 与 docs 改动揉进同一片上下文 +- 先复核当前 worktree、active tracking 与 `origin/main` 基线后确认: + - 当前分支头最初与 `origin/main` 对齐,批次阈值从 `0 files / 0 lines` 起算 + - 本轮可以安全拆成三个互不冲突的切片:request pipeline executor 形状缓存、precise runtime type lookup 数组回归补强、CQRS 入口文档对齐 + - 主线程保留集成与验证职责,subagent 只负责各自写集 +- 已接受并整合的并行写集: + - docs 切片:更新 `GFramework.Cqrs/README.md`、`docs/zh-CN/core/cqrs.md`、`docs/zh-CN/api-reference/index.md`,明确 generated registry 优先、targeted fallback 只补剩余 handler + - generator 切片:在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增多维数组、交错数组、外部程序集隐藏元素类型三组 precise lookup 回归 + - dispatcher 切片:在 `GFramework.Cqrs/Internal/CqrsDispatcher.cs` 中将 request pipeline 从“每次分发重建 next 链”收敛为“binding 内按 behaviorCount 缓存 executor 形状”,并补充 dispatcher cache / 顺序回归 +- docs 切片已作为独立提交落地: + - `66830ba2` `docs(cqrs): 更新入口与回退语义说明` +- 本轮定向验证已通过: + - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - `0 warning / 0 error` + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` + - `4/4` passed + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - `21/21` passed +- 本轮停止时,当前工作区相对 `origin/main` 的累计 diff 为 `13 files / 709 lines` +- 结论: + - primary stop condition `50 files` 尚未触发,本轮停止是因为三条低风险切片已收口完毕 + - 下一批更适合重新做一轮热点筛选,而不是在同一轮继续扩写集 + +### 阶段:precise runtime type lookup 数组回归补强(CQRS-REWRITE-RP-053) + +- 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮选择一个更窄的 generator 覆盖缺口:锁定 precise runtime type lookup 下数组类型形态的回归 +- 先复核当前实现后确认: + - `TryCreateRuntimeTypeReference` 已会把 `IArrayTypeSymbol` 递归建模为 `RuntimeTypeReferenceSpec.FromArray(element, rank)` + - `AppendArrayRuntimeTypeReferenceResolution` 已按 `ArrayRank == 1` 发射 `MakeArrayType()`,按 `rank > 1` 发射 `MakeArrayType(rank)` + - 当前缺口主要是测试面不足,尚未显式覆盖多维数组、交错数组、外部程序集隐藏元素类型这三类 precise lookup 场景 +- 已在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 补充三组回归: + - 隐藏元素类型的多维数组响应,锁定 `MakeArrayType(2)` 发射 + - 隐藏元素类型的交错数组响应,锁定递归 `MakeArrayType().MakeArrayType()` 发射 + - 外部程序集隐藏元素类型的多维数组响应,锁定 `ResolveReferencedAssemblyType(...)` 与 `MakeArrayType(2)` 的组合 +- 本轮定向测试全部通过,未暴露数组发射缺陷: + - 因此没有修改 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs` + - 也没有改动 `CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs` + - fallback 合同选择逻辑与 direct / named / mixed fallback 排版路径保持不变 +- 定向验证已通过: + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - `21/21` passed + ### 阶段:mixed fallback 元数据拆分(CQRS-REWRITE-RP-052) - 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮把上一批的“全部可直接引用 fallback handlers 走 `Type[]`”继续推进到 mixed 场景 From e51b64f8d52491d9f48b7a6c36c6c0db81c507a0 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:33:27 +0800 Subject: [PATCH 04/15] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E9=BD=90=E5=A4=96?= =?UTF-8?q?=E9=83=A8=E9=9A=90=E8=97=8F=E6=B3=9B=E5=9E=8B=E7=B2=BE=E7=A1=AE?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增外部程序集隐藏泛型定义与可见类型实参的 precise registration 回归 - 更新 CQRS 重写跟踪与 trace,记录本轮覆盖范围和验证结果 --- GFramework.Cqrs/Internal/CqrsDispatcher.cs | 18 ++++-- .../Internal/CqrsHandlerRegistrar.cs | 6 +- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 58 +++++++++++++++++++ .../todos/cqrs-rewrite-migration-tracking.md | 7 +++ .../traces/cqrs-rewrite-migration-trace.md | 4 ++ 5 files changed, 86 insertions(+), 7 deletions(-) diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index 505299c5..17e81e8e 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -62,7 +62,7 @@ internal sealed class CqrsDispatcher( var notificationType = notification.GetType(); var dispatchBinding = NotificationDispatchBindings.GetOrAdd( notificationType, - CreateNotificationDispatchBinding); + static notificationType => CreateNotificationDispatchBinding(notificationType)); var handlers = container.GetAll(dispatchBinding.HandlerType); if (handlers.Count == 0) @@ -134,7 +134,7 @@ internal sealed class CqrsDispatcher( var dispatchBinding = StreamDispatchBindings.GetOrAdd( requestType, typeof(TResponse), - CreateStreamDispatchBinding); + static (requestType, responseType) => CreateStreamDispatchBinding(requestType, responseType)); var handler = container.Get(dispatchBinding.HandlerType) ?? throw new InvalidOperationException( $"No CQRS stream handler registered for {requestType.FullName}."); @@ -181,7 +181,8 @@ internal sealed class CqrsDispatcher( var bindingBox = RequestDispatchBindings.GetOrAdd( requestType, typeof(TResponse), - CreateRequestDispatchBindingBox); + static (cachedRequestType, cachedResponseType) => + CreateRequestDispatchBindingBox(cachedRequestType, cachedResponseType)); return bindingBox.Get(); } @@ -438,9 +439,10 @@ internal sealed class CqrsDispatcher( public RequestPipelineExecutor GetPipelineExecutor(int behaviorCount) { ArgumentOutOfRangeException.ThrowIfNegative(behaviorCount); - return _pipelineExecutors.GetOrAdd( + return _pipelineExecutors.GetOrAdd>( behaviorCount, - count => CreateRequestPipelineExecutor(requestType, count)); + static (count, state) => CreateRequestPipelineExecutor(state.RequestType, count), + new RequestPipelineExecutorFactoryState(requestType)); } /// @@ -504,6 +506,12 @@ internal sealed class CqrsDispatcher( } } + /// + /// 为 pipeline executor 缓存携带当前请求类型,避免按行为数量建缓存时创建闭包。 + /// + /// 请求响应类型。 + private readonly record struct RequestPipelineExecutorFactoryState(Type RequestType); + /// /// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。 /// 该对象只存在于本次分发,不会跨请求保留容器解析出的实例。 diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index 21db7821..1be2729c 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -83,7 +83,8 @@ internal static class CqrsHandlerRegistrar { var assemblyMetadata = AssemblyMetadataCache.GetOrAdd( assembly, - key => AnalyzeAssemblyRegistrationMetadata(key, logger)); + logger, + static (key, state) => AnalyzeAssemblyRegistrationMetadata(key, state)); var registryTypes = assemblyMetadata.RegistryTypes; if (registryTypes.Count == 0) @@ -442,7 +443,8 @@ internal static class CqrsHandlerRegistrar { return LoadableTypesCache.GetOrAdd( assembly, - key => LoadAndSortTypes(key, logger)); + logger, + static (key, state) => LoadAndSortTypes(key, state)); } /// diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index e8cc2378..51926a0d 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1103,6 +1103,26 @@ public class CqrsHandlerRegistryGeneratorTests } """; + private const string ExternalProtectedGenericDefinitionDependencySource = """ + using GFramework.Cqrs.Abstractions.Cqrs; + + namespace Dep; + + public abstract class VisibilityScope + { + protected internal sealed class ProtectedEnvelope + { + } + + protected internal sealed record ProtectedRequest() : IRequest>; + } + + public abstract class HandlerBase : + IRequestHandler> + { + } + """; + private const string LegacyFallbackMarkerHiddenHandlerSource = """ using System; @@ -1901,6 +1921,44 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当外部程序集隐藏泛型定义以“隐藏定义 + 可见类型实参”的形式参与 CQRS 合同时, + /// 生成器会继续输出定向程序集查找与运行时泛型重建,而不是退回字符串 fallback 元数据。 + /// + [Test] + public void Generates_Precise_Assembly_Type_Lookups_For_Inaccessible_External_Generic_Definitions_With_Visible_Type_Arguments() + { + var contractsReference = MetadataReferenceTestBuilder.CreateFromSource( + "Contracts", + ExternalProtectedTypeContractsSource); + var dependencyReference = MetadataReferenceTestBuilder.CreateFromSource( + "Dependency", + ExternalProtectedGenericDefinitionDependencySource, + contractsReference); + var generatedSource = RunGenerator( + ExternalProtectedTypeLookupSource, + contractsReference, + dependencyReference); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "var serviceType0_0Argument0 = ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedRequest\");")); + Assert.That( + generatedSource, + Does.Contain( + "var serviceType0_0Argument1GenericDefinition = ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedEnvelope`1\");")); + Assert.That( + generatedSource, + Does.Contain( + "var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1GenericDefinition.MakeGenericType(typeof(string)));")); + Assert.That(generatedSource, Does.Not.Contain("RegisterRemainingReflectedHandlerInterfaces(")); + Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute(")); + }); + } + /// /// 验证即使 runtime 仍暴露旧版无参 fallback marker,生成器也会优先在生成注册器内部处理隐藏 handler, /// 不再输出 fallback marker。 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 7390846d..6825adb2 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 @@ -77,6 +77,10 @@ CQRS 迁移与收敛。 - `GFramework.SourceGenerators.Tests` 已新增多维数组、交错数组、外部程序集隐藏元素类型三类回归 - 当前生成器在 precise runtime type lookup 下已稳定保留数组秩信息,并递归发射交错数组的 `MakeArrayType()` 链 - 本轮定向测试未暴露数组发射缺陷,因此未改动 fallback 合同选择逻辑,也未调整 direct / named / mixed fallback 排版路径 +- `2026-04-29` 已补齐一轮外部程序集隐藏泛型定义回归覆盖: + - `GFramework.SourceGenerators.Tests` 已新增“外部程序集隐藏泛型定义 + 可见类型实参”的 precise registration 回归 + - 当前生成器会继续为这类 handler 合同发射 `ResolveReferencedAssemblyType(...) + MakeGenericType(...)` 组合,而不是退回字符串 fallback 元数据 + - 本轮定向测试未暴露新的实现缺口,因此未改动 direct / named / mixed fallback 选择逻辑,也未调整 generator runtime type 建模实现 - `2026-04-29` 已完成一轮 request pipeline executor 形状缓存: - `CqrsDispatcher` 现会继续按 `requestType + responseType` 缓存 request dispatch binding,并在 binding 内按 `behaviorCount` 缓存强类型 pipeline executor - 每次分发只绑定当前 handler / behaviors 实例,不缓存容器解析结果,因此不改变 transient 生命周期与上下文注入语义 @@ -149,6 +153,9 @@ CQRS 迁移与收敛。 - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - 结果:通过 - 备注:`21/21` 测试通过;本轮新增多维数组、交错数组与外部程序集隐藏元素类型的 precise runtime type lookup 回归 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`22/22` 测试通过;本轮新增“外部程序集隐藏泛型定义 + 可见类型实参”的 precise registration 回归,确认仍走定向运行时类型重建 - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` - 结果:通过 - 备注:`4/4` 测试通过;本轮覆盖 request pipeline executor 的首次创建、复用与双行为顺序回归 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 d01396a5..fc868571 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 @@ -9,6 +9,10 @@ - 当前分支头最初与 `origin/main` 对齐,批次阈值从 `0 files / 0 lines` 起算 - 本轮可以安全拆成三个互不冲突的切片:request pipeline executor 形状缓存、precise runtime type lookup 数组回归补强、CQRS 入口文档对齐 - 主线程保留集成与验证职责,subagent 只负责各自写集 +- 本轮继续收口一个更窄的 generator 覆盖缺口: + - 在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增“外部程序集隐藏泛型定义 + 可见类型实参”的 precise registration 回归 + - 该回归锁定生成器会输出 `ResolveReferencedAssemblyType("...ProtectedEnvelope\`1")` 与 `MakeGenericType(typeof(string))` 的组合,而不是退回程序集级字符串 fallback + - 定向测试 `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` 通过,结果为 `22/22` passed,因此本轮未触发 `RuntimeTypeReferences` / `SourceEmission` 的实现修正 - 已接受并整合的并行写集: - docs 切片:更新 `GFramework.Cqrs/README.md`、`docs/zh-CN/core/cqrs.md`、`docs/zh-CN/api-reference/index.md`,明确 generated registry 优先、targeted fallback 只补剩余 handler - generator 切片:在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增多维数组、交错数组、外部程序集隐藏元素类型三组 precise lookup 回归 From 5365f9aec2ae25dc505aa8fce3f2b6b1a5cf9ddd Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:40:44 +0800 Subject: [PATCH 05/15] =?UTF-8?q?refactor(cqrs):=20=E5=88=A0=E9=99=A4=20po?= =?UTF-8?q?inter=20=E8=BF=90=E8=A1=8C=E6=97=B6=E9=87=8D=E5=BB=BA=E6=AE=8B?= =?UTF-8?q?=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 CqrsHandlerRegistryGenerator 的运行时类型引用模型,移除不可达的 pointer 子结构 - 删除 SourceEmission 中已失效的 MakePointerType 发射分支,保持 pointer 拒绝语义不变 - 更新 cqrs-rewrite 跟踪与 trace,记录本轮清理和定向验证结果 --- .../CqrsHandlerRegistryGenerator.Models.cs | 6 --- ...RegistryGenerator.RuntimeTypeReferences.cs | 6 --- ...HandlerRegistryGenerator.SourceEmission.cs | 34 --------------- .../todos/cqrs-rewrite-migration-tracking.md | 27 ++++++++++-- .../traces/cqrs-rewrite-migration-trace.md | 42 ++++++++++++++++++- 5 files changed, 65 insertions(+), 50 deletions(-) diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs index e86ec1a0..ccd7e8cb 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs @@ -60,7 +60,6 @@ public sealed partial class CqrsHandlerRegistryGenerator string? ReflectionAssemblyName, RuntimeTypeReferenceSpec? ArrayElementTypeReference, int ArrayRank, - RuntimeTypeReferenceSpec? PointerElementTypeReference, RuntimeTypeReferenceSpec? GenericTypeDefinitionReference, ImmutableArray GenericTypeArguments) { @@ -76,7 +75,6 @@ public sealed partial class CqrsHandlerRegistryGenerator null, 0, null, - null, ImmutableArray.Empty); } @@ -92,7 +90,6 @@ public sealed partial class CqrsHandlerRegistryGenerator null, 0, null, - null, ImmutableArray.Empty); } @@ -110,7 +107,6 @@ public sealed partial class CqrsHandlerRegistryGenerator null, 0, null, - null, ImmutableArray.Empty); } @@ -126,7 +122,6 @@ public sealed partial class CqrsHandlerRegistryGenerator elementTypeReference, arrayRank, null, - null, ImmutableArray.Empty); } @@ -143,7 +138,6 @@ public sealed partial class CqrsHandlerRegistryGenerator null, null, 0, - null, genericTypeDefinitionReference, genericTypeArguments); } diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs index bce72464..8157d45d 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs @@ -304,12 +304,6 @@ public sealed partial class CqrsHandlerRegistryGenerator return true; } - if (runtimeTypeReference.PointerElementTypeReference is not null && - ContainsExternalAssemblyTypeLookup(runtimeTypeReference.PointerElementTypeReference)) - { - return true; - } - if (runtimeTypeReference.GenericTypeDefinitionReference is not null && ContainsExternalAssemblyTypeLookup(runtimeTypeReference.GenericTypeDefinitionReference)) { diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs index 4db622cb..9dd72b7f 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs @@ -662,14 +662,6 @@ public sealed partial class CqrsHandlerRegistryGenerator reflectedArgumentNames, indent); - if (runtimeTypeReference.PointerElementTypeReference is not null) - return AppendPointerRuntimeTypeReferenceResolution( - builder, - runtimeTypeReference, - variableBaseName, - reflectedArgumentNames, - indent); - if (runtimeTypeReference.GenericTypeDefinitionReference is not null) return AppendConstructedGenericRuntimeTypeReferenceResolution( builder, @@ -714,32 +706,6 @@ public sealed partial class CqrsHandlerRegistryGenerator : $"{elementExpression}.MakeArrayType({runtimeTypeReference.ArrayRank})"; } - /// - /// 发射指针类型引用的运行时重建表达式。 - /// - /// 生成源码构造器。 - /// 指针类型引用描述。 - /// 用于递归生成变量名的稳定前缀。 - /// 需要空值检查的反射解析变量集合。 - /// 当前生成语句的缩进。 - /// 指针类型表达式。 - private static string AppendPointerRuntimeTypeReferenceResolution( - StringBuilder builder, - RuntimeTypeReferenceSpec runtimeTypeReference, - string variableBaseName, - ICollection reflectedArgumentNames, - string indent) - { - var pointedAtExpression = AppendRuntimeTypeReferenceResolution( - builder, - runtimeTypeReference.PointerElementTypeReference!, - $"{variableBaseName}PointedAt", - reflectedArgumentNames, - indent); - - return $"{pointedAtExpression}.MakePointerType()"; - } - /// /// 发射已构造泛型类型引用的运行时重建表达式。 /// 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 6825adb2..af69ff10 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 @@ -7,7 +7,7 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-054` +- 恢复点编号:`CQRS-REWRITE-RP-056` - 当前阶段:`Phase 8` - 当前焦点: - 当前功能历史已归档,active 跟踪仅保留 `Phase 8` 主线的恢复入口 @@ -23,6 +23,7 @@ CQRS 迁移与收敛。 - 已补充非法 CQRS 泛型合同的输入诊断断言,明确 `CS0306` 与 fallback / diagnostic 路径的组合语义 - 已为 registrar 的 reflection 注册路径补充 handler-interface 元数据缓存,减少跨容器重复注册时的 `GetInterfaces()` 反射 - 已将 registrar 的重复映射判定从线性扫描 `IServiceCollection` 收敛为本地映射索引,减少 fallback 注册路径的重复查找 + - 已完成一轮 `static lambda + state` 微收敛:`CqrsDispatcher` 与 `CqrsHandlerRegistrar` 现会在弱缓存 / 并发缓存入口优先使用无捕获工厂,继续压低热路径上的额外闭包分配 - 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点 ## 当前状态摘要 @@ -81,13 +82,21 @@ CQRS 迁移与收敛。 - `GFramework.SourceGenerators.Tests` 已新增“外部程序集隐藏泛型定义 + 可见类型实参”的 precise registration 回归 - 当前生成器会继续为这类 handler 合同发射 `ResolveReferencedAssemblyType(...) + MakeGenericType(...)` 组合,而不是退回字符串 fallback 元数据 - 本轮定向测试未暴露新的实现缺口,因此未改动 direct / named / mixed fallback 选择逻辑,也未调整 generator runtime type 建模实现 +- `2026-04-29` 已完成一轮缓存工厂闭包收敛: + - `CqrsDispatcher` 现会在 notification / stream / request binding 与 pipeline executor 缓存入口优先使用无捕获工厂 + - `CqrsHandlerRegistrar` 现会在程序集元数据缓存与可加载类型缓存入口复用 `static` 工厂 + 显式状态参数 + - 本轮未改动公开语义,也未修改 fallback 合同与 handler / behavior 生命周期边界 - `2026-04-29` 已完成一轮 request pipeline executor 形状缓存: - `CqrsDispatcher` 现会继续按 `requestType + responseType` 缓存 request dispatch binding,并在 binding 内按 `behaviorCount` 缓存强类型 pipeline executor - 每次分发只绑定当前 handler / behaviors 实例,不缓存容器解析结果,因此不改变 transient 生命周期与上下文注入语义 - `GFramework.Cqrs.Tests` 已补充 executor 首次创建 / 后续复用与双行为顺序回归 - `2026-04-29` 已完成一轮 CQRS 入口文档对齐: - `GFramework.Cqrs/README.md`、`docs/zh-CN/core/cqrs.md` 与 `docs/zh-CN/api-reference/index.md` 现已明确 generated registry 优先、targeted fallback 补齐剩余 handler 的当前语义 - - 当前工作区相对 `origin/main` 的累计 diff 已达到 `13 files / 709 lines`,仍低于本轮 `gframework-batch-boot 50` 的主要 stop condition +- `2026-04-29` 已完成一轮 generator pointer runtime-reconstruction 残留清理: + - `CqrsHandlerRegistryGenerator` 的运行时类型引用模型已移除不可达的 pointer 子结构 + - `SourceEmission` 不再保留 `MakePointerType()` 源码发射分支,`RuntimeTypeReferences` 也已删掉对应的外部程序集递归扫描死代码 + - pointer / function pointer 的拒绝语义保持不变,direct / named / mixed fallback 逻辑未改动 + - 当前工作区相对 `origin/main` 的累计 diff 已达到 `14 files`,仍低于本轮 `gframework-batch-boot 50` 的主要 stop condition - 当前主线优先级: - generator 覆盖面继续扩大 - dispatch/invoker 反射占比继续下降 @@ -162,9 +171,21 @@ CQRS 迁移与收敛。 - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - 结果:通过 - 备注:`0 warning / 0 error`;本轮确认 dispatcher request pipeline 形状缓存未破坏 `net8.0` / `net9.0` / `net10.0` 目标构建 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~RegisterHandlers_Should_Cache_Assembly_Metadata_Across_Containers|FullyQualifiedName~RegisterHandlers_Should_Cache_Loadable_Types_Across_Containers|FullyQualifiedName~Dispatcher_Should_Cache_Request_Pipeline_Executors_Per_Behavior_Count"` + - 结果:通过 + - 备注:`3/3` 测试通过;本轮确认无捕获缓存工厂没有破坏 registrar / dispatcher 现有缓存行为 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`22/22` 测试通过;本轮新增外部程序集隐藏泛型定义的 precise registration 回归 +- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;本轮确认删除 pointer runtime-reconstruction 残留后生成器项目仍可正常构建 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`22/22` 测试通过;本轮确认 pointer / function pointer 拒绝语义保持不变,且未回归既有 precise runtime type lookup 场景 ## 下一步 -1. 继续 `Phase 8` 主线,优先再找一个收益明确且写集独立的 generator 或 dispatch 热点;当前累计 diff 为 `13 files / 709 lines`,距离 `50 files` stop condition 仍有余量 +1. 继续 `Phase 8` 主线,优先再找一个收益明确且写集独立的 generator 或 registrar/dispatcher 热点;在下一次提交后重新计算相对 `origin/main` 的累计 diff,并继续朝 `50 files` stop condition 推进 2. 若继续文档主线,优先再扫教程入口页与 API 参考中的 CQRS 采用说明,确认是否还有旧 Command / Query 迁移口径残留 3. 若后续再出现新的 PR review 或 review thread 变化,再重新执行 `$gframework-pr-review` 作为独立验证步骤 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 fc868571..a465b160 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 @@ -2,6 +2,46 @@ ## 2026-04-29 +### 阶段:pointer runtime-reconstruction 残留清理(CQRS-REWRITE-RP-056) + +- 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮只处理一个写集很窄的 generator 清理切片:删除 `CqrsHandlerRegistryGenerator` 里已经不可达的 pointer runtime-reconstruction 残留 +- 先复核当前实现后确认: + - `TryCreateRuntimeTypeReference` 已在入口直接拒绝 `IPointerTypeSymbol` 与 `IFunctionPointerTypeSymbol` + - `CanReferenceFromGeneratedRegistry` 也已统一把 pointer / function pointer 判定为不可直接引用 + - 但 `RuntimeTypeReferenceSpec`、`AppendRuntimeTypeReferenceResolution(...)` 和 `ContainsExternalAssemblyTypeLookup(...)` 仍残留 pointer 子结构与 `MakePointerType()` 分支,属于已失效的死代码 +- 已完成的清理: + - `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs` 已移除 `PointerElementTypeReference` + - `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs` 已移除 pointer 运行时重建分支与 `AppendPointerRuntimeTypeReferenceResolution(...)` + - `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs` 已移除 pointer 外部程序集查找递归 + - direct / named / mixed fallback 逻辑未改动,pointer / function pointer 拒绝语义保持不变 +- 定向验证已通过: + - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - `0 warning / 0 error` + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - `22/22` passed + +### 阶段:缓存工厂闭包收敛(CQRS-REWRITE-RP-055) + +- 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮在不扩大语义面的前提下继续做一个更窄的 runtime 微切片:把弱缓存 / 并发缓存入口剩余的捕获型工厂收敛为 `static lambda + state` +- 先复核当前 runtime 热点后确认: + - `CqrsDispatcher` 的 notification / stream / request binding 与 pipeline executor 缓存仍存在少量可消除的捕获型工厂 + - `CqrsHandlerRegistrar` 的程序集元数据缓存与可加载类型缓存也仍通过捕获 `logger` 的 lambda 建值 + - 这些入口都只影响内部缓存建值,不触碰 handler / behavior 生命周期和 fallback 合同 +- 已完成的收敛: + - `CqrsDispatcher` 现为 notification / stream / request binding 命中路径改用无捕获工厂;pipeline executor 缓存改为显式状态对象承载 `requestType` + - `CqrsHandlerRegistrar` 现为 `AssemblyMetadataCache` 与 `LoadableTypesCache` 改用 `static` 工厂 + `logger` 显式状态参数 + - 该批次没有改动 `RequestPipelineInvocation` 的 `next` 语义,也没有缓存 handler / behavior 实例 +- 同轮继续补了一个独立 generator 覆盖缺口: + - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增“外部程序集隐藏泛型定义 + 可见类型实参”的 precise registration 回归 + - 该回归锁定生成器会输出 `ResolveReferencedAssemblyType("...ProtectedEnvelope\`1")` 与 `MakeGenericType(typeof(string))` 的组合,而不是退回程序集级字符串 fallback +- 定向验证已通过: + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~RegisterHandlers_Should_Cache_Assembly_Metadata_Across_Containers|FullyQualifiedName~RegisterHandlers_Should_Cache_Loadable_Types_Across_Containers|FullyQualifiedName~Dispatcher_Should_Cache_Request_Pipeline_Executors_Per_Behavior_Count"` + - `3/3` passed + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - `22/22` passed + - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - `0 warning / 0 error` + ### 阶段:低风险并行批次收口(CQRS-REWRITE-RP-054) - 继续按 `gframework-batch-boot 50` 推进 `Phase 8`,本轮先完成批次评估后再并行拆分写集,避免把 generator、runtime 与 docs 改动揉进同一片上下文 @@ -197,6 +237,6 @@ ### 当前下一步 -1. 回到 `Phase 8` 主线,优先再找一个 generator 覆盖缺口,继续减少仍需程序集级字符串 fallback 元数据的 handler 场景 +1. 回到 `Phase 8` 主线,优先再找一个写集独立的 generator 或 runtime 热点;pointer runtime-reconstruction 残留已清空,后续不要恢复任何 `MakePointerType()` 发射路径 2. 若继续文档主线,优先补齐 `docs/zh-CN/api-reference` 与教程入口页中仍过时的 CQRS API / 命名空间表述 3. 若后续 review thread 或 PR 状态再次变化,再重新执行 `$gframework-pr-review` 复核远端信号 From 16cd96b94b6b315e6f730c06a12e1c2110a5ac0c Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:42:21 +0800 Subject: [PATCH 06/15] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E5=85=85=20dispat?= =?UTF-8?q?cher=20=E7=BC=93=E5=AD=98=E4=B8=8A=E4=B8=8B=E6=96=87=E5=9B=9E?= =?UTF-8?q?=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 cached request pipeline executor 的上下文刷新回归测试与专用测试替身 - 记录 singleton behavior 生命周期语义下的上下文重新注入结论 - 更新 cqrs-rewrite 跟踪与 trace 恢复点到 RP-057 --- .../Cqrs/CqrsDispatcherCacheTests.cs | 56 ++++++++++++++++ ...ispatcherPipelineContextRefreshBehavior.cs | 31 +++++++++ ...DispatcherPipelineContextRefreshRequest.cs | 9 +++ ...herPipelineContextRefreshRequestHandler.cs | 29 ++++++++ .../DispatcherPipelineContextRefreshState.cs | 66 +++++++++++++++++++ .../Cqrs/DispatcherPipelineContextSnapshot.cs | 14 ++++ .../todos/cqrs-rewrite-migration-tracking.md | 10 ++- .../traces/cqrs-rewrite-migration-trace.md | 21 ++++++ 8 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshBehavior.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshRequest.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshRequestHandler.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshState.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextSnapshot.cs diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs index 13ffb761..919a15d7 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs @@ -24,6 +24,7 @@ internal sealed class CqrsDispatcherCacheTests LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); _container = new MicrosoftDiContainer(); _container.RegisterCqrsPipelineBehavior(); + _container.RegisterCqrsPipelineBehavior(); _container.RegisterCqrsPipelineBehavior(); _container.RegisterCqrsPipelineBehavior(); @@ -34,6 +35,7 @@ internal sealed class CqrsDispatcherCacheTests _container.Freeze(); _context = new ArchitectureContext(_container); + DispatcherPipelineContextRefreshState.Reset(); ClearDispatcherCaches(); } @@ -244,6 +246,60 @@ internal sealed class CqrsDispatcherCacheTests }); } + /// + /// 验证缓存的 request pipeline executor 在重复分发时仍会重新解析 handler/behavior, + /// 并为当次实例重新注入当前架构上下文。 + /// + [Test] + public async Task Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Request_Pipeline_Executor() + { + DispatcherPipelineContextRefreshState.Reset(); + + var requestBindings = GetCacheField("RequestDispatchBindings"); + var firstContext = new ArchitectureContext(_container!); + var secondContext = new ArchitectureContext(_container!); + + await firstContext.SendRequestAsync(new DispatcherPipelineContextRefreshRequest("first")); + + var executorAfterFirstDispatch = GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineContextRefreshRequest), + typeof(int), + 1); + + await secondContext.SendRequestAsync(new DispatcherPipelineContextRefreshRequest("second")); + + var executorAfterSecondDispatch = GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineContextRefreshRequest), + typeof(int), + 1); + var behaviorSnapshots = DispatcherPipelineContextRefreshState.BehaviorSnapshots.ToArray(); + var handlerSnapshots = DispatcherPipelineContextRefreshState.HandlerSnapshots.ToArray(); + + Assert.Multiple(() => + { + Assert.That(executorAfterFirstDispatch, Is.Not.Null); + Assert.That(executorAfterSecondDispatch, Is.SameAs(executorAfterFirstDispatch)); + + Assert.That(behaviorSnapshots, Has.Length.EqualTo(2)); + Assert.That(handlerSnapshots, Has.Length.EqualTo(2)); + + Assert.That(behaviorSnapshots[0].DispatchId, Is.EqualTo("first")); + Assert.That(behaviorSnapshots[0].Context, Is.SameAs(firstContext)); + Assert.That(behaviorSnapshots[1].DispatchId, Is.EqualTo("second")); + Assert.That(behaviorSnapshots[1].Context, Is.SameAs(secondContext)); + Assert.That(behaviorSnapshots[1].Context, Is.Not.SameAs(behaviorSnapshots[0].Context)); + + Assert.That(handlerSnapshots[0].DispatchId, Is.EqualTo("first")); + Assert.That(handlerSnapshots[0].Context, Is.SameAs(firstContext)); + Assert.That(handlerSnapshots[1].DispatchId, Is.EqualTo("second")); + Assert.That(handlerSnapshots[1].Context, Is.SameAs(secondContext)); + Assert.That(handlerSnapshots[1].Context, Is.Not.SameAs(handlerSnapshots[0].Context)); + Assert.That(handlerSnapshots[1].InstanceId, Is.Not.EqualTo(handlerSnapshots[0].InstanceId)); + }); + } + /// /// 通过反射读取 dispatcher 的静态缓存对象。 /// diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshBehavior.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshBehavior.cs new file mode 100644 index 00000000..360eaf46 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshBehavior.cs @@ -0,0 +1,31 @@ +using System.Threading; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 记录缓存 executor 复用场景下每次分发注入到 behavior 的上下文与实例身份。 +/// +internal sealed class DispatcherPipelineContextRefreshBehavior + : CqrsContextAwareHandlerBase, + IPipelineBehavior +{ + private readonly int _instanceId = DispatcherPipelineContextRefreshState.AllocateBehaviorInstanceId(); + + /// + /// 记录当前 behavior 实例实际收到的上下文,然后继续执行下游处理器。 + /// + /// 当前请求。 + /// 下一个处理阶段。 + /// 取消令牌。 + /// 下游处理结果。 + public async ValueTask Handle( + DispatcherPipelineContextRefreshRequest request, + MessageHandlerDelegate next, + CancellationToken cancellationToken) + { + DispatcherPipelineContextRefreshState.RecordBehavior(request.DispatchId, _instanceId, Context); + return await next(request, cancellationToken).ConfigureAwait(false); + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshRequest.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshRequest.cs new file mode 100644 index 00000000..6e0035ef --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshRequest.cs @@ -0,0 +1,9 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 为 pipeline executor 上下文刷新回归提供带分发标识的最小请求。 +/// +/// 当前分发的稳定标识,便于断言 handler 与 behavior 看到的是同一次请求。 +internal sealed record DispatcherPipelineContextRefreshRequest(string DispatchId) : IRequest; diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshRequestHandler.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshRequestHandler.cs new file mode 100644 index 00000000..f69923a9 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshRequestHandler.cs @@ -0,0 +1,29 @@ +using System.Threading; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 记录缓存 executor 复用场景下每次分发注入到 request handler 的上下文与实例身份。 +/// +internal sealed class DispatcherPipelineContextRefreshRequestHandler + : CqrsContextAwareHandlerBase, + IRequestHandler +{ + private readonly int _instanceId = DispatcherPipelineContextRefreshState.AllocateHandlerInstanceId(); + + /// + /// 记录当前 handler 实例收到的上下文,并返回稳定结果。 + /// + /// 当前请求。 + /// 取消令牌。 + /// 固定整数结果。 + public ValueTask Handle( + DispatcherPipelineContextRefreshRequest request, + CancellationToken cancellationToken) + { + DispatcherPipelineContextRefreshState.RecordHandler(request.DispatchId, _instanceId, Context); + return ValueTask.FromResult(7); + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshState.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshState.cs new file mode 100644 index 00000000..15913678 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshState.cs @@ -0,0 +1,66 @@ +using System.Threading; +using GFramework.Core.Abstractions.Architectures; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 记录 pipeline executor 缓存回归中每次分发实际使用的上下文与实例身份。 +/// +internal static class DispatcherPipelineContextRefreshState +{ + private static int _nextBehaviorInstanceId; + private static int _nextHandlerInstanceId; + + /// + /// 获取每次 behavior 执行时记录的快照。 + /// + public static List BehaviorSnapshots { get; } = []; + + /// + /// 获取每次 handler 执行时记录的快照。 + /// + public static List HandlerSnapshots { get; } = []; + + /// + /// 为新的 behavior 测试实例分配稳定编号。 + /// + public static int AllocateBehaviorInstanceId() + { + return Interlocked.Increment(ref _nextBehaviorInstanceId); + } + + /// + /// 为新的 handler 测试实例分配稳定编号。 + /// + public static int AllocateHandlerInstanceId() + { + return Interlocked.Increment(ref _nextHandlerInstanceId); + } + + /// + /// 记录 behavior 在当前分发中观察到的上下文。 + /// + public static void RecordBehavior(string dispatchId, int instanceId, IArchitectureContext context) + { + BehaviorSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + } + + /// + /// 记录 handler 在当前分发中观察到的上下文。 + /// + public static void RecordHandler(string dispatchId, int instanceId, IArchitectureContext context) + { + HandlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + } + + /// + /// 清空历史记录与实例编号,避免跨测试污染断言。 + /// + public static void Reset() + { + _nextBehaviorInstanceId = 0; + _nextHandlerInstanceId = 0; + BehaviorSnapshots.Clear(); + HandlerSnapshots.Clear(); + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextSnapshot.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextSnapshot.cs new file mode 100644 index 00000000..24d70c52 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextSnapshot.cs @@ -0,0 +1,14 @@ +using GFramework.Core.Abstractions.Architectures; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 描述单次分发阶段记录下来的上下文与实例身份。 +/// +/// 触发本次记录的请求标识。 +/// 当次 handler 或 behavior 实例编号。 +/// 当次分发注入的架构上下文。 +internal sealed record DispatcherPipelineContextSnapshot( + string DispatchId, + int InstanceId, + IArchitectureContext Context); 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 af69ff10..ec7fcc6b 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 @@ -7,7 +7,7 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-056` +- 恢复点编号:`CQRS-REWRITE-RP-057` - 当前阶段:`Phase 8` - 当前焦点: - 当前功能历史已归档,active 跟踪仅保留 `Phase 8` 主线的恢复入口 @@ -17,6 +17,7 @@ CQRS 迁移与收敛。 - 当 runtime 不支持多实例 fallback 特性或缺少对应构造函数时,mixed fallback 场景仍会整体保守回退到字符串元数据,避免仅部分 handler 走 `Type[]` 时漏掉剩余需按名称恢复的 handlers - 已完成 request pipeline executor 形状缓存:`CqrsDispatcher` 现会在单个 request binding 内按 `behaviorCount` 复用强类型 pipeline executor,而不是每次 `SendAsync` 都重建整条 `next` 委托链 - 已补充 dispatcher pipeline executor 缓存与双行为顺序回归,锁定缓存复用后仍保持现有行为执行顺序 + - 已补充 cached request pipeline executor 的上下文刷新回归,锁定 executor 复用时仍会为当次 handler / singleton behavior 重新注入当前 `ArchitectureContext` - 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke` - 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物 - 已修正 pointer / function pointer 泛型合同的错误覆盖:生成器不再为这两类类型发射 precise runtime type 重建代码 @@ -90,6 +91,10 @@ CQRS 迁移与收敛。 - `CqrsDispatcher` 现会继续按 `requestType + responseType` 缓存 request dispatch binding,并在 binding 内按 `behaviorCount` 缓存强类型 pipeline executor - 每次分发只绑定当前 handler / behaviors 实例,不缓存容器解析结果,因此不改变 transient 生命周期与上下文注入语义 - `GFramework.Cqrs.Tests` 已补充 executor 首次创建 / 后续复用与双行为顺序回归 +- `2026-04-29` 已完成一轮 cached executor 上下文刷新回归补强: + - `GFramework.Cqrs.Tests` 已新增 `DispatcherPipelineContextRefresh*` 测试替身,分别记录 request handler 与 pipeline behavior 在每次分发中实际观察到的实例身份与 `ArchitectureContext` + - `CqrsDispatcherCacheTests` 现明确断言:同一个 cached request pipeline executor 在重复分发时会继续命中同一 executor 形状,但不会跨分发保留旧上下文 + - 本轮定向测试未暴露新的 runtime 缺口,因此没有改动 `GFramework.Cqrs/Internal/CqrsDispatcher.cs` - `2026-04-29` 已完成一轮 CQRS 入口文档对齐: - `GFramework.Cqrs/README.md`、`docs/zh-CN/core/cqrs.md` 与 `docs/zh-CN/api-reference/index.md` 现已明确 generated registry 优先、targeted fallback 补齐剩余 handler 的当前语义 - `2026-04-29` 已完成一轮 generator pointer runtime-reconstruction 残留清理: @@ -117,6 +122,9 @@ CQRS 迁移与收敛。 - `RP-043` 之前的详细阶段记录、定向验证命令和阶段性决策均已移入主题内归档 - active 跟踪文件只保留当前恢复点、当前活跃事实、风险和下一步,避免 `boot` 在默认入口中重复扫描 1000+ 行历史 trace +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` + - 结果:通过 + - 备注:`5/5` 测试通过;本轮新增 cached executor 上下文刷新回归,确认 executor 复用时仍按当次分发重新注入上下文 - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false` - 结果:通过 - 备注:`63/63` 测试通过;当前沙箱限制了 MSBuild named pipe,验证需在提权环境下运行 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 a465b160..7cf929c4 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 @@ -2,6 +2,27 @@ ## 2026-04-29 +### 阶段:cached executor 上下文刷新回归(CQRS-REWRITE-RP-057) + +- 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮只处理一个窄写集测试批次:为 cached request pipeline executor 增加“重复分发仍重新注入上下文”的回归 +- 先复核上一轮 request pipeline executor 形状缓存实现与测试边界后确认: + - 当前 runtime 只允许本轮写集落在 `GFramework.Cqrs.Tests/Cqrs/`,除非测试直接打出 `CqrsDispatcher` 的真实缺陷 + - 目标是锁定 executor 缓存不会跨分发保留旧 `ArchitectureContext`,且不扩张到 notification / stream 路径 +- 已完成的测试补强: + - 在 `GFramework.Cqrs.Tests/Cqrs/` 新增 `DispatcherPipelineContextRefreshRequest`、`DispatcherPipelineContextRefreshBehavior`、`DispatcherPipelineContextRefreshRequestHandler`、`DispatcherPipelineContextRefreshState` 与 `DispatcherPipelineContextSnapshot` + - `DispatcherPipelineContextRefreshBehavior` 与 `DispatcherPipelineContextRefreshRequestHandler` 都基于 `CqrsContextAwareHandlerBase` 记录当次看到的 `ArchitectureContext` + - `CqrsDispatcherCacheTests` 新增 `Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Request_Pipeline_Executor`,断言同一个 cached executor 在两次分发间保持 executor 形状复用,但 handler 不会被 executor 黏住,且 handler / behavior 都会观察到本次分发的新上下文 +- 调试过程中的结论: + - 初版断言曾要求 behavior 实例编号跨分发变化,随后确认这是错误假设 + - `MicrosoftDiContainer.RegisterCqrsPipelineBehavior()` 对已闭合的 pipeline behavior 使用的是 `AddSingleton(...)` + - 因此本轮最终锁定的是“singleton behavior 也必须重新注入上下文”,而不是强行要求 behavior 生命周期为 transient +- 定向验证已通过: + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` + - `5/5` passed +- 结果: + - 本轮未暴露新的 runtime 实现缺口,因此没有改动 `GFramework.Cqrs/Internal/CqrsDispatcher.cs` + - 当前分支相对 `origin/main` 的累计提交 diff 仍为 `14 files`,继续低于 `gframework-batch-boot 50` 的主要 stop condition + ### 阶段:pointer runtime-reconstruction 残留清理(CQRS-REWRITE-RP-056) - 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮只处理一个写集很窄的 generator 清理切片:删除 `CqrsHandlerRegistryGenerator` 里已经不可达的 pointer runtime-reconstruction 残留 From 36596210ff89f77fc991109deac9e2d7ffd5587e Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:49:46 +0800 Subject: [PATCH 07/15] =?UTF-8?q?test(cqrs):=20=E6=96=B0=E5=A2=9E=20Reflec?= =?UTF-8?q?tionFallbackAttribute=20=E5=90=88=E5=90=8C=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CqrsReflectionFallbackAttribute 的叶子级合同测试,覆盖旧版 marker 语义 - 补充字符串与 Type 输入的过滤、去重、排序归一化断言 - 验证空参数数组保护,固定 runtime 可依赖的 attribute 元数据边界 --- .../CqrsReflectionFallbackAttributeTests.cs | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 GFramework.Cqrs.Tests/Cqrs/CqrsReflectionFallbackAttributeTests.cs diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsReflectionFallbackAttributeTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsReflectionFallbackAttributeTests.cs new file mode 100644 index 00000000..53bb2988 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsReflectionFallbackAttributeTests.cs @@ -0,0 +1,97 @@ +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 验证 公开构造器的归一化合同, +/// 以固定 runtime 读取程序集级 fallback 元数据时可依赖的可观察语义。 +/// +[TestFixture] +internal sealed class CqrsReflectionFallbackAttributeTests +{ + /// + /// 验证无参构造器会保留旧版 marker 语义,并暴露空的 fallback 集合。 + /// + [Test] + public void Constructor_Without_Arguments_Should_Expose_Empty_Fallback_Collections() + { + var attribute = new CqrsReflectionFallbackAttribute(); + + Assert.Multiple(() => + { + Assert.That(attribute.FallbackHandlerTypeNames, Is.Empty); + Assert.That(attribute.FallbackHandlerTypes, Is.Empty); + }); + } + + /// + /// 验证字符串名称重载会过滤空白项,并按序号稳定去重排序, + /// 确保 runtime 后续读取到的名称清单不依赖调用端输入顺序。 + /// + [Test] + public void Constructor_With_Type_Names_Should_Normalize_By_Filtering_Deduplicating_And_Sorting() + { + var attribute = new CqrsReflectionFallbackAttribute( + "Zeta.Handler", + " ", + "Alpha.Handler", + "Zeta.Handler", + string.Empty, + "Beta.Handler", + "Alpha.Handler"); + + Assert.Multiple(() => + { + Assert.That( + attribute.FallbackHandlerTypeNames, + Is.EqualTo(["Alpha.Handler", "Beta.Handler", "Zeta.Handler"])); + Assert.That(attribute.FallbackHandlerTypes, Is.Empty); + }); + } + + /// + /// 验证字符串名称重载收到 参数数组时会立即拒绝, + /// 避免 runtime 在读取程序集元数据时延迟暴露无效状态。 + /// + [Test] + public void Constructor_With_Null_Type_Name_Array_Should_Throw_ArgumentNullException() + { + Assert.That( + () => _ = new CqrsReflectionFallbackAttribute((string[])null!), + Throws.ArgumentNullException); + } + + /// + /// 验证 重载会过滤空引用,并按稳定名称顺序去重, + /// 确保后续 fallback 补扫不会因为重复输入或反射枚举顺序产生非确定性。 + /// + [Test] + public void Constructor_With_Types_Should_Normalize_By_Filtering_Deduplicating_And_Sorting() + { + var attribute = new CqrsReflectionFallbackAttribute( + typeof(string), + null!, + typeof(Uri), + typeof(string), + typeof(Version)); + + // 这里按 FullName 的 Ordinal 顺序断言,固定该 attribute 对 runtime 暴露的元数据排序合同。 + Assert.Multiple(() => + { + Assert.That( + attribute.FallbackHandlerTypes, + Is.EqualTo([typeof(string), typeof(Uri), typeof(Version)])); + Assert.That(attribute.FallbackHandlerTypeNames, Is.Empty); + }); + } + + /// + /// 验证 重载收到 参数数组时会立即拒绝, + /// 从而维持 attribute 元数据的最小有效性边界。 + /// + [Test] + public void Constructor_With_Null_Type_Array_Should_Throw_ArgumentNullException() + { + Assert.That( + () => _ = new CqrsReflectionFallbackAttribute((Type[])null!), + Throws.ArgumentNullException); + } +} From 226c0b3b498701857fa7a0d8a68a808da33093b1 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:52:52 +0800 Subject: [PATCH 08/15] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E5=85=85=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E6=9C=8D=E5=8A=A1=E7=A8=8B=E5=BA=8F=E9=9B=86=E5=8E=BB?= =?UTF-8?q?=E9=87=8D=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 DefaultCqrsRegistrationService 的独立测试文件,覆盖同次调用内的重复程序集键去重行为 - 验证跨两次调用重复程序集键时会跳过注册并写入 debug 日志 --- .../Cqrs/CqrsRegistrationServiceTests.cs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs new file mode 100644 index 00000000..b8b03b1c --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs @@ -0,0 +1,96 @@ +using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Tests.Logging; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 验证 CQRS 程序集注册协调器在程序集键去重层面的可观察行为。 +/// +[TestFixture] +internal sealed class CqrsRegistrationServiceTests +{ + /// + /// 验证同一次调用内出现重复程序集键时,底层注册器只会接收到一次注册请求。 + /// + [Test] + public void RegisterHandlers_Should_Register_Duplicate_Assembly_Key_Only_Once_Per_Call() + { + var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug); + var registrar = new Mock(MockBehavior.Strict); + var duplicateAssemblyA = CreateAssembly("GFramework.Cqrs.Tests.DuplicateAssembly, Version=1.0.0.0"); + var duplicateAssemblyB = CreateAssembly("GFramework.Cqrs.Tests.DuplicateAssembly, Version=1.0.0.0"); + var expectedAssembly = duplicateAssemblyA.Object; + IEnumerable? registeredAssemblies = null; + + registrar + .Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>())) + .Callback>(assemblies => registeredAssemblies = assemblies.ToArray()); + + var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger); + + service.RegisterHandlers([duplicateAssemblyA.Object, duplicateAssemblyB.Object]); + + registrar.Verify( + static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>()), + Times.Once); + Assert.Multiple(() => + { + Assert.That(registeredAssemblies, Is.Not.Null); + Assert.That(registeredAssemblies, Is.EqualTo([expectedAssembly])); + Assert.That(logger.Logs, Has.Count.EqualTo(0)); + }); + } + + /// + /// 验证跨两次调用重复程序集键时,协调器会跳过重复注册并写入 debug 日志。 + /// + [Test] + public void RegisterHandlers_Should_Skip_Already_Registered_Assembly_Key_Across_Calls_And_Log_Debug_Message() + { + var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug); + var registrar = new Mock(MockBehavior.Strict); + var firstAssembly = CreateAssembly("GFramework.Cqrs.Tests.RegisteredAssembly, Version=1.0.0.0"); + var secondAssembly = CreateAssembly("GFramework.Cqrs.Tests.RegisteredAssembly, Version=1.0.0.0"); + IEnumerable? registeredAssemblies = null; + + registrar + .Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>())) + .Callback>(assemblies => registeredAssemblies = assemblies.ToArray()); + + var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger); + + service.RegisterHandlers([firstAssembly.Object]); + service.RegisterHandlers([secondAssembly.Object]); + + registrar.Verify( + static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>()), + Times.Once); + Assert.Multiple(() => + { + Assert.That(registeredAssemblies, Is.EqualTo([firstAssembly.Object])); + Assert.That( + logger.Logs.Where(static log => log.Level == LogLevel.Debug).Select(static log => log.Message), + Is.EqualTo( + [ + "Skipping CQRS handler registration for assembly " + + "GFramework.Cqrs.Tests.RegisteredAssembly, Version=1.0.0.0 because it was already registered." + ])); + }); + } + + /// + /// 创建一个带稳定程序集键的程序集 mock,用于模拟不同 实例表示同一程序集的场景。 + /// + /// 要返回的程序集完整名称。 + /// 配置好完整名称的程序集 mock。 + private static Mock CreateAssembly(string assemblyFullName) + { + var assembly = new Mock(); + assembly + .SetupGet(static currentAssembly => currentAssembly.FullName) + .Returns(assemblyFullName); + + return assembly; + } +} From 57d848546fd785c566257175e8410f5c82e18268 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:54:59 +0800 Subject: [PATCH 09/15] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E5=85=85=E9=9D=9E?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E5=88=86=E5=8F=91=E4=B8=8A=E4=B8=8B=E6=96=87?= =?UTF-8?q?=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 notification 与 stream dispatch binding 上下文刷新回归,锁定缓存复用时仍按当次分发重新注入上下文 - 补充测试替身记录 handler 实例身份与 ArchitectureContext,覆盖重复分发场景 - 更新 cqrs-rewrite 跟踪与 trace,记录 RP-058 和 RP-059 的验证结论 --- .../Cqrs/CqrsDispatcherCacheTests.cs | 88 +++++++++++++++++++ ...atcherNotificationContextRefreshHandler.cs | 29 ++++++ ...rNotificationContextRefreshNotification.cs | 8 ++ ...spatcherNotificationContextRefreshState.cs | 42 +++++++++ .../DispatcherStreamContextRefreshHandler.cs | 32 +++++++ .../DispatcherStreamContextRefreshRequest.cs | 8 ++ .../DispatcherStreamContextRefreshState.cs | 42 +++++++++ .../todos/cqrs-rewrite-migration-tracking.md | 19 +++- .../traces/cqrs-rewrite-migration-trace.md | 27 ++++++ 9 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshHandler.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshNotification.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshState.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshHandler.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshRequest.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs index 919a15d7..3d2040ab 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs @@ -35,7 +35,9 @@ internal sealed class CqrsDispatcherCacheTests _container.Freeze(); _context = new ArchitectureContext(_container); + DispatcherNotificationContextRefreshState.Reset(); DispatcherPipelineContextRefreshState.Reset(); + DispatcherStreamContextRefreshState.Reset(); ClearDispatcherCaches(); } @@ -300,6 +302,92 @@ internal sealed class CqrsDispatcherCacheTests }); } + /// + /// 验证缓存的 notification dispatch binding 在重复分发时仍会重新解析 handler, + /// 并为当次实例重新注入当前架构上下文。 + /// + [Test] + public async Task Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Notification_Dispatch_Binding() + { + DispatcherNotificationContextRefreshState.Reset(); + + var notificationBindings = GetCacheField("NotificationDispatchBindings"); + var firstContext = new ArchitectureContext(_container!); + var secondContext = new ArchitectureContext(_container!); + + await firstContext.PublishAsync(new DispatcherNotificationContextRefreshNotification("first")); + + var bindingAfterFirstDispatch = GetSingleKeyCacheValue( + notificationBindings, + typeof(DispatcherNotificationContextRefreshNotification)); + + await secondContext.PublishAsync(new DispatcherNotificationContextRefreshNotification("second")); + + var bindingAfterSecondDispatch = GetSingleKeyCacheValue( + notificationBindings, + typeof(DispatcherNotificationContextRefreshNotification)); + var handlerSnapshots = DispatcherNotificationContextRefreshState.HandlerSnapshots.ToArray(); + + Assert.Multiple(() => + { + Assert.That(bindingAfterFirstDispatch, Is.Not.Null); + Assert.That(bindingAfterSecondDispatch, Is.SameAs(bindingAfterFirstDispatch)); + + Assert.That(handlerSnapshots, Has.Length.EqualTo(2)); + Assert.That(handlerSnapshots[0].DispatchId, Is.EqualTo("first")); + Assert.That(handlerSnapshots[0].Context, Is.SameAs(firstContext)); + Assert.That(handlerSnapshots[1].DispatchId, Is.EqualTo("second")); + Assert.That(handlerSnapshots[1].Context, Is.SameAs(secondContext)); + Assert.That(handlerSnapshots[1].Context, Is.Not.SameAs(handlerSnapshots[0].Context)); + Assert.That(handlerSnapshots[1].InstanceId, Is.Not.EqualTo(handlerSnapshots[0].InstanceId)); + }); + } + + /// + /// 验证缓存的 stream dispatch binding 在重复建流时仍会重新解析 handler, + /// 并为当次实例重新注入当前架构上下文。 + /// + [Test] + public async Task Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Stream_Dispatch_Binding() + { + DispatcherStreamContextRefreshState.Reset(); + + var streamBindings = GetCacheField("StreamDispatchBindings"); + var firstContext = new ArchitectureContext(_container!); + var secondContext = new ArchitectureContext(_container!); + + var firstStream = firstContext.CreateStream(new DispatcherStreamContextRefreshRequest("first")); + await DrainAsync(firstStream); + + var bindingAfterFirstDispatch = GetPairCacheValue( + streamBindings, + typeof(DispatcherStreamContextRefreshRequest), + typeof(int)); + + var secondStream = secondContext.CreateStream(new DispatcherStreamContextRefreshRequest("second")); + await DrainAsync(secondStream); + + var bindingAfterSecondDispatch = GetPairCacheValue( + streamBindings, + typeof(DispatcherStreamContextRefreshRequest), + typeof(int)); + var handlerSnapshots = DispatcherStreamContextRefreshState.HandlerSnapshots.ToArray(); + + Assert.Multiple(() => + { + Assert.That(bindingAfterFirstDispatch, Is.Not.Null); + Assert.That(bindingAfterSecondDispatch, Is.SameAs(bindingAfterFirstDispatch)); + + Assert.That(handlerSnapshots, Has.Length.EqualTo(2)); + Assert.That(handlerSnapshots[0].DispatchId, Is.EqualTo("first")); + Assert.That(handlerSnapshots[0].Context, Is.SameAs(firstContext)); + Assert.That(handlerSnapshots[1].DispatchId, Is.EqualTo("second")); + Assert.That(handlerSnapshots[1].Context, Is.SameAs(secondContext)); + Assert.That(handlerSnapshots[1].Context, Is.Not.SameAs(handlerSnapshots[0].Context)); + Assert.That(handlerSnapshots[1].InstanceId, Is.Not.EqualTo(handlerSnapshots[0].InstanceId)); + }); + } + /// /// 通过反射读取 dispatcher 的静态缓存对象。 /// diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshHandler.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshHandler.cs new file mode 100644 index 00000000..61a07ccc --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshHandler.cs @@ -0,0 +1,29 @@ +using System.Threading; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 记录缓存 notification binding 复用场景下每次分发注入到 handler 的上下文与实例身份。 +/// +internal sealed class DispatcherNotificationContextRefreshHandler + : CqrsContextAwareHandlerBase, + INotificationHandler +{ + private readonly int _instanceId = DispatcherNotificationContextRefreshState.AllocateHandlerInstanceId(); + + /// + /// 记录当前 handler 实例收到的上下文。 + /// + /// 当前通知。 + /// 取消令牌。 + /// 已完成任务。 + public ValueTask Handle( + DispatcherNotificationContextRefreshNotification notification, + CancellationToken cancellationToken) + { + DispatcherNotificationContextRefreshState.Record(notification.DispatchId, _instanceId, Context); + return ValueTask.CompletedTask; + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshNotification.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshNotification.cs new file mode 100644 index 00000000..f1f54dd2 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshNotification.cs @@ -0,0 +1,8 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 为 notification dispatch binding 上下文刷新回归提供带分发标识的最小通知。 +/// +internal sealed record DispatcherNotificationContextRefreshNotification(string DispatchId) : INotification; diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshState.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshState.cs new file mode 100644 index 00000000..43c32531 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshState.cs @@ -0,0 +1,42 @@ +using System.Threading; +using GFramework.Core.Abstractions.Architectures; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 记录 notification dispatch binding 缓存回归中每次分发实际使用的上下文与实例身份。 +/// +internal static class DispatcherNotificationContextRefreshState +{ + private static int _nextHandlerInstanceId; + + /// + /// 获取每次 notification 分发时记录的快照。 + /// + public static List HandlerSnapshots { get; } = []; + + /// + /// 为新的 handler 测试实例分配稳定编号。 + /// + public static int AllocateHandlerInstanceId() + { + return Interlocked.Increment(ref _nextHandlerInstanceId); + } + + /// + /// 记录 handler 在当前分发中观察到的上下文。 + /// + public static void Record(string dispatchId, int instanceId, IArchitectureContext context) + { + HandlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + } + + /// + /// 清空历史记录与实例编号,避免跨测试污染断言。 + /// + public static void Reset() + { + _nextHandlerInstanceId = 0; + HandlerSnapshots.Clear(); + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshHandler.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshHandler.cs new file mode 100644 index 00000000..95d7c998 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshHandler.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 记录缓存 stream binding 复用场景下每次分发注入到 handler 的上下文与实例身份。 +/// +internal sealed class DispatcherStreamContextRefreshHandler + : CqrsContextAwareHandlerBase, + IStreamRequestHandler +{ + private readonly int _instanceId = DispatcherStreamContextRefreshState.AllocateHandlerInstanceId(); + + /// + /// 记录当前 handler 实例收到的上下文,并返回稳定元素。 + /// + /// 当前流请求。 + /// 取消令牌。 + /// 包含一个固定元素的异步流。 + public async IAsyncEnumerable Handle( + DispatcherStreamContextRefreshRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + DispatcherStreamContextRefreshState.Record(request.DispatchId, _instanceId, Context); + yield return 11; + await ValueTask.CompletedTask.ConfigureAwait(false); + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshRequest.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshRequest.cs new file mode 100644 index 00000000..c8d69cfe --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshRequest.cs @@ -0,0 +1,8 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 为 stream dispatch binding 上下文刷新回归提供带分发标识的最小流请求。 +/// +internal sealed record DispatcherStreamContextRefreshRequest(string DispatchId) : IStreamRequest; diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs new file mode 100644 index 00000000..a315451d --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs @@ -0,0 +1,42 @@ +using System.Threading; +using GFramework.Core.Abstractions.Architectures; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 记录 stream dispatch binding 缓存回归中每次分发实际使用的上下文与实例身份。 +/// +internal static class DispatcherStreamContextRefreshState +{ + private static int _nextHandlerInstanceId; + + /// + /// 获取每次建流时记录的快照。 + /// + public static List HandlerSnapshots { get; } = []; + + /// + /// 为新的 handler 测试实例分配稳定编号。 + /// + public static int AllocateHandlerInstanceId() + { + return Interlocked.Increment(ref _nextHandlerInstanceId); + } + + /// + /// 记录 handler 在当前建流中观察到的上下文。 + /// + public static void Record(string dispatchId, int instanceId, IArchitectureContext context) + { + HandlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + } + + /// + /// 清空历史记录与实例编号,避免跨测试污染断言。 + /// + public static void Reset() + { + _nextHandlerInstanceId = 0; + HandlerSnapshots.Clear(); + } +} 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 ec7fcc6b..fec9f710 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 @@ -7,7 +7,7 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-057` +- 恢复点编号:`CQRS-REWRITE-RP-059` - 当前阶段:`Phase 8` - 当前焦点: - 当前功能历史已归档,active 跟踪仅保留 `Phase 8` 主线的恢复入口 @@ -18,6 +18,7 @@ CQRS 迁移与收敛。 - 已完成 request pipeline executor 形状缓存:`CqrsDispatcher` 现会在单个 request binding 内按 `behaviorCount` 复用强类型 pipeline executor,而不是每次 `SendAsync` 都重建整条 `next` 委托链 - 已补充 dispatcher pipeline executor 缓存与双行为顺序回归,锁定缓存复用后仍保持现有行为执行顺序 - 已补充 cached request pipeline executor 的上下文刷新回归,锁定 executor 复用时仍会为当次 handler / singleton behavior 重新注入当前 `ArchitectureContext` + - 已补充 cached notification / stream dispatch binding 的上下文刷新回归,锁定 binding 复用时仍会为当次 handler 重新注入当前 `ArchitectureContext` - 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke` - 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物 - 已修正 pointer / function pointer 泛型合同的错误覆盖:生成器不再为这两类类型发射 precise runtime type 重建代码 @@ -25,6 +26,7 @@ CQRS 迁移与收敛。 - 已为 registrar 的 reflection 注册路径补充 handler-interface 元数据缓存,减少跨容器重复注册时的 `GetInterfaces()` 反射 - 已将 registrar 的重复映射判定从线性扫描 `IServiceCollection` 收敛为本地映射索引,减少 fallback 注册路径的重复查找 - 已完成一轮 `static lambda + state` 微收敛:`CqrsDispatcher` 与 `CqrsHandlerRegistrar` 现会在弱缓存 / 并发缓存入口优先使用无捕获工厂,继续压低热路径上的额外闭包分配 + - 已补充 `CqrsReflectionFallbackAttribute` 叶子级合同测试,锁定空 marker、字符串 fallback 名称归一化、直接 `Type` fallback 归一化与空参数防御语义 - 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点 ## 当前状态摘要 @@ -95,6 +97,13 @@ CQRS 迁移与收敛。 - `GFramework.Cqrs.Tests` 已新增 `DispatcherPipelineContextRefresh*` 测试替身,分别记录 request handler 与 pipeline behavior 在每次分发中实际观察到的实例身份与 `ArchitectureContext` - `CqrsDispatcherCacheTests` 现明确断言:同一个 cached request pipeline executor 在重复分发时会继续命中同一 executor 形状,但不会跨分发保留旧上下文 - 本轮定向测试未暴露新的 runtime 缺口,因此没有改动 `GFramework.Cqrs/Internal/CqrsDispatcher.cs` +- `2026-04-29` 已完成一轮 cached notification / stream binding 上下文刷新回归补强: + - `GFramework.Cqrs.Tests` 已新增 `DispatcherNotificationContextRefresh*` 与 `DispatcherStreamContextRefresh*` 测试替身,分别记录 notification handler 与 stream handler 在重复分发时观察到的实例身份与 `ArchitectureContext` + - `CqrsDispatcherCacheTests` 现明确断言:同一个 cached notification / stream dispatch binding 在重复分发时会继续命中同一 binding,但不会跨分发保留旧上下文 + - 本轮定向测试未暴露新的 runtime 缺口,因此没有改动 `GFramework.Cqrs/Internal/CqrsDispatcher.cs` +- `2026-04-29` 已接受一轮 delegated 叶子级 fallback 合同测试: + - `GFramework.Cqrs.Tests/Cqrs/CqrsReflectionFallbackAttributeTests.cs` 已锁定空 marker、字符串 fallback 名称去空/去重/排序、直接 `Type` fallback 去空/去重/排序与空参数数组防御语义 + - 当前 runtime 读取程序集级 fallback 元数据时所依赖的 attribute 归一化合同,现已有独立叶子级测试文件覆盖 - `2026-04-29` 已完成一轮 CQRS 入口文档对齐: - `GFramework.Cqrs/README.md`、`docs/zh-CN/core/cqrs.md` 与 `docs/zh-CN/api-reference/index.md` 现已明确 generated registry 优先、targeted fallback 补齐剩余 handler 的当前语义 - `2026-04-29` 已完成一轮 generator pointer runtime-reconstruction 残留清理: @@ -125,6 +134,12 @@ CQRS 迁移与收敛。 - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` - 结果:通过 - 备注:`5/5` 测试通过;本轮新增 cached executor 上下文刷新回归,确认 executor 复用时仍按当次分发重新注入上下文 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsReflectionFallbackAttributeTests"` + - 结果:通过 + - 备注:`5/5` 测试通过;本轮锁定 fallback attribute 的公开归一化合同与空参数防御语义 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` + - 结果:通过 + - 备注:`7/7` 测试通过;本轮新增 cached notification / stream binding 上下文刷新回归,确认 binding 复用时仍按当次分发重新注入上下文 - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false` - 结果:通过 - 备注:`63/63` 测试通过;当前沙箱限制了 MSBuild named pipe,验证需在提权环境下运行 @@ -194,6 +209,6 @@ CQRS 迁移与收敛。 ## 下一步 -1. 继续 `Phase 8` 主线,优先再找一个收益明确且写集独立的 generator 或 registrar/dispatcher 热点;在下一次提交后重新计算相对 `origin/main` 的累计 diff,并继续朝 `50 files` stop condition 推进 +1. 继续 `Phase 8` 主线,优先再找一个收益明确且写集独立的 generator 或 registrar/dispatcher 热点;当前工作区若提交主线程 notification / stream 回归批次,相对 `origin/main` 的累计 diff 将达到 `29 files`,仍低于本轮 `gframework-batch-boot 50` 的主要 stop condition 2. 若继续文档主线,优先再扫教程入口页与 API 参考中的 CQRS 采用说明,确认是否还有旧 Command / Query 迁移口径残留 3. 若后续再出现新的 PR review 或 review thread 变化,再重新执行 `$gframework-pr-review` 作为独立验证步骤 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 7cf929c4..41a14f44 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 @@ -2,6 +2,33 @@ ## 2026-04-29 +### 阶段:notification / stream binding 上下文刷新回归(CQRS-REWRITE-RP-059) + +- 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮继续沿着上一批 dispatcher cached executor 上下文回归往外扩一圈,但只覆盖 notification / stream 两条非 request 路径 +- 主线程先复核 `CqrsDispatcher` 当前实现后确认: + - `PublishAsync(...)` 与 `CreateStream(...)` 都会在命中缓存 binding 后重新解析 handler,并在调用前执行 `PrepareHandler(...)` + - 因此本轮最稳妥的切片仍是测试补强,而不是继续改 runtime +- 已完成的测试补强: + - 在 `GFramework.Cqrs.Tests/Cqrs/` 新增 `DispatcherNotificationContextRefresh*` 与 `DispatcherStreamContextRefresh*` 测试替身,记录重复分发时 handler 实例身份与 `ArchitectureContext` + - `CqrsDispatcherCacheTests` 新增 `Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Notification_Dispatch_Binding` + - `CqrsDispatcherCacheTests` 新增 `Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Stream_Dispatch_Binding` +- 定向验证已通过: + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` + - `7/7` passed +- 结果: + - 本轮未暴露新的 runtime 实现缺口,因此没有改动 `GFramework.Cqrs/Internal/CqrsDispatcher.cs` + - 若连同当前工作区一起计算,当前分支相对 `origin/main` 的累计 diff 将达到 `29 files`,继续低于 `gframework-batch-boot 50` 的主要 stop condition + +### 阶段:delegated fallback attribute 合同测试(CQRS-REWRITE-RP-058) + +- 本轮按 `gframework-batch-boot 50` 的并行约束,把一个与主线程写集完全独立的叶子级测试文件交给 worker: + - delegated scope:`GFramework.Cqrs.Tests/Cqrs/CqrsReflectionFallbackAttributeTests.cs` + - delegated objective:锁定 `CqrsReflectionFallbackAttribute` 的公开归一化合同,而不扩张到 registrar / generator / dispatcher 实现 +- 已接受的 worker 结果: + - 新增 `CqrsReflectionFallbackAttributeTests`,覆盖空 marker、字符串 fallback 名称的去空/去重/排序、直接 `Type` fallback 的去空/去重/排序,以及两个重载对空参数数组的防御行为 + - worker 已独立验证 `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsReflectionFallbackAttributeTests"`,结果为 `5/5` passed + - 该叶子级测试批次已作为独立提交落地:`86a24e00` `test(cqrs): 新增 ReflectionFallbackAttribute 合同测试` + ### 阶段:cached executor 上下文刷新回归(CQRS-REWRITE-RP-057) - 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮只处理一个窄写集测试批次:为 cached request pipeline executor 增加“重复分发仍重新注入上下文”的回归 From 52b9ddd4a7e32c00c7a791c2c00a262cda7042b5 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 18:00:07 +0800 Subject: [PATCH 10/15] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E5=85=85=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87=E5=89=8D=E7=BD=AE=E6=9D=A1=E4=BB=B6=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 dispatcher 上下文校验测试,锁定非 IArchitectureContext 上下文的 request、notification 与 stream 失败语义 - 通过公开 runtime 工厂与最小容器 mock 覆盖调用前校验路径,不改 runtime 实现 - 更新 cqrs-rewrite 跟踪与 trace,记录 RP-060 的验证结论 --- .../CqrsDispatcherContextValidationTests.cs | 165 ++++++++++++++++++ .../todos/cqrs-rewrite-migration-tracking.md | 12 +- .../traces/cqrs-rewrite-migration-trace.md | 17 ++ 3 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs new file mode 100644 index 00000000..bf1998a9 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs @@ -0,0 +1,165 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Cqrs; +using GFramework.Cqrs.Tests.Logging; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 验证默认 dispatcher 在上下文注入前置条件不满足时的失败语义。 +/// +[TestFixture] +internal sealed class CqrsDispatcherContextValidationTests +{ + /// + /// 验证当 request handler 需要上下文注入、但当前 CQRS 上下文不实现 时, + /// dispatcher 会在调用前显式失败。 + /// + [Test] + public void SendAsync_Should_Throw_When_Context_Does_Not_Implement_IArchitectureContext() + { + var runtime = CreateRuntime( + container => + { + container + .Setup(currentContainer => currentContainer.Get(typeof(IRequestHandler))) + .Returns(new ContextAwareRequestHandler()); + container + .Setup(currentContainer => currentContainer.GetAll(typeof(IPipelineBehavior))) + .Returns(Array.Empty()); + }); + + Assert.That( + async () => await runtime.SendAsync(new FakeCqrsContext(), new ContextAwareRequest()).ConfigureAwait(false), + Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext")); + } + + /// + /// 验证当 notification handler 需要上下文注入、但当前 CQRS 上下文不实现 时, + /// dispatcher 会在发布前显式失败。 + /// + [Test] + public void PublishAsync_Should_Throw_When_Context_Does_Not_Implement_IArchitectureContext() + { + var runtime = CreateRuntime( + container => + { + container + .Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler))) + .Returns([new ContextAwareNotificationHandler()]); + }); + + Assert.That( + async () => await runtime.PublishAsync(new FakeCqrsContext(), new ContextAwareNotification()).ConfigureAwait(false), + Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext")); + } + + /// + /// 验证当 stream handler 需要上下文注入、但当前 CQRS 上下文不实现 时, + /// dispatcher 会在建流前显式失败。 + /// + [Test] + public void CreateStream_Should_Throw_When_Context_Does_Not_Implement_IArchitectureContext() + { + var runtime = CreateRuntime( + container => + { + container + .Setup(currentContainer => currentContainer.Get(typeof(IStreamRequestHandler))) + .Returns(new ContextAwareStreamHandler()); + }); + + Assert.That( + () => runtime.CreateStream(new FakeCqrsContext(), new ContextAwareStreamRequest()), + Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext")); + } + + /// + /// 创建一个只满足当前测试最小依赖面的 dispatcher runtime。 + /// + /// 对容器 mock 的额外配置。 + /// 默认 CQRS runtime。 + private static GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime CreateRuntime( + Action> configureContainer) + { + var container = new Mock(MockBehavior.Strict); + var logger = new TestLogger("CqrsDispatcherContextValidationTests", LogLevel.Debug); + + configureContainer(container); + return CqrsRuntimeFactory.CreateRuntime(container.Object, logger); + } + + /// + /// 为失败语义测试提供最小 CQRS 上下文标记,但故意不实现架构上下文能力。 + /// + private sealed class FakeCqrsContext : ICqrsContext + { + } + + /// + /// 为 request 上下文校验提供最小测试请求。 + /// + private sealed record ContextAwareRequest : IRequest; + + /// + /// 为 notification 上下文校验提供最小测试通知。 + /// + private sealed record ContextAwareNotification : INotification; + + /// + /// 为 stream 上下文校验提供最小测试请求。 + /// + private sealed record ContextAwareStreamRequest : IStreamRequest; + + /// + /// 为 request 上下文校验提供需要注入架构上下文的最小 handler。 + /// + private sealed class ContextAwareRequestHandler : CqrsContextAwareHandlerBase, IRequestHandler + { + /// + /// 返回固定结果;当前测试只关心调用前的上下文校验。 + /// + public ValueTask Handle(ContextAwareRequest request, CancellationToken cancellationToken) + { + return ValueTask.FromResult(1); + } + } + + /// + /// 为 notification 上下文校验提供需要注入架构上下文的最小 handler。 + /// + private sealed class ContextAwareNotificationHandler + : CqrsContextAwareHandlerBase, + INotificationHandler + { + /// + /// 返回已完成任务;当前测试只关心调用前的上下文校验。 + /// + public ValueTask Handle(ContextAwareNotification notification, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } + } + + /// + /// 为 stream 上下文校验提供需要注入架构上下文的最小 handler。 + /// + private sealed class ContextAwareStreamHandler + : CqrsContextAwareHandlerBase, + IStreamRequestHandler + { + /// + /// 返回一个最小流;当前测试只关心建流前的上下文校验。 + /// + public async IAsyncEnumerable Handle( + ContextAwareStreamRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + yield return 1; + await ValueTask.CompletedTask.ConfigureAwait(false); + } + } +} 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 fec9f710..da5f6f2e 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 @@ -7,7 +7,7 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-059` +- 恢复点编号:`CQRS-REWRITE-RP-060` - 当前阶段:`Phase 8` - 当前焦点: - 当前功能历史已归档,active 跟踪仅保留 `Phase 8` 主线的恢复入口 @@ -19,6 +19,7 @@ CQRS 迁移与收敛。 - 已补充 dispatcher pipeline executor 缓存与双行为顺序回归,锁定缓存复用后仍保持现有行为执行顺序 - 已补充 cached request pipeline executor 的上下文刷新回归,锁定 executor 复用时仍会为当次 handler / singleton behavior 重新注入当前 `ArchitectureContext` - 已补充 cached notification / stream dispatch binding 的上下文刷新回归,锁定 binding 复用时仍会为当次 handler 重新注入当前 `ArchitectureContext` + - 已补充非 `IArchitectureContext` 的 dispatcher 失败语义回归,锁定 context-aware request / notification / stream handler 在注入前置条件不满足时会显式抛出异常 - 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke` - 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物 - 已修正 pointer / function pointer 泛型合同的错误覆盖:生成器不再为这两类类型发射 precise runtime type 重建代码 @@ -101,6 +102,10 @@ CQRS 迁移与收敛。 - `GFramework.Cqrs.Tests` 已新增 `DispatcherNotificationContextRefresh*` 与 `DispatcherStreamContextRefresh*` 测试替身,分别记录 notification handler 与 stream handler 在重复分发时观察到的实例身份与 `ArchitectureContext` - `CqrsDispatcherCacheTests` 现明确断言:同一个 cached notification / stream dispatch binding 在重复分发时会继续命中同一 binding,但不会跨分发保留旧上下文 - 本轮定向测试未暴露新的 runtime 缺口,因此没有改动 `GFramework.Cqrs/Internal/CqrsDispatcher.cs` +- `2026-04-29` 已完成一轮 dispatcher 上下文前置条件失败语义回归: + - `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 已通过公开工厂 `CqrsRuntimeFactory.CreateRuntime(...)` 锁定默认 dispatcher 的失败语义 + - 当 context-aware request / notification / stream handler 遇到仅实现 `ICqrsContext`、但未实现 `IArchitectureContext` 的上下文时,dispatcher 会在调用前显式抛出 `InvalidOperationException` + - 本轮只补测试,不改 runtime 实现与文档口径 - `2026-04-29` 已接受一轮 delegated 叶子级 fallback 合同测试: - `GFramework.Cqrs.Tests/Cqrs/CqrsReflectionFallbackAttributeTests.cs` 已锁定空 marker、字符串 fallback 名称去空/去重/排序、直接 `Type` fallback 去空/去重/排序与空参数数组防御语义 - 当前 runtime 读取程序集级 fallback 元数据时所依赖的 attribute 归一化合同,现已有独立叶子级测试文件覆盖 @@ -140,6 +145,9 @@ CQRS 迁移与收敛。 - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` - 结果:通过 - 备注:`7/7` 测试通过;本轮新增 cached notification / stream binding 上下文刷新回归,确认 binding 复用时仍按当次分发重新注入上下文 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherContextValidationTests"` + - 结果:通过 + - 备注:`3/3` 测试通过;本轮锁定默认 dispatcher 对非 `IArchitectureContext` 上下文的 request / notification / stream 失败语义,且未引入新增 warning - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false` - 结果:通过 - 备注:`63/63` 测试通过;当前沙箱限制了 MSBuild named pipe,验证需在提权环境下运行 @@ -209,6 +217,6 @@ CQRS 迁移与收敛。 ## 下一步 -1. 继续 `Phase 8` 主线,优先再找一个收益明确且写集独立的 generator 或 registrar/dispatcher 热点;当前工作区若提交主线程 notification / stream 回归批次,相对 `origin/main` 的累计 diff 将达到 `29 files`,仍低于本轮 `gframework-batch-boot 50` 的主要 stop condition +1. 继续 `Phase 8` 主线,优先再找一个收益明确且写集独立的 generator 或 registrar/dispatcher 热点;当前工作区若提交 dispatcher 上下文前置条件回归批次,相对 `origin/main` 的累计 diff 将达到 `31 files`,仍低于本轮 `gframework-batch-boot 50` 的主要 stop condition 2. 若继续文档主线,优先再扫教程入口页与 API 参考中的 CQRS 采用说明,确认是否还有旧 Command / Query 迁移口径残留 3. 若后续再出现新的 PR review 或 review thread 变化,再重新执行 `$gframework-pr-review` 作为独立验证步骤 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 41a14f44..1e376e35 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 @@ -2,6 +2,23 @@ ## 2026-04-29 +### 阶段:dispatcher 上下文前置条件失败语义回归(CQRS-REWRITE-RP-060) + +- 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮选择一个新的单文件测试切片:锁定默认 dispatcher 对“仅实现 `ICqrsContext`、但未实现 `IArchitectureContext` 的上下文”会如何失败 +- 主线程先复核当前公开契约与实现后确认: + - `GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime` 的 XML 文档已经把这类失败语义写成公开契约 + - `CqrsDispatcher.PrepareHandler(...)` 当前正是唯一的上下文前置条件检查点,因此本轮最稳妥的切片仍是测试补强,而不是继续改 runtime +- 已完成的测试补强: + - 新增 `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` + - 通过 `CqrsRuntimeFactory.CreateRuntime(...)` + `Mock` 构造最小 runtime,分别锁定 request、notification、stream 三条路径的失败语义 + - 三个测试都只在需要上下文注入的 handler 已解析出来时触发,避免把“找不到 handler”与“上下文不满足注入前置条件”混淆成同一种异常 +- 定向验证已通过: + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherContextValidationTests"` + - `3/3` passed +- 结果: + - 本轮只补测试,不改 `GFramework.Cqrs/Internal/CqrsDispatcher.cs` + - 若连同当前工作区一起计算,当前分支相对 `origin/main` 的累计 diff 将达到 `31 files` + ### 阶段:notification / stream binding 上下文刷新回归(CQRS-REWRITE-RP-059) - 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮继续沿着上一批 dispatcher cached executor 上下文回归往外扩一圈,但只覆盖 notification / stream 两条非 request 路径 From a445807b83d76ecdc91754d6a227b6eb407bbd8a Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 18:02:23 +0800 Subject: [PATCH 11/15] =?UTF-8?q?test(cqrs):=20=E6=96=B0=E5=A2=9E=20regist?= =?UTF-8?q?rar=20fallback=20=E5=A4=B1=E8=B4=A5=E5=88=86=E6=94=AF=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增独立测试文件覆盖 fallback 名称无法解析时的 warning 与跳过行为 - 新增 fallback 名称解析抛异常时的 warning 回归断言 - 补充 direct fallback 跨程序集条目被跳过并记录 warning 的验证 --- ...qrsHandlerRegistrarFallbackFailureTests.cs | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs new file mode 100644 index 00000000..213f3870 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs @@ -0,0 +1,254 @@ +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Ioc; +using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Tests.Logging; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 验证 CQRS handler registrar 在 reflection fallback 元数据失效时的可观察告警行为。 +/// +[TestFixture] +internal sealed class CqrsHandlerRegistrarFallbackFailureTests +{ + private ILoggerFactoryProvider? _originalLoggerFactoryProvider; + private CapturingLoggerFactoryProvider? _capturingLoggerFactoryProvider; + + /// + /// 切换为捕获型日志工厂,并清空 registrar 进程级缓存,避免跨用例共享状态污染断言。 + /// + [SetUp] + public void SetUp() + { + _originalLoggerFactoryProvider = LoggerFactoryResolver.Provider; + _capturingLoggerFactoryProvider = new CapturingLoggerFactoryProvider(LogLevel.Warning); + LoggerFactoryResolver.Provider = _capturingLoggerFactoryProvider; + ClearRegistrarCaches(); + } + + /// + /// 恢复测试前的日志工厂,并清理 registrar 缓存。 + /// + [TearDown] + public void TearDown() + { + LoggerFactoryResolver.Provider = _originalLoggerFactoryProvider!; + _capturingLoggerFactoryProvider = null; + _originalLoggerFactoryProvider = null; + ClearRegistrarCaches(); + } + + /// + /// 验证当 fallback 类型名无法解析时,registrar 会跳过该条目并记录告警。 + /// + [Test] + public void RegisterHandlers_Should_Skip_Unresolvable_Named_Fallback_And_Log_Warning() + { + const string missingTypeName = + "GFramework.Cqrs.Tests.Cqrs.MissingGeneratedRegistryNotificationHandler"; + var generatedAssembly = CreateGeneratedFallbackAssembly( + "GFramework.Cqrs.Tests.Cqrs.NamedFallbackMissingAssembly, Version=1.0.0.0", + new CqrsReflectionFallbackAttribute(missingTypeName)); + generatedAssembly + .Setup(static assembly => assembly.GetType(missingTypeName, false, false)) + .Returns((Type?)null); + + var container = new MicrosoftDiContainer(); + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + + Assert.Multiple(() => + { + Assert.That( + GetGeneratedRegistryNotificationHandlerTypes(container), + Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)])); + Assert.That( + GetWarningLogs().Any(log => + log.Message.Contains( + $"Generated CQRS reflection fallback type {missingTypeName} could not be resolved", + StringComparison.Ordinal)), + Is.True); + }); + } + + /// + /// 验证当 fallback 类型名解析抛出异常时,registrar 会记录该加载失败告警并继续跳过条目。 + /// + [Test] + public void RegisterHandlers_Should_Log_Warning_When_Named_Fallback_Resolution_Throws() + { + const string failingTypeName = + "GFramework.Cqrs.Tests.Cqrs.ThrowingGeneratedRegistryNotificationHandler"; + const string exceptionMessage = "Fallback resolution exploded."; + var generatedAssembly = CreateGeneratedFallbackAssembly( + "GFramework.Cqrs.Tests.Cqrs.NamedFallbackThrowingAssembly, Version=1.0.0.0", + new CqrsReflectionFallbackAttribute(failingTypeName)); + generatedAssembly + .Setup(static assembly => assembly.GetType(failingTypeName, false, false)) + .Throws(new TypeLoadException(exceptionMessage)); + + var container = new MicrosoftDiContainer(); + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + + Assert.Multiple(() => + { + Assert.That( + GetGeneratedRegistryNotificationHandlerTypes(container), + Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)])); + Assert.That( + GetWarningLogs().Any(log => + log.Message.Contains( + $"Generated CQRS reflection fallback type {failingTypeName} failed to load", + StringComparison.Ordinal) && + log.Message.Contains(exceptionMessage, StringComparison.Ordinal)), + Is.True); + }); + } + + /// + /// 验证当 direct fallback 类型属于其他程序集时,registrar 会跳过该条目并记录跨程序集告警。 + /// + [Test] + public void RegisterHandlers_Should_Skip_Cross_Assembly_Direct_Fallback_Type_And_Log_Warning() + { + var crossAssemblyFallbackType = ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType; + var generatedAssembly = CreateGeneratedFallbackAssembly( + "GFramework.Cqrs.Tests.Cqrs.DirectFallbackMismatchAssembly, Version=1.0.0.0", + new CqrsReflectionFallbackAttribute(crossAssemblyFallbackType)); + + var container = new MicrosoftDiContainer(); + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + + Assert.Multiple(() => + { + Assert.That( + GetGeneratedRegistryNotificationHandlerTypes(container), + Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)])); + Assert.That( + GetWarningLogs().Any(log => + log.Message.Contains( + $"Generated CQRS reflection fallback type {crossAssemblyFallbackType.FullName} was declared on assembly", + StringComparison.Ordinal) && + log.Message.Contains("Skipping mismatched fallback entry.", StringComparison.Ordinal)), + Is.True); + }); + } + + /// + /// 创建一个仅通过 generated registry 注册主 handler、并附带指定 fallback 元数据的程序集替身。 + /// + /// 用于日志与缓存键的程序集名。 + /// 要暴露给 registrar 的 fallback attribute。 + /// 已完成基础接线的程序集 mock。 + private static Mock CreateGeneratedFallbackAssembly( + string assemblyName, + CqrsReflectionFallbackAttribute fallbackAttribute) + { + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns(assemblyName); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false)) + .Returns([fallbackAttribute]); + return generatedAssembly; + } + + /// + /// 提取容器中针对 generated notification 注册的处理器实现类型。 + /// + /// 已执行注册的测试容器。 + /// 按注册顺序返回的处理器类型数组。 + private static Type[] GetGeneratedRegistryNotificationHandlerTypes(MicrosoftDiContainer container) + { + return container.GetServicesUnsafe + .Where(static descriptor => + descriptor.ServiceType == typeof(INotificationHandler) && + descriptor.ImplementationType is not null) + .Select(static descriptor => descriptor.ImplementationType!) + .ToArray(); + } + + /// + /// 清空本测试依赖的 registrar 静态缓存,确保每个用例都会重新执行 fallback 元数据解析。 + /// + private static void ClearRegistrarCaches() + { + ClearCache(GetRegistrarCacheField("AssemblyMetadataCache")); + ClearCache(GetRegistrarCacheField("RegistryActivationMetadataCache")); + ClearCache(GetRegistrarCacheField("LoadableTypesCache")); + ClearCache(GetRegistrarCacheField("SupportedHandlerInterfacesCache")); + } + + /// + /// 通过反射读取 registrar 的静态缓存字段。 + /// + /// 缓存字段名。 + /// 缓存实例。 + private static object GetRegistrarCacheField(string fieldName) + { + var field = GetRegistrarType().GetField( + fieldName, + BindingFlags.NonPublic | BindingFlags.Static); + + Assert.That(field, Is.Not.Null, $"Missing registrar cache field {fieldName}."); + + return field!.GetValue(null) + ?? throw new InvalidOperationException( + $"Registrar cache field {fieldName} returned null."); + } + + /// + /// 清空缓存对象中的已保存条目。 + /// + /// 目标缓存实例。 + private static void ClearCache(object cache) + { + _ = InvokeInstanceMethod(cache, "Clear"); + } + + /// + /// 调用缓存对象上的实例方法。 + /// + /// 目标对象。 + /// 方法名。 + /// 方法参数。 + /// 方法返回值。 + private static object? InvokeInstanceMethod(object target, string methodName, params object[] arguments) + { + var method = target.GetType().GetMethod( + methodName, + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + Assert.That(method, Is.Not.Null, $"Missing cache method {target.GetType().FullName}.{methodName}."); + + return method!.Invoke(target, arguments); + } + + /// + /// 获取 CQRS handler registrar 的运行时类型。 + /// + /// registrar 实现类型。 + private static Type GetRegistrarType() + { + return typeof(CqrsReflectionFallbackAttribute).Assembly + .GetType("GFramework.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)!; + } + + /// + /// 汇总当前测试期间捕获到的 warning 日志。 + /// + /// 所有 warning 级别日志条目。 + private IReadOnlyList GetWarningLogs() + { + Assert.That(_capturingLoggerFactoryProvider, Is.Not.Null); + + return _capturingLoggerFactoryProvider!.Loggers + .SelectMany(static logger => logger.Logs) + .Where(static log => log.Level == LogLevel.Warning) + .ToArray(); + } +} From bc365197e80b3431f1316eac05b8df053e35f694 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 18:03:52 +0800 Subject: [PATCH 12/15] =?UTF-8?q?docs(cqrs):=20=E5=88=B7=E6=96=B0=E6=89=B9?= =?UTF-8?q?=E5=A4=84=E7=90=86=E6=81=A2=E5=A4=8D=E7=82=B9=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 cqrs-rewrite 跟踪与 trace,记录 RP-061 的 registrar fallback 失败分支批次 - 同步当前 gframework-batch-boot 50 的分支规模与下一步恢复入口 --- .../todos/cqrs-rewrite-migration-tracking.md | 11 +++++++++-- .../traces/cqrs-rewrite-migration-trace.md | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) 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 da5f6f2e..e41c245f 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 @@ -7,7 +7,7 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-060` +- 恢复点编号:`CQRS-REWRITE-RP-061` - 当前阶段:`Phase 8` - 当前焦点: - 当前功能历史已归档,active 跟踪仅保留 `Phase 8` 主线的恢复入口 @@ -20,6 +20,7 @@ CQRS 迁移与收敛。 - 已补充 cached request pipeline executor 的上下文刷新回归,锁定 executor 复用时仍会为当次 handler / singleton behavior 重新注入当前 `ArchitectureContext` - 已补充 cached notification / stream dispatch binding 的上下文刷新回归,锁定 binding 复用时仍会为当次 handler 重新注入当前 `ArchitectureContext` - 已补充非 `IArchitectureContext` 的 dispatcher 失败语义回归,锁定 context-aware request / notification / stream handler 在注入前置条件不满足时会显式抛出异常 + - 已补充 registrar fallback 失败分支回归,锁定 named fallback 无法解析、named fallback 解析抛异常、direct fallback 跨程序集三类 warning 语义 - 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke` - 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物 - 已修正 pointer / function pointer 泛型合同的错误覆盖:生成器不再为这两类类型发射 precise runtime type 重建代码 @@ -106,6 +107,9 @@ CQRS 迁移与收敛。 - `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 已通过公开工厂 `CqrsRuntimeFactory.CreateRuntime(...)` 锁定默认 dispatcher 的失败语义 - 当 context-aware request / notification / stream handler 遇到仅实现 `ICqrsContext`、但未实现 `IArchitectureContext` 的上下文时,dispatcher 会在调用前显式抛出 `InvalidOperationException` - 本轮只补测试,不改 runtime 实现与文档口径 +- `2026-04-29` 已接受一轮 delegated registrar fallback 失败分支测试: + - `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs` 已覆盖 named fallback 无法解析、named fallback 解析抛异常、direct fallback 跨程序集三类 warning 语义 + - 主线程已复核该新文件并重新执行定向测试,确认当前 registrar 在 fallback 元数据失效时仍保持“跳过条目 + 记录告警”的既有语义 - `2026-04-29` 已接受一轮 delegated 叶子级 fallback 合同测试: - `GFramework.Cqrs.Tests/Cqrs/CqrsReflectionFallbackAttributeTests.cs` 已锁定空 marker、字符串 fallback 名称去空/去重/排序、直接 `Type` fallback 去空/去重/排序与空参数数组防御语义 - 当前 runtime 读取程序集级 fallback 元数据时所依赖的 attribute 归一化合同,现已有独立叶子级测试文件覆盖 @@ -148,6 +152,9 @@ CQRS 迁移与收敛。 - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherContextValidationTests"` - 结果:通过 - 备注:`3/3` 测试通过;本轮锁定默认 dispatcher 对非 `IArchitectureContext` 上下文的 request / notification / stream 失败语义,且未引入新增 warning +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarFallbackFailureTests"` + - 结果:通过 + - 备注:`3/3` 测试通过;本轮锁定 registrar 在 fallback 元数据失效时的 warning 语义,且保持 generated registry 主路径不回退 - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false` - 结果:通过 - 备注:`63/63` 测试通过;当前沙箱限制了 MSBuild named pipe,验证需在提权环境下运行 @@ -217,6 +224,6 @@ CQRS 迁移与收敛。 ## 下一步 -1. 继续 `Phase 8` 主线,优先再找一个收益明确且写集独立的 generator 或 registrar/dispatcher 热点;当前工作区若提交 dispatcher 上下文前置条件回归批次,相对 `origin/main` 的累计 diff 将达到 `31 files`,仍低于本轮 `gframework-batch-boot 50` 的主要 stop condition +1. 继续 `Phase 8` 主线,优先再找一个收益明确且写集独立的 generator 或 registrar/dispatcher 热点;当前工作区若提交 registrar fallback 失败分支回归批次,相对 `origin/main` 的累计 diff 将达到 `32 files`,仍低于本轮 `gframework-batch-boot 50` 的主要 stop condition 2. 若继续文档主线,优先再扫教程入口页与 API 参考中的 CQRS 采用说明,确认是否还有旧 Command / Query 迁移口径残留 3. 若后续再出现新的 PR review 或 review thread 变化,再重新执行 `$gframework-pr-review` 作为独立验证步骤 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 1e376e35..eb37d88c 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 @@ -2,6 +2,21 @@ ## 2026-04-29 +### 阶段:registrar fallback 失败分支回归(CQRS-REWRITE-RP-061) + +- 本轮继续按 `gframework-batch-boot 50` 的并行约束,把一个与主线程写集独立的新测试文件交给 worker: + - delegated scope:`GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs` + - delegated objective:锁定 registrar 在 fallback 元数据失效时的 warning 语义,而不扩张到 runtime 实现修改 +- 主线程接受结果前的复核结论: + - 该文件只复用现有 generated-registry 测试替身与捕获型日志工厂,不修改 `CqrsHandlerRegistrarTests.cs` 与生产代码 + - 三个用例分别覆盖 named fallback 无法解析、named fallback 解析抛异常、direct fallback 类型跨程序集三条失败分支 +- 主线程已复核并重新执行定向验证: + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarFallbackFailureTests"` + - `3/3` passed +- 结果: + - 当前 registrar 仍保持“跳过无效 fallback 条目 + 记录 warning”的既有语义 + - 若连同当前工作区一起计算,当前分支相对 `origin/main` 的累计 diff 将达到 `32 files` + ### 阶段:dispatcher 上下文前置条件失败语义回归(CQRS-REWRITE-RP-060) - 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮选择一个新的单文件测试切片:锁定默认 dispatcher 对“仅实现 `ICqrsContext`、但未实现 `IArchitectureContext` 的上下文”会如何失败 From 255a6a152e06be24c9f8215107f5af22b8c297d7 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:43:42 +0800 Subject: [PATCH 13/15] =?UTF-8?q?fix(cqrs):=20=E6=94=B6=E6=95=9B=20PR=2030?= =?UTF-8?q?4=20review=20=E8=B7=9F=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 CqrsDispatcher 的 pipeline invoker 重复创建,并补齐缓存线程模型文档 - 优化 CQRS 与 generator 回归测试的并发保护和稳定语义断言 - 更新 cqrs-rewrite 跟踪与 trace,记录 RP-062 的 PR review follow-up 验证结果 --- .../Cqrs/CqrsDispatcherCacheTests.cs | 10 ++++ ...qrsHandlerRegistrarFallbackFailureTests.cs | 8 ++- .../Cqrs/CqrsRegistrationServiceTests.cs | 17 +++--- ...spatcherNotificationContextRefreshState.cs | 28 ++++++++-- .../DispatcherPipelineContextRefreshState.cs | 52 +++++++++++++++---- ...patcherPipelineOrderCacheRequestHandler.cs | 2 +- .../DispatcherPipelineOrderInnerBehavior.cs | 4 +- .../DispatcherPipelineOrderOuterBehavior.cs | 4 +- .../Cqrs/DispatcherPipelineOrderState.cs | 36 +++++++++++-- .../DispatcherStreamContextRefreshState.cs | 28 ++++++++-- GFramework.Cqrs/Internal/CqrsDispatcher.cs | 27 +++++++--- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 33 ++++++------ .../todos/cqrs-rewrite-migration-tracking.md | 10 +++- .../traces/cqrs-rewrite-migration-trace.md | 31 +++++++++++ 14 files changed, 228 insertions(+), 62 deletions(-) diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs index 3d2040ab..cd95d2d8 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs @@ -10,6 +10,7 @@ namespace GFramework.Cqrs.Tests.Cqrs; /// 验证 CQRS dispatcher 会缓存热路径中的 dispatch binding。 /// [TestFixture] +[NonParallelizable] internal sealed class CqrsDispatcherCacheTests { private MicrosoftDiContainer? _container; @@ -434,6 +435,11 @@ internal sealed class CqrsDispatcherCacheTests /// /// 读取 request dispatch binding 中指定行为数量的 pipeline executor 缓存项。 /// + /// dispatcher 内部的 request binding 缓存对象。 + /// 要读取的请求运行时类型。 + /// 要读取的响应运行时类型。 + /// 目标 executor 对应的行为数量。 + /// 已缓存的 executor;若 binding 或 executor 尚未建立则返回 private static object? GetRequestPipelineExecutorValue( object requestBindings, Type requestType, @@ -471,6 +477,10 @@ internal sealed class CqrsDispatcherCacheTests /// /// 读取指定请求/响应类型对对应的强类型 request dispatch binding。 /// + /// dispatcher 内部的 request binding 缓存对象。 + /// 要读取的请求运行时类型。 + /// 要读取的响应运行时类型。 + /// 强类型 binding;若缓存尚未建立则返回 private static object? GetRequestDispatchBindingValue(object requestBindings, Type requestType, Type responseType) { var bindingBox = GetPairCacheValue(requestBindings, requestType, responseType); diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs index 213f3870..e4b8633f 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs @@ -174,6 +174,7 @@ internal sealed class CqrsHandlerRegistrarFallbackFailureTests /// /// 清空本测试依赖的 registrar 静态缓存,确保每个用例都会重新执行 fallback 元数据解析。 + /// 这些字段名直接耦合 CqrsHandlerRegistrar 当前内部实现;若后续重构缓存布局,需要同步更新这里。 /// private static void ClearRegistrarCaches() { @@ -194,11 +195,14 @@ internal sealed class CqrsHandlerRegistrarFallbackFailureTests fieldName, BindingFlags.NonPublic | BindingFlags.Static); - Assert.That(field, Is.Not.Null, $"Missing registrar cache field {fieldName}."); + Assert.That( + field, + Is.Not.Null, + $"Expected field '{fieldName}' on CqrsHandlerRegistrar not found; rename/refactor may require test update."); return field!.GetValue(null) ?? throw new InvalidOperationException( - $"Registrar cache field {fieldName} returned null."); + $"Registrar cache field '{fieldName}' on CqrsHandlerRegistrar returned null."); } /// diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs index b8b03b1c..b1c3c60b 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs @@ -69,13 +69,18 @@ internal sealed class CqrsRegistrationServiceTests Assert.Multiple(() => { Assert.That(registeredAssemblies, Is.EqualTo([firstAssembly.Object])); + var debugMessages = logger.Logs + .Where(static log => log.Level == LogLevel.Debug) + .Select(static log => log.Message) + .ToArray(); + Assert.That(debugMessages, Has.Length.EqualTo(1)); Assert.That( - logger.Logs.Where(static log => log.Level == LogLevel.Debug).Select(static log => log.Message), - Is.EqualTo( - [ - "Skipping CQRS handler registration for assembly " + - "GFramework.Cqrs.Tests.RegisteredAssembly, Version=1.0.0.0 because it was already registered." - ])); + debugMessages[0], + Does.Contain("Skipping CQRS handler registration for assembly")); + Assert.That( + debugMessages[0], + Does.Contain("GFramework.Cqrs.Tests.RegisteredAssembly, Version=1.0.0.0")); + Assert.That(debugMessages[0], Does.Contain("already registered")); }); } diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshState.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshState.cs index 43c32531..b6d8a7ca 100644 --- a/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshState.cs +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshState.cs @@ -8,12 +8,24 @@ namespace GFramework.Cqrs.Tests.Cqrs; /// internal static class DispatcherNotificationContextRefreshState { + private static readonly Lock SyncRoot = new(); private static int _nextHandlerInstanceId; + private static readonly List _handlerSnapshots = []; /// - /// 获取每次 notification 分发时记录的快照。 + /// 获取每次 notification 分发时记录的快照副本。 + /// 共享状态通过 SyncRoot 串行化,避免并行测试写入抖动。 /// - public static List HandlerSnapshots { get; } = []; + public static IReadOnlyList HandlerSnapshots + { + get + { + lock (SyncRoot) + { + return _handlerSnapshots.ToArray(); + } + } + } /// /// 为新的 handler 测试实例分配稳定编号。 @@ -28,7 +40,10 @@ internal static class DispatcherNotificationContextRefreshState /// public static void Record(string dispatchId, int instanceId, IArchitectureContext context) { - HandlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + lock (SyncRoot) + { + _handlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + } } /// @@ -36,7 +51,10 @@ internal static class DispatcherNotificationContextRefreshState /// public static void Reset() { - _nextHandlerInstanceId = 0; - HandlerSnapshots.Clear(); + lock (SyncRoot) + { + _nextHandlerInstanceId = 0; + _handlerSnapshots.Clear(); + } } } diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshState.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshState.cs index 15913678..99cae23a 100644 --- a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshState.cs +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshState.cs @@ -8,18 +8,41 @@ namespace GFramework.Cqrs.Tests.Cqrs; /// internal static class DispatcherPipelineContextRefreshState { + private static readonly Lock SyncRoot = new(); private static int _nextBehaviorInstanceId; private static int _nextHandlerInstanceId; + private static readonly List _behaviorSnapshots = []; + private static readonly List _handlerSnapshots = []; /// - /// 获取每次 behavior 执行时记录的快照。 + /// 获取每次 behavior 执行时记录的快照副本。 + /// 共享状态通过 SyncRoot 串行化,读取端始终拿到当前稳定快照。 /// - public static List BehaviorSnapshots { get; } = []; + public static IReadOnlyList BehaviorSnapshots + { + get + { + lock (SyncRoot) + { + return _behaviorSnapshots.ToArray(); + } + } + } /// - /// 获取每次 handler 执行时记录的快照。 + /// 获取每次 handler 执行时记录的快照副本。 + /// 共享状态通过 SyncRoot 串行化,读取端始终拿到当前稳定快照。 /// - public static List HandlerSnapshots { get; } = []; + public static IReadOnlyList HandlerSnapshots + { + get + { + lock (SyncRoot) + { + return _handlerSnapshots.ToArray(); + } + } + } /// /// 为新的 behavior 测试实例分配稳定编号。 @@ -42,7 +65,10 @@ internal static class DispatcherPipelineContextRefreshState /// public static void RecordBehavior(string dispatchId, int instanceId, IArchitectureContext context) { - BehaviorSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + lock (SyncRoot) + { + _behaviorSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + } } /// @@ -50,7 +76,10 @@ internal static class DispatcherPipelineContextRefreshState /// public static void RecordHandler(string dispatchId, int instanceId, IArchitectureContext context) { - HandlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + lock (SyncRoot) + { + _handlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + } } /// @@ -58,9 +87,12 @@ internal static class DispatcherPipelineContextRefreshState /// public static void Reset() { - _nextBehaviorInstanceId = 0; - _nextHandlerInstanceId = 0; - BehaviorSnapshots.Clear(); - HandlerSnapshots.Clear(); + lock (SyncRoot) + { + _nextBehaviorInstanceId = 0; + _nextHandlerInstanceId = 0; + _behaviorSnapshots.Clear(); + _handlerSnapshots.Clear(); + } } } diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequestHandler.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequestHandler.cs index 00224ff8..d2b88baf 100644 --- a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequestHandler.cs +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequestHandler.cs @@ -15,7 +15,7 @@ internal sealed class DispatcherPipelineOrderCacheRequestHandler : IRequestHandl /// 固定整数结果。 public ValueTask Handle(DispatcherPipelineOrderCacheRequest request, CancellationToken cancellationToken) { - DispatcherPipelineOrderState.Steps.Add("Handler"); + DispatcherPipelineOrderState.Record("Handler"); return ValueTask.FromResult(3); } } diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderInnerBehavior.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderInnerBehavior.cs index a4b7def6..cf327d2a 100644 --- a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderInnerBehavior.cs +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderInnerBehavior.cs @@ -19,9 +19,9 @@ internal sealed class DispatcherPipelineOrderInnerBehavior : IPipelineBehavior next, CancellationToken cancellationToken) { - DispatcherPipelineOrderState.Steps.Add("Inner:Before"); + DispatcherPipelineOrderState.Record("Inner:Before"); var result = await next(request, cancellationToken).ConfigureAwait(false); - DispatcherPipelineOrderState.Steps.Add("Inner:After"); + DispatcherPipelineOrderState.Record("Inner:After"); return result; } } diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderOuterBehavior.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderOuterBehavior.cs index b9ba2315..aa1f0291 100644 --- a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderOuterBehavior.cs +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderOuterBehavior.cs @@ -19,9 +19,9 @@ internal sealed class DispatcherPipelineOrderOuterBehavior : IPipelineBehavior next, CancellationToken cancellationToken) { - DispatcherPipelineOrderState.Steps.Add("Outer:Before"); + DispatcherPipelineOrderState.Record("Outer:Before"); var result = await next(request, cancellationToken).ConfigureAwait(false); - DispatcherPipelineOrderState.Steps.Add("Outer:After"); + DispatcherPipelineOrderState.Record("Outer:After"); return result; } } diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderState.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderState.cs index 2673371e..fa202b85 100644 --- a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderState.cs +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderState.cs @@ -1,3 +1,5 @@ +using System.Threading; + namespace GFramework.Cqrs.Tests.Cqrs; /// @@ -5,16 +7,44 @@ namespace GFramework.Cqrs.Tests.Cqrs; /// internal static class DispatcherPipelineOrderState { + private static readonly Lock SyncRoot = new(); + private static readonly List _steps = []; + /// - /// 获取按执行顺序追加的步骤名称。 + /// 获取按执行顺序追加的步骤快照。 + /// 共享状态通过 SyncRoot 串行化,避免并行行为测试互相污染步骤列表。 /// - public static List Steps { get; } = []; + public static IReadOnlyList Steps + { + get + { + lock (SyncRoot) + { + return _steps.ToArray(); + } + } + } + + /// + /// 记录一个新的 pipeline 执行步骤。 + /// + /// 要追加的步骤名称。 + public static void Record(string step) + { + lock (SyncRoot) + { + _steps.Add(step); + } + } /// /// 清空当前记录,供下一次断言使用。 /// public static void Reset() { - Steps.Clear(); + lock (SyncRoot) + { + _steps.Clear(); + } } } diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs index a315451d..4f747b7e 100644 --- a/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs @@ -8,12 +8,24 @@ namespace GFramework.Cqrs.Tests.Cqrs; /// internal static class DispatcherStreamContextRefreshState { + private static readonly Lock SyncRoot = new(); private static int _nextHandlerInstanceId; + private static readonly List _handlerSnapshots = []; /// - /// 获取每次建流时记录的快照。 + /// 获取每次建流时记录的快照副本。 + /// 共享状态通过 SyncRoot 串行化,避免并行测试写入抖动。 /// - public static List HandlerSnapshots { get; } = []; + public static IReadOnlyList HandlerSnapshots + { + get + { + lock (SyncRoot) + { + return _handlerSnapshots.ToArray(); + } + } + } /// /// 为新的 handler 测试实例分配稳定编号。 @@ -28,7 +40,10 @@ internal static class DispatcherStreamContextRefreshState /// public static void Record(string dispatchId, int instanceId, IArchitectureContext context) { - HandlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + lock (SyncRoot) + { + _handlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + } } /// @@ -36,7 +51,10 @@ internal static class DispatcherStreamContextRefreshState /// public static void Reset() { - _nextHandlerInstanceId = 0; - HandlerSnapshots.Clear(); + lock (SyncRoot) + { + _nextHandlerInstanceId = 0; + _handlerSnapshots.Clear(); + } } } diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index 17e81e8e..d604beac 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -415,7 +415,11 @@ internal sealed class CqrsDispatcher( RequestInvoker requestInvoker, Type requestType) { + // 线程安全:该缓存按 behaviorCount 复用 pipeline executor 形状,GetPipelineExecutor 通过 ConcurrentDictionary + // 的 GetOrAdd 支持并发读写。缓存项只保存委托形状,不保留 handler/behavior 实例;若行为数量组合持续增长, + // 字典会随之增长且当前实现不提供回收。 private readonly ConcurrentDictionary> _pipelineExecutors = new(); + private readonly RequestPipelineInvoker _pipelineInvoker = CreateRequestPipelineInvoker(requestType); /// /// 获取请求处理器在容器中的服务类型。 @@ -441,8 +445,8 @@ internal sealed class CqrsDispatcher( ArgumentOutOfRangeException.ThrowIfNegative(behaviorCount); return _pipelineExecutors.GetOrAdd>( behaviorCount, - static (count, state) => CreateRequestPipelineExecutor(state.RequestType, count), - new RequestPipelineExecutorFactoryState(requestType)); + static (count, state) => CreateRequestPipelineExecutor(count, state.PipelineInvoker), + new RequestPipelineExecutorFactoryState(_pipelineInvoker)); } /// @@ -460,17 +464,23 @@ internal sealed class CqrsDispatcher( /// 行为数量用于表达缓存形状,实际分发仍会消费本次容器解析出的 handler 与 behaviors 实例。 /// private static RequestPipelineExecutor CreateRequestPipelineExecutor( - Type requestType, - int behaviorCount) + int behaviorCount, + RequestPipelineInvoker invoker) { ArgumentOutOfRangeException.ThrowIfNegative(behaviorCount); + return new RequestPipelineExecutor(behaviorCount, invoker); + } + /// + /// 为指定请求/响应类型创建可跨多个 behaviorCount 复用的 typed pipeline invoker。 + /// + private static RequestPipelineInvoker CreateRequestPipelineInvoker(Type requestType) + { var method = RequestPipelineInvokerMethodDefinition .MakeGenericMethod(requestType, typeof(TResponse)); - var invoker = (RequestPipelineInvoker)Delegate.CreateDelegate( + return (RequestPipelineInvoker)Delegate.CreateDelegate( typeof(RequestPipelineInvoker), method); - return new RequestPipelineExecutor(behaviorCount, invoker); } /// @@ -510,7 +520,8 @@ internal sealed class CqrsDispatcher( /// 为 pipeline executor 缓存携带当前请求类型,避免按行为数量建缓存时创建闭包。 /// /// 请求响应类型。 - private readonly record struct RequestPipelineExecutorFactoryState(Type RequestType); + private readonly record struct RequestPipelineExecutorFactoryState( + RequestPipelineInvoker PipelineInvoker); /// /// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。 @@ -537,6 +548,8 @@ internal sealed class CqrsDispatcher( /// /// 获取指定阶段的 continuation,并在首次请求时为该阶段绑定一次不可变调用入口。 /// 同一行为多次调用 next 时会命中相同 continuation,保持与传统链式委托一致的语义。 + /// 线程模型上,该缓存仅假定单次分发链按顺序推进;若某个 behavior 并发调用多个 next, + /// 这里可能重复创建等价 continuation,但不会跨分发共享,也不会缓存容器解析出的实例。 /// private MessageHandlerDelegate GetContinuation(int index) { diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 51926a0d..2b83cddc 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1763,13 +1763,12 @@ public class CqrsHandlerRegistryGeneratorTests { Assert.That( generatedSource, - Does.Contain( - "var serviceType0_0Argument1Element = registryAssembly.GetType(\"TestApp.Container+HiddenResponse\", throwOnError: false, ignoreCase: false);")); + Does.Contain("registryAssembly.GetType(\"TestApp.Container+HiddenResponse\", throwOnError: false, ignoreCase: false);")); Assert.That( generatedSource, - Does.Contain( - "var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1Element.MakeArrayType(2));")); - Assert.That(generatedSource, Does.Not.Contain("RegisterRemainingReflectedHandlerInterfaces(")); + Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(")); + Assert.That(generatedSource, Does.Contain(".MakeArrayType(2)")); + Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute(")); }); } @@ -1786,13 +1785,12 @@ public class CqrsHandlerRegistryGeneratorTests { Assert.That( generatedSource, - Does.Contain( - "var serviceType0_0Argument1ElementElement = registryAssembly.GetType(\"TestApp.Container+HiddenResponse\", throwOnError: false, ignoreCase: false);")); + Does.Contain("registryAssembly.GetType(\"TestApp.Container+HiddenResponse\", throwOnError: false, ignoreCase: false);")); Assert.That( generatedSource, - Does.Contain( - "var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1ElementElement.MakeArrayType().MakeArrayType());")); - Assert.That(generatedSource, Does.Not.Contain("RegisterRemainingReflectedHandlerInterfaces(")); + Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(")); + Assert.That(generatedSource, Does.Contain(".MakeArrayType().MakeArrayType()")); + Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute(")); }); } @@ -1912,11 +1910,11 @@ public class CqrsHandlerRegistryGeneratorTests Assert.That( generatedSource, Does.Contain( - "var serviceType0_0Argument1Element = ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedResponse\");")); + "ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedResponse\")")); Assert.That( generatedSource, - Does.Contain( - "var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1Element.MakeArrayType(2));")); + Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(")); + Assert.That(generatedSource, Does.Contain(".MakeArrayType(2)")); Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute(")); }); } @@ -1945,16 +1943,15 @@ public class CqrsHandlerRegistryGeneratorTests Assert.That( generatedSource, Does.Contain( - "var serviceType0_0Argument0 = ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedRequest\");")); + "ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedRequest\")")); Assert.That( generatedSource, Does.Contain( - "var serviceType0_0Argument1GenericDefinition = ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedEnvelope`1\");")); + "ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedEnvelope`1\")")); Assert.That( generatedSource, - Does.Contain( - "var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1GenericDefinition.MakeGenericType(typeof(string)));")); - Assert.That(generatedSource, Does.Not.Contain("RegisterRemainingReflectedHandlerInterfaces(")); + Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(")); + Assert.That(generatedSource, Does.Contain(".MakeGenericType(typeof(string))")); Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute(")); }); } 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 e41c245f..65874cd5 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 @@ -7,7 +7,7 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-061` +- 恢复点编号:`CQRS-REWRITE-RP-062` - 当前阶段:`Phase 8` - 当前焦点: - 当前功能历史已归档,active 跟踪仅保留 `Phase 8` 主线的恢复入口 @@ -29,6 +29,9 @@ CQRS 迁移与收敛。 - 已将 registrar 的重复映射判定从线性扫描 `IServiceCollection` 收敛为本地映射索引,减少 fallback 注册路径的重复查找 - 已完成一轮 `static lambda + state` 微收敛:`CqrsDispatcher` 与 `CqrsHandlerRegistrar` 现会在弱缓存 / 并发缓存入口优先使用无捕获工厂,继续压低热路径上的额外闭包分配 - 已补充 `CqrsReflectionFallbackAttribute` 叶子级合同测试,锁定空 marker、字符串 fallback 名称归一化、直接 `Type` fallback 归一化与空参数防御语义 + - 已完成 `PR #304` review follow-up 收敛:`CqrsDispatcher` 现补齐 pipeline executor / continuation 缓存的线程模型文档,并把 request pipeline invoker 从按 `behaviorCount` 重复创建收敛为 binding 内复用 + - 已收紧 CQRS / generator 回归测试的脆弱断言:日志断言改为语义匹配,precise runtime type lookup 回归改为锁定数组秩、外部类型查找与“未发射 fallback metadata”这些稳定语义 + - 已为 dispatcher cache / context refresh / pipeline order 三组测试状态容器补齐并发保护,并将 `CqrsDispatcherCacheTests` 标记为 `NonParallelizable`,避免静态缓存与共享快照在并行测试中相互污染 - 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点 ## 当前状态摘要 @@ -79,6 +82,11 @@ CQRS 迁移与收敛。 - latest reviewed commit 当前剩余 `3` 条 open AI review threads:`2` 条 Greptile、`1` 条 CodeRabbit - 本地核对后确认 `dotnet-format` 仍只有 `Restore operation failed` 噪音,没有附带当前仍成立的文件级格式诊断 - 已按 review triage 修正 generator source preamble 的多实例 fallback 特性排版、移除死参数,并补强 mixed/direct fallback 发射回归断言与 XML 文档 +- `2026-04-30` 已重新执行 `$gframework-pr-review`: + - 当前分支对应 `PR #304`,状态为 `OPEN` + - latest reviewed commit 当前剩余 `7` 条 CodeRabbit nitpick 与 `2` 条 Greptile open threads,集中在测试脆弱断言、共享测试状态并发保护,以及 `CqrsDispatcher` 的缓存线程模型文档 + - 本地核对后,已确认这些评论仍对应当前代码;MegaLinter 继续只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,CTRF 汇总为 `2203/2203` passed + - 已在本地完成 follow-up:request pipeline invoker 改为 binding 级复用、共享测试状态切换到 `System.Threading.Lock` 保护、顺序测试改为受控记录接口、`CqrsDispatcherCacheTests` 标记为 `NonParallelizable`,并补齐相关 XML / 线程模型注释 - `2026-04-29` 已完成一轮 precise runtime type lookup 的数组回归补强: - `GFramework.SourceGenerators.Tests` 已新增多维数组、交错数组、外部程序集隐藏元素类型三类回归 - 当前生成器在 precise runtime type lookup 下已稳定保留数组秩信息,并递归发射交错数组的 `MakeArrayType()` 链 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 eb37d88c..25dc1c6a 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 @@ -1,5 +1,36 @@ # CQRS 重写迁移追踪 +## 2026-04-30 + +### 阶段:PR #304 review follow-up 收敛(CQRS-REWRITE-RP-062) + +- 本轮使用 `$gframework-pr-review` 重新抓取当前分支 PR: + - 当前分支 `feat/cqrs-optimization` 对应 `PR #304` + - latest review 信号主要由 `7` 条 CodeRabbit nitpick 与 `2` 条 Greptile open threads 组成 + - MegaLinter 仍只给出 `dotnet-format` 的 `Restore operation failed`,未附带当前仍成立的文件级格式问题;CTRF 汇总为 `2203/2203` passed +- 本地复核后接受并收敛的 review follow-up: + - `GFramework.Cqrs/Internal/CqrsDispatcher.cs` + - 为 `_pipelineExecutors` 与 `RequestPipelineInvocation.GetContinuation(...)` 补齐线程模型与失败模式说明 + - 将 request pipeline invoker 从“按 `behaviorCount` 重复创建”收敛为“binding 内创建一次、executor 缓存复用” + - `GFramework.Cqrs.Tests/Cqrs/*.cs` + - 将 `DispatcherPipelineContextRefreshState`、`DispatcherNotificationContextRefreshState`、`DispatcherStreamContextRefreshState` 与 `DispatcherPipelineOrderState` 切换为 `System.Threading.Lock` 保护的共享状态 + - 将 pipeline 顺序记录从公开可变 `List` 收敛为 `Record(...)` + 快照只读访问 + - 为 `CqrsDispatcherCacheTests` 添加 `[NonParallelizable]`,并补齐反射辅助方法的 XML `param` / `returns` + - 将 `CqrsRegistrationServiceTests` 的 debug 日志断言改为锁定语义片段而非整句文本 + - 将 `CqrsHandlerRegistrarFallbackFailureTests` 的缓存字段诊断改为显式指出 `CqrsHandlerRegistrar` 耦合点 + - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` + - 将 precise runtime type lookup 的数组 / 外部泛型回归断言从局部变量名绑定改为稳定的语义片段断言 +- 验证过程与结果: + - 首次把多个 `dotnet` restore / test 并发跑在同一 worktree 时,`GFramework.Cqrs.Tests` 出现 `*.nuget.g.props already exists` 竞争;该失败属于本地并发 restore 冲突,不代表代码问题 + - 串行重跑后确认: + - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests|FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsRegistrationServiceTests|FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarFallbackFailureTests"` + - 在第一次串行测试中暴露 `MA0158 Use System.Threading.Lock` warning 后,已同轮切换同步原语并准备重跑无 warning 验证 +- 结果: + - 本轮把仍成立的 PR review 评论全部收敛到本地代码或测试基础设施 + - 下一步应以“重跑无 warning 验证 + 提交本轮 follow-up”为恢复入口,而不是继续扩写新的 CQRS 优化切片 + ## 2026-04-29 ### 阶段:registrar fallback 失败分支回归(CQRS-REWRITE-RP-061) From 98021f59e77e829f15cd571ed2bda01df1451596 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 09:00:24 +0800 Subject: [PATCH 14/15] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E9=BD=90=20PR304?= =?UTF-8?q?=20=E6=B5=8B=E8=AF=95=20XML=20=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 补齐上下文校验 handler 的 Handle 参数与返回 XML 注释 - 更新带 DispatchId 的测试请求与通知 XML 参数注释 - 记录 cqrs-rewrite 主题的本轮 PR review 跟进 --- .../Cqrs/CqrsDispatcherContextValidationTests.cs | 9 +++++++++ .../DispatcherNotificationContextRefreshNotification.cs | 1 + .../Cqrs/DispatcherStreamContextRefreshRequest.cs | 1 + .../todos/cqrs-rewrite-migration-tracking.md | 1 + 4 files changed, 12 insertions(+) diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs index bf1998a9..bc12c773 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs @@ -122,6 +122,9 @@ internal sealed class CqrsDispatcherContextValidationTests /// /// 返回固定结果;当前测试只关心调用前的上下文校验。 /// + /// 当前请求。 + /// 取消令牌。 + /// 固定整型结果。 public ValueTask Handle(ContextAwareRequest request, CancellationToken cancellationToken) { return ValueTask.FromResult(1); @@ -138,6 +141,9 @@ internal sealed class CqrsDispatcherContextValidationTests /// /// 返回已完成任务;当前测试只关心调用前的上下文校验。 /// + /// 当前通知。 + /// 取消令牌。 + /// 已完成任务。 public ValueTask Handle(ContextAwareNotification notification, CancellationToken cancellationToken) { return ValueTask.CompletedTask; @@ -154,6 +160,9 @@ internal sealed class CqrsDispatcherContextValidationTests /// /// 返回一个最小流;当前测试只关心建流前的上下文校验。 /// + /// 当前流请求。 + /// 取消枚举时使用的取消令牌。 + /// 包含单个固定元素的异步流。 public async IAsyncEnumerable Handle( ContextAwareStreamRequest request, [EnumeratorCancellation] CancellationToken cancellationToken) diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshNotification.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshNotification.cs index f1f54dd2..faa3f5af 100644 --- a/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshNotification.cs +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshNotification.cs @@ -5,4 +5,5 @@ namespace GFramework.Cqrs.Tests.Cqrs; /// /// 为 notification dispatch binding 上下文刷新回归提供带分发标识的最小通知。 /// +/// 当前分发的稳定标识,便于断言缓存 binding 复用时观察到的是同一次通知。 internal sealed record DispatcherNotificationContextRefreshNotification(string DispatchId) : INotification; diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshRequest.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshRequest.cs index c8d69cfe..f453886e 100644 --- a/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshRequest.cs +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshRequest.cs @@ -5,4 +5,5 @@ namespace GFramework.Cqrs.Tests.Cqrs; /// /// 为 stream dispatch binding 上下文刷新回归提供带分发标识的最小流请求。 /// +/// 当前分发的稳定标识,便于断言缓存 binding 复用时观察到的是同一次建流。 internal sealed record DispatcherStreamContextRefreshRequest(string DispatchId) : IStreamRequest; 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 65874cd5..1d539457 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 @@ -30,6 +30,7 @@ CQRS 迁移与收敛。 - 已完成一轮 `static lambda + state` 微收敛:`CqrsDispatcher` 与 `CqrsHandlerRegistrar` 现会在弱缓存 / 并发缓存入口优先使用无捕获工厂,继续压低热路径上的额外闭包分配 - 已补充 `CqrsReflectionFallbackAttribute` 叶子级合同测试,锁定空 marker、字符串 fallback 名称归一化、直接 `Type` fallback 归一化与空参数防御语义 - 已完成 `PR #304` review follow-up 收敛:`CqrsDispatcher` 现补齐 pipeline executor / continuation 缓存的线程模型文档,并把 request pipeline invoker 从按 `behaviorCount` 重复创建收敛为 binding 内复用 + - 已补齐 `CqrsDispatcherContextValidationTests` 三个上下文校验 handler 的 XML `param` / `returns` 注释,以及 `DispatcherNotificationContextRefreshNotification`、`DispatcherStreamContextRefreshRequest` 的 `DispatchId` XML 参数注释,收敛上一轮 PR review 遗留的文档类 minor feedback - 已收紧 CQRS / generator 回归测试的脆弱断言:日志断言改为语义匹配,precise runtime type lookup 回归改为锁定数组秩、外部类型查找与“未发射 fallback metadata”这些稳定语义 - 已为 dispatcher cache / context refresh / pipeline order 三组测试状态容器补齐并发保护,并将 `CqrsDispatcherCacheTests` 标记为 `NonParallelizable`,避免静态缓存与共享快照在并行测试中相互污染 - 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点 From 72ce0f1199dd46feb5e4dbff717c75509712ac4e Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 09:23:01 +0800 Subject: [PATCH 15/15] =?UTF-8?q?test(cqrs):=20=E6=94=B6=E6=95=9B=E5=89=A9?= =?UTF-8?q?=E4=BD=99=20PR304=20review=20=E8=B7=9F=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 fallback failure 测试夹具的并行执行与 stream state 文档命名问题 - 归档 cqrs-rewrite 历史 trace 与验证记录并压缩 active 恢复入口 - 更新当前验证结果与下一步,保持 PR304 review follow-up 可恢复 --- ...qrsHandlerRegistrarFallbackFailureTests.cs | 1 + .../DispatcherStreamContextRefreshState.cs | 17 +- ...ewrite-validation-history-through-rp062.md | 91 +++++ ...qrs-rewrite-history-rp046-through-rp061.md | 35 ++ .../todos/cqrs-rewrite-migration-tracking.md | 93 +---- .../traces/cqrs-rewrite-migration-trace.md | 360 ++---------------- 6 files changed, 171 insertions(+), 426 deletions(-) create mode 100644 ai-plan/public/cqrs-rewrite/archive/todos/cqrs-rewrite-validation-history-through-rp062.md create mode 100644 ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-rp046-through-rp061.md diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs index e4b8633f..e4d7ef3f 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs @@ -10,6 +10,7 @@ namespace GFramework.Cqrs.Tests.Cqrs; /// 验证 CQRS handler registrar 在 reflection fallback 元数据失效时的可观察告警行为。 /// [TestFixture] +[NonParallelizable] internal sealed class CqrsHandlerRegistrarFallbackFailureTests { private ILoggerFactoryProvider? _originalLoggerFactoryProvider; diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs index 4f747b7e..47be718b 100644 --- a/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs @@ -8,19 +8,20 @@ namespace GFramework.Cqrs.Tests.Cqrs; /// internal static class DispatcherStreamContextRefreshState { - private static readonly Lock SyncRoot = new(); + private static readonly Lock _syncRoot = new(); private static int _nextHandlerInstanceId; private static readonly List _handlerSnapshots = []; /// /// 获取每次建流时记录的快照副本。 - /// 共享状态通过 SyncRoot 串行化,避免并行测试写入抖动。 /// + /// 当前已记录的 handler 上下文快照副本。 + /// 共享状态通过 _syncRoot 串行化,避免并行测试写入抖动。 public static IReadOnlyList HandlerSnapshots { get { - lock (SyncRoot) + lock (_syncRoot) { return _handlerSnapshots.ToArray(); } @@ -30,6 +31,7 @@ internal static class DispatcherStreamContextRefreshState /// /// 为新的 handler 测试实例分配稳定编号。 /// + /// 单调递增的 handler 实例编号。 public static int AllocateHandlerInstanceId() { return Interlocked.Increment(ref _nextHandlerInstanceId); @@ -38,9 +40,13 @@ internal static class DispatcherStreamContextRefreshState /// /// 记录 handler 在当前建流中观察到的上下文。 /// + /// 触发本次记录的稳定分发标识。 + /// 观察到该上下文的 handler 实例编号。 + /// 当前分发注入到 handler 的架构上下文。 + /// 写入过程通过 _syncRoot 串行化,确保快照列表保持稳定顺序。 public static void Record(string dispatchId, int instanceId, IArchitectureContext context) { - lock (SyncRoot) + lock (_syncRoot) { _handlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); } @@ -49,9 +55,10 @@ internal static class DispatcherStreamContextRefreshState /// /// 清空历史记录与实例编号,避免跨测试污染断言。 /// + /// 重置过程通过 _syncRoot 串行化,避免读取端观察到半清理状态。 public static void Reset() { - lock (SyncRoot) + lock (_syncRoot) { _nextHandlerInstanceId = 0; _handlerSnapshots.Clear(); diff --git a/ai-plan/public/cqrs-rewrite/archive/todos/cqrs-rewrite-validation-history-through-rp062.md b/ai-plan/public/cqrs-rewrite/archive/todos/cqrs-rewrite-validation-history-through-rp062.md new file mode 100644 index 00000000..a89af879 --- /dev/null +++ b/ai-plan/public/cqrs-rewrite/archive/todos/cqrs-rewrite-validation-history-through-rp062.md @@ -0,0 +1,91 @@ +# CQRS 重写迁移验证归档(至 RP-062) + +## 说明 + +- 本文件归档原 active tracking 中累积的历史验证命令与阶段性验证结论。 +- `boot` 默认恢复入口应回到 `ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md`,不要再从这里挑选旧命令作为当前下一步。 + +## 原验证记录 + +- `RP-043` 之前的详细阶段记录、定向验证命令和阶段性决策均已移入主题内归档 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` + - 结果:通过 + - 备注:`5/5` 测试通过;本轮新增 cached executor 上下文刷新回归,确认 executor 复用时仍按当次分发重新注入上下文 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsReflectionFallbackAttributeTests"` + - 结果:通过 + - 备注:`5/5` 测试通过;本轮锁定 fallback attribute 的公开归一化合同与空参数防御语义 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` + - 结果:通过 + - 备注:`7/7` 测试通过;本轮新增 cached notification / stream binding 上下文刷新回归,确认 binding 复用时仍按当次分发重新注入上下文 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherContextValidationTests"` + - 结果:通过 + - 备注:`3/3` 测试通过;本轮锁定默认 dispatcher 对非 `IArchitectureContext` 上下文的 request / notification / stream 失败语义,且未引入新增 warning +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarFallbackFailureTests"` + - 结果:通过 + - 备注:`3/3` 测试通过;本轮锁定 registrar 在 fallback 元数据失效时的 warning 语义,且保持 generated registry 主路径不回退 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false` + - 结果:通过 + - 备注:`63/63` 测试通过;当前沙箱限制了 MSBuild named pipe,验证需在提权环境下运行 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`14/14` 测试通过;本轮覆盖 pointer / function pointer 合同拒绝、fallback 诊断与现有精确注册路径 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Reports_Compilation_Error_And_Skips_Precise_Registration_For_Hidden_Pointer_Response|FullyQualifiedName~Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute|FullyQualifiedName~Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available"` + - 结果:通过 + - 备注:`3/3` 测试通过;本轮直接覆盖 PR #261 指向的 3 个 pointer / function pointer 回归场景 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` + - 结果:通过 + - 备注:`11/11` 测试通过;本轮覆盖 registrar 的 supported handler interface 缓存与 duplicate mapping 去重路径 +- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error` +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`17/17` 测试通过;本轮覆盖字符串 fallback 合同兼容路径与直接 `Type` fallback 元数据优先级 +- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error` +- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error` +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` + - 结果:通过 + - 备注:`13/13` 测试通过;本轮覆盖 mixed fallback metadata 的 registrar 消费路径 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`18/18` 测试通过;本轮覆盖 mixed fallback metadata 的双特性发射路径 +- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-pr-review.json` + - 结果:通过 + - 备注:确认当前分支对应 `PR #302`;latest head review 仍有 `3` 条 open AI threads,其中 MegaLinter 仅报告 `dotnet-format` restore failure 噪音 +- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error` +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`18/18` 测试通过;本轮直接覆盖 fallback preamble 排版与特性个数断言收紧 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` + - 结果:通过 + - 备注:`13/13` 测试通过;本轮确认 mixed fallback metadata 的 registrar 消费路径未回归 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`21/21` 测试通过;本轮新增多维数组、交错数组与外部程序集隐藏元素类型的 precise runtime type lookup 回归 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`22/22` 测试通过;本轮新增“外部程序集隐藏泛型定义 + 可见类型实参”的 precise registration 回归,确认仍走定向运行时类型重建 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` + - 结果:通过 + - 备注:`4/4` 测试通过;本轮覆盖 request pipeline executor 的首次创建、复用与双行为顺序回归 +- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;本轮确认 dispatcher request pipeline 形状缓存未破坏 `net8.0` / `net9.0` / `net10.0` 目标构建 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~RegisterHandlers_Should_Cache_Assembly_Metadata_Across_Containers|FullyQualifiedName~RegisterHandlers_Should_Cache_Loadable_Types_Across_Containers|FullyQualifiedName~Dispatcher_Should_Cache_Request_Pipeline_Executors_Per_Behavior_Count"` + - 结果:通过 + - 备注:`3/3` 测试通过;本轮确认无捕获缓存工厂没有破坏 registrar / dispatcher 现有缓存行为 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`22/22` 测试通过;本轮新增外部程序集隐藏泛型定义的 precise registration 回归 +- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;本轮确认删除 pointer runtime-reconstruction 残留后生成器项目仍可正常构建 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`22/22` 测试通过;本轮确认 pointer / function pointer 拒绝语义保持不变,且未回归既有 precise runtime type lookup 场景 diff --git a/ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-rp046-through-rp061.md b/ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-rp046-through-rp061.md new file mode 100644 index 00000000..c7bec3ca --- /dev/null +++ b/ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-rp046-through-rp061.md @@ -0,0 +1,35 @@ +# CQRS 重写迁移追踪归档(RP-046 至 RP-061) + +## 说明 + +- 本文件承接从 active trace 中迁出的已完成阶段细节。 +- `boot` 默认恢复入口应回到 `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md`,不要从本归档直接挑选旧阶段作为当前恢复点。 + +## 覆盖范围 + +- `CQRS-REWRITE-RP-046` 至 `CQRS-REWRITE-RP-061` +- 对应 active trace 清理前的 `2026-04-20`、`2026-04-29` 历史阶段记录 + +## 归档摘要 + +- `RP-046`:generated registry 激活反射收敛,补齐私有无参构造兼容回归 +- `RP-047`:pointer precise runtime type 方案探索,后续已被 `RP-050` 明确覆盖并废弃 +- `RP-048`:registrar handler-interface 反射缓存 +- `RP-049`:registrar duplicate mapping 索引收敛 +- `RP-050`:pointer / function pointer 泛型合同拒绝 +- `RP-051`:direct fallback 元数据优先级收敛 +- `RP-052`:mixed fallback 元数据拆分 +- `RP-053`:precise runtime type lookup 数组回归补强 +- `RP-054`:低风险并行批次收口 +- `RP-055`:缓存工厂闭包收敛 +- `RP-056`:pointer runtime-reconstruction 残留清理 +- `RP-057`:cached executor 上下文刷新回归 +- `RP-058`:delegated fallback attribute 合同测试 +- `RP-059`:notification / stream binding 上下文刷新回归 +- `RP-060`:dispatcher 上下文前置条件失败语义回归 +- `RP-061`:registrar fallback 失败分支回归 + +## 备注 + +- 若后续需要恢复这些阶段的详细上下文,应以对应提交、测试文件与本主题源码为准。 +- 当前 active trace 已不再保留这些阶段的逐段叙述,以保证 `boot` 能直接落到 `RP-062`。 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 1d539457..4703ad8c 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 @@ -138,101 +138,28 @@ CQRS 迁移与收敛。 - 当前 `dotnet build GFramework.sln -c Release` 在 WSL 环境仍会受顶层 `GFramework.csproj` 的 Windows NuGet fallback 配置影响 - 当前 `GFramework.Cqrs.Tests` 仍直接引用 `GFramework.Core`,说明测试已按模块意图拆分,但 runtime 物理迁移尚未完全切断依赖 -- `RegisterMediatorBehavior`、`MediatorCoroutineExtensions` 与 `ContextAwareMediator*Extensions` 仍作为兼容层存在,未来真正移除时仍需单独规划弃用窗口 ## 活跃文档 - 历史跟踪归档:[cqrs-rewrite-history-through-rp043.md](../archive/todos/cqrs-rewrite-history-through-rp043.md) +- 验证历史归档:[cqrs-rewrite-validation-history-through-rp062.md](../archive/todos/cqrs-rewrite-validation-history-through-rp062.md) - 历史 trace 归档:[cqrs-rewrite-history-through-rp043.md](../archive/traces/cqrs-rewrite-history-through-rp043.md) +- `RP-046` 至 `RP-061` trace 归档:[cqrs-rewrite-history-rp046-through-rp061.md](../archive/traces/cqrs-rewrite-history-rp046-through-rp061.md) ## 验证说明 - `RP-043` 之前的详细阶段记录、定向验证命令和阶段性决策均已移入主题内归档 -- active 跟踪文件只保留当前恢复点、当前活跃事实、风险和下一步,避免 `boot` 在默认入口中重复扫描 1000+ 行历史 trace -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` +- `RP-046` 至 `RP-062` 的历史验证命令与阶段性结果已移入验证归档,active tracking 只保留当前恢复入口需要的最新验证 +- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json` - 结果:通过 - - 备注:`5/5` 测试通过;本轮新增 cached executor 上下文刷新回归,确认 executor 复用时仍按当次分发重新注入上下文 -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsReflectionFallbackAttributeTests"` + - 备注:确认当前分支对应 `PR #304`,并定位到仍需本地复核的 CodeRabbit / Greptile open thread +- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` - 结果:通过 - - 备注:`5/5` 测试通过;本轮锁定 fallback attribute 的公开归一化合同与空参数防御语义 -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` + - 备注:`0 warning / 0 error`;本轮确认 XML 文档补齐、`NonParallelizable`、`_syncRoot` 命名与 `ai-plan` 收敛未引入新增编译问题 +- `bash scripts/validate-csharp-naming.sh` - 结果:通过 - - 备注:`7/7` 测试通过;本轮新增 cached notification / stream binding 上下文刷新回归,确认 binding 复用时仍按当次分发重新注入上下文 -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherContextValidationTests"` - - 结果:通过 - - 备注:`3/3` 测试通过;本轮锁定默认 dispatcher 对非 `IArchitectureContext` 上下文的 request / notification / stream 失败语义,且未引入新增 warning -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarFallbackFailureTests"` - - 结果:通过 - - 备注:`3/3` 测试通过;本轮锁定 registrar 在 fallback 元数据失效时的 warning 语义,且保持 generated registry 主路径不回退 -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false` - - 结果:通过 - - 备注:`63/63` 测试通过;当前沙箱限制了 MSBuild named pipe,验证需在提权环境下运行 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - 结果:通过 - - 备注:`14/14` 测试通过;本轮覆盖 pointer / function pointer 合同拒绝、fallback 诊断与现有精确注册路径 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Reports_Compilation_Error_And_Skips_Precise_Registration_For_Hidden_Pointer_Response|FullyQualifiedName~Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute|FullyQualifiedName~Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available"` - - 结果:通过 - - 备注:`3/3` 测试通过;本轮直接覆盖 PR #261 指向的 3 个 pointer / function pointer 回归场景 -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` - - 结果:通过 - - 备注:`11/11` 测试通过;本轮覆盖 registrar 的 supported handler interface 缓存与 duplicate mapping 去重路径 -- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error` -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - 结果:通过 - - 备注:`17/17` 测试通过;本轮覆盖字符串 fallback 合同兼容路径与直接 `Type` fallback 元数据优先级 -- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error` -- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error` -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` - - 结果:通过 - - 备注:`13/13` 测试通过;本轮覆盖 mixed fallback metadata 的 registrar 消费路径 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - 结果:通过 - - 备注:`18/18` 测试通过;本轮覆盖 mixed fallback metadata 的双特性发射路径 -- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-pr-review.json` - - 结果:通过 - - 备注:确认当前分支对应 `PR #302`;latest head review 仍有 `3` 条 open AI threads,其中 MegaLinter 仅报告 `dotnet-format` restore failure 噪音 -- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error` -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - 结果:通过 - - 备注:`18/18` 测试通过;本轮直接覆盖 fallback preamble 排版与特性个数断言收紧 -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` - - 结果:通过 - - 备注:`13/13` 测试通过;本轮确认 mixed fallback metadata 的 registrar 消费路径未回归 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - 结果:通过 - - 备注:`21/21` 测试通过;本轮新增多维数组、交错数组与外部程序集隐藏元素类型的 precise runtime type lookup 回归 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - 结果:通过 - - 备注:`22/22` 测试通过;本轮新增“外部程序集隐藏泛型定义 + 可见类型实参”的 precise registration 回归,确认仍走定向运行时类型重建 -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` - - 结果:通过 - - 备注:`4/4` 测试通过;本轮覆盖 request pipeline executor 的首次创建、复用与双行为顺序回归 -- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error`;本轮确认 dispatcher request pipeline 形状缓存未破坏 `net8.0` / `net9.0` / `net10.0` 目标构建 -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~RegisterHandlers_Should_Cache_Assembly_Metadata_Across_Containers|FullyQualifiedName~RegisterHandlers_Should_Cache_Loadable_Types_Across_Containers|FullyQualifiedName~Dispatcher_Should_Cache_Request_Pipeline_Executors_Per_Behavior_Count"` - - 结果:通过 - - 备注:`3/3` 测试通过;本轮确认无捕获缓存工厂没有破坏 registrar / dispatcher 现有缓存行为 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - 结果:通过 - - 备注:`22/22` 测试通过;本轮新增外部程序集隐藏泛型定义的 precise registration 回归 -- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error`;本轮确认删除 pointer runtime-reconstruction 残留后生成器项目仍可正常构建 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - 结果:通过 - - 备注:`22/22` 测试通过;本轮确认 pointer / function pointer 拒绝语义保持不变,且未回归既有 precise runtime type lookup 场景 + - 备注:使用显式 `GIT_DIR` / `GIT_WORK_TREE` 绑定重跑后,`1045` 个 tracked C# 文件的命名校验全部通过;本轮 `_syncRoot` 改名未引入命名规则回归 ## 下一步 -1. 继续 `Phase 8` 主线,优先再找一个收益明确且写集独立的 generator 或 registrar/dispatcher 热点;当前工作区若提交 registrar fallback 失败分支回归批次,相对 `origin/main` 的累计 diff 将达到 `32 files`,仍低于本轮 `gframework-batch-boot 50` 的主要 stop condition -2. 若继续文档主线,优先再扫教程入口页与 API 参考中的 CQRS 采用说明,确认是否还有旧 Command / Query 迁移口径残留 -3. 若后续再出现新的 PR review 或 review thread 变化,再重新执行 `$gframework-pr-review` 作为独立验证步骤 +1. push 当前 follow-up 提交后,重新执行 `$gframework-pr-review`,确认 `PR #304` 的 latest unresolved threads 是否已刷新为已解决,或仅剩新增有效项 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 25dc1c6a..821683e2 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 @@ -2,352 +2,36 @@ ## 2026-04-30 -### 阶段:PR #304 review follow-up 收敛(CQRS-REWRITE-RP-062) +### 阶段:PR #304 剩余 review follow-up 收敛(CQRS-REWRITE-RP-062) -- 本轮使用 `$gframework-pr-review` 重新抓取当前分支 PR: - - 当前分支 `feat/cqrs-optimization` 对应 `PR #304` - - latest review 信号主要由 `7` 条 CodeRabbit nitpick 与 `2` 条 Greptile open threads 组成 - - MegaLinter 仍只给出 `dotnet-format` 的 `Restore operation failed`,未附带当前仍成立的文件级格式问题;CTRF 汇总为 `2203/2203` passed -- 本地复核后接受并收敛的 review follow-up: - - `GFramework.Cqrs/Internal/CqrsDispatcher.cs` - - 为 `_pipelineExecutors` 与 `RequestPipelineInvocation.GetContinuation(...)` 补齐线程模型与失败模式说明 - - 将 request pipeline invoker 从“按 `behaviorCount` 重复创建”收敛为“binding 内创建一次、executor 缓存复用” - - `GFramework.Cqrs.Tests/Cqrs/*.cs` - - 将 `DispatcherPipelineContextRefreshState`、`DispatcherNotificationContextRefreshState`、`DispatcherStreamContextRefreshState` 与 `DispatcherPipelineOrderState` 切换为 `System.Threading.Lock` 保护的共享状态 - - 将 pipeline 顺序记录从公开可变 `List` 收敛为 `Record(...)` + 快照只读访问 - - 为 `CqrsDispatcherCacheTests` 添加 `[NonParallelizable]`,并补齐反射辅助方法的 XML `param` / `returns` - - 将 `CqrsRegistrationServiceTests` 的 debug 日志断言改为锁定语义片段而非整句文本 - - 将 `CqrsHandlerRegistrarFallbackFailureTests` 的缓存字段诊断改为显式指出 `CqrsHandlerRegistrar` 耦合点 - - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` - - 将 precise runtime type lookup 的数组 / 外部泛型回归断言从局部变量名绑定改为稳定的语义片段断言 -- 验证过程与结果: - - 首次把多个 `dotnet` restore / test 并发跑在同一 worktree 时,`GFramework.Cqrs.Tests` 出现 `*.nuget.g.props already exists` 竞争;该失败属于本地并发 restore 冲突,不代表代码问题 - - 串行重跑后确认: - - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests|FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsRegistrationServiceTests|FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarFallbackFailureTests"` - - 在第一次串行测试中暴露 `MA0158 Use System.Threading.Lock` warning 后,已同轮切换同步原语并准备重跑无 warning 验证 -- 结果: - - 本轮把仍成立的 PR review 评论全部收敛到本地代码或测试基础设施 - - 下一步应以“重跑无 warning 验证 + 提交本轮 follow-up”为恢复入口,而不是继续扩写新的 CQRS 优化切片 +- 本轮再次执行 `$gframework-pr-review`,确认当前分支 `feat/cqrs-optimization` 仍对应 `PR #304` +- 本地复核后继续收敛了上一轮遗留的 review 项: + - `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs` 已补 `NonParallelizable` + - `GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs` 已改用 `_syncRoot` 命名,并补齐缺失的 XML 文档标签 + - `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 三个内部 `Handle(...)` 已补齐 XML `param` / `returns` + - `DispatcherNotificationContextRefreshNotification` 与 `DispatcherStreamContextRefreshRequest` 已补 `DispatchId` XML 参数注释 + - `cqrs-rewrite` active tracking / trace 已压缩为当前恢复入口,并将已完成阶段的详细历史移入 archive +- 验证: + - `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` + - 结果:通过,`0 warning / 0 error` -## 2026-04-29 +## 活跃事实 -### 阶段:registrar fallback 失败分支回归(CQRS-REWRITE-RP-061) +- 当前主题仍处于 `Phase 8` +- `PR #304` 的本地 follow-up 已再次收口一轮,后续需要在 push 后重新观察 GitHub 的 unresolved thread 刷新结果 +- 已完成阶段的详细执行历史不再留在 active trace;默认恢复入口只保留当前恢复点、活跃事实、风险与下一步 -- 本轮继续按 `gframework-batch-boot 50` 的并行约束,把一个与主线程写集独立的新测试文件交给 worker: - - delegated scope:`GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs` - - delegated objective:锁定 registrar 在 fallback 元数据失效时的 warning 语义,而不扩张到 runtime 实现修改 -- 主线程接受结果前的复核结论: - - 该文件只复用现有 generated-registry 测试替身与捕获型日志工厂,不修改 `CqrsHandlerRegistrarTests.cs` 与生产代码 - - 三个用例分别覆盖 named fallback 无法解析、named fallback 解析抛异常、direct fallback 类型跨程序集三条失败分支 -- 主线程已复核并重新执行定向验证: - - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarFallbackFailureTests"` - - `3/3` passed -- 结果: - - 当前 registrar 仍保持“跳过无效 fallback 条目 + 记录 warning”的既有语义 - - 若连同当前工作区一起计算,当前分支相对 `origin/main` 的累计 diff 将达到 `32 files` +## 当前风险 -### 阶段:dispatcher 上下文前置条件失败语义回归(CQRS-REWRITE-RP-060) +- 当前 `dotnet build GFramework.sln -c Release` 在 WSL 环境仍会受顶层 `GFramework.csproj` 的 Windows NuGet fallback 配置影响 +- 远端 review thread 在本地提交前不会自动刷新,GitHub 上看到的 open 状态可能暂时滞后于当前代码 -- 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮选择一个新的单文件测试切片:锁定默认 dispatcher 对“仅实现 `ICqrsContext`、但未实现 `IArchitectureContext` 的上下文”会如何失败 -- 主线程先复核当前公开契约与实现后确认: - - `GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime` 的 XML 文档已经把这类失败语义写成公开契约 - - `CqrsDispatcher.PrepareHandler(...)` 当前正是唯一的上下文前置条件检查点,因此本轮最稳妥的切片仍是测试补强,而不是继续改 runtime -- 已完成的测试补强: - - 新增 `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` - - 通过 `CqrsRuntimeFactory.CreateRuntime(...)` + `Mock` 构造最小 runtime,分别锁定 request、notification、stream 三条路径的失败语义 - - 三个测试都只在需要上下文注入的 handler 已解析出来时触发,避免把“找不到 handler”与“上下文不满足注入前置条件”混淆成同一种异常 -- 定向验证已通过: - - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherContextValidationTests"` - - `3/3` passed -- 结果: - - 本轮只补测试,不改 `GFramework.Cqrs/Internal/CqrsDispatcher.cs` - - 若连同当前工作区一起计算,当前分支相对 `origin/main` 的累计 diff 将达到 `31 files` +## Archive Context -### 阶段:notification / stream binding 上下文刷新回归(CQRS-REWRITE-RP-059) - -- 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮继续沿着上一批 dispatcher cached executor 上下文回归往外扩一圈,但只覆盖 notification / stream 两条非 request 路径 -- 主线程先复核 `CqrsDispatcher` 当前实现后确认: - - `PublishAsync(...)` 与 `CreateStream(...)` 都会在命中缓存 binding 后重新解析 handler,并在调用前执行 `PrepareHandler(...)` - - 因此本轮最稳妥的切片仍是测试补强,而不是继续改 runtime -- 已完成的测试补强: - - 在 `GFramework.Cqrs.Tests/Cqrs/` 新增 `DispatcherNotificationContextRefresh*` 与 `DispatcherStreamContextRefresh*` 测试替身,记录重复分发时 handler 实例身份与 `ArchitectureContext` - - `CqrsDispatcherCacheTests` 新增 `Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Notification_Dispatch_Binding` - - `CqrsDispatcherCacheTests` 新增 `Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Stream_Dispatch_Binding` -- 定向验证已通过: - - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` - - `7/7` passed -- 结果: - - 本轮未暴露新的 runtime 实现缺口,因此没有改动 `GFramework.Cqrs/Internal/CqrsDispatcher.cs` - - 若连同当前工作区一起计算,当前分支相对 `origin/main` 的累计 diff 将达到 `29 files`,继续低于 `gframework-batch-boot 50` 的主要 stop condition - -### 阶段:delegated fallback attribute 合同测试(CQRS-REWRITE-RP-058) - -- 本轮按 `gframework-batch-boot 50` 的并行约束,把一个与主线程写集完全独立的叶子级测试文件交给 worker: - - delegated scope:`GFramework.Cqrs.Tests/Cqrs/CqrsReflectionFallbackAttributeTests.cs` - - delegated objective:锁定 `CqrsReflectionFallbackAttribute` 的公开归一化合同,而不扩张到 registrar / generator / dispatcher 实现 -- 已接受的 worker 结果: - - 新增 `CqrsReflectionFallbackAttributeTests`,覆盖空 marker、字符串 fallback 名称的去空/去重/排序、直接 `Type` fallback 的去空/去重/排序,以及两个重载对空参数数组的防御行为 - - worker 已独立验证 `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsReflectionFallbackAttributeTests"`,结果为 `5/5` passed - - 该叶子级测试批次已作为独立提交落地:`86a24e00` `test(cqrs): 新增 ReflectionFallbackAttribute 合同测试` - -### 阶段:cached executor 上下文刷新回归(CQRS-REWRITE-RP-057) - -- 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮只处理一个窄写集测试批次:为 cached request pipeline executor 增加“重复分发仍重新注入上下文”的回归 -- 先复核上一轮 request pipeline executor 形状缓存实现与测试边界后确认: - - 当前 runtime 只允许本轮写集落在 `GFramework.Cqrs.Tests/Cqrs/`,除非测试直接打出 `CqrsDispatcher` 的真实缺陷 - - 目标是锁定 executor 缓存不会跨分发保留旧 `ArchitectureContext`,且不扩张到 notification / stream 路径 -- 已完成的测试补强: - - 在 `GFramework.Cqrs.Tests/Cqrs/` 新增 `DispatcherPipelineContextRefreshRequest`、`DispatcherPipelineContextRefreshBehavior`、`DispatcherPipelineContextRefreshRequestHandler`、`DispatcherPipelineContextRefreshState` 与 `DispatcherPipelineContextSnapshot` - - `DispatcherPipelineContextRefreshBehavior` 与 `DispatcherPipelineContextRefreshRequestHandler` 都基于 `CqrsContextAwareHandlerBase` 记录当次看到的 `ArchitectureContext` - - `CqrsDispatcherCacheTests` 新增 `Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Request_Pipeline_Executor`,断言同一个 cached executor 在两次分发间保持 executor 形状复用,但 handler 不会被 executor 黏住,且 handler / behavior 都会观察到本次分发的新上下文 -- 调试过程中的结论: - - 初版断言曾要求 behavior 实例编号跨分发变化,随后确认这是错误假设 - - `MicrosoftDiContainer.RegisterCqrsPipelineBehavior()` 对已闭合的 pipeline behavior 使用的是 `AddSingleton(...)` - - 因此本轮最终锁定的是“singleton behavior 也必须重新注入上下文”,而不是强行要求 behavior 生命周期为 transient -- 定向验证已通过: - - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` - - `5/5` passed -- 结果: - - 本轮未暴露新的 runtime 实现缺口,因此没有改动 `GFramework.Cqrs/Internal/CqrsDispatcher.cs` - - 当前分支相对 `origin/main` 的累计提交 diff 仍为 `14 files`,继续低于 `gframework-batch-boot 50` 的主要 stop condition - -### 阶段:pointer runtime-reconstruction 残留清理(CQRS-REWRITE-RP-056) - -- 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮只处理一个写集很窄的 generator 清理切片:删除 `CqrsHandlerRegistryGenerator` 里已经不可达的 pointer runtime-reconstruction 残留 -- 先复核当前实现后确认: - - `TryCreateRuntimeTypeReference` 已在入口直接拒绝 `IPointerTypeSymbol` 与 `IFunctionPointerTypeSymbol` - - `CanReferenceFromGeneratedRegistry` 也已统一把 pointer / function pointer 判定为不可直接引用 - - 但 `RuntimeTypeReferenceSpec`、`AppendRuntimeTypeReferenceResolution(...)` 和 `ContainsExternalAssemblyTypeLookup(...)` 仍残留 pointer 子结构与 `MakePointerType()` 分支,属于已失效的死代码 -- 已完成的清理: - - `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs` 已移除 `PointerElementTypeReference` - - `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs` 已移除 pointer 运行时重建分支与 `AppendPointerRuntimeTypeReferenceResolution(...)` - - `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs` 已移除 pointer 外部程序集查找递归 - - direct / named / mixed fallback 逻辑未改动,pointer / function pointer 拒绝语义保持不变 -- 定向验证已通过: - - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - `0 warning / 0 error` - - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - `22/22` passed - -### 阶段:缓存工厂闭包收敛(CQRS-REWRITE-RP-055) - -- 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮在不扩大语义面的前提下继续做一个更窄的 runtime 微切片:把弱缓存 / 并发缓存入口剩余的捕获型工厂收敛为 `static lambda + state` -- 先复核当前 runtime 热点后确认: - - `CqrsDispatcher` 的 notification / stream / request binding 与 pipeline executor 缓存仍存在少量可消除的捕获型工厂 - - `CqrsHandlerRegistrar` 的程序集元数据缓存与可加载类型缓存也仍通过捕获 `logger` 的 lambda 建值 - - 这些入口都只影响内部缓存建值,不触碰 handler / behavior 生命周期和 fallback 合同 -- 已完成的收敛: - - `CqrsDispatcher` 现为 notification / stream / request binding 命中路径改用无捕获工厂;pipeline executor 缓存改为显式状态对象承载 `requestType` - - `CqrsHandlerRegistrar` 现为 `AssemblyMetadataCache` 与 `LoadableTypesCache` 改用 `static` 工厂 + `logger` 显式状态参数 - - 该批次没有改动 `RequestPipelineInvocation` 的 `next` 语义,也没有缓存 handler / behavior 实例 -- 同轮继续补了一个独立 generator 覆盖缺口: - - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增“外部程序集隐藏泛型定义 + 可见类型实参”的 precise registration 回归 - - 该回归锁定生成器会输出 `ResolveReferencedAssemblyType("...ProtectedEnvelope\`1")` 与 `MakeGenericType(typeof(string))` 的组合,而不是退回程序集级字符串 fallback -- 定向验证已通过: - - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~RegisterHandlers_Should_Cache_Assembly_Metadata_Across_Containers|FullyQualifiedName~RegisterHandlers_Should_Cache_Loadable_Types_Across_Containers|FullyQualifiedName~Dispatcher_Should_Cache_Request_Pipeline_Executors_Per_Behavior_Count"` - - `3/3` passed - - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - `22/22` passed - - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - - `0 warning / 0 error` - -### 阶段:低风险并行批次收口(CQRS-REWRITE-RP-054) - -- 继续按 `gframework-batch-boot 50` 推进 `Phase 8`,本轮先完成批次评估后再并行拆分写集,避免把 generator、runtime 与 docs 改动揉进同一片上下文 -- 先复核当前 worktree、active tracking 与 `origin/main` 基线后确认: - - 当前分支头最初与 `origin/main` 对齐,批次阈值从 `0 files / 0 lines` 起算 - - 本轮可以安全拆成三个互不冲突的切片:request pipeline executor 形状缓存、precise runtime type lookup 数组回归补强、CQRS 入口文档对齐 - - 主线程保留集成与验证职责,subagent 只负责各自写集 -- 本轮继续收口一个更窄的 generator 覆盖缺口: - - 在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增“外部程序集隐藏泛型定义 + 可见类型实参”的 precise registration 回归 - - 该回归锁定生成器会输出 `ResolveReferencedAssemblyType("...ProtectedEnvelope\`1")` 与 `MakeGenericType(typeof(string))` 的组合,而不是退回程序集级字符串 fallback - - 定向测试 `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` 通过,结果为 `22/22` passed,因此本轮未触发 `RuntimeTypeReferences` / `SourceEmission` 的实现修正 -- 已接受并整合的并行写集: - - docs 切片:更新 `GFramework.Cqrs/README.md`、`docs/zh-CN/core/cqrs.md`、`docs/zh-CN/api-reference/index.md`,明确 generated registry 优先、targeted fallback 只补剩余 handler - - generator 切片:在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增多维数组、交错数组、外部程序集隐藏元素类型三组 precise lookup 回归 - - dispatcher 切片:在 `GFramework.Cqrs/Internal/CqrsDispatcher.cs` 中将 request pipeline 从“每次分发重建 next 链”收敛为“binding 内按 behaviorCount 缓存 executor 形状”,并补充 dispatcher cache / 顺序回归 -- docs 切片已作为独立提交落地: - - `66830ba2` `docs(cqrs): 更新入口与回退语义说明` -- 本轮定向验证已通过: - - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - - `0 warning / 0 error` - - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` - - `4/4` passed - - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - `21/21` passed -- 本轮停止时,当前工作区相对 `origin/main` 的累计 diff 为 `13 files / 709 lines` -- 结论: - - primary stop condition `50 files` 尚未触发,本轮停止是因为三条低风险切片已收口完毕 - - 下一批更适合重新做一轮热点筛选,而不是在同一轮继续扩写集 - -### 阶段:precise runtime type lookup 数组回归补强(CQRS-REWRITE-RP-053) - -- 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮选择一个更窄的 generator 覆盖缺口:锁定 precise runtime type lookup 下数组类型形态的回归 -- 先复核当前实现后确认: - - `TryCreateRuntimeTypeReference` 已会把 `IArrayTypeSymbol` 递归建模为 `RuntimeTypeReferenceSpec.FromArray(element, rank)` - - `AppendArrayRuntimeTypeReferenceResolution` 已按 `ArrayRank == 1` 发射 `MakeArrayType()`,按 `rank > 1` 发射 `MakeArrayType(rank)` - - 当前缺口主要是测试面不足,尚未显式覆盖多维数组、交错数组、外部程序集隐藏元素类型这三类 precise lookup 场景 -- 已在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 补充三组回归: - - 隐藏元素类型的多维数组响应,锁定 `MakeArrayType(2)` 发射 - - 隐藏元素类型的交错数组响应,锁定递归 `MakeArrayType().MakeArrayType()` 发射 - - 外部程序集隐藏元素类型的多维数组响应,锁定 `ResolveReferencedAssemblyType(...)` 与 `MakeArrayType(2)` 的组合 -- 本轮定向测试全部通过,未暴露数组发射缺陷: - - 因此没有修改 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs` - - 也没有改动 `CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs` - - fallback 合同选择逻辑与 direct / named / mixed fallback 排版路径保持不变 -- 定向验证已通过: - - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - `21/21` passed - -### 阶段:mixed fallback 元数据拆分(CQRS-REWRITE-RP-052) - -- 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮把上一批的“全部可直接引用 fallback handlers 走 `Type[]`”继续推进到 mixed 场景 -- 先复核现状后确认: - - `CqrsHandlerRegistrar` 已天然支持读取多个 `CqrsReflectionFallbackAttribute` 实例 - - 上一批真正阻止 mixed 场景继续收敛的点,是 runtime attribute 本身尚未开放多实例,以及 generator 只能二选一发射单个 fallback 特性 -- 已在 `GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs` 中将特性约束改为 `AllowMultiple = true`,并补充注释说明多个实例的用途 -- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中扩展 fallback 合同探测: - - 探测 runtime 是否支持 `params string[]` - - 探测 runtime 是否支持 `params Type[]` - - 探测 runtime 是否允许多个 `CqrsReflectionFallbackAttribute` 实例 -- 已在 `CqrsHandlerRegistryGenerator.Models.cs` 与 `CqrsHandlerRegistryGenerator.SourceEmission.cs` 中重构 fallback 发射模型: - - fallback 元数据现在可表示为一个或多个程序集级特性实例 - - 当 fallback handlers 全部可直接引用时,继续优先输出单个 `Type[]` 特性 - - 当 fallback 同时包含可直接引用与仅能按名称恢复的 handlers,且 runtime 支持多实例时,拆分输出一条 `Type[]` 特性和一条字符串特性 - - 若 runtime 不支持多实例或缺少相应构造函数,仍整体回退到字符串元数据,避免 mixed 场景漏注册 -- 已补充 runtime 与 generator 双侧回归: - - `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs` 新增 mixed fallback metadata 用例,锁定 registrar 只对字符串条目调用一次 `Assembly.GetType(...)` - - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增 mixed fallback emission 用例,锁定 generator 会输出两个程序集级 fallback 特性实例 -- 同步更新: - - `GFramework.Cqrs.SourceGenerators/README.md` - - `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md` - - 说明 mixed 场景现在会拆分 `Type` 元数据与字符串元数据 -- 定向验证已通过: - - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` - - `13/13` passed - - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - `18/18` passed -- 随后按 `$gframework-pr-review` 重新拉取当前分支 PR 审查数据: - - 当前 worktree `feat/cqrs-optimization` 已对应 `PR #302` - - latest head commit 仍有 `3` 条 open AI review threads:Greptile 指向 generator preamble 的死参数与多实例 fallback 特性空行,CodeRabbit 指向 mixed/direct fallback 测试断言过宽 - - MegaLinter 仍只暴露 `dotnet-format` 的 `Restore operation failed`,未给出本地仍成立的格式文件线索,因此按环境噪音处理 -- 本轮已继续收口 `RP-052` 的 follow-up: - - 在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs` 中移除已不再参与判断的 `generationEnvironment` 透传参数 - - 调整多实例 fallback 特性发射时的换行策略,避免最后一个 fallback 特性与 `CqrsHandlerRegistryAttribute` 之间保留多余空行 - - 在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 中补强 direct/mixed fallback 发射断言,锁定特性实例个数、拒绝空 marker,并确保 mixed 场景的程序集级 preamble 排版稳定 - - 在 `GFramework.Cqrs.Tests/Cqrs/ReflectionFallbackNotificationContainer.cs` 中为 `DirectFallbackHandlerType` 补齐 `` XML 文档 -- 本轮 review follow-up 验证已通过: - - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - `0 warning / 0 error` - - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - `18/18` passed - - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` - - `13/13` passed - -## 2026-04-20 - -### 阶段:direct fallback 元数据优先级收敛(CQRS-REWRITE-RP-051) - -- 重新按 `gframework-batch-boot 50` 恢复 `Phase 8` 后,先复核当前 worktree 的恢复入口、`origin/main` 基线与分支规模: - - worktree 仍映射到 `cqrs-rewrite` - - 基线按批处理约定固定为 `origin/main` - - 本轮开始前分支累计 diff 为 `0 files / 0 lines` -- 结合当前代码热点与历史归档后,选择本轮批次目标为“继续收敛 generator fallback 元数据,进一步减少 runtime 按字符串类型名回查 handler 的场景” -- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中新增 runtime fallback 合同探测: - - 识别 `CqrsReflectionFallbackAttribute` 是否支持 `params string[]` - - 识别 `CqrsReflectionFallbackAttribute` 是否支持 `params Type[]` -- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs` 与 - `CqrsHandlerRegistryGenerator.SourceEmission.cs` 中收敛 fallback 发射策略: - - 当本轮所有 fallback handlers 都可被生成代码直接引用,且 runtime 支持 `params Type[]` 时,生成器现优先发射 `typeof(...)` 形式的程序集级 fallback 元数据 - - 当 fallback handlers 中仍存在不能直接引用的实现类型时,生成器继续整体回退到字符串元数据,避免 mixed 场景下部分 handler 走 `Type[]`、其余 handler 丢失恢复入口 -- 已在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 补充回归: - - 锁定 runtime 同时暴露字符串与 `Type` 两类 fallback 构造函数时,生成器优先选择直接 `Type` 元数据 - - 保留现有字符串 fallback 合同测试,确保旧 contract 兼容路径不回退 -- 同步更新: - - `GFramework.Cqrs.SourceGenerators/README.md` - - `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md` - - 说明“可直接引用的 fallback handlers 会优先走 `typeof(...)` 元数据,减少运行时字符串回查” -- 定向验证已通过: - - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - `17/17` passed -- 额外修正: - - active tracking 中原先引用的 `ai-plan/migration/CQRS_MODULE_SPLIT_PLAN.md` 在当前 worktree 已不存在;本轮已移除该失效路径,后续以 active tracking / trace 作为默认恢复入口 - -### 阶段:pointer / function pointer 泛型合同拒绝(CQRS-REWRITE-RP-050) - -- 重新执行 `$gframework-pr-review` 后,确认当前分支对应 `PR #261`,状态仍为 `OPEN` -- latest reviewed commit 当前剩余 `1` 条 open CodeRabbit thread,指向 `RP-047` 历史记录仍把 `MakePointerType()` precise registration 写成现行路径 -- 本地核对后确认该评论有效:当前 pointer / function pointer 语义已由 `RP-050` 收敛为 fallback / diagnostic 路径,历史追踪必须显式标注 `RP-047` 已废弃,避免后续恢复时误回滚到旧方案 -- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中收紧 `TryCreateRuntimeTypeReference` 与 `CanReferenceFromGeneratedRegistry` -- pointer / function pointer 现统一视为不可精确生成的 CQRS 泛型合同,生成器会保守回退到既有 fallback / diagnostic 路径,而不再发射运行时 `MakeGenericType(...)` 风险代码 -- 已在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 中补充输入源诊断分离,并将相关测试改为显式断言 `CS0306` 与 fallback / diagnostic 结果 -- 已同步修正 `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md` 中 `RP-047` 段落,明确其已被 `RP-050` 覆盖,且不得恢复 `MakePointerType()` precise registration -- 定向验证已通过: - - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Reports_Compilation_Error_And_Skips_Precise_Registration_For_Hidden_Pointer_Response|FullyQualifiedName~Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute|FullyQualifiedName~Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available"` - - `3/3` passed -- 扩展验证已通过: - - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - `14/14` passed - -### 阶段:registrar duplicate mapping 索引收敛(CQRS-REWRITE-RP-049) - -- 已将 `CqrsHandlerRegistrar` 的重复 handler mapping 判定从逐条线性扫描 `IServiceCollection` 收敛为单次构建的本地映射索引 -- reflection fallback 或重复类型输入场景下,后续 duplicate mapping 判定改为 `HashSet` 命中,不再重复遍历已有服务描述符 -- `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs` 已补充“程序集枚举返回重复 handler 类型时仍只注册一份映射”的回归 -- 定向验证已通过: - - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` - - `11/11` passed - - 当前沙箱限制 MSBuild named pipe,因此验证在提权环境下执行 - -### 阶段:registrar handler-interface 反射缓存(CQRS-REWRITE-RP-048) - -- 已在 `CqrsHandlerRegistrar` 中新增按 `Type` 弱键缓存的 supported handler interface 元数据,reflection 注册路径现会复用已筛选且排序好的接口列表 -- 同一 handler 类型跨容器重复注册时,不再重复执行 `GetInterfaces()` 与支持接口筛选;缓存仍保持卸载安全,不会长期钉住 collectible 类型 -- `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs` 已补充 registrar 静态缓存清理与 supported interface 缓存复用回归 -- 定向验证已通过: - - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` - - `10/10` passed - - 当前沙箱限制 MSBuild named pipe,因此验证在提权环境下执行 - -### 阶段:pointer precise runtime type 覆盖扩展(CQRS-REWRITE-RP-047,已由 RP-050 覆盖) - -- 曾在 `CqrsHandlerRegistryGenerator` 中尝试补充 pointer 类型的 runtime type 递归建模与源码发射,计划通过 `MakePointerType()` 还原隐藏 pointer 响应类型 -- 该方案后续已被 `RP-050` 明确废弃:pointer / function pointer 不能作为 CQRS 泛型合同的 precise registration 输入,当前实现统一回到 fallback / diagnostic 路径,不能恢复到 `MakePointerType()` 精确注册 -- 已同步收紧 function pointer 签名的可直接生成判定,只有当签名中的返回值与参数类型均可从 generated registry 安全引用时才走静态注册 -- 已保留含隐藏类型 function pointer handler 的 fallback / 诊断回归覆盖,确保 pointer 支持扩展不会误删原有程序集级 fallback 契约边界 -- 后续若需恢复当前 pointer / function pointer 行为,应以 `RP-050` 为权威记录,而不是继续沿用本阶段的旧设计假设 -- 定向验证与 `CqrsHandlerRegistryGeneratorTests` 全组验证均已通过: - - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Generates_Precise_Service_Type_For_Hidden_Pointer_Response|FullyQualifiedName~Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute|FullyQualifiedName~Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available"` - - `3/3` passed - - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - `14/14` passed - - 当前沙箱限制 MSBuild named pipe,因此验证在提权环境下执行 - -### 阶段:generated registry 激活反射收敛(CQRS-REWRITE-RP-046) - -- 已在 `CqrsHandlerRegistrar` 中将 generated registry 的无参构造激活改为类型级缓存工厂 -- 默认路径优先使用一次性动态方法直接创建 registry,避免后续每次命中缓存仍走 `ConstructorInfo.Invoke` -- 若运行环境不允许动态方法,则保留原有反射激活回退,确保 generated registry 路径不因运行时限制失效 -- 已补充“私有无参构造 generated registry 仍可激活”的回归测试,覆盖现有生成器产物兼容性 -- 定向验证已通过: - - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false` - - `63/63` passed - - 当前沙箱限制 MSBuild named pipe,因此验证在提权环境下执行 - -### Archive Context - -- 历史跟踪归档: - - `ai-plan/public/cqrs-rewrite/archive/todos/cqrs-rewrite-history-through-rp043.md` - 历史 trace 归档: - `ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-through-rp043.md` + - `ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-rp046-through-rp061.md` -### 当前下一步 +## 当前下一步 -1. 回到 `Phase 8` 主线,优先再找一个写集独立的 generator 或 runtime 热点;pointer runtime-reconstruction 残留已清空,后续不要恢复任何 `MakePointerType()` 发射路径 -2. 若继续文档主线,优先补齐 `docs/zh-CN/api-reference` 与教程入口页中仍过时的 CQRS API / 命名空间表述 -3. 若后续 review thread 或 PR 状态再次变化,再重新执行 `$gframework-pr-review` 复核远端信号 +1. push 当前 follow-up 提交后,重新执行 `$gframework-pr-review`,确认 `PR #304` 的 latest unresolved threads 是否已刷新为已解决,或仅剩新增有效项