test(cqrs): 补齐外部隐藏泛型精确注册回归

- 新增外部程序集隐藏泛型定义与可见类型实参的 precise registration 回归

- 更新 CQRS 重写跟踪与 trace,记录本轮覆盖范围和验证结果
This commit is contained in:
gewuyou 2026-04-29 17:33:27 +08:00 committed by GeWuYou
parent 7b5efde3bd
commit e51b64f8d5
5 changed files with 86 additions and 7 deletions

View File

@ -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<TResponse>);
static (cachedRequestType, cachedResponseType) =>
CreateRequestDispatchBindingBox<TResponse>(cachedRequestType, cachedResponseType));
return bindingBox.Get<TResponse>();
}
@ -438,9 +439,10 @@ internal sealed class CqrsDispatcher(
public RequestPipelineExecutor<TResponse> GetPipelineExecutor(int behaviorCount)
{
ArgumentOutOfRangeException.ThrowIfNegative(behaviorCount);
return _pipelineExecutors.GetOrAdd(
return _pipelineExecutors.GetOrAdd<RequestPipelineExecutorFactoryState<TResponse>>(
behaviorCount,
count => CreateRequestPipelineExecutor<TResponse>(requestType, count));
static (count, state) => CreateRequestPipelineExecutor<TResponse>(state.RequestType, count),
new RequestPipelineExecutorFactoryState<TResponse>(requestType));
}
/// <summary>
@ -504,6 +506,12 @@ internal sealed class CqrsDispatcher(
}
}
/// <summary>
/// 为 pipeline executor 缓存携带当前请求类型,避免按行为数量建缓存时创建闭包。
/// </summary>
/// <typeparam name="TResponse">请求响应类型。</typeparam>
private readonly record struct RequestPipelineExecutorFactoryState<TResponse>(Type RequestType);
/// <summary>
/// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。
/// 该对象只存在于本次分发,不会跨请求保留容器解析出的实例。

View File

@ -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));
}
/// <summary>

View File

@ -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<T>
{
}
protected internal sealed record ProtectedRequest() : IRequest<ProtectedEnvelope<string>>;
}
public abstract class HandlerBase :
IRequestHandler<VisibilityScope.ProtectedRequest, VisibilityScope.ProtectedEnvelope<string>>
{
}
""";
private const string LegacyFallbackMarkerHiddenHandlerSource = """
using System;
@ -1901,6 +1921,44 @@ public class CqrsHandlerRegistryGeneratorTests
});
}
/// <summary>
/// 验证当外部程序集隐藏泛型定义以“隐藏定义 + 可见类型实参”的形式参与 CQRS 合同时,
/// 生成器会继续输出定向程序集查找与运行时泛型重建,而不是退回字符串 fallback 元数据。
/// </summary>
[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("));
});
}
/// <summary>
/// 验证即使 runtime 仍暴露旧版无参 fallback marker生成器也会优先在生成注册器内部处理隐藏 handler
/// 不再输出 fallback marker。

View File

@ -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 的首次创建、复用与双行为顺序回归

View File

@ -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 回归