test(cqrs): 补充 dispatcher 缓存上下文回归

- 新增 cached request pipeline executor 的上下文刷新回归测试与专用测试替身

- 记录 singleton behavior 生命周期语义下的上下文重新注入结论

- 更新 cqrs-rewrite 跟踪与 trace 恢复点到 RP-057
This commit is contained in:
gewuyou 2026-04-29 17:42:21 +08:00 committed by GeWuYou
parent 5365f9aec2
commit 16cd96b94b
8 changed files with 235 additions and 1 deletions

View File

@ -24,6 +24,7 @@ internal sealed class CqrsDispatcherCacheTests
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
_container = new MicrosoftDiContainer(); _container = new MicrosoftDiContainer();
_container.RegisterCqrsPipelineBehavior<DispatcherPipelineCacheBehavior>(); _container.RegisterCqrsPipelineBehavior<DispatcherPipelineCacheBehavior>();
_container.RegisterCqrsPipelineBehavior<DispatcherPipelineContextRefreshBehavior>();
_container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderOuterBehavior>(); _container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderOuterBehavior>();
_container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderInnerBehavior>(); _container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderInnerBehavior>();
@ -34,6 +35,7 @@ internal sealed class CqrsDispatcherCacheTests
_container.Freeze(); _container.Freeze();
_context = new ArchitectureContext(_container); _context = new ArchitectureContext(_container);
DispatcherPipelineContextRefreshState.Reset();
ClearDispatcherCaches(); ClearDispatcherCaches();
} }
@ -244,6 +246,60 @@ internal sealed class CqrsDispatcherCacheTests
}); });
} }
/// <summary>
/// 验证缓存的 request pipeline executor 在重复分发时仍会重新解析 handler/behavior
/// 并为当次实例重新注入当前架构上下文。
/// </summary>
[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));
});
}
/// <summary> /// <summary>
/// 通过反射读取 dispatcher 的静态缓存对象。 /// 通过反射读取 dispatcher 的静态缓存对象。
/// </summary> /// </summary>

View File

@ -0,0 +1,31 @@
using System.Threading;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 记录缓存 executor 复用场景下每次分发注入到 behavior 的上下文与实例身份。
/// </summary>
internal sealed class DispatcherPipelineContextRefreshBehavior
: CqrsContextAwareHandlerBase,
IPipelineBehavior<DispatcherPipelineContextRefreshRequest, int>
{
private readonly int _instanceId = DispatcherPipelineContextRefreshState.AllocateBehaviorInstanceId();
/// <summary>
/// 记录当前 behavior 实例实际收到的上下文,然后继续执行下游处理器。
/// </summary>
/// <param name="request">当前请求。</param>
/// <param name="next">下一个处理阶段。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>下游处理结果。</returns>
public async ValueTask<int> Handle(
DispatcherPipelineContextRefreshRequest request,
MessageHandlerDelegate<DispatcherPipelineContextRefreshRequest, int> next,
CancellationToken cancellationToken)
{
DispatcherPipelineContextRefreshState.RecordBehavior(request.DispatchId, _instanceId, Context);
return await next(request, cancellationToken).ConfigureAwait(false);
}
}

View File

@ -0,0 +1,9 @@
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 为 pipeline executor 上下文刷新回归提供带分发标识的最小请求。
/// </summary>
/// <param name="DispatchId">当前分发的稳定标识,便于断言 handler 与 behavior 看到的是同一次请求。</param>
internal sealed record DispatcherPipelineContextRefreshRequest(string DispatchId) : IRequest<int>;

View File

