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 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator 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 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator } 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 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator : $"{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 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator 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 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator 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 sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator 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 sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator 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 @@ internal sealed class CqrsHandlerRegistrarTests _container.Freeze(); _context = new ArchitectureContext(_container); + ClearRegistrarCaches(); } /// @@ -43,6 +44,7 @@ internal sealed class CqrsHandlerRegistrarTests _context = null; _container = null; DeterministicNotificationHandlerState.Reset(); + ClearRegistrarCaches(); } /// @@ -140,6 +142,31 @@ internal sealed class CqrsHandlerRegistrarTests 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 @@ internal sealed class CqrsHandlerRegistrarTests 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 @@ internal sealed class PartialGeneratedNotificationHandlerRegistry : ICqrsHandler $"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 @@ internal static class CqrsHandlerRegistrar 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 @@ internal static class CqrsHandlerRegistrar // 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 @@ internal static class CqrsHandlerRegistrar : 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 @@ internal static class CqrsHandlerRegistrar 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 @@ internal static class CqrsHandlerRegistrar 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 @@ public class CqrsHandlerRegistryGeneratorTests ("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 @@ public class CqrsHandlerRegistryGeneratorTests { } - 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 class CqrsHandlerRegistryGeneratorTests 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(); @@ -1251,10 +1357,12 @@ public class CqrsHandlerRegistryGeneratorTests .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ToArray(); var missingContractDiagnostic = - generatorErrors.SingleOrDefault(static diagnostic => diagnostic.Id == "GF_Cqrs_001"); + 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); @@ -1341,15 +1449,15 @@ public class CqrsHandlerRegistryGeneratorTests { } - private unsafe sealed record AlphaRequest() : IRequest; + private unsafe sealed record AlphaRequest() : IRequest>; - private unsafe sealed record BetaRequest() : IRequest; + private unsafe sealed record BetaRequest() : IRequest>; - public unsafe sealed class BetaHandler : IRequestHandler + public unsafe sealed class BetaHandler : IRequestHandler> { } - public unsafe sealed class AlphaHandler : IRequestHandler + public unsafe sealed class AlphaHandler : IRequestHandler> { } } @@ -1359,6 +1467,9 @@ public class CqrsHandlerRegistryGeneratorTests 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(); @@ -1368,6 +1479,7 @@ public class CqrsHandlerRegistryGeneratorTests Assert.Multiple(() => { + Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306")); Assert.That(generatedCompilationErrors, Is.Empty); Assert.That(generatorErrors, Is.Empty); Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1)); @@ -1480,6 +1592,11 @@ public class CqrsHandlerRegistryGeneratorTests (filename: sourceResult.HintName, content: sourceResult.SourceText.ToString())) .ToArray(); var compilationDiagnostics = updatedCompilation.GetDiagnostics().ToArray(); + var inputCompilationDiagnostics = compilationDiagnostics + .Where(diagnostic => + diagnostic.Location.SourceTree is null || + !generatedSyntaxTrees.Contains(diagnostic.Location.SourceTree)) + .ToArray(); var generatedCompilationDiagnostics = compilationDiagnostics .Where(diagnostic => diagnostic.Location.SourceTree is not null && @@ -1489,6 +1606,7 @@ public class CqrsHandlerRegistryGeneratorTests generatedSources, generatorDiagnostics.ToArray(), compilationDiagnostics, + inputCompilationDiagnostics, generatedCompilationDiagnostics); } @@ -1498,10 +1616,12 @@ public class CqrsHandlerRegistryGeneratorTests /// 本轮生成产生的源文件集合。 /// 生成器自身报告的诊断集合。 /// 将生成结果并回编译后的完整编译诊断集合。 + /// 仅来自输入源文件的编译诊断集合。 /// 仅来自生成源文件的编译诊断集合。 private sealed record GeneratorExecutionResult( (string filename, string content)[] GeneratedSources, Diagnostic[] GeneratorDiagnostics, Diagnostic[] CompilationDiagnostics, + Diagnostic[] InputCompilationDiagnostics, Diagnostic[] GeneratedCompilationDiagnostics); } diff --git a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md index 4dda7fbc..86a99c7a 100644 --- a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md +++ b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md @@ -7,12 +7,17 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-044` +- 恢复点编号:`CQRS-REWRITE-RP-050` - 当前阶段:`Phase 8` - 当前焦点: - 当前功能历史已归档,active 跟踪仅保留 `Phase 8` 主线的恢复入口 - - 短期上先处理 `PR #253` 的 latest head review thread 复核,确认当前本地修正是否已在远端收敛 - - 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,扩大 generator 覆盖、减少 dispatch/invoker 热路径反射,并继续收口 package / facade / 兼容层 + - 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke` + - 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物 + - 已修正 pointer / function pointer 泛型合同的错误覆盖:生成器不再为这两类类型发射 precise runtime type 重建代码 + - 已补充非法 CQRS 泛型合同的输入诊断断言,明确 `CS0306` 与 fallback / diagnostic 路径的组合语义 + - 已为 registrar 的 reflection 注册路径补充 handler-interface 元数据缓存,减少跨容器重复注册时的 `GetInterfaces()` 反射 + - 已将 registrar 的重复映射判定从线性扫描 `IServiceCollection` 收敛为本地映射索引,减少 fallback 注册路径的重复查找 + - 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点 ## 当前状态摘要 @@ -25,11 +30,28 @@ CQRS 迁移与收敛。 ## 当前活跃事实 - `Phase 8` 仍是当前主线,不再回退到 `Phase 7` -- 最近一轮功能恢复点是 `RP-043`: - - tracking 顶部阶段与恢复建议已对齐到 `Phase 8` - - `$gframework-pr-review` 会在 open thread 中显式提醒“`Addressed in commit ...` 文案不等于线程已关闭” -- 若当前分支已推送,应优先重新执行 `$gframework-pr-review`,确认 PR `#253` 的 latest head review threads 是否已收敛 -- 若 PR review 噪音已收敛,再回到以下主线优先级: +- `2026-04-20` 已重新执行 `$gframework-pr-review`: + - 当前分支对应 `PR #261`,状态为 `OPEN` + - latest reviewed commit 当前剩余 `1` 条 open CodeRabbit thread,指向 `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md` 中 `RP-047` 与 `RP-050` 的历史语义冲突 + - 本地已同步修正该追踪歧义:`RP-047` 明确标注为已被 `RP-050` 覆盖,后续不得恢复 `MakePointerType()` precise registration + - 远端测试信号保持通过:最新 CTRF 汇总为 `2118/2118` passed;MegaLinter 仅剩 `dotnet-format` restore failure 预警,当前未提供本地仍然成立的文件级格式问题 +- `2026-04-20` 已完成一轮冷启动反射收敛: + - generated registry 类型首次分析后,会缓存一个可复用的激活工厂,而不是在后续容器注册时重复走 `ConstructorInfo.Invoke` + - 若运行环境不允许动态方法,仍保留原有的反射激活回退,避免阻塞 generated registry 路径 + - `GFramework.Cqrs.Tests` 已补充“私有无参构造 registry 仍可激活”的回归覆盖 +- `2026-04-20` 已完成一轮 generator 覆盖面扩展: + - `CqrsHandlerRegistryGenerator` 现会在 runtime type 建模入口直接拒绝 `IPointerTypeSymbol` 与 `IFunctionPointerTypeSymbol` + - `CanReferenceFromGeneratedRegistry` 不再递归判断 pointer / function pointer 的内部元素,而是统一返回 `false` + - 相关 source-generator 回归已改为区分输入源诊断与生成源诊断,避免把非法泛型合同误判为成功生成 +- `2026-04-20` 已完成一轮 registrar reflection 路径收敛: + - `CqrsHandlerRegistrar` 现会按 `Type` 弱键缓存已筛选且排序好的 supported handler interface 列表 + - 同一 handler 类型跨容器重复注册时,不再重复执行 `GetInterfaces()` 与支持接口筛选 + - `GFramework.Cqrs.Tests` 已补充 registrar 静态缓存隔离与 supported interface 缓存复用回归 +- `2026-04-20` 已完成一轮 registrar 去重路径收敛: + - `CqrsHandlerRegistrar` 现会在单次 reflection 注册流程开始时构建已注册 handler 映射索引 + - 同一批注册中后续 duplicate handler mapping 不再重复线性扫描 `IServiceCollection` + - `GFramework.Cqrs.Tests` 已补充“程序集返回重复 handler 类型时仍只注册一份映射”的回归 +- 当前主线优先级: - generator 覆盖面继续扩大 - dispatch/invoker 反射占比继续下降 - package / facade / 兼容层继续收口 @@ -50,9 +72,21 @@ CQRS 迁移与收敛。 - `RP-043` 之前的详细阶段记录、定向验证命令和阶段性决策均已移入主题内归档 - active 跟踪文件只保留当前恢复点、当前活跃事实、风险和下一步,避免 `boot` 在默认入口中重复扫描 1000+ 行历史 trace +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false` + - 结果:通过 + - 备注:`63/63` 测试通过;当前沙箱限制了 MSBuild named pipe,验证需在提权环境下运行 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`14/14` 测试通过;本轮覆盖 pointer / function pointer 合同拒绝、fallback 诊断与现有精确注册路径 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Reports_Compilation_Error_And_Skips_Precise_Registration_For_Hidden_Pointer_Response|FullyQualifiedName~Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute|FullyQualifiedName~Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available"` + - 结果:通过 + - 备注:`3/3` 测试通过;本轮直接覆盖 PR #261 指向的 3 个 pointer / function pointer 回归场景 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` + - 结果:通过 + - 备注:`11/11` 测试通过;本轮覆盖 registrar 的 supported handler interface 缓存与 duplicate mapping 去重路径 ## 下一步 -1. 推送当前分支后重新执行 `$gframework-pr-review`,确认 `PR #253` 的 latest head review threads 是否已收敛 -2. 若 PR review 已收敛,回到 `Phase 8` 主线,优先选择一个收益明确的反射收敛点继续推进 -3. 若继续文档主线,优先再扫 `docs/zh-CN/api-reference` 与教程入口页,补齐仍过时的 CQRS API / 命名空间表述 +1. 继续 `Phase 8` 主线,优先再找一个收益明确的 generator 覆盖缺口或 dispatch / invoker 反射收敛点继续推进 +2. 若继续文档主线,优先再扫 `docs/zh-CN/api-reference` 与教程入口页,补齐仍过时的 CQRS API / 命名空间表述 +3. 若后续再出现新的 PR review 或 review thread 变化,再重新执行 `$gframework-pr-review` 作为独立验证步骤 diff --git a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md index dc02bae2..307dcf55 100644 --- a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md +++ b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md @@ -1,14 +1,67 @@ # CQRS 重写迁移追踪 -## 2026-04-19 +## 2026-04-20 -### 阶段:active 入口归档收口(CQRS-REWRITE-RP-044) +### 阶段:pointer / function pointer 泛型合同拒绝(CQRS-REWRITE-RP-050) -- 已将截至 `RP-043` 的详细实现历史、验证记录与阶段性 trace 迁入主题内归档 -- active trace 现在只保留当前恢复点与下一步,避免默认 boot 入口继续读取 1400+ 行已完成历史 -- 当前功能主线保持不变: - - 先复核 `PR #253` 的 latest head review threads 是否已收敛 - - 再继续 `Phase 8` 的 generator / dispatch / package 收口工作 +- 重新执行 `$gframework-pr-review` 后,确认当前分支对应 `PR #261`,状态仍为 `OPEN` +- latest reviewed commit 当前剩余 `1` 条 open CodeRabbit thread,指向 `RP-047` 历史记录仍把 `MakePointerType()` precise registration 写成现行路径 +- 本地核对后确认该评论有效:当前 pointer / function pointer 语义已由 `RP-050` 收敛为 fallback / diagnostic 路径,历史追踪必须显式标注 `RP-047` 已废弃,避免后续恢复时误回滚到旧方案 +- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中收紧 `TryCreateRuntimeTypeReference` 与 `CanReferenceFromGeneratedRegistry` +- pointer / function pointer 现统一视为不可精确生成的 CQRS 泛型合同,生成器会保守回退到既有 fallback / diagnostic 路径,而不再发射运行时 `MakeGenericType(...)` 风险代码 +- 已在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 中补充输入源诊断分离,并将相关测试改为显式断言 `CS0306` 与 fallback / diagnostic 结果 +- 已同步修正 `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md` 中 `RP-047` 段落,明确其已被 `RP-050` 覆盖,且不得恢复 `MakePointerType()` precise registration +- 定向验证已通过: + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Reports_Compilation_Error_And_Skips_Precise_Registration_For_Hidden_Pointer_Response|FullyQualifiedName~Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute|FullyQualifiedName~Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available"` + - `3/3` passed +- 扩展验证已通过: + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - `14/14` passed + +### 阶段:registrar duplicate mapping 索引收敛(CQRS-REWRITE-RP-049) + +- 已将 `CqrsHandlerRegistrar` 的重复 handler mapping 判定从逐条线性扫描 `IServiceCollection` 收敛为单次构建的本地映射索引 +- reflection fallback 或重复类型输入场景下,后续 duplicate mapping 判定改为 `HashSet` 命中,不再重复遍历已有服务描述符 +- `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs` 已补充“程序集枚举返回重复 handler 类型时仍只注册一份映射”的回归 +- 定向验证已通过: + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` + - `11/11` passed + - 当前沙箱限制 MSBuild named pipe,因此验证在提权环境下执行 + +### 阶段:registrar handler-interface 反射缓存(CQRS-REWRITE-RP-048) + +- 已在 `CqrsHandlerRegistrar` 中新增按 `Type` 弱键缓存的 supported handler interface 元数据,reflection 注册路径现会复用已筛选且排序好的接口列表 +- 同一 handler 类型跨容器重复注册时,不再重复执行 `GetInterfaces()` 与支持接口筛选;缓存仍保持卸载安全,不会长期钉住 collectible 类型 +- `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs` 已补充 registrar 静态缓存清理与 supported interface 缓存复用回归 +- 定向验证已通过: + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` + - `10/10` passed + - 当前沙箱限制 MSBuild named pipe,因此验证在提权环境下执行 + +### 阶段:pointer precise runtime type 覆盖扩展(CQRS-REWRITE-RP-047,已由 RP-050 覆盖) + +- 曾在 `CqrsHandlerRegistryGenerator` 中尝试补充 pointer 类型的 runtime type 递归建模与源码发射,计划通过 `MakePointerType()` 还原隐藏 pointer 响应类型 +- 该方案后续已被 `RP-050` 明确废弃:pointer / function pointer 不能作为 CQRS 泛型合同的 precise registration 输入,当前实现统一回到 fallback / diagnostic 路径,不能恢复到 `MakePointerType()` 精确注册 +- 已同步收紧 function pointer 签名的可直接生成判定,只有当签名中的返回值与参数类型均可从 generated registry 安全引用时才走静态注册 +- 已保留含隐藏类型 function pointer handler 的 fallback / 诊断回归覆盖,确保 pointer 支持扩展不会误删原有程序集级 fallback 契约边界 +- 后续若需恢复当前 pointer / function pointer 行为,应以 `RP-050` 为权威记录,而不是继续沿用本阶段的旧设计假设 +- 定向验证与 `CqrsHandlerRegistryGeneratorTests` 全组验证均已通过: + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Generates_Precise_Service_Type_For_Hidden_Pointer_Response|FullyQualifiedName~Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute|FullyQualifiedName~Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available"` + - `3/3` passed + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - `14/14` passed + - 当前沙箱限制 MSBuild named pipe,因此验证在提权环境下执行 + +### 阶段:generated registry 激活反射收敛(CQRS-REWRITE-RP-046) + +- 已在 `CqrsHandlerRegistrar` 中将 generated registry 的无参构造激活改为类型级缓存工厂 +- 默认路径优先使用一次性动态方法直接创建 registry,避免后续每次命中缓存仍走 `ConstructorInfo.Invoke` +- 若运行环境不允许动态方法,则保留原有反射激活回退,确保 generated registry 路径不因运行时限制失效 +- 已补充“私有无参构造 generated registry 仍可激活”的回归测试,覆盖现有生成器产物兼容性 +- 定向验证已通过: + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false` + - `63/63` passed + - 当前沙箱限制 MSBuild named pipe,因此验证在提权环境下执行 ### Archive Context @@ -19,6 +72,6 @@ ### 当前下一步 -1. 推送当前分支后重新执行 `$gframework-pr-review` -2. 以 latest head review thread 状态和本地文件事实为准,确认 `RP-042` / `RP-043` 修正是否真正收敛 -3. 若收敛完成,回到 `Phase 8` 主线,优先选一个明确的反射缩减点继续推进 +1. 回到 `Phase 8` 主线,优先选一个明确的 dispatch / invoker 反射缩减点继续推进 +2. 若继续文档主线,优先补齐 `docs/zh-CN/api-reference` 与教程入口页中仍过时的 CQRS API / 命名空间表述 +3. 若后续 review thread 或 PR 状态再次变化,再重新执行 `$gframework-pr-review` 复核远端信号