From 255a6a152e06be24c9f8215107f5af22b8c297d7 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:43:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(cqrs):=20=E6=94=B6=E6=95=9B=20PR=20304=20re?= =?UTF-8?q?view=20=E8=B7=9F=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 CqrsDispatcher 的 pipeline invoker 重复创建,并补齐缓存线程模型文档 - 优化 CQRS 与 generator 回归测试的并发保护和稳定语义断言 - 更新 cqrs-rewrite 跟踪与 trace,记录 RP-062 的 PR review follow-up 验证结果 --- .../Cqrs/CqrsDispatcherCacheTests.cs | 10 ++++ ...qrsHandlerRegistrarFallbackFailureTests.cs | 8 ++- .../Cqrs/CqrsRegistrationServiceTests.cs | 17 +++--- ...spatcherNotificationContextRefreshState.cs | 28 ++++++++-- .../DispatcherPipelineContextRefreshState.cs | 52 +++++++++++++++---- ...patcherPipelineOrderCacheRequestHandler.cs | 2 +- .../DispatcherPipelineOrderInnerBehavior.cs | 4 +- .../DispatcherPipelineOrderOuterBehavior.cs | 4 +- .../Cqrs/DispatcherPipelineOrderState.cs | 36 +++++++++++-- .../DispatcherStreamContextRefreshState.cs | 28 ++++++++-- GFramework.Cqrs/Internal/CqrsDispatcher.cs | 27 +++++++--- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 33 ++++++------ .../todos/cqrs-rewrite-migration-tracking.md | 10 +++- .../traces/cqrs-rewrite-migration-trace.md | 31 +++++++++++ 14 files changed, 228 insertions(+), 62 deletions(-) diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs index 3d2040ab..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; @@ -434,6 +435,11 @@ internal sealed class CqrsDispatcherCacheTests /// /// 读取 request dispatch binding 中指定行为数量的 pipeline executor 缓存项。 /// + /// dispatcher 内部的 request binding 缓存对象。 + /// 要读取的请求运行时类型。 + /// 要读取的响应运行时类型。 + /// 目标 executor 对应的行为数量。 + /// 已缓存的 executor;若 binding 或 executor 尚未建立则返回 private static object? GetRequestPipelineExecutorValue( object requestBindings, Type requestType, @@ -471,6 +477,10 @@ internal sealed class CqrsDispatcherCacheTests /// /// 读取指定请求/响应类型对对应的强类型 request dispatch binding。 /// + /// dispatcher 内部的 request binding 缓存对象。 + /// 要读取的请求运行时类型。 + /// 要读取的响应运行时类型。 + /// 强类型 binding;若缓存尚未建立则返回 private static object? GetRequestDispatchBindingValue(object requestBindings, Type requestType, Type responseType) { var bindingBox = GetPairCacheValue(requestBindings, requestType, responseType); diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs index 213f3870..e4b8633f 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs @@ -174,6 +174,7 @@ internal sealed class CqrsHandlerRegistrarFallbackFailureTests /// /// 清空本测试依赖的 registrar 静态缓存,确保每个用例都会重新执行 fallback 元数据解析。 + /// 这些字段名直接耦合 CqrsHandlerRegistrar 当前内部实现;若后续重构缓存布局,需要同步更新这里。 /// private static void ClearRegistrarCaches() { @@ -194,11 +195,14 @@ internal sealed class CqrsHandlerRegistrarFallbackFailureTests fieldName, BindingFlags.NonPublic | BindingFlags.Static); - Assert.That(field, Is.Not.Null, $"Missing registrar cache field {fieldName}."); + 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} returned null."); + $"Registrar cache field '{fieldName}' on CqrsHandlerRegistrar returned null."); } /// diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs index b8b03b1c..b1c3c60b 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs @@ -69,13 +69,18 @@ internal sealed class CqrsRegistrationServiceTests 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( - logger.Logs.Where(static log => log.Level == LogLevel.Debug).Select(static log => log.Message), - Is.EqualTo( - [ - "Skipping CQRS handler registration for assembly " + - "GFramework.Cqrs.Tests.RegisteredAssembly, Version=1.0.0.0 because it was already registered." - ])); + 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")); }); } diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshState.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshState.cs index 43c32531..b6d8a7ca 100644 --- a/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshState.cs +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherNotificationContextRefreshState.cs @@ -8,12 +8,24 @@ namespace GFramework.Cqrs.Tests.Cqrs; /// internal static class DispatcherNotificationContextRefreshState { + private static readonly Lock SyncRoot = new(); private static int _nextHandlerInstanceId; + private static readonly List _handlerSnapshots = []; /// - /// 获取每次 notification 分发时记录的快照。 + /// 获取每次 notification 分发时记录的快照副本。 + /// 共享状态通过 SyncRoot 串行化,避免并行测试写入抖动。 /// - public static List HandlerSnapshots { get; } = []; + public static IReadOnlyList HandlerSnapshots + { + get + { + lock (SyncRoot) + { + return _handlerSnapshots.ToArray(); + } + } + } /// /// 为新的 handler 测试实例分配稳定编号。 @@ -28,7 +40,10 @@ internal static class DispatcherNotificationContextRefreshState /// public static void Record(string dispatchId, int instanceId, IArchitectureContext context) { - HandlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + lock (SyncRoot) + { + _handlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + } } /// @@ -36,7 +51,10 @@ internal static class DispatcherNotificationContextRefreshState /// public static void Reset() { - _nextHandlerInstanceId = 0; - HandlerSnapshots.Clear(); + lock (SyncRoot) + { + _nextHandlerInstanceId = 0; + _handlerSnapshots.Clear(); + } } } diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshState.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshState.cs index 15913678..99cae23a 100644 --- a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshState.cs +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshState.cs @@ -8,18 +8,41 @@ namespace GFramework.Cqrs.Tests.Cqrs; /// 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 执行时记录的快照。 + /// 获取每次 behavior 执行时记录的快照副本。 + /// 共享状态通过 SyncRoot 串行化,读取端始终拿到当前稳定快照。 /// - public static List BehaviorSnapshots { get; } = []; + public static IReadOnlyList BehaviorSnapshots + { + get + { + lock (SyncRoot) + { + return _behaviorSnapshots.ToArray(); + } + } + } /// - /// 获取每次 handler 执行时记录的快照。 + /// 获取每次 handler 执行时记录的快照副本。 + /// 共享状态通过 SyncRoot 串行化,读取端始终拿到当前稳定快照。 /// - public static List HandlerSnapshots { get; } = []; + public static IReadOnlyList HandlerSnapshots + { + get + { + lock (SyncRoot) + { + return _handlerSnapshots.ToArray(); + } + } + } /// /// 为新的 behavior 测试实例分配稳定编号。 @@ -42,7 +65,10 @@ internal static class DispatcherPipelineContextRefreshState /// public static void RecordBehavior(string dispatchId, int instanceId, IArchitectureContext context) { - BehaviorSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + lock (SyncRoot) + { + _behaviorSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + } } /// @@ -50,7 +76,10 @@ internal static class DispatcherPipelineContextRefreshState /// public static void RecordHandler(string dispatchId, int instanceId, IArchitectureContext context) { - HandlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + lock (SyncRoot) + { + _handlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + } } /// @@ -58,9 +87,12 @@ internal static class DispatcherPipelineContextRefreshState /// public static void Reset() { - _nextBehaviorInstanceId = 0; - _nextHandlerInstanceId = 0; - BehaviorSnapshots.Clear(); - HandlerSnapshots.Clear(); + lock (SyncRoot) + { + _nextBehaviorInstanceId = 0; + _nextHandlerInstanceId = 0; + _behaviorSnapshots.Clear(); + _handlerSnapshots.Clear(); + } } } diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequestHandler.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequestHandler.cs index 00224ff8..d2b88baf 100644 --- a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequestHandler.cs +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderCacheRequestHandler.cs @@ -15,7 +15,7 @@ internal sealed class DispatcherPipelineOrderCacheRequestHandler : IRequestHandl /// 固定整数结果。 public ValueTask Handle(DispatcherPipelineOrderCacheRequest request, CancellationToken cancellationToken) { - DispatcherPipelineOrderState.Steps.Add("Handler"); + DispatcherPipelineOrderState.Record("Handler"); return ValueTask.FromResult(3); } } diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderInnerBehavior.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderInnerBehavior.cs index a4b7def6..cf327d2a 100644 --- a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderInnerBehavior.cs +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderInnerBehavior.cs @@ -19,9 +19,9 @@ internal sealed class DispatcherPipelineOrderInnerBehavior : IPipelineBehavior next, CancellationToken cancellationToken) { - DispatcherPipelineOrderState.Steps.Add("Inner:Before"); + DispatcherPipelineOrderState.Record("Inner:Before"); var result = await next(request, cancellationToken).ConfigureAwait(false); - DispatcherPipelineOrderState.Steps.Add("Inner:After"); + DispatcherPipelineOrderState.Record("Inner:After"); return result; } } diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderOuterBehavior.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderOuterBehavior.cs index b9ba2315..aa1f0291 100644 --- a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderOuterBehavior.cs +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderOuterBehavior.cs @@ -19,9 +19,9 @@ internal sealed class DispatcherPipelineOrderOuterBehavior : IPipelineBehavior next, CancellationToken cancellationToken) { - DispatcherPipelineOrderState.Steps.Add("Outer:Before"); + DispatcherPipelineOrderState.Record("Outer:Before"); var result = await next(request, cancellationToken).ConfigureAwait(false); - DispatcherPipelineOrderState.Steps.Add("Outer:After"); + DispatcherPipelineOrderState.Record("Outer:After"); return result; } } diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderState.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderState.cs index 2673371e..fa202b85 100644 --- a/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderState.cs +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderState.cs @@ -1,3 +1,5 @@ +using System.Threading; + namespace GFramework.Cqrs.Tests.Cqrs; /// @@ -5,16 +7,44 @@ namespace GFramework.Cqrs.Tests.Cqrs; /// internal static class DispatcherPipelineOrderState { + private static readonly Lock SyncRoot = new(); + private static readonly List _steps = []; + /// - /// 获取按执行顺序追加的步骤名称。 + /// 获取按执行顺序追加的步骤快照。 + /// 共享状态通过 SyncRoot 串行化,避免并行行为测试互相污染步骤列表。 /// - public static List Steps { get; } = []; + 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() { - Steps.Clear(); + lock (SyncRoot) + { + _steps.Clear(); + } } } diff --git a/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs index a315451d..4f747b7e 100644 --- a/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs @@ -8,12 +8,24 @@ namespace GFramework.Cqrs.Tests.Cqrs; /// internal static class DispatcherStreamContextRefreshState { + private static readonly Lock SyncRoot = new(); private static int _nextHandlerInstanceId; + private static readonly List _handlerSnapshots = []; /// - /// 获取每次建流时记录的快照。 + /// 获取每次建流时记录的快照副本。 + /// 共享状态通过 SyncRoot 串行化,避免并行测试写入抖动。 /// - public static List HandlerSnapshots { get; } = []; + public static IReadOnlyList HandlerSnapshots + { + get + { + lock (SyncRoot) + { + return _handlerSnapshots.ToArray(); + } + } + } /// /// 为新的 handler 测试实例分配稳定编号。 @@ -28,7 +40,10 @@ internal static class DispatcherStreamContextRefreshState /// public static void Record(string dispatchId, int instanceId, IArchitectureContext context) { - HandlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + lock (SyncRoot) + { + _handlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + } } /// @@ -36,7 +51,10 @@ internal static class DispatcherStreamContextRefreshState /// public static void Reset() { - _nextHandlerInstanceId = 0; - HandlerSnapshots.Clear(); + lock (SyncRoot) + { + _nextHandlerInstanceId = 0; + _handlerSnapshots.Clear(); + } } } diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index 17e81e8e..d604beac 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -415,7 +415,11 @@ internal sealed class CqrsDispatcher( RequestInvoker requestInvoker, Type requestType) { + // 线程安全:该缓存按 behaviorCount 复用 pipeline executor 形状,GetPipelineExecutor 通过 ConcurrentDictionary + // 的 GetOrAdd 支持并发读写。缓存项只保存委托形状,不保留 handler/behavior 实例;若行为数量组合持续增长, + // 字典会随之增长且当前实现不提供回收。 private readonly ConcurrentDictionary> _pipelineExecutors = new(); + private readonly RequestPipelineInvoker _pipelineInvoker = CreateRequestPipelineInvoker(requestType); /// /// 获取请求处理器在容器中的服务类型。 @@ -441,8 +445,8 @@ internal sealed class CqrsDispatcher( ArgumentOutOfRangeException.ThrowIfNegative(behaviorCount); return _pipelineExecutors.GetOrAdd>( behaviorCount, - static (count, state) => CreateRequestPipelineExecutor(state.RequestType, count), - new RequestPipelineExecutorFactoryState(requestType)); + static (count, state) => CreateRequestPipelineExecutor(count, state.PipelineInvoker), + new RequestPipelineExecutorFactoryState(_pipelineInvoker)); } /// @@ -460,17 +464,23 @@ internal sealed class CqrsDispatcher( /// 行为数量用于表达缓存形状,实际分发仍会消费本次容器解析出的 handler 与 behaviors 实例。 /// private static RequestPipelineExecutor CreateRequestPipelineExecutor( - Type requestType, - int behaviorCount) + 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)); - var invoker = (RequestPipelineInvoker)Delegate.CreateDelegate( + return (RequestPipelineInvoker)Delegate.CreateDelegate( typeof(RequestPipelineInvoker), method); - return new RequestPipelineExecutor(behaviorCount, invoker); } /// @@ -510,7 +520,8 @@ internal sealed class CqrsDispatcher( /// 为 pipeline executor 缓存携带当前请求类型,避免按行为数量建缓存时创建闭包。 /// /// 请求响应类型。 - private readonly record struct RequestPipelineExecutorFactoryState(Type RequestType); + private readonly record struct RequestPipelineExecutorFactoryState( + RequestPipelineInvoker PipelineInvoker); /// /// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。 @@ -537,6 +548,8 @@ internal sealed class CqrsDispatcher( /// /// 获取指定阶段的 continuation,并在首次请求时为该阶段绑定一次不可变调用入口。 /// 同一行为多次调用 next 时会命中相同 continuation,保持与传统链式委托一致的语义。 + /// 线程模型上,该缓存仅假定单次分发链按顺序推进;若某个 behavior 并发调用多个 next, + /// 这里可能重复创建等价 continuation,但不会跨分发共享,也不会缓存容器解析出的实例。 /// private MessageHandlerDelegate GetContinuation(int index) { diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 51926a0d..2b83cddc 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1763,13 +1763,12 @@ public class CqrsHandlerRegistryGeneratorTests { Assert.That( generatedSource, - Does.Contain( - "var serviceType0_0Argument1Element = registryAssembly.GetType(\"TestApp.Container+HiddenResponse\", throwOnError: false, ignoreCase: false);")); + Does.Contain("registryAssembly.GetType(\"TestApp.Container+HiddenResponse\", throwOnError: false, ignoreCase: false);")); Assert.That( generatedSource, - Does.Contain( - "var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1Element.MakeArrayType(2));")); - Assert.That(generatedSource, Does.Not.Contain("RegisterRemainingReflectedHandlerInterfaces(")); + Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(")); + Assert.That(generatedSource, Does.Contain(".MakeArrayType(2)")); + Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute(")); }); } @@ -1786,13 +1785,12 @@ public class CqrsHandlerRegistryGeneratorTests { Assert.That( generatedSource, - Does.Contain( - "var serviceType0_0Argument1ElementElement = registryAssembly.GetType(\"TestApp.Container+HiddenResponse\", throwOnError: false, ignoreCase: false);")); + Does.Contain("registryAssembly.GetType(\"TestApp.Container+HiddenResponse\", throwOnError: false, ignoreCase: false);")); Assert.That( generatedSource, - Does.Contain( - "var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1ElementElement.MakeArrayType().MakeArrayType());")); - Assert.That(generatedSource, Does.Not.Contain("RegisterRemainingReflectedHandlerInterfaces(")); + Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(")); + Assert.That(generatedSource, Does.Contain(".MakeArrayType().MakeArrayType()")); + Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute(")); }); } @@ -1912,11 +1910,11 @@ public class CqrsHandlerRegistryGeneratorTests Assert.That( generatedSource, Does.Contain( - "var serviceType0_0Argument1Element = ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedResponse\");")); + "ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedResponse\")")); Assert.That( generatedSource, - Does.Contain( - "var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1Element.MakeArrayType(2));")); + Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(")); + Assert.That(generatedSource, Does.Contain(".MakeArrayType(2)")); Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute(")); }); } @@ -1945,16 +1943,15 @@ public class CqrsHandlerRegistryGeneratorTests Assert.That( generatedSource, Does.Contain( - "var serviceType0_0Argument0 = ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedRequest\");")); + "ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedRequest\")")); Assert.That( generatedSource, Does.Contain( - "var serviceType0_0Argument1GenericDefinition = ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedEnvelope`1\");")); + "ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedEnvelope`1\")")); Assert.That( generatedSource, - Does.Contain( - "var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1GenericDefinition.MakeGenericType(typeof(string)));")); - Assert.That(generatedSource, Does.Not.Contain("RegisterRemainingReflectedHandlerInterfaces(")); + 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(")); }); } 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 e41c245f..65874cd5 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-061` +- 恢复点编号:`CQRS-REWRITE-RP-062` - 当前阶段:`Phase 8` - 当前焦点: - 当前功能历史已归档,active 跟踪仅保留 `Phase 8` 主线的恢复入口 @@ -29,6 +29,9 @@ CQRS 迁移与收敛。 - 已将 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 内复用 + - 已收紧 CQRS / generator 回归测试的脆弱断言:日志断言改为语义匹配,precise runtime type lookup 回归改为锁定数组秩、外部类型查找与“未发射 fallback metadata”这些稳定语义 + - 已为 dispatcher cache / context refresh / pipeline order 三组测试状态容器补齐并发保护,并将 `CqrsDispatcherCacheTests` 标记为 `NonParallelizable`,避免静态缓存与共享快照在并行测试中相互污染 - 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点 ## 当前状态摘要 @@ -79,6 +82,11 @@ 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()` 链 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 eb37d88c..25dc1c6a 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,5 +1,36 @@ # CQRS 重写迁移追踪 +## 2026-04-30 + +### 阶段:PR #304 review follow-up 收敛(CQRS-REWRITE-RP-062) + +- 本轮使用 `$gframework-pr-review` 重新抓取当前分支 PR: + - 当前分支 `feat/cqrs-optimization` 对应 `PR #304` + - latest review 信号主要由 `7` 条 CodeRabbit nitpick 与 `2` 条 Greptile open threads 组成 + - MegaLinter 仍只给出 `dotnet-format` 的 `Restore operation failed`,未附带当前仍成立的文件级格式问题;CTRF 汇总为 `2203/2203` passed +- 本地复核后接受并收敛的 review follow-up: + - `GFramework.Cqrs/Internal/CqrsDispatcher.cs` + - 为 `_pipelineExecutors` 与 `RequestPipelineInvocation.GetContinuation(...)` 补齐线程模型与失败模式说明 + - 将 request pipeline invoker 从“按 `behaviorCount` 重复创建”收敛为“binding 内创建一次、executor 缓存复用” + - `GFramework.Cqrs.Tests/Cqrs/*.cs` + - 将 `DispatcherPipelineContextRefreshState`、`DispatcherNotificationContextRefreshState`、`DispatcherStreamContextRefreshState` 与 `DispatcherPipelineOrderState` 切换为 `System.Threading.Lock` 保护的共享状态 + - 将 pipeline 顺序记录从公开可变 `List` 收敛为 `Record(...)` + 快照只读访问 + - 为 `CqrsDispatcherCacheTests` 添加 `[NonParallelizable]`,并补齐反射辅助方法的 XML `param` / `returns` + - 将 `CqrsRegistrationServiceTests` 的 debug 日志断言改为锁定语义片段而非整句文本 + - 将 `CqrsHandlerRegistrarFallbackFailureTests` 的缓存字段诊断改为显式指出 `CqrsHandlerRegistrar` 耦合点 + - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` + - 将 precise runtime type lookup 的数组 / 外部泛型回归断言从局部变量名绑定改为稳定的语义片段断言 +- 验证过程与结果: + - 首次把多个 `dotnet` restore / test 并发跑在同一 worktree 时,`GFramework.Cqrs.Tests` 出现 `*.nuget.g.props already exists` 竞争;该失败属于本地并发 restore 冲突,不代表代码问题 + - 串行重跑后确认: + - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests|FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsRegistrationServiceTests|FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarFallbackFailureTests"` + - 在第一次串行测试中暴露 `MA0158 Use System.Threading.Lock` warning 后,已同轮切换同步原语并准备重跑无 warning 验证 +- 结果: + - 本轮把仍成立的 PR review 评论全部收敛到本地代码或测试基础设施 + - 下一步应以“重跑无 warning 验证 + 提交本轮 follow-up”为恢复入口,而不是继续扩写新的 CQRS 优化切片 + ## 2026-04-29 ### 阶段:registrar fallback 失败分支回归(CQRS-REWRITE-RP-061)