@ -0,0 +1,29 @@
using System.Threading;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 记录缓存 executor 复用场景下每次分发注入到 request handler 的上下文与实例身份。
/// </summary>
internal sealed class DispatcherPipelineContextRefreshRequestHandler
: CqrsContextAwareHandlerBase,
IRequestHandler<DispatcherPipelineContextRefreshRequest, int>
{
private readonly int _instanceId = DispatcherPipelineContextRefreshState.AllocateHandlerInstanceId();
/// <summary>
/// 记录当前 handler 实例收到的上下文,并返回稳定结果。
/// </summary>
/// <param name="request">当前请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>固定整数结果。</returns>
public ValueTask<int> Handle(
DispatcherPipelineContextRefreshRequest request,
CancellationToken cancellationToken)
{
DispatcherPipelineContextRefreshState.RecordHandler(request.DispatchId, _instanceId, Context);
return ValueTask.FromResult(7);
}
}

View File

@ -0,0 +1,66 @@
using System.Threading;
using GFramework.Core.Abstractions.Architectures;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 记录 pipeline executor 缓存回归中每次分发实际使用的上下文与实例身份。
/// </summary>
internal static class DispatcherPipelineContextRefreshState
{
private static int _nextBehaviorInstanceId;
private static int _nextHandlerInstanceId;
/// <summary>
/// 获取每次 behavior 执行时记录的快照。
/// </summary>
public static List<DispatcherPipelineContextSnapshot> BehaviorSnapshots { get; } = [];
/// <summary>
/// 获取每次 handler 执行时记录的快照。
/// </summary>
public static List<DispatcherPipelineContextSnapshot> HandlerSnapshots { get; } = [];
/// <summary>
/// 为新的 behavior 测试实例分配稳定编号。
/// </summary>
public static int AllocateBehaviorInstanceId()
{
return Interlocked.Increment(ref _nextBehaviorInstanceId);
}
/// <summary>
/// 为新的 handler 测试实例分配稳定编号。
/// </summary>
public static int AllocateHandlerInstanceId()
{
return Interlocked.Increment(ref _nextHandlerInstanceId);
}
/// <summary>
/// 记录 behavior 在当前分发中观察到的上下文。
/// </summary>
public static void RecordBehavior(string dispatchId, int instanceId, IArchitectureContext context)
{
BehaviorSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context));
}
/// <summary>
/// 记录 handler 在当前分发中观察到的上下文。
/// </summary>
public static void RecordHandler(string dispatchId, int instanceId, IArchitectureContext context)
{
HandlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context));
}
/// <summary>
/// 清空历史记录与实例编号,避免跨测试污染断言。
/// </summary>
public static void Reset()
{
_nextBehaviorInstanceId = 0;
_nextHandlerInstanceId = 0;
BehaviorSnapshots.Clear();
HandlerSnapshots.Clear();
}
}

View File

@ -0,0 +1,14 @@
using GFramework.Core.Abstractions.Architectures;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 描述单次分发阶段记录下来的上下文与实例身份。
/// </summary>
/// <param name="DispatchId">触发本次记录的请求标识。</param>
/// <param name="InstanceId">当次 handler 或 behavior 实例编号。</param>
/// <param name="Context">当次分发注入的架构上下文。</param>
internal sealed record DispatcherPipelineContextSnapshot(
string DispatchId,
int InstanceId,
IArchitectureContext Context);

View File

