feat(cqrs): 添加 CQRS 处理器自动注册功能

- 实现 CqrsHandlerRegistrar 类,支持扫描并注册 CQRS 请求/通知/流式处理器
- 添加程序集级源码生成注册器支持,减少冷启动反射开销
- 实现反射回退机制,在生成注册器不可用时进行类型扫描
- 添加进程级缓存机制,避免重复分析程序集元数据和类型加载
- 支持确定性的处理器注册顺序,按名称排序保证稳定性
- 实现类型加载容错机制,部分类型失败时保留其他处理器注册
- 添加完整的单元测试覆盖,验证各种注册场景和错误处理
- 实现日志记录功能,提供详细的注册过程诊断信息
This commit is contained in:
GeWuYou 2026-04-17 16:19:35 +08:00
parent be59dc7f27
commit c7516800e7
2 changed files with 197 additions and 14 deletions

View File

@ -292,6 +292,97 @@ internal sealed class CqrsHandlerRegistrarTests
Times.Never);
generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Never);
}
/// <summary>
/// 验证同一程序集对象重复接入多个容器时,会复用已解析的 registry / fallback 元数据,
/// 而不是重复读取程序集级 attribute 或重复执行 type-name lookup。
/// </summary>
[Test]
public void RegisterHandlers_Should_Cache_Assembly_Metadata_Across_Containers()
{
var generatedAssembly = new Mock<Assembly>();
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);
}
/// <summary>
/// 验证同一程序集对象在未命中 generated registry 时,会复用首次扫描得到的可加载类型列表,
/// 而不是为每个容器重复执行整程序集 <c>GetTypes()</c>。
/// </summary>
[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<Assembly>();
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<INotificationHandler<DeterministicOrderNotification>>()
.Select(static handler => handler.GetType())
.ToArray(),
Is.EqualTo([typeof(AlphaDeterministicNotificationHandler)]));
Assert.That(
secondContainer.GetAll<INotificationHandler<DeterministicOrderNotification>>()
.Select(static handler => handler.GetType())
.ToArray(),
Is.EqualTo([typeof(AlphaDeterministicNotificationHandler)]));
});
partiallyLoadableAssembly.Verify(static assembly => assembly.GetTypes(), Times.Once);
}
}
/// <summary>

View File

@ -11,6 +11,19 @@ namespace GFramework.Cqrs.Internal;
/// </summary>
internal static class CqrsHandlerRegistrar
{
// 进程级缓存:同一程序集的 generated-registry 元数据与 reflection-fallback 元数据在加载后保持稳定,
// 因此可跨容器复用分析结果,避免每次注册都重复读取程序集级 attribute。
private static readonly ConcurrentDictionary<Assembly, AssemblyRegistrationMetadata> AssemblyMetadataCache =
new(ReferenceEqualityComparer.Instance);
// 进程级缓存registry 类型的可激活性与构造入口是稳定的,可跨多次容器初始化复用。
private static readonly ConcurrentDictionary<Type, RegistryActivationMetadata> RegistryActivationMetadataCache =
new();
// 进程级缓存:对未命中 generated-registry 的程序集,缓存可加载类型列表以避免重复 GetTypes() 扫描。
private static readonly ConcurrentDictionary<Assembly, IReadOnlyList<Type>> LoadableTypesCache =
new(ReferenceEqualityComparer.Instance);
/// <summary>
/// 扫描指定程序集并注册所有 CQRS 请求/通知/流式处理器。
/// </summary>
@ -60,14 +73,10 @@ internal static class CqrsHandlerRegistrar
try
{
var registryTypes = assembly
.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), inherit: false)
.OfType<CqrsHandlerRegistryAttribute>()
.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<ICqrsHandlerRegistry>(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
/// 安全获取程序集中的可加载类型,并在部分类型加载失败时保留其余处理器注册能力。
/// </summary>
private static IReadOnlyList<Type> GetLoadableTypes(Assembly assembly, ILogger logger)
{
return LoadableTypesCache.GetOrAdd(
assembly,
key => LoadAndSortTypes(key, logger));
}
/// <summary>
/// 分析并缓存指定程序集上的 generated-registry 与 fallback 元数据。
/// </summary>
private static AssemblyRegistrationMetadata AnalyzeAssemblyRegistrationMetadata(
Assembly assembly,
ILogger logger)
{
var registryTypes = assembly
.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), inherit: false)
.OfType<CqrsHandlerRegistryAttribute>()
.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);
}
/// <summary>
/// 分析并缓存 registry 类型的可激活性,避免每次注册都重复检查接口实现与构造函数。
/// </summary>
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));
}
/// <summary>
/// 首次命中未生成 registry 的程序集时加载并排序全部可扫描类型,后续复用缓存结果。
/// </summary>
private static IReadOnlyList<Type> 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<Type> registryTypes,
ReflectionFallbackMetadata? reflectionFallbackMetadata)
{
public IReadOnlyList<Type> RegistryTypes { get; } =
registryTypes ?? throw new ArgumentNullException(nameof(registryTypes));
public ReflectionFallbackMetadata? ReflectionFallbackMetadata { get; } = reflectionFallbackMetadata;
}
private sealed class RegistryActivationMetadata(
bool implementsRegistryContract,
bool isAbstract,
Func<ICqrsHandlerRegistry>? factory)
{
public bool ImplementsRegistryContract { get; } = implementsRegistryContract;
public bool IsAbstract { get; } = isAbstract;
public Func<ICqrsHandlerRegistry>? Factory { get; } = factory;
}
}