mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
feat(cqrs): 添加CQRS处理器自动注册功能
- 实现CqrsHandlerRegistrar类,支持扫描并注册CQRS请求/通知/流式处理器 - 添加源码生成注册器优先策略,减少冷启动时的反射开销 - 实现运行时反射扫描回退机制,确保处理器注册的完整性 - 添加CqrsReflectionFallbackAttribute特性,标记需要运行时补充扫描的程序集 - 创建完整的单元测试套件,验证处理器注册顺序与容错行为 - 实现CqrsHandlerRegistryGenerator源码生成器,自动生成处理器注册代码 - 添加详细的日志记录与诊断功能,便于调试注册过程 - 实现类型安全的处理器映射验证与重复注册检测机制
This commit is contained in:
parent
21627c0381
commit
391e3e9813
@ -190,11 +190,11 @@ internal sealed class CqrsHandlerRegistrarTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当生成注册器显式要求 reflection fallback 时,运行时会补扫剩余 handlers,
|
||||
/// 同时避免把已由生成注册器注册的映射重复写入服务集合。
|
||||
/// 验证当生成注册器提供精确 fallback 类型名时,运行时会定向补扫剩余 handlers,
|
||||
/// 而不是重新枚举整个程序集的类型列表。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterHandlers_Should_Combine_Generated_Registry_With_Reflection_Fallback_Without_Duplicates()
|
||||
public void RegisterHandlers_Should_Use_Targeted_Type_Lookups_For_Reflection_Fallback_Without_Duplicates()
|
||||
{
|
||||
var generatedAssembly = new Mock<Assembly>();
|
||||
generatedAssembly
|
||||
@ -205,14 +205,17 @@ internal sealed class CqrsHandlerRegistrarTests
|
||||
.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
|
||||
new CqrsReflectionFallbackAttribute(
|
||||
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!)
|
||||
]);
|
||||
generatedAssembly
|
||||
.Setup(static assembly => assembly.GetType(
|
||||
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!,
|
||||
false,
|
||||
false))
|
||||
.Returns(ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType);
|
||||
|
||||
var container = new MicrosoftDiContainer();
|
||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||
@ -231,6 +234,14 @@ internal sealed class CqrsHandlerRegistrarTests
|
||||
typeof(GeneratedRegistryNotificationHandler),
|
||||
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType
|
||||
]));
|
||||
|
||||
generatedAssembly.Verify(
|
||||
static assembly => assembly.GetType(
|
||||
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!,
|
||||
false,
|
||||
false),
|
||||
Times.Once);
|
||||
generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Never);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -11,4 +11,26 @@ namespace GFramework.Cqrs;
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
public sealed class CqrsReflectionFallbackAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化 <see cref="CqrsReflectionFallbackAttribute" />。
|
||||
/// </summary>
|
||||
/// <param name="fallbackHandlerTypeNames">
|
||||
/// 需要运行时补充反射注册的处理器类型全名。
|
||||
/// 当该清单为空时,运行时会回退到整程序集扫描,以兼容旧版 marker 语义。
|
||||
/// </param>
|
||||
public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fallbackHandlerTypeNames);
|
||||
|
||||
FallbackHandlerTypeNames = fallbackHandlerTypeNames
|
||||
.Where(static typeName => !string.IsNullOrWhiteSpace(typeName))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(static typeName => typeName, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取需要运行时补充反射注册的处理器类型全名集合。
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> FallbackHandlerTypeNames { get; }
|
||||
}
|
||||
|
||||
@ -33,10 +33,14 @@ internal static class CqrsHandlerRegistrar
|
||||
{
|
||||
var generatedRegistrationResult =
|
||||
TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger);
|
||||
if (generatedRegistrationResult == GeneratedRegistrationResult.FullyHandled)
|
||||
if (generatedRegistrationResult is { UsedGeneratedRegistry: true, RequiresReflectionFallback: false })
|
||||
continue;
|
||||
|
||||
RegisterAssemblyHandlers(container.GetServicesUnsafe, assembly, logger);
|
||||
RegisterAssemblyHandlers(
|
||||
container.GetServicesUnsafe,
|
||||
assembly,
|
||||
logger,
|
||||
generatedRegistrationResult.ReflectionFallbackTypeNames);
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,7 +70,7 @@ internal static class CqrsHandlerRegistrar
|
||||
.ToList();
|
||||
|
||||
if (registryTypes.Count == 0)
|
||||
return GeneratedRegistrationResult.NoGeneratedRegistry;
|
||||
return GeneratedRegistrationResult.NoGeneratedRegistry();
|
||||
|
||||
var registries = new List<ICqrsHandlerRegistry>(registryTypes.Count);
|
||||
foreach (var registryType in registryTypes)
|
||||
@ -75,21 +79,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 GeneratedRegistrationResult.NoGeneratedRegistry;
|
||||
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;
|
||||
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;
|
||||
return GeneratedRegistrationResult.NoGeneratedRegistry();
|
||||
}
|
||||
|
||||
registries.Add(registry);
|
||||
@ -102,14 +106,24 @@ internal static class CqrsHandlerRegistrar
|
||||
registry.Register(services, logger);
|
||||
}
|
||||
|
||||
if (RequiresReflectionFallback(assembly))
|
||||
var reflectionFallbackTypeNames = GetReflectionFallbackTypeNames(assembly);
|
||||
if (reflectionFallbackTypeNames is not null)
|
||||
{
|
||||
logger.Debug(
|
||||
$"Generated CQRS registry for assembly {assemblyName} requested reflection fallback for unsupported handlers.");
|
||||
return GeneratedRegistrationResult.RequiresReflectionFallback;
|
||||
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;
|
||||
return GeneratedRegistrationResult.FullyHandled();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
@ -117,16 +131,21 @@ 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 GeneratedRegistrationResult.NoGeneratedRegistry;
|
||||
return GeneratedRegistrationResult.NoGeneratedRegistry();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册单个程序集里的所有 CQRS 处理器映射。
|
||||
/// </summary>
|
||||
private static void RegisterAssemblyHandlers(IServiceCollection services, Assembly assembly, ILogger logger)
|
||||
private static void RegisterAssemblyHandlers(
|
||||
IServiceCollection services,
|
||||
Assembly assembly,
|
||||
ILogger logger,
|
||||
IReadOnlyList<string>? reflectionFallbackTypeNames)
|
||||
{
|
||||
foreach (var implementationType in GetLoadableTypes(assembly, logger).Where(IsConcreteHandlerType))
|
||||
foreach (var implementationType in GetCandidateHandlerTypes(assembly, logger, reflectionFallbackTypeNames)
|
||||
.Where(IsConcreteHandlerType))
|
||||
{
|
||||
var handlerInterfaces = implementationType
|
||||
.GetInterfaces()
|
||||
@ -155,6 +174,58 @@ internal static class CqrsHandlerRegistrar
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@ -221,11 +292,24 @@ internal static class CqrsHandlerRegistrar
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断生成注册器是否要求运行时继续补充反射扫描。
|
||||
/// 获取生成注册器要求运行时继续补充反射扫描的 handler 类型名清单。
|
||||
/// </summary>
|
||||
private static bool RequiresReflectionFallback(Assembly assembly)
|
||||
private static IReadOnlyList<string>? GetReflectionFallbackTypeNames(Assembly assembly)
|
||||
{
|
||||
return assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), inherit: false)?.Length > 0;
|
||||
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>
|
||||
@ -259,10 +343,36 @@ internal static class CqrsHandlerRegistrar
|
||||
return type.FullName ?? type.Name;
|
||||
}
|
||||
|
||||
private enum GeneratedRegistrationResult
|
||||
private readonly record struct GeneratedRegistrationResult(
|
||||
bool UsedGeneratedRegistry,
|
||||
bool RequiresReflectionFallback,
|
||||
IReadOnlyList<string>? ReflectionFallbackTypeNames)
|
||||
{
|
||||
NoGeneratedRegistry,
|
||||
FullyHandled,
|
||||
RequiresReflectionFallback
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,6 +65,7 @@ public class CqrsHandlerRegistryGeneratorTests
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
public sealed class CqrsReflectionFallbackAttribute : Attribute
|
||||
{
|
||||
public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { }
|
||||
}
|
||||
}
|
||||
|
||||
@ -180,6 +181,116 @@ public class CqrsHandlerRegistryGeneratorTests
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
public sealed class CqrsReflectionFallbackAttribute : Attribute
|
||||
{
|
||||
public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { }
|
||||
}
|
||||
}
|
||||
|
||||
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("TestApp.Container+HiddenHandler")]
|
||||
|
||||
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 仅支持旧版无参 fallback marker 时,生成器会退回旧语义,
|
||||
/// 只输出 marker 而不输出精确类型名。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Generates_Legacy_Fallback_Marker_When_Runtime_Does_Not_Support_Type_Name_List()
|
||||
{
|
||||
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
|
||||
{
|
||||
public CqrsReflectionFallbackAttribute() { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -59,7 +59,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
|
||||
return new GenerationEnvironment(
|
||||
generationEnabled,
|
||||
compilation.GetTypeByMetadataName(CqrsReflectionFallbackAttributeMetadataName) is not null);
|
||||
GetReflectionFallbackEmissionMode(
|
||||
compilation.GetTypeByMetadataName(CqrsReflectionFallbackAttributeMetadataName)));
|
||||
}
|
||||
|
||||
private static bool IsHandlerCandidate(SyntaxNode node)
|
||||
@ -96,7 +97,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
return new HandlerCandidateAnalysis(
|
||||
implementationTypeDisplayName,
|
||||
ImmutableArray<HandlerRegistrationSpec>.Empty,
|
||||
true);
|
||||
true,
|
||||
GetReflectionFallbackTypeName(type));
|
||||
}
|
||||
|
||||
var implementationLogName = GetLogDisplayName(type);
|
||||
@ -113,7 +115,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
return new HandlerCandidateAnalysis(
|
||||
implementationTypeDisplayName,
|
||||
registrations.MoveToImmutable(),
|
||||
false);
|
||||
false,
|
||||
null);
|
||||
}
|
||||
|
||||
private static void Execute(SourceProductionContext context, GenerationEnvironment generationEnvironment,
|
||||
@ -122,27 +125,37 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
if (!generationEnvironment.GenerationEnabled)
|
||||
return;
|
||||
|
||||
var registrations = CollectRegistrations(candidates, out var hasUnsupportedConcreteHandler);
|
||||
var registrations = CollectRegistrations(
|
||||
candidates,
|
||||
out var hasUnsupportedConcreteHandler,
|
||||
out var reflectionFallbackTypeNames);
|
||||
|
||||
if (registrations.Count == 0)
|
||||
return;
|
||||
|
||||
// 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)
|
||||
if (hasUnsupportedConcreteHandler &&
|
||||
generationEnvironment.ReflectionFallbackEmissionMode == ReflectionFallbackEmissionMode.Disabled)
|
||||
return;
|
||||
|
||||
context.AddSource(
|
||||
HintName,
|
||||
GenerateSource(registrations, hasUnsupportedConcreteHandler));
|
||||
GenerateSource(
|
||||
registrations,
|
||||
hasUnsupportedConcreteHandler,
|
||||
generationEnvironment.ReflectionFallbackEmissionMode,
|
||||
reflectionFallbackTypeNames));
|
||||
}
|
||||
|
||||
private static List<HandlerRegistrationSpec> CollectRegistrations(
|
||||
ImmutableArray<HandlerCandidateAnalysis?> candidates,
|
||||
out bool hasUnsupportedConcreteHandler)
|
||||
out bool hasUnsupportedConcreteHandler,
|
||||
out IReadOnlyList<string> reflectionFallbackTypeNames)
|
||||
{
|
||||
var registrations = new List<HandlerRegistrationSpec>();
|
||||
hasUnsupportedConcreteHandler = false;
|
||||
var fallbackTypeNames = new SortedSet<string>(StringComparer.Ordinal);
|
||||
|
||||
// Partial declarations surface the same symbol through multiple syntax nodes.
|
||||
// Collapse them by implementation type so generated registrations stay stable and duplicate-free.
|
||||
@ -156,6 +169,13 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
if (candidate.Value.HasUnsupportedConcreteHandler)
|
||||
{
|
||||
hasUnsupportedConcreteHandler = true;
|
||||
var reflectionFallbackTypeName = candidate.Value.ReflectionFallbackTypeName;
|
||||
if (reflectionFallbackTypeName is not null &&
|
||||
!string.IsNullOrWhiteSpace(reflectionFallbackTypeName))
|
||||
{
|
||||
fallbackTypeNames.Add(reflectionFallbackTypeName);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -178,9 +198,30 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
: StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName);
|
||||
});
|
||||
|
||||
reflectionFallbackTypeNames = fallbackTypeNames.ToArray();
|
||||
return registrations;
|
||||
}
|
||||
|
||||
private static ReflectionFallbackEmissionMode GetReflectionFallbackEmissionMode(INamedTypeSymbol? attributeType)
|
||||
{
|
||||
if (attributeType is null)
|
||||
return ReflectionFallbackEmissionMode.Disabled;
|
||||
|
||||
foreach (var constructor in attributeType.InstanceConstructors)
|
||||
{
|
||||
if (constructor.Parameters.Length != 1)
|
||||
continue;
|
||||
|
||||
if (constructor.Parameters[0].Type is IArrayTypeSymbol arrayType &&
|
||||
arrayType.ElementType.SpecialType == SpecialType.System_String)
|
||||
{
|
||||
return ReflectionFallbackEmissionMode.PreciseTypeNames;
|
||||
}
|
||||
}
|
||||
|
||||
return ReflectionFallbackEmissionMode.MarkerOnly;
|
||||
}
|
||||
|
||||
private static bool IsConcreteHandlerType(INamedTypeSymbol type)
|
||||
{
|
||||
return type.TypeKind is TypeKind.Class or TypeKind.Struct &&
|
||||
@ -272,6 +313,34 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string GetReflectionFallbackTypeName(INamedTypeSymbol type)
|
||||
{
|
||||
var nestedTypes = new Stack<string>();
|
||||
for (var current = type; current is not null; current = current.ContainingType)
|
||||
{
|
||||
nestedTypes.Push(current.MetadataName);
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
if (!type.ContainingNamespace.IsGlobalNamespace)
|
||||
{
|
||||
builder.Append(type.ContainingNamespace.ToDisplayString());
|
||||
builder.Append('.');
|
||||
}
|
||||
|
||||
var isFirstType = true;
|
||||
while (nestedTypes.Count > 0)
|
||||
{
|
||||
if (!isFirstType)
|
||||
builder.Append('+');
|
||||
|
||||
builder.Append(nestedTypes.Pop());
|
||||
isFirstType = false;
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string GetTypeSortKey(ITypeSymbol type)
|
||||
{
|
||||
return type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
@ -284,7 +353,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
|
||||
private static string GenerateSource(
|
||||
IReadOnlyList<HandlerRegistrationSpec> registrations,
|
||||
bool emitReflectionFallbackAttribute)
|
||||
bool emitReflectionFallbackAttribute,
|
||||
ReflectionFallbackEmissionMode reflectionFallbackEmissionMode,
|
||||
IReadOnlyList<string> reflectionFallbackTypeNames)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("// <auto-generated />");
|
||||
@ -297,11 +368,10 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
builder.Append('.');
|
||||
builder.Append(GeneratedTypeName);
|
||||
builder.AppendLine("))]");
|
||||
if (emitReflectionFallbackAttribute)
|
||||
if (emitReflectionFallbackAttribute &&
|
||||
reflectionFallbackEmissionMode != ReflectionFallbackEmissionMode.Disabled)
|
||||
{
|
||||
builder.Append("[assembly: global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.AppendLine(".CqrsReflectionFallbackAttribute()]");
|
||||
AppendReflectionFallbackAttribute(builder, reflectionFallbackEmissionMode, reflectionFallbackTypeNames);
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
@ -349,6 +419,36 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static void AppendReflectionFallbackAttribute(
|
||||
StringBuilder builder,
|
||||
ReflectionFallbackEmissionMode reflectionFallbackEmissionMode,
|
||||
IReadOnlyList<string> reflectionFallbackTypeNames)
|
||||
{
|
||||
builder.Append("[assembly: global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.Append(".CqrsReflectionFallbackAttribute");
|
||||
|
||||
if (reflectionFallbackEmissionMode == ReflectionFallbackEmissionMode.PreciseTypeNames &&
|
||||
reflectionFallbackTypeNames.Count > 0)
|
||||
{
|
||||
builder.Append('(');
|
||||
for (var index = 0; index < reflectionFallbackTypeNames.Count; index++)
|
||||
{
|
||||
if (index > 0)
|
||||
builder.Append(", ");
|
||||
|
||||
builder.Append('"');
|
||||
builder.Append(EscapeStringLiteral(reflectionFallbackTypeNames[index]));
|
||||
builder.Append('"');
|
||||
}
|
||||
|
||||
builder.AppendLine(")]");
|
||||
return;
|
||||
}
|
||||
|
||||
builder.AppendLine("()]");
|
||||
}
|
||||
|
||||
private static string EscapeStringLiteral(string value)
|
||||
{
|
||||
return value.Replace("\\", "\\\\")
|
||||
@ -368,11 +468,13 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
public HandlerCandidateAnalysis(
|
||||
string implementationTypeDisplayName,
|
||||
ImmutableArray<HandlerRegistrationSpec> registrations,
|
||||
bool hasUnsupportedConcreteHandler)
|
||||
bool hasUnsupportedConcreteHandler,
|
||||
string? reflectionFallbackTypeName)
|
||||
{
|
||||
ImplementationTypeDisplayName = implementationTypeDisplayName;
|
||||
Registrations = registrations;
|
||||
HasUnsupportedConcreteHandler = hasUnsupportedConcreteHandler;
|
||||
ReflectionFallbackTypeName = reflectionFallbackTypeName;
|
||||
}
|
||||
|
||||
public string ImplementationTypeDisplayName { get; }
|
||||
@ -381,11 +483,15 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
|
||||
public bool HasUnsupportedConcreteHandler { get; }
|
||||
|
||||
public string? ReflectionFallbackTypeName { get; }
|
||||
|
||||
public bool Equals(HandlerCandidateAnalysis other)
|
||||
{
|
||||
if (!string.Equals(ImplementationTypeDisplayName, other.ImplementationTypeDisplayName,
|
||||
StringComparison.Ordinal) ||
|
||||
HasUnsupportedConcreteHandler != other.HasUnsupportedConcreteHandler ||
|
||||
!string.Equals(ReflectionFallbackTypeName, other.ReflectionFallbackTypeName,
|
||||
StringComparison.Ordinal) ||
|
||||
Registrations.Length != other.Registrations.Length)
|
||||
{
|
||||
return false;
|
||||
@ -411,6 +517,10 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
{
|
||||
var hashCode = StringComparer.Ordinal.GetHashCode(ImplementationTypeDisplayName);
|
||||
hashCode = (hashCode * 397) ^ HasUnsupportedConcreteHandler.GetHashCode();
|
||||
hashCode = (hashCode * 397) ^
|
||||
(ReflectionFallbackTypeName is null
|
||||
? 0
|
||||
: StringComparer.Ordinal.GetHashCode(ReflectionFallbackTypeName));
|
||||
foreach (var registration in Registrations)
|
||||
{
|
||||
hashCode = (hashCode * 397) ^ registration.GetHashCode();
|
||||
@ -423,5 +533,12 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
|
||||
private readonly record struct GenerationEnvironment(
|
||||
bool GenerationEnabled,
|
||||
bool SupportsReflectionFallbackMarker);
|
||||
ReflectionFallbackEmissionMode ReflectionFallbackEmissionMode);
|
||||
|
||||
private enum ReflectionFallbackEmissionMode
|
||||
{
|
||||
Disabled,
|
||||
MarkerOnly,
|
||||
PreciseTypeNames
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user