feat(cqrs): 添加 CQRS 处理器注册器和源码生成器

- 实现 CqrsHandlerRegistrar 类用于扫描并注册 CQRS 处理器
- 添加源码生成器自动生成 CQRS 处理器注册器减少反射开销
- 实现运行时回退机制在生成注册器不可用时使用反射扫描
- 添加完整的单元测试验证处理器注册顺序和容错行为
- 支持请求、通知和流式处理器的自动注册功能
- 实现稳定的处理器注册顺序保证跨环境一致性
- 添加详细的诊断日志记录注册过程和异常情况
This commit is contained in:
GeWuYou 2026-04-16 08:49:13 +08:00
parent a7604de804
commit bc9336428e
5 changed files with 328 additions and 35 deletions

View File

@ -13,6 +13,9 @@ namespace GFramework.Cqrs.Tests.Cqrs;
[TestFixture]
internal sealed class CqrsHandlerRegistrarTests
{
private MicrosoftDiContainer? _container;
private ArchitectureContext? _context;
/// <summary>
/// 初始化测试容器并重置共享状态。
/// </summary>
@ -42,9 +45,6 @@ internal sealed class CqrsHandlerRegistrarTests
DeterministicNotificationHandlerState.Reset();
}
private MicrosoftDiContainer? _container;
private ArchitectureContext? _context;
/// <summary>
/// 验证自动扫描到的通知处理器会按稳定名称顺序执行,而不是依赖反射枚举顺序。
/// </summary>
@ -188,6 +188,50 @@ internal sealed class CqrsHandlerRegistrarTests
LoggerFactoryResolver.Provider = originalProvider;
}
}
/// <summary>
/// 验证当生成注册器显式要求 reflection fallback 时,运行时会补扫剩余 handlers
/// 同时避免把已由生成注册器注册的映射重复写入服务集合。
/// </summary>
[Test]
public void RegisterHandlers_Should_Combine_Generated_Registry_With_Reflection_Fallback_Without_Duplicates()
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Cqrs.PartialGeneratedRegistryAssembly, 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()]);
generatedAssembly
.Setup(static assembly => assembly.GetTypes())
.Returns(
[
typeof(GeneratedRegistryNotificationHandler),
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType
]);
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
var registrations = container.GetServicesUnsafe
.Where(static descriptor =>
descriptor.ServiceType == typeof(INotificationHandler<GeneratedRegistryNotification>) &&
descriptor.ImplementationType is not null)
.Select(static descriptor => descriptor.ImplementationType!)
.ToList();
Assert.That(
registrations,
Is.EqualTo(
[
typeof(GeneratedRegistryNotificationHandler),
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType
]));
}
}
/// <summary>
@ -337,3 +381,52 @@ internal sealed class GeneratedNotificationHandlerRegistry : ICqrsHandlerRegistr
$"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler<GeneratedRegistryNotification>).FullName}.");
}
}
/// <summary>
/// 用于验证“生成注册器 + reflection fallback”组合路径的私有嵌套处理器容器。
/// </summary>
internal sealed class ReflectionFallbackNotificationContainer
{
/// <summary>
/// 获取仅能通过反射补扫接入的私有嵌套处理器类型。
/// </summary>
public static Type ReflectionOnlyHandlerType => typeof(ReflectionOnlyGeneratedRegistryNotificationHandler);
private sealed class ReflectionOnlyGeneratedRegistryNotificationHandler
: INotificationHandler<GeneratedRegistryNotification>
{
/// <summary>
/// 处理测试通知。
/// </summary>
/// <param name="notification">通知实例。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>已完成任务。</returns>
public ValueTask Handle(GeneratedRegistryNotification notification, CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
}
}
/// <summary>
/// 模拟局部生成注册器场景中,仅注册“可由生成代码直接引用”的那部分 handlers。
/// </summary>
internal sealed class PartialGeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry
{
/// <summary>
/// 将生成路径可见的通知处理器注册到目标服务集合。
/// </summary>
/// <param name="services">承载处理器映射的服务集合。</param>
/// <param name="logger">用于记录注册诊断的日志器。</param>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(INotificationHandler<GeneratedRegistryNotification>),
typeof(GeneratedRegistryNotificationHandler));
logger.Debug(
$"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler<GeneratedRegistryNotification>).FullName}.");
}
}

View File

