fix(cqrs): 收敛 PR 304 review 跟进

- 修复 CqrsDispatcher 的 pipeline invoker 重复创建,并补齐缓存线程模型文档

- 优化 CQRS 与 generator 回归测试的并发保护和稳定语义断言

- 更新 cqrs-rewrite 跟踪与 trace,记录 RP-062 的 PR review follow-up 验证结果
This commit is contained in:
gewuyou 2026-04-30 07:43:42 +08:00
parent bc365197e8
commit 255a6a152e
14 changed files with 228 additions and 62 deletions

View File

@ -10,6 +10,7 @@ namespace GFramework.Cqrs.Tests.Cqrs;
/// 验证 CQRS dispatcher 会缓存热路径中的 dispatch binding。
/// </summary>
[TestFixture]
[NonParallelizable]
internal sealed class CqrsDispatcherCacheTests
{
private MicrosoftDiContainer? _container;
@ -434,6 +435,11 @@ internal sealed class CqrsDispatcherCacheTests
/// <summary>
/// 读取 request dispatch binding 中指定行为数量的 pipeline executor 缓存项。
/// </summary>
/// <param name="requestBindings">dispatcher 内部的 request binding 缓存对象。</param>
/// <param name="requestType">要读取的请求运行时类型。</param>
/// <param name="responseType">要读取的响应运行时类型。</param>
/// <param name="behaviorCount">目标 executor 对应的行为数量。</param>
/// <returns>已缓存的 executor若 binding 或 executor 尚未建立则返回 <see langword="null" />。</returns>
private static object? GetRequestPipelineExecutorValue(
object requestBindings,
Type requestType,
@ -471,6 +477,10 @@ internal sealed class CqrsDispatcherCacheTests
/// <summary>
/// 读取指定请求/响应类型对对应的强类型 request dispatch binding。
/// </summary>
/// <param name="requestBindings">dispatcher 内部的 request binding 缓存对象。</param>
/// <param name="requestType">要读取的请求运行时类型。</param>
/// <param name="responseType">要读取的响应运行时类型。</param>
/// <returns>强类型 binding若缓存尚未建立则返回 <see langword="null" />。</returns>
private static object? GetRequestDispatchBindingValue(object requestBindings, Type requestType, Type responseType)
{
var bindingBox = GetPairCacheValue(requestBindings, requestType, responseType);

View File

@ -174,6 +174,7 @@ internal sealed class CqrsHandlerRegistrarFallbackFailureTests
/// <summary>
/// 清空本测试依赖的 registrar 静态缓存,确保每个用例都会重新执行 fallback 元数据解析。
/// 这些字段名直接耦合 <c>CqrsHandlerRegistrar</c> 当前内部实现;若后续重构缓存布局,需要同步更新这里。
/// </summary>
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.");
}
/// <summary>

View File

@ -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"));
});
}

View File

@ -8,12 +8,24 @@ namespace GFramework.Cqrs.Tests.Cqrs;
/// </summary>
internal static class DispatcherNotificationContextRefreshState
{
private static readonly Lock SyncRoot = new();
private static int _nextHandlerInstanceId;
private static readonly List<DispatcherPipelineContextSnapshot> _handlerSnapshots = [];
/// <summary>
/// 获取每次 notification 分发时记录的快照。
/// 获取每次 notification 分发时记录的快照副本。
/// 共享状态通过 <c>SyncRoot</c> 串行化,避免并行测试写入抖动。
/// </summary>
public static List<DispatcherPipelineContextSnapshot> HandlerSnapshots { get; } = [];
public static IReadOnlyList<DispatcherPipelineContextSnapshot> HandlerSnapshots
{
get
{
lock (SyncRoot)
{
return _handlerSnapshots.ToArray();
}
}
}
/// <summary>
/// 为新的 handler 测试实例分配稳定编号。
@ -28,7 +40,10 @@ internal static class DispatcherNotificationContextRefreshState
/// </summary>
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));
}
}
/// <summary>
@ -36,7 +51,10 @@ internal static class DispatcherNotificationContextRefreshState
/// </summary>
public static void Reset()
{
_nextHandlerInstanceId = 0;
HandlerSnapshots.Clear();
lock (SyncRoot)
{
_nextHandlerInstanceId = 0;
_handlerSnapshots.Clear();
}
}
}

View File

