From c7516800e7132d02efe5fbc51b9ca7dd1a48dd57 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Fri, 17 Apr 2026 16:19:35 +0800
Subject: [PATCH] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0=20CQRS=20?=
=?UTF-8?q?=E5=A4=84=E7=90=86=E5=99=A8=E8=87=AA=E5=8A=A8=E6=B3=A8=E5=86=8C?=
=?UTF-8?q?=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现 CqrsHandlerRegistrar 类,支持扫描并注册 CQRS 请求/通知/流式处理器
- 添加程序集级源码生成注册器支持,减少冷启动反射开销
- 实现反射回退机制,在生成注册器不可用时进行类型扫描
- 添加进程级缓存机制,避免重复分析程序集元数据和类型加载
- 支持确定性的处理器注册顺序,按名称排序保证稳定性
- 实现类型加载容错机制,部分类型失败时保留其他处理器注册
- 添加完整的单元测试覆盖,验证各种注册场景和错误处理
- 实现日志记录功能,提供详细的注册过程诊断信息
---
.../Cqrs/CqrsHandlerRegistrarTests.cs | 91 +++++++++++++
.../Internal/CqrsHandlerRegistrar.cs | 120 ++++++++++++++++--
2 files changed, 197 insertions(+), 14 deletions(-)
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;
+ }
}