From 391e3e98138d5023192857c37eca201a5fee78b3 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 16 Apr 2026 11:11:29 +0800
Subject: [PATCH] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0CQRS=E5=A4=84?=
=?UTF-8?q?=E7=90=86=E5=99=A8=E8=87=AA=E5=8A=A8=E6=B3=A8=E5=86=8C=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现CqrsHandlerRegistrar类,支持扫描并注册CQRS请求/通知/流式处理器
- 添加源码生成注册器优先策略,减少冷启动时的反射开销
- 实现运行时反射扫描回退机制,确保处理器注册的完整性
- 添加CqrsReflectionFallbackAttribute特性,标记需要运行时补充扫描的程序集
- 创建完整的单元测试套件,验证处理器注册顺序与容错行为
- 实现CqrsHandlerRegistryGenerator源码生成器,自动生成处理器注册代码
- 添加详细的日志记录与诊断功能,便于调试注册过程
- 实现类型安全的处理器映射验证与重复注册检测机制
---
.../Cqrs/CqrsHandlerRegistrarTests.cs | 27 +++-
.../CqrsReflectionFallbackAttribute.cs | 22 +++
.../Internal/CqrsHandlerRegistrar.cs | 152 +++++++++++++++---
.../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 111 +++++++++++++
.../Cqrs/CqrsHandlerRegistryGenerator.cs | 145 +++++++++++++++--
5 files changed, 414 insertions(+), 43 deletions(-)
diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs
index 318f6d21..95afa92f 100644
--- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs
+++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs
@@ -190,11 +190,11 @@ internal sealed class CqrsHandlerRegistrarTests
}
///
- /// 验证当生成注册器显式要求 reflection fallback 时,运行时会补扫剩余 handlers,
- /// 同时避免把已由生成注册器注册的映射重复写入服务集合。
+ /// 验证当生成注册器提供精确 fallback 类型名时,运行时会定向补扫剩余 handlers,
+ /// 而不是重新枚举整个程序集的类型列表。
///
[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();
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);
}
}
diff --git a/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs b/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs
index f18a7344..9d3c21bf 100644
--- a/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs
+++ b/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs
@@ -11,4 +11,26 @@ namespace GFramework.Cqrs;
[AttributeUsage(AttributeTargets.Assembly)]
public sealed class CqrsReflectionFallbackAttribute : Attribute
{
+ ///
+ /// 初始化 。
+ ///
+ ///
+ /// 需要运行时补充反射注册的处理器类型全名。
+ /// 当该清单为空时,运行时会回退到整程序集扫描,以兼容旧版 marker 语义。
+ ///
+ 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();
+ }
+
+ ///
+ /// 获取需要运行时补充反射注册的处理器类型全名集合。
+ ///
+ public IReadOnlyList FallbackHandlerTypeNames { get; }
}
diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs
index 867c6887..968424b7 100644
--- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs
+++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs
@@ -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(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();
}
}
///
/// 注册单个程序集里的所有 CQRS 处理器映射。
///
- private static void RegisterAssemblyHandlers(IServiceCollection services, Assembly assembly, ILogger logger)
+ private static void RegisterAssemblyHandlers(
+ IServiceCollection services,
+ Assembly assembly,
+ ILogger logger,
+ IReadOnlyList? 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
}
}
+ ///
+ /// 根据生成器提供的 fallback 清单或整程序集扫描结果,获取本轮要注册的候选处理器类型。
+ ///
+ private static IReadOnlyList GetCandidateHandlerTypes(
+ Assembly assembly,
+ ILogger logger,
+ IReadOnlyList? reflectionFallbackTypeNames)
+ {
+ return reflectionFallbackTypeNames is { Count: > 0 }
+ ? GetNamedFallbackTypes(assembly, reflectionFallbackTypeNames, logger)
+ : GetLoadableTypes(assembly, logger);
+ }
+
+ ///
+ /// 根据生成器记录的类型全名,精确解析仍需运行时补充注册的处理器类型。
+ ///
+ private static IReadOnlyList GetNamedFallbackTypes(
+ Assembly assembly,
+ IReadOnlyList reflectionFallbackTypeNames,
+ ILogger logger)
+ {
+ var assemblyName = GetAssemblySortKey(assembly);
+ var resolvedTypes = new List(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();
+ }
+
///
/// 安全获取程序集中的可加载类型,并在部分类型加载失败时保留其余处理器注册能力。
///
@@ -221,11 +292,24 @@ internal static class CqrsHandlerRegistrar
}
///
- /// 判断生成注册器是否要求运行时继续补充反射扫描。
+ /// 获取生成注册器要求运行时继续补充反射扫描的 handler 类型名清单。
///
- private static bool RequiresReflectionFallback(Assembly assembly)
+ private static IReadOnlyList? GetReflectionFallbackTypeNames(Assembly assembly)
{
- return assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), inherit: false)?.Length > 0;
+ var fallbackAttributes = assembly
+ .GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), inherit: false)
+ .OfType()
+ .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();
}
///
@@ -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? 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 reflectionFallbackTypeNames)
+ {
+ ArgumentNullException.ThrowIfNull(reflectionFallbackTypeNames);
+
+ return new GeneratedRegistrationResult(
+ UsedGeneratedRegistry: true,
+ RequiresReflectionFallback: true,
+ ReflectionFallbackTypeNames: reflectionFallbackTypeNames);
+ }
}
}
diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs
index 0cd91844..e1ec1546 100644
--- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs
+++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs
@@ -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;
+
+ public sealed class Container
+ {
+ private sealed record HiddenRequest() : IRequest;
+
+ private sealed class HiddenHandler : IRequestHandler { }
+ }
+
+ public sealed class VisibleHandler : IRequestHandler { }
+ }
+ """;
+
+ const string expected = """
+ //
+ #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),
+ typeof(global::TestApp.VisibleHandler));
+ logger.Debug("Registered CQRS handler TestApp.VisibleHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler.");
+ }
+ }
+
+ """;
+
+ await GeneratorTest.RunAsync(
+ source,
+ ("CqrsHandlerRegistry.g.cs", expected));
+ }
+
+ ///
+ /// 验证当 runtime 仅支持旧版无参 fallback marker 时,生成器会退回旧语义,
+ /// 只输出 marker 而不输出精确类型名。
+ ///
+ [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 { }
+ public interface INotification { }
+ public interface IStreamRequest { }
+
+ public interface IRequestHandler where TRequest : IRequest { }
+ public interface INotificationHandler where TNotification : INotification { }
+ public interface IStreamRequestHandler where TRequest : IStreamRequest { }
+ }
+
+ 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() { }
}
}
diff --git a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs
index 80561248..1e260e32 100644
--- a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs
+++ b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs
@@ -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.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 CollectRegistrations(
ImmutableArray candidates,
- out bool hasUnsupportedConcreteHandler)
+ out bool hasUnsupportedConcreteHandler,
+ out IReadOnlyList reflectionFallbackTypeNames)
{
var registrations = new List();
hasUnsupportedConcreteHandler = false;
+ var fallbackTypeNames = new SortedSet(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();
+ 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 registrations,
- bool emitReflectionFallbackAttribute)
+ bool emitReflectionFallbackAttribute,
+ ReflectionFallbackEmissionMode reflectionFallbackEmissionMode,
+ IReadOnlyList reflectionFallbackTypeNames)
{
var builder = new StringBuilder();
builder.AppendLine("// ");
@@ -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 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 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
+ }
}