using System.Reflection;
using GFramework.SourceGenerators.Cqrs;
using GFramework.SourceGenerators.Tests.Core;
namespace GFramework.SourceGenerators.Tests.Cqrs;
///
/// 验证 CQRS 处理器注册生成器的输出与回退边界。
///
[TestFixture]
public class CqrsHandlerRegistryGeneratorTests
{
///
/// 验证生成器会为当前程序集中的 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));
}
///
/// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器仍会为可见 handlers 生成注册器,
/// 并额外标记运行时补充反射扫描。
///
[Test]
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 { }
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 { }
}
""";
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() { }
}
}
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()]
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 合同中不存在 reflection fallback 标记特性时,
/// 生成器会保留此前的整程序集回退行为,避免丢失不可见 handlers。
///
[Test]
public async Task Skips_Generation_For_Unsupported_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 { }
}
""";
var test = new CSharpSourceGeneratorTest
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" }
};
await test.RunAsync();
}
///
/// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。
///
[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));
}
}