refactor(cqrs): 缓存处理器接口反射元数据

- 优化 CqrsHandlerRegistrar 复用 supported handler interface 缓存
- 补充 registrar 静态缓存隔离与接口缓存复用回归测试
- 更新 cqrs-rewrite 跟踪与 trace 到 RP-048
This commit is contained in:
gewuyou 2026-04-20 10:34:24 +08:00 committed by GeWuYou
parent 5f3964d4c0
commit 110666d06b
4 changed files with 163 additions and 7 deletions

View File

@ -32,6 +32,7 @@ internal sealed class CqrsHandlerRegistrarTests
_container.Freeze();
_context = new ArchitectureContext(_container);
ClearRegistrarCaches();
}
/// <summary>
@ -43,6 +44,7 @@ internal sealed class CqrsHandlerRegistrarTests
_context = null;
_container = null;
DeterministicNotificationHandlerState.Reset();
ClearRegistrarCaches();
}
/// <summary>
@ -435,6 +437,123 @@ internal sealed class CqrsHandlerRegistrarTests
partiallyLoadableAssembly.Verify(static assembly => assembly.GetTypes(), Times.Once);
}
/// <summary>
/// 验证同一 handler 类型跨容器重复注册时,会复用已筛选的 supported handler interface 列表,
/// 而不是为每个容器重新执行接口反射分析。
/// </summary>
[Test]
public void RegisterHandlers_Should_Cache_Supported_Handler_Interfaces_Across_Containers()
{
var supportedHandlerInterfacesCache = GetRegistrarCacheField("SupportedHandlerInterfacesCache");
var firstHandlerType = typeof(AlphaDeterministicNotificationHandler);
var secondHandlerType = typeof(ZetaDeterministicNotificationHandler);
var handlerAssembly = new Mock<Assembly>();
handlerAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Cqrs.CachedHandlerInterfacesAssembly, Version=1.0.0.0");
handlerAssembly
.Setup(static assembly => assembly.GetTypes())
.Returns([firstHandlerType, secondHandlerType]);
Assert.Multiple(() =>
{
Assert.That(GetSingleKeyCacheValue(supportedHandlerInterfacesCache, firstHandlerType), Is.Null);
Assert.That(GetSingleKeyCacheValue(supportedHandlerInterfacesCache, secondHandlerType), Is.Null);
});
var firstContainer = new MicrosoftDiContainer();
var secondContainer = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(firstContainer, handlerAssembly.Object);
var firstHandlerInterfaces =
GetSingleKeyCacheValue(supportedHandlerInterfacesCache, firstHandlerType);
var secondHandlerInterfaces =
GetSingleKeyCacheValue(supportedHandlerInterfacesCache, secondHandlerType);
CqrsTestRuntime.RegisterHandlers(secondContainer, handlerAssembly.Object);
Assert.Multiple(() =>
{
Assert.That(firstHandlerInterfaces, Is.Not.Null);
Assert.That(secondHandlerInterfaces, Is.Not.Null);
Assert.That(
GetSingleKeyCacheValue(supportedHandlerInterfacesCache, firstHandlerType),
Is.SameAs(firstHandlerInterfaces));
Assert.That(
GetSingleKeyCacheValue(supportedHandlerInterfacesCache, secondHandlerType),
Is.SameAs(secondHandlerInterfaces));
});
handlerAssembly.Verify(static assembly => assembly.GetTypes(), Times.Once);
}
/// <summary>
/// 清空本测试依赖的 registrar 静态缓存,避免跨用例共享进程级状态导致断言漂移。
/// </summary>
private static void ClearRegistrarCaches()
{
ClearCache(GetRegistrarCacheField("AssemblyMetadataCache"));
ClearCache(GetRegistrarCacheField("RegistryActivationMetadataCache"));
ClearCache(GetRegistrarCacheField("LoadableTypesCache"));
ClearCache(GetRegistrarCacheField("SupportedHandlerInterfacesCache"));
}
/// <summary>
/// 通过反射读取 registrar 的静态缓存对象。
/// </summary>
private static object GetRegistrarCacheField(string fieldName)
{
var registrarType = GetRegistrarType();
var field = registrarType.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.");
}
/// <summary>
/// 清空指定缓存对象。
/// </summary>
private static void ClearCache(object cache)
{
_ = InvokeInstanceMethod(cache, "Clear");
}
/// <summary>
/// 读取单键缓存中当前保存的对象。
/// </summary>
private static object? GetSingleKeyCacheValue(object cache, Type key)
{
return InvokeInstanceMethod(cache, "GetValueOrDefaultForTesting", key);
}
/// <summary>
/// 调用缓存对象上的实例方法。
/// </summary>
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);
}
/// <summary>
/// 获取 CQRS handler registrar 运行时类型。
/// </summary>
private static Type GetRegistrarType()
{
return typeof(CqrsReflectionFallbackAttribute).Assembly
.GetType("GFramework.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)!;
}
}
/// <summary>

