mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
- 实现CqrsHandlerRegistrar类,支持扫描并注册CQRS请求/通知/流式处理器 - 添加源码生成注册器优先策略,减少冷启动时的反射开销 - 实现运行时反射扫描回退机制,确保处理器注册的完整性 - 添加CqrsReflectionFallbackAttribute特性,标记需要运行时补充扫描的程序集 - 创建完整的单元测试套件,验证处理器注册顺序与容错行为 - 实现CqrsHandlerRegistryGenerator源码生成器,自动生成处理器注册代码 - 添加详细的日志记录与诊断功能,便于调试注册过程 - 实现类型安全的处理器映射验证与重复注册检测机制
379 lines
15 KiB
C#
379 lines
15 KiB
C#
using GFramework.Core.Abstractions.Ioc;
|
|
using GFramework.Core.Abstractions.Logging;
|
|
using GFramework.Cqrs.Abstractions.Cqrs;
|
|
|
|
namespace GFramework.Cqrs.Internal;
|
|
|
|
/// <summary>
|
|
/// 在架构初始化期间扫描并注册 CQRS 处理器。
|
|
/// 运行时会优先尝试使用源码生成的程序集级注册器,以减少冷启动阶段的反射开销;
|
|
/// 当目标程序集没有生成注册器,或注册器不可用时,再回退到运行时反射扫描。
|
|
/// </summary>
|
|
internal static class CqrsHandlerRegistrar
|
|
{
|
|
/// <summary>
|
|
/// 扫描指定程序集并注册所有 CQRS 请求/通知/流式处理器。
|
|
/// </summary>
|
|
/// <param name="container">目标容器。</param>
|
|
/// <param name="assemblies">要扫描的程序集集合。</param>
|
|
/// <param name="logger">日志记录器。</param>
|
|
public static void RegisterHandlers(
|
|
IIocContainer container,
|
|
IEnumerable<Assembly> assemblies,
|
|
ILogger logger)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(container);
|
|
ArgumentNullException.ThrowIfNull(assemblies);
|
|
ArgumentNullException.ThrowIfNull(logger);
|
|
|
|
foreach (var assembly in assemblies
|
|
.Where(static assembly => assembly is not null)
|
|
.Distinct()
|
|
.OrderBy(GetAssemblySortKey, StringComparer.Ordinal))
|
|
{
|
|
var generatedRegistrationResult =
|
|
TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger);
|
|
if (generatedRegistrationResult is { UsedGeneratedRegistry: true, RequiresReflectionFallback: false })
|
|
continue;
|
|
|
|
RegisterAssemblyHandlers(
|
|
container.GetServicesUnsafe,
|
|
assembly,
|
|
logger,
|
|
generatedRegistrationResult.ReflectionFallbackTypeNames);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 优先使用程序集级源码生成注册器完成 CQRS 映射注册。
|
|
/// </summary>
|
|
/// <param name="services">目标服务集合。</param>
|
|
/// <param name="assembly">当前要处理的程序集。</param>
|
|
/// <param name="logger">日志记录器。</param>
|
|
/// <returns>生成注册器的使用结果。</returns>
|
|
private static GeneratedRegistrationResult TryRegisterGeneratedHandlers(
|
|
IServiceCollection services,
|
|
Assembly assembly,
|
|
ILogger logger)
|
|
{
|
|
var assemblyName = GetAssemblySortKey(assembly);
|
|
|
|
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();
|
|
|
|
if (registryTypes.Count == 0)
|
|
return GeneratedRegistrationResult.NoGeneratedRegistry();
|
|
|
|
var registries = new List<ICqrsHandlerRegistry>(registryTypes.Count);
|
|
foreach (var registryType in registryTypes)
|
|
{
|
|
if (!typeof(ICqrsHandlerRegistry).IsAssignableFrom(registryType))
|
|
{
|
|
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)
|
|
{
|
|
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)
|
|
{
|
|
logger.Warn(
|
|
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it could not be instantiated.");
|
|
return GeneratedRegistrationResult.NoGeneratedRegistry();
|
|
}
|
|
|
|
registries.Add(registry);
|
|
}
|
|
|
|
foreach (var registry in registries)
|
|
{
|
|
logger.Debug(
|
|
$"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}.");
|
|
registry.Register(services, logger);
|
|
}
|
|
|
|
var reflectionFallbackTypeNames = GetReflectionFallbackTypeNames(assembly);
|
|
if (reflectionFallbackTypeNames is not null)
|
|
{
|
|
if (reflectionFallbackTypeNames.Count > 0)
|
|
{
|
|
logger.Debug(
|
|
$"Generated CQRS registry for assembly {assemblyName} requested targeted reflection fallback for {reflectionFallbackTypeNames.Count} unsupported handler type(s).");
|
|
}
|
|
else
|
|
{
|
|
logger.Debug(
|
|
$"Generated CQRS registry for assembly {assemblyName} requested full reflection fallback for unsupported handlers.");
|
|
}
|
|
|
|
return GeneratedRegistrationResult.WithReflectionFallback(reflectionFallbackTypeNames);
|
|
}
|
|
|
|
return GeneratedRegistrationResult.FullyHandled();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
logger.Warn(
|
|
$"Generated CQRS handler registry discovery failed for assembly {assemblyName}. Falling back to reflection scan.");
|
|
logger.Warn(
|
|
$"Failed to use generated CQRS handler registry for assembly {assemblyName}: {exception.Message}");
|
|
return GeneratedRegistrationResult.NoGeneratedRegistry();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 注册单个程序集里的所有 CQRS 处理器映射。
|
|
/// </summary>
|
|
private static void RegisterAssemblyHandlers(
|
|
IServiceCollection services,
|
|
Assembly assembly,
|
|
ILogger logger,
|
|
IReadOnlyList<string>? reflectionFallbackTypeNames)
|
|
{
|
|
foreach (var implementationType in GetCandidateHandlerTypes(assembly, logger, reflectionFallbackTypeNames)
|
|
.Where(IsConcreteHandlerType))
|
|
{
|
|
var handlerInterfaces = implementationType
|
|
.GetInterfaces()
|
|
.Where(IsSupportedHandlerInterface)
|
|
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
|
|
.ToList();
|
|
|
|
if (handlerInterfaces.Count == 0)
|
|
continue;
|
|
|
|
foreach (var handlerInterface in handlerInterfaces)
|
|
{
|
|
if (IsHandlerMappingAlreadyRegistered(services, handlerInterface, implementationType))
|
|
{
|
|
logger.Debug(
|
|
$"Skipping duplicate CQRS handler {implementationType.FullName} as {handlerInterface.FullName}.");
|
|
continue;
|
|
}
|
|
|
|
// Request/notification handlers receive context injection before every dispatch.
|
|
// Transient registration avoids sharing mutable Context across concurrent requests.
|
|
services.AddTransient(handlerInterface, implementationType);
|
|
logger.Debug(
|
|
$"Registered CQRS handler {implementationType.FullName} as {handlerInterface.FullName}.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 根据生成器提供的 fallback 清单或整程序集扫描结果,获取本轮要注册的候选处理器类型。
|
|
/// </summary>
|
|
private static IReadOnlyList<Type> GetCandidateHandlerTypes(
|
|
Assembly assembly,
|
|
ILogger logger,
|
|
IReadOnlyList<string>? reflectionFallbackTypeNames)
|
|
{
|
|
return reflectionFallbackTypeNames is { Count: > 0 }
|
|
? GetNamedFallbackTypes(assembly, reflectionFallbackTypeNames, logger)
|
|
: GetLoadableTypes(assembly, logger);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 根据生成器记录的类型全名,精确解析仍需运行时补充注册的处理器类型。
|
|
/// </summary>
|
|
private static IReadOnlyList<Type> GetNamedFallbackTypes(
|
|
Assembly assembly,
|
|
IReadOnlyList<string> reflectionFallbackTypeNames,
|
|
ILogger logger)
|
|
{
|
|
var assemblyName = GetAssemblySortKey(assembly);
|
|
var resolvedTypes = new List<Type>(reflectionFallbackTypeNames.Count);
|
|
foreach (var typeName in reflectionFallbackTypeNames
|
|
.Where(static name => !string.IsNullOrWhiteSpace(name))
|
|
.Distinct(StringComparer.Ordinal)
|
|
.OrderBy(static name => name, StringComparer.Ordinal))
|
|
{
|
|
try
|
|
{
|
|
var type = assembly.GetType(typeName, throwOnError: false, ignoreCase: false);
|
|
if (type is null)
|
|
{
|
|
logger.Warn(
|
|
$"Generated CQRS reflection fallback type {typeName} could not be resolved in assembly {assemblyName}. Skipping targeted fallback entry.");
|
|
continue;
|
|
}
|
|
|
|
resolvedTypes.Add(type);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
logger.Warn(
|
|
$"Generated CQRS reflection fallback type {typeName} failed to load in assembly {assemblyName}: {exception.Message}");
|
|
}
|
|
}
|
|
|
|
return resolvedTypes
|
|
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
|
|
.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 安全获取程序集中的可加载类型,并在部分类型加载失败时保留其余处理器注册能力。
|
|
/// </summary>
|
|
private static IReadOnlyList<Type> GetLoadableTypes(Assembly assembly, ILogger logger)
|
|
{
|
|
try
|
|
{
|
|
return assembly.GetTypes()
|
|
.Where(static type => type is not null)
|
|
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
|
|
.ToList();
|
|
}
|
|
catch (ReflectionTypeLoadException exception)
|
|
{
|
|
return RecoverLoadableTypes(assembly, exception, logger);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 记录部分类型加载失败,并返回仍然可用的类型集合。
|
|
/// </summary>
|
|
private static IReadOnlyList<Type> RecoverLoadableTypes(
|
|
Assembly assembly,
|
|
ReflectionTypeLoadException exception,
|
|
ILogger logger)
|
|
{
|
|
var assemblyName = GetAssemblySortKey(assembly);
|
|
logger.Warn(
|
|
$"CQRS handler scan partially failed for assembly {assemblyName}. Continuing with loadable types.");
|
|
|
|
foreach (var loaderException in exception.LoaderExceptions.Where(static ex => ex is not null))
|
|
{
|
|
logger.Warn(
|
|
$"Failed to load one or more types while scanning {assemblyName}: {loaderException!.Message}");
|
|
}
|
|
|
|
return exception.Types
|
|
.Where(static type => type is not null)
|
|
.Cast<Type>()
|
|
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
|
|
.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 判断指定类型是否可作为可实例化处理器。
|
|
/// </summary>
|
|
private static bool IsConcreteHandlerType(Type type)
|
|
{
|
|
return type is { IsAbstract: false, IsInterface: false } && !type.ContainsGenericParameters;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 判断接口是否为当前运行时支持的 CQRS 处理器接口。
|
|
/// </summary>
|
|
private static bool IsSupportedHandlerInterface(Type type)
|
|
{
|
|
if (!type.IsGenericType)
|
|
return false;
|
|
|
|
var definition = type.GetGenericTypeDefinition();
|
|
return definition == typeof(IRequestHandler<,>) ||
|
|
definition == typeof(INotificationHandler<>) ||
|
|
definition == typeof(IStreamRequestHandler<,>);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取生成注册器要求运行时继续补充反射扫描的 handler 类型名清单。
|
|
/// </summary>
|
|
private static IReadOnlyList<string>? GetReflectionFallbackTypeNames(Assembly assembly)
|
|
{
|
|
var fallbackAttributes = assembly
|
|
.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), inherit: false)
|
|
.OfType<CqrsReflectionFallbackAttribute>()
|
|
.ToList();
|
|
|
|
if (fallbackAttributes.Count == 0)
|
|
return null;
|
|
|
|
return fallbackAttributes
|
|
.SelectMany(static attribute => attribute.FallbackHandlerTypeNames)
|
|
.Where(static typeName => !string.IsNullOrWhiteSpace(typeName))
|
|
.Distinct(StringComparer.Ordinal)
|
|
.OrderBy(static typeName => typeName, StringComparer.Ordinal)
|
|
.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 判断同一 handler 映射是否已经由生成注册器或先前扫描步骤写入服务集合。
|
|
/// </summary>
|
|
private static bool IsHandlerMappingAlreadyRegistered(
|
|
IServiceCollection services,
|
|
Type handlerInterface,
|
|
Type implementationType)
|
|
{
|
|
// 这里保持线性扫描,避免为常见的小到中等规模程序集长期维护额外索引。
|
|
// 若未来大型服务集合出现热点,可在更高层批处理中引入 HashSet<(Type, Type)> 做 O(1) 去重。
|
|
return services.Any(descriptor =>
|
|
descriptor.ServiceType == handlerInterface &&
|
|
descriptor.ImplementationType == implementationType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 生成程序集排序键,保证跨运行环境的处理器注册顺序稳定。
|
|
/// </summary>
|
|
private static string GetAssemblySortKey(Assembly assembly)
|
|
{
|
|
return assembly.FullName ?? assembly.GetName().Name ?? assembly.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 生成类型排序键,保证同一程序集内的处理器与接口映射顺序稳定。
|
|
/// </summary>
|
|
private static string GetTypeSortKey(Type type)
|
|
{
|
|
return type.FullName ?? type.Name;
|
|
}
|
|
|
|
private readonly record struct GeneratedRegistrationResult(
|
|
bool UsedGeneratedRegistry,
|
|
bool RequiresReflectionFallback,
|
|
IReadOnlyList<string>? ReflectionFallbackTypeNames)
|
|
{
|
|
public static GeneratedRegistrationResult NoGeneratedRegistry()
|
|
{
|
|
return new GeneratedRegistrationResult(
|
|
UsedGeneratedRegistry: false,
|
|
RequiresReflectionFallback: false,
|
|
ReflectionFallbackTypeNames: null);
|
|
}
|
|
|
|
public static GeneratedRegistrationResult FullyHandled()
|
|
{
|
|
return new GeneratedRegistrationResult(
|
|
UsedGeneratedRegistry: true,
|
|
RequiresReflectionFallback: false,
|
|
ReflectionFallbackTypeNames: null);
|
|
}
|
|
|
|
public static GeneratedRegistrationResult WithReflectionFallback(
|
|
IReadOnlyList<string> reflectionFallbackTypeNames)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(reflectionFallbackTypeNames);
|
|
|
|
return new GeneratedRegistrationResult(
|
|
UsedGeneratedRegistry: true,
|
|
RequiresReflectionFallback: true,
|
|
ReflectionFallbackTypeNames: reflectionFallbackTypeNames);
|
|
}
|
|
}
|
|
}
|