test(cqrs): 补充非请求分发上下文回归

- 新增 notification 与 stream dispatch binding 上下文刷新回归,锁定缓存复用时仍按当次分发重新注入上下文

- 补充测试替身记录 handler 实例身份与 ArchitectureContext,覆盖重复分发场景

- 更新 cqrs-rewrite 跟踪与 trace,记录 RP-058 和 RP-059 的验证结论
This commit is contained in:
gewuyou 2026-04-29 17:54:59 +08:00 committed by GeWuYou
parent 226c0b3b49
commit 57d848546f
9 changed files with 293 additions and 2 deletions

View File

@ -35,7 +35,9 @@ internal sealed class CqrsDispatcherCacheTests
_container.Freeze(); _container.Freeze();
_context = new ArchitectureContext(_container); _context = new ArchitectureContext(_container);
DispatcherNotificationContextRefreshState.Reset();
DispatcherPipelineContextRefreshState.Reset(); DispatcherPipelineContextRefreshState.Reset();
DispatcherStreamContextRefreshState.Reset();
ClearDispatcherCaches(); ClearDispatcherCaches();
} }
@ -300,6 +302,92 @@ internal sealed class CqrsDispatcherCacheTests
}); });
} }
/// <summary>
/// 验证缓存的 notification dispatch binding 在重复分发时仍会重新解析 handler
/// 并为当次实例重新注入当前架构上下文。
/// </summary>
[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));
});
}
/// <summary>
/// 验证缓存的 stream dispatch binding 在重复建流时仍会重新解析 handler
/// 并为当次实例重新注入当前架构上下文。
/// </summary>
[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));
});
}
/// <summary> /// <summary>
/// 通过反射读取 dispatcher 的静态缓存对象。 /// 通过反射读取 dispatcher 的静态缓存对象。
/// </summary> /// </summary>

View File

@ -0,0 +1,29 @@
using System.Threading;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 记录缓存 notification binding 复用场景下每次分发注入到 handler 的上下文与实例身份。
/// </summary>
internal sealed class DispatcherNotificationContextRefreshHandler
: CqrsContextAwareHandlerBase,
INotificationHandler<DispatcherNotificationContextRefreshNotification>
{
private readonly int _instanceId = DispatcherNotificationContextRefreshState.AllocateHandlerInstanceId();
/// <summary>
/// 记录当前 handler 实例收到的上下文。
/// </summary>
/// <param name="notification">当前通知。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>已完成任务。</returns>
public ValueTask Handle(
DispatcherNotificationContextRefreshNotification notification,
CancellationToken cancellationToken)
{
DispatcherNotificationContextRefreshState.Record(notification.DispatchId, _instanceId, Context);
return ValueTask.CompletedTask;
}
}

View File

@ -0,0 +1,8 @@
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 为 notification dispatch binding 上下文刷新回归提供带分发标识的最小通知。
/// </summary>
internal sealed record DispatcherNotificationContextRefreshNotification(string DispatchId) : INotification;

View File

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

View File

@ -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;
/// <summary>
/// 记录缓存 stream binding 复用场景下每次分发注入到 handler 的上下文与实例身份。
/// </summary>
internal sealed class DispatcherStreamContextRefreshHandler
: CqrsContextAwareHandlerBase,
IStreamRequestHandler<DispatcherStreamContextRefreshRequest, int>
{
private readonly int _instanceId = DispatcherStreamContextRefreshState.AllocateHandlerInstanceId();
/// <summary>
/// 记录当前 handler 实例收到的上下文,并返回稳定元素。
/// </summary>
/// <param name="request">当前流请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>包含一个固定元素的异步流。</returns>
public async IAsyncEnumerable<int> Handle(
DispatcherStreamContextRefreshRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
DispatcherStreamContextRefreshState.Record(request.DispatchId, _instanceId, Context);
yield return 11;
await ValueTask.CompletedTask.ConfigureAwait(false);
}
}

View File

@ -0,0 +1,8 @@
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 为 stream dispatch binding 上下文刷新回归提供带分发标识的最小流请求。
/// </summary>
internal sealed record DispatcherStreamContextRefreshRequest(string DispatchId) : IStreamRequest<int>;

View File

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

View File

