mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-11 20:38:58 +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]
|
[TestFixture]
|
||||||
internal sealed class CqrsHandlerRegistrarTests
|
internal sealed class CqrsHandlerRegistrarTests
|
||||||
{
|
{
|
||||||
|
private MicrosoftDiContainer? _container;
|
||||||
|
private ArchitectureContext? _context;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 初始化测试容器并重置共享状态。
|
/// 初始化测试容器并重置共享状态。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -42,9 +45,6 @@ internal sealed class CqrsHandlerRegistrarTests
|
|||||||
DeterministicNotificationHandlerState.Reset();
|
DeterministicNotificationHandlerState.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
private MicrosoftDiContainer? _container;
|
|
||||||
private ArchitectureContext? _context;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证自动扫描到的通知处理器会按稳定名称顺序执行,而不是依赖反射枚举顺序。
|
/// 验证自动扫描到的通知处理器会按稳定名称顺序执行,而不是依赖反射枚举顺序。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -188,6 +188,50 @@ internal sealed class CqrsHandlerRegistrarTests
|
|||||||
LoggerFactoryResolver.Provider = originalProvider;
|
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>
|
/// <summary>
|
||||||
@ -337,3 +381,52 @@ internal sealed class GeneratedNotificationHandlerRegistry : ICqrsHandlerRegistr
|
|||||||
$"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler<GeneratedRegistryNotification>).FullName}.");
|
$"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.Ioc;
|
||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||||
@ -32,7 +31,9 @@ internal static class CqrsHandlerRegistrar
|
|||||||
.Distinct()
|
.Distinct()
|
||||||
.OrderBy(GetAssemblySortKey, StringComparer.Ordinal))
|
.OrderBy(GetAssemblySortKey, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
if (TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger))
|
var generatedRegistrationResult =
|
||||||
|
TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger);
|
||||||
|
if (generatedRegistrationResult == GeneratedRegistrationResult.FullyHandled)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
RegisterAssemblyHandlers(container.GetServicesUnsafe, assembly, logger);
|
RegisterAssemblyHandlers(container.GetServicesUnsafe, assembly, logger);
|
||||||
@ -45,8 +46,11 @@ internal static class CqrsHandlerRegistrar
|
|||||||
/// <param name="services">目标服务集合。</param>
|
/// <param name="services">目标服务集合。</param>
|
||||||
/// <param name="assembly">当前要处理的程序集。</param>
|
/// <param name="assembly">当前要处理的程序集。</param>
|
||||||
/// <param name="logger">日志记录器。</param>
|
/// <param name="logger">日志记录器。</param>
|
||||||
/// <returns>当成功使用生成注册器时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
/// <returns>生成注册器的使用结果。</returns>
|
||||||
private static bool TryRegisterGeneratedHandlers(IServiceCollection services, Assembly assembly, ILogger logger)
|
private static GeneratedRegistrationResult TryRegisterGeneratedHandlers(
|
||||||
|
IServiceCollection services,
|
||||||
|
Assembly assembly,
|
||||||
|
ILogger logger)
|
||||||
{
|
{
|
||||||
var assemblyName = GetAssemblySortKey(assembly);
|
var assemblyName = GetAssemblySortKey(assembly);
|
||||||
|
|
||||||
@ -62,7 +66,7 @@ internal static class CqrsHandlerRegistrar
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (registryTypes.Count == 0)
|
if (registryTypes.Count == 0)
|
||||||
return false;
|
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)
|
||||||
@ -71,21 +75,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 false;
|
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 false;
|
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 false;
|
return GeneratedRegistrationResult.NoGeneratedRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
registries.Add(registry);
|
registries.Add(registry);
|
||||||
@ -98,7 +102,14 @@ internal static class CqrsHandlerRegistrar
|
|||||||
registry.Register(services, logger);
|
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)
|
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.");
|
$"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 false;
|
return GeneratedRegistrationResult.NoGeneratedRegistry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,6 +139,13 @@ internal static class CqrsHandlerRegistrar
|
|||||||
|
|
||||||
foreach (var handlerInterface in handlerInterfaces)
|
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.
|
// Request/notification handlers receive context injection before every dispatch.
|
||||||
// Transient registration avoids sharing mutable Context across concurrent requests.
|
// Transient registration avoids sharing mutable Context across concurrent requests.
|
||||||
services.AddTransient(handlerInterface, implementationType);
|
services.AddTransient(handlerInterface, implementationType);
|
||||||
@ -202,6 +220,27 @@ internal static class CqrsHandlerRegistrar
|
|||||||
definition == typeof(IStreamRequestHandler<,>);
|
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>
|
||||||
/// 生成程序集排序键,保证跨运行环境的处理器注册顺序稳定。
|
/// 生成程序集排序键,保证跨运行环境的处理器注册顺序稳定。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -217,4 +256,11 @@ internal static class CqrsHandlerRegistrar
|
|||||||
{
|
{
|
||||||
return type.FullName ?? type.Name;
|
return type.FullName ?? type.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum GeneratedRegistrationResult
|
||||||
|
{
|
||||||
|
NoGeneratedRegistry,
|
||||||
|
FullyHandled,
|
||||||
|
RequiresReflectionFallback
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,6 +61,11 @@ public class CqrsHandlerRegistryGeneratorTests
|
|||||||
{
|
{
|
||||||
public CqrsHandlerRegistryAttribute(Type registryType) { }
|
public CqrsHandlerRegistryAttribute(Type registryType) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Assembly)]
|
||||||
|
public sealed class CqrsReflectionFallbackAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace TestApp
|
namespace TestApp
|
||||||
@ -120,10 +125,120 @@ public class CqrsHandlerRegistryGeneratorTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器会放弃产出并让运行时回退到反射扫描。
|
/// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器仍会为可见 handlers 生成注册器,
|
||||||
|
/// 并额外标记运行时补充反射扫描。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[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 = """
|
const string source = """
|
||||||
using System;
|
using System;
|
||||||
|
|||||||
@ -16,6 +16,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
private const string IStreamRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IStreamRequestHandler`2";
|
private const string IStreamRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IStreamRequestHandler`2";
|
||||||
private const string ICqrsHandlerRegistryMetadataName = $"{CqrsRuntimeNamespace}.ICqrsHandlerRegistry";
|
private const string ICqrsHandlerRegistryMetadataName = $"{CqrsRuntimeNamespace}.ICqrsHandlerRegistry";
|
||||||
|
|
||||||
|
private const string CqrsReflectionFallbackAttributeMetadataName =
|
||||||
|
$"{CqrsRuntimeNamespace}.CqrsReflectionFallbackAttribute";
|
||||||
|
|
||||||
private const string CqrsHandlerRegistryAttributeMetadataName =
|
private const string CqrsHandlerRegistryAttributeMetadataName =
|
||||||
$"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute";
|
$"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute";
|
||||||
|
|
||||||
@ -28,8 +31,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||||
{
|
{
|
||||||
var generationEnabled = context.CompilationProvider
|
var generationEnvironment = context.CompilationProvider
|
||||||
.Select(static (compilation, _) => HasRequiredTypes(compilation));
|
.Select(static (compilation, _) => CreateGenerationEnvironment(compilation));
|
||||||
|
|
||||||
// Restrict semantic analysis to type declarations that can actually contribute implemented interfaces.
|
// Restrict semantic analysis to type declarations that can actually contribute implemented interfaces.
|
||||||
var handlerCandidates = context.SyntaxProvider.CreateSyntaxProvider(
|
var handlerCandidates = context.SyntaxProvider.CreateSyntaxProvider(
|
||||||
@ -39,19 +42,24 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
.Collect();
|
.Collect();
|
||||||
|
|
||||||
context.RegisterSourceOutput(
|
context.RegisterSourceOutput(
|
||||||
generationEnabled.Combine(handlerCandidates),
|
generationEnvironment.Combine(handlerCandidates),
|
||||||
static (productionContext, pair) => Execute(productionContext, pair.Left, pair.Right));
|
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 &&
|
var generationEnabled = compilation.GetTypeByMetadataName(IRequestHandlerMetadataName) is not null &&
|
||||||
compilation.GetTypeByMetadataName(INotificationHandlerMetadataName) is not null &&
|
compilation.GetTypeByMetadataName(INotificationHandlerMetadataName) is not null &&
|
||||||
compilation.GetTypeByMetadataName(IStreamRequestHandlerMetadataName) is not null &&
|
compilation.GetTypeByMetadataName(IStreamRequestHandlerMetadataName) is not null &&
|
||||||
compilation.GetTypeByMetadataName(ICqrsHandlerRegistryMetadataName) is not null &&
|
compilation.GetTypeByMetadataName(ICqrsHandlerRegistryMetadataName) is not null &&
|
||||||
compilation.GetTypeByMetadataName(CqrsHandlerRegistryAttributeMetadataName) is not null &&
|
compilation.GetTypeByMetadataName(
|
||||||
compilation.GetTypeByMetadataName(ILoggerMetadataName) is not null &&
|
CqrsHandlerRegistryAttributeMetadataName) is not null &&
|
||||||
compilation.GetTypeByMetadataName(IServiceCollectionMetadataName) 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)
|
private static bool IsHandlerCandidate(SyntaxNode node)
|
||||||
@ -108,21 +116,25 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Execute(SourceProductionContext context, bool generationEnabled,
|
private static void Execute(SourceProductionContext context, GenerationEnvironment generationEnvironment,
|
||||||
ImmutableArray<HandlerCandidateAnalysis?> candidates)
|
ImmutableArray<HandlerCandidateAnalysis?> candidates)
|
||||||
{
|
{
|
||||||
if (!generationEnabled)
|
if (!generationEnvironment.GenerationEnabled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var registrations = CollectRegistrations(candidates, out var hasUnsupportedConcreteHandler);
|
var registrations = CollectRegistrations(candidates, out var hasUnsupportedConcreteHandler);
|
||||||
|
|
||||||
// If the assembly contains handlers that generated code cannot legally reference
|
if (registrations.Count == 0)
|
||||||
// (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)
|
|
||||||
return;
|
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(
|
private static List<HandlerRegistrationSpec> CollectRegistrations(
|
||||||
@ -144,7 +156,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
if (candidate.Value.HasUnsupportedConcreteHandler)
|
if (candidate.Value.HasUnsupportedConcreteHandler)
|
||||||
{
|
{
|
||||||
hasUnsupportedConcreteHandler = true;
|
hasUnsupportedConcreteHandler = true;
|
||||||
return [];
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
uniqueCandidates[candidate.Value.ImplementationTypeDisplayName] = candidate.Value;
|
uniqueCandidates[candidate.Value.ImplementationTypeDisplayName] = candidate.Value;
|
||||||
@ -270,7 +282,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
return GetTypeSortKey(type).Replace("global::", string.Empty);
|
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();
|
var builder = new StringBuilder();
|
||||||
builder.AppendLine("// <auto-generated />");
|
builder.AppendLine("// <auto-generated />");
|
||||||
@ -283,6 +297,13 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
builder.Append('.');
|
builder.Append('.');
|
||||||
builder.Append(GeneratedTypeName);
|
builder.Append(GeneratedTypeName);
|
||||||
builder.AppendLine("))]");
|
builder.AppendLine("))]");
|
||||||
|
if (emitReflectionFallbackAttribute)
|
||||||
|
{
|
||||||
|
builder.Append("[assembly: global::");
|
||||||
|
builder.Append(CqrsRuntimeNamespace);
|
||||||
|
builder.AppendLine(".CqrsReflectionFallbackAttribute()]");
|
||||||
|
}
|
||||||
|
|
||||||
builder.AppendLine();
|
builder.AppendLine();
|
||||||
builder.Append("namespace ");
|
builder.Append("namespace ");
|
||||||
builder.Append(GeneratedNamespace);
|
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