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>
|
/// <summary>
|
||||||
/// 验证当生成注册器显式要求 reflection fallback 时,运行时会补扫剩余 handlers,
|
/// 验证当生成注册器提供精确 fallback 类型名时,运行时会定向补扫剩余 handlers,
|
||||||
/// 同时避免把已由生成注册器注册的映射重复写入服务集合。
|
/// 而不是重新枚举整个程序集的类型列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[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>();
|
var generatedAssembly = new Mock<Assembly>();
|
||||||
generatedAssembly
|
generatedAssembly
|
||||||
@ -205,14 +205,17 @@ internal sealed class CqrsHandlerRegistrarTests
|
|||||||
.Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]);
|
.Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]);
|
||||||
generatedAssembly
|
generatedAssembly
|
||||||
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false))
|
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false))
|
||||||
.Returns([new CqrsReflectionFallbackAttribute()]);
|
|
||||||
generatedAssembly
|
|
||||||
.Setup(static assembly => assembly.GetTypes())
|
|
||||||
.Returns(
|
.Returns(
|
||||||
[
|
[
|
||||||
typeof(GeneratedRegistryNotificationHandler),
|
new CqrsReflectionFallbackAttribute(
|
||||||
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType
|
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!)
|
||||||
]);
|
]);
|
||||||
|
generatedAssembly
|
||||||
|
.Setup(static assembly => assembly.GetType(
|
||||||
|
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!,
|
||||||
|
false,
|
||||||
|
false))
|
||||||
|
.Returns(ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType);
|
||||||
|
|
||||||
var container = new MicrosoftDiContainer();
|
var container = new MicrosoftDiContainer();
|
||||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||||
@ -231,6 +234,14 @@ internal sealed class CqrsHandlerRegistrarTests
|
|||||||
typeof(GeneratedRegistryNotificationHandler),
|
typeof(GeneratedRegistryNotificationHandler),
|
||||||
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType
|
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)]
|
[AttributeUsage(AttributeTargets.Assembly)]
|
||||||
public sealed class CqrsReflectionFallbackAttribute : Attribute
|
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 =
|
var generatedRegistrationResult =
|
||||||
TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger);
|
TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger);
|
||||||
if (generatedRegistrationResult == GeneratedRegistrationResult.FullyHandled)
|
if (generatedRegistrationResult is { UsedGeneratedRegistry: true, RequiresReflectionFallback: false })
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
RegisterAssemblyHandlers(container.GetServicesUnsafe, assembly, logger);
|
RegisterAssemblyHandlers(
|
||||||
|
container.GetServicesUnsafe,
|
||||||
|
assembly,
|
||||||
|
logger,
|
||||||
|
generatedRegistrationResult.ReflectionFallbackTypeNames);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +70,7 @@ internal static class CqrsHandlerRegistrar
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (registryTypes.Count == 0)
|
if (registryTypes.Count == 0)
|
||||||
return GeneratedRegistrationResult.NoGeneratedRegistry;
|
return GeneratedRegistrationResult.NoGeneratedRegistry();
|
||||||
|
|
||||||
var registries = new List<ICqrsHandlerRegistry>(registryTypes.Count);
|
var registries = new List<ICqrsHandlerRegistry>(registryTypes.Count);
|
||||||
foreach (var registryType in registryTypes)
|
foreach (var registryType in registryTypes)
|
||||||
@ -75,21 +79,21 @@ internal static class CqrsHandlerRegistrar
|
|||||||
{
|
{
|
||||||
logger.Warn(
|
logger.Warn(
|
||||||
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}.");
|
$"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)
|
if (registryType.IsAbstract)
|
||||||
{
|
{
|
||||||
logger.Warn(
|
logger.Warn(
|
||||||
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract.");
|
$"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)
|
if (Activator.CreateInstance(registryType, nonPublic: true) is not ICqrsHandlerRegistry registry)
|
||||||
{
|
{
|
||||||
logger.Warn(
|
logger.Warn(
|
||||||
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it could not be instantiated.");
|
$"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);
|
registries.Add(registry);
|
||||||
@ -102,14 +106,24 @@ internal static class CqrsHandlerRegistrar
|
|||||||
registry.Register(services, logger);
|
registry.Register(services, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RequiresReflectionFallback(assembly))
|
var reflectionFallbackTypeNames = GetReflectionFallbackTypeNames(assembly);
|
||||||
|
if (reflectionFallbackTypeNames is not null)
|
||||||
{
|
{
|
||||||
logger.Debug(
|
if (reflectionFallbackTypeNames.Count > 0)
|
||||||
$"Generated CQRS registry for assembly {assemblyName} requested reflection fallback for unsupported handlers.");
|
{
|
||||||
return GeneratedRegistrationResult.RequiresReflectionFallback;
|
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)
|
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.");
|
$"Generated CQRS handler registry discovery failed for assembly {assemblyName}. Falling back to reflection scan.");
|
||||||
logger.Warn(
|
logger.Warn(
|
||||||
$"Failed to use generated CQRS handler registry for assembly {assemblyName}: {exception.Message}");
|
$"Failed to use generated CQRS handler registry for assembly {assemblyName}: {exception.Message}");
|
||||||
return GeneratedRegistrationResult.NoGeneratedRegistry;
|
return GeneratedRegistrationResult.NoGeneratedRegistry();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册单个程序集里的所有 CQRS 处理器映射。
|
/// 注册单个程序集里的所有 CQRS 处理器映射。
|
||||||
/// </summary>
|
/// </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
|
var handlerInterfaces = implementationType
|
||||||
.GetInterfaces()
|
.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>
|
||||||
/// 安全获取程序集中的可加载类型,并在部分类型加载失败时保留其余处理器注册能力。
|
/// 安全获取程序集中的可加载类型,并在部分类型加载失败时保留其余处理器注册能力。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -221,11 +292,24 @@ internal static class CqrsHandlerRegistrar
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 判断生成注册器是否要求运行时继续补充反射扫描。
|
/// 获取生成注册器要求运行时继续补充反射扫描的 handler 类型名清单。
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
@ -259,10 +343,36 @@ internal static class CqrsHandlerRegistrar
|
|||||||
return type.FullName ?? type.Name;
|
return type.FullName ?? type.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum GeneratedRegistrationResult
|
private readonly record struct GeneratedRegistrationResult(
|
||||||
|
bool UsedGeneratedRegistry,
|
||||||
|
bool RequiresReflectionFallback,
|
||||||
|
IReadOnlyList<string>? ReflectionFallbackTypeNames)
|
||||||
{
|
{
|
||||||
NoGeneratedRegistry,
|
public static GeneratedRegistrationResult NoGeneratedRegistry()
|
||||||
FullyHandled,
|
{
|
||||||
RequiresReflectionFallback
|
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)]
|
[AttributeUsage(AttributeTargets.Assembly)]
|
||||||
public sealed class CqrsReflectionFallbackAttribute : Attribute
|
public sealed class CqrsReflectionFallbackAttribute : Attribute
|
||||||
{
|
{
|
||||||
|
public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,6 +181,116 @@ public class CqrsHandlerRegistryGeneratorTests
|
|||||||
[AttributeUsage(AttributeTargets.Assembly)]
|
[AttributeUsage(AttributeTargets.Assembly)]
|
||||||
public sealed class CqrsReflectionFallbackAttribute : Attribute
|
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(
|
return new GenerationEnvironment(
|
||||||
generationEnabled,
|
generationEnabled,
|
||||||
compilation.GetTypeByMetadataName(CqrsReflectionFallbackAttributeMetadataName) is not null);
|
GetReflectionFallbackEmissionMode(
|
||||||
|
compilation.GetTypeByMetadataName(CqrsReflectionFallbackAttributeMetadataName)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsHandlerCandidate(SyntaxNode node)
|
private static bool IsHandlerCandidate(SyntaxNode node)
|
||||||
@ -96,7 +97,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
return new HandlerCandidateAnalysis(
|
return new HandlerCandidateAnalysis(
|
||||||
implementationTypeDisplayName,
|
implementationTypeDisplayName,
|
||||||
ImmutableArray<HandlerRegistrationSpec>.Empty,
|
ImmutableArray<HandlerRegistrationSpec>.Empty,
|
||||||
true);
|
true,
|
||||||
|
GetReflectionFallbackTypeName(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
var implementationLogName = GetLogDisplayName(type);
|
var implementationLogName = GetLogDisplayName(type);
|
||||||
@ -113,7 +115,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
return new HandlerCandidateAnalysis(
|
return new HandlerCandidateAnalysis(
|
||||||
implementationTypeDisplayName,
|
implementationTypeDisplayName,
|
||||||
registrations.MoveToImmutable(),
|
registrations.MoveToImmutable(),
|
||||||
false);
|
false,
|
||||||
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Execute(SourceProductionContext context, GenerationEnvironment generationEnvironment,
|
private static void Execute(SourceProductionContext context, GenerationEnvironment generationEnvironment,
|
||||||
@ -122,27 +125,37 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
if (!generationEnvironment.GenerationEnabled)
|
if (!generationEnvironment.GenerationEnabled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var registrations = CollectRegistrations(candidates, out var hasUnsupportedConcreteHandler);
|
var registrations = CollectRegistrations(
|
||||||
|
candidates,
|
||||||
|
out var hasUnsupportedConcreteHandler,
|
||||||
|
out var reflectionFallbackTypeNames);
|
||||||
|
|
||||||
if (registrations.Count == 0)
|
if (registrations.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// If the runtime contract does not yet expose the reflection fallback marker,
|
// 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.
|
// 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;
|
return;
|
||||||
|
|
||||||
context.AddSource(
|
context.AddSource(
|
||||||
HintName,
|
HintName,
|
||||||
GenerateSource(registrations, hasUnsupportedConcreteHandler));
|
GenerateSource(
|
||||||
|
registrations,
|
||||||
|
hasUnsupportedConcreteHandler,
|
||||||
|
generationEnvironment.ReflectionFallbackEmissionMode,
|
||||||
|
reflectionFallbackTypeNames));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<HandlerRegistrationSpec> CollectRegistrations(
|
private static List<HandlerRegistrationSpec> CollectRegistrations(
|
||||||
ImmutableArray<HandlerCandidateAnalysis?> candidates,
|
ImmutableArray<HandlerCandidateAnalysis?> candidates,
|
||||||
out bool hasUnsupportedConcreteHandler)
|
out bool hasUnsupportedConcreteHandler,
|
||||||
|
out IReadOnlyList<string> reflectionFallbackTypeNames)
|
||||||
{
|
{
|
||||||
var registrations = new List<HandlerRegistrationSpec>();
|
var registrations = new List<HandlerRegistrationSpec>();
|
||||||
hasUnsupportedConcreteHandler = false;
|
hasUnsupportedConcreteHandler = false;
|
||||||
|
var fallbackTypeNames = new SortedSet<string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
// Partial declarations surface the same symbol through multiple syntax nodes.
|
// Partial declarations surface the same symbol through multiple syntax nodes.
|
||||||
// Collapse them by implementation type so generated registrations stay stable and duplicate-free.
|
// 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)
|
if (candidate.Value.HasUnsupportedConcreteHandler)
|
||||||
{
|
{
|
||||||
hasUnsupportedConcreteHandler = true;
|
hasUnsupportedConcreteHandler = true;
|
||||||
|
var reflectionFallbackTypeName = candidate.Value.ReflectionFallbackTypeName;
|
||||||
|
if (reflectionFallbackTypeName is not null &&
|
||||||
|
!string.IsNullOrWhiteSpace(reflectionFallbackTypeName))
|
||||||
|
{
|
||||||
|
fallbackTypeNames.Add(reflectionFallbackTypeName);
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,9 +198,30 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
: StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName);
|
: StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
reflectionFallbackTypeNames = fallbackTypeNames.ToArray();
|
||||||
return registrations;
|
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)
|
private static bool IsConcreteHandlerType(INamedTypeSymbol type)
|
||||||
{
|
{
|
||||||
return type.TypeKind is TypeKind.Class or TypeKind.Struct &&
|
return type.TypeKind is TypeKind.Class or TypeKind.Struct &&
|
||||||
@ -272,6 +313,34 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
return builder.ToString();
|
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)
|
private static string GetTypeSortKey(ITypeSymbol type)
|
||||||
{
|
{
|
||||||
return type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
return type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||||
@ -284,7 +353,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
|
|
||||||
private static string GenerateSource(
|
private static string GenerateSource(
|
||||||
IReadOnlyList<HandlerRegistrationSpec> registrations,
|
IReadOnlyList<HandlerRegistrationSpec> registrations,
|
||||||
bool emitReflectionFallbackAttribute)
|
bool emitReflectionFallbackAttribute,
|
||||||
|
ReflectionFallbackEmissionMode reflectionFallbackEmissionMode,
|
||||||
|
IReadOnlyList<string> reflectionFallbackTypeNames)
|
||||||
{
|
{
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
builder.AppendLine("// <auto-generated />");
|
builder.AppendLine("// <auto-generated />");
|
||||||
@ -297,11 +368,10 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
builder.Append('.');
|
builder.Append('.');
|
||||||
builder.Append(GeneratedTypeName);
|
builder.Append(GeneratedTypeName);
|
||||||
builder.AppendLine("))]");
|
builder.AppendLine("))]");
|
||||||
if (emitReflectionFallbackAttribute)
|
if (emitReflectionFallbackAttribute &&
|
||||||
|
reflectionFallbackEmissionMode != ReflectionFallbackEmissionMode.Disabled)
|
||||||
{
|
{
|
||||||
builder.Append("[assembly: global::");
|
AppendReflectionFallbackAttribute(builder, reflectionFallbackEmissionMode, reflectionFallbackTypeNames);
|
||||||
builder.Append(CqrsRuntimeNamespace);
|
|
||||||
builder.AppendLine(".CqrsReflectionFallbackAttribute()]");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.AppendLine();
|
builder.AppendLine();
|
||||||
@ -349,6 +419,36 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
return builder.ToString();
|
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)
|
private static string EscapeStringLiteral(string value)
|
||||||
{
|
{
|
||||||
return value.Replace("\\", "\\\\")
|
return value.Replace("\\", "\\\\")
|
||||||
@ -368,11 +468,13 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
public HandlerCandidateAnalysis(
|
public HandlerCandidateAnalysis(
|
||||||
string implementationTypeDisplayName,
|
string implementationTypeDisplayName,
|
||||||
ImmutableArray<HandlerRegistrationSpec> registrations,
|
ImmutableArray<HandlerRegistrationSpec> registrations,
|
||||||
bool hasUnsupportedConcreteHandler)
|
bool hasUnsupportedConcreteHandler,
|
||||||
|
string? reflectionFallbackTypeName)
|
||||||
{
|
{
|
||||||
ImplementationTypeDisplayName = implementationTypeDisplayName;
|
ImplementationTypeDisplayName = implementationTypeDisplayName;
|
||||||
Registrations = registrations;
|
Registrations = registrations;
|
||||||
HasUnsupportedConcreteHandler = hasUnsupportedConcreteHandler;
|
HasUnsupportedConcreteHandler = hasUnsupportedConcreteHandler;
|
||||||
|
ReflectionFallbackTypeName = reflectionFallbackTypeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ImplementationTypeDisplayName { get; }
|
public string ImplementationTypeDisplayName { get; }
|
||||||
@ -381,11 +483,15 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
|
|
||||||
public bool HasUnsupportedConcreteHandler { get; }
|
public bool HasUnsupportedConcreteHandler { get; }
|
||||||
|
|
||||||
|
public string? ReflectionFallbackTypeName { get; }
|
||||||
|
|
||||||
public bool Equals(HandlerCandidateAnalysis other)
|
public bool Equals(HandlerCandidateAnalysis other)
|
||||||
{
|
{
|
||||||
if (!string.Equals(ImplementationTypeDisplayName, other.ImplementationTypeDisplayName,
|
if (!string.Equals(ImplementationTypeDisplayName, other.ImplementationTypeDisplayName,
|
||||||
StringComparison.Ordinal) ||
|
StringComparison.Ordinal) ||
|
||||||
HasUnsupportedConcreteHandler != other.HasUnsupportedConcreteHandler ||
|
HasUnsupportedConcreteHandler != other.HasUnsupportedConcreteHandler ||
|
||||||
|
!string.Equals(ReflectionFallbackTypeName, other.ReflectionFallbackTypeName,
|
||||||
|
StringComparison.Ordinal) ||
|
||||||
Registrations.Length != other.Registrations.Length)
|
Registrations.Length != other.Registrations.Length)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@ -411,6 +517,10 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
{
|
{
|
||||||
var hashCode = StringComparer.Ordinal.GetHashCode(ImplementationTypeDisplayName);
|
var hashCode = StringComparer.Ordinal.GetHashCode(ImplementationTypeDisplayName);
|
||||||
hashCode = (hashCode * 397) ^ HasUnsupportedConcreteHandler.GetHashCode();
|
hashCode = (hashCode * 397) ^ HasUnsupportedConcreteHandler.GetHashCode();
|
||||||
|
hashCode = (hashCode * 397) ^
|
||||||
|
(ReflectionFallbackTypeName is null
|
||||||
|
? 0
|
||||||
|
: StringComparer.Ordinal.GetHashCode(ReflectionFallbackTypeName));
|
||||||
foreach (var registration in Registrations)
|
foreach (var registration in Registrations)
|
||||||
{
|
{
|
||||||
hashCode = (hashCode * 397) ^ registration.GetHashCode();
|
hashCode = (hashCode * 397) ^ registration.GetHashCode();
|
||||||
@ -423,5 +533,12 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
|
|
||||||
private readonly record struct GenerationEnvironment(
|
private readonly record struct GenerationEnvironment(
|
||||||
bool GenerationEnabled,
|
bool GenerationEnabled,
|
||||||
bool SupportsReflectionFallbackMarker);
|
ReflectionFallbackEmissionMode ReflectionFallbackEmissionMode);
|
||||||
|
|
||||||
|
private enum ReflectionFallbackEmissionMode
|
||||||
|
{
|
||||||
|
Disabled,
|
||||||
|
MarkerOnly,
|
||||||
|
PreciseTypeNames
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user