@ -7,7 +7,7 @@ CQRS 迁移与收敛。
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`CQRS-REWRITE-RP-057` - 恢复点编号:`CQRS-REWRITE-RP-059`
- 当前阶段:`Phase 8` - 当前阶段:`Phase 8`
- 当前焦点: - 当前焦点:
- 当前功能历史已归档active 跟踪仅保留 `Phase 8` 主线的恢复入口 - 当前功能历史已归档active 跟踪仅保留 `Phase 8` 主线的恢复入口
@ -18,6 +18,7 @@ CQRS 迁移与收敛。
- 已完成 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` - 已补充 cached request pipeline executor 的上下文刷新回归,锁定 executor 复用时仍会为当次 handler / singleton behavior 重新注入当前 `ArchitectureContext`
- 已补充 cached notification / stream dispatch binding 的上下文刷新回归,锁定 binding 复用时仍会为当次 handler 重新注入当前 `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 重建代码
@ -25,6 +26,7 @@ CQRS 迁移与收敛。
- 已为 registrar 的 reflection 注册路径补充 handler-interface 元数据缓存,减少跨容器重复注册时的 `GetInterfaces()` 反射 - 已为 registrar 的 reflection 注册路径补充 handler-interface 元数据缓存,减少跨容器重复注册时的 `GetInterfaces()` 反射
- 已将 registrar 的重复映射判定从线性扫描 `IServiceCollection` 收敛为本地映射索引,减少 fallback 注册路径的重复查找 - 已将 registrar 的重复映射判定从线性扫描 `IServiceCollection` 收敛为本地映射索引,减少 fallback 注册路径的重复查找
- 已完成一轮 `static lambda + state` 微收敛:`CqrsDispatcher``CqrsHandlerRegistrar` 现会在弱缓存 / 并发缓存入口优先使用无捕获工厂,继续压低热路径上的额外闭包分配 - 已完成一轮 `static lambda + state` 微收敛:`CqrsDispatcher``CqrsHandlerRegistrar` 现会在弱缓存 / 并发缓存入口优先使用无捕获工厂,继续压低热路径上的额外闭包分配
- 已补充 `CqrsReflectionFallbackAttribute` 叶子级合同测试,锁定空 marker、字符串 fallback 名称归一化、直接 `Type` fallback 归一化与空参数防御语义
- 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点 - 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点
## 当前状态摘要 ## 当前状态摘要
@ -95,6 +97,13 @@ CQRS 迁移与收敛。
- `GFramework.Cqrs.Tests` 已新增 `DispatcherPipelineContextRefresh*` 测试替身,分别记录 request handler 与 pipeline behavior 在每次分发中实际观察到的实例身份与 `ArchitectureContext` - `GFramework.Cqrs.Tests` 已新增 `DispatcherPipelineContextRefresh*` 测试替身,分别记录 request handler 与 pipeline behavior 在每次分发中实际观察到的实例身份与 `ArchitectureContext`
- `CqrsDispatcherCacheTests` 现明确断言:同一个 cached request pipeline executor 在重复分发时会继续命中同一 executor 形状,但不会跨分发保留旧上下文 - `CqrsDispatcherCacheTests` 现明确断言:同一个 cached request pipeline executor 在重复分发时会继续命中同一 executor 形状,但不会跨分发保留旧上下文
- 本轮定向测试未暴露新的 runtime 缺口,因此没有改动 `GFramework.Cqrs/Internal/CqrsDispatcher.cs` - 本轮定向测试未暴露新的 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` 已接受一轮 delegated 叶子级 fallback 合同测试:
- `GFramework.Cqrs.Tests/Cqrs/CqrsReflectionFallbackAttributeTests.cs` 已锁定空 marker、字符串 fallback 名称去空/去重/排序、直接 `Type` fallback 去空/去重/排序与空参数数组防御语义
- 当前 runtime 读取程序集级 fallback 元数据时所依赖的 attribute 归一化合同,现已有独立叶子级测试文件覆盖
- `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 残留清理:
@ -125,6 +134,12 @@ CQRS 迁移与收敛。
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"` - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"`
- 结果:通过 - 结果:通过
- 备注:`5/5` 测试通过;本轮新增 cached executor 上下文刷新回归,确认 executor 复用时仍按当次分发重新注入上下文 - 备注:`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 --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验证需在提权环境下运行
@ -194,6 +209,6 @@ CQRS 迁移与收敛。
## 下一步 ## 下一步
1. 继续 `Phase 8` 主线,优先再找一个收益明确且写集独立的 generator 或 registrar/dispatcher 热点;在下一次提交后重新计算相对 `origin/main` 的累计 diff并继续朝 `50 files` stop condition 推进 1. 继续 `Phase 8` 主线,优先再找一个收益明确且写集独立的 generator 或 registrar/dispatcher 热点;当前工作区若提交主线程 notification / stream 回归批次,相对 `origin/main` 的累计 diff 将达到 `29 files`,仍低于本轮 `gframework-batch-boot 50` 的主要 stop condition
2. 若继续文档主线,优先再扫教程入口页与 API 参考中的 CQRS 采用说明,确认是否还有旧 Command / Query 迁移口径残留 2. 若继续文档主线,优先再扫教程入口页与 API 参考中的 CQRS 采用说明,确认是否还有旧 Command / Query 迁移口径残留
3. 若后续再出现新的 PR review 或 review thread 变化,再重新执行 `$gframework-pr-review` 作为独立验证步骤 3. 若后续再出现新的 PR review 或 review thread 变化,再重新执行 `$gframework-pr-review` 作为独立验证步骤

