test(cqrs): 补强数组类型生成回归

- 新增多维数组、交错数组与外部程序集隐藏元素类型的 precise runtime type lookup 回归

- 更新 cqrs-rewrite 跟踪与追踪,记录 RP-053 到 RP-054 的并行批次收口与验证结果
This commit is contained in:
gewuyou 2026-04-29 16:56:05 +08:00 committed by GeWuYou
parent e81a43680d
commit 7b5efde3bd
3 changed files with 288 additions and 3 deletions

View File

@ -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<TResponse> { }
public interface INotification { }
public interface IStreamRequest<TResponse> { }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
}
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<HiddenResponse[,]>;
private sealed class HiddenHandler : IRequestHandler<HiddenRequest, HiddenResponse[,]> { }
}
}
""";
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<TResponse> { }
public interface INotification { }
public interface IStreamRequest<TResponse> { }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
}
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<HiddenResponse[][]>;
private sealed class HiddenHandler : IRequestHandler<HiddenRequest, HiddenResponse[][]> { }
}
}
""";
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<ProtectedResponse[,]>;
}
public abstract class HandlerBase :
IRequestHandler<VisibilityScope.ProtectedRequest, VisibilityScope.ProtectedResponse[,]>
{
}
""";
private const string LegacyFallbackMarkerHiddenHandlerSource = """
using System;
@ -1590,6 +1730,52 @@ public class CqrsHandlerRegistryGeneratorTests
("CqrsHandlerRegistry.g.cs", HiddenGenericEnvelopeResponseExpected));
}
/// <summary>
/// 验证精确重建路径会保留隐藏元素类型的多维数组秩信息,
/// 使生成注册器继续走定向运行时类型重建,而不是退回宽松接口发现。
/// </summary>
[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("));
});
}
/// <summary>
/// 验证精确重建路径会递归覆盖交错数组,
/// 确保隐藏元素类型的每一层数组都继续通过数组发射分支稳定重建。
/// </summary>
[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("));
});
}
/// <summary>
/// 验证当 handler 合同把 pointer 响应类型放进 CQRS 泛型参数时,
/// 生成器会保守回退而不是继续发射不可构造的精确注册代码。
@ -1682,6 +1868,39 @@ public class CqrsHandlerRegistryGeneratorTests
Is.EqualTo(ExternalAssemblyPreciseLookupExpected));
}
/// <summary>
/// 验证当外部程序集隐藏元素类型以多维数组形式参与 CQRS 合同时,
/// 生成器仍会保留外部程序集定向查找与数组秩信息,而不是退回 fallback 元数据。
/// </summary>
[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("));
});
}
/// <summary>
/// 验证即使 runtime 仍暴露旧版无参 fallback marker生成器也会优先在生成注册器内部处理隐藏 handler
/// 不再输出 fallback marker。

View File

@ -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` 作为独立验证步骤

View File

@ -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 场景