From 110666d06b0e4ff5ab5690e7df6bda0b675962b1 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:34:24 +0800 Subject: [PATCH] =?UTF-8?q?refactor(cqrs):=20=E7=BC=93=E5=AD=98=E5=A4=84?= =?UTF-8?q?=E7=90=86=E5=99=A8=E6=8E=A5=E5=8F=A3=E5=8F=8D=E5=B0=84=E5=85=83?= =?UTF-8?q?=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化 CqrsHandlerRegistrar 复用 supported handler interface 缓存 - 补充 registrar 静态缓存隔离与接口缓存复用回归测试 - 更新 cqrs-rewrite 跟踪与 trace 到 RP-048 --- .../Cqrs/CqrsHandlerRegistrarTests.cs | 119 ++++++++++++++++++ .../Internal/CqrsHandlerRegistrar.cs | 31 ++++- .../todos/cqrs-rewrite-migration-tracking.md | 10 +- .../traces/cqrs-rewrite-migration-trace.md | 10 ++ 4 files changed, 163 insertions(+), 7 deletions(-) diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index 349c80f7..878ba484 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -32,6 +32,7 @@ internal sealed class CqrsHandlerRegistrarTests _container.Freeze(); _context = new ArchitectureContext(_container); + ClearRegistrarCaches(); } /// @@ -43,6 +44,7 @@ internal sealed class CqrsHandlerRegistrarTests _context = null; _container = null; DeterministicNotificationHandlerState.Reset(); + ClearRegistrarCaches(); } /// @@ -435,6 +437,123 @@ internal sealed class CqrsHandlerRegistrarTests partiallyLoadableAssembly.Verify(static assembly => assembly.GetTypes(), Times.Once); } + + /// + /// 验证同一 handler 类型跨容器重复注册时,会复用已筛选的 supported handler interface 列表, + /// 而不是为每个容器重新执行接口反射分析。 + /// + [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(); + 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); + } + + /// + /// 清空本测试依赖的 registrar 静态缓存,避免跨用例共享进程级状态导致断言漂移。 + /// + private static void ClearRegistrarCaches() + { + ClearCache(GetRegistrarCacheField("AssemblyMetadataCache")); + ClearCache(GetRegistrarCacheField("RegistryActivationMetadataCache")); + ClearCache(GetRegistrarCacheField("LoadableTypesCache")); + ClearCache(GetRegistrarCacheField("SupportedHandlerInterfacesCache")); + } + + /// + /// 通过反射读取 registrar 的静态缓存对象。 + /// + 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."); + } + + /// + /// 清空指定缓存对象。 + /// + private static void ClearCache(object cache) + { + _ = InvokeInstanceMethod(cache, "Clear"); + } + + /// + /// 读取单键缓存中当前保存的对象。 + /// + private static object? GetSingleKeyCacheValue(object cache, Type key) + { + return InvokeInstanceMethod(cache, "GetValueOrDefaultForTesting", key); + } + + /// + /// 调用缓存对象上的实例方法。 + /// + 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 运行时类型。 + /// + private static Type GetRegistrarType() + { + return typeof(CqrsReflectionFallbackAttribute).Assembly + .GetType("GFramework.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)!; + } } /// diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index c9562b17..ce380d48 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -26,6 +26,11 @@ internal static class CqrsHandlerRegistrar private static readonly WeakKeyCache> LoadableTypesCache = new(); + // 卸载安全的进程级缓存:同一 handler 类型跨容器重复注册时, + // 复用已筛选且排序好的 supported handler interface 列表,避免重复执行 GetInterfaces()。 + private static readonly WeakKeyCache> SupportedHandlerInterfacesCache = + new(); + /// /// 扫描指定程序集并注册所有 CQRS 请求/通知/流式处理器。 /// @@ -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}."); } } } + /// + /// 获取指定实现类型上所有受支持的 CQRS handler 接口,并缓存筛选与排序结果。 + /// + /// 要分析的处理器实现类型。 + /// 当前实现类型声明的受支持 handler 接口列表。 + private static IReadOnlyList GetSupportedHandlerInterfaces(Type implementationType) + { + ArgumentNullException.ThrowIfNull(implementationType); + + return SupportedHandlerInterfacesCache.GetOrAdd( + implementationType, + static key => key + .GetInterfaces() + .Where(IsSupportedHandlerInterface) + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToArray()); + } + /// /// 根据生成器提供的 fallback 清单或整程序集扫描结果,获取本轮要注册的候选处理器类型。 /// 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 a0843460..8b85d05f 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-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 缓存 ## 下一步 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 98023113..6ad5b62a 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,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 响应类型