@ -8,18 +8,41 @@ namespace GFramework.Cqrs.Tests.Cqrs;
/// </summary>
internal static class DispatcherPipelineContextRefreshState
{
private static readonly Lock SyncRoot = new();
private static int _nextBehaviorInstanceId;
private static int _nextHandlerInstanceId;
private static readonly List<DispatcherPipelineContextSnapshot> _behaviorSnapshots = [];
private static readonly List<DispatcherPipelineContextSnapshot> _handlerSnapshots = [];
/// <summary>
/// 获取每次 behavior 执行时记录的快照。
/// 获取每次 behavior 执行时记录的快照副本。
/// 共享状态通过 <c>SyncRoot</c> 串行化,读取端始终拿到当前稳定快照。
/// </summary>
public static List<DispatcherPipelineContextSnapshot> BehaviorSnapshots { get; } = [];
public static IReadOnlyList<DispatcherPipelineContextSnapshot> BehaviorSnapshots
{
get
{
lock (SyncRoot)
{
return _behaviorSnapshots.ToArray();
}
}
}
/// <summary>
/// 获取每次 handler 执行时记录的快照。
/// 获取每次 handler 执行时记录的快照副本。
/// 共享状态通过 <c>SyncRoot</c> 串行化,读取端始终拿到当前稳定快照。
/// </summary>
public static List<DispatcherPipelineContextSnapshot> HandlerSnapshots { get; } = [];
public static IReadOnlyList<DispatcherPipelineContextSnapshot> HandlerSnapshots
{
get
{
lock (SyncRoot)
{
return _handlerSnapshots.ToArray();
}
}
}
/// <summary>
/// 为新的 behavior 测试实例分配稳定编号。
@ -42,7 +65,10 @@ internal static class DispatcherPipelineContextRefreshState
/// </summary>
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));
}
}
/// <summary>
@ -50,7 +76,10 @@ internal static class DispatcherPipelineContextRefreshState
/// </summary>
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));
}
}
/// <summary>
@ -58,9 +87,12 @@ internal static class DispatcherPipelineContextRefreshState
/// </summary>
public static void Reset()
{
_nextBehaviorInstanceId = 0;
_nextHandlerInstanceId = 0;
BehaviorSnapshots.Clear();
HandlerSnapshots.Clear();
lock (SyncRoot)
{
_nextBehaviorInstanceId = 0;
_nextHandlerInstanceId = 0;
_behaviorSnapshots.Clear();
_handlerSnapshots.Clear();
}
}
}

View File

@ -15,7 +15,7 @@ internal sealed class DispatcherPipelineOrderCacheRequestHandler : IRequestHandl
/// <returns>固定整数结果。</returns>
public ValueTask<int> Handle(DispatcherPipelineOrderCacheRequest request, CancellationToken cancellationToken)
{
DispatcherPipelineOrderState.Steps.Add("Handler");
DispatcherPipelineOrderState.Record("Handler");
return ValueTask.FromResult(3);
}
}

View File

@ -19,9 +19,9 @@ internal sealed class DispatcherPipelineOrderInnerBehavior : IPipelineBehavior<D
MessageHandlerDelegate<DispatcherPipelineOrderCacheRequest, int> 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;
}
}

View File

@ -19,9 +19,9 @@ internal sealed class DispatcherPipelineOrderOuterBehavior : IPipelineBehavior<D
MessageHandlerDelegate<DispatcherPipelineOrderCacheRequest, int> 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;
}
}

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
@ -5,16 +7,44 @@ namespace GFramework.Cqrs.Tests.Cqrs;
/// </summary>
internal static class DispatcherPipelineOrderState
{
private static readonly Lock SyncRoot = new();
private static readonly List<string> _steps = [];
/// <summary>
/// 获取按执行顺序追加的步骤名称。
/// 获取按执行顺序追加的步骤快照。
/// 共享状态通过 <c>SyncRoot</c> 串行化,避免并行行为测试互相污染步骤列表。
/// </summary>
public static List<string> Steps { get; } = [];
public static IReadOnlyList<string> Steps
{
get
{
lock (SyncRoot)
{
return _steps.ToArray();
}
}
}
/// <summary>
/// 记录一个新的 pipeline 执行步骤。
/// </summary>
/// <param name="step">要追加的步骤名称。</param>
public static void Record(string step)
{
lock (SyncRoot)
{
_steps.Add(step);
}
}
/// <summary>
/// 清空当前记录,供下一次断言使用。
/// </summary>
public static void Reset()
{
Steps.Clear();
lock (SyncRoot)
{
_steps.Clear();
}
}
}

View File

@ -8,12 +8,24 @@ namespace GFramework.Cqrs.Tests.Cqrs;
/// </summary>
internal static class DispatcherStreamContextRefreshState
{
private static readonly Lock SyncRoot = new();
private static int _nextHandlerInstanceId;
private static readonly List<DispatcherPipelineContextSnapshot> _handlerSnapshots = [];
/// <summary>
/// 获取每次建流时记录的快照。
/// 获取每次建流时记录的快照副本。
/// 共享状态通过 <c>SyncRoot</c> 串行化,避免并行测试写入抖动。
/// </summary>
public static List<DispatcherPipelineContextSnapshot> HandlerSnapshots { get; } = [];
public static IReadOnlyList<DispatcherPipelineContextSnapshot> HandlerSnapshots
{
get
{
lock (SyncRoot)
{
return _handlerSnapshots.ToArray();
}
}
}
/// <summary>
/// 为新的 handler 测试实例分配稳定编号。
@ -28,7 +40,10 @@ internal static class DispatcherStreamContextRefreshState
/// </summary>
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));
}
}
/// <summary>
@ -36,7 +51,10 @@ internal static class DispatcherStreamContextRefreshState
/// </summary>
public static void Reset()
{
_nextHandlerInstanceId = 0;
HandlerSnapshots.Clear();
lock (SyncRoot)
{
_nextHandlerInstanceId = 0;
_handlerSnapshots.Clear();
}
}
}