@ -0,0 +1,14 @@
namespace GFramework.Cqrs;
/// <summary>
/// 标记程序集中的 CQRS 生成注册器仍需要运行时补充反射扫描。
/// </summary>
/// <remarks>
/// 该特性通常由源码生成器自动添加到消费端程序集。
/// 当生成器只能安全生成部分 handler 映射时,运行时会先执行生成注册器,再补一次带去重的反射扫描,
/// 以覆盖那些生成代码无法直接引用的 handler 类型。
/// </remarks>
[AttributeUsage(AttributeTargets.Assembly)]
public sealed class CqrsReflectionFallbackAttribute : Attribute
{
}

View File

@ -1,4 +1,3 @@
using System.Reflection;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
@ -32,7 +31,9 @@ internal static class CqrsHandlerRegistrar
.Distinct()
.OrderBy(GetAssemblySortKey, StringComparer.Ordinal))
{
if (TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger))
var generatedRegistrationResult =
TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger);
if (generatedRegistrationResult == GeneratedRegistrationResult.FullyHandled)
continue;
RegisterAssemblyHandlers(container.GetServicesUnsafe, assembly, logger);
@ -45,8 +46,11 @@ internal static class CqrsHandlerRegistrar
/// <param name="services">目标服务集合。</param>
/// <param name="assembly">当前要处理的程序集。</param>
/// <param name="logger">日志记录器。</param>
/// <returns>当成功使用生成注册器时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
private static bool TryRegisterGeneratedHandlers(IServiceCollection services, Assembly assembly, ILogger logger)
/// <returns>生成注册器的使用结果。</returns>
private static GeneratedRegistrationResult TryRegisterGeneratedHandlers(
IServiceCollection services,
Assembly assembly,
ILogger logger)
{
var assemblyName = GetAssemblySortKey(assembly);
@ -62,7 +66,7 @@ internal static class CqrsHandlerRegistrar
.ToList();
if (registryTypes.Count == 0)
return false;
return GeneratedRegistrationResult.NoGeneratedRegistry;
var registries = new List<ICqrsHandlerRegistry>(registryTypes.Count);
foreach (var registryType in registryTypes)
@ -71,21 +75,21 @@ internal static class CqrsHandlerRegistrar
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}.");
return false;
return GeneratedRegistrationResult.NoGeneratedRegistry;
}
if (registryType.IsAbstract)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract.");
return false;
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 false;
return GeneratedRegistrationResult.NoGeneratedRegistry;
}
registries.Add(registry);
@ -98,7 +102,14 @@ internal static class CqrsHandlerRegistrar
registry.Register(services, logger);
}
return true;
if (RequiresReflectionFallback(assembly))
{
logger.Debug(
$"Generated CQRS registry for assembly {assemblyName} requested reflection fallback for unsupported handlers.");
return GeneratedRegistrationResult.RequiresReflectionFallback;
}
return GeneratedRegistrationResult.FullyHandled;
}
catch (Exception exception)
{
@ -106,7 +117,7 @@ internal static class CqrsHandlerRegistrar
$"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 false;
return GeneratedRegistrationResult.NoGeneratedRegistry;
}
}
@ -128,6 +139,13 @@ internal static class CqrsHandlerRegistrar
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);
@ -202,6 +220,27 @@ internal static class CqrsHandlerRegistrar
definition == typeof(IStreamRequestHandler<,>);
}
/// <summary>
/// 判断生成注册器是否要求运行时继续补充反射扫描。
/// </summary>
private static bool RequiresReflectionFallback(Assembly assembly)
{
return assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), inherit: false)?.Length > 0;
}
/// <summary>
/// 判断同一 handler 映射是否已经由生成注册器或先前扫描步骤写入服务集合。
/// </summary>
private static bool IsHandlerMappingAlreadyRegistered(
IServiceCollection services,
Type handlerInterface,
Type implementationType)
{
return services.Any(descriptor =>
descriptor.ServiceType == handlerInterface &&
descriptor.ImplementationType == implementationType);
}
/// <summary>
/// 生成程序集排序键,保证跨运行环境的处理器注册顺序稳定。
/// </summary>
@ -217,4 +256,11 @@ internal static class CqrsHandlerRegistrar
{
return type.FullName ?? type.Name;
}
private enum GeneratedRegistrationResult
{
NoGeneratedRegistry,
FullyHandled,
RequiresReflectionFallback
}
}