View File

@ -26,6 +26,11 @@ internal static class CqrsHandlerRegistrar
private static readonly WeakKeyCache<Assembly, IReadOnlyList<Type>> LoadableTypesCache =
new();
// 卸载安全的进程级缓存:同一 handler 类型跨容器重复注册时,
// 复用已筛选且排序好的 supported handler interface 列表,避免重复执行 GetInterfaces()。
private static readonly WeakKeyCache<Type, IReadOnlyList<Type>> SupportedHandlerInterfacesCache =
new();
/// <summary>
/// 扫描指定程序集并注册所有 CQRS 请求/通知/流式处理器。
/// </summary>
@ -163,11 +168,7 @@ internal static class CqrsHandlerRegistrar
foreach (var implementationType in GetCandidateHandlerTypes(assembly, logger, reflectionFallbackMetadata)
.Where(IsConcreteHandlerType))
{
var handlerInterfaces = implementationType
.GetInterfaces()
.Where(IsSupportedHandlerInterface)
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
.ToList();
var handlerInterfaces = GetSupportedHandlerInterfaces(implementationType);
if (handlerInterfaces.Count == 0)
continue;
@ -184,12 +185,30 @@ internal static class CqrsHandlerRegistrar
// Request/notification handlers receive context injection before every dispatch.
// Transient registration avoids sharing mutable Context across concurrent requests.
services.AddTransient(handlerInterface, implementationType);
logger.Debug(
logger.Debug(
$"Registered CQRS handler {implementationType.FullName} as {handlerInterface.FullName}.");
}
}
}
/// <summary>
/// 获取指定实现类型上所有受支持的 CQRS handler 接口,并缓存筛选与排序结果。
/// </summary>
/// <param name="implementationType">要分析的处理器实现类型。</param>
/// <returns>当前实现类型声明的受支持 handler 接口列表。</returns>
private static IReadOnlyList<Type> GetSupportedHandlerInterfaces(Type implementationType)
{
ArgumentNullException.ThrowIfNull(implementationType);
return SupportedHandlerInterfacesCache.GetOrAdd(
implementationType,
static key => key
.GetInterfaces()
.Where(IsSupportedHandlerInterface)
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
.ToArray());
}
/// <summary>
/// 根据生成器提供的 fallback 清单或整程序集扫描结果,获取本轮要注册的候选处理器类型。
/// </summary>

View File

@ -7,7 +7,7 @@ CQRS 迁移与收敛。
## 当前恢复点
- 恢复点编号:`CQRS-REWRITE-RP-047`
- 恢复点编号:`CQRS-REWRITE-RP-048`
- 当前阶段:`Phase 8`
- 当前焦点:
- 当前功能历史已归档active 跟踪仅保留 `Phase 8` 主线的恢复入口
@ -15,6 +15,7 @@ CQRS 迁移与收敛。
- 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物
- 已补充 pointer 响应类型的 precise runtime type 生成,避免这类 handler 再退回程序集级 reflection fallback
- 已收紧 function pointer 签名的可直接生成判定,仅在其返回值与参数类型都可安全引用时才走静态注册路径
- 已为 registrar 的 reflection 注册路径补充 handler-interface 元数据缓存,减少跨容器重复注册时的 `GetInterfaces()` 反射
- 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点
## 当前状态摘要
@ -40,6 +41,10 @@ CQRS 迁移与收敛。
- `CqrsHandlerRegistryGenerator` 现可为 pointer 类型递归重建 runtime type并通过 `MakePointerType()` 生成精确 service type
- function pointer 签名不再默认视为“可直接引用”;只有当返回值与每个参数类型都可从 generated registry 安全引用时,才允许直接生成
- 含隐藏类型的 function pointer handler 仍会保留原有 fallback / 诊断路径,避免此次覆盖扩展误伤已有回退边界
- `2026-04-20` 已完成一轮 registrar reflection 路径收敛:
- `CqrsHandlerRegistrar` 现会按 `Type` 弱键缓存已筛选且排序好的 supported handler interface 列表
- 同一 handler 类型跨容器重复注册时,不再重复执行 `GetInterfaces()` 与支持接口筛选
- `GFramework.Cqrs.Tests` 已补充 registrar 静态缓存隔离与 supported interface 缓存复用回归
- 当前主线优先级:
- generator 覆盖面继续扩大
- dispatch/invoker 反射占比继续下降
@ -67,6 +72,9 @@ CQRS 迁移与收敛。
- `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 precise registration 与 function pointer fallback 边界
- `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` 测试通过;本轮覆盖 registrar 的 supported handler interface 缓存
## 下一步

View File

@ -2,6 +2,16 @@
## 2026-04-20
### 阶段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
- 已在 `CqrsHandlerRegistryGenerator` 中补充 pointer 类型的 runtime type 递归建模与源码发射precise registration 现可通过 `MakePointerType()` 还原隐藏 pointer 响应类型