View File

@ -415,7 +415,11 @@ internal sealed class CqrsDispatcher(
RequestInvoker<TResponse> requestInvoker,
Type requestType)
{
// 线程安全:该缓存按 behaviorCount 复用 pipeline executor 形状GetPipelineExecutor 通过 ConcurrentDictionary
// 的 GetOrAdd 支持并发读写。缓存项只保存委托形状,不保留 handler/behavior 实例;若行为数量组合持续增长,
// 字典会随之增长且当前实现不提供回收。
private readonly ConcurrentDictionary<int, RequestPipelineExecutor<TResponse>> _pipelineExecutors = new();
private readonly RequestPipelineInvoker<TResponse> _pipelineInvoker = CreateRequestPipelineInvoker<TResponse>(requestType);
/// <summary>
/// 获取请求处理器在容器中的服务类型。
@ -441,8 +445,8 @@ internal sealed class CqrsDispatcher(
ArgumentOutOfRangeException.ThrowIfNegative(behaviorCount);
return _pipelineExecutors.GetOrAdd<RequestPipelineExecutorFactoryState<TResponse>>(
behaviorCount,
static (count, state) => CreateRequestPipelineExecutor<TResponse>(state.RequestType, count),
new RequestPipelineExecutorFactoryState<TResponse>(requestType));
static (count, state) => CreateRequestPipelineExecutor(count, state.PipelineInvoker),
new RequestPipelineExecutorFactoryState<TResponse>(_pipelineInvoker));
}
/// <summary>
@ -460,17 +464,23 @@ internal sealed class CqrsDispatcher(
/// 行为数量用于表达缓存形状,实际分发仍会消费本次容器解析出的 handler 与 behaviors 实例。
/// </summary>
private static RequestPipelineExecutor<TResponse> CreateRequestPipelineExecutor<TResponse>(
Type requestType,
int behaviorCount)
int behaviorCount,
RequestPipelineInvoker<TResponse> invoker)
{
ArgumentOutOfRangeException.ThrowIfNegative(behaviorCount);
return new RequestPipelineExecutor<TResponse>(behaviorCount, invoker);
}
/// <summary>
/// 为指定请求/响应类型创建可跨多个 behaviorCount 复用的 typed pipeline invoker。
/// </summary>
private static RequestPipelineInvoker<TResponse> CreateRequestPipelineInvoker<TResponse>(Type requestType)
{
var method = RequestPipelineInvokerMethodDefinition
.MakeGenericMethod(requestType, typeof(TResponse));
var invoker = (RequestPipelineInvoker<TResponse>)Delegate.CreateDelegate(
return (RequestPipelineInvoker<TResponse>)Delegate.CreateDelegate(
typeof(RequestPipelineInvoker<TResponse>),
method);
return new RequestPipelineExecutor<TResponse>(behaviorCount, invoker);
}
/// <summary>
@ -510,7 +520,8 @@ internal sealed class CqrsDispatcher(
/// 为 pipeline executor 缓存携带当前请求类型,避免按行为数量建缓存时创建闭包。
/// </summary>
/// <typeparam name="TResponse">请求响应类型。</typeparam>
private readonly record struct RequestPipelineExecutorFactoryState<TResponse>(Type RequestType);
private readonly record struct RequestPipelineExecutorFactoryState<TResponse>(
RequestPipelineInvoker<TResponse> PipelineInvoker);
/// <summary>
/// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。
@ -537,6 +548,8 @@ internal sealed class CqrsDispatcher(
/// <summary>
/// 获取指定阶段的 continuation并在首次请求时为该阶段绑定一次不可变调用入口。
/// 同一行为多次调用 <c>next</c> 时会命中相同 continuation保持与传统链式委托一致的语义。
/// 线程模型上,该缓存仅假定单次分发链按顺序推进;若某个 behavior 并发调用多个 <c>next</c>
/// 这里可能重复创建等价 continuation但不会跨分发共享也不会缓存容器解析出的实例。
/// </summary>
private MessageHandlerDelegate<TRequest, TResponse> GetContinuation(int index)
{

View File

@ -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("));
});
}

View File

@ -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-uprequest pipeline invoker 改为 binding 级复用、共享测试状态切换到 `System.Threading.Lock` 保护、顺序测试改为受控记录接口、`CqrsDispatcherCacheTests` 标记为 `NonParallelizable`,并补齐相关 XML / 线程模型注释
- `2026-04-29` 已完成一轮 precise runtime type lookup 的数组回归补强:
- `GFramework.SourceGenerators.Tests` 已新增多维数组、交错数组、外部程序集隐藏元素类型三类回归
- 当前生成器在 precise runtime type lookup 下已稳定保留数组秩信息,并递归发射交错数组的 `MakeArrayType()`

View File

@ -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<string>` 收敛为 `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