mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
feat(cqrs): 添加 CQRS 处理器注册器和源码生成器
- 实现 CqrsHandlerRegistrar 类用于扫描并注册 CQRS 处理器 - 添加源码生成器自动生成 CQRS 处理器注册器减少反射开销 - 实现运行时回退机制在生成注册器不可用时使用反射扫描 - 添加完整的单元测试验证处理器注册顺序和容错行为 - 支持请求、通知和流式处理器的自动注册功能 - 实现稳定的处理器注册顺序保证跨环境一致性 - 添加详细的诊断日志记录注册过程和异常情况
This commit is contained in:
parent
a7604de804
commit
bc9336428e
@ -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}.");
|
||||
}
|
||||
}
|
||||
|
||||
14
GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs
Normal file
14
GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace GFramework.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 标记程序集中的 CQRS 生成注册器仍需要运行时补充反射扫描。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该特性通常由源码生成器自动添加到消费端程序集。
|
||||
/// 当生成器只能安全生成部分 handler 映射时,运行时会先执行生成注册器,再补一次带去重的反射扫描,
|
||||
/// 以覆盖那些生成代码无法直接引用的 handler 类型。
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
public sealed class CqrsReflectionFallbackAttribute : Attribute
|
||||
{
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user