mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
- 实现CqrsHandlerRegistrar类,支持扫描并注册CQRS请求/通知/流式处理器 - 添加源码生成注册器优先策略,减少冷启动时的反射开销 - 实现运行时反射扫描回退机制,确保处理器注册的完整性 - 添加CqrsReflectionFallbackAttribute特性,标记需要运行时补充扫描的程序集 - 创建完整的单元测试套件,验证处理器注册顺序与容错行为 - 实现CqrsHandlerRegistryGenerator源码生成器,自动生成处理器注册代码 - 添加详细的日志记录与诊断功能,便于调试注册过程 - 实现类型安全的处理器映射验证与重复注册检测机制
448 lines
24 KiB
C#
448 lines
24 KiB
C#
using System.Reflection;
|
|
using GFramework.SourceGenerators.Cqrs;
|
|
using GFramework.SourceGenerators.Tests.Core;
|
|
|
|
namespace GFramework.SourceGenerators.Tests.Cqrs;
|
|
|
|
/// <summary>
|
|
/// 验证 CQRS 处理器注册生成器的输出与回退边界。
|
|
/// </summary>
|
|
[TestFixture]
|
|
public class CqrsHandlerRegistryGeneratorTests
|
|
{
|
|
/// <summary>
|
|
/// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。
|
|
/// </summary>
|
|
[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<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(params string[] fallbackHandlerTypeNames) { }
|
|
}
|
|
}
|
|
|
|
namespace TestApp
|
|
{
|
|
using GFramework.Cqrs.Abstractions.Cqrs;
|
|
|
|
public sealed record PingQuery() : IRequest<string>;
|
|
public sealed record DomainEvent() : INotification;
|
|
public sealed record NumberStream() : IStreamRequest<int>;
|
|
|
|
public sealed class ZetaNotificationHandler : INotificationHandler<DomainEvent> { }
|
|
public sealed class AlphaQueryHandler : IRequestHandler<PingQuery, string> { }
|
|
public sealed class StreamHandler : IStreamRequestHandler<NumberStream, int> { }
|
|
}
|
|
""";
|
|
|
|
const string expected = """
|
|
// <auto-generated />
|
|
#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<global::TestApp.PingQuery, string>),
|
|
typeof(global::TestApp.AlphaQueryHandler));
|
|
logger.Debug("Registered CQRS handler TestApp.AlphaQueryHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<TestApp.PingQuery, string>.");
|
|
global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient(
|
|
services,
|
|
typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<global::TestApp.NumberStream, int>),
|
|
typeof(global::TestApp.StreamHandler));
|
|
logger.Debug("Registered CQRS handler TestApp.StreamHandler as GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<TestApp.NumberStream, int>.");
|
|
global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient(
|
|
services,
|
|
typeof(global::GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<global::TestApp.DomainEvent>),
|
|
typeof(global::TestApp.ZetaNotificationHandler));
|
|
logger.Debug("Registered CQRS handler TestApp.ZetaNotificationHandler as GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<TestApp.DomainEvent>.");
|
|
}
|
|
}
|
|
|
|
""";
|
|
|
|
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
|
|
source,
|
|
("CqrsHandlerRegistry.g.cs", expected));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器仍会为可见 handlers 生成注册器,
|
|
/// 并额外标记运行时补充反射扫描。
|
|
/// </summary>
|
|
[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<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(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() { }
|
|
}
|
|
}
|
|
|
|
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 = """
|
|
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) { }
|
|
}
|
|
}
|
|
|
|
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> { }
|
|
}
|
|
""";
|
|
|
|
var test = new CSharpSourceGeneratorTest<CqrsHandlerRegistryGenerator, DefaultVerifier>
|
|
{
|
|
TestState =
|
|
{
|
|
Sources = { source }
|
|
},
|
|
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
|
};
|
|
|
|
await test.RunAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。
|
|
/// </summary>
|
|
[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));
|
|
}
|
|
}
|