diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs index e86ec1a0..ccd7e8cb 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs @@ -60,7 +60,6 @@ public sealed partial class CqrsHandlerRegistryGenerator string? ReflectionAssemblyName, RuntimeTypeReferenceSpec? ArrayElementTypeReference, int ArrayRank, - RuntimeTypeReferenceSpec? PointerElementTypeReference, RuntimeTypeReferenceSpec? GenericTypeDefinitionReference, ImmutableArray GenericTypeArguments) { @@ -76,7 +75,6 @@ public sealed partial class CqrsHandlerRegistryGenerator null, 0, null, - null, ImmutableArray.Empty); } @@ -92,7 +90,6 @@ public sealed partial class CqrsHandlerRegistryGenerator null, 0, null, - null, ImmutableArray.Empty); } @@ -110,7 +107,6 @@ public sealed partial class CqrsHandlerRegistryGenerator null, 0, null, - null, ImmutableArray.Empty); } @@ -126,7 +122,6 @@ public sealed partial class CqrsHandlerRegistryGenerator elementTypeReference, arrayRank, null, - null, ImmutableArray.Empty); } @@ -143,7 +138,6 @@ public sealed partial class CqrsHandlerRegistryGenerator null, null, 0, - null, genericTypeDefinitionReference, genericTypeArguments); } diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs index bce72464..8157d45d 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs @@ -304,12 +304,6 @@ public sealed partial class CqrsHandlerRegistryGenerator return true; } - if (runtimeTypeReference.PointerElementTypeReference is not null && - ContainsExternalAssemblyTypeLookup(runtimeTypeReference.PointerElementTypeReference)) - { - return true; - } - if (runtimeTypeReference.GenericTypeDefinitionReference is not null && ContainsExternalAssemblyTypeLookup(runtimeTypeReference.GenericTypeDefinitionReference)) { diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs index 4db622cb..9dd72b7f 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs @@ -662,14 +662,6 @@ public sealed partial class CqrsHandlerRegistryGenerator reflectedArgumentNames, indent); - if (runtimeTypeReference.PointerElementTypeReference is not null) - return AppendPointerRuntimeTypeReferenceResolution( - builder, - runtimeTypeReference, - variableBaseName, - reflectedArgumentNames, - indent); - if (runtimeTypeReference.GenericTypeDefinitionReference is not null) return AppendConstructedGenericRuntimeTypeReferenceResolution( builder, @@ -714,32 +706,6 @@ public sealed partial class CqrsHandlerRegistryGenerator : $"{elementExpression}.MakeArrayType({runtimeTypeReference.ArrayRank})"; } - /// - /// 发射指针类型引用的运行时重建表达式。 - /// - /// 生成源码构造器。 - /// 指针类型引用描述。 - /// 用于递归生成变量名的稳定前缀。 - /// 需要空值检查的反射解析变量集合。 - /// 当前生成语句的缩进。 - /// 指针类型表达式。 - private static string AppendPointerRuntimeTypeReferenceResolution( - StringBuilder builder, - RuntimeTypeReferenceSpec runtimeTypeReference, - string variableBaseName, - ICollection reflectedArgumentNames, - string indent) - { - var pointedAtExpression = AppendRuntimeTypeReferenceResolution( - builder, - runtimeTypeReference.PointerElementTypeReference!, - $"{variableBaseName}PointedAt", - reflectedArgumentNames, - indent); - - return $"{pointedAtExpression}.MakePointerType()"; - } - /// /// 发射已构造泛型类型引用的运行时重建表达式。 /// diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs index ee8c549a..cd95d2d8 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs @@ -10,6 +10,7 @@ namespace GFramework.Cqrs.Tests.Cqrs; /// 验证 CQRS dispatcher 会缓存热路径中的 dispatch binding。 /// [TestFixture] +[NonParallelizable] internal sealed class CqrsDispatcherCacheTests { private MicrosoftDiContainer? _container; @@ -24,6 +25,9 @@ internal sealed class CqrsDispatcherCacheTests LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); _container = new MicrosoftDiContainer(); _container.RegisterCqrsPipelineBehavior(); + _container.RegisterCqrsPipelineBehavior(); + _container.RegisterCqrsPipelineBehavior(); + _container.RegisterCqrsPipelineBehavior(); CqrsTestRuntime.RegisterHandlers( _container, @@ -32,6 +36,9 @@ internal sealed class CqrsDispatcherCacheTests _container.Freeze(); _context = new ArchitectureContext(_container); + DispatcherNotificationContextRefreshState.Reset(); + DispatcherPipelineContextRefreshState.Reset(); + DispatcherStreamContextRefreshState.Reset(); ClearDispatcherCaches(); } @@ -145,6 +152,243 @@ internal sealed class CqrsDispatcherCacheTests }); } + /// + /// 验证 request pipeline executor 会按行为数量在 binding 内首次创建并在后续分发中复用。 + /// + [Test] + public async Task Dispatcher_Should_Cache_Request_Pipeline_Executors_Per_Behavior_Count() + { + var requestBindings = GetCacheField("RequestDispatchBindings"); + + Assert.Multiple(() => + { + Assert.That( + GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineCacheRequest), + typeof(int), + 1), + Is.Null); + Assert.That( + GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineOrderCacheRequest), + typeof(int), + 2), + Is.Null); + }); + + await _context!.SendRequestAsync(new DispatcherPipelineCacheRequest()); + await _context.SendRequestAsync(new DispatcherPipelineOrderCacheRequest()); + + var singleBehaviorExecutor = GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineCacheRequest), + typeof(int), + 1); + var twoBehaviorExecutor = GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineOrderCacheRequest), + typeof(int), + 2); + + await _context.SendRequestAsync(new DispatcherPipelineCacheRequest()); + await _context.SendRequestAsync(new DispatcherPipelineOrderCacheRequest()); + + Assert.Multiple(() => + { + Assert.That(singleBehaviorExecutor, Is.Not.Null); + Assert.That(twoBehaviorExecutor, Is.Not.Null); + Assert.That(singleBehaviorExecutor, Is.Not.SameAs(twoBehaviorExecutor)); + Assert.That( + GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineCacheRequest), + typeof(int), + 1), + Is.SameAs(singleBehaviorExecutor)); + Assert.That( + GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineOrderCacheRequest), + typeof(int), + 2), + Is.SameAs(twoBehaviorExecutor)); + }); + } + + /// + /// 验证复用缓存的 request pipeline executor 后,行为顺序和最终处理器顺序保持不变。 + /// + [Test] + public async Task Dispatcher_Should_Preserve_Request_Pipeline_Order_When_Reusing_Cached_Executor() + { + DispatcherPipelineOrderState.Reset(); + + await _context!.SendRequestAsync(new DispatcherPipelineOrderCacheRequest()); + var firstInvocation = DispatcherPipelineOrderState.Steps.ToArray(); + + DispatcherPipelineOrderState.Reset(); + + await _context.SendRequestAsync(new DispatcherPipelineOrderCacheRequest()); + var secondInvocation = DispatcherPipelineOrderState.Steps.ToArray(); + + var expectedOrder = new[] + { + "Outer:Before", + "Inner:Before", + "Handler", + "Inner:After", + "Outer:After" + }; + + Assert.Multiple(() => + { + Assert.That(firstInvocation, Is.EqualTo(expectedOrder)); + Assert.That(secondInvocation, Is.EqualTo(expectedOrder)); + }); + } + + /// + /// 验证缓存的 request pipeline executor 在重复分发时仍会重新解析 handler/behavior, + /// 并为当次实例重新注入当前架构上下文。 + /// + [Test] + public async Task Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Request_Pipeline_Executor() + { + DispatcherPipelineContextRefreshState.Reset(); + + var requestBindings = GetCacheField("RequestDispatchBindings"); + var firstContext = new ArchitectureContext(_container!); + var secondContext = new ArchitectureContext(_container!); + + await firstContext.SendRequestAsync(new DispatcherPipelineContextRefreshRequest("first")); + + var executorAfterFirstDispatch = GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineContextRefreshRequest), + typeof(int), + 1); + + await secondContext.SendRequestAsync(new DispatcherPipelineContextRefreshRequest("second")); + + var executorAfterSecondDispatch = GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineContextRefreshRequest), + typeof(int), + 1); + var behaviorSnapshots = DispatcherPipelineContextRefreshState.BehaviorSnapshots.ToArray(); + var handlerSnapshots = DispatcherPipelineContextRefreshState.HandlerSnapshots.ToArray(); + + Assert.Multiple(() => + { + Assert.That(executorAfterFirstDispatch, Is.Not.Null); + Assert.That(executorAfterSecondDispatch, Is.SameAs(executorAfterFirstDispatch)); + + Assert.That(behaviorSnapshots, Has.Length.EqualTo(2)); + Assert.That(handlerSnapshots, Has.Length.EqualTo(2)); + + Assert.That(behaviorSnapshots[0].DispatchId, Is.EqualTo("first")); + Assert.That(behaviorSnapshots[0].Context, Is.SameAs(firstContext)); + Assert.That(behaviorSnapshots[1].DispatchId, Is.EqualTo("second")); + Assert.That(behaviorSnapshots[1].Context, Is.SameAs(secondContext)); + Assert.That(behaviorSnapshots[1].Context, Is.Not.SameAs(behaviorSnapshots[0].Context)); + + Assert.That(handlerSnapshots[0].DispatchId, Is.EqualTo("first")); + Assert.That(handlerSnapshots[0].Context, Is.SameAs(firstContext)); + Assert.That(handlerSnapshots[1].DispatchId, Is.EqualTo("second")); + Assert.That(handlerSnapshots[1].Context, Is.SameAs(secondContext)); + Assert.That(handlerSnapshots[1].Context, Is.Not.SameAs(handlerSnapshots[0].Context)); + Assert.That(handlerSnapshots[1].InstanceId, Is.Not.EqualTo(handlerSnapshots[0].InstanceId)); + }); + } + + /// + /// 验证缓存的 notification dispatch binding 在重复分发时仍会重新解析 handler, + /// 并为当次实例重新注入当前架构上下文。 + /// + [Test] + public async Task Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Notification_Dispatch_Binding() + { + DispatcherNotificationContextRefreshState.Reset(); + + var notificationBindings = GetCacheField("NotificationDispatchBindings"); + var firstContext = new ArchitectureContext(_container!); + var secondContext = new ArchitectureContext(_container!); + + await firstContext.PublishAsync(new DispatcherNotificationContextRefreshNotification("first")); + + var bindingAfterFirstDispatch = GetSingleKeyCacheValue( + notificationBindings, + typeof(DispatcherNotificationContextRefreshNotification)); + + await secondContext.PublishAsync(new DispatcherNotificationContextRefreshNotification("second")); + + var bindingAfterSecondDispatch = GetSingleKeyCacheValue( + notificationBindings, + typeof(DispatcherNotificationContextRefreshNotification)); + var handlerSnapshots = DispatcherNotificationContextRefreshState.HandlerSnapshots.ToArray(); + + Assert.Multiple(() => + { + Assert.That(bindingAfterFirstDispatch, Is.Not.Null); + Assert.That(bindingAfterSecondDispatch, Is.SameAs(bindingAfterFirstDispatch)); + + Assert.That(handlerSnapshots, Has.Length.EqualTo(2)); + Assert.That(handlerSnapshots[0].DispatchId, Is.EqualTo("first")); + Assert.That(handlerSnapshots[0].Context, Is.SameAs(firstContext)); + Assert.That(handlerSnapshots[1].DispatchId, Is.EqualTo("second")); + Assert.That(handlerSnapshots[1].Context, Is.SameAs(secondContext)); + Assert.That(handlerSnapshots[1].Context, Is.Not.SameAs(handlerSnapshots[0].Context)); + Assert.That(handlerSnapshots[1].InstanceId, Is.Not.EqualTo(handlerSnapshots[0].InstanceId)); + }); + } + + /// + /// 验证缓存的 stream dispatch binding 在重复建流时仍会重新解析 handler, + /// 并为当次实例重新注入当前架构上下文。 + /// + [Test] + public async Task Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Stream_Dispatch_Binding() + { + DispatcherStreamContextRefreshState.Reset(); + + var streamBindings = GetCacheField("StreamDispatchBindings"); + var firstContext = new ArchitectureContext(_container!); + var secondContext = new ArchitectureContext(_container!); + + var firstStream = firstContext.CreateStream(new DispatcherStreamContextRefreshRequest("first")); + await DrainAsync(firstStream); + + var bindingAfterFirstDispatch = GetPairCacheValue( + streamBindings, + typeof(DispatcherStreamContextRefreshRequest), + typeof(int)); + + var secondStream = secondContext.CreateStream(new DispatcherStreamContextRefreshRequest("second")); + await DrainAsync(secondStream); + + var bindingAfterSecondDispatch = GetPairCacheValue( + streamBindings, + typeof(DispatcherStreamContextRefreshRequest), + typeof(int)); + var handlerSnapshots = DispatcherStreamContextRefreshState.HandlerSnapshots.ToArray(); + + Assert.Multiple(() => + { + Assert.That(bindingAfterFirstDispatch, Is.Not.Null); + Assert.That(bindingAfterSecondDispatch, Is.SameAs(bindingAfterFirstDispatch)); + + Assert.That(handlerSnapshots, Has.Length.EqualTo(2)); + Assert.That(handlerSnapshots[0].DispatchId, Is.EqualTo("first")); + Assert.That(handlerSnapshots[0].Context, Is.SameAs(firstContext)); + Assert.That(handlerSnapshots[1].DispatchId, Is.EqualTo("second")); + Assert.That(handlerSnapshots[1].Context, Is.SameAs(secondContext)); + Assert.That(handlerSnapshots[1].Context, Is.Not.SameAs(handlerSnapshots[0].Context)); + Assert.That(handlerSnapshots[1].InstanceId, Is.Not.EqualTo(handlerSnapshots[0].InstanceId)); + }); + } + /// /// 通过反射读取 dispatcher 的静态缓存对象。 /// @@ -188,6 +432,26 @@ internal sealed class CqrsDispatcherCacheTests return InvokeInstanceMethod(cache, "GetValueOrDefaultForTesting", primaryType, secondaryType); } + /// + /// 读取 request dispatch binding 中指定行为数量的 pipeline executor 缓存项。 + /// + /// dispatcher 内部的 request binding 缓存对象。 + /// 要读取的请求运行时类型。 + /// 要读取的响应运行时类型。 + /// 目标 executor 对应的行为数量。 + /// 已缓存的 executor;若 binding 或 executor 尚未建立则返回 + private static object? GetRequestPipelineExecutorValue( + object requestBindings, + Type requestType, + Type responseType, + int behaviorCount) + { + var binding = GetRequestDispatchBindingValue(requestBindings, requestType, responseType); + return binding is null + ? null + : InvokeInstanceMethod(binding, "GetPipelineExecutorForTesting", behaviorCount); + } + /// /// 调用缓存实例上的无参清理方法。 /// @@ -210,6 +474,32 @@ internal sealed class CqrsDispatcherCacheTests return method!.Invoke(target, arguments); } + /// + /// 读取指定请求/响应类型对对应的强类型 request dispatch binding。 + /// + /// dispatcher 内部的 request binding 缓存对象。 + /// 要读取的请求运行时类型。 + /// 要读取的响应运行时类型。 + /// 强类型 binding;若缓存尚未建立则返回 + private static object? GetRequestDispatchBindingValue(object requestBindings, Type requestType, Type responseType) + { + var bindingBox = GetPairCacheValue(requestBindings, requestType, responseType); + if (bindingBox is null) + { + return null; + } + + var method = bindingBox.GetType().GetMethod( + "Get", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + Assert.That(method, Is.Not.Null, $"Missing request binding accessor on {bindingBox.GetType().FullName}."); + + return method! + .MakeGenericMethod(responseType) + .Invoke(bindingBox, Array.Empty()); + } + /// /// 获取 CQRS dispatcher 运行时类型。 /// diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs new file mode 100644 index 00000000..bc12c773 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs @@ -0,0 +1,174 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Cqrs; +using GFramework.Cqrs.Tests.Logging; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 验证默认 dispatcher 在上下文注入前置条件不满足时的失败语义。 +/// +[TestFixture] +internal sealed class CqrsDispatcherContextValidationTests +{ + /// + /// 验证当 request handler 需要上下文注入、但当前 CQRS 上下文不实现 时, + /// dispatcher 会在调用前显式失败。 + /// + [Test] + public void SendAsync_Should_Throw_When_Context_Does_Not_Implement_IArchitectureContext() + { + var runtime = CreateRuntime( + container => + { + container + .Setup(currentContainer => currentContainer.Get(typeof(IRequestHandler))) + .Returns(new ContextAwareRequestHandler()); + container + .Setup(currentContainer => currentContainer.GetAll(typeof(IPipelineBehavior))) + .Returns(Array.Empty()); + }); + + Assert.That( + async () => await runtime.SendAsync(new FakeCqrsContext(), new ContextAwareRequest()).ConfigureAwait(false), + Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext")); + } + + /// + /// 验证当 notification handler 需要上下文注入、但当前 CQRS 上下文不实现 时, + /// dispatcher 会在发布前显式失败。 + /// + [Test] + public void PublishAsync_Should_Throw_When_Context_Does_Not_Implement_IArchitectureContext() + { + var runtime = CreateRuntime( + container => + { + container + .Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler))) + .Returns([new ContextAwareNotificationHandler()]); + }); + + Assert.That( + async () => await runtime.PublishAsync(new FakeCqrsContext(), new ContextAwareNotification()).ConfigureAwait(false), + Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext")); + } + + /// + /// 验证当 stream handler 需要上下文注入、但当前 CQRS 上下文不实现 时, + /// dispatcher 会在建流前显式失败。 + /// + [Test] + public void CreateStream_Should_Throw_When_Context_Does_Not_Implement_IArchitectureContext() + { + var runtime = CreateRuntime( + container => + { + container + .Setup(currentContainer => currentContainer.Get(typeof(IStreamRequestHandler))) + .Returns(new ContextAwareStreamHandler()); + }); + + Assert.That( + () => runtime.CreateStream(new FakeCqrsContext(), new ContextAwareStreamRequest()), + Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext")); + } + + /// + /// 创建一个只满足当前测试最小依赖面的 dispatcher runtime。 + /// + /// 对容器 mock 的额外配置。 + /// 默认 CQRS runtime。 + private static GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime CreateRuntime( + Action> configureContainer) + { + var container = new Mock(MockBehavior.Strict); + var logger = new TestLogger("CqrsDispatcherContextValidationTests", LogLevel.Debug); + + configureContainer(container); + return CqrsRuntimeFactory.CreateRuntime(container.Object, logger); + } + + /// + /// 为失败语义测试提供最小 CQRS 上下文标记,但故意不实现架构上下文能力。 + /// + private sealed class FakeCqrsContext : ICqrsContext + { + } + + /// + /// 为 request 上下文校验提供最小测试请求。 + /// + private sealed record ContextAwareRequest : IRequest; + + /// + /// 为 notification 上下文校验提供最小测试通知。 + /// + private sealed record ContextAwareNotification : INotification; + + /// + /// 为 stream 上下文校验提供最小测试请求。 + /// + private sealed record ContextAwareStreamRequest : IStreamRequest; + + /// + /// 为 request 上下文校验提供需要注入架构上下文的最小 handler。 + /// + private sealed class ContextAwareRequestHandler : CqrsContextAwareHandlerBase, IRequestHandler + { + /// + /// 返回固定结果;当前测试只关心调用前的上下文校验。 + /// + /// 当前请求。 + /// 取消令牌。 + /// 固定整型结果。 + public ValueTask Handle(ContextAwareRequest request, CancellationToken cancellationToken) + { + return ValueTask.FromResult(1); + } + } + + /// + /// 为 notification 上下文校验提供需要注入架构上下文的最小 handler。 + /// + private sealed class ContextAwareNotificationHandler + : CqrsContextAwareHandlerBase, + INotificationHandler + { + /// + /// 返回已完成任务;当前测试只关心调用前的上下文校验。 + /// + /// 当前通知。 + /// 取消令牌。 + /// 已完成任务。 + public ValueTask Handle(ContextAwareNotification notification, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } + } + + /// + /// 为 stream 上下文校验提供需要注入架构上下文的最小 handler。 + /// + private sealed class ContextAwareStreamHandler + : CqrsContextAwareHandlerBase, + IStreamRequestHandler + { + /// + /// 返回一个最小流;当前测试只关心建流前的上下文校验。 + /// + /// 当前流请求。 + /// 取消枚举时使用的取消令牌。 + /// 包含单个固定元素的异步流。 + public async IAsyncEnumerable Handle( + ContextAwareStreamRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + yield return 1; + await ValueTask.CompletedTask.ConfigureAwait(false); + } + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs new file mode 100644 index 00000000..e4d7ef3f --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs @@ -0,0 +1,259 @@ +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Ioc; +using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Tests.Logging; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 验证 CQRS handler registrar 在 reflection fallback 元数据失效时的可观察告警行为。 +/// +[TestFixture] +[NonParallelizable] +internal sealed class CqrsHandlerRegistrarFallbackFailureTests +{ + private ILoggerFactoryProvider? _originalLoggerFactoryProvider; + private CapturingLoggerFactoryProvider? _capturingLoggerFactoryProvider; + + /// + /// 切换为捕获型日志工厂,并清空 registrar 进程级缓存,避免跨用例共享状态污染断言。 + /// + [SetUp] + public void SetUp() + { + _originalLoggerFactoryProvider = LoggerFactoryResolver.Provider; + _capturingLoggerFactoryProvider = new CapturingLoggerFactoryProvider(LogLevel.Warning); + LoggerFactoryResolver.Provider = _capturingLoggerFactoryProvider; + ClearRegistrarCaches(); + } + + /// + /// 恢复测试前的日志工厂,并清理 registrar 缓存。 + /// + [TearDown] + public void TearDown() + { + LoggerFactoryResolver.Provider = _originalLoggerFactoryProvider!; + _capturingLoggerFactoryProvider = null; + _originalLoggerFactoryProvider = null; + ClearRegistrarCaches(); + } + + /// + /// 验证当 fallback 类型名无法解析时,registrar 会跳过该条目并记录告警。 + /// + [Test] + public void RegisterHandlers_Should_Skip_Unresolvable_Named_Fallback_And_Log_Warning() + { + const string missingTypeName = + "GFramework.Cqrs.Tests.Cqrs.MissingGeneratedRegistryNotificationHandler"; + var generatedAssembly = CreateGeneratedFallbackAssembly( + "GFramework.Cqrs.Tests.Cqrs.NamedFallbackMissingAssembly, Version=1.0.0.0", + new CqrsReflectionFallbackAttribute(missingTypeName)); + generatedAssembly + .Setup(static assembly => assembly.GetType(missingTypeName, false, false)) + .Returns((Type?)null); + + var container = new MicrosoftDiContainer(); + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + + Assert.Multiple(() => + { + Assert.That( + GetGeneratedRegistryNotificationHandlerTypes(container), + Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)])); + Assert.That( + GetWarningLogs().Any(log => + log.Message.Contains( + $"Generated CQRS reflection fallback type {missingTypeName} could not be resolved", + StringComparison.Ordinal)), + Is.True); + }); + } + + /// + /// 验证当 fallback 类型名解析抛出异常时,registrar 会记录该加载失败告警并继续跳过条目。 + /// + [Test] + public void RegisterHandlers_Should_Log_Warning_When_Named_Fallback_Resolution_Throws() + { + const string failingTypeName = + "GFramework.Cqrs.Tests.Cqrs.ThrowingGeneratedRegistryNotificationHandler"; + const string exceptionMessage = "Fallback resolution exploded."; + var generatedAssembly = CreateGeneratedFallbackAssembly( + "GFramework.Cqrs.Tests.Cqrs.NamedFallbackThrowingAssembly, Version=1.0.0.0", + new CqrsReflectionFallbackAttribute(failingTypeName)); + generatedAssembly + .Setup(static assembly => assembly.GetType(failingTypeName, false, false)) + .Throws(new TypeLoadException(exceptionMessage)); + + var container = new MicrosoftDiContainer(); + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + + Assert.Multiple(() => + { + Assert.That( + GetGeneratedRegistryNotificationHandlerTypes(container), + Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)])); + Assert.That( + GetWarningLogs().Any(log => + log.Message.Contains( + $"Generated CQRS reflection fallback type {failingTypeName} failed to load", + StringComparison.Ordinal) && + log.Message.Contains(exceptionMessage, StringComparison.Ordinal)), + Is.True); + }); + } + + /// + /// 验证当 direct fallback 类型属于其他程序集时,registrar 会跳过该条目并记录跨程序集告警。 + /// + [Test] + public void RegisterHandlers_Should_Skip_Cross_Assembly_Direct_Fallback_Type_And_Log_Warning() + { + var crossAssemblyFallbackType = ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType; + var generatedAssembly = CreateGeneratedFallbackAssembly( + "GFramework.Cqrs.Tests.Cqrs.DirectFallbackMismatchAssembly, Version=1.0.0.0", + new CqrsReflectionFallbackAttribute(crossAssemblyFallbackType)); + + var container = new MicrosoftDiContainer(); + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + + Assert.Multiple(() => + { + Assert.That( + GetGeneratedRegistryNotificationHandlerTypes(container), + Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)])); + Assert.That( + GetWarningLogs().Any(log => + log.Message.Contains( + $"Generated CQRS reflection fallback type {crossAssemblyFallbackType.FullName} was declared on assembly", + StringComparison.Ordinal) && + log.Message.Contains("Skipping mismatched fallback entry.", StringComparison.Ordinal)), + Is.True); + }); + } + + /// + /// 创建一个仅通过 generated registry 注册主 handler、并附带指定 fallback 元数据的程序集替身。 + /// + /// 用于日志与缓存键的程序集名。 + /// 要暴露给 registrar 的 fallback attribute。 + /// 已完成基础接线的程序集 mock。 + private static Mock CreateGeneratedFallbackAssembly( + string assemblyName, + CqrsReflectionFallbackAttribute fallbackAttribute) + { + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns(assemblyName); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false)) + .Returns([fallbackAttribute]); + return generatedAssembly; + } + + /// + /// 提取容器中针对 generated notification 注册的处理器实现类型。 + /// + /// 已执行注册的测试容器。 + /// 按注册顺序返回的处理器类型数组。 + private static Type[] GetGeneratedRegistryNotificationHandlerTypes(MicrosoftDiContainer container) + { + return container.GetServicesUnsafe + .Where(static descriptor => + descriptor.ServiceType == typeof(INotificationHandler) && + descriptor.ImplementationType is not null) + .Select(static descriptor => descriptor.ImplementationType!) + .ToArray(); + } + + /// + /// 清空本测试依赖的 registrar 静态缓存,确保每个用例都会重新执行 fallback 元数据解析。 + /// 这些字段名直接耦合 CqrsHandlerRegistrar 当前内部实现;若后续重构缓存布局,需要同步更新这里。 + /// + private static void ClearRegistrarCaches() + { + ClearCache(GetRegistrarCacheField("AssemblyMetadataCache")); + ClearCache(GetRegistrarCacheField("RegistryActivationMetadataCache")); + ClearCache(GetRegistrarCacheField("LoadableTypesCache")); + ClearCache(GetRegistrarCacheField("SupportedHandlerInterfacesCache")); + } + + /// + /// 通过反射读取 registrar 的静态缓存字段。 + /// + /// 缓存字段名。 + /// 缓存实例。 + private static object GetRegistrarCacheField(string fieldName) + { + var field = GetRegistrarType().GetField( + fieldName, + BindingFlags.NonPublic | BindingFlags.Static); + + Assert.That( + field, + Is.Not.Null, + $"Expected field '{fieldName}' on CqrsHandlerRegistrar not found; rename/refactor may require test update."); + + return field!.GetValue(null) + ?? throw new InvalidOperationException( + $"Registrar cache field '{fieldName}' on CqrsHandlerRegistrar returned null."); + } + + /// + /// 清空缓存对象中的已保存条目。 + /// + /// 目标缓存实例。 + private static void ClearCache(object cache) + { + _ = InvokeInstanceMethod(cache, "Clear"); + } + + /// + /// 调用缓存对象上的实例方法。 + /// + /// 目标对象。 + /// 方法名。 + /// 方法参数。 + /// 方法返回值。 + 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 的运行时类型。 + /// + /// registrar 实现类型。 + private static Type GetRegistrarType() + { + return typeof(CqrsReflectionFallbackAttribute).Assembly + .GetType("GFramework.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)!; + } + + /// + /// 汇总当前测试期间捕获到的 warning 日志。 + /// + /// 所有 warning 级别日志条目。 + private IReadOnlyList GetWarningLogs() + { + Assert.That(_capturingLoggerFactoryProvider, Is.Not.Null); + + return _capturingLoggerFactoryProvider!.Loggers + .SelectMany(static logger => logger.Logs) + .Where(static log => log.Level == LogLevel.Warning) + .ToArray(); + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsReflectionFallbackAttributeTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsReflectionFallbackAttributeTests.cs new file mode 100644 index 00000000..53bb2988 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsReflectionFallbackAttributeTests.cs @@ -0,0 +1,97 @@ +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 验证 公开构造器的归一化合同, +/// 以固定 runtime 读取程序集级 fallback 元数据时可依赖的可观察语义。 +/// +[TestFixture] +internal sealed class CqrsReflectionFallbackAttributeTests +{ + /// + /// 验证无参构造器会保留旧版 marker 语义,并暴露空的 fallback 集合。 + /// + [Test] + public void Constructor_Without_Arguments_Should_Expose_Empty_Fallback_Collections() + { + var attribute = new CqrsReflectionFallbackAttribute(); + + Assert.Multiple(() => + { + Assert.That(attribute.FallbackHandlerTypeNames, Is.Empty); + Assert.That(attribute.FallbackHandlerTypes, Is.Empty); + }); + } + + /// + /// 验证字符串名称重载会过滤空白项,并按序号稳定去重排序, + /// 确保 runtime 后续读取到的名称清单不依赖调用端输入顺序。 + /// + [Test] + public void Constructor_With_Type_Names_Should_Normalize_By_Filtering_Deduplicating_And_Sorting() + { + var attribute = new CqrsReflectionFallbackAttribute( + "Zeta.Handler", + " ", + "Alpha.Handler", + "Zeta.Handler", + string.Empty, + "Beta.Handler", + "Alpha.Handler"); + + Assert.Multiple(() => + { + Assert.That( + attribute.FallbackHandlerTypeNames, + Is.EqualTo(["Alpha.Handler", "Beta.Handler", "Zeta.Handler"])); + Assert.That(attribute.FallbackHandlerTypes, Is.Empty); + }); + } + + /// + /// 验证字符串名称重载收到 参数数组时会立即拒绝, + /// 避免 runtime 在读取程序集元数据时延迟暴露无效状态。 + /// + [Test] + public void Constructor_With_Null_Type_Name_Array_Should_Throw_ArgumentNullException() + { + Assert.That( + () => _ = new CqrsReflectionFallbackAttribute((string[])null!), + Throws.ArgumentNullException); + } + + /// + /// 验证 重载会过滤空引用,并按稳定名称顺序去重, + /// 确保后续 fallback 补扫不会因为重复输入或反射枚举顺序产生非确定性。 + /// + [Test] + public void Constructor_With_Types_Should_Normalize_By_Filtering_Deduplicating_And_Sorting() + { + var attribute = new CqrsReflectionFallbackAttribute( + typeof(string), + null!, + typeof(Uri), + typeof(string), + typeof(Version)); + + // 这里按 FullName 的 Ordinal 顺序断言,固定该 attribute 对 runtime 暴露的元数据排序合同。 + Assert.Multiple(() => + { + Assert.That( + attribute.FallbackHandlerTypes, + Is.EqualTo([typeof(string), typeof(Uri), typeof(Version)])); + Assert.That(attribute.FallbackHandlerTypeNames, Is.Empty); + }); + } + + /// + /// 验证 重载收到 参数数组时会立即拒绝, + /// 从而维持 attribute 元数据的最小有效性边界。 + /// + [Test] + public void Constructor_With_Null_Type_Array_Should_Throw_ArgumentNullException() + { + Assert.That( + () => _ = new CqrsReflectionFallbackAttribute((Type[])null!), + Throws.ArgumentNullException); + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs new file mode 100644 index 00000000..b1c3c60b --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs @@ -0,0 +1,101 @@ +using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Tests.Logging; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 验证 CQRS 程序集注册协调器在程序集键去重层面的可观察行为。 +/// +[TestFixture] +internal sealed class CqrsRegistrationServiceTests +{ + /// + /// 验证同一次调用内出现重复程序集键时,底层注册器只会接收到一次注册请求。 + /// + [Test] + public void RegisterHandlers_Should_Register_Duplicate_Assembly_Key_Only_Once_Per_Call() + { + var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug); + var registrar = new Mock(MockBehavior.Strict); + var duplicateAssemblyA = CreateAssembly("GFramework.Cqrs.Tests.DuplicateAssembly, Version=1.0.0.0"); + var duplicateAssemblyB = CreateAssembly("GFramework.Cqrs.Tests.DuplicateAssembly, Version=1.0.0.0"); + var expectedAssembly = duplicateAssemblyA.Object; + IEnumerable? registeredAssemblies = null; + + registrar + .Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>())) + .Callback>(assemblies => registeredAssemblies = assemblies.ToArray()); + + var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger); + + service.RegisterHandlers([duplicateAssemblyA.Object, duplicateAssemblyB.Object]); + + registrar.Verify( + static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>()), + Times.Once); + Assert.Multiple(() => + { + Assert.That(registeredAssemblies, Is.Not.Null); + Assert.That(registeredAssemblies, Is.EqualTo([expectedAssembly])); + Assert.That(logger.Logs, Has.Count.EqualTo(0)); + }); + } + + /// + /// 验证跨两次调用重复程序集键时,协调器会跳过重复注册并写入 debug 日志。 + /// + [Test] + public void RegisterHandlers_Should_Skip_Already_Registered_Assembly_Key_Across_Calls_And_Log_Debug_Message() + { + var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug); + var registrar = new Mock(MockBehavior.Strict); + var firstAssembly = CreateAssembly("GFramework.Cqrs.Tests.RegisteredAssembly, Version=1.0.0.0"); + var secondAssembly = CreateAssembly("GFramework.Cqrs.Tests.RegisteredAssembly, Version=1.0.0.0"); + IEnumerable? registeredAssemblies = null; + + registrar + .Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>())) + .Callback>(assemblies => registeredAssemblies = assemblies.ToArray()); + + var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger); + + service.RegisterHandlers([firstAssembly.Object]); + service.RegisterHandlers([secondAssembly.Object]); + + registrar.Verify( + static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>()), + Times.Once); + Assert.Multiple(() => + { + Assert.That(registeredAssemblies, Is.EqualTo([firstAssembly.Object])); + var debugMessages = logger.Logs + .Where(static log => log.Level == LogLevel.Debug) + .Select(static log => log.Message) + .ToArray(); + Assert.That(debugMessages, Has.Length.EqualTo(1)); + Assert.That( + debugMessages[0], + Does.Contain("Skipping CQRS handler registration for assembly")); + Assert.That( + debugMessages[0], + Does.Contain("GFramework.Cqrs.Tests.RegisteredAssembly, Version=1.0.0.0")); + Assert.That(debugMessages[0], Does.Contain("already registered")); + }); + } + + /// + /// 创建一个带稳定程序集键的程序集 mock,用于模拟不同 实例表示同一程序集的场景。 + /// + /// 要返回的程序集完整名称。 + /// 配置好完整名称的程序集 mock。 + private static Mock CreateAssembly(string assemblyFullName) + { + var assembly = new Mock(); + assembly + .SetupGet(static currentAssembly => currentAssembly.FullName) + .Returns(assemblyFullName); + + return assembly; + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshHandler.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshHandler.cs new file mode 100644 index 00000000..61a07ccc --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshHandler.cs @@ -0,0 +1,29 @@ +using System.Threading; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 记录缓存 notification binding 复用场景下每次分发注入到 handler 的上下文与实例身份。 +/// +internal sealed class DispatcherNotificationContextRefreshHandler + : CqrsContextAwareHandlerBase, + INotificationHandler +{ + private readonly int _instanceId = DispatcherNotificationContextRefreshState.AllocateHandlerInstanceId(); + + /// + /// 记录当前 handler 实例收到的上下文。 + /// + /// 当前通知。 + /// 取消令牌。 + /// 已完成任务。 + public ValueTask Handle( + DispatcherNotificationContextRefreshNotification notification, + CancellationToken cancellationToken) + { + DispatcherNotificationContextRefreshState.Record(notification.DispatchId, _instanceId, Context); + return ValueTask.CompletedTask; + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshNotification.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshNotification.cs new file mode 100644 index 00000000..faa3f5af --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshNotification.cs @@ -0,0 +1,9 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 为 notification dispatch binding 上下文刷新回归提供带分发标识的最小通知。 +/// +/// 当前分发的稳定标识,便于断言缓存 binding 复用时观察到的是同一次通知。 +internal sealed record DispatcherNotificationContextRefreshNotification(string DispatchId) : INotification; diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshState.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshState.cs new file mode 100644 index 00000000..b6d8a7ca --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshState.cs @@ -0,0 +1,60 @@ +using System.Threading; +using GFramework.Core.Abstractions.Architectures; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 记录 notification dispatch binding 缓存回归中每次分发实际使用的上下文与实例身份。 +/// +internal static class DispatcherNotificationContextRefreshState +{ + private static readonly Lock SyncRoot = new(); + private static int _nextHandlerInstanceId; + private static readonly List _handlerSnapshots = []; + + /// + /// 获取每次 notification 分发时记录的快照副本。 + /// 共享状态通过 SyncRoot 串行化,避免并行测试写入抖动。 + /// + public static IReadOnlyList HandlerSnapshots + { + get + { + lock (SyncRoot) + { + return _handlerSnapshots.ToArray(); + } + } + } + + /// + /// 为新的 handler 测试实例分配稳定编号。 + /// + public static int AllocateHandlerInstanceId() + { + return Interlocked.Increment(ref _nextHandlerInstanceId); + } + + /// + /// 记录 handler 在当前分发中观察到的上下文。 + /// + public static void Record(string dispatchId, int instanceId, IArchitectureContext context) + { + lock (SyncRoot) + { + _handlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + } + } + + /// + /// 清空历史记录与实例编号,避免跨测试污染断言。 + /// + public static void Reset() + { + lock (SyncRoot) + { + _nextHandlerInstanceId = 0; + _handlerSnapshots.Clear(); + } + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshBehavior.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshBehavior.cs new file mode 100644 index 00000000..360eaf46 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshBehavior.cs @@ -0,0 +1,31 @@ +using System.Threading; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 记录缓存 executor 复用场景下每次分发注入到 behavior 的上下文与实例身份。 +/// +internal sealed class DispatcherPipelineContextRefreshBehavior + : CqrsContextAwareHandlerBase, + IPipelineBehavior +{ + private readonly int _instanceId = DispatcherPipelineContextRefreshState.AllocateBehaviorInstanceId(); + + /// + /// 记录当前 behavior 实例实际收到的上下文,然后继续执行下游处理器。 + /// + /// 当前请求。 + /// 下一个处理阶段。 + /// 取消令牌。 + /// 下游处理结果。 + public async ValueTask Handle( + DispatcherPipelineContextRefreshRequest request, + MessageHandlerDelegate next, + CancellationToken cancellationToken) + { + DispatcherPipelineContextRefreshState.RecordBehavior(request.DispatchId, _instanceId, Context); + return await next(request, cancellationToken).ConfigureAwait(false); + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshRequest.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshRequest.cs new file mode 100644 index 00000000..6e0035ef --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshRequest.cs @@ -0,0 +1,9 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 为 pipeline executor 上下文刷新回归提供带分发标识的最小请求。 +/// +/// 当前分发的稳定标识,便于断言 handler 与 behavior 看到的是同一次请求。 +internal sealed record DispatcherPipelineContextRefreshRequest(string DispatchId) : IRequest; diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshRequestHandler.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshRequestHandler.cs new file mode 100644 index 00000000..f69923a9 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshRequestHandler.cs @@ -0,0 +1,29 @@ +using System.Threading; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 记录缓存 executor 复用场景下每次分发注入到 request handler 的上下文与实例身份。 +/// +internal sealed class DispatcherPipelineContextRefreshRequestHandler + : CqrsContextAwareHandlerBase, + IRequestHandler +{ + private readonly int _instanceId = DispatcherPipelineContextRefreshState.AllocateHandlerInstanceId(); + + /// + /// 记录当前 handler 实例收到的上下文,并返回稳定结果。 + /// + /// 当前请求。 + /// 取消令牌。 + /// 固定整数结果。 + public ValueTask Handle( + DispatcherPipelineContextRefreshRequest request, + CancellationToken cancellationToken) + { + DispatcherPipelineContextRefreshState.RecordHandler(request.DispatchId, _instanceId, Context); + return ValueTask.FromResult(7); + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshState.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshState.cs new file mode 100644 index 00000000..99cae23a --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshState.cs @@ -0,0 +1,98 @@ +using System.Threading; +using GFramework.Core.Abstractions.Architectures; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 记录 pipeline executor 缓存回归中每次分发实际使用的上下文与实例身份。 +/// +internal static class DispatcherPipelineContextRefreshState +{ + private static readonly Lock SyncRoot = new(); + private static int _nextBehaviorInstanceId; + private static int _nextHandlerInstanceId; + private static readonly List _behaviorSnapshots = []; + private static readonly List _handlerSnapshots = []; + + /// + /// 获取每次 behavior 执行时记录的快照副本。 + /// 共享状态通过 SyncRoot 串行化,读取端始终拿到当前稳定快照。 + /// + public static IReadOnlyList BehaviorSnapshots + { + get + { + lock (SyncRoot) + { + return _behaviorSnapshots.ToArray(); + } + } + } + + /// + /// 获取每次 handler 执行时记录的快照副本。 + /// 共享状态通过 SyncRoot 串行化,读取端始终拿到当前稳定快照。 + /// + public static IReadOnlyList HandlerSnapshots + { + get + { + lock (SyncRoot) + { + return _handlerSnapshots.ToArray(); + } + } + } + + /// + /// 为新的 behavior 测试实例分配稳定编号。 + /// + public static int AllocateBehaviorInstanceId() + { + return Interlocked.Increment(ref _nextBehaviorInstanceId); + } + + /// + /// 为新的 handler 测试实例分配稳定编号。 + /// + public static int AllocateHandlerInstanceId() + { + return Interlocked.Increment(ref _nextHandlerInstanceId); + } + + /// + /// 记录 behavior 在当前分发中观察到的上下文。 + /// + public static void RecordBehavior(string dispatchId, int instanceId, IArchitectureContext context) + { + lock (SyncRoot) + { + _behaviorSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + } + } + + /// + /// 记录 handler 在当前分发中观察到的上下文。 + /// + public static void RecordHandler(string dispatchId, int instanceId, IArchitectureContext context) + { + lock (SyncRoot) + { + _handlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + } + } + + /// + /// 清空历史记录与实例编号,避免跨测试污染断言。 + /// + public static void Reset() + { + lock (SyncRoot) + { + _nextBehaviorInstanceId = 0; + _nextHandlerInstanceId = 0; + _behaviorSnapshots.Clear(); + _handlerSnapshots.Clear(); + } + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextSnapshot.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextSnapshot.cs new file mode 100644 index 00000000..24d70c52 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextSnapshot.cs @@ -0,0 +1,14 @@ +using GFramework.Core.Abstractions.Architectures; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 描述单次分发阶段记录下来的上下文与实例身份。 +/// +/// 触发本次记录的请求标识。 +/// 当次 handler 或 behavior 实例编号。 +/// 当次分发注入的架构上下文。 +internal sealed record DispatcherPipelineContextSnapshot( + string DispatchId, + int InstanceId, + IArchitectureContext Context); diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequest.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequest.cs new file mode 100644 index 00000000..9528a060 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequest.cs @@ -0,0 +1,8 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 为双行为 pipeline 顺序回归提供最小请求。 +/// +internal sealed record DispatcherPipelineOrderCacheRequest : IRequest; diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequestHandler.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequestHandler.cs new file mode 100644 index 00000000..d2b88baf --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequestHandler.cs @@ -0,0 +1,21 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 为双行为顺序回归提供最终请求处理器。 +/// +internal sealed class DispatcherPipelineOrderCacheRequestHandler : IRequestHandler +{ + /// + /// 记录处理器执行并返回固定结果。 + /// + /// 当前请求。 + /// 取消令牌。 + /// 固定整数结果。 + public ValueTask Handle(DispatcherPipelineOrderCacheRequest request, CancellationToken cancellationToken) + { + DispatcherPipelineOrderState.Record("Handler"); + return ValueTask.FromResult(3); + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderInnerBehavior.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderInnerBehavior.cs new file mode 100644 index 00000000..cf327d2a --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderInnerBehavior.cs @@ -0,0 +1,27 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 作为内层行为验证缓存 executor 复用后仍保持注册顺序。 +/// +internal sealed class DispatcherPipelineOrderInnerBehavior : IPipelineBehavior +{ + /// + /// 记录内层行为的前后执行节点。 + /// + /// 当前请求。 + /// 下一个处理阶段。 + /// 取消令牌。 + /// 下游处理器结果。 + public async ValueTask Handle( + DispatcherPipelineOrderCacheRequest request, + MessageHandlerDelegate next, + CancellationToken cancellationToken) + { + DispatcherPipelineOrderState.Record("Inner:Before"); + var result = await next(request, cancellationToken).ConfigureAwait(false); + DispatcherPipelineOrderState.Record("Inner:After"); + return result; + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderOuterBehavior.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderOuterBehavior.cs new file mode 100644 index 00000000..aa1f0291 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderOuterBehavior.cs @@ -0,0 +1,27 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 作为外层行为验证缓存 executor 复用后仍保持注册顺序。 +/// +internal sealed class DispatcherPipelineOrderOuterBehavior : IPipelineBehavior +{ + /// + /// 记录外层行为的前后执行节点。 + /// + /// 当前请求。 + /// 下一个处理阶段。 + /// 取消令牌。 + /// 下游处理器结果。 + public async ValueTask Handle( + DispatcherPipelineOrderCacheRequest request, + MessageHandlerDelegate next, + CancellationToken cancellationToken) + { + DispatcherPipelineOrderState.Record("Outer:Before"); + var result = await next(request, cancellationToken).ConfigureAwait(false); + DispatcherPipelineOrderState.Record("Outer:After"); + return result; + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderState.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderState.cs new file mode 100644 index 00000000..fa202b85 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderState.cs @@ -0,0 +1,50 @@ +using System.Threading; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 记录双行为 pipeline 的实际执行顺序。 +/// +internal static class DispatcherPipelineOrderState +{ + private static readonly Lock SyncRoot = new(); + private static readonly List _steps = []; + + /// + /// 获取按执行顺序追加的步骤快照。 + /// 共享状态通过 SyncRoot 串行化,避免并行行为测试互相污染步骤列表。 + /// + public static IReadOnlyList Steps + { + get + { + lock (SyncRoot) + { + return _steps.ToArray(); + } + } + } + + /// + /// 记录一个新的 pipeline 执行步骤。 + /// + /// 要追加的步骤名称。 + public static void Record(string step) + { + lock (SyncRoot) + { + _steps.Add(step); + } + } + + /// + /// 清空当前记录,供下一次断言使用。 + /// + public static void Reset() + { + lock (SyncRoot) + { + _steps.Clear(); + } + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshHandler.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshHandler.cs new file mode 100644 index 00000000..95d7c998 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshHandler.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 记录缓存 stream binding 复用场景下每次分发注入到 handler 的上下文与实例身份。 +/// +internal sealed class DispatcherStreamContextRefreshHandler + : CqrsContextAwareHandlerBase, + IStreamRequestHandler +{ + private readonly int _instanceId = DispatcherStreamContextRefreshState.AllocateHandlerInstanceId(); + + /// + /// 记录当前 handler 实例收到的上下文,并返回稳定元素。 + /// + /// 当前流请求。 + /// 取消令牌。 + /// 包含一个固定元素的异步流。 + public async IAsyncEnumerable Handle( + DispatcherStreamContextRefreshRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + DispatcherStreamContextRefreshState.Record(request.DispatchId, _instanceId, Context); + yield return 11; + await ValueTask.CompletedTask.ConfigureAwait(false); + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshRequest.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshRequest.cs new file mode 100644 index 00000000..f453886e --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshRequest.cs @@ -0,0 +1,9 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 为 stream dispatch binding 上下文刷新回归提供带分发标识的最小流请求。 +/// +/// 当前分发的稳定标识,便于断言缓存 binding 复用时观察到的是同一次建流。 +internal sealed record DispatcherStreamContextRefreshRequest(string DispatchId) : IStreamRequest; diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs new file mode 100644 index 00000000..47be718b --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs @@ -0,0 +1,67 @@ +using System.Threading; +using GFramework.Core.Abstractions.Architectures; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 记录 stream dispatch binding 缓存回归中每次分发实际使用的上下文与实例身份。 +/// +internal static class DispatcherStreamContextRefreshState +{ + private static readonly Lock _syncRoot = new(); + private static int _nextHandlerInstanceId; + private static readonly List _handlerSnapshots = []; + + /// + /// 获取每次建流时记录的快照副本。 + /// + /// 当前已记录的 handler 上下文快照副本。 + /// 共享状态通过 _syncRoot 串行化,避免并行测试写入抖动。 + public static IReadOnlyList HandlerSnapshots + { + get + { + lock (_syncRoot) + { + return _handlerSnapshots.ToArray(); + } + } + } + + /// + /// 为新的 handler 测试实例分配稳定编号。 + /// + /// 单调递增的 handler 实例编号。 + public static int AllocateHandlerInstanceId() + { + return Interlocked.Increment(ref _nextHandlerInstanceId); + } + + /// + /// 记录 handler 在当前建流中观察到的上下文。 + /// + /// 触发本次记录的稳定分发标识。 + /// 观察到该上下文的 handler 实例编号。 + /// 当前分发注入到 handler 的架构上下文。 + /// 写入过程通过 _syncRoot 串行化,确保快照列表保持稳定顺序。 + public static void Record(string dispatchId, int instanceId, IArchitectureContext context) + { + lock (_syncRoot) + { + _handlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + } + } + + /// + /// 清空历史记录与实例编号,避免跨测试污染断言。 + /// + /// 重置过程通过 _syncRoot 串行化,避免读取端观察到半清理状态。 + public static void Reset() + { + lock (_syncRoot) + { + _nextHandlerInstanceId = 0; + _handlerSnapshots.Clear(); + } + } +} diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index 108f6c03..d604beac 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; @@ -34,7 +35,7 @@ internal sealed class CqrsDispatcher( .GetMethod(nameof(InvokeRequestHandlerAsync), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo RequestPipelineInvokerMethodDefinition = typeof(CqrsDispatcher) - .GetMethod(nameof(InvokeRequestPipelineAsync), BindingFlags.NonPublic | BindingFlags.Static)!; + .GetMethod(nameof(InvokeRequestPipelineExecutorAsync), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo NotificationHandlerInvokerMethodDefinition = typeof(CqrsDispatcher) .GetMethod(nameof(InvokeNotificationHandlerAsync), BindingFlags.NonPublic | BindingFlags.Static)!; @@ -61,7 +62,7 @@ internal sealed class CqrsDispatcher( var notificationType = notification.GetType(); var dispatchBinding = NotificationDispatchBindings.GetOrAdd( notificationType, - CreateNotificationDispatchBinding); + static notificationType => CreateNotificationDispatchBinding(notificationType)); var handlers = container.GetAll(dispatchBinding.HandlerType); if (handlers.Count == 0) @@ -108,7 +109,8 @@ internal sealed class CqrsDispatcher( if (behaviors.Count == 0) return await dispatchBinding.RequestInvoker(handler, request, cancellationToken).ConfigureAwait(false); - return await dispatchBinding.PipelineInvoker(handler, behaviors, request, cancellationToken) + return await dispatchBinding.GetPipelineExecutor(behaviors.Count) + .Invoke(handler, behaviors, request, cancellationToken) .ConfigureAwait(false); } @@ -132,7 +134,7 @@ internal sealed class CqrsDispatcher( var dispatchBinding = StreamDispatchBindings.GetOrAdd( requestType, typeof(TResponse), - CreateStreamDispatchBinding); + static (requestType, responseType) => CreateStreamDispatchBinding(requestType, responseType)); var handler = container.Get(dispatchBinding.HandlerType) ?? throw new InvalidOperationException( $"No CQRS stream handler registered for {requestType.FullName}."); @@ -168,7 +170,7 @@ internal sealed class CqrsDispatcher( typeof(IRequestHandler<,>).MakeGenericType(requestType, typeof(TResponse)), typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)), CreateRequestInvoker(requestType), - CreateRequestPipelineInvoker(requestType)); + requestType); } /// @@ -179,7 +181,8 @@ internal sealed class CqrsDispatcher( var bindingBox = RequestDispatchBindings.GetOrAdd( requestType, typeof(TResponse), - CreateRequestDispatchBindingBox); + static (cachedRequestType, cachedResponseType) => + CreateRequestDispatchBindingBox(cachedRequestType, cachedResponseType)); return bindingBox.Get(); } @@ -227,18 +230,6 @@ internal sealed class CqrsDispatcher( return (RequestInvoker)Delegate.CreateDelegate(typeof(RequestInvoker), method); } - /// - /// 生成带管道行为的请求处理委托,避免每次发送都重复反射。 - /// - private static RequestPipelineInvoker CreateRequestPipelineInvoker(Type requestType) - { - var method = RequestPipelineInvokerMethodDefinition - .MakeGenericMethod(requestType, typeof(TResponse)); - return (RequestPipelineInvoker)Delegate.CreateDelegate( - typeof(RequestPipelineInvoker), - method); - } - /// /// 生成通知处理器调用委托,避免每次发布都重复反射。 /// @@ -274,29 +265,20 @@ internal sealed class CqrsDispatcher( } /// - /// 执行包含管道行为链的请求处理。 + /// 执行指定行为数量的强类型 request pipeline executor。 + /// 该入口本身是缓存的固定 executor 形状;每次分发只绑定当前 handler 与 behaviors 实例。 /// - private static ValueTask InvokeRequestPipelineAsync( + private static ValueTask InvokeRequestPipelineExecutorAsync( object handler, IReadOnlyList behaviors, object request, CancellationToken cancellationToken) where TRequest : IRequest { - var typedHandler = (IRequestHandler)handler; - var typedRequest = (TRequest)request; - - MessageHandlerDelegate next = - (message, token) => typedHandler.Handle(message, token); - - for (var i = behaviors.Count - 1; i >= 0; i--) - { - var behavior = (IPipelineBehavior)behaviors[i]; - var currentNext = next; - next = (message, token) => behavior.Handle(message, currentNext, token); - } - - return next(typedRequest, cancellationToken); + var invocation = new RequestPipelineInvocation( + (IRequestHandler)handler, + behaviors); + return invocation.InvokeAsync((TRequest)request, cancellationToken); } /// @@ -424,15 +406,21 @@ internal sealed class CqrsDispatcher( /// /// 保存普通请求分发路径所需的 handler 服务类型、pipeline 服务类型与强类型调用委托。 - /// 该绑定同时覆盖“直接请求处理”和“带 pipeline 的请求处理”两条路径。 + /// 该绑定同时覆盖“直接请求处理”和“按行为数量缓存 pipeline executor 形状”的两条路径。 /// /// 请求响应类型。 private sealed class RequestDispatchBinding( Type handlerType, Type behaviorType, RequestInvoker requestInvoker, - RequestPipelineInvoker pipelineInvoker) + Type requestType) { + // 线程安全:该缓存按 behaviorCount 复用 pipeline executor 形状,GetPipelineExecutor 通过 ConcurrentDictionary + // 的 GetOrAdd 支持并发读写。缓存项只保存委托形状,不保留 handler/behavior 实例;若行为数量组合持续增长, + // 字典会随之增长且当前实现不提供回收。 + private readonly ConcurrentDictionary> _pipelineExecutors = new(); + private readonly RequestPipelineInvoker _pipelineInvoker = CreateRequestPipelineInvoker(requestType); + /// /// 获取请求处理器在容器中的服务类型。 /// @@ -449,8 +437,173 @@ internal sealed class CqrsDispatcher( public RequestInvoker RequestInvoker { get; } = requestInvoker; /// - /// 获取执行 pipeline 行为链的强类型委托。 + /// 获取指定行为数量对应的 pipeline executor。 + /// executor 形状会按请求/响应类型与行为数量缓存,但不会缓存 handler 或 behavior 实例。 /// - public RequestPipelineInvoker PipelineInvoker { get; } = pipelineInvoker; + public RequestPipelineExecutor GetPipelineExecutor(int behaviorCount) + { + ArgumentOutOfRangeException.ThrowIfNegative(behaviorCount); + return _pipelineExecutors.GetOrAdd>( + behaviorCount, + static (count, state) => CreateRequestPipelineExecutor(count, state.PipelineInvoker), + new RequestPipelineExecutorFactoryState(_pipelineInvoker)); + } + + /// + /// 仅供测试读取指定行为数量是否已存在缓存 executor。 + /// + public object? GetPipelineExecutorForTesting(int behaviorCount) + { + _pipelineExecutors.TryGetValue(behaviorCount, out var executor); + return executor; + } + } + + /// + /// 为指定请求/响应类型与固定行为数量创建 pipeline executor。 + /// 行为数量用于表达缓存形状,实际分发仍会消费本次容器解析出的 handler 与 behaviors 实例。 + /// + private static RequestPipelineExecutor CreateRequestPipelineExecutor( + int behaviorCount, + RequestPipelineInvoker invoker) + { + ArgumentOutOfRangeException.ThrowIfNegative(behaviorCount); + return new RequestPipelineExecutor(behaviorCount, invoker); + } + + /// + /// 为指定请求/响应类型创建可跨多个 behaviorCount 复用的 typed pipeline invoker。 + /// + private static RequestPipelineInvoker CreateRequestPipelineInvoker(Type requestType) + { + var method = RequestPipelineInvokerMethodDefinition + .MakeGenericMethod(requestType, typeof(TResponse)); + return (RequestPipelineInvoker)Delegate.CreateDelegate( + typeof(RequestPipelineInvoker), + method); + } + + /// + /// 保存固定行为数量下的 typed pipeline executor 形状。 + /// 该对象自身可跨分发复用,但每次调用都只绑定当前 handler 与 behavior 实例。 + /// + /// 请求响应类型。 + private sealed class RequestPipelineExecutor( + int behaviorCount, + RequestPipelineInvoker invoker) + { + /// + /// 获取此 executor 预期处理的行为数量。 + /// + public int BehaviorCount { get; } = behaviorCount; + + /// + /// 使用当前 handler / behaviors / request 执行缓存的 pipeline 形状。 + /// + public ValueTask Invoke( + object handler, + IReadOnlyList behaviors, + object request, + CancellationToken cancellationToken) + { + if (behaviors.Count != BehaviorCount) + { + throw new InvalidOperationException( + $"Cached request pipeline executor expected {BehaviorCount} behaviors, but received {behaviors.Count}."); + } + + return invoker(handler, behaviors, request, cancellationToken); + } + } + + /// + /// 为 pipeline executor 缓存携带当前请求类型,避免按行为数量建缓存时创建闭包。 + /// + /// 请求响应类型。 + private readonly record struct RequestPipelineExecutorFactoryState( + RequestPipelineInvoker PipelineInvoker); + + /// + /// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。 + /// 该对象只存在于本次分发,不会跨请求保留容器解析出的实例。 + /// + private sealed class RequestPipelineInvocation( + IRequestHandler handler, + IReadOnlyList behaviors) + where TRequest : IRequest + { + private readonly IRequestHandler _handler = handler; + private readonly IReadOnlyList _behaviors = behaviors; + private readonly MessageHandlerDelegate?[] _continuations = + new MessageHandlerDelegate?[behaviors.Count + 1]; + + /// + /// 从 pipeline 起点执行当前请求。 + /// + public ValueTask InvokeAsync(TRequest request, CancellationToken cancellationToken) + { + return GetContinuation(0)(request, cancellationToken); + } + + /// + /// 获取指定阶段的 continuation,并在首次请求时为该阶段绑定一次不可变调用入口。 + /// 同一行为多次调用 next 时会命中相同 continuation,保持与传统链式委托一致的语义。 + /// 线程模型上,该缓存仅假定单次分发链按顺序推进;若某个 behavior 并发调用多个 next, + /// 这里可能重复创建等价 continuation,但不会跨分发共享,也不会缓存容器解析出的实例。 + /// + private MessageHandlerDelegate GetContinuation(int index) + { + var continuation = _continuations[index]; + if (continuation is not null) + { + return continuation; + } + + continuation = index == _behaviors.Count + ? InvokeHandlerAsync + : new RequestPipelineContinuation(this, index).InvokeAsync; + _continuations[index] = continuation; + return continuation; + } + + /// + /// 执行指定索引的 pipeline behavior。 + /// + private ValueTask InvokeBehaviorAsync( + int index, + TRequest request, + CancellationToken cancellationToken) + { + var behavior = (IPipelineBehavior)_behaviors[index]; + return behavior.Handle(request, GetContinuation(index + 1), cancellationToken); + } + + /// + /// 调用最终请求处理器。 + /// + private ValueTask InvokeHandlerAsync(TRequest request, CancellationToken cancellationToken) + { + return _handler.Handle(request, cancellationToken); + } + + /// + /// 将固定阶段索引绑定为标准 。 + /// 该包装只在单次分发生命周期内存在,用于把缓存 shape 套入当前实例。 + /// + private sealed class RequestPipelineContinuation( + RequestPipelineInvocation invocation, + int index) + where TCurrentRequest : IRequest + { + /// + /// 执行当前阶段并跳转到下一个 continuation。 + /// + public ValueTask InvokeAsync( + TCurrentRequest request, + CancellationToken cancellationToken) + { + return invocation.InvokeBehaviorAsync(index, request, cancellationToken); + } + } } } diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index 21db7821..1be2729c 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -83,7 +83,8 @@ internal static class CqrsHandlerRegistrar { var assemblyMetadata = AssemblyMetadataCache.GetOrAdd( assembly, - key => AnalyzeAssemblyRegistrationMetadata(key, logger)); + logger, + static (key, state) => AnalyzeAssemblyRegistrationMetadata(key, state)); var registryTypes = assemblyMetadata.RegistryTypes; if (registryTypes.Count == 0) @@ -442,7 +443,8 @@ internal static class CqrsHandlerRegistrar { return LoadableTypesCache.GetOrAdd( assembly, - key => LoadAndSortTypes(key, logger)); + logger, + static (key, state) => LoadAndSortTypes(key, state)); } /// diff --git a/GFramework.Cqrs/README.md b/GFramework.Cqrs/README.md index 65c3cd99..62547a39 100644 --- a/GFramework.Cqrs/README.md +++ b/GFramework.Cqrs/README.md @@ -20,7 +20,7 @@ - `GeWuYou.GFramework.Cqrs` - 默认 runtime 与业务侧常用基类。 - `GeWuYou.GFramework.Cqrs.SourceGenerators` - - 可选。为消费端程序集生成 `ICqrsHandlerRegistry`,运行时优先走生成注册表;缺失或不适用时,回退到反射扫描。 + - 可选。为消费端程序集生成 `ICqrsHandlerRegistry`,运行时优先走生成注册表;只有缺失、不适用,或 fallback 仍需补齐剩余 handler 时,才继续进入反射路径。 - `GFramework.Core` - 架构上下文中实际调用 `ICqrsRuntime`,并在模块初始化时注册 CQRS 基础设施。 @@ -137,7 +137,10 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu - 同一程序集按稳定键去重,避免重复注册。 - 优先尝试消费端程序集上的 `ICqrsHandlerRegistry` 生成注册器。 -- 生成注册器不可用,或声明了 `CqrsReflectionFallbackAttribute` 时,回退到反射扫描。 +- 生成注册器不可用或元数据损坏时,记录告警并回退到反射扫描。 +- 当程序集声明了 `CqrsReflectionFallbackAttribute` 时,运行时会先执行生成注册器,再只补它未覆盖的 handler。 +- `CqrsReflectionFallbackAttribute` 现在可以多次声明,并同时承载 `Type[]` 与 `string[]` 两类 fallback 清单。 +- 运行时会优先复用 fallback 特性里直接提供的 `Type` 条目,只对字符串条目执行定向 `Assembly.GetType(...)` 查找;只有旧版空 marker 才会退回整程序集扫描。 - 处理器以 transient 方式注册,避免上下文感知处理器在并发请求间共享可变上下文。 如果你走标准 `GFramework.Core` 架构初始化路径,这些步骤通常由框架自动完成;裸容器或测试环境则需要显式补齐 runtime 与注册入口。 diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 78441c5b..2b83cddc 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -314,6 +314,128 @@ public class CqrsHandlerRegistryGeneratorTests """; + private const string HiddenMultiDimensionalArrayResponseSource = """ + 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 sealed record HiddenResponse(); + + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenHandler : IRequestHandler { } + } + } + """; + + private const string HiddenJaggedArrayResponseSource = """ + 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 sealed record HiddenResponse(); + + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenHandler : IRequestHandler { } + } + } + """; + private const string HiddenGenericEnvelopeResponseSource = """ using System; @@ -963,6 +1085,44 @@ public class CqrsHandlerRegistryGeneratorTests } """; + private const string ExternalProtectedMultiDimensionalTypeDependencySource = """ + using GFramework.Cqrs.Abstractions.Cqrs; + + namespace Dep; + + public abstract class VisibilityScope + { + protected internal sealed record ProtectedResponse(); + + protected internal sealed record ProtectedRequest() : IRequest; + } + + public abstract class HandlerBase : + IRequestHandler + { + } + """; + + private const string ExternalProtectedGenericDefinitionDependencySource = """ + using GFramework.Cqrs.Abstractions.Cqrs; + + namespace Dep; + + public abstract class VisibilityScope + { + protected internal sealed class ProtectedEnvelope + { + } + + protected internal sealed record ProtectedRequest() : IRequest>; + } + + public abstract class HandlerBase : + IRequestHandler> + { + } + """; + private const string LegacyFallbackMarkerHiddenHandlerSource = """ using System; @@ -1590,6 +1750,50 @@ public class CqrsHandlerRegistryGeneratorTests ("CqrsHandlerRegistry.g.cs", HiddenGenericEnvelopeResponseExpected)); } + /// + /// 验证精确重建路径会保留隐藏元素类型的多维数组秩信息, + /// 使生成注册器继续走定向运行时类型重建,而不是退回宽松接口发现。 + /// + [Test] + public void Generates_Precise_Service_Type_For_Hidden_MultiDimensional_Array_Type_Arguments() + { + var generatedSource = RunGenerator(HiddenMultiDimensionalArrayResponseSource); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain("registryAssembly.GetType(\"TestApp.Container+HiddenResponse\", throwOnError: false, ignoreCase: false);")); + Assert.That( + generatedSource, + Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(")); + Assert.That(generatedSource, Does.Contain(".MakeArrayType(2)")); + Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute(")); + }); + } + + /// + /// 验证精确重建路径会递归覆盖交错数组, + /// 确保隐藏元素类型的每一层数组都继续通过数组发射分支稳定重建。 + /// + [Test] + public void Generates_Precise_Service_Type_For_Hidden_Jagged_Array_Type_Arguments() + { + var generatedSource = RunGenerator(HiddenJaggedArrayResponseSource); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain("registryAssembly.GetType(\"TestApp.Container+HiddenResponse\", throwOnError: false, ignoreCase: false);")); + Assert.That( + generatedSource, + Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(")); + Assert.That(generatedSource, Does.Contain(".MakeArrayType().MakeArrayType()")); + Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute(")); + }); + } + /// /// 验证当 handler 合同把 pointer 响应类型放进 CQRS 泛型参数时, /// 生成器会保守回退而不是继续发射不可构造的精确注册代码。 @@ -1682,6 +1886,76 @@ public class CqrsHandlerRegistryGeneratorTests Is.EqualTo(ExternalAssemblyPreciseLookupExpected)); } + /// + /// 验证当外部程序集隐藏元素类型以多维数组形式参与 CQRS 合同时, + /// 生成器仍会保留外部程序集定向查找与数组秩信息,而不是退回 fallback 元数据。 + /// + [Test] + public void Generates_Precise_Assembly_Type_Lookups_For_Inaccessible_External_MultiDimensional_Array_Elements() + { + var contractsReference = MetadataReferenceTestBuilder.CreateFromSource( + "Contracts", + ExternalProtectedTypeContractsSource); + var dependencyReference = MetadataReferenceTestBuilder.CreateFromSource( + "Dependency", + ExternalProtectedMultiDimensionalTypeDependencySource, + contractsReference); + var generatedSource = RunGenerator( + ExternalProtectedTypeLookupSource, + contractsReference, + dependencyReference); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedResponse\")")); + Assert.That( + generatedSource, + Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(")); + Assert.That(generatedSource, Does.Contain(".MakeArrayType(2)")); + Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute(")); + }); + } + + /// + /// 验证当外部程序集隐藏泛型定义以“隐藏定义 + 可见类型实参”的形式参与 CQRS 合同时, + /// 生成器会继续输出定向程序集查找与运行时泛型重建,而不是退回字符串 fallback 元数据。 + /// + [Test] + public void Generates_Precise_Assembly_Type_Lookups_For_Inaccessible_External_Generic_Definitions_With_Visible_Type_Arguments() + { + var contractsReference = MetadataReferenceTestBuilder.CreateFromSource( + "Contracts", + ExternalProtectedTypeContractsSource); + var dependencyReference = MetadataReferenceTestBuilder.CreateFromSource( + "Dependency", + ExternalProtectedGenericDefinitionDependencySource, + contractsReference); + var generatedSource = RunGenerator( + ExternalProtectedTypeLookupSource, + contractsReference, + dependencyReference); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedRequest\")")); + Assert.That( + generatedSource, + Does.Contain( + "ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedEnvelope`1\")")); + Assert.That( + generatedSource, + Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(")); + Assert.That(generatedSource, Does.Contain(".MakeGenericType(typeof(string))")); + Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute(")); + }); + } + /// /// 验证即使 runtime 仍暴露旧版无参 fallback marker,生成器也会优先在生成注册器内部处理隐藏 handler, /// 不再输出 fallback marker。 diff --git a/ai-plan/public/cqrs-rewrite/archive/todos/cqrs-rewrite-validation-history-through-rp062.md b/ai-plan/public/cqrs-rewrite/archive/todos/cqrs-rewrite-validation-history-through-rp062.md new file mode 100644 index 00000000..a89af879 --- /dev/null +++ b/ai-plan/public/cqrs-rewrite/archive/todos/cqrs-rewrite-validation-history-through-rp062.md @@ -0,0 +1,91 @@ +# CQRS 重写迁移验证归档(至 RP-062) + +## 说明 + +- 本文件归档原 active tracking 中累积的历史验证命令与阶段性验证结论。 +- `boot` 默认恢复入口应回到 `ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md`,不要再从这里挑选旧命令作为当前下一步。 + +## 原验证记录 + +- `RP-043` 之前的详细阶段记录、定向验证命令和阶段性决策均已移入主题内归档 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` + - 结果:通过 + - 备注:`5/5` 测试通过;本轮新增 cached executor 上下文刷新回归,确认 executor 复用时仍按当次分发重新注入上下文 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsReflectionFallbackAttributeTests"` + - 结果:通过 + - 备注:`5/5` 测试通过;本轮锁定 fallback attribute 的公开归一化合同与空参数防御语义 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` + - 结果:通过 + - 备注:`7/7` 测试通过;本轮新增 cached notification / stream binding 上下文刷新回归,确认 binding 复用时仍按当次分发重新注入上下文 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherContextValidationTests"` + - 结果:通过 + - 备注:`3/3` 测试通过;本轮锁定默认 dispatcher 对非 `IArchitectureContext` 上下文的 request / notification / stream 失败语义,且未引入新增 warning +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarFallbackFailureTests"` + - 结果:通过 + - 备注:`3/3` 测试通过;本轮锁定 registrar 在 fallback 元数据失效时的 warning 语义,且保持 generated registry 主路径不回退 +- `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 去重路径 +- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error` +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`17/17` 测试通过;本轮覆盖字符串 fallback 合同兼容路径与直接 `Type` fallback 元数据优先级 +- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error` +- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error` +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` + - 结果:通过 + - 备注:`13/13` 测试通过;本轮覆盖 mixed fallback metadata 的 registrar 消费路径 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`18/18` 测试通过;本轮覆盖 mixed fallback metadata 的双特性发射路径 +- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-pr-review.json` + - 结果:通过 + - 备注:确认当前分支对应 `PR #302`;latest head review 仍有 `3` 条 open AI threads,其中 MegaLinter 仅报告 `dotnet-format` restore failure 噪音 +- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error` +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`18/18` 测试通过;本轮直接覆盖 fallback preamble 排版与特性个数断言收紧 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` + - 结果:通过 + - 备注:`13/13` 测试通过;本轮确认 mixed fallback metadata 的 registrar 消费路径未回归 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`21/21` 测试通过;本轮新增多维数组、交错数组与外部程序集隐藏元素类型的 precise runtime type lookup 回归 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`22/22` 测试通过;本轮新增“外部程序集隐藏泛型定义 + 可见类型实参”的 precise registration 回归,确认仍走定向运行时类型重建 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` + - 结果:通过 + - 备注:`4/4` 测试通过;本轮覆盖 request pipeline executor 的首次创建、复用与双行为顺序回归 +- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;本轮确认 dispatcher request pipeline 形状缓存未破坏 `net8.0` / `net9.0` / `net10.0` 目标构建 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~RegisterHandlers_Should_Cache_Assembly_Metadata_Across_Containers|FullyQualifiedName~RegisterHandlers_Should_Cache_Loadable_Types_Across_Containers|FullyQualifiedName~Dispatcher_Should_Cache_Request_Pipeline_Executors_Per_Behavior_Count"` + - 结果:通过 + - 备注:`3/3` 测试通过;本轮确认无捕获缓存工厂没有破坏 registrar / dispatcher 现有缓存行为 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`22/22` 测试通过;本轮新增外部程序集隐藏泛型定义的 precise registration 回归 +- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;本轮确认删除 pointer runtime-reconstruction 残留后生成器项目仍可正常构建 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`22/22` 测试通过;本轮确认 pointer / function pointer 拒绝语义保持不变,且未回归既有 precise runtime type lookup 场景 diff --git a/ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-rp046-through-rp061.md b/ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-rp046-through-rp061.md new file mode 100644 index 00000000..c7bec3ca --- /dev/null +++ b/ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-rp046-through-rp061.md @@ -0,0 +1,35 @@ +# CQRS 重写迁移追踪归档(RP-046 至 RP-061) + +## 说明 + +- 本文件承接从 active trace 中迁出的已完成阶段细节。 +- `boot` 默认恢复入口应回到 `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md`,不要从本归档直接挑选旧阶段作为当前恢复点。 + +## 覆盖范围 + +- `CQRS-REWRITE-RP-046` 至 `CQRS-REWRITE-RP-061` +- 对应 active trace 清理前的 `2026-04-20`、`2026-04-29` 历史阶段记录 + +## 归档摘要 + +- `RP-046`:generated registry 激活反射收敛,补齐私有无参构造兼容回归 +- `RP-047`:pointer precise runtime type 方案探索,后续已被 `RP-050` 明确覆盖并废弃 +- `RP-048`:registrar handler-interface 反射缓存 +- `RP-049`:registrar duplicate mapping 索引收敛 +- `RP-050`:pointer / function pointer 泛型合同拒绝 +- `RP-051`:direct fallback 元数据优先级收敛 +- `RP-052`:mixed fallback 元数据拆分 +- `RP-053`:precise runtime type lookup 数组回归补强 +- `RP-054`:低风险并行批次收口 +- `RP-055`:缓存工厂闭包收敛 +- `RP-056`:pointer runtime-reconstruction 残留清理 +- `RP-057`:cached executor 上下文刷新回归 +- `RP-058`:delegated fallback attribute 合同测试 +- `RP-059`:notification / stream binding 上下文刷新回归 +- `RP-060`:dispatcher 上下文前置条件失败语义回归 +- `RP-061`:registrar fallback 失败分支回归 + +## 备注 + +- 若后续需要恢复这些阶段的详细上下文,应以对应提交、测试文件与本主题源码为准。 +- 当前 active trace 已不再保留这些阶段的逐段叙述,以保证 `boot` 能直接落到 `RP-062`。 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 64365852..4703ad8c 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,7 +7,7 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-052` +- 恢复点编号:`CQRS-REWRITE-RP-062` - 当前阶段:`Phase 8` - 当前焦点: - 当前功能历史已归档,active 跟踪仅保留 `Phase 8` 主线的恢复入口 @@ -15,12 +15,24 @@ CQRS 迁移与收敛。 - `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出 - 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据 - 当 runtime 不支持多实例 fallback 特性或缺少对应构造函数时,mixed fallback 场景仍会整体保守回退到字符串元数据,避免仅部分 handler 走 `Type[]` 时漏掉剩余需按名称恢复的 handlers + - 已完成 request pipeline executor 形状缓存:`CqrsDispatcher` 现会在单个 request binding 内按 `behaviorCount` 复用强类型 pipeline executor,而不是每次 `SendAsync` 都重建整条 `next` 委托链 + - 已补充 dispatcher pipeline executor 缓存与双行为顺序回归,锁定缓存复用后仍保持现有行为执行顺序 + - 已补充 cached request pipeline executor 的上下文刷新回归,锁定 executor 复用时仍会为当次 handler / singleton behavior 重新注入当前 `ArchitectureContext` + - 已补充 cached notification / stream dispatch binding 的上下文刷新回归,锁定 binding 复用时仍会为当次 handler 重新注入当前 `ArchitectureContext` + - 已补充非 `IArchitectureContext` 的 dispatcher 失败语义回归,锁定 context-aware request / notification / stream handler 在注入前置条件不满足时会显式抛出异常 + - 已补充 registrar fallback 失败分支回归,锁定 named fallback 无法解析、named fallback 解析抛异常、direct fallback 跨程序集三类 warning 语义 - 已完成 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 注册路径的重复查找 + - 已完成一轮 `static lambda + state` 微收敛:`CqrsDispatcher` 与 `CqrsHandlerRegistrar` 现会在弱缓存 / 并发缓存入口优先使用无捕获工厂,继续压低热路径上的额外闭包分配 + - 已补充 `CqrsReflectionFallbackAttribute` 叶子级合同测试,锁定空 marker、字符串 fallback 名称归一化、直接 `Type` fallback 归一化与空参数防御语义 + - 已完成 `PR #304` review follow-up 收敛:`CqrsDispatcher` 现补齐 pipeline executor / continuation 缓存的线程模型文档,并把 request pipeline invoker 从按 `behaviorCount` 重复创建收敛为 binding 内复用 + - 已补齐 `CqrsDispatcherContextValidationTests` 三个上下文校验 handler 的 XML `param` / `returns` 注释,以及 `DispatcherNotificationContextRefreshNotification`、`DispatcherStreamContextRefreshRequest` 的 `DispatchId` XML 参数注释,收敛上一轮 PR review 遗留的文档类 minor feedback + - 已收紧 CQRS / generator 回归测试的脆弱断言:日志断言改为语义匹配,precise runtime type lookup 回归改为锁定数组秩、外部类型查找与“未发射 fallback metadata”这些稳定语义 + - 已为 dispatcher cache / context refresh / pipeline order 三组测试状态容器补齐并发保护,并将 `CqrsDispatcherCacheTests` 标记为 `NonParallelizable`,避免静态缓存与共享快照在并行测试中相互污染 - 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点 ## 当前状态摘要 @@ -71,6 +83,52 @@ CQRS 迁移与收敛。 - latest reviewed commit 当前剩余 `3` 条 open AI review threads:`2` 条 Greptile、`1` 条 CodeRabbit - 本地核对后确认 `dotnet-format` 仍只有 `Restore operation failed` 噪音,没有附带当前仍成立的文件级格式诊断 - 已按 review triage 修正 generator source preamble 的多实例 fallback 特性排版、移除死参数,并补强 mixed/direct fallback 发射回归断言与 XML 文档 +- `2026-04-30` 已重新执行 `$gframework-pr-review`: + - 当前分支对应 `PR #304`,状态为 `OPEN` + - latest reviewed commit 当前剩余 `7` 条 CodeRabbit nitpick 与 `2` 条 Greptile open threads,集中在测试脆弱断言、共享测试状态并发保护,以及 `CqrsDispatcher` 的缓存线程模型文档 + - 本地核对后,已确认这些评论仍对应当前代码;MegaLinter 继续只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,CTRF 汇总为 `2203/2203` passed + - 已在本地完成 follow-up:request pipeline invoker 改为 binding 级复用、共享测试状态切换到 `System.Threading.Lock` 保护、顺序测试改为受控记录接口、`CqrsDispatcherCacheTests` 标记为 `NonParallelizable`,并补齐相关 XML / 线程模型注释 +- `2026-04-29` 已完成一轮 precise runtime type lookup 的数组回归补强: + - `GFramework.SourceGenerators.Tests` 已新增多维数组、交错数组、外部程序集隐藏元素类型三类回归 + - 当前生成器在 precise runtime type lookup 下已稳定保留数组秩信息,并递归发射交错数组的 `MakeArrayType()` 链 + - 本轮定向测试未暴露数组发射缺陷,因此未改动 fallback 合同选择逻辑,也未调整 direct / named / mixed fallback 排版路径 +- `2026-04-29` 已补齐一轮外部程序集隐藏泛型定义回归覆盖: + - `GFramework.SourceGenerators.Tests` 已新增“外部程序集隐藏泛型定义 + 可见类型实参”的 precise registration 回归 + - 当前生成器会继续为这类 handler 合同发射 `ResolveReferencedAssemblyType(...) + MakeGenericType(...)` 组合,而不是退回字符串 fallback 元数据 + - 本轮定向测试未暴露新的实现缺口,因此未改动 direct / named / mixed fallback 选择逻辑,也未调整 generator runtime type 建模实现 +- `2026-04-29` 已完成一轮缓存工厂闭包收敛: + - `CqrsDispatcher` 现会在 notification / stream / request binding 与 pipeline executor 缓存入口优先使用无捕获工厂 + - `CqrsHandlerRegistrar` 现会在程序集元数据缓存与可加载类型缓存入口复用 `static` 工厂 + 显式状态参数 + - 本轮未改动公开语义,也未修改 fallback 合同与 handler / behavior 生命周期边界 +- `2026-04-29` 已完成一轮 request pipeline executor 形状缓存: + - `CqrsDispatcher` 现会继续按 `requestType + responseType` 缓存 request dispatch binding,并在 binding 内按 `behaviorCount` 缓存强类型 pipeline executor + - 每次分发只绑定当前 handler / behaviors 实例,不缓存容器解析结果,因此不改变 transient 生命周期与上下文注入语义 + - `GFramework.Cqrs.Tests` 已补充 executor 首次创建 / 后续复用与双行为顺序回归 +- `2026-04-29` 已完成一轮 cached executor 上下文刷新回归补强: + - `GFramework.Cqrs.Tests` 已新增 `DispatcherPipelineContextRefresh*` 测试替身,分别记录 request handler 与 pipeline behavior 在每次分发中实际观察到的实例身份与 `ArchitectureContext` + - `CqrsDispatcherCacheTests` 现明确断言:同一个 cached request pipeline executor 在重复分发时会继续命中同一 executor 形状,但不会跨分发保留旧上下文 + - 本轮定向测试未暴露新的 runtime 缺口,因此没有改动 `GFramework.Cqrs/Internal/CqrsDispatcher.cs` +- `2026-04-29` 已完成一轮 cached notification / stream binding 上下文刷新回归补强: + - `GFramework.Cqrs.Tests` 已新增 `DispatcherNotificationContextRefresh*` 与 `DispatcherStreamContextRefresh*` 测试替身,分别记录 notification handler 与 stream handler 在重复分发时观察到的实例身份与 `ArchitectureContext` + - `CqrsDispatcherCacheTests` 现明确断言:同一个 cached notification / stream dispatch binding 在重复分发时会继续命中同一 binding,但不会跨分发保留旧上下文 + - 本轮定向测试未暴露新的 runtime 缺口,因此没有改动 `GFramework.Cqrs/Internal/CqrsDispatcher.cs` +- `2026-04-29` 已完成一轮 dispatcher 上下文前置条件失败语义回归: + - `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 已通过公开工厂 `CqrsRuntimeFactory.CreateRuntime(...)` 锁定默认 dispatcher 的失败语义 + - 当 context-aware request / notification / stream handler 遇到仅实现 `ICqrsContext`、但未实现 `IArchitectureContext` 的上下文时,dispatcher 会在调用前显式抛出 `InvalidOperationException` + - 本轮只补测试,不改 runtime 实现与文档口径 +- `2026-04-29` 已接受一轮 delegated registrar fallback 失败分支测试: + - `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs` 已覆盖 named fallback 无法解析、named fallback 解析抛异常、direct fallback 跨程序集三类 warning 语义 + - 主线程已复核该新文件并重新执行定向测试,确认当前 registrar 在 fallback 元数据失效时仍保持“跳过条目 + 记录告警”的既有语义 +- `2026-04-29` 已接受一轮 delegated 叶子级 fallback 合同测试: + - `GFramework.Cqrs.Tests/Cqrs/CqrsReflectionFallbackAttributeTests.cs` 已锁定空 marker、字符串 fallback 名称去空/去重/排序、直接 `Type` fallback 去空/去重/排序与空参数数组防御语义 + - 当前 runtime 读取程序集级 fallback 元数据时所依赖的 attribute 归一化合同,现已有独立叶子级测试文件覆盖 +- `2026-04-29` 已完成一轮 CQRS 入口文档对齐: + - `GFramework.Cqrs/README.md`、`docs/zh-CN/core/cqrs.md` 与 `docs/zh-CN/api-reference/index.md` 现已明确 generated registry 优先、targeted fallback 补齐剩余 handler 的当前语义 +- `2026-04-29` 已完成一轮 generator pointer runtime-reconstruction 残留清理: + - `CqrsHandlerRegistryGenerator` 的运行时类型引用模型已移除不可达的 pointer 子结构 + - `SourceEmission` 不再保留 `MakePointerType()` 源码发射分支,`RuntimeTypeReferences` 也已删掉对应的外部程序集递归扫描死代码 + - pointer / function pointer 的拒绝语义保持不变,direct / named / mixed fallback 逻辑未改动 + - 当前工作区相对 `origin/main` 的累计 diff 已达到 `14 files`,仍低于本轮 `gframework-batch-boot 50` 的主要 stop condition - 当前主线优先级: - generator 覆盖面继续扩大 - dispatch/invoker 反射占比继续下降 @@ -80,62 +138,28 @@ CQRS 迁移与收敛。 - 当前 `dotnet build GFramework.sln -c Release` 在 WSL 环境仍会受顶层 `GFramework.csproj` 的 Windows NuGet fallback 配置影响 - 当前 `GFramework.Cqrs.Tests` 仍直接引用 `GFramework.Core`,说明测试已按模块意图拆分,但 runtime 物理迁移尚未完全切断依赖 -- `RegisterMediatorBehavior`、`MediatorCoroutineExtensions` 与 `ContextAwareMediator*Extensions` 仍作为兼容层存在,未来真正移除时仍需单独规划弃用窗口 ## 活跃文档 - 历史跟踪归档:[cqrs-rewrite-history-through-rp043.md](../archive/todos/cqrs-rewrite-history-through-rp043.md) +- 验证历史归档:[cqrs-rewrite-validation-history-through-rp062.md](../archive/todos/cqrs-rewrite-validation-history-through-rp062.md) - 历史 trace 归档:[cqrs-rewrite-history-through-rp043.md](../archive/traces/cqrs-rewrite-history-through-rp043.md) +- `RP-046` 至 `RP-061` trace 归档:[cqrs-rewrite-history-rp046-through-rp061.md](../archive/traces/cqrs-rewrite-history-rp046-through-rp061.md) ## 验证说明 - `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` +- `RP-046` 至 `RP-062` 的历史验证命令与阶段性结果已移入验证归档,active tracking 只保留当前恢复入口需要的最新验证 +- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json` - 结果:通过 - - 备注:`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"` + - 备注:确认当前分支对应 `PR #304`,并定位到仍需本地复核的 CodeRabbit / Greptile open thread +- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` - 结果:通过 - - 备注:`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"` + - 备注:`0 warning / 0 error`;本轮确认 XML 文档补齐、`NonParallelizable`、`_syncRoot` 命名与 `ai-plan` 收敛未引入新增编译问题 +- `bash scripts/validate-csharp-naming.sh` - 结果:通过 - - 备注:`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 去重路径 -- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error` -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - 结果:通过 - - 备注:`17/17` 测试通过;本轮覆盖字符串 fallback 合同兼容路径与直接 `Type` fallback 元数据优先级 -- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error` -- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error` -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` - - 结果:通过 - - 备注:`13/13` 测试通过;本轮覆盖 mixed fallback metadata 的 registrar 消费路径 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - 结果:通过 - - 备注:`18/18` 测试通过;本轮覆盖 mixed fallback metadata 的双特性发射路径 -- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-pr-review.json` - - 结果:通过 - - 备注:确认当前分支对应 `PR #302`;latest head review 仍有 `3` 条 open AI threads,其中 MegaLinter 仅报告 `dotnet-format` restore failure 噪音 -- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error` -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - 结果:通过 - - 备注:`18/18` 测试通过;本轮直接覆盖 fallback preamble 排版与特性个数断言收紧 -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` - - 结果:通过 - - 备注:`13/13` 测试通过;本轮确认 mixed fallback metadata 的 registrar 消费路径未回归 + - 备注:使用显式 `GIT_DIR` / `GIT_WORK_TREE` 绑定重跑后,`1045` 个 tracked C# 文件的命名校验全部通过;本轮 `_syncRoot` 改名未引入命名规则回归 ## 下一步 -1. 继续 `Phase 8` 主线,优先再找一个收益明确的 generator 覆盖缺口,继续减少仍必须依赖字符串 fallback 元数据的 handler 类型形态 -2. 若继续文档主线,优先再扫 `docs/zh-CN/api-reference` 与教程入口页,补齐仍过时的 CQRS API / 命名空间表述 -3. 若后续再出现新的 PR review 或 review thread 变化,再重新执行 `$gframework-pr-review` 作为独立验证步骤 +1. push 当前 follow-up 提交后,重新执行 `$gframework-pr-review`,确认 `PR #304` 的 latest unresolved threads 是否已刷新为已解决,或仅剩新增有效项 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 cb008db1..821683e2 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,154 +1,37 @@ # CQRS 重写迁移追踪 -## 2026-04-29 +## 2026-04-30 -### 阶段:mixed fallback 元数据拆分(CQRS-REWRITE-RP-052) +### 阶段:PR #304 剩余 review follow-up 收敛(CQRS-REWRITE-RP-062) -- 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮把上一批的“全部可直接引用 fallback handlers 走 `Type[]`”继续推进到 mixed 场景 -- 先复核现状后确认: - - `CqrsHandlerRegistrar` 已天然支持读取多个 `CqrsReflectionFallbackAttribute` 实例 - - 上一批真正阻止 mixed 场景继续收敛的点,是 runtime attribute 本身尚未开放多实例,以及 generator 只能二选一发射单个 fallback 特性 -- 已在 `GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs` 中将特性约束改为 `AllowMultiple = true`,并补充注释说明多个实例的用途 -- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中扩展 fallback 合同探测: - - 探测 runtime 是否支持 `params string[]` - - 探测 runtime 是否支持 `params Type[]` - - 探测 runtime 是否允许多个 `CqrsReflectionFallbackAttribute` 实例 -- 已在 `CqrsHandlerRegistryGenerator.Models.cs` 与 `CqrsHandlerRegistryGenerator.SourceEmission.cs` 中重构 fallback 发射模型: - - fallback 元数据现在可表示为一个或多个程序集级特性实例 - - 当 fallback handlers 全部可直接引用时,继续优先输出单个 `Type[]` 特性 - - 当 fallback 同时包含可直接引用与仅能按名称恢复的 handlers,且 runtime 支持多实例时,拆分输出一条 `Type[]` 特性和一条字符串特性 - - 若 runtime 不支持多实例或缺少相应构造函数,仍整体回退到字符串元数据,避免 mixed 场景漏注册 -- 已补充 runtime 与 generator 双侧回归: - - `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs` 新增 mixed fallback metadata 用例,锁定 registrar 只对字符串条目调用一次 `Assembly.GetType(...)` - - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增 mixed fallback emission 用例,锁定 generator 会输出两个程序集级 fallback 特性实例 -- 同步更新: - - `GFramework.Cqrs.SourceGenerators/README.md` - - `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md` - - 说明 mixed 场景现在会拆分 `Type` 元数据与字符串元数据 -- 定向验证已通过: - - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` - - `13/13` passed - - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - `18/18` passed -- 随后按 `$gframework-pr-review` 重新拉取当前分支 PR 审查数据: - - 当前 worktree `feat/cqrs-optimization` 已对应 `PR #302` - - latest head commit 仍有 `3` 条 open AI review threads:Greptile 指向 generator preamble 的死参数与多实例 fallback 特性空行,CodeRabbit 指向 mixed/direct fallback 测试断言过宽 - - MegaLinter 仍只暴露 `dotnet-format` 的 `Restore operation failed`,未给出本地仍成立的格式文件线索,因此按环境噪音处理 -- 本轮已继续收口 `RP-052` 的 follow-up: - - 在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs` 中移除已不再参与判断的 `generationEnvironment` 透传参数 - - 调整多实例 fallback 特性发射时的换行策略,避免最后一个 fallback 特性与 `CqrsHandlerRegistryAttribute` 之间保留多余空行 - - 在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 中补强 direct/mixed fallback 发射断言,锁定特性实例个数、拒绝空 marker,并确保 mixed 场景的程序集级 preamble 排版稳定 - - 在 `GFramework.Cqrs.Tests/Cqrs/ReflectionFallbackNotificationContainer.cs` 中为 `DirectFallbackHandlerType` 补齐 `` XML 文档 -- 本轮 review follow-up 验证已通过: - - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - `0 warning / 0 error` - - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - `18/18` passed - - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` - - `13/13` passed +- 本轮再次执行 `$gframework-pr-review`,确认当前分支 `feat/cqrs-optimization` 仍对应 `PR #304` +- 本地复核后继续收敛了上一轮遗留的 review 项: + - `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs` 已补 `NonParallelizable` + - `GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs` 已改用 `_syncRoot` 命名,并补齐缺失的 XML 文档标签 + - `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 三个内部 `Handle(...)` 已补齐 XML `param` / `returns` + - `DispatcherNotificationContextRefreshNotification` 与 `DispatcherStreamContextRefreshRequest` 已补 `DispatchId` XML 参数注释 + - `cqrs-rewrite` active tracking / trace 已压缩为当前恢复入口,并将已完成阶段的详细历史移入 archive +- 验证: + - `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` + - 结果:通过,`0 warning / 0 error` -## 2026-04-20 +## 活跃事实 -### 阶段:direct fallback 元数据优先级收敛(CQRS-REWRITE-RP-051) +- 当前主题仍处于 `Phase 8` +- `PR #304` 的本地 follow-up 已再次收口一轮,后续需要在 push 后重新观察 GitHub 的 unresolved thread 刷新结果 +- 已完成阶段的详细执行历史不再留在 active trace;默认恢复入口只保留当前恢复点、活跃事实、风险与下一步 -- 重新按 `gframework-batch-boot 50` 恢复 `Phase 8` 后,先复核当前 worktree 的恢复入口、`origin/main` 基线与分支规模: - - worktree 仍映射到 `cqrs-rewrite` - - 基线按批处理约定固定为 `origin/main` - - 本轮开始前分支累计 diff 为 `0 files / 0 lines` -- 结合当前代码热点与历史归档后,选择本轮批次目标为“继续收敛 generator fallback 元数据,进一步减少 runtime 按字符串类型名回查 handler 的场景” -- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中新增 runtime fallback 合同探测: - - 识别 `CqrsReflectionFallbackAttribute` 是否支持 `params string[]` - - 识别 `CqrsReflectionFallbackAttribute` 是否支持 `params Type[]` -- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs` 与 - `CqrsHandlerRegistryGenerator.SourceEmission.cs` 中收敛 fallback 发射策略: - - 当本轮所有 fallback handlers 都可被生成代码直接引用,且 runtime 支持 `params Type[]` 时,生成器现优先发射 `typeof(...)` 形式的程序集级 fallback 元数据 - - 当 fallback handlers 中仍存在不能直接引用的实现类型时,生成器继续整体回退到字符串元数据,避免 mixed 场景下部分 handler 走 `Type[]`、其余 handler 丢失恢复入口 -- 已在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 补充回归: - - 锁定 runtime 同时暴露字符串与 `Type` 两类 fallback 构造函数时,生成器优先选择直接 `Type` 元数据 - - 保留现有字符串 fallback 合同测试,确保旧 contract 兼容路径不回退 -- 同步更新: - - `GFramework.Cqrs.SourceGenerators/README.md` - - `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md` - - 说明“可直接引用的 fallback handlers 会优先走 `typeof(...)` 元数据,减少运行时字符串回查” -- 定向验证已通过: - - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - - `17/17` passed -- 额外修正: - - active tracking 中原先引用的 `ai-plan/migration/CQRS_MODULE_SPLIT_PLAN.md` 在当前 worktree 已不存在;本轮已移除该失效路径,后续以 active tracking / trace 作为默认恢复入口 +## 当前风险 -### 阶段:pointer / function pointer 泛型合同拒绝(CQRS-REWRITE-RP-050) +- 当前 `dotnet build GFramework.sln -c Release` 在 WSL 环境仍会受顶层 `GFramework.csproj` 的 Windows NuGet fallback 配置影响 +- 远端 review thread 在本地提交前不会自动刷新,GitHub 上看到的 open 状态可能暂时滞后于当前代码 -- 重新执行 `$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 +## Archive Context -### 阶段: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 - -- 历史跟踪归档: - - `ai-plan/public/cqrs-rewrite/archive/todos/cqrs-rewrite-history-through-rp043.md` - 历史 trace 归档: - `ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-through-rp043.md` + - `ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-rp046-through-rp061.md` -### 当前下一步 +## 当前下一步 -1. 回到 `Phase 8` 主线,优先再找一个 generator 覆盖缺口,继续减少仍需程序集级字符串 fallback 元数据的 handler 场景 -2. 若继续文档主线,优先补齐 `docs/zh-CN/api-reference` 与教程入口页中仍过时的 CQRS API / 命名空间表述 -3. 若后续 review thread 或 PR 状态再次变化,再重新执行 `$gframework-pr-review` 复核远端信号 +1. push 当前 follow-up 提交后,重新执行 `$gframework-pr-review`,确认 `PR #304` 的 latest unresolved threads 是否已刷新为已解决,或仅剩新增有效项 diff --git a/docs/zh-CN/api-reference/index.md b/docs/zh-CN/api-reference/index.md index e6fac1dc..7a496c5b 100644 --- a/docs/zh-CN/api-reference/index.md +++ b/docs/zh-CN/api-reference/index.md @@ -29,7 +29,7 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题 | 模块族 | 先看什么 | 继续深入 | XML 文档关注点 | | --- | --- | --- | --- | | `Core` / `Core.Abstractions` | [Core 模块](../core/index.md) | [Core 抽象层说明](../abstractions/core-abstractions.md)、[快速开始](../getting-started/quick-start.md) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 | -| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [CQRS 运行时](../core/cqrs.md) | [CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)、[协程系统](../core/coroutine.md) | request / notification / handler / pipeline / registry / fallback contract | +| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [CQRS 运行时](../core/cqrs.md) | [CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)、[协程系统](../core/coroutine.md) | request / notification / handler / pipeline / generated registry / targeted fallback contract | | `Game` / `Game.Abstractions` / `Game.SourceGenerators` | [Game 模块总览](../game/index.md) | [Game 抽象层说明](../abstractions/game-abstractions.md)、[配置系统](../game/config-system.md) | 配置、数据、设置、场景、UI、存储、序列化契约 | | `Godot` / `Godot.SourceGenerators` | [Godot 模块总览](../godot/index.md) | [Godot 项目生成器](../source-generators/godot-project-generator.md)、[GetNode 生成器](../source-generators/get-node-generator.md)、[BindNodeSignal 生成器](../source-generators/bind-node-signal-generator.md) | 节点扩展、场景 / UI 适配、配置 / 存储 / 设置接线、Godot 生成器入口 | | `Ecs.Arch` / `Ecs.Arch.Abstractions` | [ECS 模块总览](../ecs/index.md) | [Arch ECS 集成](../ecs/arch.md)、[Ecs.Arch 抽象层说明](../abstractions/ecs-arch-abstractions.md) | ECS 模块契约、系统适配、配置对象和运行时装配边界 | diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 7bde2c3a..655f0b66 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -19,7 +19,7 @@ description: Cqrs 模块族的运行时、契约层、生成器入口,以及 | --- | --- | --- | | `GeWuYou.GFramework.Cqrs.Abstractions` | 纯契约层,定义 request、notification、stream、handler、pipeline、runtime seam | 需要把消息契约放到更稳定的共享层,或只依赖接口做解耦 | | `GeWuYou.GFramework.Cqrs` | 默认 runtime,提供 dispatcher、handler 基类、上下文扩展和程序集注册流程 | 大多数直接消费 CQRS 的业务模块 | -| `GeWuYou.GFramework.Cqrs.SourceGenerators` | 编译期生成 `ICqrsHandlerRegistry`,缩小运行时反射扫描范围 | handler 较多,想把注册映射前移到编译期 | +| `GeWuYou.GFramework.Cqrs.SourceGenerators` | 编译期生成 `ICqrsHandlerRegistry`,让运行时先走生成注册器,再只对剩余 handler 做定向 fallback | handler 较多,想把注册映射前移到编译期 | ## 最小接入路径 @@ -156,8 +156,12 @@ protected override void OnInitialize() 1. 优先读取消费端程序集上的 `CqrsHandlerRegistryAttribute` 2. 存在生成注册器时优先使用 `ICqrsHandlerRegistry` 3. 生成注册器不可用或元数据异常时记录告警并回退到反射路径 -4. 如果程序集带有 `CqrsReflectionFallbackAttribute`,只补扫剩余 handler -5. 同一程序集按稳定键去重,避免重复注册 +4. 当生成注册器携带 `CqrsReflectionFallbackAttribute` 元数据时,运行时会先完成生成注册器注册,再补剩余 handler +5. `CqrsReflectionFallbackAttribute` 可以同时携带 `Type[]` 和 `string[]` 两类清单;运行时会优先复用直接 `Type` 条目,只对名称条目做定向 `Assembly.GetType(...)` 查找 +6. 只有旧版空 marker 或生成注册器不可用时,才会回到整程序集反射扫描 +7. 同一程序集按稳定键去重,避免重复注册 + +换句话说,声明 fallback 特性本身不等于“整包反射扫描”。当前推荐理解是:生成注册器负责能静态表达的部分,fallback 只补它覆盖不到的 handler。 `Cqrs.SourceGenerators` 的专题入口见[CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)。 @@ -205,8 +209,8 @@ RegisterCqrsPipelineBehavior>(); | `GFramework.Cqrs.Abstractions/Cqrs/` | `ICqrsRuntime`、`ICqrsHandlerRegistrar`、`IPipelineBehavior<,>`、`IRequestHandler<,>`、`Unit` | 请求、处理器和 runtime seam 的最小契约 | | `GFramework.Cqrs/Command` `Query` `Notification` `Request` `Extensions` | `CommandBase`、`QueryBase`、`NotificationBase`、`RequestBase`、`ContextAwareCqrsExtensions` | 业务侧常用基类和上下文发送入口 | | `GFramework.Cqrs/Cqrs/` | `AbstractCommandHandler<,>`、`AbstractQueryHandler<,>`、`AbstractRequestHandler<,>`、`AbstractStreamCommandHandler<,>`、`AbstractStreamQueryHandler<,>`、`LoggingBehavior<,>` | 默认处理器基类、上下文注入、流式处理与行为管道 | -| `GFramework.Cqrs` 根入口与 `Internal/` | `CqrsRuntimeFactory`、`ICqrsHandlerRegistry`、`CqrsHandlerRegistryAttribute`、`CqrsReflectionFallbackAttribute`、`DefaultCqrsRegistrationService` | runtime 创建入口、registry 协议、fallback 语义和程序集去重规则 | -| `GFramework.Cqrs.SourceGenerators/Cqrs/` | `CqrsHandlerRegistryGenerator`、`RuntimeTypeReferenceSpec`、`OrderedRegistrationKind` | 生成注册器、精确 type lookup 和 fallback 诊断边界 | +| `GFramework.Cqrs` 根入口与 `Internal/` | `CqrsRuntimeFactory`、`ICqrsHandlerRegistry`、`CqrsHandlerRegistryAttribute`、`CqrsReflectionFallbackAttribute`、`DefaultCqrsRegistrationService` | runtime 创建入口、generated-registry 优先级、targeted fallback 语义和程序集去重规则 | +| `GFramework.Cqrs.SourceGenerators/Cqrs/` | `CqrsHandlerRegistryGenerator`、`RuntimeTypeReferenceSpec`、`OrderedRegistrationKind` | 生成注册器、可直接引用类型判定、mixed fallback 发射与诊断边界 | ## 继续阅读