View File

@ -61,6 +61,11 @@ public class CqrsHandlerRegistryGeneratorTests
{
public CqrsHandlerRegistryAttribute(Type registryType) { }
}
[AttributeUsage(AttributeTargets.Assembly)]
public sealed class CqrsReflectionFallbackAttribute : Attribute
{
}
}
namespace TestApp
@ -120,10 +125,120 @@ public class CqrsHandlerRegistryGeneratorTests
}
/// <summary>
/// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器会放弃产出并让运行时回退到反射扫描。
/// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器仍会为可见 handlers 生成注册器,
/// 并额外标记运行时补充反射扫描。
/// </summary>
[Test]
public async Task Skips_Generation_When_Assembly_Contains_Private_Nested_Handler()
public async Task
Generates_Visible_Handlers_And_Requests_Reflection_Fallback_When_Assembly_Contains_Private_Nested_Handler()
{
const string source = """
using System;
namespace Microsoft.Extensions.DependencyInjection
{
public interface IServiceCollection { }
public static class ServiceCollectionServiceExtensions
{
public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
}
}
namespace GFramework.Core.Abstractions.Logging
{
public interface ILogger
{
void Debug(string msg);
}
}
namespace GFramework.Cqrs.Abstractions.Cqrs
{
public interface IRequest<TResponse> { }
public interface INotification { }
public interface IStreamRequest<TResponse> { }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
}
namespace GFramework.Cqrs
{
public interface ICqrsHandlerRegistry
{
void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class CqrsHandlerRegistryAttribute : Attribute
{
public CqrsHandlerRegistryAttribute(Type registryType) { }
}
[AttributeUsage(AttributeTargets.Assembly)]
public sealed class CqrsReflectionFallbackAttribute : Attribute
{
}
}
namespace TestApp
{
using GFramework.Cqrs.Abstractions.Cqrs;
public sealed record VisibleRequest() : IRequest<string>;
public sealed class Container
{
private sealed record HiddenRequest() : IRequest<string>;
private sealed class HiddenHandler : IRequestHandler<HiddenRequest, string> { }
}
public sealed class VisibleHandler : IRequestHandler<VisibleRequest, string> { }
}
""";
const string expected = """
// <auto-generated />
#nullable enable
[assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))]
[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute()]
namespace GFramework.Generated.Cqrs;
internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry
{
public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger)
{
if (services is null)
throw new global::System.ArgumentNullException(nameof(services));
if (logger is null)
throw new global::System.ArgumentNullException(nameof(logger));
global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient(
services,
typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<global::TestApp.VisibleRequest, string>),
typeof(global::TestApp.VisibleHandler));
logger.Debug("Registered CQRS handler TestApp.VisibleHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<TestApp.VisibleRequest, string>.");
}
}
""";
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
source,
("CqrsHandlerRegistry.g.cs", expected));
}
/// <summary>
/// 验证当旧版 runtime 合同中不存在 reflection fallback 标记特性时,
/// 生成器会保留此前的整程序集回退行为,避免丢失不可见 handlers。
/// </summary>
[Test]
public async Task Skips_Generation_For_Unsupported_Handler_When_Fallback_Marker_Is_Unavailable()
{
const string source = """
using System;

View File

@ -16,6 +16,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
private const string IStreamRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IStreamRequestHandler`2";
private const string ICqrsHandlerRegistryMetadataName = $"{CqrsRuntimeNamespace}.ICqrsHandlerRegistry";
private const string CqrsReflectionFallbackAttributeMetadataName =
$"{CqrsRuntimeNamespace}.CqrsReflectionFallbackAttribute";
private const string CqrsHandlerRegistryAttributeMetadataName =
$"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute";
@ -28,8 +31,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
/// <inheritdoc />
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var generationEnabled = context.CompilationProvider
.Select(static (compilation, _) => HasRequiredTypes(compilation));
var generationEnvironment = context.CompilationProvider
.Select(static (compilation, _) => CreateGenerationEnvironment(compilation));
// Restrict semantic analysis to type declarations that can actually contribute implemented interfaces.
var handlerCandidates = context.SyntaxProvider.CreateSyntaxProvider(
@ -39,19 +42,24 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
.Collect();
context.RegisterSourceOutput(
generationEnabled.Combine(handlerCandidates),
generationEnvironment.Combine(handlerCandidates),
static (productionContext, pair) => Execute(productionContext, pair.Left, pair.Right));
}
private static bool HasRequiredTypes(Compilation compilation)
private static GenerationEnvironment CreateGenerationEnvironment(Compilation compilation)
{
return compilation.GetTypeByMetadataName(IRequestHandlerMetadataName) is not null &&
compilation.GetTypeByMetadataName(INotificationHandlerMetadataName) is not null &&
compilation.GetTypeByMetadataName(IStreamRequestHandlerMetadataName) is not null &&
compilation.GetTypeByMetadataName(ICqrsHandlerRegistryMetadataName) is not null &&
compilation.GetTypeByMetadataName(CqrsHandlerRegistryAttributeMetadataName) is not null &&
compilation.GetTypeByMetadataName(ILoggerMetadataName) is not null &&
compilation.GetTypeByMetadataName(IServiceCollectionMetadataName) is not null;
var generationEnabled = compilation.GetTypeByMetadataName(IRequestHandlerMetadataName) is not null &&
compilation.GetTypeByMetadataName(INotificationHandlerMetadataName) is not null &&
compilation.GetTypeByMetadataName(IStreamRequestHandlerMetadataName) is not null &&
compilation.GetTypeByMetadataName(ICqrsHandlerRegistryMetadataName) is not null &&
compilation.GetTypeByMetadataName(
CqrsHandlerRegistryAttributeMetadataName) is not null &&
compilation.GetTypeByMetadataName(ILoggerMetadataName) is not null &&
compilation.GetTypeByMetadataName(IServiceCollectionMetadataName) is not null;
return new GenerationEnvironment(
generationEnabled,
compilation.GetTypeByMetadataName(CqrsReflectionFallbackAttributeMetadataName) is not null);
}
private static bool IsHandlerCandidate(SyntaxNode node)
@ -108,21 +116,25 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
false);
}
private static void Execute(SourceProductionContext context, bool generationEnabled,
private static void Execute(SourceProductionContext context, GenerationEnvironment generationEnvironment,
ImmutableArray<HandlerCandidateAnalysis?> candidates)
{
if (!generationEnabled)
if (!generationEnvironment.GenerationEnabled)
return;
var registrations = CollectRegistrations(candidates, out var hasUnsupportedConcreteHandler);
// If the assembly contains handlers that generated code cannot legally reference
// (for example private nested handlers), keep the runtime on the reflection path
// so registration behavior remains complete instead of silently dropping handlers.
if (hasUnsupportedConcreteHandler || registrations.Count == 0)
if (registrations.Count == 0)
return;
context.AddSource(HintName, GenerateSource(registrations));
// If the runtime contract does not yet expose the reflection fallback marker,
// keep the previous all-or-nothing behavior so unsupported handlers are not silently dropped.
if (hasUnsupportedConcreteHandler && !generationEnvironment.SupportsReflectionFallbackMarker)
return;
context.AddSource(
HintName,
GenerateSource(registrations, hasUnsupportedConcreteHandler));
}
private static List<HandlerRegistrationSpec> CollectRegistrations(
@ -144,7 +156,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
if (candidate.Value.HasUnsupportedConcreteHandler)
{
hasUnsupportedConcreteHandler = true;
return [];
continue;
}
uniqueCandidates[candidate.Value.ImplementationTypeDisplayName] = candidate.Value;
@ -270,7 +282,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
return GetTypeSortKey(type).Replace("global::", string.Empty);
}
private static string GenerateSource(IReadOnlyList<HandlerRegistrationSpec> registrations)
private static string GenerateSource(
IReadOnlyList<HandlerRegistrationSpec> registrations,
bool emitReflectionFallbackAttribute)
{
var builder = new StringBuilder();
builder.AppendLine("// <auto-generated />");
@ -283,6 +297,13 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
builder.Append('.');
builder.Append(GeneratedTypeName);
builder.AppendLine("))]");
if (emitReflectionFallbackAttribute)
{
builder.Append("[assembly: global::");
builder.Append(CqrsRuntimeNamespace);
builder.AppendLine(".CqrsReflectionFallbackAttribute()]");
}
builder.AppendLine();
builder.Append("namespace ");
builder.Append(GeneratedNamespace);
@ -399,4 +420,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
}
}
}
private readonly record struct GenerationEnvironment(
bool GenerationEnabled,
bool SupportsReflectionFallbackMarker);
}