View File

@ -2,6 +2,33 @@
## 2026-04-29 ## 2026-04-29
### 阶段notification / stream binding 上下文刷新回归CQRS-REWRITE-RP-059
- 延续 `gframework-batch-boot 50``Phase 8` 主线,本轮继续沿着上一批 dispatcher cached executor 上下文回归往外扩一圈,但只覆盖 notification / stream 两条非 request 路径
- 主线程先复核 `CqrsDispatcher` 当前实现后确认:
- `PublishAsync(...)``CreateStream(...)` 都会在命中缓存 binding 后重新解析 handler并在调用前执行 `PrepareHandler(...)`
- 因此本轮最稳妥的切片仍是测试补强,而不是继续改 runtime
- 已完成的测试补强:
- 在 `GFramework.Cqrs.Tests/Cqrs/` 新增 `DispatcherNotificationContextRefresh*``DispatcherStreamContextRefresh*` 测试替身,记录重复分发时 handler 实例身份与 `ArchitectureContext`
- `CqrsDispatcherCacheTests` 新增 `Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Notification_Dispatch_Binding`
- `CqrsDispatcherCacheTests` 新增 `Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Stream_Dispatch_Binding`
- 定向验证已通过:
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"`
- `7/7` passed
- 结果:
- 本轮未暴露新的 runtime 实现缺口,因此没有改动 `GFramework.Cqrs/Internal/CqrsDispatcher.cs`
- 若连同当前工作区一起计算,当前分支相对 `origin/main` 的累计 diff 将达到 `29 files`,继续低于 `gframework-batch-boot 50` 的主要 stop condition
### 阶段delegated fallback attribute 合同测试CQRS-REWRITE-RP-058
- 本轮按 `gframework-batch-boot 50` 的并行约束,把一个与主线程写集完全独立的叶子级测试文件交给 worker
- delegated scope`GFramework.Cqrs.Tests/Cqrs/CqrsReflectionFallbackAttributeTests.cs`
- delegated objective锁定 `CqrsReflectionFallbackAttribute` 的公开归一化合同,而不扩张到 registrar / generator / dispatcher 实现
- 已接受的 worker 结果:
- 新增 `CqrsReflectionFallbackAttributeTests`,覆盖空 marker、字符串 fallback 名称的去空/去重/排序、直接 `Type` fallback 的去空/去重/排序,以及两个重载对空参数数组的防御行为
- worker 已独立验证 `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsReflectionFallbackAttributeTests"`,结果为 `5/5` passed
- 该叶子级测试批次已作为独立提交落地:`86a24e00` `test(cqrs): 新增 ReflectionFallbackAttribute 合同测试`
### 阶段cached executor 上下文刷新回归CQRS-REWRITE-RP-057 ### 阶段cached executor 上下文刷新回归CQRS-REWRITE-RP-057
- 延续 `gframework-batch-boot 50``Phase 8` 主线,本轮只处理一个窄写集测试批次:为 cached request pipeline executor 增加“重复分发仍重新注入上下文”的回归 - 延续 `gframework-batch-boot 50``Phase 8` 主线,本轮只处理一个窄写集测试批次:为 cached request pipeline executor 增加“重复分发仍重新注入上下文”的回归