diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 6421e0ca..9b3f889e 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -398,6 +398,15 @@ private static bool TryCreateRuntimeTypeReference( ITypeSymbol type, out RuntimeTypeReferenceSpec? runtimeTypeReference) { + // CLR forbids pointer and function-pointer types from being used as generic arguments. + // CQRS handler contracts are generic interfaces, so emitting runtime reconstruction code for these + // shapes would only defer the failure to MakeGenericType(...) at runtime. + if (type is IPointerTypeSymbol or IFunctionPointerTypeSymbol) + { + runtimeTypeReference = null; + return false; + } + if (CanReferenceFromGeneratedRegistry(compilation, type)) { runtimeTypeReference = RuntimeTypeReferenceSpec.FromDirectReference( @@ -518,8 +527,9 @@ private static bool CanReferenceFromGeneratedRegistry(Compilation compilation, I } return true; - case IPointerTypeSymbol pointerType: - return CanReferenceFromGeneratedRegistry(compilation, pointerType.PointedAtType); + case IPointerTypeSymbol: + case IFunctionPointerTypeSymbol: + return false; case ITypeParameterSymbol: return false; default: @@ -975,6 +985,18 @@ private static string AppendRuntimeTypeReferenceResolution( : $"{elementExpression}.MakeArrayType({runtimeTypeReference.ArrayRank})"; } + if (runtimeTypeReference.PointerElementTypeReference is not null) + { + var pointedAtExpression = AppendRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference.PointerElementTypeReference, + $"{variableBaseName}PointedAt", + reflectedArgumentNames, + indent); + + return $"{pointedAtExpression}.MakePointerType()"; + } + if (runtimeTypeReference.GenericTypeDefinitionReference is not null) { var genericTypeDefinitionExpression = AppendRuntimeTypeReferenceResolution( @@ -1091,6 +1113,12 @@ private static bool ContainsExternalAssemblyTypeLookup(RuntimeTypeReferenceSpec return true; } + if (runtimeTypeReference.PointerElementTypeReference is not null && + ContainsExternalAssemblyTypeLookup(runtimeTypeReference.PointerElementTypeReference)) + { + return true; + } + if (runtimeTypeReference.GenericTypeDefinitionReference is not null && ContainsExternalAssemblyTypeLookup(runtimeTypeReference.GenericTypeDefinitionReference)) { @@ -1129,18 +1157,19 @@ private sealed record RuntimeTypeReferenceSpec( string? ReflectionAssemblyName, RuntimeTypeReferenceSpec? ArrayElementTypeReference, int ArrayRank, + RuntimeTypeReferenceSpec? PointerElementTypeReference, RuntimeTypeReferenceSpec? GenericTypeDefinitionReference, ImmutableArray GenericTypeArguments) { public static RuntimeTypeReferenceSpec FromDirectReference(string typeDisplayName) { - return new RuntimeTypeReferenceSpec(typeDisplayName, null, null, null, 0, null, + return new RuntimeTypeReferenceSpec(typeDisplayName, null, null, null, 0, null, null, ImmutableArray.Empty); } public static RuntimeTypeReferenceSpec FromReflectionLookup(string reflectionTypeMetadataName) { - return new RuntimeTypeReferenceSpec(null, reflectionTypeMetadataName, null, null, 0, null, + return new RuntimeTypeReferenceSpec(null, reflectionTypeMetadataName, null, null, 0, null, null, ImmutableArray.Empty); } @@ -1149,13 +1178,19 @@ public static RuntimeTypeReferenceSpec FromExternalReflectionLookup( string reflectionTypeMetadataName) { return new RuntimeTypeReferenceSpec(null, reflectionTypeMetadataName, reflectionAssemblyName, null, 0, - null, + null, null, ImmutableArray.Empty); } public static RuntimeTypeReferenceSpec FromArray(RuntimeTypeReferenceSpec elementTypeReference, int arrayRank) { - return new RuntimeTypeReferenceSpec(null, null, null, elementTypeReference, arrayRank, null, + return new RuntimeTypeReferenceSpec(null, null, null, elementTypeReference, arrayRank, null, null, + ImmutableArray.Empty); + } + + public static RuntimeTypeReferenceSpec FromPointer(RuntimeTypeReferenceSpec pointedAtTypeReference) + { + return new RuntimeTypeReferenceSpec(null, null, null, null, 0, pointedAtTypeReference, null, ImmutableArray.Empty); } @@ -1163,7 +1198,7 @@ public static RuntimeTypeReferenceSpec FromConstructedGeneric( RuntimeTypeReferenceSpec genericTypeDefinitionReference, ImmutableArray genericTypeArguments) { - return new RuntimeTypeReferenceSpec(null, null, null, null, 0, genericTypeDefinitionReference, + return new RuntimeTypeReferenceSpec(null, null, null, null, 0, null, genericTypeDefinitionReference, genericTypeArguments); } } diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index 3e92281d..8bcdeff3 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -32,6 +32,7 @@ public void SetUp() _container.Freeze(); _context = new ArchitectureContext(_container); + ClearRegistrarCaches(); } /// @@ -43,6 +44,7 @@ public void TearDown() _context = null; _container = null; DeterministicNotificationHandlerState.Reset(); + ClearRegistrarCaches(); } /// @@ -140,6 +142,31 @@ public void RegisterHandlers_Should_Use_Generated_Registry_When_Available() Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)])); } + /// + /// 验证 generated registry 使用私有无参构造器时,运行时仍可激活它并完成处理器注册。 + /// + [Test] + public void RegisterHandlers_Should_Activate_Generated_Registry_With_Private_Parameterless_Constructor() + { + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns("GFramework.Core.Tests.Cqrs.PrivateGeneratedRegistryAssembly, Version=1.0.0.0"); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(typeof(PrivateConstructorNotificationHandlerRegistry))]); + + var container = new MicrosoftDiContainer(); + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + container.Freeze(); + + var handlers = container.GetAll>(); + + Assert.That( + handlers.Select(static handler => handler.GetType()), + Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)])); + } + /// /// 验证当生成注册器元数据损坏时,运行时会记录告警并回退到反射扫描路径。 /// @@ -410,6 +437,150 @@ public void RegisterHandlers_Should_Cache_Loadable_Types_Across_Containers() partiallyLoadableAssembly.Verify(static assembly => assembly.GetTypes(), Times.Once); } + + /// + /// 验证同一 handler 类型跨容器重复注册时,会复用已筛选的 supported handler interface 列表, + /// 而不是为每个容器重新执行接口反射分析。 + /// + [Test] + public void RegisterHandlers_Should_Cache_Supported_Handler_Interfaces_Across_Containers() + { + var supportedHandlerInterfacesCache = GetRegistrarCacheField("SupportedHandlerInterfacesCache"); + var firstHandlerType = typeof(AlphaDeterministicNotificationHandler); + var secondHandlerType = typeof(ZetaDeterministicNotificationHandler); + var handlerAssembly = new Mock(); + handlerAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns("GFramework.Core.Tests.Cqrs.CachedHandlerInterfacesAssembly, Version=1.0.0.0"); + handlerAssembly + .Setup(static assembly => assembly.GetTypes()) + .Returns([firstHandlerType, secondHandlerType]); + + Assert.Multiple(() => + { + Assert.That(GetSingleKeyCacheValue(supportedHandlerInterfacesCache, firstHandlerType), Is.Null); + Assert.That(GetSingleKeyCacheValue(supportedHandlerInterfacesCache, secondHandlerType), Is.Null); + }); + + var firstContainer = new MicrosoftDiContainer(); + var secondContainer = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(firstContainer, handlerAssembly.Object); + var firstHandlerInterfaces = + GetSingleKeyCacheValue(supportedHandlerInterfacesCache, firstHandlerType); + var secondHandlerInterfaces = + GetSingleKeyCacheValue(supportedHandlerInterfacesCache, secondHandlerType); + + CqrsTestRuntime.RegisterHandlers(secondContainer, handlerAssembly.Object); + + Assert.Multiple(() => + { + Assert.That(firstHandlerInterfaces, Is.Not.Null); + Assert.That(secondHandlerInterfaces, Is.Not.Null); + Assert.That( + GetSingleKeyCacheValue(supportedHandlerInterfacesCache, firstHandlerType), + Is.SameAs(firstHandlerInterfaces)); + Assert.That( + GetSingleKeyCacheValue(supportedHandlerInterfacesCache, secondHandlerType), + Is.SameAs(secondHandlerInterfaces)); + }); + + handlerAssembly.Verify(static assembly => assembly.GetTypes(), Times.Once); + } + + /// + /// 验证当程序集枚举结果包含重复 handler 类型时,registrar 仍只会写入一份 handler 映射。 + /// + [Test] + public void RegisterHandlers_Should_Skip_Duplicate_Handler_Mappings_When_Assembly_Returns_Duplicate_Types() + { + var handlerType = typeof(AlphaDeterministicNotificationHandler); + var handlerAssembly = new Mock(); + handlerAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns("GFramework.Core.Tests.Cqrs.DuplicateHandlerMappingsAssembly, Version=1.0.0.0"); + handlerAssembly + .Setup(static assembly => assembly.GetTypes()) + .Returns([handlerType, handlerType]); + + var container = new MicrosoftDiContainer(); + CqrsTestRuntime.RegisterHandlers(container, handlerAssembly.Object); + + var registrations = container.GetServicesUnsafe + .Where(static descriptor => + descriptor.ServiceType == typeof(INotificationHandler) && + descriptor.ImplementationType == typeof(AlphaDeterministicNotificationHandler)) + .ToArray(); + + Assert.That(registrations, Has.Length.EqualTo(1)); + } + + /// + /// 清空本测试依赖的 registrar 静态缓存,避免跨用例共享进程级状态导致断言漂移。 + /// + private static void ClearRegistrarCaches() + { + ClearCache(GetRegistrarCacheField("AssemblyMetadataCache")); + ClearCache(GetRegistrarCacheField("RegistryActivationMetadataCache")); + ClearCache(GetRegistrarCacheField("LoadableTypesCache")); + ClearCache(GetRegistrarCacheField("SupportedHandlerInterfacesCache")); + } + + /// + /// 通过反射读取 registrar 的静态缓存对象。 + /// + private static object GetRegistrarCacheField(string fieldName) + { + var registrarType = GetRegistrarType(); + var field = registrarType.GetField( + fieldName, + BindingFlags.NonPublic | BindingFlags.Static); + + Assert.That(field, Is.Not.Null, $"Missing registrar cache field {fieldName}."); + + return field!.GetValue(null) + ?? throw new InvalidOperationException( + $"Registrar cache field {fieldName} returned null."); + } + + /// + /// 清空指定缓存对象。 + /// + private static void ClearCache(object cache) + { + _ = InvokeInstanceMethod(cache, "Clear"); + } + + /// + /// 读取单键缓存中当前保存的对象。 + /// + private static object? GetSingleKeyCacheValue(object cache, Type key) + { + return InvokeInstanceMethod(cache, "GetValueOrDefaultForTesting", key); + } + + /// + /// 调用缓存对象上的实例方法。 + /// + private static object? InvokeInstanceMethod(object target, string methodName, params object[] arguments) + { + var method = target.GetType().GetMethod( + methodName, + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + Assert.That(method, Is.Not.Null, $"Missing cache method {target.GetType().FullName}.{methodName}."); + + return method!.Invoke(target, arguments); + } + + /// + /// 获取 CQRS handler registrar 运行时类型。 + /// + private static Type GetRegistrarType() + { + return typeof(CqrsReflectionFallbackAttribute).Assembly + .GetType("GFramework.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)!; + } } /// @@ -608,3 +779,33 @@ public void Register(IServiceCollection services, ILogger logger) $"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler).FullName}."); } } + +/// +/// 模拟生成注册器使用私有无参构造器的场景,验证运行时仍可通过缓存工厂激活它。 +/// +internal sealed class PrivateConstructorNotificationHandlerRegistry : ICqrsHandlerRegistry +{ + /// + /// 初始化一个新的私有生成注册器实例。 + /// + private PrivateConstructorNotificationHandlerRegistry() + { + } + + /// + /// 将测试通知处理器注册到目标服务集合。 + /// + /// 承载处理器映射的服务集合。 + /// 用于记录注册诊断的日志器。 + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(INotificationHandler), + typeof(GeneratedRegistryNotificationHandler)); + logger.Debug( + $"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler).FullName}."); + } +} diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index 031cb7e4..c6fd3909 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -1,6 +1,7 @@ using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Cqrs.Abstractions.Cqrs; +using System.Reflection.Emit; namespace GFramework.Cqrs.Internal; @@ -25,6 +26,11 @@ internal static class CqrsHandlerRegistrar private static readonly WeakKeyCache> LoadableTypesCache = new(); + // 卸载安全的进程级缓存:同一 handler 类型跨容器重复注册时, + // 复用已筛选且排序好的 supported handler interface 列表,避免重复执行 GetInterfaces()。 + private static readonly WeakKeyCache> SupportedHandlerInterfacesCache = + new(); + /// /// 扫描指定程序集并注册所有 CQRS 请求/通知/流式处理器。 /// @@ -159,21 +165,18 @@ private static void RegisterAssemblyHandlers( ILogger logger, ReflectionFallbackMetadata? reflectionFallbackMetadata) { + var registeredMappings = CreateRegisteredHandlerMappings(services); foreach (var implementationType in GetCandidateHandlerTypes(assembly, logger, reflectionFallbackMetadata) .Where(IsConcreteHandlerType)) { - var handlerInterfaces = implementationType - .GetInterfaces() - .Where(IsSupportedHandlerInterface) - .OrderBy(GetTypeSortKey, StringComparer.Ordinal) - .ToList(); + var handlerInterfaces = GetSupportedHandlerInterfaces(implementationType); if (handlerInterfaces.Count == 0) continue; foreach (var handlerInterface in handlerInterfaces) { - if (IsHandlerMappingAlreadyRegistered(services, handlerInterface, implementationType)) + if (!registeredMappings.Add(new HandlerMapping(handlerInterface, implementationType))) { logger.Debug( $"Skipping duplicate CQRS handler {implementationType.FullName} as {handlerInterface.FullName}."); @@ -183,12 +186,45 @@ private static void RegisterAssemblyHandlers( // Request/notification handlers receive context injection before every dispatch. // Transient registration avoids sharing mutable Context across concurrent requests. services.AddTransient(handlerInterface, implementationType); - logger.Debug( + logger.Debug( $"Registered CQRS handler {implementationType.FullName} as {handlerInterface.FullName}."); } } } + /// + /// 获取指定实现类型上所有受支持的 CQRS handler 接口,并缓存筛选与排序结果。 + /// + /// 要分析的处理器实现类型。 + /// 当前实现类型声明的受支持 handler 接口列表。 + private static IReadOnlyList GetSupportedHandlerInterfaces(Type implementationType) + { + ArgumentNullException.ThrowIfNull(implementationType); + + return SupportedHandlerInterfacesCache.GetOrAdd( + implementationType, + static key => key + .GetInterfaces() + .Where(IsSupportedHandlerInterface) + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToArray()); + } + + /// + /// 根据当前服务集合创建已注册 handler 映射的快速索引,避免 reflection fallback 路径重复线性扫描服务描述符。 + /// + /// 当前容器的服务描述符集合。 + /// 已存在的 handler 映射集合。 + private static HashSet CreateRegisteredHandlerMappings(IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + + return services + .Where(static descriptor => descriptor.ImplementationType is not null) + .Select(static descriptor => new HandlerMapping(descriptor.ServiceType, descriptor.ImplementationType!)) + .ToHashSet(); + } + /// /// 根据生成器提供的 fallback 清单或整程序集扫描结果,获取本轮要注册的候选处理器类型。 /// @@ -323,7 +359,51 @@ private static RegistryActivationMetadata AnalyzeRegistryActivation(Type registr : new RegistryActivationMetadata( true, false, - () => (ICqrsHandlerRegistry)constructor.Invoke(null)); + CreateRegistryFactory(registryType, constructor)); + } + + /// + /// 为生成注册器创建可复用的激活工厂,优先使用一次性编译的动态方法, + /// 避免后续每次命中缓存时仍走 的反射激活路径。 + /// + /// 生成注册器类型。 + /// 已解析的无参构造函数。 + /// 可直接实例化注册器的工厂委托。 + private static Func CreateRegistryFactory( + Type registryType, + ConstructorInfo constructor) + { + ArgumentNullException.ThrowIfNull(registryType); + ArgumentNullException.ThrowIfNull(constructor); + + try + { + // 生成器产物通常是稳定的无参 registry;这里把构造反射收敛为一次性 IL 工厂, + // 这样同一 registry 类型在多个容器间复用缓存时不会重复付出 ConstructorInfo.Invoke 成本。 + var dynamicMethod = new DynamicMethod( + $"Create_{registryType.Name}_CqrsHandlerRegistry", + typeof(ICqrsHandlerRegistry), + Type.EmptyTypes, + registryType.Module, + skipVisibility: true); + var il = dynamicMethod.GetILGenerator(); + il.Emit(OpCodes.Newobj, constructor); + + if (registryType.IsValueType) + { + il.Emit(OpCodes.Box, registryType); + } + + il.Emit(OpCodes.Castclass, typeof(ICqrsHandlerRegistry)); + il.Emit(OpCodes.Ret); + + return (Func)dynamicMethod.CreateDelegate(typeof(Func)); + } + catch + { + // 某些受限运行环境若不允许动态方法,仍保留原有的反射激活语义,避免阻塞 generated registry 路径。 + return () => (ICqrsHandlerRegistry)constructor.Invoke(null); + } } /// @@ -391,21 +471,6 @@ private static bool IsSupportedHandlerInterface(Type type) definition == typeof(IStreamRequestHandler<,>); } - /// - /// 判断同一 handler 映射是否已经由生成注册器或先前扫描步骤写入服务集合。 - /// - private static bool IsHandlerMappingAlreadyRegistered( - IServiceCollection services, - Type handlerInterface, - Type implementationType) - { - // 这里保持线性扫描,避免为常见的小到中等规模程序集长期维护额外索引。 - // 若未来大型服务集合出现热点,可在更高层批处理中引入 HashSet<(Type, Type)> 做 O(1) 去重。 - return services.Any(descriptor => - descriptor.ServiceType == handlerInterface && - descriptor.ImplementationType == implementationType); - } - /// /// 生成程序集排序键,保证跨运行环境的处理器注册顺序稳定。 /// @@ -422,6 +487,8 @@ private static string GetTypeSortKey(Type type) return type.FullName ?? type.Name; } + private readonly record struct HandlerMapping(Type ServiceType, Type ImplementationType); + private readonly record struct GeneratedRegistrationResult( bool UsedGeneratedRegistry, bool RequiresReflectionFallback, diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 6db73364..e9a9bfa2 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -745,6 +745,109 @@ await GeneratorTest.RunAsync( ("CqrsHandlerRegistry.g.cs", HiddenGenericEnvelopeResponseExpected)); } + /// + /// 验证当 handler 合同把 pointer 响应类型放进 CQRS 泛型参数时, + /// 生成器会保守回退而不是继续发射不可构造的精确注册代码。 + /// + [Test] + public void Reports_Compilation_Error_And_Skips_Precise_Registration_For_Hidden_Pointer_Response() + { + 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 class Container + { + private unsafe struct HiddenResponse + { + } + + private unsafe sealed record HiddenRequest() : IRequest; + + public unsafe sealed class HiddenHandler : IRequestHandler + { + } + } + } + """; + + var execution = ExecuteGenerator( + source, + allowUnsafe: true); + var inputCompilationErrors = execution.InputCompilationDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + var generatorErrors = execution.GeneratorDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + var missingContractDiagnostic = + generatorErrors.SingleOrDefault(static diagnostic => + string.Equals(diagnostic.Id, "GF_Cqrs_001", StringComparison.Ordinal)); + + Assert.Multiple(() => + { + Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306")); + Assert.That(generatedCompilationErrors, Is.Empty); + Assert.That(execution.GeneratedSources, Is.Empty); + Assert.That(missingContractDiagnostic, Is.Not.Null); + Assert.That( + missingContractDiagnostic!.GetMessage(), + Does.Contain("TestApp.Container+HiddenHandler")); + Assert.That( + missingContractDiagnostic.GetMessage(), + Does.Contain("GFramework.Cqrs.CqrsReflectionFallbackAttribute")); + }); + } + /// /// 验证同一个 implementation 同时包含可直接注册接口与需精确重建接口时, /// 生成器会保留两类注册,并继续按 handler interface 名称稳定排序。 @@ -1232,9 +1335,9 @@ private unsafe struct HiddenResponse { } - private unsafe sealed record HiddenRequest() : IRequest; + private unsafe sealed record HiddenRequest() : IRequest>; - public unsafe sealed class HiddenHandler : IRequestHandler + public unsafe sealed class HiddenHandler : IRequestHandler> { } } @@ -1244,6 +1347,9 @@ public unsafe sealed class HiddenHandler : IRequestHandler