mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
feat(cqrs): 添加 CQRS 处理器自动注册功能
- 实现 CqrsHandlerRegistrar 类,支持扫描并注册 CQRS 请求/通知/流式处理器 - 添加程序集级源码生成注册器支持,减少冷启动反射开销 - 实现反射回退机制,在生成注册器不可用时进行类型扫描 - 添加进程级缓存机制,避免重复分析程序集元数据和类型加载 - 支持确定性的处理器注册顺序,按名称排序保证稳定性 - 实现类型加载容错机制,部分类型失败时保留其他处理器注册 - 添加完整的单元测试覆盖,验证各种注册场景和错误处理 - 实现日志记录功能,提供详细的注册过程诊断信息
This commit is contained in:
parent
be59dc7f27
commit
c7516800e7
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user