using System.Reflection; using GFramework.SourceGenerators.Cqrs; using GFramework.SourceGenerators.Tests.Core; namespace GFramework.SourceGenerators.Tests.Cqrs; /// /// 验证 CQRS 处理器注册生成器的输出与回退边界。 /// [TestFixture] public class CqrsHandlerRegistryGeneratorTests { private const string HiddenNestedHandlerSelfRegistrationExpected = """ // #nullable enable [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] 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)); var registryAssembly = typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry).Assembly; RegisterReflectedHandler(services, logger, registryAssembly, "TestApp.Container+HiddenHandler"); 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."); } private static void RegisterReflectedHandler(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger, global::System.Reflection.Assembly registryAssembly, string implementationTypeMetadataName) { var implementationType = registryAssembly.GetType(implementationTypeMetadataName, throwOnError: false, ignoreCase: false); if (implementationType is null) return; var handlerInterfaces = implementationType.GetInterfaces(); global::System.Array.Sort(handlerInterfaces, CompareTypes); foreach (var handlerInterface in handlerInterfaces) { if (!IsSupportedHandlerInterface(handlerInterface)) continue; global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( services, handlerInterface, implementationType); logger.Debug($"Registered CQRS handler {GetRuntimeTypeDisplayName(implementationType)} as {GetRuntimeTypeDisplayName(handlerInterface)}."); } } private static int CompareTypes(global::System.Type left, global::System.Type right) { return global::System.StringComparer.Ordinal.Compare(GetRuntimeTypeDisplayName(left), GetRuntimeTypeDisplayName(right)); } private static bool IsSupportedHandlerInterface(global::System.Type interfaceType) { if (!interfaceType.IsGenericType) return false; var definitionFullName = interfaceType.GetGenericTypeDefinition().FullName; return global::System.StringComparer.Ordinal.Equals(definitionFullName, "GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler`2") || global::System.StringComparer.Ordinal.Equals(definitionFullName, "GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler`1") || global::System.StringComparer.Ordinal.Equals(definitionFullName, "GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler`2"); } private static string GetRuntimeTypeDisplayName(global::System.Type type) { if (type == typeof(string)) return "string"; if (type == typeof(int)) return "int"; if (type == typeof(long)) return "long"; if (type == typeof(short)) return "short"; if (type == typeof(byte)) return "byte"; if (type == typeof(bool)) return "bool"; if (type == typeof(object)) return "object"; if (type == typeof(void)) return "void"; if (type == typeof(uint)) return "uint"; if (type == typeof(ulong)) return "ulong"; if (type == typeof(ushort)) return "ushort"; if (type == typeof(sbyte)) return "sbyte"; if (type == typeof(float)) return "float"; if (type == typeof(double)) return "double"; if (type == typeof(decimal)) return "decimal"; if (type == typeof(char)) return "char"; if (type.IsArray) return GetRuntimeTypeDisplayName(type.GetElementType()!) + "[]"; if (!type.IsGenericType) return (type.FullName ?? type.Name).Replace('+', '.'); var genericTypeName = type.GetGenericTypeDefinition().FullName ?? type.Name; var arityIndex = genericTypeName.IndexOf('`'); if (arityIndex >= 0) genericTypeName = genericTypeName[..arityIndex]; genericTypeName = genericTypeName.Replace('+', '.'); var arguments = type.GetGenericArguments(); var builder = new global::System.Text.StringBuilder(); builder.Append(genericTypeName); builder.Append('<'); for (var index = 0; index < arguments.Length; index++) { if (index > 0) builder.Append(", "); builder.Append(GetRuntimeTypeDisplayName(arguments[index])); } builder.Append('>'); return builder.ToString(); } } """; private const string HiddenImplementationDirectInterfaceRegistrationExpected = """ // #nullable enable [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] 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)); var registryAssembly = typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry).Assembly; var implementationType0 = registryAssembly.GetType("TestApp.Container+HiddenHandler", throwOnError: false, ignoreCase: false); if (implementationType0 is not null) { global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( services, typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), implementationType0); logger.Debug("Registered CQRS handler TestApp.Container.HiddenHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); } } } """; /// /// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 /// [Test] public async Task Generates_Assembly_Level_Cqrs_Handler_Registry() { const string source = """ using System; using System.Collections.Generic; 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(params string[] fallbackHandlerTypeNames) { } } } namespace TestApp { using GFramework.Cqrs.Abstractions.Cqrs; public sealed record PingQuery() : IRequest; public sealed record DomainEvent() : INotification; public sealed record NumberStream() : IStreamRequest; public sealed class ZetaNotificationHandler : INotificationHandler { } public sealed class AlphaQueryHandler : IRequestHandler { } public sealed class StreamHandler : IStreamRequestHandler { } } """; const string expected = """ // #nullable enable [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] 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.AlphaQueryHandler)); logger.Debug("Registered CQRS handler TestApp.AlphaQueryHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( services, typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler), typeof(global::TestApp.StreamHandler)); logger.Debug("Registered CQRS handler TestApp.StreamHandler as GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler."); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( services, typeof(global::GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler), typeof(global::TestApp.ZetaNotificationHandler)); logger.Debug("Registered CQRS handler TestApp.ZetaNotificationHandler as GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler."); } } """; await GeneratorTest.RunAsync( source, ("CqrsHandlerRegistry.g.cs", expected)); } /// /// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器会在生成注册器内部执行定向反射注册, /// 不再依赖程序集级 fallback marker。 /// [Test] public async Task Generates_Visible_Handlers_And_Self_Registers_Private_Nested_Handler_When_Assembly_Contains_Hidden_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 { } 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(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 { } } """; await GeneratorTest.RunAsync( source, ("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected)); } /// /// 验证当隐藏实现类型的 handler 接口仍可被生成代码直接引用时, /// 生成器只会定向反射实现类型,而不会再生成基于 GetInterfaces() 的接口发现辅助逻辑。 /// [Test] public async Task Generates_Direct_Interface_Registrations_For_Hidden_Implementation_When_Handler_Interface_Is_Public() { 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) { } } } namespace TestApp { using GFramework.Cqrs.Abstractions.Cqrs; public sealed record VisibleRequest() : IRequest; public sealed class Container { private sealed class HiddenHandler : IRequestHandler { } } } """; await GeneratorTest.RunAsync( source, ("CqrsHandlerRegistry.g.cs", HiddenImplementationDirectInterfaceRegistrationExpected)); } /// /// 验证即使 runtime 仍暴露旧版无参 fallback marker,生成器也会优先在生成注册器内部处理隐藏 handler, /// 不再输出 fallback marker。 /// [Test] public async Task Does_Not_Emit_Legacy_Fallback_Marker_When_Generated_Registry_Can_Self_Register_Hidden_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 { } 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() { } } } 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 { } } """; await GeneratorTest.RunAsync( source, ("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected)); } /// /// 验证即使 runtime 合同中完全不存在 reflection fallback 标记特性, /// 生成器仍能通过生成注册器内部的定向反射逻辑覆盖隐藏 handler。 /// [Test] public async Task Generates_Registry_For_Hidden_Handler_When_Fallback_Marker_Is_Unavailable() { 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) { } } } 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 { } } """; await GeneratorTest.RunAsync( source, ("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected)); } /// /// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。 /// [Test] public void Escape_String_Literal_Handles_Control_Characters() { var method = typeof(CqrsHandlerRegistryGenerator).GetMethod( "EscapeStringLiteral", BindingFlags.NonPublic | BindingFlags.Static); Assert.That(method, Is.Not.Null); const string input = "line1\r\nline2\\\""; const string expected = "line1\\r\\nline2\\\\\\\""; var escaped = method!.Invoke(null, [input]) as string; Assert.That(escaped, Is.EqualTo(expected)); } }