@ -7,7 +7,7 @@ CQRS 迁移与收敛。
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`CQRS-REWRITE-RP-056` - 恢复点编号:`CQRS-REWRITE-RP-057`
- 当前阶段:`Phase 8` - 当前阶段:`Phase 8`
- 当前焦点: - 当前焦点:
- 当前功能历史已归档active 跟踪仅保留 `Phase 8` 主线的恢复入口 - 当前功能历史已归档active 跟踪仅保留 `Phase 8` 主线的恢复入口
@ -17,6 +17,7 @@ CQRS 迁移与收敛。
- 当 runtime 不支持多实例 fallback 特性或缺少对应构造函数时mixed fallback 场景仍会整体保守回退到字符串元数据,避免仅部分 handler 走 `Type[]` 时漏掉剩余需按名称恢复的 handlers - 当 runtime 不支持多实例 fallback 特性或缺少对应构造函数时mixed fallback 场景仍会整体保守回退到字符串元数据,避免仅部分 handler 走 `Type[]` 时漏掉剩余需按名称恢复的 handlers
- 已完成 request pipeline executor 形状缓存:`CqrsDispatcher` 现会在单个 request binding 内按 `behaviorCount` 复用强类型 pipeline executor而不是每次 `SendAsync` 都重建整条 `next` 委托链 - 已完成 request pipeline executor 形状缓存:`CqrsDispatcher` 现会在单个 request binding 内按 `behaviorCount` 复用强类型 pipeline executor而不是每次 `SendAsync` 都重建整条 `next` 委托链
- 已补充 dispatcher pipeline executor 缓存与双行为顺序回归,锁定缓存复用后仍保持现有行为执行顺序 - 已补充 dispatcher pipeline executor 缓存与双行为顺序回归,锁定缓存复用后仍保持现有行为执行顺序
- 已补充 cached request pipeline executor 的上下文刷新回归,锁定 executor 复用时仍会为当次 handler / singleton behavior 重新注入当前 `ArchitectureContext`
- 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke` - 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke`
- 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物 - 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物
- 已修正 pointer / function pointer 泛型合同的错误覆盖:生成器不再为这两类类型发射 precise runtime type 重建代码 - 已修正 pointer / function pointer 泛型合同的错误覆盖:生成器不再为这两类类型发射 precise runtime type 重建代码
@ -90,6 +91,10 @@ CQRS 迁移与收敛。
- `CqrsDispatcher` 现会继续按 `requestType + responseType` 缓存 request dispatch binding并在 binding 内按 `behaviorCount` 缓存强类型 pipeline executor - `CqrsDispatcher` 现会继续按 `requestType + responseType` 缓存 request dispatch binding并在 binding 内按 `behaviorCount` 缓存强类型 pipeline executor
- 每次分发只绑定当前 handler / behaviors 实例,不缓存容器解析结果,因此不改变 transient 生命周期与上下文注入语义 - 每次分发只绑定当前 handler / behaviors 实例,不缓存容器解析结果,因此不改变 transient 生命周期与上下文注入语义
- `GFramework.Cqrs.Tests` 已补充 executor 首次创建 / 后续复用与双行为顺序回归 - `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 入口文档对齐: - `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 的当前语义 - `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 残留清理: - `2026-04-29` 已完成一轮 generator pointer runtime-reconstruction 残留清理:
@ -117,6 +122,9 @@ CQRS 迁移与收敛。
- `RP-043` 之前的详细阶段记录、定向验证命令和阶段性决策均已移入主题内归档 - `RP-043` 之前的详细阶段记录、定向验证命令和阶段性决策均已移入主题内归档
- active 跟踪文件只保留当前恢复点、当前活跃事实、风险和下一步,避免 `boot` 在默认入口中重复扫描 1000+ 行历史 trace - 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` - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false`
- 结果:通过 - 结果:通过
- 备注:`63/63` 测试通过;当前沙箱限制了 MSBuild named pipe验证需在提权环境下运行 - 备注:`63/63` 测试通过;当前沙箱限制了 MSBuild named pipe验证需在提权环境下运行

View File

@ -2,6 +2,27 @@
## 2026-04-29 ## 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<TBehavior>()` 对已闭合的 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 ### 阶段pointer runtime-reconstruction 残留清理CQRS-REWRITE-RP-056
- 延续 `gframework-batch-boot 50``Phase 8` 主线,本轮只处理一个写集很窄的 generator 清理切片:删除 `CqrsHandlerRegistryGenerator` 里已经不可达的 pointer runtime-reconstruction 残留 - 延续 `gframework-batch-boot 50``Phase 8` 主线,本轮只处理一个写集很窄的 generator 清理切片:删除 `CqrsHandlerRegistryGenerator` 里已经不可达的 pointer runtime-reconstruction 残留