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] =?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 场景