From 83528742bbace90035eb9b64be2f0c7296ec5936 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:25:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(cqrs):=20=E6=94=B6=E6=95=9B=E7=94=9F?= =?UTF-8?q?=E6=88=90=E8=B0=83=E7=94=A8=E6=8F=8F=E8=BF=B0=E7=AC=A6=E4=B8=8E?= =?UTF-8?q?PR=E8=AF=84=E5=AE=A1=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 request 与 stream generated invoker 描述符的静态方法与空值防御,提前拒绝非法元数据 - 补充 provider 空描述符枚举与非静态 invoker 回退回归,更新相关 XML 注释与中文文档语义 - 更新 cqrs-rewrite 活跃跟踪、执行 trace 与验证归档,记录 PR #307 的当前验证结论 --- ...qrsGeneratedRequestInvokerProviderTests.cs | 214 +++++++++++++++--- ...nGeneratedStreamInvokerProviderRegistry.cs | 9 + .../CqrsRequestInvokerDescriptor.cs | 22 +- .../CqrsRequestInvokerDescriptorEntry.cs | 43 +++- .../CqrsStreamInvokerDescriptor.cs | 22 +- .../CqrsStreamInvokerDescriptorEntry.cs | 43 +++- ...-validation-history-rp063-through-rp074.md | 111 +++++++++ .../todos/cqrs-rewrite-migration-tracking.md | 113 ++------- .../traces/cqrs-rewrite-migration-trace.md | 29 +++ docs/zh-CN/core/cqrs.md | 4 +- 10 files changed, 457 insertions(+), 153 deletions(-) create mode 100644 ai-plan/public/cqrs-rewrite/archive/todos/cqrs-rewrite-validation-history-rp063-through-rp074.md diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs index 3c81fbfb..74a4ac44 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs @@ -226,11 +226,11 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests } /// - /// 验证当 generated request invoker provider 返回实例方法时, - /// dispatcher 会显式拒绝该描述符,而不是在后续绑定阶段静默接受非法合同。 + /// 验证当 generated request invoker provider 暴露实例方法时, + /// registrar 会放弃该 generated registry 并回退到运行时反射路径。 /// [Test] - public void SendAsync_Should_Throw_When_Generated_Request_Invoker_Is_Not_Static() + public async Task SendAsync_Should_Fall_Back_To_Runtime_Path_When_Generated_Request_Invoker_Is_Not_Static() { var generatedAssembly = CreateGeneratedAssembly( typeof(NonStaticRequestInvokerProviderRegistry), @@ -241,10 +241,8 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests container.Freeze(); var context = new ArchitectureContext(container); - var exception = Assert.ThrowsAsync(async () => - await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false)); - Assert.That(exception, Is.Not.Null); - Assert.That(exception!.Message, Does.Contain("non-static invoker method")); + var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false); + Assert.That(response, Is.EqualTo("runtime:payload")); } /// @@ -270,11 +268,11 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests } /// - /// 验证当 generated stream invoker provider 返回实例方法时, - /// dispatcher 会在首次建流时显式拒绝该描述符。 + /// 验证当 generated stream invoker provider 暴露实例方法时, + /// registrar 会放弃该 generated registry 并回退到运行时反射路径。 /// [Test] - public void CreateStream_Should_Throw_When_Generated_Stream_Invoker_Is_Not_Static() + public async Task CreateStream_Should_Fall_Back_To_Runtime_Path_When_Generated_Stream_Invoker_Is_Not_Static() { var generatedAssembly = CreateGeneratedAssembly( typeof(NonStaticStreamInvokerProviderRegistry), @@ -285,10 +283,8 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests container.Freeze(); var context = new ArchitectureContext(container); - var exception = Assert.ThrowsAsync(async () => - await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false)); - Assert.That(exception, Is.Not.Null); - Assert.That(exception!.Message, Does.Contain("non-static invoker method")); + var results = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false); + Assert.That(results, Is.EqualTo([3, 4])); } /// @@ -313,6 +309,46 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Assert.That(exception!.Message, Does.Contain("incompatible invoker")); } + /// + /// 验证当 generated request invoker provider 实现枚举契约、但返回空描述符集合时, + /// dispatcher 仍会回退到既有反射路径。 + /// + [Test] + public async Task SendAsync_Should_Fall_Back_To_Runtime_Path_When_Request_Descriptor_Enumeration_Is_Empty() + { + var generatedAssembly = CreateGeneratedAssembly( + typeof(EmptyEnumeratingRequestInvokerProviderRegistry), + "GFramework.Cqrs.Tests.Cqrs.EmptyEnumeratingRequestInvokerAssembly, Version=1.0.0.0"); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + container.Freeze(); + + var context = new ArchitectureContext(container); + var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false); + Assert.That(response, Is.EqualTo("runtime:payload")); + } + + /// + /// 验证当 generated stream invoker provider 实现枚举契约、但返回空描述符集合时, + /// dispatcher 仍会回退到既有流式反射路径。 + /// + [Test] + public async Task CreateStream_Should_Fall_Back_To_Runtime_Path_When_Stream_Descriptor_Enumeration_Is_Empty() + { + var generatedAssembly = CreateGeneratedAssembly( + typeof(EmptyEnumeratingStreamInvokerProviderRegistry), + "GFramework.Cqrs.Tests.Cqrs.EmptyEnumeratingStreamInvokerAssembly, Version=1.0.0.0"); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + container.Freeze(); + + var context = new ArchitectureContext(container); + var results = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false); + Assert.That(results, Is.EqualTo([3, 4])); + } + /// /// 模拟返回实例 request invoker 方法的 generated registry。 /// @@ -321,15 +357,6 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests ICqrsRequestInvokerProvider, IEnumeratesCqrsRequestInvokerDescriptors { - private static readonly CqrsRequestInvokerDescriptorEntry DescriptorEntry = new( - typeof(GeneratedRequestInvokerRequest), - typeof(string), - new CqrsRequestInvokerDescriptor( - typeof(IRequestHandler), - typeof(NonStaticRequestInvokerProviderRegistry).GetMethod( - nameof(InvokeGenerated), - BindingFlags.NonPublic | BindingFlags.Instance)!)); - /// public void Register(IServiceCollection services, ILogger logger) { @@ -347,9 +374,16 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Type responseType, out CqrsRequestInvokerDescriptor? descriptor) { + ArgumentNullException.ThrowIfNull(requestType); + ArgumentNullException.ThrowIfNull(responseType); + if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string)) { - descriptor = DescriptorEntry.Descriptor; + descriptor = new CqrsRequestInvokerDescriptor( + typeof(IRequestHandler), + typeof(NonStaticRequestInvokerProviderRegistry).GetMethod( + nameof(InvokeGenerated), + BindingFlags.NonPublic | BindingFlags.Instance)!); return true; } @@ -360,7 +394,17 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests /// public IReadOnlyList GetDescriptors() { - return [DescriptorEntry]; + return + [ + new CqrsRequestInvokerDescriptorEntry( + typeof(GeneratedRequestInvokerRequest), + typeof(string), + new CqrsRequestInvokerDescriptor( + typeof(IRequestHandler), + typeof(NonStaticRequestInvokerProviderRegistry).GetMethod( + nameof(InvokeGenerated), + BindingFlags.NonPublic | BindingFlags.Instance)!)) + ]; } private ValueTask InvokeGenerated(object handler, object request, CancellationToken cancellationToken) @@ -403,6 +447,9 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Type responseType, out CqrsRequestInvokerDescriptor? descriptor) { + ArgumentNullException.ThrowIfNull(requestType); + ArgumentNullException.ThrowIfNull(responseType); + if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string)) { descriptor = DescriptorEntry.Descriptor; @@ -433,15 +480,6 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests ICqrsStreamInvokerProvider, IEnumeratesCqrsStreamInvokerDescriptors { - private static readonly CqrsStreamInvokerDescriptorEntry DescriptorEntry = new( - typeof(GeneratedStreamInvokerRequest), - typeof(int), - new CqrsStreamInvokerDescriptor( - typeof(IStreamRequestHandler), - typeof(NonStaticStreamInvokerProviderRegistry).GetMethod( - nameof(InvokeGenerated), - BindingFlags.NonPublic | BindingFlags.Instance)!)); - /// public void Register(IServiceCollection services, ILogger logger) { @@ -459,9 +497,16 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Type responseType, out CqrsStreamInvokerDescriptor? descriptor) { + ArgumentNullException.ThrowIfNull(requestType); + ArgumentNullException.ThrowIfNull(responseType); + if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int)) { - descriptor = DescriptorEntry.Descriptor; + descriptor = new CqrsStreamInvokerDescriptor( + typeof(IStreamRequestHandler), + typeof(NonStaticStreamInvokerProviderRegistry).GetMethod( + nameof(InvokeGenerated), + BindingFlags.NonPublic | BindingFlags.Instance)!); return true; } @@ -472,7 +517,17 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests /// public IReadOnlyList GetDescriptors() { - return [DescriptorEntry]; + return + [ + new CqrsStreamInvokerDescriptorEntry( + typeof(GeneratedStreamInvokerRequest), + typeof(int), + new CqrsStreamInvokerDescriptor( + typeof(IStreamRequestHandler), + typeof(NonStaticStreamInvokerProviderRegistry).GetMethod( + nameof(InvokeGenerated), + BindingFlags.NonPublic | BindingFlags.Instance)!)) + ]; } private object InvokeGenerated(object handler, object request, CancellationToken cancellationToken) @@ -515,6 +570,9 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Type responseType, out CqrsStreamInvokerDescriptor? descriptor) { + ArgumentNullException.ThrowIfNull(requestType); + ArgumentNullException.ThrowIfNull(responseType); + if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int)) { descriptor = DescriptorEntry.Descriptor; @@ -567,6 +625,9 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Type responseType, out CqrsRequestInvokerDescriptor? descriptor) { + ArgumentNullException.ThrowIfNull(requestType); + ArgumentNullException.ThrowIfNull(responseType); + if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string)) { descriptor = Descriptor; @@ -608,6 +669,9 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Type responseType, out CqrsStreamInvokerDescriptor? descriptor) { + ArgumentNullException.ThrowIfNull(requestType); + ArgumentNullException.ThrowIfNull(responseType); + if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int)) { descriptor = Descriptor; @@ -619,6 +683,84 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests } } + /// + /// 模拟实现 request descriptor 枚举契约、但当前不暴露任何 descriptor 的 generated registry。 + /// + private sealed class EmptyEnumeratingRequestInvokerProviderRegistry : + ICqrsHandlerRegistry, + ICqrsRequestInvokerProvider, + IEnumeratesCqrsRequestInvokerDescriptors + { + /// + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(IRequestHandler), + typeof(GeneratedRequestInvokerRequestHandler)); + } + + /// + public bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsRequestInvokerDescriptor? descriptor) + { + ArgumentNullException.ThrowIfNull(requestType); + ArgumentNullException.ThrowIfNull(responseType); + + descriptor = null; + return false; + } + + /// + public IReadOnlyList GetDescriptors() + { + return Array.Empty(); + } + } + + /// + /// 模拟实现 stream descriptor 枚举契约、但当前不暴露任何 descriptor 的 generated registry。 + /// + private sealed class EmptyEnumeratingStreamInvokerProviderRegistry : + ICqrsHandlerRegistry, + ICqrsStreamInvokerProvider, + IEnumeratesCqrsStreamInvokerDescriptors + { + /// + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(IStreamRequestHandler), + typeof(GeneratedStreamInvokerRequestHandler)); + } + + /// + public bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsStreamInvokerDescriptor? descriptor) + { + ArgumentNullException.ThrowIfNull(requestType); + ArgumentNullException.ThrowIfNull(responseType); + + descriptor = null; + return false; + } + + /// + public IReadOnlyList GetDescriptors() + { + return Array.Empty(); + } + } + /// /// 创建带有 generated request invoker registry 元数据的程序集替身。 /// diff --git a/GFramework.Cqrs.Tests/Cqrs/HiddenImplementationGeneratedStreamInvokerProviderRegistry.cs b/GFramework.Cqrs.Tests/Cqrs/HiddenImplementationGeneratedStreamInvokerProviderRegistry.cs index 8d14cca1..94f86b8c 100644 --- a/GFramework.Cqrs.Tests/Cqrs/HiddenImplementationGeneratedStreamInvokerProviderRegistry.cs +++ b/GFramework.Cqrs.Tests/Cqrs/HiddenImplementationGeneratedStreamInvokerProviderRegistry.cs @@ -32,6 +32,9 @@ internal sealed class HiddenImplementationGeneratedStreamInvokerProviderRegistry /// /// 承载处理器映射的服务集合。 /// 用于记录注册诊断的日志器。 + /// + /// 当 时抛出。 + /// public void Register(IServiceCollection services, ILogger logger) { ArgumentNullException.ThrowIfNull(services); @@ -50,11 +53,17 @@ internal sealed class HiddenImplementationGeneratedStreamInvokerProviderRegistry /// 流式响应元素类型。 /// 命中时返回的描述符。 /// 若类型对匹配当前测试流式请求则返回 + /// + /// 当 时抛出。 + /// public bool TryGetDescriptor( Type requestType, Type responseType, out CqrsStreamInvokerDescriptor? descriptor) { + ArgumentNullException.ThrowIfNull(requestType); + ArgumentNullException.ThrowIfNull(responseType); + if (requestType == typeof(HiddenImplementationStreamInvokerContainer.VisibleStreamRequest) && responseType == typeof(int)) { diff --git a/GFramework.Cqrs/CqrsRequestInvokerDescriptor.cs b/GFramework.Cqrs/CqrsRequestInvokerDescriptor.cs index cc7cce14..d05c6b2e 100644 --- a/GFramework.Cqrs/CqrsRequestInvokerDescriptor.cs +++ b/GFramework.Cqrs/CqrsRequestInvokerDescriptor.cs @@ -19,6 +19,9 @@ public sealed class CqrsRequestInvokerDescriptor( Type handlerType, MethodInfo invokerMethod) { + private static readonly string NonStaticInvokerMessage = + "CQRS request invoker descriptors require an open static invoker method so generated metadata can be bound deterministically."; + /// /// 获取请求处理器在容器中的服务类型。 /// @@ -27,5 +30,22 @@ public sealed class CqrsRequestInvokerDescriptor( /// /// 获取执行请求处理器的开放静态方法。 /// - public MethodInfo InvokerMethod { get; } = invokerMethod ?? throw new ArgumentNullException(nameof(invokerMethod)); + public MethodInfo InvokerMethod { get; } = ValidateInvokerMethod(invokerMethod); + + /// + /// 在描述符构造阶段拒绝实例方法,避免非法 generated metadata 延迟到首次分发时才暴露。 + /// + /// 待验证的 generated invoker 方法。 + /// 通过校验的静态方法。 + /// 时抛出。 + /// 不是静态方法时抛出。 + private static MethodInfo ValidateInvokerMethod(MethodInfo invokerMethod) + { + ArgumentNullException.ThrowIfNull(invokerMethod); + + if (!invokerMethod.IsStatic) + throw new ArgumentException(NonStaticInvokerMessage, nameof(invokerMethod)); + + return invokerMethod; + } } diff --git a/GFramework.Cqrs/CqrsRequestInvokerDescriptorEntry.cs b/GFramework.Cqrs/CqrsRequestInvokerDescriptorEntry.cs index 679652b6..81e36731 100644 --- a/GFramework.Cqrs/CqrsRequestInvokerDescriptorEntry.cs +++ b/GFramework.Cqrs/CqrsRequestInvokerDescriptorEntry.cs @@ -3,10 +3,39 @@ namespace GFramework.Cqrs; /// /// 描述单个 request/response 类型对与其 generated invoker 元数据之间的映射条目。 /// -/// 请求运行时类型。 -/// 响应运行时类型。 -/// 对应的 generated request invoker 描述符。 -public sealed record CqrsRequestInvokerDescriptorEntry( - Type RequestType, - Type ResponseType, - CqrsRequestInvokerDescriptor Descriptor); +public sealed record CqrsRequestInvokerDescriptorEntry +{ + /// + /// 初始化 request invoker 描述符映射条目。 + /// + /// 请求运行时类型。 + /// 响应运行时类型。 + /// 对应的 generated request invoker 描述符。 + /// + /// 当 时抛出。 + /// + public CqrsRequestInvokerDescriptorEntry( + Type requestType, + Type responseType, + CqrsRequestInvokerDescriptor descriptor) + { + RequestType = requestType ?? throw new ArgumentNullException(nameof(requestType)); + ResponseType = responseType ?? throw new ArgumentNullException(nameof(responseType)); + Descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor)); + } + + /// + /// 获取请求运行时类型。 + /// + public Type RequestType { get; } + + /// + /// 获取响应运行时类型。 + /// + public Type ResponseType { get; } + + /// + /// 获取对应的 generated request invoker 描述符。 + /// + public CqrsRequestInvokerDescriptor Descriptor { get; } +} diff --git a/GFramework.Cqrs/CqrsStreamInvokerDescriptor.cs b/GFramework.Cqrs/CqrsStreamInvokerDescriptor.cs index 4f9f67e1..fd8d5a3c 100644 --- a/GFramework.Cqrs/CqrsStreamInvokerDescriptor.cs +++ b/GFramework.Cqrs/CqrsStreamInvokerDescriptor.cs @@ -19,6 +19,9 @@ public sealed class CqrsStreamInvokerDescriptor( Type handlerType, MethodInfo invokerMethod) { + private static readonly string NonStaticInvokerMessage = + "CQRS stream invoker descriptors require an open static invoker method so generated metadata can be bound deterministically."; + /// /// 获取流式请求处理器在容器中的服务类型。 /// @@ -27,5 +30,22 @@ public sealed class CqrsStreamInvokerDescriptor( /// /// 获取执行流式请求处理器的开放静态方法。 /// - public MethodInfo InvokerMethod { get; } = invokerMethod ?? throw new ArgumentNullException(nameof(invokerMethod)); + public MethodInfo InvokerMethod { get; } = ValidateInvokerMethod(invokerMethod); + + /// + /// 在描述符构造阶段拒绝实例方法,避免非法 generated metadata 延迟到首次建流时才暴露。 + /// + /// 待验证的 generated invoker 方法。 + /// 通过校验的静态方法。 + /// 时抛出。 + /// 不是静态方法时抛出。 + private static MethodInfo ValidateInvokerMethod(MethodInfo invokerMethod) + { + ArgumentNullException.ThrowIfNull(invokerMethod); + + if (!invokerMethod.IsStatic) + throw new ArgumentException(NonStaticInvokerMessage, nameof(invokerMethod)); + + return invokerMethod; + } } diff --git a/GFramework.Cqrs/CqrsStreamInvokerDescriptorEntry.cs b/GFramework.Cqrs/CqrsStreamInvokerDescriptorEntry.cs index ffff9fb8..b760fb7f 100644 --- a/GFramework.Cqrs/CqrsStreamInvokerDescriptorEntry.cs +++ b/GFramework.Cqrs/CqrsStreamInvokerDescriptorEntry.cs @@ -3,10 +3,39 @@ namespace GFramework.Cqrs; /// /// 描述单个 stream request/response 类型对与其 generated invoker 元数据之间的映射条目。 /// -/// 流式请求运行时类型。 -/// 流式响应元素类型。 -/// 对应的 generated stream invoker 描述符。 -public sealed record CqrsStreamInvokerDescriptorEntry( - Type RequestType, - Type ResponseType, - CqrsStreamInvokerDescriptor Descriptor); +public sealed record CqrsStreamInvokerDescriptorEntry +{ + /// + /// 初始化 stream invoker 描述符映射条目。 + /// + /// 流式请求运行时类型。 + /// 流式响应元素类型。 + /// 对应的 generated stream invoker 描述符。 + /// + /// 当 时抛出。 + /// + public CqrsStreamInvokerDescriptorEntry( + Type requestType, + Type responseType, + CqrsStreamInvokerDescriptor descriptor) + { + RequestType = requestType ?? throw new ArgumentNullException(nameof(requestType)); + ResponseType = responseType ?? throw new ArgumentNullException(nameof(responseType)); + Descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor)); + } + + /// + /// 获取流式请求运行时类型。 + /// + public Type RequestType { get; } + + /// + /// 获取流式响应元素类型。 + /// + public Type ResponseType { get; } + + /// + /// 获取对应的 generated stream invoker 描述符。 + /// + public CqrsStreamInvokerDescriptor Descriptor { get; } +} diff --git a/ai-plan/public/cqrs-rewrite/archive/todos/cqrs-rewrite-validation-history-rp063-through-rp074.md b/ai-plan/public/cqrs-rewrite/archive/todos/cqrs-rewrite-validation-history-rp063-through-rp074.md new file mode 100644 index 00000000..6a890501 --- /dev/null +++ b/ai-plan/public/cqrs-rewrite/archive/todos/cqrs-rewrite-validation-history-rp063-through-rp074.md @@ -0,0 +1,111 @@ +# CQRS 重写迁移验证归档(RP-063 至 RP-074) + +## 说明 + +- 本文件承接 `cqrs-rewrite-validation-history-through-rp062.md` 之后的详细验证历史。 +- active tracking 只保留当前权威验证批次、最近 PR 锚点与下一恢复点;更早的命令级明细统一归档到这里。 + +## 验证记录 + +- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json` + - 结果:通过 + - 备注:确认当时当前分支对应 `PR #305`,并定位到仍需本地复核的 CodeRabbit / Greptile open thread +- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;本轮确认 XML 文档补齐、`NonParallelizable`、`_syncRoot` 命名与 `ai-plan` 收敛未引入新增编译问题 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsArchitectureContextIntegrationTests.Handler_Can_Access_Architecture_Context|FullyQualifiedName~CqrsArchitectureContextAdvancedFeaturesTests.Request_With_Retry_Behavior_Should_Succeed_On_First_Attempt|FullyQualifiedName~CqrsArchitectureContextAdvancedFeaturesTests.Transient_Error_Request_Should_Succeed_Without_Simulated_Errors"` + - 结果:通过 + - 备注:`5/5` passed;覆盖 generated invoker provider、真实上下文注入与两条重命名高级行为测试 +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~SendRequestAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently|FullyQualifiedName~PublishAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently|FullyQualifiedName~CreateStream_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently"` + - 结果:通过 + - 备注:`3/3` passed;确认并发首次解析测试在失败路径释放调整后保持通过 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available|FullyQualifiedName~Emits_Direct_Type_Fallback_Metadata_When_All_Fallback_Handlers_Are_Referenceable_And_Runtime_Type_Contract_Is_Available|FullyQualifiedName~Emits_Mixed_Direct_Type_And_String_Fallback_Metadata_When_Runtime_Allows_Multiple_Fallback_Attributes"` + - 结果:通过 + - 备注:`3/3` passed;确认 provider 生成分支注释与断言顺序修正未改变生成语义 +- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过 + - 备注:构建成功;并行验证期间出现过 `MSB3026` 拷贝重试噪音,属于同时运行多个 `dotnet` 命令时的输出文件竞争,不是持久性编译 warning +- `bash scripts/validate-csharp-naming.sh` + - 结果:通过 + - 备注:使用显式 `GIT_DIR` / `GIT_WORK_TREE` 绑定重跑后,`1045` 个 tracked C# 文件的命名校验全部通过;本轮 `_syncRoot` 改名未引入命名规则回归 +- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;本轮确认 notification publisher seam、README 与文档更新未引入 `GFramework.Cqrs` 构建告警 +- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;确认 stream invoker provider 生成与显式枚举接口实现未引入生成器编译问题 +- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;确认 stream invoker provider fixture 与回归断言可以编译通过 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"` + - 结果:通过 + - 备注:`4/4` passed;覆盖 generated request / stream invoker provider 的 registrar 接线与 dispatcher 消费语义 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` + - 结果:通过 + - 备注:`2/2` passed;确认 generated registry 会同时发射 request / stream invoker provider 描述符与静态 invoker 方法 +- `GIT_DIR=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/.git/worktrees/GFramework-cqrs GIT_WORK_TREE=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework-WorkTree/GFramework-cqrs bash scripts/validate-csharp-naming.sh` + - 结果:通过 + - 备注:`1059` 个 tracked C# 文件命名校验全部通过;本轮新增 stream invoker 类型与测试命名未引入回归 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_For_Hidden_Implementation_With_Visible_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_For_Hidden_Implementation_With_Visible_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` + - 结果:通过 + - 备注:`4/4` passed;确认 hidden implementation + visible interface 场景也会继续发射 request / stream invoker provider 元数据 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"` + - 结果:通过 + - 备注:`8/8` passed;补齐 hidden implementation + visible interface 场景后,确认 generated request / stream invoker 在 runtime 侧也会优先命中 provider descriptor +- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;确认本轮 precise reflected invoker provider 合同回归未引入 generator 编译告警 +- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;并行验证时曾出现过 `MSB3026` 输出文件竞争噪音,随后已串行重跑并得到干净构建结果 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_For_Precise_Reflected_Request_Registrations|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_For_Precise_Reflected_Stream_Registrations|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_For_Hidden_Implementation_With_Visible_Handler_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_For_Hidden_Implementation_With_Visible_Handler_Interface"` + - 结果:通过 + - 备注:`4/4` passed;串行确认 visible-interface hidden-implementation 仍发射 provider 元数据,而 precise reflected 注册继续保持“不发射 provider descriptor”的当前合同 +- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` + - 结果:通过 + - 备注:并行执行 build/test 时出现 `MSB3026` 输出文件竞争噪音;无真实编译错误,后续以串行 test 结果作为本轮 authoritative 行为验证 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Provider_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Descriptor_Enumerator|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Provider_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Enumerator|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` + - 结果:通过 + - 备注:`6/6` passed;锁定 request / stream provider gate 依赖“provider 接口 + descriptor 枚举接口”同时存在,且原有 happy-path 发射仍保持通过 +- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` + - 结果:通过 + - 备注:并行执行 build/test 时出现 `MSB3026` 输出文件竞争噪音;当前已确认没有新增 analyzer warning,`GFramework.Cqrs.Tests` 仍能完成 Release 构建 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.SendAsync_Should_Throw_When_Generated_Request_Invoker_Is_Not_Static|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.SendAsync_Should_Throw_When_Generated_Request_Invoker_Is_Incompatible|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.CreateStream_Should_Throw_When_Generated_Stream_Invoker_Is_Not_Static|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.CreateStream_Should_Throw_When_Generated_Stream_Invoker_Is_Incompatible|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.SendAsync_Should_Use_Generated_Request_Invoker_When_Provider_Is_Registered|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.CreateStream_Should_Use_Generated_Stream_Invoker_When_Provider_Is_Registered"` + - 结果:通过 + - 备注:`6/6` passed;确认 request / stream 的非法 generated invoker 现统一抛出 `InvalidOperationException`,且原有 happy-path 未回归 +- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;确认新增 non-enumerating provider 回归未引入构建告警 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"` + - 结果:通过 + - 备注:`14/14` passed;确认 request / stream 的 generated happy-path、异常路径与 non-enumerating provider 反射回退语义均保持通过 +- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;确认 `CqrsRuntimeModule` 接线变更未引入 `GFramework.Core` 模块构建问题 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsNotificationPublisherTests"` + - 结果:通过 + - 备注:`5/5` 通过;覆盖自定义 publisher 顺序、上下文注入、零处理器、首错即停与默认接线复用 +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"` + - 结果:通过 + - 备注:`41/41` 通过;确认 CQRS 基础设施默认接线与容器行为未回归 +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"` + - 结果:通过 + - 备注:`42/42` 通过;本轮新增 legacy alias 回填回归后,确认正式 seam 与旧命名空间 alias 仍指向同一实例 +- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;确认 legacy alias helper 收敛与文档更新未引入 `GFramework.Core` 模块构建告警 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsHandlerRegistrarTests|FullyQualifiedName~CqrsDispatcherCacheTests"` + - 结果:通过 + - 备注:`22/22` 通过;确认 generated request invoker provider 的 registrar 接线、dispatcher 消费与现有 request/notification/stream cache 语义未回归 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` + - 结果:通过 + - 备注:`1/1` 通过;锁定 generator 会在 runtime 合同可用时发射 request invoker provider 成员 +- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;确认 request invoker provider seam 与 dispatcher/registrar 接线未引入新增构建告警 +- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;确认三份 `Mediator` 命名收口后的 CQRS 测试项目构建仍然干净 +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests"` + - 结果:通过 + - 备注:`22/22` 通过;新增 `PublishAsync` / `CreateStream` 并发首次访问只解析一次 `ICqrsRuntime` 的回归 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 1908139c..ee254359 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-074` +- 恢复点编号:`CQRS-REWRITE-RP-075` - 当前阶段:`Phase 8` - 当前焦点: - 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md` @@ -81,6 +81,12 @@ CQRS 迁移与收敛。 - 已完成一轮 non-enumerating provider reflection fallback 回归: - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 现新增 request / stream 两条回归,锁定当 registry 只暴露 provider 接口、但不实现 `IEnumeratesCqrs*InvokerDescriptors` 时,registrar 不会预热 dispatcher 缓存,后续 dispatch 会继续回退到既有反射路径 - 当前回归明确区分“provider 已注册”和“descriptor 已枚举入缓存”这两个阶段,避免后续把 `TryGetDescriptor(...)` 的存在误当成 dispatcher 会主动查询 provider 的合同 + - 已完成 `PR #307` review follow-up 的当前批次: + - `CqrsRequestInvokerDescriptor` / `CqrsStreamInvokerDescriptor` 现会在构造阶段拒绝实例方法,避免非法 generated metadata 延迟到首次分发时才暴露 + - `CqrsRequestInvokerDescriptorEntry` / `CqrsStreamInvokerDescriptorEntry` 现补齐公开入口空值防御,保持 request / stream 描述符合同一致 + - `CqrsGeneratedRequestInvokerProviderTests` 现补齐空 descriptor 枚举回退回归,并把“非静态 invoker”语义收敛为 registrar 放弃 generated registry 后回退到反射路径 + - `docs/zh-CN/core/cqrs.md` 已改正文档里“元数据异常会回退到反射”的错误表述,并将源码阅读表中的 `Internal/` 路径文案改为语义标签 + - active tracking 已把 `RP-063` 至 `RP-074` 的命令级验证明细迁移到 `archive/todos/cqrs-rewrite-validation-history-rp063-through-rp074.md`,当前入口只保留最近权威验证与恢复点 - 当前相对 `origin/main` 的累计 branch diff 为 `24 files / 1754 changed lines`,仍低于本轮 `$gframework-batch-boot 50` 的主要 stop condition,可继续推进下一批低风险切片 - 已将 mixed fallback 场景进一步收敛:当 runtime 允许同一程序集声明多个 `CqrsReflectionFallbackAttribute` 实例时,generator 现会把可直接引用的 fallback handlers 与仅能按名称恢复的 fallback handlers 拆分发射 - `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出 @@ -236,6 +242,7 @@ CQRS 迁移与收敛。 - 历史跟踪归档:[cqrs-rewrite-history-through-rp043.md](../archive/todos/cqrs-rewrite-history-through-rp043.md) - 验证历史归档:[cqrs-rewrite-validation-history-through-rp062.md](../archive/todos/cqrs-rewrite-validation-history-through-rp062.md) +- `RP-063` 至 `RP-074` 验证归档:[cqrs-rewrite-validation-history-rp063-through-rp074.md](../archive/todos/cqrs-rewrite-validation-history-rp063-through-rp074.md) - CQRS 与 Mediator 评估归档:[cqrs-vs-mediator-assessment-rp063.md](../archive/todos/cqrs-vs-mediator-assessment-rp063.md) - 历史 trace 归档:[cqrs-rewrite-history-through-rp043.md](../archive/traces/cqrs-rewrite-history-through-rp043.md) - `RP-046` 至 `RP-061` trace 归档:[cqrs-rewrite-history-rp046-through-rp061.md](../archive/traces/cqrs-rewrite-history-rp046-through-rp061.md) @@ -244,111 +251,19 @@ CQRS 迁移与收敛。 - `RP-043` 之前的详细阶段记录、定向验证命令和阶段性决策均已移入主题内归档 - `RP-046` 至 `RP-062` 的历史验证命令与阶段性结果已移入验证归档,active tracking 只保留当前恢复入口需要的最新验证 -- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json` +- `RP-063` 至 `RP-074` 的详细验证命令与阶段性结果已移入验证归档,active tracking 只保留当前 PR 复核批次的权威结果 +- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json` - 结果:通过 - - 备注:确认当前分支对应 `PR #305`,并定位到仍需本地复核的 CodeRabbit / Greptile open thread -- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error`;本轮确认 XML 文档补齐、`NonParallelizable`、`_syncRoot` 命名与 `ai-plan` 收敛未引入新增编译问题 -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsArchitectureContextIntegrationTests.Handler_Can_Access_Architecture_Context|FullyQualifiedName~CqrsArchitectureContextAdvancedFeaturesTests.Request_With_Retry_Behavior_Should_Succeed_On_First_Attempt|FullyQualifiedName~CqrsArchitectureContextAdvancedFeaturesTests.Transient_Error_Request_Should_Succeed_Without_Simulated_Errors"` - - 结果:通过 - - 备注:`5/5` passed;覆盖 generated invoker provider、真实上下文注入与两条重命名高级行为测试 -- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~SendRequestAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently|FullyQualifiedName~PublishAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently|FullyQualifiedName~CreateStream_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently"` - - 结果:通过 - - 备注:`3/3` passed;确认并发首次解析测试在失败路径释放调整后保持通过 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available|FullyQualifiedName~Emits_Direct_Type_Fallback_Metadata_When_All_Fallback_Handlers_Are_Referenceable_And_Runtime_Type_Contract_Is_Available|FullyQualifiedName~Emits_Mixed_Direct_Type_And_String_Fallback_Metadata_When_Runtime_Allows_Multiple_Fallback_Attributes"` - - 结果:通过 - - 备注:`3/3` passed;确认 provider 生成分支注释与断言顺序修正未改变生成语义 + - 备注:确认当前分支对应 `PR #307`,状态为 `OPEN`;当前仍有 `9` 条 CodeRabbit open thread,本轮只接受其中经本地复核后仍成立的合同防御、文档和 tracking 建议 - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - 结果:通过 - - 备注:构建成功;并行验证期间出现过 `MSB3026` 拷贝重试噪音,属于同时运行多个 `dotnet` 命令时的输出文件竞争,不是持久性编译 warning -- `bash scripts/validate-csharp-naming.sh` - - 结果:通过 - - 备注:使用显式 `GIT_DIR` / `GIT_WORK_TREE` 绑定重跑后,`1045` 个 tracked C# 文件的命名校验全部通过;本轮 `_syncRoot` 改名未引入命名规则回归 -- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error`;本轮确认 notification publisher seam、README 与文档更新未引入 `GFramework.Cqrs` 构建告警 -- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error`;确认 stream invoker provider 生成与显式枚举接口实现未引入生成器编译问题 -- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error`;确认 stream invoker provider fixture 与回归断言可以编译通过 + - 备注:`0 warning / 0 error`;确认描述符前置防御、XML 文档与文档修正未引入 `GFramework.Cqrs` 模块告警 - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"` - 结果:通过 - - 备注:`4/4` passed;覆盖 generated request / stream invoker provider 的 registrar 接线与 dispatcher 消费语义 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` - - 结果:通过 - - 备注:`2/2` passed;确认 generated registry 会同时发射 request / stream invoker provider 描述符与静态 invoker 方法 -- `GIT_DIR=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/.git/worktrees/GFramework-cqrs GIT_WORK_TREE=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework-WorkTree/GFramework-cqrs bash scripts/validate-csharp-naming.sh` - - 结果:通过 - - 备注:`1059` 个 tracked C# 文件命名校验全部通过;本轮新增 stream invoker 类型与测试命名未引入回归 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_For_Hidden_Implementation_With_Visible_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_For_Hidden_Implementation_With_Visible_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` - - 结果:通过 - - 备注:`4/4` passed;确认 hidden implementation + visible interface 场景也会继续发射 request / stream invoker provider 元数据 -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"` - - 结果:通过 - - 备注:`8/8` passed;补齐 hidden implementation + visible interface 场景后,确认 generated request / stream invoker 在 runtime 侧也会优先命中 provider descriptor -- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error`;确认本轮 precise reflected invoker provider 合同回归未引入 generator 编译告警 -- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error`;并行验证时曾出现过 `MSB3026` 输出文件竞争噪音,随后已串行重跑并得到干净构建结果 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_For_Precise_Reflected_Request_Registrations|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_For_Precise_Reflected_Stream_Registrations|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_For_Hidden_Implementation_With_Visible_Handler_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_For_Hidden_Implementation_With_Visible_Handler_Interface"` - - 结果:通过 - - 备注:`4/4` passed;串行确认 visible-interface hidden-implementation 仍发射 provider 元数据,而 precise reflected 注册继续保持“不发射 provider descriptor”的当前合同 -- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` - - 结果:通过 - - 备注:并行执行 build/test 时出现 `MSB3026` 输出文件竞争噪音;无真实编译错误,后续以串行 test 结果作为本轮 authoritative 行为验证 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Provider_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Descriptor_Enumerator|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Provider_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Enumerator|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` - - 结果:通过 - - 备注:`6/6` passed;锁定 request / stream provider gate 依赖“provider 接口 + descriptor 枚举接口”同时存在,且原有 happy-path 发射仍保持通过 -- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` - - 结果:通过 - - 备注:并行执行 build/test 时出现 `MSB3026` 输出文件竞争噪音;当前已确认没有新增 analyzer warning,`GFramework.Cqrs.Tests` 仍能完成 Release 构建 -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.SendAsync_Should_Throw_When_Generated_Request_Invoker_Is_Not_Static|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.SendAsync_Should_Throw_When_Generated_Request_Invoker_Is_Incompatible|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.CreateStream_Should_Throw_When_Generated_Stream_Invoker_Is_Not_Static|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.CreateStream_Should_Throw_When_Generated_Stream_Invoker_Is_Incompatible|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.SendAsync_Should_Use_Generated_Request_Invoker_When_Provider_Is_Registered|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.CreateStream_Should_Use_Generated_Stream_Invoker_When_Provider_Is_Registered"` - - 结果:通过 - - 备注:`6/6` passed;确认 request / stream 的非法 generated invoker 现统一抛出 `InvalidOperationException`,且原有 happy-path 未回归 -- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error`;确认新增 non-enumerating provider 回归未引入构建告警 -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"` - - 结果:通过 - - 备注:`14/14` passed;确认 request / stream 的 generated happy-path、异常路径与 non-enumerating provider 反射回退语义均保持通过 -- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error`;确认 `CqrsRuntimeModule` 接线变更未引入 `GFramework.Core` 模块构建问题 -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsNotificationPublisherTests"` - - 结果:通过 - - 备注:`5/5` 通过;覆盖自定义 publisher 顺序、上下文注入、零处理器、首错即停与默认接线复用 -- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"` - - 结果:通过 - - 备注:`41/41` 通过;确认 CQRS 基础设施默认接线与容器行为未回归 -- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"` - - 结果:通过 - - 备注:`42/42` 通过;本轮新增 legacy alias 回填回归后,确认正式 seam 与旧命名空间 alias 仍指向同一实例 -- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error`;确认 legacy alias helper 收敛与文档更新未引入 `GFramework.Core` 模块构建告警 -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsHandlerRegistrarTests|FullyQualifiedName~CqrsDispatcherCacheTests"` - - 结果:通过 - - 备注:`22/22` 通过;确认 generated request invoker provider 的 registrar 接线、dispatcher 消费与现有 request/notification/stream cache 语义未回归 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` - - 结果:通过 - - 备注:`1/1` 通过;锁定 generator 会在 runtime 合同可用时发射 request invoker provider 成员 -- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error`;确认 request invoker provider seam 与 dispatcher/registrar 接线未引入新增构建告警 -- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error`;确认三份 `Mediator` 命名收口后的 CQRS 测试项目构建仍然干净 -- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests"` - - 结果:通过 - - 备注:`22/22` 通过;新增 `PublishAsync` / `CreateStream` 并发首次访问只解析一次 `ICqrsRuntime` 的回归 + - 备注:`16/16` passed;确认非静态 invoker 回退语义、空 descriptor 枚举回退,以及既有 generated / incompatible 分支均保持通过 ## 下一步 -1. 在保持 branch diff 明显低于 `50 files` 的前提下,继续挑选下一批低风险 `dispatch/invoker` 收敛切片,并优先考虑 request / stream provider 的剩余 runtime 失败边界、缓存预热边界或 generator gate 合同补强 +1. 在保持 branch diff 明显低于 `50 files` 的前提下,继续挑选下一批低风险 `dispatch/invoker` 收敛切片,并优先考虑 request / stream provider 的剩余缓存预热边界或 generator gate 合同补强 2. 基于已落地的 notification publisher seam,评估是否需要第二阶段公开配置面、并行 publisher 或 telemetry decorator 3. 单独规划旧 `Command` / `Query` API 的收口顺序;`LegacyICqrsRuntime` compatibility slice 已收口到显式 helper 与专门测试,可暂时移出最高优先级 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 4140cff1..8a3d6cde 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,35 @@ ## 2026-04-30 +### 阶段:PR #307 review follow-up 收敛(CQRS-REWRITE-RP-075) + +- 在 `RP-074` 后继续沿用 `gframework-batch-boot 50` 的低风险切片策略,本轮只处理 `$gframework-pr-review` 对当前 `PR #307` 仍然成立的本地问题 +- 主线程先用 `fetch_current_pr_review.py --json-output /tmp/current-pr-review.json` 抓取 PR #307 的 latest-head open threads,确认真正仍需处理的项集中在: + - stream/request invoker 描述符入口缺少更早的合同防御 + - request/stream provider 测试缺少“实现枚举契约但返回空 descriptor 集合”的回退覆盖 + - `docs/zh-CN/core/cqrs.md` 把 generated metadata 不兼容时的行为误写成“回退到反射” + - active tracking 累积了 `RP-063` 至 `RP-074` 的长验证历史,不再适合作为默认恢复入口 +- 本轮实现收敛: + - `GFramework.Cqrs/CqrsRequestInvokerDescriptor.cs` 与 `GFramework.Cqrs/CqrsStreamInvokerDescriptor.cs` 现会在构造阶段拒绝实例方法,把非法 generated metadata 失败点前移到 registrar 激活/预热阶段 + - `GFramework.Cqrs/CqrsRequestInvokerDescriptorEntry.cs` 与 `GFramework.Cqrs/CqrsStreamInvokerDescriptorEntry.cs` 现补齐公开构造入口的空值防御,并保持 request / stream 形状对称 + - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 现补齐 request / stream 的空 descriptor 枚举回退回归,并把“非静态 invoker”断言从首次分发抛错收敛为 registrar 放弃 generated registry 后回退到反射路径 + - `GFramework.Cqrs.Tests/Cqrs/HiddenImplementationGeneratedStreamInvokerProviderRegistry.cs` 现补齐 `` XML 注释与 `TryGetDescriptor(...)` 参数空值防御 + - `docs/zh-CN/core/cqrs.md` 现明确区分“未命中 generated descriptor 时回退到反射绑定”和“已命中的不兼容 generated metadata 会直接抛错”,并把 reader-facing 表格里的 `Internal/` 路径标签改成语义文案 + - `ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md` 现把恢复点推进到 `RP-075`,同时把 `RP-063` 至 `RP-074` 的命令级验证历史迁移到新的归档文件,active 入口只保留最近 PR 锚点与权威验证 + +### 验证(RP-075) + +- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"` + - 结果:通过,`16/16` passed + +### 当前下一步(RP-075) + +1. 提交本轮 PR #307 review follow-up 收敛,保持恢复点、trace 与已验证代码状态一致 +2. 若继续下一批,优先挑选 request / stream provider 的缓存预热边界或 generator gate 合同补强,而不是扩散到新的模块 +3. 保持只暂存本轮相关文件,避免把工作区里无关的 `.gitignore` 本地改动混入提交 + ### 阶段:non-enumerating provider reflection fallback 回归(CQRS-REWRITE-RP-074) - 在 `RP-073` 提交后继续按 `gframework-batch-boot 50` 执行;当前 branch diff 相对 `origin/main` 仍远低于 `50 files` 阈值,因此继续追加一轮单文件 runtime contract 回归 diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index e9b502ed..c705bfd3 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -167,7 +167,7 @@ protected override void OnInitialize() 2. 存在生成注册器时优先使用 `ICqrsHandlerRegistry` 3. 当生成注册器同时暴露 generated request invoker provider 时,runtime 会把 request/response 类型对对应的 descriptor 预先接线到 dispatcher 缓存,后续请求分发优先消费这些 generated request invoker 元数据 4. 当生成注册器同时暴露 generated stream invoker provider 时,runtime 会以同样方式优先消费 stream request 对应的 generated stream invoker descriptor;只有当前类型对未命中时,才回退到既有反射 stream binding -5. 生成注册器不可用或元数据异常时记录告警并回退到反射路径 +5. 生成注册器不可用时记录告警并回退到反射路径;只有“未命中 generated descriptor”才会走反射绑定,已命中的不兼容元数据会直接抛出异常 6. 当生成注册器携带 `CqrsReflectionFallbackAttribute` 元数据时,运行时会先完成生成注册器注册,再补剩余 handler 7. `CqrsReflectionFallbackAttribute` 可以同时携带 `Type[]` 和 `string[]` 两类清单;运行时会优先复用直接 `Type` 条目,只对名称条目做定向 `Assembly.GetType(...)` 查找 8. 只有旧版空 marker 或生成注册器不可用时,才会回到整程序集反射扫描 @@ -236,7 +236,7 @@ RegisterCqrsPipelineBehavior>(); | `GFramework.Cqrs.Abstractions/Cqrs/` | `ICqrsRuntime`、`ICqrsHandlerRegistrar`、`IPipelineBehavior<,>`、`IRequestHandler<,>`、`Unit` | 请求、处理器和 runtime seam 的最小契约 | | `GFramework.Cqrs/Command` `Query` `Notification` `Request` `Extensions` | `CommandBase`、`QueryBase`、`NotificationBase`、`RequestBase`、`ContextAwareCqrsExtensions` | 业务侧常用基类和上下文发送入口 | | `GFramework.Cqrs/Cqrs/` | `AbstractCommandHandler<,>`、`AbstractQueryHandler<,>`、`AbstractRequestHandler<,>`、`AbstractStreamCommandHandler<,>`、`AbstractStreamQueryHandler<,>`、`LoggingBehavior<,>` | 默认处理器基类、上下文注入、流式处理与行为管道 | -| `GFramework.Cqrs` 根入口与 `Internal/` | `CqrsRuntimeFactory`、`ICqrsHandlerRegistry`、`CqrsHandlerRegistryAttribute`、`CqrsReflectionFallbackAttribute`、`ICqrsRequestInvokerProvider`、`ICqrsStreamInvokerProvider` | runtime 创建入口、generated-registry 优先级、request / stream invoker provider 协作点、targeted fallback 语义和程序集去重规则 | +| 运行时入口与内部协作层 | `CqrsRuntimeFactory`、`ICqrsHandlerRegistry`、`CqrsHandlerRegistryAttribute`、`CqrsReflectionFallbackAttribute`、`ICqrsRequestInvokerProvider`、`ICqrsStreamInvokerProvider` | runtime 创建入口、generated-registry 优先级、request / stream invoker provider 协作点、targeted fallback 语义和程序集去重规则 | | `GFramework.Cqrs.SourceGenerators/Cqrs/` | `CqrsHandlerRegistryGenerator`、`RuntimeTypeReferenceSpec`、`OrderedRegistrationKind` | 生成注册器、可直接引用类型判定、mixed fallback 发射与诊断边界 | ## 继续阅读