GFramework/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs
GeWuYou 4951fb0254 feat(cqrs): 添加 CQRS 分发器和服务注册生成器
- 实现 CqrsDispatcher 类,支持请求/通知/流式请求的分发处理
- 添加进程级缓存机制,优化热路径中的反射和类型构造性能
- 实现上下文感知处理器的 CQRS 分发上下文注入功能
- 开发 CqrsHandlerRegistryGenerator 源代码生成器,减少运行时反射扫描
- 添加完整的单元测试验证缓存机制和服务类型注册功能
- 支持管道行为链处理和异步流式请求响应模式
2026-04-16 12:19:44 +08:00

517 lines
33 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Reflection;
using GFramework.SourceGenerators.Cqrs;
using GFramework.SourceGenerators.Tests.Core;
namespace GFramework.SourceGenerators.Tests.Cqrs;
/// <summary>
/// 验证 CQRS 处理器注册生成器的输出与回退边界。
/// </summary>
[TestFixture]
public class CqrsHandlerRegistryGeneratorTests
{
private const string HiddenNestedHandlerSelfRegistrationExpected = """
// <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));
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<global::TestApp.VisibleRequest, string>),
typeof(global::TestApp.VisibleHandler));
logger.Debug("Registered CQRS handler TestApp.VisibleHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<TestApp.VisibleRequest, string>.");
}
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();
}
}
""";
/// <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>
/// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器会在生成注册器内部执行定向反射注册,
/// 不再依赖程序集级 fallback marker。
/// </summary>
[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<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> { }
}
""";
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
source,
("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected));
}
/// <summary>
/// 验证即使 runtime 仍暴露旧版无参 fallback marker生成器也会优先在生成注册器内部处理隐藏 handler
/// 不再输出 fallback marker。
/// </summary>
[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<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> { }
}
""";
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
source,
("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected));
}
/// <summary>
/// 验证即使 runtime 合同中完全不存在 reflection fallback 标记特性,
/// 生成器仍能通过生成注册器内部的定向反射逻辑覆盖隐藏 handler。
/// </summary>
[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<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> { }
}
""";
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
source,
("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected));
}
/// <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));
}
}