diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index b44b0bb1..ea1fae30 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -292,6 +292,97 @@ internal sealed class CqrsHandlerRegistrarTests Times.Never); generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Never); } + + /// + /// 验证同一程序集对象重复接入多个容器时,会复用已解析的 registry / fallback 元数据, + /// 而不是重复读取程序集级 attribute 或重复执行 type-name lookup。 + /// + [Test] + public void RegisterHandlers_Should_Cache_Assembly_Metadata_Across_Containers() + { + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns("GFramework.Core.Tests.Cqrs.CachedMetadataAssembly, Version=1.0.0.0"); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false)) + .Returns( + [ + new CqrsReflectionFallbackAttribute( + ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!) + ]); + generatedAssembly + .Setup(static assembly => assembly.GetType( + ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!, + false, + false)) + .Returns(ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType); + + var firstContainer = new MicrosoftDiContainer(); + var secondContainer = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(firstContainer, generatedAssembly.Object); + CqrsTestRuntime.RegisterHandlers(secondContainer, generatedAssembly.Object); + + generatedAssembly.Verify( + static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false), + Times.Once); + generatedAssembly.Verify( + static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false), + Times.Once); + generatedAssembly.Verify( + static assembly => assembly.GetType( + ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!, + false, + false), + Times.Once); + } + + /// + /// 验证同一程序集对象在未命中 generated registry 时,会复用首次扫描得到的可加载类型列表, + /// 而不是为每个容器重复执行整程序集 GetTypes()。 + /// + [Test] + public void RegisterHandlers_Should_Cache_Loadable_Types_Across_Containers() + { + var reflectionTypeLoadException = new ReflectionTypeLoadException( + [typeof(AlphaDeterministicNotificationHandler), null], + [new TypeLoadException("Cached loadable-type probe.")]); + var partiallyLoadableAssembly = new Mock(); + partiallyLoadableAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns("GFramework.Core.Tests.Cqrs.CachedLoadableTypesAssembly, Version=1.0.0.0"); + partiallyLoadableAssembly + .Setup(static assembly => assembly.GetTypes()) + .Throws(reflectionTypeLoadException); + + var firstContainer = new MicrosoftDiContainer(); + var secondContainer = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(firstContainer, partiallyLoadableAssembly.Object); + CqrsTestRuntime.RegisterHandlers(secondContainer, partiallyLoadableAssembly.Object); + firstContainer.Freeze(); + secondContainer.Freeze(); + + Assert.Multiple(() => + { + Assert.That( + firstContainer.GetAll>() + .Select(static handler => handler.GetType()) + .ToArray(), + Is.EqualTo([typeof(AlphaDeterministicNotificationHandler)])); + Assert.That( + secondContainer.GetAll>() + .Select(static handler => handler.GetType()) + .ToArray(), + Is.EqualTo([typeof(AlphaDeterministicNotificationHandler)])); + }); + + partiallyLoadableAssembly.Verify(static assembly => assembly.GetTypes(), Times.Once); + } } /// diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index 3604de83..a4f20396 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -11,6 +11,19 @@ namespace GFramework.Cqrs.Internal; /// internal static class CqrsHandlerRegistrar { + // 进程级缓存:同一程序集的 generated-registry 元数据与 reflection-fallback 元数据在加载后保持稳定, + // 因此可跨容器复用分析结果,避免每次注册都重复读取程序集级 attribute。 + private static readonly ConcurrentDictionary AssemblyMetadataCache = + new(ReferenceEqualityComparer.Instance); + + // 进程级缓存:registry 类型的可激活性与构造入口是稳定的,可跨多次容器初始化复用。 + private static readonly ConcurrentDictionary RegistryActivationMetadataCache = + new(); + + // 进程级缓存:对未命中 generated-registry 的程序集,缓存可加载类型列表以避免重复 GetTypes() 扫描。 + private static readonly ConcurrentDictionary> LoadableTypesCache = + new(ReferenceEqualityComparer.Instance); + /// /// 扫描指定程序集并注册所有 CQRS 请求/通知/流式处理器。 /// @@ -60,14 +73,10 @@ internal static class CqrsHandlerRegistrar try { - var registryTypes = assembly - .GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), inherit: false) - .OfType() - .Select(static attribute => attribute.RegistryType) - .Where(static type => type is not null) - .Distinct() - .OrderBy(GetTypeSortKey, StringComparer.Ordinal) - .ToList(); + var assemblyMetadata = AssemblyMetadataCache.GetOrAdd( + assembly, + key => AnalyzeAssemblyRegistrationMetadata(key, logger)); + var registryTypes = assemblyMetadata.RegistryTypes; if (registryTypes.Count == 0) return GeneratedRegistrationResult.NoGeneratedRegistry(); @@ -75,27 +84,32 @@ internal static class CqrsHandlerRegistrar var registries = new List(registryTypes.Count); foreach (var registryType in registryTypes) { - if (!typeof(ICqrsHandlerRegistry).IsAssignableFrom(registryType)) + var activationMetadata = RegistryActivationMetadataCache.GetOrAdd( + registryType, + AnalyzeRegistryActivation); + + if (!activationMetadata.ImplementsRegistryContract) { logger.Warn( $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}."); return GeneratedRegistrationResult.NoGeneratedRegistry(); } - if (registryType.IsAbstract) + if (activationMetadata.IsAbstract) { logger.Warn( $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract."); return GeneratedRegistrationResult.NoGeneratedRegistry(); } - if (Activator.CreateInstance(registryType, nonPublic: true) is not ICqrsHandlerRegistry registry) + if (activationMetadata.Factory is null) { logger.Warn( - $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it could not be instantiated."); + $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not expose an accessible parameterless constructor."); return GeneratedRegistrationResult.NoGeneratedRegistry(); } + var registry = activationMetadata.Factory(); registries.Add(registry); } @@ -106,7 +120,7 @@ internal static class CqrsHandlerRegistrar registry.Register(services, logger); } - var reflectionFallbackMetadata = GetReflectionFallbackMetadata(assembly, logger); + var reflectionFallbackMetadata = assemblyMetadata.ReflectionFallbackMetadata; if (reflectionFallbackMetadata is not null) { if (reflectionFallbackMetadata.HasExplicitTypes) @@ -259,13 +273,69 @@ internal static class CqrsHandlerRegistrar /// 安全获取程序集中的可加载类型,并在部分类型加载失败时保留其余处理器注册能力。 /// private static IReadOnlyList GetLoadableTypes(Assembly assembly, ILogger logger) + { + return LoadableTypesCache.GetOrAdd( + assembly, + key => LoadAndSortTypes(key, logger)); + } + + /// + /// 分析并缓存指定程序集上的 generated-registry 与 fallback 元数据。 + /// + private static AssemblyRegistrationMetadata AnalyzeAssemblyRegistrationMetadata( + Assembly assembly, + ILogger logger) + { + var registryTypes = assembly + .GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), inherit: false) + .OfType() + .Select(static attribute => attribute.RegistryType) + .Where(static type => type is not null) + .Distinct() + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToArray(); + + var reflectionFallbackMetadata = GetReflectionFallbackMetadata(assembly, logger); + return new AssemblyRegistrationMetadata(registryTypes, reflectionFallbackMetadata); + } + + /// + /// 分析并缓存 registry 类型的可激活性,避免每次注册都重复检查接口实现与构造函数。 + /// + private static RegistryActivationMetadata AnalyzeRegistryActivation(Type registryType) + { + var implementsRegistryContract = typeof(ICqrsHandlerRegistry).IsAssignableFrom(registryType); + if (!implementsRegistryContract) + return new RegistryActivationMetadata(false, registryType.IsAbstract, null); + + if (registryType.IsAbstract) + return new RegistryActivationMetadata(true, true, null); + + var constructor = registryType.GetConstructor( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + binder: null, + Type.EmptyTypes, + modifiers: null); + + return constructor is null + ? new RegistryActivationMetadata(true, false, null) + : new RegistryActivationMetadata( + true, + false, + () => (ICqrsHandlerRegistry)constructor.Invoke(null)); + } + + /// + /// 首次命中未生成 registry 的程序集时加载并排序全部可扫描类型,后续复用缓存结果。 + /// + private static IReadOnlyList LoadAndSortTypes(Assembly assembly, ILogger logger) { try { return assembly.GetTypes() .Where(static type => type is not null) .OrderBy(GetTypeSortKey, StringComparer.Ordinal) - .ToList(); + .ToArray(); } catch (ReflectionTypeLoadException exception) { @@ -390,4 +460,26 @@ internal static class CqrsHandlerRegistrar public bool HasExplicitTypes => Types.Count > 0; } + + private sealed class AssemblyRegistrationMetadata( + IReadOnlyList registryTypes, + ReflectionFallbackMetadata? reflectionFallbackMetadata) + { + public IReadOnlyList RegistryTypes { get; } = + registryTypes ?? throw new ArgumentNullException(nameof(registryTypes)); + + public ReflectionFallbackMetadata? ReflectionFallbackMetadata { get; } = reflectionFallbackMetadata; + } + + private sealed class RegistryActivationMetadata( + bool implementsRegistryContract, + bool isAbstract, + Func? factory) + { + public bool ImplementsRegistryContract { get; } = implementsRegistryContract; + + public bool IsAbstract { get; } = isAbstract; + + public Func? Factory { get; } = factory; + } }