From 16cd96b94b6b315e6f730c06a12e1c2110a5ac0c Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:42:21 +0800 Subject: [PATCH] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E5=85=85=20dispatcher?= =?UTF-8?q?=20=E7=BC=93=E5=AD=98=E4=B8=8A=E4=B8=8B=E6=96=87=E5=9B=9E?= =?UTF-8?q?=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 cached request pipeline executor 的上下文刷新回归测试与专用测试替身 - 记录 singleton behavior 生命周期语义下的上下文重新注入结论 - 更新 cqrs-rewrite 跟踪与 trace 恢复点到 RP-057 --- .../Cqrs/CqrsDispatcherCacheTests.cs | 56 ++++++++++++++++ ...ispatcherPipelineContextRefreshBehavior.cs | 31 +++++++++ ...DispatcherPipelineContextRefreshRequest.cs | 9 +++ ...herPipelineContextRefreshRequestHandler.cs | 29 ++++++++ .../DispatcherPipelineContextRefreshState.cs | 66 +++++++++++++++++++ .../Cqrs/DispatcherPipelineContextSnapshot.cs | 14 ++++ .../todos/cqrs-rewrite-migration-tracking.md | 10 ++- .../traces/cqrs-rewrite-migration-trace.md | 21 ++++++ 8 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshBehavior.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshRequest.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshRequestHandler.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshState.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextSnapshot.cs diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs index 13ffb761..919a15d7 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs @@ -24,6 +24,7 @@ internal sealed class CqrsDispatcherCacheTests LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); _container = new MicrosoftDiContainer(); _container.RegisterCqrsPipelineBehavior(); + _container.RegisterCqrsPipelineBehavior(); _container.RegisterCqrsPipelineBehavior(); _container.RegisterCqrsPipelineBehavior(); @@ -34,6 +35,7 @@ internal sealed class CqrsDispatcherCacheTests _container.Freeze(); _context = new ArchitectureContext(_container); + DispatcherPipelineContextRefreshState.Reset(); ClearDispatcherCaches(); } @@ -244,6 +246,60 @@ internal sealed class CqrsDispatcherCacheTests }); } + /// + /// 验证缓存的 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)); + }); + } + /// /// 通过反射读取 dispatcher 的静态缓存对象。 /// 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..15913678 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineContextRefreshState.cs @@ -0,0 +1,66 @@ +using System.Threading; +using GFramework.Core.Abstractions.Architectures; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 记录 pipeline executor 缓存回归中每次分发实际使用的上下文与实例身份。 +/// +internal static class DispatcherPipelineContextRefreshState +{ + private static int _nextBehaviorInstanceId; + private static int _nextHandlerInstanceId; + + /// + /// 获取每次 behavior 执行时记录的快照。 + /// + public static List BehaviorSnapshots { get; } = []; + + /// + /// 获取每次 handler 执行时记录的快照。 + /// + public static List HandlerSnapshots { get; } = []; + + /// + /// 为新的 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) + { + BehaviorSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + } + + /// + /// 记录 handler 在当前分发中观察到的上下文。 + /// + public static void RecordHandler(string dispatchId, int instanceId, IArchitectureContext context) + { + HandlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context)); + } + + /// + /// 清空历史记录与实例编号,避免跨测试污染断言。 + /// + public static void Reset() + { + _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/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md index af69ff10..ec7fcc6b 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-056` +- 恢复点编号:`CQRS-REWRITE-RP-057` - 当前阶段:`Phase 8` - 当前焦点: - 当前功能历史已归档,active 跟踪仅保留 `Phase 8` 主线的恢复入口 @@ -17,6 +17,7 @@ CQRS 迁移与收敛。 - 当 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` - 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke` - 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物 - 已修正 pointer / function pointer 泛型合同的错误覆盖:生成器不再为这两类类型发射 precise runtime type 重建代码 @@ -90,6 +91,10 @@ CQRS 迁移与收敛。 - `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` 已完成一轮 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 残留清理: @@ -117,6 +122,9 @@ CQRS 迁移与收敛。 - `RP-043` 之前的详细阶段记录、定向验证命令和阶段性决策均已移入主题内归档 - active 跟踪文件只保留当前恢复点、当前活跃事实、风险和下一步,避免 `boot` 在默认入口中重复扫描 1000+ 行历史 trace +- `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 --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false` - 结果:通过 - 备注:`63/63` 测试通过;当前沙箱限制了 MSBuild named pipe,验证需在提权环境下运行 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 a465b160..7cf929c4 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 @@ -2,6 +2,27 @@ ## 2026-04-29 +### 阶段:cached executor 上下文刷新回归(CQRS-REWRITE-RP-057) + +- 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮只处理一个窄写集测试批次:为 cached request pipeline executor 增加“重复分发仍重新注入上下文”的回归 +- 先复核上一轮 request pipeline executor 形状缓存实现与测试边界后确认: + - 当前 runtime 只允许本轮写集落在 `GFramework.Cqrs.Tests/Cqrs/`,除非测试直接打出 `CqrsDispatcher` 的真实缺陷 + - 目标是锁定 executor 缓存不会跨分发保留旧 `ArchitectureContext`,且不扩张到 notification / stream 路径 +- 已完成的测试补强: + - 在 `GFramework.Cqrs.Tests/Cqrs/` 新增 `DispatcherPipelineContextRefreshRequest`、`DispatcherPipelineContextRefreshBehavior`、`DispatcherPipelineContextRefreshRequestHandler`、`DispatcherPipelineContextRefreshState` 与 `DispatcherPipelineContextSnapshot` + - `DispatcherPipelineContextRefreshBehavior` 与 `DispatcherPipelineContextRefreshRequestHandler` 都基于 `CqrsContextAwareHandlerBase` 记录当次看到的 `ArchitectureContext` + - `CqrsDispatcherCacheTests` 新增 `Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Request_Pipeline_Executor`,断言同一个 cached executor 在两次分发间保持 executor 形状复用,但 handler 不会被 executor 黏住,且 handler / behavior 都会观察到本次分发的新上下文 +- 调试过程中的结论: + - 初版断言曾要求 behavior 实例编号跨分发变化,随后确认这是错误假设 + - `MicrosoftDiContainer.RegisterCqrsPipelineBehavior()` 对已闭合的 pipeline behavior 使用的是 `AddSingleton(...)` + - 因此本轮最终锁定的是“singleton behavior 也必须重新注入上下文”,而不是强行要求 behavior 生命周期为 transient +- 定向验证已通过: + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` + - `5/5` passed +- 结果: + - 本轮未暴露新的 runtime 实现缺口,因此没有改动 `GFramework.Cqrs/Internal/CqrsDispatcher.cs` + - 当前分支相对 `origin/main` 的累计提交 diff 仍为 `14 files`,继续低于 `gframework-batch-boot 50` 的主要 stop condition + ### 阶段:pointer runtime-reconstruction 残留清理(CQRS-REWRITE-RP-056) - 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮只处理一个写集很窄的 generator 清理切片:删除 `CqrsHandlerRegistryGenerator` 里已经不可达的 pointer runtime-reconstruction 残留