From 98477068d68bbc86558324310ee2ace608476a4a Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:14:11 +0800 Subject: [PATCH 01/14] =?UTF-8?q?docs(cqrs):=20=E8=A1=A5=E5=85=85=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=BC=8F=20stream=20invoker=20=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 CQRS runtime 与生成器文档,补充 generated stream invoker provider / descriptor 的并列表述。 - 说明 runtime 优先消费 generated request / stream invoker 元数据,未命中时回退到既有反射 binding。 - 调整 request-only 措辞,使 reader-facing 文案与现有 generated request invoker 语义保持一致。 --- GFramework.Cqrs/README.md | 7 ++++-- docs/zh-CN/core/cqrs.md | 23 ++++++++++++++----- .../cqrs-handler-registry-generator.md | 21 +++++++++++++---- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/GFramework.Cqrs/README.md b/GFramework.Cqrs/README.md index 65a92db5..b3176983 100644 --- a/GFramework.Cqrs/README.md +++ b/GFramework.Cqrs/README.md @@ -20,7 +20,7 @@ - `GeWuYou.GFramework.Cqrs` - 默认 runtime 与业务侧常用基类。 - `GeWuYou.GFramework.Cqrs.SourceGenerators` - - 可选。为消费端程序集生成 `ICqrsHandlerRegistry`,运行时优先走生成注册表;只有缺失、不适用,或 fallback 仍需补齐剩余 handler 时,才继续进入反射路径。 + - 可选。为消费端程序集生成 `ICqrsHandlerRegistry`,并在可用时补充 generated request / stream invoker provider 元数据;运行时会优先消费这些编译期元数据,只有缺失、不适用,或 fallback 仍需补齐剩余 handler 时,才继续进入反射路径。 - `GFramework.Core` - 架构上下文中实际调用 `ICqrsRuntime`,并在模块初始化时注册 CQRS 基础设施。 @@ -120,13 +120,15 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu ## 运行时行为 - 请求分发 - - `CqrsDispatcher` 按请求实际类型解析 `IRequestHandler<,>`,未找到处理器会抛出异常。 + - `CqrsDispatcher` 按请求实际类型解析 `IRequestHandler<,>`,若当前程序集提供 generated request invoker provider,则会先复用对应 descriptor 中的处理器服务类型与 invoker 元数据;未命中时仍回退到既有反射 request binding 创建路径。 + - 未找到处理器会抛出异常。 - 通知分发 - 通知会分发给所有已注册 `INotificationHandler<>`;零处理器时默认静默完成。 - 默认通知发布器会按容器解析顺序逐个执行处理器,并在首个处理器抛出异常时立即停止后续分发。 - 若容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置顺序发布器。 - 流式请求 - 通过 `IStreamRequest` 和 `IStreamRequestHandler<,>` 返回 `IAsyncEnumerable`。 + - 当消费端程序集提供 generated stream invoker provider / descriptor 后,runtime 会优先消费这组 stream invoker 元数据;未命中时仍回退到既有反射 stream binding 创建路径。 - 上下文注入 - 处理器基类继承 `CqrsContextAwareHandlerBase`,runtime 会在分发前注入当前 `IArchitectureContext`。 - 如果处理器或行为需要上下文注入,而当前 `ICqrsContext` 不是 `IArchitectureContext`,默认实现会抛出异常。 @@ -140,6 +142,7 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu - 同一程序集按稳定键去重,避免重复注册。 - 优先尝试消费端程序集上的 `ICqrsHandlerRegistry` 生成注册器。 +- 当生成注册器同时暴露 generated request invoker provider 或 generated stream invoker provider 时,registrar 会把对应 descriptor 元数据接线到 runtime 缓存。 - 生成注册器不可用或元数据损坏时,记录告警并回退到反射扫描。 - 当程序集声明了 `CqrsReflectionFallbackAttribute` 时,运行时会先执行生成注册器,再只补它未覆盖的 handler。 - `CqrsReflectionFallbackAttribute` 现在可以多次声明,并同时承载 `Type[]` 与 `string[]` 两类 fallback 清单。 diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 1fd1f601..697656cc 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -165,14 +165,25 @@ protected override void OnInitialize() 1. 优先读取消费端程序集上的 `CqrsHandlerRegistryAttribute` 2. 存在生成注册器时优先使用 `ICqrsHandlerRegistry` -3. 生成注册器不可用或元数据异常时记录告警并回退到反射路径 -4. 当生成注册器携带 `CqrsReflectionFallbackAttribute` 元数据时,运行时会先完成生成注册器注册,再补剩余 handler -5. `CqrsReflectionFallbackAttribute` 可以同时携带 `Type[]` 和 `string[]` 两类清单;运行时会优先复用直接 `Type` 条目,只对名称条目做定向 `Assembly.GetType(...)` 查找 -6. 只有旧版空 marker 或生成注册器不可用时,才会回到整程序集反射扫描 -7. 同一程序集按稳定键去重,避免重复注册 +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. 生成注册器不可用或元数据异常时记录告警并回退到反射路径 +6. 当生成注册器携带 `CqrsReflectionFallbackAttribute` 元数据时,运行时会先完成生成注册器注册,再补剩余 handler +7. `CqrsReflectionFallbackAttribute` 可以同时携带 `Type[]` 和 `string[]` 两类清单;运行时会优先复用直接 `Type` 条目,只对名称条目做定向 `Assembly.GetType(...)` 查找 +8. 只有旧版空 marker 或生成注册器不可用时,才会回到整程序集反射扫描 +9. 同一程序集按稳定键去重,避免重复注册 换句话说,声明 fallback 特性本身不等于“整包反射扫描”。当前推荐理解是:生成注册器负责能静态表达的部分,fallback 只补它覆盖不到的 handler。 +如果你在阅读 dispatcher 行为,可以把这部分理解成两组并列能力: + +- request invoker provider / descriptor + - 面向 `SendRequestAsync(...)`、`SendAsync(...)`、`SendQueryAsync(...)` 这类单次请求分发 +- stream invoker provider / descriptor + - 面向 `CreateStream(...)` 触发的流式请求分发 + +两者的共同点都是“优先消费 generated invoker 元数据,未命中时保留既有反射绑定作为兜底”,而不是要求业务侧切换到另一套 runtime 入口。 + `Cqrs.SourceGenerators` 的专题入口见[CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)。 ## Pipeline Behavior @@ -225,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`、`DefaultCqrsRegistrationService` | runtime 创建入口、generated-registry 优先级、targeted fallback 语义和程序集去重规则 | +| `GFramework.Cqrs` 根入口与 `Internal/` | `CqrsRuntimeFactory`、`ICqrsHandlerRegistry`、`CqrsHandlerRegistryAttribute`、`CqrsReflectionFallbackAttribute`、`ICqrsRequestInvokerProvider` | runtime 创建入口、generated-registry 优先级、request / stream invoker provider 协作点、targeted fallback 语义和程序集去重规则 | | `GFramework.Cqrs.SourceGenerators/Cqrs/` | `CqrsHandlerRegistryGenerator`、`RuntimeTypeReferenceSpec`、`OrderedRegistrationKind` | 生成注册器、可直接引用类型判定、mixed fallback 发射与诊断边界 | ## 继续阅读 diff --git a/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md b/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md index 21436faa..cce62f85 100644 --- a/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md +++ b/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md @@ -6,7 +6,8 @@ description: 为消费端程序集生成 CQRS handler registry,并在需要时 # CQRS Handler Registry 生成器 `GFramework.Cqrs.SourceGenerators` 会在编译期为当前业务程序集生成 `ICqrsHandlerRegistry`,让 `GFramework.Cqrs` -runtime 在注册 handlers 时优先走静态注册表,而不是先扫描整个程序集。 +runtime 在注册 handlers 时优先走静态注册表;当运行时合同允许时,也会把 request / stream 分发可直接复用的 invoker +元数据前移到编译期,而不是总是先扫描整个程序集或在首次分发时再走反射绑定。 它服务的是 `Cqrs` 家族,不是独立运行时: @@ -27,11 +28,17 @@ runtime 在注册 handlers 时优先走静态注册表,而不是先扫描整 1. 一个实现 `ICqrsHandlerRegistry` 的内部注册器类型 2. 程序集级 `CqrsHandlerRegistryAttribute` +当运行时暴露对应合同、且当前 handler 可被安全静态表达时,生成注册器还可以继续暴露: + +- generated request invoker provider / descriptor +- generated stream invoker provider / descriptor + 当某些 handler 不能被生成代码安全地直接引用时,还会补发: - 程序集级 `CqrsReflectionFallbackAttribute` -这意味着运行时会先使用生成注册器完成可静态表达的映射,再只对剩余类型做补扫,而不是退回整程序集盲扫。 +这意味着运行时会先使用生成注册器完成可静态表达的映射;对 request 与 stream 分发来说,也会优先消费 generated invoker +descriptor。只有当前类型对没有 generated metadata,或 registry / fallback 无法覆盖时,才继续回到既有反射 binding 或补扫路径,而不是退回整程序集盲扫。 如果这些 fallback handlers 本身仍可直接引用,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据;当 runtime 允许同一程序集声明多个 fallback 特性实例时,mixed 场景也会拆成 `Type` 元数据和字符串元数据两段,进一步减少 runtime 再做字符串类型名回查的成本。 ## 最小接入路径 @@ -78,9 +85,11 @@ RegisterCqrsHandlersFromAssemblies( 1. 先读取程序集上的 `CqrsHandlerRegistryAttribute` 2. 优先激活生成的 `ICqrsHandlerRegistry` -3. 若生成元数据损坏、registry 不可激活,记录告警并回退到反射路径 -4. 若存在 `CqrsReflectionFallbackAttribute`,只补扫剩余 handler -5. 同一程序集按稳定键去重,避免重复注册 +3. 若生成注册器同时提供 request invoker provider / descriptor,registrar 会把这些 request invoker 元数据预先登记到 dispatcher 缓存 +4. 若生成注册器同时提供 stream invoker provider / descriptor,runtime 也会优先消费对应的 generated stream invoker 元数据;未命中时仍回退到既有反射 stream binding +5. 若生成元数据损坏、registry 不可激活,记录告警并回退到反射路径 +6. 若存在 `CqrsReflectionFallbackAttribute`,只补扫剩余 handler +7. 同一程序集按稳定键去重,避免重复注册 这个行为由 `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs` 和 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 共同覆盖。 @@ -91,6 +100,7 @@ RegisterCqrsHandlersFromAssemblies( - 业务程序集内 handler 数量较多 - 想把 handler 注册路径前移到编译期 +- 想把 request / stream 分发里可静态确定的 invoker metadata 一并前移到编译期 - 希望冷启动阶段减少整程序集反射扫描 - 需要更明确地观察“哪些 handler 走静态注册,哪些只能走 fallback” @@ -127,6 +137,7 @@ RegisterCqrsHandlersFromAssemblies( - `GFramework.Cqrs.ICqrsHandlerRegistry` - `GFramework.Cqrs.CqrsHandlerRegistryAttribute` - `GFramework.Cqrs.CqrsReflectionFallbackAttribute` +- `GFramework.Cqrs.ICqrsRequestInvokerProvider` - `GFramework.Cqrs.SourceGenerators.Cqrs.CqrsHandlerRegistryGenerator` 模块族入口见: From f17f9f3da6a1de91b8a7ab0b15a03d59d3e91c81 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:14:29 +0800 Subject: [PATCH 02/14] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E5=85=85=20stream?= =?UTF-8?q?=20invoker=20provider=20=E7=94=9F=E6=88=90=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 stream invoker provider runtime fixture,复用 request provider 测试风格锁定 descriptor 和静态 invoker 形状 - 补充 Phase 8 stream invoker provider 回归测试骨架,并暂时以 Ignore 挂起等待主线程生成实现落地 --- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 9961a139..ad8a93f7 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1790,6 +1790,112 @@ public class CqrsHandlerRegistryGeneratorTests } """; + private const string StreamInvokerProviderSource = """ + using System; + using System.Collections.Generic; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest + { + ValueTask Handle(TRequest request, CancellationToken cancellationToken); + } + + public interface INotificationHandler where TNotification : INotification { } + + public interface IStreamRequestHandler where TRequest : IStreamRequest + { + IAsyncEnumerable Handle(TRequest request, CancellationToken cancellationToken); + } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + public interface ICqrsStreamInvokerProvider + { + bool TryGetDescriptor(Type requestType, Type responseType, out CqrsStreamInvokerDescriptor? descriptor); + } + + public interface IEnumeratesCqrsStreamInvokerDescriptors + { + IReadOnlyList GetDescriptors(); + } + + public sealed class CqrsStreamInvokerDescriptor + { + public CqrsStreamInvokerDescriptor(Type handlerType, MethodInfo invokerMethod) { } + } + + public sealed class CqrsStreamInvokerDescriptorEntry + { + public CqrsStreamInvokerDescriptorEntry(Type requestType, Type responseType, CqrsStreamInvokerDescriptor descriptor) + { + RequestType = requestType; + ResponseType = responseType; + Descriptor = descriptor; + } + + public Type RequestType { get; } + + public Type ResponseType { get; } + + public CqrsStreamInvokerDescriptor Descriptor { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record VisibleStream(int Count) : IStreamRequest; + + public sealed class VisibleStreamHandler : IStreamRequestHandler + { + public async IAsyncEnumerable Handle(VisibleStream request, CancellationToken cancellationToken) + { + yield return request.Count; + await Task.CompletedTask; + } + } + } + """; + /// /// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 /// @@ -2387,6 +2493,66 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当 runtime 暴露 stream invoker provider 契约时,生成器会让 generated registry 同时发射 + /// stream invoker 描述符与对应的开放静态 invoker 方法。 + /// + [Test] + [Ignore("Enable after generated stream invoker provider / descriptor emission lands in Phase 8.")] + public void Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available() + { + var execution = ExecuteGenerator(StreamInvokerProviderSource); + var inputCompilationErrors = execution.InputCompilationDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + var generatorErrors = execution.GeneratorDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + Assert.Multiple(() => + { + Assert.That(inputCompilationErrors, Is.Empty); + Assert.That(generatedCompilationErrors, Is.Empty); + Assert.That(generatorErrors, Is.Empty); + Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1)); + Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs")); + var generatedSource = execution.GeneratedSources[0].content; + Assert.That( + generatedSource, + Does.Contain( + "internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry, global::GFramework.Cqrs.ICqrsStreamInvokerProvider, global::GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors")); + Assert.That( + generatedSource, + Does.Contain( + "new global::GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(typeof(global::TestApp.VisibleStream), typeof(int),")); + Assert.That( + generatedSource, + Does.Contain( + "new global::GFramework.Cqrs.CqrsStreamInvokerDescriptor(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler), typeof(__GFrameworkGeneratedCqrsHandlerRegistry).GetMethod(nameof(InvokeStreamHandler0), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!)")); + Assert.That( + generatedSource, + Does.Contain( + "public bool TryGetDescriptor(global::System.Type requestType, global::System.Type responseType, out global::GFramework.Cqrs.CqrsStreamInvokerDescriptor? descriptor)")); + Assert.That( + generatedSource, + Does.Contain( + "private static object InvokeStreamHandler0(object handler, object request, global::System.Threading.CancellationToken cancellationToken)")); + Assert.That( + generatedSource, + Does.Contain( + "var typedHandler = (global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler)handler;")); + Assert.That( + generatedSource, + Does.Contain("return typedHandler.Handle(typedRequest, cancellationToken);")); + Assert.That( + generatedSource, + Does.Contain( + "public global::System.Collections.Generic.IReadOnlyList GetDescriptors()")); + }); + } + /// /// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。 /// From ea0b9377055d7ce41d278ca2494894d2f82b1f0f Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:26:54 +0800 Subject: [PATCH 03/14] =?UTF-8?q?feat(cqrs):=20=E8=A1=A5=E5=85=85=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=BC=8F=20stream=20invoker=20=E6=8E=A5=E7=BC=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 stream invoker provider、descriptor 与 dispatcher/registrar 接线 - 更新 source generator 与回归测试,覆盖 generated stream invoker 发射和消费语义 - 更新 CQRS 文档与 ai-plan 恢复点,补充 stream invoker 的接入与验证记录 --- .../CqrsHandlerRegistryGenerator.Models.cs | 22 ++- ...HandlerRegistryGenerator.SourceEmission.cs | 179 +++++++++++++++++- .../Cqrs/CqrsHandlerRegistryGenerator.cs | 46 ++++- GFramework.Cqrs.SourceGenerators/README.md | 4 + ...qrsGeneratedRequestInvokerProviderTests.cs | 69 +++++++ .../GeneratedStreamInvokerProviderRegistry.cs | 102 ++++++++++ .../Cqrs/GeneratedStreamInvokerRequest.cs | 9 + .../GeneratedStreamInvokerRequestHandler.cs | 34 ++++ .../CqrsStreamInvokerDescriptor.cs | 31 +++ .../CqrsStreamInvokerDescriptorEntry.cs | 12 ++ GFramework.Cqrs/ICqrsStreamInvokerProvider.cs | 33 ++++ ...IEnumeratesCqrsStreamInvokerDescriptors.cs | 18 ++ GFramework.Cqrs/Internal/CqrsDispatcher.cs | 97 ++++++++++ .../Internal/CqrsHandlerRegistrar.cs | 56 ++++++ .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 5 +- .../todos/cqrs-rewrite-migration-tracking.md | 28 ++- .../traces/cqrs-rewrite-migration-trace.md | 56 ++++++ docs/zh-CN/core/cqrs.md | 2 +- .../cqrs-handler-registry-generator.md | 1 + 19 files changed, 792 insertions(+), 12 deletions(-) create mode 100644 GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerProviderRegistry.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerRequest.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerRequestHandler.cs create mode 100644 GFramework.Cqrs/CqrsStreamInvokerDescriptor.cs create mode 100644 GFramework.Cqrs/CqrsStreamInvokerDescriptorEntry.cs create mode 100644 GFramework.Cqrs/ICqrsStreamInvokerProvider.cs create mode 100644 GFramework.Cqrs/IEnumeratesCqrsStreamInvokerDescriptors.cs diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs index beb22f1e..7edbfdac 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs @@ -9,12 +9,17 @@ public sealed partial class CqrsHandlerRegistryGenerator string RequestTypeDisplayName, string ResponseTypeDisplayName); + private readonly record struct StreamInvokerRegistrationSpec( + string RequestTypeDisplayName, + string ResponseTypeDisplayName); + private readonly record struct HandlerRegistrationSpec( string HandlerInterfaceDisplayName, string ImplementationTypeDisplayName, string HandlerInterfaceLogName, string ImplementationLogName, - RequestInvokerRegistrationSpec? RequestInvokerRegistration); + RequestInvokerRegistrationSpec? RequestInvokerRegistration, + StreamInvokerRegistrationSpec? StreamInvokerRegistration); private readonly record struct ReflectedImplementationRegistrationSpec( string HandlerInterfaceDisplayName, @@ -31,7 +36,9 @@ public sealed partial class CqrsHandlerRegistryGenerator bool HasReflectionTypeLookups, bool HasExternalAssemblyTypeLookups, bool SupportsRequestInvokerProvider, - ImmutableArray RequestInvokerEmissions) + ImmutableArray RequestInvokerEmissions, + bool SupportsStreamInvokerProvider, + ImmutableArray StreamInvokerEmissions) { public bool RequiresRegistryAssemblyVariable => HasReflectedImplementationRegistrations || @@ -39,6 +46,8 @@ public sealed partial class CqrsHandlerRegistryGenerator HasReflectionTypeLookups; public bool HasRequestInvokerProvider => SupportsRequestInvokerProvider && !RequestInvokerEmissions.IsDefaultOrEmpty; + + public bool HasStreamInvokerProvider => SupportsStreamInvokerProvider && !StreamInvokerEmissions.IsDefaultOrEmpty; } private readonly record struct RequestInvokerEmissionSpec( @@ -47,6 +56,12 @@ public sealed partial class CqrsHandlerRegistryGenerator string HandlerInterfaceDisplayName, int MethodIndex); + private readonly record struct StreamInvokerEmissionSpec( + string RequestTypeDisplayName, + string ResponseTypeDisplayName, + string HandlerInterfaceDisplayName, + int MethodIndex); + /// /// 标记某条 handler 注册语句在生成阶段采用的表达策略。 /// @@ -328,5 +343,6 @@ public sealed partial class CqrsHandlerRegistryGenerator bool SupportsNamedReflectionFallbackTypes, bool SupportsDirectReflectionFallbackTypes, bool SupportsMultipleReflectionFallbackAttributes, - bool SupportsRequestInvokerProvider); + bool SupportsRequestInvokerProvider, + bool SupportsStreamInvokerProvider); } diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs index 858c00d9..ae539d23 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs @@ -57,6 +57,9 @@ public sealed partial class CqrsHandlerRegistryGenerator var requestInvokerEmissions = CreateRequestInvokerEmissions( generationEnvironment.SupportsRequestInvokerProvider, registrations); + var streamInvokerEmissions = CreateStreamInvokerEmissions( + generationEnvironment.SupportsStreamInvokerProvider, + registrations); return new GeneratedRegistrySourceShape( hasReflectedImplementationRegistrations, @@ -64,7 +67,9 @@ public sealed partial class CqrsHandlerRegistryGenerator hasReflectionTypeLookups, hasExternalAssemblyTypeLookups, generationEnvironment.SupportsRequestInvokerProvider, - requestInvokerEmissions); + requestInvokerEmissions, + generationEnvironment.SupportsStreamInvokerProvider, + streamInvokerEmissions); } /// @@ -111,6 +116,46 @@ public sealed partial class CqrsHandlerRegistryGenerator return builder.ToImmutable(); } + /// + /// 从 direct handler 注册描述中提取 stream invoker 发射计划。 + /// + /// + /// 指示当前 runtime 是否同时暴露 ICqrsStreamInvokerProvider 与 + /// IEnumeratesCqrsStreamInvokerDescriptors 契约;若不支持,则本方法必须返回空结果并让后续发射路径整体跳过。 + /// + /// 已按稳定顺序整理完成的 handler 注册描述。 + /// + /// 由 directRegistration.StreamInvokerRegistration 派生出的 集合。 + /// methodIndex 与其 direct registration 的遍历顺序单调递增, + /// 因而只要上游排序稳定,生成的 invoker 方法名与描述符顺序就跨运行保持稳定。 + /// + private static ImmutableArray CreateStreamInvokerEmissions( + bool supportsStreamInvokerProvider, + IReadOnlyList registrations) + { + if (!supportsStreamInvokerProvider) + return ImmutableArray.Empty; + + var builder = ImmutableArray.CreateBuilder(); + var methodIndex = 0; + foreach (var registration in registrations) + { + foreach (var directRegistration in registration.DirectRegistrations) + { + if (directRegistration.StreamInvokerRegistration is not { } streamInvokerRegistration) + continue; + + builder.Add(new StreamInvokerEmissionSpec( + streamInvokerRegistration.RequestTypeDisplayName, + streamInvokerRegistration.ResponseTypeDisplayName, + directRegistration.HandlerInterfaceDisplayName, + methodIndex++)); + } + } + + return builder.ToImmutable(); + } + /// /// 发射生成文件头、nullable 指令以及注册器所需的程序集级元数据特性。 /// @@ -221,6 +266,15 @@ public sealed partial class CqrsHandlerRegistryGenerator builder.Append(".IEnumeratesCqrsRequestInvokerDescriptors"); } + if (sourceShape.HasStreamInvokerProvider) + { + builder.Append(", global::"); + builder.Append(CqrsRuntimeNamespace); + builder.Append(".ICqrsStreamInvokerProvider, global::"); + builder.Append(CqrsRuntimeNamespace); + builder.Append(".IEnumeratesCqrsStreamInvokerDescriptors"); + } + builder.AppendLine(); builder.AppendLine("{"); AppendRegisterMethod(builder, registrations, sourceShape); @@ -231,6 +285,12 @@ public sealed partial class CqrsHandlerRegistryGenerator AppendRequestInvokerProviderMembers(builder, sourceShape.RequestInvokerEmissions); } + if (sourceShape.HasStreamInvokerProvider) + { + builder.AppendLine(); + AppendStreamInvokerProviderMembers(builder, sourceShape.StreamInvokerEmissions); + } + if (sourceShape.HasExternalAssemblyTypeLookups) { builder.AppendLine(); @@ -366,9 +426,11 @@ public sealed partial class CqrsHandlerRegistryGenerator /// private static void AppendRequestInvokerProviderMethods(StringBuilder builder) { - builder.Append(" public global::System.Collections.Generic.IReadOnlyList GetDescriptors()"); + builder.Append(".CqrsRequestInvokerDescriptorEntry> global::"); + builder.Append(CqrsRuntimeNamespace); + builder.AppendLine(".IEnumeratesCqrsRequestInvokerDescriptors.GetDescriptors()"); builder.AppendLine(" {"); builder.AppendLine(" return RequestInvokerDescriptors;"); builder.AppendLine(" }"); @@ -424,6 +486,117 @@ public sealed partial class CqrsHandlerRegistryGenerator builder.AppendLine(" }"); } + /// + /// 发射 generated registry 的 stream invoker provider 成员。 + /// + /// 生成源码构造器。 + /// 当前要输出的 stream invoker 发射计划。 + private static void AppendStreamInvokerProviderMembers( + StringBuilder builder, + ImmutableArray streamInvokerEmissions) + { + AppendStreamInvokerDescriptorArray(builder, streamInvokerEmissions); + builder.AppendLine(); + AppendStreamInvokerProviderMethods(builder); + + for (var index = 0; index < streamInvokerEmissions.Length; index++) + { + builder.AppendLine(); + AppendStreamInvokerMethod(builder, streamInvokerEmissions[index]); + } + } + + /// + /// 发射 generated registry 的 stream invoker 描述符数组。 + /// + private static void AppendStreamInvokerDescriptorArray( + StringBuilder builder, + ImmutableArray streamInvokerEmissions) + { + builder.AppendLine(" private static readonly global::GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry[] StreamInvokerDescriptors ="); + builder.AppendLine(" ["); + + for (var index = 0; index < streamInvokerEmissions.Length; index++) + { + var emission = streamInvokerEmissions[index]; + builder.Append(" new global::"); + builder.Append(CqrsRuntimeNamespace); + builder.Append(".CqrsStreamInvokerDescriptorEntry(typeof("); + builder.Append(emission.RequestTypeDisplayName); + builder.Append("), typeof("); + builder.Append(emission.ResponseTypeDisplayName); + builder.Append("), new global::"); + builder.Append(CqrsRuntimeNamespace); + builder.Append(".CqrsStreamInvokerDescriptor(typeof("); + builder.Append(emission.HandlerInterfaceDisplayName); + builder.Append("), typeof("); + builder.Append(GeneratedTypeName); + builder.Append(").GetMethod(nameof(InvokeStreamHandler"); + builder.Append(emission.MethodIndex); + builder.Append("), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!))"); + builder.AppendLine(index == streamInvokerEmissions.Length - 1 ? string.Empty : ","); + } + + builder.AppendLine(" ];"); + } + + /// + /// 发射 generated registry 对 stream invoker provider 契约的实现方法。 + /// + private static void AppendStreamInvokerProviderMethods(StringBuilder builder) + { + builder.Append(" global::System.Collections.Generic.IReadOnlyList global::"); + builder.Append(CqrsRuntimeNamespace); + builder.AppendLine(".IEnumeratesCqrsStreamInvokerDescriptors.GetDescriptors()"); + builder.AppendLine(" {"); + builder.AppendLine(" return StreamInvokerDescriptors;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.Append(" public bool TryGetDescriptor(global::System.Type requestType, global::System.Type responseType, out global::"); + builder.Append(CqrsRuntimeNamespace); + builder.AppendLine(".CqrsStreamInvokerDescriptor? descriptor)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (requestType is null)"); + builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(requestType));"); + builder.AppendLine(" if (responseType is null)"); + builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(responseType));"); + builder.AppendLine(); + builder.AppendLine(" foreach (var entry in StreamInvokerDescriptors)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (entry.RequestType == requestType && entry.ResponseType == responseType)"); + builder.AppendLine(" {"); + builder.AppendLine(" descriptor = entry.Descriptor;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" descriptor = null;"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + } + + /// + /// 为单个 stream invoker 描述符发射对应的静态强类型桥接方法。 + /// + private static void AppendStreamInvokerMethod(StringBuilder builder, StreamInvokerEmissionSpec emission) + { + builder.Append(" private static object InvokeStreamHandler"); + builder.Append(emission.MethodIndex); + builder.Append("(object handler, object request, global::System.Threading.CancellationToken cancellationToken)"); + builder.AppendLine(); + builder.AppendLine(" {"); + builder.Append(" var typedHandler = ("); + builder.Append(emission.HandlerInterfaceDisplayName); + builder.AppendLine(")handler;"); + builder.Append(" var typedRequest = ("); + builder.Append(emission.RequestTypeDisplayName); + builder.AppendLine(")request;"); + builder.AppendLine(" return typedHandler.Handle(typedRequest, cancellationToken);"); + builder.AppendLine(" }"); + } + private static void AppendDirectRegistrations( StringBuilder builder, ImplementationRegistrationSpec registration) diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index db5f1bc7..80a2c5a6 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -22,6 +22,13 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator $"{CqrsRuntimeNamespace}.CqrsRequestInvokerDescriptor"; private const string CqrsRequestInvokerDescriptorEntryMetadataName = $"{CqrsRuntimeNamespace}.CqrsRequestInvokerDescriptorEntry"; + private const string ICqrsStreamInvokerProviderMetadataName = $"{CqrsRuntimeNamespace}.ICqrsStreamInvokerProvider"; + private const string IEnumeratesCqrsStreamInvokerDescriptorsMetadataName = + $"{CqrsRuntimeNamespace}.IEnumeratesCqrsStreamInvokerDescriptors"; + private const string CqrsStreamInvokerDescriptorMetadataName = + $"{CqrsRuntimeNamespace}.CqrsStreamInvokerDescriptor"; + private const string CqrsStreamInvokerDescriptorEntryMetadataName = + $"{CqrsRuntimeNamespace}.CqrsStreamInvokerDescriptorEntry"; private const string CqrsHandlerRegistryAttributeMetadataName = $"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute"; @@ -78,6 +85,11 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator compilation.GetTypeByMetadataName(IEnumeratesCqrsRequestInvokerDescriptorsMetadataName) is not null && compilation.GetTypeByMetadataName(CqrsRequestInvokerDescriptorMetadataName) is not null && compilation.GetTypeByMetadataName(CqrsRequestInvokerDescriptorEntryMetadataName) is not null; + var supportsStreamInvokerProvider = + compilation.GetTypeByMetadataName(ICqrsStreamInvokerProviderMetadataName) is not null && + compilation.GetTypeByMetadataName(IEnumeratesCqrsStreamInvokerDescriptorsMetadataName) is not null && + compilation.GetTypeByMetadataName(CqrsStreamInvokerDescriptorMetadataName) is not null && + compilation.GetTypeByMetadataName(CqrsStreamInvokerDescriptorEntryMetadataName) is not null; var stringType = compilation.GetSpecialType(SpecialType.System_String); var typeType = compilation.GetTypeByMetadataName("System.Type"); var supportsNamedReflectionFallbackTypes = reflectionFallbackAttributeType is not null && @@ -98,7 +110,8 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator supportsNamedReflectionFallbackTypes, supportsDirectReflectionFallbackTypes, supportsMultipleReflectionFallbackAttributes, - supportsRequestInvokerProvider); + supportsRequestInvokerProvider, + supportsStreamInvokerProvider); } private static bool IsHandlerCandidate(SyntaxNode node) @@ -234,6 +247,9 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator implementationLogName, TryCreateRequestInvokerRegistrationSpec(handlerInterface, out var requestInvokerRegistration) ? requestInvokerRegistration + : null, + TryCreateStreamInvokerRegistrationSpec(handlerInterface, out var streamInvokerRegistration) + ? streamInvokerRegistration : null)); return true; } @@ -281,6 +297,34 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator return true; } + /// + /// 当当前直接注册项属于流式请求处理器时,提取 stream invoker provider 所需的请求/响应类型显示名。 + /// + private static bool TryCreateStreamInvokerRegistrationSpec( + INamedTypeSymbol handlerInterface, + out StreamInvokerRegistrationSpec streamInvokerRegistration) + { + if (!string.Equals( + handlerInterface.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + $"global::{CqrsContractsNamespace}.IStreamRequestHandler", + StringComparison.Ordinal)) + { + streamInvokerRegistration = default; + return false; + } + + if (handlerInterface.TypeArguments.Length != 2) + { + streamInvokerRegistration = default; + return false; + } + + streamInvokerRegistration = new StreamInvokerRegistrationSpec( + handlerInterface.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + handlerInterface.TypeArguments[1].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + return true; + } + /// /// 执行 CQRS handler registry 生成管线的最终发射阶段,负责将候选 handler 分析结果汇总为单个 /// CqrsHandlerRegistry.g.cs,并在需要时附带程序集级 reflection fallback 元数据。 diff --git a/GFramework.Cqrs.SourceGenerators/README.md b/GFramework.Cqrs.SourceGenerators/README.md index 1a77b07a..12e8f7c9 100644 --- a/GFramework.Cqrs.SourceGenerators/README.md +++ b/GFramework.Cqrs.SourceGenerators/README.md @@ -15,6 +15,7 @@ 并生成: - `ICqrsHandlerRegistry` 实现 +- 在运行时合同允许时,额外生成 request / stream invoker provider 与 descriptor 元数据 - 程序集级 `CqrsHandlerRegistryAttribute` - 必要时的 `CqrsReflectionFallbackAttribute` 元数据 @@ -34,6 +35,8 @@ 它会在可以安全生成静态注册器时前移注册工作;对无法由生成代码直接引用的 handler,则通过 reflection fallback 元数据让运行时做定向补扫,而不是整程序集盲扫。 当 fallback handler 本身仍可直接引用时,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据;如果 runtime 允许同一程序集声明多个 fallback 特性实例,mixed 场景也会拆成 `Type` 元数据和字符串元数据两段,进一步减少运行时按类型名回查程序集的成本。 +当 runtime 同时暴露 request / stream invoker provider 契约时,生成注册器还会为可直接静态表达的 `IRequestHandler<,>` 与 +`IStreamRequestHandler<,>` 发射对应 descriptor 与开放静态 invoker 方法,让 runtime 在首次创建 request / stream binding 时优先消费这些编译期元数据;未命中时仍保持既有反射 binding 创建语义。 ## 最小接入路径 @@ -55,6 +58,7 @@ RegisterCqrsHandlersFromAssembly(typeof(GameArchitecture).Assembly); ``` 安装生成器后,运行时会优先走生成的 registry;无法静态表达的部分再走定向回退。 +如果当前 runtime 合同已经包含 request / stream invoker provider seam,generated registry 还会把这两类 invoker 元数据一并前移到编译期。 ## 什么时候值得安装 diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs index e5e5171f..b49e9d86 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs @@ -57,6 +57,24 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Is.EqualTo([typeof(GeneratedRequestInvokerProviderRegistry)])); } + /// + /// 验证 registrar 激活 generated registry 后,会把 stream invoker provider 注册到容器中。 + /// + [Test] + public void RegisterHandlers_Should_Register_Generated_Stream_Invoker_Provider() + { + var generatedAssembly = CreateGeneratedStreamInvokerAssembly(); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + + var providers = container.GetAll(); + + Assert.That( + providers.Select(static provider => provider.GetType()), + Is.EqualTo([typeof(GeneratedStreamInvokerProviderRegistry)])); + } + /// /// 验证 dispatcher 在首次创建 request binding 时,会优先消费 generated request invoker provider。 /// @@ -74,6 +92,23 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Assert.That(response, Is.EqualTo("generated:payload")); } + /// + /// 验证 dispatcher 在首次创建 stream binding 时,会优先消费 generated stream invoker provider。 + /// + [Test] + public async Task CreateStream_Should_Use_Generated_Stream_Invoker_When_Provider_Is_Registered() + { + var generatedAssembly = CreateGeneratedStreamInvokerAssembly(); + 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))); + Assert.That(results, Is.EqualTo([30, 31])); + } + /// /// 创建带有 generated request invoker registry 元数据的程序集替身。 /// @@ -89,6 +124,21 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests return generatedAssembly; } + /// + /// 创建带有 generated stream invoker registry 元数据的程序集替身。 + /// + private static Mock CreateGeneratedStreamInvokerAssembly() + { + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns("GFramework.Cqrs.Tests.Cqrs.GeneratedStreamInvokerAssembly, Version=1.0.0.0"); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(typeof(GeneratedStreamInvokerProviderRegistry))]); + return generatedAssembly; + } + /// /// 清空 registrar 静态缓存。 /// @@ -109,6 +159,7 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests ClearCache(GetDispatcherCacheField("RequestDispatchBindings")); ClearCache(GetDispatcherCacheField("StreamDispatchBindings")); ClearCache(GetDispatcherCacheField("GeneratedRequestInvokers")); + ClearCache(GetDispatcherCacheField("GeneratedStreamInvokers")); } /// @@ -149,4 +200,22 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests .Invoke(cache, Array.Empty()); } + /// + /// 枚举并收集当前异步流中的全部元素,便于断言 generated stream invoker 的输出。 + /// + /// 流元素类型。 + /// 待消耗的异步流。 + /// 按产出顺序收集得到的元素列表。 + private static async Task> DrainAsync(IAsyncEnumerable stream) + { + ArgumentNullException.ThrowIfNull(stream); + + var items = new List(); + await foreach (var item in stream.ConfigureAwait(false)) + { + items.Add(item); + } + + return items; + } } diff --git a/GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerProviderRegistry.cs b/GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerProviderRegistry.cs new file mode 100644 index 00000000..0159c7ce --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerProviderRegistry.cs @@ -0,0 +1,102 @@ +using System.Reflection; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Ioc; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 模拟同时提供 handler 注册与 stream invoker 元数据的 generated registry。 +/// +internal sealed class GeneratedStreamInvokerProviderRegistry : + ICqrsHandlerRegistry, + ICqrsStreamInvokerProvider, + IEnumeratesCqrsStreamInvokerDescriptors +{ + private static readonly CqrsStreamInvokerDescriptor Descriptor = new( + typeof(IStreamRequestHandler), + typeof(GeneratedStreamInvokerProviderRegistry).GetMethod( + nameof(InvokeGenerated), + BindingFlags.NonPublic | BindingFlags.Static)!); + + private static readonly CqrsStreamInvokerDescriptorEntry DescriptorEntry = new( + typeof(GeneratedStreamInvokerRequest), + typeof(int), + Descriptor); + + /// + /// 将测试流式请求处理器注册到目标服务集合。 + /// + /// 承载处理器映射的服务集合。 + /// 用于记录注册诊断的日志器。 + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(IStreamRequestHandler), + typeof(GeneratedStreamInvokerRequestHandler)); + logger.Debug( + $"Registered CQRS handler {typeof(GeneratedStreamInvokerRequestHandler).FullName} as {typeof(IStreamRequestHandler).FullName}."); + } + + /// + /// 尝试返回指定 stream request/response 类型对对应的 generated invoker 描述符。 + /// + /// 流式请求运行时类型。 + /// 流式响应元素类型。 + /// 命中时返回的描述符。 + /// 若类型对匹配当前测试流式请求则返回 + public bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsStreamInvokerDescriptor? descriptor) + { + if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int)) + { + descriptor = Descriptor; + return true; + } + + descriptor = null; + return false; + } + + /// + /// 返回当前 registry 暴露的全部 generated stream invoker 描述符。 + /// + /// 单条测试 stream invoker 描述符条目。 + public IReadOnlyList GetDescriptors() + { + return [DescriptorEntry]; + } + + /// + /// 模拟 generated stream invoker 直接执行后的返回值。 + /// + /// 当前流式请求处理器实例。 + /// 当前测试流式请求。 + /// 取消令牌。 + /// 带有 generated 语义的异步流,便于断言 dispatcher 走了 provider 路径。 + private static object InvokeGenerated(object handler, object request, CancellationToken cancellationToken) + { + _ = handler as IStreamRequestHandler + ?? throw new InvalidOperationException("Generated stream invoker received an incompatible handler instance."); + var typedRequest = (GeneratedStreamInvokerRequest)request; + return StreamResultsAsync(typedRequest.Start, cancellationToken); + } + + /// + /// 构造供测试断言使用的固定异步流结果。 + /// + private static async IAsyncEnumerable StreamResultsAsync( + int start, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + yield return start * 10; + await Task.Yield(); + cancellationToken.ThrowIfCancellationRequested(); + yield return start * 10 + 1; + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerRequest.cs b/GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerRequest.cs new file mode 100644 index 00000000..71c6fc56 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerRequest.cs @@ -0,0 +1,9 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 用于验证 generated stream invoker provider 接线的测试流式请求。 +/// +/// 用于构造 generated stream 输出的起始值。 +internal sealed record GeneratedStreamInvokerRequest(int Start) : IStreamRequest; diff --git a/GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerRequestHandler.cs b/GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerRequestHandler.cs new file mode 100644 index 00000000..f60a84ff --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerRequestHandler.cs @@ -0,0 +1,34 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 供 generated stream invoker provider 测试使用的流式请求处理器。 +/// +internal sealed class GeneratedStreamInvokerRequestHandler : IStreamRequestHandler +{ + /// + /// 返回带有运行时处理器语义的异步流,便于和 generated invoker 自定义结果区分。 + /// + /// 当前测试流式请求。 + /// 取消令牌。 + /// 运行时处理器生成的异步流结果。 + public IAsyncEnumerable Handle(GeneratedStreamInvokerRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + return StreamResultsAsync(request.Start, cancellationToken); + } + + /// + /// 生成用于区分 runtime 路径的固定异步流结果。 + /// + private static async IAsyncEnumerable StreamResultsAsync( + int start, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + yield return start; + await Task.Yield(); + cancellationToken.ThrowIfCancellationRequested(); + yield return start + 1; + } +} diff --git a/GFramework.Cqrs/CqrsStreamInvokerDescriptor.cs b/GFramework.Cqrs/CqrsStreamInvokerDescriptor.cs new file mode 100644 index 00000000..4f9f67e1 --- /dev/null +++ b/GFramework.Cqrs/CqrsStreamInvokerDescriptor.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs; + +/// +/// 描述单个 stream request/response 类型对在运行时建流时需要复用的元数据。 +/// +/// 当前流式请求处理器在容器中的服务类型。 +/// +/// 执行单个流式请求处理器的开放静态方法。 +/// dispatcher 会在首次创建 stream binding 时,把该方法绑定成内部使用的调用委托。 +/// +/// +/// dispatcher 仍会负责上下文注入; +/// 该描述符只前移流式请求处理器服务类型与直接调用方法元数据。 +/// +public sealed class CqrsStreamInvokerDescriptor( + Type handlerType, + MethodInfo invokerMethod) +{ + /// + /// 获取流式请求处理器在容器中的服务类型。 + /// + public Type HandlerType { get; } = handlerType ?? throw new ArgumentNullException(nameof(handlerType)); + + /// + /// 获取执行流式请求处理器的开放静态方法。 + /// + public MethodInfo InvokerMethod { get; } = invokerMethod ?? throw new ArgumentNullException(nameof(invokerMethod)); +} diff --git a/GFramework.Cqrs/CqrsStreamInvokerDescriptorEntry.cs b/GFramework.Cqrs/CqrsStreamInvokerDescriptorEntry.cs new file mode 100644 index 00000000..ffff9fb8 --- /dev/null +++ b/GFramework.Cqrs/CqrsStreamInvokerDescriptorEntry.cs @@ -0,0 +1,12 @@ +namespace GFramework.Cqrs; + +/// +/// 描述单个 stream request/response 类型对与其 generated invoker 元数据之间的映射条目。 +/// +/// 流式请求运行时类型。 +/// 流式响应元素类型。 +/// 对应的 generated stream invoker 描述符。 +public sealed record CqrsStreamInvokerDescriptorEntry( + Type RequestType, + Type ResponseType, + CqrsStreamInvokerDescriptor Descriptor); diff --git a/GFramework.Cqrs/ICqrsStreamInvokerProvider.cs b/GFramework.Cqrs/ICqrsStreamInvokerProvider.cs new file mode 100644 index 00000000..2a2d907e --- /dev/null +++ b/GFramework.Cqrs/ICqrsStreamInvokerProvider.cs @@ -0,0 +1,33 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs; + +/// +/// 定义由源码生成器或手写注册器提供的 stream invoker 元数据契约。 +/// +/// +/// 该 seam 允许运行时在首次创建 stream dispatch binding 时, +/// 直接复用编译期已知的流式请求/响应类型映射,而不是总是通过反射闭合泛型方法生成调用委托。 +/// 当当前程序集没有提供匹配项时,dispatcher 仍会回退到既有的反射 binding 创建路径。 +/// 当前默认 runtime 通过 在注册阶段一次性读取并缓存 +/// provider 暴露的描述符; +/// 主要用于 provider 自检、测试和显式调用场景,而不是 dispatcher 在建流热路径上的二次回调入口。 +/// +public interface ICqrsStreamInvokerProvider +{ + /// + /// 尝试为指定流式请求/响应类型对提供运行时元数据。 + /// + /// 流式请求运行时类型。 + /// 流式响应元素类型。 + /// 命中时返回的 stream invoker 元数据。 + /// 若当前 provider 可处理该流式请求/响应类型对则返回 ;否则返回 + /// + /// 若 provider 希望被默认 runtime 自动接线到 dispatcher 的 generated invoker 缓存中, + /// 还必须同时实现 ,以便 registrar 在注册阶段枚举全部描述符。 + /// + bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsStreamInvokerDescriptor? descriptor); +} diff --git a/GFramework.Cqrs/IEnumeratesCqrsStreamInvokerDescriptors.cs b/GFramework.Cqrs/IEnumeratesCqrsStreamInvokerDescriptors.cs new file mode 100644 index 00000000..df849140 --- /dev/null +++ b/GFramework.Cqrs/IEnumeratesCqrsStreamInvokerDescriptors.cs @@ -0,0 +1,18 @@ +namespace GFramework.Cqrs; + +/// +/// 为 generated stream invoker provider 暴露可枚举描述符集合的内部辅助契约。 +/// +/// +/// registrar 在激活 generated registry 后,会通过该接口读取当前程序集声明的 stream invoker 描述符, +/// 并把它们登记到 dispatcher 的进程级弱缓存中。 +/// 该接口不改变公开分发语义,只服务于 generated invoker 元数据的运行时接线。 +/// +public interface IEnumeratesCqrsStreamInvokerDescriptors +{ + /// + /// 返回当前 provider 可声明的全部 stream invoker 描述符条目。 + /// + /// 按 provider 定义顺序枚举的描述符条目集合。 + IReadOnlyList GetDescriptors(); +} diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index a88be732..17cbb67a 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -23,6 +23,11 @@ internal sealed class CqrsDispatcher( private static readonly WeakTypePairCache GeneratedRequestInvokers = new(); + // 卸载安全的进程级缓存:当 generated registry 提供 stream invoker 元数据时, + // registrar 会按流式请求/响应类型对把它们写入这里;若类型被卸载,条目会自然失效。 + private static readonly WeakTypePairCache + GeneratedStreamInvokers = new(); + // 卸载安全的进程级缓存:通知类型只以弱键语义保留。 // 若插件/热重载程序集中的通知类型被卸载,对应分发绑定会自然失效,下次命中时再重新计算。 private static readonly WeakKeyCache @@ -276,11 +281,62 @@ internal sealed class CqrsDispatcher( /// private static StreamDispatchBinding CreateStreamDispatchBinding(Type requestType, Type responseType) { + var generatedDescriptor = TryGetGeneratedStreamInvokerDescriptor(requestType, responseType); + if (generatedDescriptor is not null) + { + var resolvedGeneratedDescriptor = generatedDescriptor.Value; + return new StreamDispatchBinding( + resolvedGeneratedDescriptor.HandlerType, + resolvedGeneratedDescriptor.Invoker); + } + return new StreamDispatchBinding( typeof(IStreamRequestHandler<,>).MakeGenericType(requestType, responseType), CreateStreamInvoker(requestType, responseType)); } + /// + /// 尝试从容器已注册的 generated stream invoker provider 中获取指定流式请求/响应类型对的元数据。 + /// + /// 流式请求运行时类型。 + /// 流式响应元素类型。 + /// 命中时返回强类型化后的描述符;否则返回 + private static StreamInvokerDescriptor? TryGetGeneratedStreamInvokerDescriptor(Type requestType, Type responseType) + { + return GeneratedStreamInvokers.TryGetValue(requestType, responseType, out var metadata) && + metadata is not null + ? CreateStreamInvokerDescriptor(requestType, responseType, metadata) + : null; + } + + /// + /// 把 provider 返回的弱类型描述符转换为 dispatcher 内部使用的 stream invoker 描述符。 + /// + /// 流式请求运行时类型。 + /// 流式响应元素类型。 + /// provider 返回的弱类型描述符。 + /// 可直接用于创建 stream dispatch binding 的描述符。 + /// 当 provider 返回的委托签名与当前流式请求/响应类型对不匹配时抛出。 + private static StreamInvokerDescriptor CreateStreamInvokerDescriptor( + Type requestType, + Type responseType, + GeneratedStreamInvokerMetadata descriptor) + { + if (!descriptor.InvokerMethod.IsStatic) + { + throw new InvalidOperationException( + $"Generated CQRS stream invoker provider returned a non-static invoker method for request type {requestType.FullName} and response type {responseType.FullName}."); + } + + if (Delegate.CreateDelegate(typeof(StreamInvoker), descriptor.InvokerMethod) is not StreamInvoker invoker) + { + throw new InvalidOperationException( + $"Generated CQRS stream invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {responseType.FullName}."); + } + + return new StreamInvokerDescriptor(descriptor.HandlerType, invoker); + } + /// /// 生成请求处理器调用委托,避免每次发送都重复反射。 /// @@ -646,6 +702,15 @@ internal sealed class CqrsDispatcher( Type HandlerType, MethodInfo InvokerMethod); + /// + /// 记录 registrar 写入的 generated stream invoker 元数据。 + /// + /// 流式请求处理器在容器中的服务类型。 + /// 执行流式请求处理器的开放静态方法。 + private sealed record GeneratedStreamInvokerMetadata( + Type HandlerType, + MethodInfo InvokerMethod); + /// /// 保存 provider 返回的请求处理器服务类型与强类型 request invoker。 /// @@ -654,6 +719,15 @@ internal sealed class CqrsDispatcher( Type HandlerType, RequestInvoker Invoker); + /// + /// 保存 provider 返回的流式请求处理器服务类型与 stream invoker。 + /// + /// 流式请求处理器在容器中的服务类型。 + /// 执行流式请求处理器的调用委托。 + private readonly record struct StreamInvokerDescriptor( + Type HandlerType, + StreamInvoker Invoker); + /// /// 供 registrar 在 generated registry 激活后登记 request invoker 元数据。 /// @@ -677,6 +751,29 @@ internal sealed class CqrsDispatcher( descriptor.InvokerMethod)); } + /// + /// 供 registrar 在 generated registry 激活后登记 stream invoker 元数据。 + /// + /// 流式请求运行时类型。 + /// 流式响应元素类型。 + /// 要登记的 generated stream invoker 描述符。 + internal static void RegisterGeneratedStreamInvokerDescriptor( + Type requestType, + Type responseType, + CqrsStreamInvokerDescriptor descriptor) + { + ArgumentNullException.ThrowIfNull(requestType); + ArgumentNullException.ThrowIfNull(responseType); + ArgumentNullException.ThrowIfNull(descriptor); + + _ = GeneratedStreamInvokers.GetOrAdd( + requestType, + responseType, + (_, _) => new GeneratedStreamInvokerMetadata( + descriptor.HandlerType, + descriptor.InvokerMethod)); + } + /// /// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。 /// 该对象只存在于本次分发,不会跨请求保留容器解析出的实例。 diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index f6e88c1d..8284c912 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -240,6 +240,7 @@ internal static class CqrsHandlerRegistrar $"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}."); registry.Register(services, logger); RegisterGeneratedRequestInvokerProvider(services, registry, assemblyName, logger); + RegisterGeneratedStreamInvokerProvider(services, registry, assemblyName, logger); } } @@ -298,6 +299,61 @@ internal static class CqrsHandlerRegistrar } } + /// + /// 当 generated registry 同时提供 stream invoker 元数据时,把该 provider 注册到当前容器中。 + /// + /// 目标服务集合。 + /// 当前已激活的 generated registry。 + /// 当前程序集的稳定名称。 + /// 日志记录器。 + /// + /// provider 作为 registry 的附加能力注册到容器后,dispatcher 才能在首次建流时优先消费编译期生成的 invoker 元数据。 + /// 若 registry 不实现该契约,则保持现有纯反射 stream binding 创建语义。 + /// + private static void RegisterGeneratedStreamInvokerProvider( + IServiceCollection services, + ICqrsHandlerRegistry registry, + string assemblyName, + ILogger logger) + { + if (registry is not ICqrsStreamInvokerProvider provider) + return; + + RegisterGeneratedStreamInvokerDescriptors(provider, assemblyName, logger); + services.AddSingleton(typeof(ICqrsStreamInvokerProvider), provider); + logger.Debug( + $"Registered CQRS stream invoker provider {provider.GetType().FullName} for assembly {assemblyName}."); + } + + /// + /// 读取 generated stream invoker provider 中当前可见的描述符,并写入 dispatcher 的进程级弱缓存。 + /// + /// 当前已激活的 stream invoker provider。 + /// 当前程序集的稳定名称。 + /// 日志记录器。 + /// + /// 运行时当前只要求 provider 暴露可枚举的描述符集合,而不是在 dispatcher 首次命中时再回调容器。 + /// 这样 stream dispatch binding 的静态缓存创建仍然只依赖类型键,不需要依赖具体容器实例。 + /// + private static void RegisterGeneratedStreamInvokerDescriptors( + ICqrsStreamInvokerProvider provider, + string assemblyName, + ILogger logger) + { + if (provider is not IEnumeratesCqrsStreamInvokerDescriptors descriptorSource) + return; + + foreach (var descriptorEntry in descriptorSource.GetDescriptors()) + { + CqrsDispatcher.RegisterGeneratedStreamInvokerDescriptor( + descriptorEntry.RequestType, + descriptorEntry.ResponseType, + descriptorEntry.Descriptor); + logger.Debug( + $"Registered generated CQRS stream invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from assembly {assemblyName}."); + } + } + /// /// 将 generated registry 的 fallback 元数据转换为统一的注册结果,并记录下一阶段是定向补扫还是整程序集扫描。 /// diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index ad8a93f7..c22b08e6 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -2489,7 +2489,7 @@ public class CqrsHandlerRegistryGeneratorTests Assert.That( generatedSource, Does.Contain( - "public global::System.Collections.Generic.IReadOnlyList GetDescriptors()")); + "global::System.Collections.Generic.IReadOnlyList global::GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors.GetDescriptors()")); }); } @@ -2498,7 +2498,6 @@ public class CqrsHandlerRegistryGeneratorTests /// stream invoker 描述符与对应的开放静态 invoker 方法。 /// [Test] - [Ignore("Enable after generated stream invoker provider / descriptor emission lands in Phase 8.")] public void Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available() { var execution = ExecuteGenerator(StreamInvokerProviderSource); @@ -2549,7 +2548,7 @@ public class CqrsHandlerRegistryGeneratorTests Assert.That( generatedSource, Does.Contain( - "public global::System.Collections.Generic.IReadOnlyList GetDescriptors()")); + "global::System.Collections.Generic.IReadOnlyList global::GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors.GetDescriptors()")); }); } 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 64a394db..ef965277 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-067` +- 恢复点编号:`CQRS-REWRITE-RP-068` - 当前阶段:`Phase 8` - 当前焦点: - 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md` @@ -49,6 +49,17 @@ CQRS 迁移与收敛。 未命中时仍回退到既有 `MakeGenericMethod + Delegate.CreateDelegate` 路径 - `GFramework.Cqrs.Tests` 已补充 `CqrsGeneratedRequestInvokerProviderTests`,锁定 registrar 接线和 dispatcher 消费 generated invoker 的最小语义 - `GFramework.SourceGenerators.Tests` 已补充 generator 回归,锁定当 runtime 暴露新契约时,generated registry 会额外发射 request invoker provider 成员与 invoker 方法 + - 已完成一轮 `dispatch/invoker` 生成前移的最小 stream 切片: + - `GFramework.Cqrs` 新增 `ICqrsStreamInvokerProvider`、`IEnumeratesCqrsStreamInvokerDescriptors`、 + `CqrsStreamInvokerDescriptor` 与 `CqrsStreamInvokerDescriptorEntry` + - generated registry 若实现 stream invoker provider 契约,`CqrsHandlerRegistrar` 现会在激活 registry 后把 provider 注册进容器, + 并把 provider 枚举出的 stream invoker 描述符写入 dispatcher 的进程级弱缓存 + - `CqrsDispatcher` 现会在首次创建 stream dispatch binding 时优先命中 generated stream invoker 描述符; + 未命中时仍回退到既有 `MakeGenericMethod + Delegate.CreateDelegate` 流式 binding 路径 + - `GFramework.Cqrs.Tests` 已扩充 `CqrsGeneratedRequestInvokerProviderTests`,锁定 registrar 接线和 dispatcher 消费 generated stream invoker 的最小语义 + - `GFramework.SourceGenerators.Tests` 已补充 generator 回归,锁定当 runtime 暴露新契约时,generated registry 会额外发射 stream invoker provider 成员与 invoker 方法 + - `GFramework.Cqrs/README.md`、`GFramework.Cqrs.SourceGenerators/README.md`、`docs/zh-CN/core/cqrs.md` 与 + `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md` 现已同步说明 generated stream invoker 的接线与回退边界 - 已将 mixed fallback 场景进一步收敛:当 runtime 允许同一程序集声明多个 `CqrsReflectionFallbackAttribute` 实例时,generator 现会把可直接引用的 fallback handlers 与仅能按名称恢复的 fallback handlers 拆分发射 - `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出 - 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据 @@ -235,6 +246,21 @@ CQRS 迁移与收敛。 - `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 build GFramework.Core/GFramework.Core.csproj -c Release` - 结果:通过 - 备注:`0 warning / 0 error`;确认 `CqrsRuntimeModule` 接线变更未引入 `GFramework.Core` 模块构建问题 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 68ce00ac..51e5951b 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,62 @@ ## 2026-04-30 +### 阶段:generated stream invoker provider 最小落地(CQRS-REWRITE-RP-068) + +- 继续按 `gframework-batch-boot 50` 执行,基线仍为当前本地 `origin/main` +- 本轮开始前,`origin/main` 已追平到当前 `HEAD`;因此 branch diff 重新归零,主 stop condition 仍为“相对 `origin/main` 接近 `50 files`” +- 当前批次沿用上一轮 request invoker provider 的设计形状,只做 stream 路径的最小对称扩展,避免把 notification publisher seam、pipeline 或 telemetry 一并卷入 +- 本轮切片拆分: + - worker:`GFramework.Cqrs/README.md`、`docs/zh-CN/core/cqrs.md`、`docs/zh-CN/source-generators/cqrs-handler-registry-generator.md` + - worker:`GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` + - 主线程:`GFramework.Cqrs/Internal/CqrsDispatcher.cs`、`GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs`、 + `GFramework.Cqrs/*.cs` 新增 stream provider 契约、`GFramework.Cqrs.SourceGenerators/Cqrs/*`、 + `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` +- 主线程关键设计调整: + - 继续保持 dispatcher 的 stream binding 静态缓存只依赖 `requestType + responseType`,不回调具体容器实例 + - stream provider 与 request provider 一样在 registrar 注册阶段一次性枚举 descriptor,并写入 dispatcher 的进程级弱缓存 + - generated registry 同时实现 request 与 stream 两组 descriptor 枚举契约时,改用显式接口实现 `GetDescriptors()`,避免同名方法冲突 +- 已完成实现: + - `GFramework.Cqrs` 新增 `ICqrsStreamInvokerProvider`、`IEnumeratesCqrsStreamInvokerDescriptors`、 + `CqrsStreamInvokerDescriptor` 与 `CqrsStreamInvokerDescriptorEntry` + - `CqrsHandlerRegistrar` 新增 stream provider 接线与 descriptor 登记路径 + - `CqrsDispatcher` 新增 generated stream invoker 弱缓存,并在 `CreateStream(...)` 首次创建 stream binding 时优先消费 generated stream invoker 元数据 + - `CqrsHandlerRegistryGenerator` 新增 stream invoker registration 建模、descriptor 发射、显式枚举接口实现与 `InvokeStreamHandler{n}(...)` 静态桥接方法 + - `GFramework.Cqrs.Tests` 新增 `GeneratedStreamInvokerProviderRegistry`、`GeneratedStreamInvokerRequest`、`GeneratedStreamInvokerRequestHandler`,并扩充 `CqrsGeneratedRequestInvokerProviderTests` + - `GFramework.Cqrs.SourceGenerators/README.md` 额外补齐模块级 README,对齐 generated stream invoker 语义 +- worker 产出已接受: + - 文档切片已把 request / stream invoker provider 作为并列 reader-facing 语义写入公开文档 + - generator 测试切片已补齐 stream invoker provider fixture 与断言;主线程根据最终实现把 request / stream 的 `GetDescriptors()` 断言统一收敛到显式接口实现版本 + +### 验证(RP-068) + +- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"` + - 结果:通过,`4/4` passed +- `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 +- `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` + - 结果:通过 +- `git diff --name-only origin/main...HEAD | wc -l` + - 结果:通过 + - 备注:当前相对 `origin/main` 的已提交 branch diff 为 `4 files` +- `git diff --numstat origin/main...HEAD` + - 结果:通过 + - 备注:当前相对 `origin/main` 的已提交 branch diff 为 `217 changed lines` + +### 当前下一步(RP-068) + +1. 在保持 branch diff 远低于 `50 files` 阈值的前提下,继续评估下一个低风险 `dispatch/invoker` 收敛切片 +2. 优先候选仍是 notification 路径是否值得引入同类 generated invoker seam,或继续补强 request / stream provider 的公开 API 入口与诊断语义 +3. 下一批落地前先提交当前 stream provider 批次,避免未提交改动持续堆叠 + ### 阶段:generated request invoker provider 最小落地(CQRS-REWRITE-RP-067) - 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main` diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 697656cc..e9b502ed 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -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` | runtime 创建入口、generated-registry 优先级、request / stream invoker provider 协作点、targeted fallback 语义和程序集去重规则 | +| `GFramework.Cqrs` 根入口与 `Internal/` | `CqrsRuntimeFactory`、`ICqrsHandlerRegistry`、`CqrsHandlerRegistryAttribute`、`CqrsReflectionFallbackAttribute`、`ICqrsRequestInvokerProvider`、`ICqrsStreamInvokerProvider` | runtime 创建入口、generated-registry 优先级、request / stream invoker provider 协作点、targeted fallback 语义和程序集去重规则 | | `GFramework.Cqrs.SourceGenerators/Cqrs/` | `CqrsHandlerRegistryGenerator`、`RuntimeTypeReferenceSpec`、`OrderedRegistrationKind` | 生成注册器、可直接引用类型判定、mixed fallback 发射与诊断边界 | ## 继续阅读 diff --git a/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md b/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md index cce62f85..94c38765 100644 --- a/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md +++ b/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md @@ -138,6 +138,7 @@ RegisterCqrsHandlersFromAssemblies( - `GFramework.Cqrs.CqrsHandlerRegistryAttribute` - `GFramework.Cqrs.CqrsReflectionFallbackAttribute` - `GFramework.Cqrs.ICqrsRequestInvokerProvider` +- `GFramework.Cqrs.ICqrsStreamInvokerProvider` - `GFramework.Cqrs.SourceGenerators.Cqrs.CqrsHandlerRegistryGenerator` 模块族入口见: From 172c08176cdd046da04e4bbc7959a14a9b35f0c6 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:34:08 +0800 Subject: [PATCH 04/14] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E5=85=85=20hidden?= =?UTF-8?q?=20implementation=20provider=20=E5=85=83=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 hidden implementation 但 visible handler interface 的 request provider 生成断言 - 新增 hidden implementation 但 visible handler interface 的 stream provider 生成断言 --- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index c22b08e6..9b6fac2e 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1896,6 +1896,215 @@ public class CqrsHandlerRegistryGeneratorTests } """; + private const string HiddenImplementationRequestInvokerProviderSource = """ + using System; + using System.Collections.Generic; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest + { + ValueTask Handle(TRequest request, CancellationToken cancellationToken); + } + + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + public interface ICqrsRequestInvokerProvider + { + bool TryGetDescriptor(Type requestType, Type responseType, out CqrsRequestInvokerDescriptor? descriptor); + } + + public interface IEnumeratesCqrsRequestInvokerDescriptors + { + IReadOnlyList GetDescriptors(); + } + + public sealed class CqrsRequestInvokerDescriptor + { + public CqrsRequestInvokerDescriptor(Type handlerType, MethodInfo invokerMethod) { } + } + + public sealed class CqrsRequestInvokerDescriptorEntry + { + public CqrsRequestInvokerDescriptorEntry(Type requestType, Type responseType, CqrsRequestInvokerDescriptor descriptor) + { + RequestType = requestType; + ResponseType = responseType; + Descriptor = descriptor; + } + + public Type RequestType { get; } + + public Type ResponseType { get; } + + public CqrsRequestInvokerDescriptor Descriptor { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record VisibleRequest() : IRequest; + + public sealed class Container + { + private sealed class HiddenHandler : IRequestHandler + { + public ValueTask Handle(VisibleRequest request, CancellationToken cancellationToken) + { + return ValueTask.FromResult(string.Empty); + } + } + } + } + """; + + private const string HiddenImplementationStreamInvokerProviderSource = """ + using System; + using System.Collections.Generic; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + + public interface IStreamRequestHandler where TRequest : IStreamRequest + { + IAsyncEnumerable Handle(TRequest request, CancellationToken cancellationToken); + } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + public interface ICqrsStreamInvokerProvider + { + bool TryGetDescriptor(Type requestType, Type responseType, out CqrsStreamInvokerDescriptor? descriptor); + } + + public interface IEnumeratesCqrsStreamInvokerDescriptors + { + IReadOnlyList GetDescriptors(); + } + + public sealed class CqrsStreamInvokerDescriptor + { + public CqrsStreamInvokerDescriptor(Type handlerType, MethodInfo invokerMethod) { } + } + + public sealed class CqrsStreamInvokerDescriptorEntry + { + public CqrsStreamInvokerDescriptorEntry(Type requestType, Type responseType, CqrsStreamInvokerDescriptor descriptor) + { + RequestType = requestType; + ResponseType = responseType; + Descriptor = descriptor; + } + + public Type RequestType { get; } + + public Type ResponseType { get; } + + public CqrsStreamInvokerDescriptor Descriptor { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record VisibleStream() : IStreamRequest; + + public sealed class Container + { + private sealed class HiddenHandler : IStreamRequestHandler + { + public async IAsyncEnumerable Handle(VisibleStream request, CancellationToken cancellationToken) + { + yield return 1; + await Task.CompletedTask; + } + } + } + } + """; + /// /// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 /// @@ -2493,6 +2702,36 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当 handler 实现类型隐藏、但 request handler interface 仍可见时, + /// 生成器仍会发射 request invoker provider 元数据,而不是因为实现类型不可直接引用而整体退回反射路径。 + /// + [Test] + public void Emits_Request_Invoker_Provider_Metadata_For_Hidden_Implementation_With_Visible_Handler_Interface() + { + var generatedSource = RunGenerator(HiddenImplementationRequestInvokerProviderSource); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry, global::GFramework.Cqrs.ICqrsRequestInvokerProvider, global::GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors")); + Assert.That( + generatedSource, + Does.Contain( + "new global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(typeof(global::TestApp.VisibleRequest), typeof(string),")); + Assert.That( + generatedSource, + Does.Contain( + "new global::GFramework.Cqrs.CqrsRequestInvokerDescriptor(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), typeof(__GFrameworkGeneratedCqrsHandlerRegistry).GetMethod(nameof(InvokeRequestHandler0), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!)")); + Assert.That( + generatedSource, + Does.Contain( + "var typedHandler = (global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler)handler;")); + }); + } + /// /// 验证当 runtime 暴露 stream invoker provider 契约时,生成器会让 generated registry 同时发射 /// stream invoker 描述符与对应的开放静态 invoker 方法。 @@ -2552,6 +2791,36 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当 handler 实现类型隐藏、但 stream handler interface 仍可见时, + /// 生成器仍会发射 stream invoker provider 元数据,而不是放弃生成稳定的 generated invoker 桥接。 + /// + [Test] + public void Emits_Stream_Invoker_Provider_Metadata_For_Hidden_Implementation_With_Visible_Handler_Interface() + { + var generatedSource = RunGenerator(HiddenImplementationStreamInvokerProviderSource); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry, global::GFramework.Cqrs.ICqrsStreamInvokerProvider, global::GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors")); + Assert.That( + generatedSource, + Does.Contain( + "new global::GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(typeof(global::TestApp.VisibleStream), typeof(int),")); + Assert.That( + generatedSource, + Does.Contain( + "new global::GFramework.Cqrs.CqrsStreamInvokerDescriptor(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler), typeof(__GFrameworkGeneratedCqrsHandlerRegistry).GetMethod(nameof(InvokeStreamHandler0), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!)")); + Assert.That( + generatedSource, + Does.Contain( + "var typedHandler = (global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler)handler;")); + }); + } + /// /// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。 /// From eb30388267e471fb7f11e55e892086b98acd2b41 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:07:05 +0800 Subject: [PATCH 05/14] =?UTF-8?q?feat(cqrs):=20=E6=89=A9=E5=A4=A7=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=BC=8F=20invoker=20=E5=8F=91=E5=B0=84=E8=8C=83?= =?UTF-8?q?=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 扩大 request 与 stream invoker 发射范围到 reflected-implementation 注册场景 - 补充 hidden implementation 回归测试并更新 CQRS ai-plan 恢复点 --- .../CqrsHandlerRegistryGenerator.Models.cs | 4 ++- ...HandlerRegistryGenerator.SourceEmission.cs | 34 ++++++++++++++++--- .../Cqrs/CqrsHandlerRegistryGenerator.cs | 8 ++++- .../todos/cqrs-rewrite-migration-tracking.md | 9 ++++- .../traces/cqrs-rewrite-migration-trace.md | 28 +++++++++++++++ 5 files changed, 76 insertions(+), 7 deletions(-) diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs index 7edbfdac..adbe74f7 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs @@ -23,7 +23,9 @@ public sealed partial class CqrsHandlerRegistryGenerator private readonly record struct ReflectedImplementationRegistrationSpec( string HandlerInterfaceDisplayName, - string HandlerInterfaceLogName); + string HandlerInterfaceLogName, + RequestInvokerRegistrationSpec? RequestInvokerRegistration, + StreamInvokerRegistrationSpec? StreamInvokerRegistration); private readonly record struct OrderedRegistrationSpec( string HandlerInterfaceLogName, diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs index ae539d23..7a449158 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs @@ -73,7 +73,7 @@ public sealed partial class CqrsHandlerRegistryGenerator } /// - /// 从 direct handler 注册描述中提取 request invoker 发射计划。 + /// 从可直接表达 handler 接口的注册描述中提取 request invoker 发射计划。 /// /// /// 指示当前 runtime 是否同时暴露 ICqrsRequestInvokerProvider 与 @@ -81,7 +81,8 @@ public sealed partial class CqrsHandlerRegistryGenerator /// /// 已按稳定顺序整理完成的 handler 注册描述。 /// - /// 由 directRegistration.RequestInvokerRegistration 派生出的 集合。 + /// 由 direct registration 或 reflected-implementation registration 上的 + /// RequestInvokerRegistration 派生出的 集合。 /// methodIndex 与其 direct registration 的遍历顺序单调递增, /// 因而只要上游排序稳定,生成的 invoker 方法名与描述符顺序就跨运行保持稳定。 /// @@ -111,13 +112,25 @@ public sealed partial class CqrsHandlerRegistryGenerator directRegistration.HandlerInterfaceDisplayName, methodIndex++)); } + + foreach (var reflectedRegistration in registration.ReflectedImplementationRegistrations) + { + if (reflectedRegistration.RequestInvokerRegistration is not { } requestInvokerRegistration) + continue; + + builder.Add(new RequestInvokerEmissionSpec( + requestInvokerRegistration.RequestTypeDisplayName, + requestInvokerRegistration.ResponseTypeDisplayName, + reflectedRegistration.HandlerInterfaceDisplayName, + methodIndex++)); + } } return builder.ToImmutable(); } /// - /// 从 direct handler 注册描述中提取 stream invoker 发射计划。 + /// 从可直接表达 handler 接口的注册描述中提取 stream invoker 发射计划。 /// /// /// 指示当前 runtime 是否同时暴露 ICqrsStreamInvokerProvider 与 @@ -125,7 +138,8 @@ public sealed partial class CqrsHandlerRegistryGenerator /// /// 已按稳定顺序整理完成的 handler 注册描述。 /// - /// 由 directRegistration.StreamInvokerRegistration 派生出的 集合。 + /// 由 direct registration 或 reflected-implementation registration 上的 + /// StreamInvokerRegistration 派生出的 集合。 /// methodIndex 与其 direct registration 的遍历顺序单调递增, /// 因而只要上游排序稳定,生成的 invoker 方法名与描述符顺序就跨运行保持稳定。 /// @@ -151,6 +165,18 @@ public sealed partial class CqrsHandlerRegistryGenerator directRegistration.HandlerInterfaceDisplayName, methodIndex++)); } + + foreach (var reflectedRegistration in registration.ReflectedImplementationRegistrations) + { + if (reflectedRegistration.StreamInvokerRegistration is not { } streamInvokerRegistration) + continue; + + builder.Add(new StreamInvokerEmissionSpec( + streamInvokerRegistration.RequestTypeDisplayName, + streamInvokerRegistration.ResponseTypeDisplayName, + reflectedRegistration.HandlerInterfaceDisplayName, + methodIndex++)); + } } return builder.ToImmutable(); diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 80a2c5a6..db547a04 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -258,7 +258,13 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator { reflectedImplementationRegistrations.Add(new ReflectedImplementationRegistrationSpec( handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - GetLogDisplayName(handlerInterface))); + GetLogDisplayName(handlerInterface), + TryCreateRequestInvokerRegistrationSpec(handlerInterface, out var requestInvokerRegistration) + ? requestInvokerRegistration + : null, + TryCreateStreamInvokerRegistrationSpec(handlerInterface, out var streamInvokerRegistration) + ? streamInvokerRegistration + : null)); return true; } 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 ef965277..4a130c1c 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-068` +- 恢复点编号:`CQRS-REWRITE-RP-069` - 当前阶段:`Phase 8` - 当前焦点: - 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md` @@ -60,6 +60,10 @@ CQRS 迁移与收敛。 - `GFramework.SourceGenerators.Tests` 已补充 generator 回归,锁定当 runtime 暴露新契约时,generated registry 会额外发射 stream invoker provider 成员与 invoker 方法 - `GFramework.Cqrs/README.md`、`GFramework.Cqrs.SourceGenerators/README.md`、`docs/zh-CN/core/cqrs.md` 与 `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md` 现已同步说明 generated stream invoker 的接线与回退边界 + - 已完成一轮 generated invoker 发射范围补强: + - `CqrsHandlerRegistryGenerator` 现会把 generated request / stream invoker 的发射范围,从“仅 direct registration”扩大到“实现类型隐藏、但 handler interface 仍可直接表达”的 reflected-implementation registration + - 当前扩展仍刻意避开 `PreciseReflectedRegistrationSpec`,不把隐藏 request/response 类型误拉进 provider 发射,继续保持生成源码可编译边界 + - `GFramework.SourceGenerators.Tests` 已新增两条 hidden-implementation 回归,锁定 request / stream provider 在该场景下都会继续发射 descriptor 与静态 invoker 方法 - 已将 mixed fallback 场景进一步收敛:当 runtime 允许同一程序集声明多个 `CqrsReflectionFallbackAttribute` 实例时,generator 现会把可直接引用的 fallback handlers 与仅能按名称恢复的 fallback handlers 拆分发射 - `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出 - 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据 @@ -261,6 +265,9 @@ CQRS 迁移与收敛。 - `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 build GFramework.Core/GFramework.Core.csproj -c Release` - 结果:通过 - 备注:`0 warning / 0 error`;确认 `CqrsRuntimeModule` 接线变更未引入 `GFramework.Core` 模块构建问题 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 51e5951b..9a631f5f 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 @@ -58,6 +58,34 @@ 2. 优先候选仍是 notification 路径是否值得引入同类 generated invoker seam,或继续补强 request / stream provider 的公开 API 入口与诊断语义 3. 下一批落地前先提交当前 stream provider 批次,避免未提交改动持续堆叠 +### 阶段:generated invoker reflected-implementation 发射范围补强(CQRS-REWRITE-RP-069) + +- 在 `RP-068` 提交后,重新复算 branch diff,相对 `origin/main` 升至 `20 files / 1015 changed lines`,仍明显低于 `gframework-batch-boot 50` 的 stop condition,因此继续下一批 +- 本轮目标只收敛 source generator,不扩散到 runtime 或公开文档:把 generated request / stream invoker 的发射范围从“仅 direct registration”扩大到“实现类型隐藏、但 handler interface 可直接表达”的 reflected-implementation registration +- 接受只读 subagent 结论后确认: + - 现有分类阶段已经为 reflected-implementation registration 保留了 request / stream invoker registration 元数据 + - 真正缺口只在 `CreateRequestInvokerEmissions(...)` 与 `CreateStreamInvokerEmissions(...)` 仍只遍历 `DirectRegistrations` + - `PreciseReflectedRegistrationSpec` 继续排除在 provider 发射范围外,避免隐藏 request/response 类型导致生成源码不可编译 +- 主线程已完成: + - `ReflectedImplementationRegistrationSpec` 显式承载 request / stream invoker registration 元数据 + - `CreateRequestInvokerEmissions(...)` 与 `CreateStreamInvokerEmissions(...)` 现会同时消费 reflected-implementation registration + - `GFramework.SourceGenerators.Tests` 已新增 hidden-implementation + visible-interface 两条 provider 回归 +- 本轮不改 runtime:dispatcher / registrar 对 generated provider 的消费语义保持不变,变化只在 generator 愿意发射更多可安全静态表达的 descriptor + +### 验证(RP-069) + +- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Generates_Direct_Interface_Registrations_For_Hidden_Implementation_When_Handler_Interface_Is_Public|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` + - 结果:通过,`3/3` passed +- `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 + +### 当前下一步(RP-069) + +1. 提交当前 generator-only 批次,继续保持每个低风险切片可独立回滚与审查 +2. 继续评估下一个能明显降低反射占比、但不需要同时改动 runtime 语义的切片 + ### 阶段:generated request invoker provider 最小落地(CQRS-REWRITE-RP-067) - 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main` From 5a77e2fb3347115367e3423583010ad40c4b01e0 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:13:09 +0800 Subject: [PATCH 06/14] =?UTF-8?q?test(cqrs-tests):=20=E8=A1=A5=E5=85=85=20?= =?UTF-8?q?hidden=20implementation=20generated=20invoker=20=E5=9B=9E?= =?UTF-8?q?=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 hidden implementation request provider runtime 集成回归,验证 registrar 与 dispatcher 会继续消费 generated metadata - 新增 hidden implementation stream provider runtime 集成回归,覆盖可见 handler interface 下的流式 dispatch 路径 - 补充对应测试替身 registry 与隐藏 handler 容器,保持现有 generated invoker 测试风格 --- ...qrsGeneratedRequestInvokerProviderTests.cs | 106 ++++++++++++++++++ ...GeneratedRequestInvokerProviderRegistry.cs | 93 +++++++++++++++ ...nGeneratedStreamInvokerProviderRegistry.cs | 105 +++++++++++++++++ ...enImplementationRequestInvokerContainer.cs | 38 +++++++ ...denImplementationStreamInvokerContainer.cs | 51 +++++++++ 5 files changed, 393 insertions(+) create mode 100644 GFramework.Cqrs.Tests/Cqrs/HiddenImplementationGeneratedRequestInvokerProviderRegistry.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/HiddenImplementationGeneratedStreamInvokerProviderRegistry.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/HiddenImplementationRequestInvokerContainer.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/HiddenImplementationStreamInvokerContainer.cs diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs index b49e9d86..2d2725b2 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs @@ -57,6 +57,25 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Is.EqualTo([typeof(GeneratedRequestInvokerProviderRegistry)])); } + /// + /// 验证当实现类型隐藏、但 request handler interface 仍可直接表达时, + /// registrar 仍会把 generated request invoker provider 注册到容器中。 + /// + [Test] + public void RegisterHandlers_Should_Register_Generated_Request_Invoker_Provider_For_Hidden_Implementation() + { + var generatedAssembly = CreateHiddenImplementationGeneratedRequestInvokerAssembly(); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + + var providers = container.GetAll(); + + Assert.That( + providers.Select(static provider => provider.GetType()), + Is.EqualTo([typeof(HiddenImplementationGeneratedRequestInvokerProviderRegistry)])); + } + /// /// 验证 registrar 激活 generated registry 后,会把 stream invoker provider 注册到容器中。 /// @@ -75,6 +94,25 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Is.EqualTo([typeof(GeneratedStreamInvokerProviderRegistry)])); } + /// + /// 验证当实现类型隐藏、但 stream handler interface 仍可直接表达时, + /// registrar 仍会把 generated stream invoker provider 注册到容器中。 + /// + [Test] + public void RegisterHandlers_Should_Register_Generated_Stream_Invoker_Provider_For_Hidden_Implementation() + { + var generatedAssembly = CreateHiddenImplementationGeneratedStreamInvokerAssembly(); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + + var providers = container.GetAll(); + + Assert.That( + providers.Select(static provider => provider.GetType()), + Is.EqualTo([typeof(HiddenImplementationGeneratedStreamInvokerProviderRegistry)])); + } + /// /// 验证 dispatcher 在首次创建 request binding 时,会优先消费 generated request invoker provider。 /// @@ -92,6 +130,25 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Assert.That(response, Is.EqualTo("generated:payload")); } + /// + /// 验证当实现类型隐藏、但 request handler interface 仍可直接表达时, + /// dispatcher 仍会消费 generated request invoker descriptor。 + /// + [Test] + public async Task SendAsync_Should_Use_Generated_Request_Invoker_For_Hidden_Implementation_When_Provider_Is_Registered() + { + var generatedAssembly = CreateHiddenImplementationGeneratedRequestInvokerAssembly(); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + container.Freeze(); + + var context = new ArchitectureContext(container); + var response = await context.SendRequestAsync( + new HiddenImplementationRequestInvokerContainer.VisibleRequest("payload")); + Assert.That(response, Is.EqualTo("generated-hidden:payload")); + } + /// /// 验证 dispatcher 在首次创建 stream binding 时,会优先消费 generated stream invoker provider。 /// @@ -109,6 +166,25 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Assert.That(results, Is.EqualTo([30, 31])); } + /// + /// 验证当实现类型隐藏、但 stream handler interface 仍可直接表达时, + /// dispatcher 仍会消费 generated stream invoker descriptor。 + /// + [Test] + public async Task CreateStream_Should_Use_Generated_Stream_Invoker_For_Hidden_Implementation_When_Provider_Is_Registered() + { + var generatedAssembly = CreateHiddenImplementationGeneratedStreamInvokerAssembly(); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + container.Freeze(); + + var context = new ArchitectureContext(container); + var results = await DrainAsync( + context.CreateStream(new HiddenImplementationStreamInvokerContainer.VisibleStreamRequest(3))); + Assert.That(results, Is.EqualTo([300, 301])); + } + /// /// 创建带有 generated request invoker registry 元数据的程序集替身。 /// @@ -139,6 +215,36 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests return generatedAssembly; } + /// + /// 创建带有 hidden implementation request invoker registry 元数据的程序集替身。 + /// + private static Mock CreateHiddenImplementationGeneratedRequestInvokerAssembly() + { + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns("GFramework.Cqrs.Tests.Cqrs.HiddenGeneratedRequestInvokerAssembly, Version=1.0.0.0"); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(typeof(HiddenImplementationGeneratedRequestInvokerProviderRegistry))]); + return generatedAssembly; + } + + /// + /// 创建带有 hidden implementation stream invoker registry 元数据的程序集替身。 + /// + private static Mock CreateHiddenImplementationGeneratedStreamInvokerAssembly() + { + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns("GFramework.Cqrs.Tests.Cqrs.HiddenGeneratedStreamInvokerAssembly, Version=1.0.0.0"); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(typeof(HiddenImplementationGeneratedStreamInvokerProviderRegistry))]); + return generatedAssembly; + } + /// /// 清空 registrar 静态缓存。 /// diff --git a/GFramework.Cqrs.Tests/Cqrs/HiddenImplementationGeneratedRequestInvokerProviderRegistry.cs b/GFramework.Cqrs.Tests/Cqrs/HiddenImplementationGeneratedRequestInvokerProviderRegistry.cs new file mode 100644 index 00000000..d9091157 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/HiddenImplementationGeneratedRequestInvokerProviderRegistry.cs @@ -0,0 +1,93 @@ +using System.Reflection; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Ioc; +using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 模拟 generated registry 在实现类型隐藏、但 request handler interface 可见时,仍提供 request invoker 元数据。 +/// +internal sealed class HiddenImplementationGeneratedRequestInvokerProviderRegistry : + ICqrsHandlerRegistry, + ICqrsRequestInvokerProvider, + IEnumeratesCqrsRequestInvokerDescriptors +{ + private static readonly Type HandlerContractType = + typeof(IRequestHandler); + + private static readonly CqrsRequestInvokerDescriptor Descriptor = new( + HandlerContractType, + typeof(HiddenImplementationGeneratedRequestInvokerProviderRegistry).GetMethod( + nameof(InvokeGenerated), + BindingFlags.NonPublic | BindingFlags.Static)!); + + private static readonly CqrsRequestInvokerDescriptorEntry DescriptorEntry = new( + typeof(HiddenImplementationRequestInvokerContainer.VisibleRequest), + typeof(string), + Descriptor); + + /// + /// 通过可见 handler interface 把隐藏实现类型注册进目标服务集合,模拟 generator 的 reflected-implementation 路径。 + /// + /// 承载处理器映射的服务集合。 + /// 用于记录注册诊断的日志器。 + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + var implementationType = HiddenImplementationRequestInvokerContainer.HiddenHandlerType; + services.AddTransient(HandlerContractType, implementationType); + logger.Debug( + $"Registered CQRS handler {implementationType.FullName} as {HandlerContractType.FullName}."); + } + + /// + /// 尝试返回指定 request/response 类型对对应的 generated invoker 描述符。 + /// + /// 请求运行时类型。 + /// 响应运行时类型。 + /// 命中时返回的描述符。 + /// 若类型对匹配当前测试请求则返回 + public bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsRequestInvokerDescriptor? descriptor) + { + if (requestType == typeof(HiddenImplementationRequestInvokerContainer.VisibleRequest) + && responseType == typeof(string)) + { + descriptor = Descriptor; + return true; + } + + descriptor = null; + return false; + } + + /// + /// 返回当前 registry 暴露的全部 generated request invoker 描述符。 + /// + /// 单条 hidden implementation request invoker 描述符条目。 + public IReadOnlyList GetDescriptors() + { + return [DescriptorEntry]; + } + + /// + /// 模拟 generated request invoker 在隐藏实现类型场景下直接执行后的返回值。 + /// + /// 当前请求处理器实例。 + /// 当前测试请求。 + /// 取消令牌。 + /// 带有 hidden generated 前缀的结果,便于断言 dispatcher 命中了 generated provider 路径。 + private static ValueTask InvokeGenerated(object handler, object request, CancellationToken cancellationToken) + { + _ = handler as IRequestHandler + ?? throw new InvalidOperationException("Generated invoker received an incompatible hidden handler instance."); + var typedRequest = (HiddenImplementationRequestInvokerContainer.VisibleRequest)request; + return ValueTask.FromResult($"generated-hidden:{typedRequest.Value}"); + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/HiddenImplementationGeneratedStreamInvokerProviderRegistry.cs b/GFramework.Cqrs.Tests/Cqrs/HiddenImplementationGeneratedStreamInvokerProviderRegistry.cs new file mode 100644 index 00000000..8d14cca1 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/HiddenImplementationGeneratedStreamInvokerProviderRegistry.cs @@ -0,0 +1,105 @@ +using System.Reflection; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Ioc; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 模拟 generated registry 在实现类型隐藏、但 stream handler interface 可见时,仍提供 stream invoker 元数据。 +/// +internal sealed class HiddenImplementationGeneratedStreamInvokerProviderRegistry : + ICqrsHandlerRegistry, + ICqrsStreamInvokerProvider, + IEnumeratesCqrsStreamInvokerDescriptors +{ + private static readonly Type HandlerContractType = + typeof(IStreamRequestHandler); + + private static readonly CqrsStreamInvokerDescriptor Descriptor = new( + HandlerContractType, + typeof(HiddenImplementationGeneratedStreamInvokerProviderRegistry).GetMethod( + nameof(InvokeGenerated), + BindingFlags.NonPublic | BindingFlags.Static)!); + + private static readonly CqrsStreamInvokerDescriptorEntry DescriptorEntry = new( + typeof(HiddenImplementationStreamInvokerContainer.VisibleStreamRequest), + typeof(int), + Descriptor); + + /// + /// 通过可见 stream handler interface 把隐藏实现类型注册进目标服务集合,模拟 generator 的 reflected-implementation 路径。 + /// + /// 承载处理器映射的服务集合。 + /// 用于记录注册诊断的日志器。 + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + var implementationType = HiddenImplementationStreamInvokerContainer.HiddenHandlerType; + services.AddTransient(HandlerContractType, implementationType); + logger.Debug( + $"Registered CQRS handler {implementationType.FullName} as {HandlerContractType.FullName}."); + } + + /// + /// 尝试返回指定 stream request/response 类型对对应的 generated invoker 描述符。 + /// + /// 流式请求运行时类型。 + /// 流式响应元素类型。 + /// 命中时返回的描述符。 + /// 若类型对匹配当前测试流式请求则返回 + public bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsStreamInvokerDescriptor? descriptor) + { + if (requestType == typeof(HiddenImplementationStreamInvokerContainer.VisibleStreamRequest) + && responseType == typeof(int)) + { + descriptor = Descriptor; + return true; + } + + descriptor = null; + return false; + } + + /// + /// 返回当前 registry 暴露的全部 generated stream invoker 描述符。 + /// + /// 单条 hidden implementation stream invoker 描述符条目。 + public IReadOnlyList GetDescriptors() + { + return [DescriptorEntry]; + } + + /// + /// 模拟 generated stream invoker 在隐藏实现类型场景下直接执行后的返回值。 + /// + /// 当前流式请求处理器实例。 + /// 当前测试流式请求。 + /// 取消令牌。 + /// 带有 hidden generated 语义的异步流,便于断言 dispatcher 命中了 generated provider 路径。 + private static object InvokeGenerated(object handler, object request, CancellationToken cancellationToken) + { + _ = handler as IStreamRequestHandler + ?? throw new InvalidOperationException("Generated stream invoker received an incompatible hidden handler instance."); + var typedRequest = (HiddenImplementationStreamInvokerContainer.VisibleStreamRequest)request; + return StreamResultsAsync(typedRequest.Start, cancellationToken); + } + + /// + /// 构造供测试断言使用的固定异步流结果。 + /// + private static async IAsyncEnumerable StreamResultsAsync( + int start, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + yield return start * 100; + await Task.Yield(); + cancellationToken.ThrowIfCancellationRequested(); + yield return start * 100 + 1; + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/HiddenImplementationRequestInvokerContainer.cs b/GFramework.Cqrs.Tests/Cqrs/HiddenImplementationRequestInvokerContainer.cs new file mode 100644 index 00000000..0dce0e9a --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/HiddenImplementationRequestInvokerContainer.cs @@ -0,0 +1,38 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 为 hidden implementation request invoker 回归提供“可见请求 + 隐藏实现类型”的测试替身容器。 +/// +internal static class HiddenImplementationRequestInvokerContainer +{ + /// + /// 用于验证 generated request invoker metadata 在隐藏实现类型场景下仍可被 dispatcher 消费的请求。 + /// + /// 用于断言 generated 返回值的请求负载。 + internal sealed record VisibleRequest(string Value) : IRequest; + + /// + /// 供 registrar 通过可见 handler interface 注册、但自身保持隐藏的 request handler 实现。 + /// + private sealed class HiddenHandler : IRequestHandler + { + /// + /// 返回 runtime 路径专用结果,便于与 generated invoker 路径区分。 + /// + /// 当前测试请求。 + /// 取消令牌。 + /// runtime handler 生成的响应字符串。 + public ValueTask Handle(VisibleRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + return ValueTask.FromResult($"runtime-hidden:{request.Value}"); + } + } + + /// + /// 返回当前隐藏 request handler 实现类型,供 generated registry 以反射注册语义模拟 hidden implementation 场景。 + /// + internal static Type HiddenHandlerType => typeof(HiddenHandler); +} diff --git a/GFramework.Cqrs.Tests/Cqrs/HiddenImplementationStreamInvokerContainer.cs b/GFramework.Cqrs.Tests/Cqrs/HiddenImplementationStreamInvokerContainer.cs new file mode 100644 index 00000000..088eb722 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/HiddenImplementationStreamInvokerContainer.cs @@ -0,0 +1,51 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 为 hidden implementation stream invoker 回归提供“可见请求 + 隐藏实现类型”的测试替身容器。 +/// +internal static class HiddenImplementationStreamInvokerContainer +{ + /// + /// 用于验证 generated stream invoker metadata 在隐藏实现类型场景下仍可被 dispatcher 消费的流式请求。 + /// + /// 用于构造 generated stream 输出的起始值。 + internal sealed record VisibleStreamRequest(int Start) : IStreamRequest; + + /// + /// 供 registrar 通过可见 stream handler interface 注册、但自身保持隐藏的流式 handler 实现。 + /// + private sealed class HiddenHandler : IStreamRequestHandler + { + /// + /// 返回 runtime 路径专用异步流,便于与 generated invoker 路径区分。 + /// + /// 当前测试流式请求。 + /// 取消令牌。 + /// runtime handler 生成的异步流结果。 + public IAsyncEnumerable Handle(VisibleStreamRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + return StreamResultsAsync(request.Start, cancellationToken); + } + + /// + /// 生成用于区分 runtime 路径的固定异步流结果。 + /// + private static async IAsyncEnumerable StreamResultsAsync( + int start, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + yield return start; + await Task.Yield(); + cancellationToken.ThrowIfCancellationRequested(); + yield return start + 1; + } + } + + /// + /// 返回当前隐藏 stream handler 实现类型,供 generated registry 以反射注册语义模拟 hidden implementation 场景。 + /// + internal static Type HiddenHandlerType => typeof(HiddenHandler); +} From 6b5c5d9e2d6016172b3b6e1a5109a583804bafcd Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:37:12 +0800 Subject: [PATCH 07/14] =?UTF-8?q?docs(cqrs):=20=E6=9B=B4=E6=96=B0=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=BC=8F=20invoker=20=E6=81=A2=E5=A4=8D=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 CQRS 重写跟踪到 RP-070,补充 hidden-implementation generated invoker runtime 回归说明 - 补充本轮定向验证结果与当前相对 origin/main 的 branch diff 指标 --- .../todos/cqrs-rewrite-migration-tracking.md | 14 +++++++--- .../traces/cqrs-rewrite-migration-trace.md | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) 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 4a130c1c..c450560b 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-069` +- 恢复点编号:`CQRS-REWRITE-RP-070` - 当前阶段:`Phase 8` - 当前焦点: - 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md` @@ -64,6 +64,11 @@ CQRS 迁移与收敛。 - `CqrsHandlerRegistryGenerator` 现会把 generated request / stream invoker 的发射范围,从“仅 direct registration”扩大到“实现类型隐藏、但 handler interface 仍可直接表达”的 reflected-implementation registration - 当前扩展仍刻意避开 `PreciseReflectedRegistrationSpec`,不把隐藏 request/response 类型误拉进 provider 发射,继续保持生成源码可编译边界 - `GFramework.SourceGenerators.Tests` 已新增两条 hidden-implementation 回归,锁定 request / stream provider 在该场景下都会继续发射 descriptor 与静态 invoker 方法 + - 已完成一轮 hidden-implementation generated invoker runtime 回归补强: + - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 现覆盖“实现类型隐藏、但 handler interface 可见”场景下的 generated request / stream invoker 消费路径 + - `HiddenImplementationGeneratedRequestInvokerProviderRegistry`、`HiddenImplementationGeneratedStreamInvokerProviderRegistry` 与对应 container / handler fixture 现锁定 registrar 接线后,dispatcher 会优先命中 generated descriptor,而不是退回反射 invoker + - 当前 runtime 回归继续保持 `PreciseReflectedRegistrationSpec` 排除边界不变,只验证已允许发射 provider 元数据的 visible-interface hidden-implementation 场景 + - 当前相对 `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 元数据的组合输出 - 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据 @@ -268,6 +273,9 @@ CQRS 迁移与收敛。 - `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.Core/GFramework.Core.csproj -c Release` - 结果:通过 - 备注:`0 warning / 0 error`;确认 `CqrsRuntimeModule` 接线变更未引入 `GFramework.Core` 模块构建问题 @@ -301,6 +309,6 @@ CQRS 迁移与收敛。 ## 下一步 -1. 基于已落地的 notification publisher seam,评估是否需要第二阶段公开配置面、并行 publisher 或 telemetry decorator -2. 基于已落地的 request invoker provider,评估是否继续把 notification / stream 的 invoker 也前移,或先补 provider 发现/诊断与文档入口 +1. 在保持 branch diff 明显低于 `50 files` 的前提下,继续挑选下一批低风险 `dispatch/invoker` 收敛切片,并优先考虑 request / stream provider 的诊断、入口或测试补强 +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 9a631f5f..df1b4a7c 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,33 @@ ## 2026-04-30 +### 阶段:hidden-implementation generated invoker runtime 回归补强(CQRS-REWRITE-RP-070) + +- 在 `5a77e2fb` 提交后补齐 active `ai-plan` 恢复入口,继续按 `gframework-batch-boot 50` 执行,基线仍为当前本地 `origin/main` +- 当前已提交 branch diff 复算为 `24 files / 1754 changed lines`,仍低于主要 stop condition,因此本轮只补 runtime 回归与恢复点,不改 generator / runtime 生产实现 +- 本轮关键目标是把 `RP-069` 已落地的 hidden-implementation provider 发射范围补强,继续向 runtime 消费侧闭环,避免 active tracking 只记录了 generator 侧验证 +- 主线程已完成: + - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 新增 hidden-implementation + visible-interface 的 request / stream runtime 回归 + - `HiddenImplementationGeneratedRequestInvokerProviderRegistry`、`HiddenImplementationGeneratedStreamInvokerProviderRegistry` 与对应 container fixture 已被纳入同一组 provider 消费测试,锁定 registrar 接线与 dispatcher 优先命中 generated descriptor 的语义 + - 当前测试仍保持 `PreciseReflectedRegistrationSpec` 排除边界不变,不把隐藏 request/response 类型场景错误抬升为 runtime 支持承诺 + +### 验证(RP-070) + +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"` + - 结果:通过,`8/8` passed +- `git diff --name-only origin/main...HEAD | wc -l` + - 结果:通过 + - 备注:当前相对 `origin/main` 的已提交 branch diff 为 `24 files` +- `git diff --numstat origin/main...HEAD` + - 结果:通过 + - 备注:当前相对 `origin/main` 的已提交 branch diff 为 `1754 changed lines` + +### 当前下一步(RP-070) + +1. 先提交本轮 `ai-plan` 恢复点更新,保持 batch 追踪与已提交代码状态一致 +2. 在剩余 headroom 内继续选择下一批低风险 `dispatch/invoker` 收敛切片,优先考虑 request / stream provider 的诊断、入口或测试补强 +3. 如下一批写集仍可拆分,再用只读 / 写入 subagent 分离非冲突切片,继续降低主线程上下文压力 + ### 阶段:generated stream invoker provider 最小落地(CQRS-REWRITE-RP-068) - 继续按 `gframework-batch-boot 50` 执行,基线仍为当前本地 `origin/main` From dc21188c7939c7962ba035f4c381e942642b04cf Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:44:51 +0800 Subject: [PATCH 08/14] =?UTF-8?q?test(cqrs):=20=E9=94=81=E5=AE=9A=20precis?= =?UTF-8?q?e=20reflected=20provider=20=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 request 与 stream generator 回归,明确 precise reflected 注册不会发射 invoker provider 元数据 - 更新 CQRS 重写恢复点到 RP-071,并记录本轮验证与边界结论 --- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 257 ++++++++++++++++++ .../todos/cqrs-rewrite-migration-tracking.md | 16 +- .../traces/cqrs-rewrite-migration-trace.md | 33 +++ 3 files changed, 304 insertions(+), 2 deletions(-) diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 9b6fac2e..45de1b58 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -2105,6 +2105,219 @@ public class CqrsHandlerRegistryGeneratorTests } """; + private const string PreciseReflectedRequestInvokerProviderBoundarySource = """ + using System; + using System.Collections.Generic; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest + { + ValueTask Handle(TRequest request, CancellationToken cancellationToken); + } + + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + public interface ICqrsRequestInvokerProvider + { + bool TryGetDescriptor(Type requestType, Type responseType, out CqrsRequestInvokerDescriptor? descriptor); + } + + public interface IEnumeratesCqrsRequestInvokerDescriptors + { + IReadOnlyList GetDescriptors(); + } + + public sealed class CqrsRequestInvokerDescriptor + { + public CqrsRequestInvokerDescriptor(Type handlerType, MethodInfo invokerMethod) { } + } + + public sealed class CqrsRequestInvokerDescriptorEntry + { + public CqrsRequestInvokerDescriptorEntry(Type requestType, Type responseType, CqrsRequestInvokerDescriptor descriptor) + { + RequestType = requestType; + ResponseType = responseType; + Descriptor = descriptor; + } + + public Type RequestType { get; } + + public Type ResponseType { get; } + + public CqrsRequestInvokerDescriptor Descriptor { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed class Container + { + private sealed record HiddenResponse(); + + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenHandler : IRequestHandler + { + public ValueTask Handle(HiddenRequest request, CancellationToken cancellationToken) + { + return ValueTask.FromResult(Array.Empty()); + } + } + } + } + """; + + private const string PreciseReflectedStreamInvokerProviderBoundarySource = """ + using System; + using System.Collections.Generic; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + + public interface IStreamRequestHandler where TRequest : IStreamRequest + { + IAsyncEnumerable Handle(TRequest request, CancellationToken cancellationToken); + } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + public interface ICqrsStreamInvokerProvider + { + bool TryGetDescriptor(Type requestType, Type responseType, out CqrsStreamInvokerDescriptor? descriptor); + } + + public interface IEnumeratesCqrsStreamInvokerDescriptors + { + IReadOnlyList GetDescriptors(); + } + + public sealed class CqrsStreamInvokerDescriptor + { + public CqrsStreamInvokerDescriptor(Type handlerType, MethodInfo invokerMethod) { } + } + + public sealed class CqrsStreamInvokerDescriptorEntry + { + public CqrsStreamInvokerDescriptorEntry(Type requestType, Type responseType, CqrsStreamInvokerDescriptor descriptor) + { + RequestType = requestType; + ResponseType = responseType; + Descriptor = descriptor; + } + + public Type RequestType { get; } + + public Type ResponseType { get; } + + public CqrsStreamInvokerDescriptor Descriptor { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed class Container + { + private sealed record HiddenResponse(); + + private sealed record HiddenStream() : IStreamRequest; + + private sealed class HiddenHandler : IStreamRequestHandler + { + public async IAsyncEnumerable Handle(HiddenStream request, CancellationToken cancellationToken) + { + yield return Array.Empty(); + await Task.CompletedTask; + } + } + } + } + """; + /// /// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 /// @@ -2732,6 +2945,28 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当 request handler 仍需走 precise reflected 注册时, + /// 生成器即使检测到 request invoker provider runtime 合同,也不会错误发射无法稳定表达隐藏请求/响应类型的 provider 元数据。 + /// + [Test] + public void Does_Not_Emit_Request_Invoker_Provider_Metadata_For_Precise_Reflected_Request_Registrations() + { + var generatedSource = RunGenerator(PreciseReflectedRequestInvokerProviderBoundarySource); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(")); + Assert.That(generatedSource, Does.Contain(".MakeArrayType()")); + Assert.That(generatedSource, Does.Not.Contain("ICqrsRequestInvokerProvider")); + Assert.That(generatedSource, Does.Not.Contain("IEnumeratesCqrsRequestInvokerDescriptors")); + Assert.That(generatedSource, Does.Not.Contain("CqrsRequestInvokerDescriptorEntry(")); + Assert.That(generatedSource, Does.Not.Contain("InvokeRequestHandler0")); + }); + } + /// /// 验证当 runtime 暴露 stream invoker provider 契约时,生成器会让 generated registry 同时发射 /// stream invoker 描述符与对应的开放静态 invoker 方法。 @@ -2821,6 +3056,28 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当 stream handler 仍需走 precise reflected 注册时, + /// 生成器即使检测到 stream invoker provider runtime 合同,也不会错误发射无法稳定表达隐藏请求/响应类型的 provider 元数据。 + /// + [Test] + public void Does_Not_Emit_Stream_Invoker_Provider_Metadata_For_Precise_Reflected_Stream_Registrations() + { + var generatedSource = RunGenerator(PreciseReflectedStreamInvokerProviderBoundarySource); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<,>).MakeGenericType(")); + Assert.That(generatedSource, Does.Contain(".MakeArrayType()")); + Assert.That(generatedSource, Does.Not.Contain("ICqrsStreamInvokerProvider")); + Assert.That(generatedSource, Does.Not.Contain("IEnumeratesCqrsStreamInvokerDescriptors")); + Assert.That(generatedSource, Does.Not.Contain("CqrsStreamInvokerDescriptorEntry(")); + Assert.That(generatedSource, Does.Not.Contain("InvokeStreamHandler0")); + }); + } + /// /// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。 /// 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 c450560b..b82c7199 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-070` +- 恢复点编号:`CQRS-REWRITE-RP-071` - 当前阶段:`Phase 8` - 当前焦点: - 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md` @@ -68,6 +68,9 @@ CQRS 迁移与收敛。 - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 现覆盖“实现类型隐藏、但 handler interface 可见”场景下的 generated request / stream invoker 消费路径 - `HiddenImplementationGeneratedRequestInvokerProviderRegistry`、`HiddenImplementationGeneratedStreamInvokerProviderRegistry` 与对应 container / handler fixture 现锁定 registrar 接线后,dispatcher 会优先命中 generated descriptor,而不是退回反射 invoker - 当前 runtime 回归继续保持 `PreciseReflectedRegistrationSpec` 排除边界不变,只验证已允许发射 provider 元数据的 visible-interface hidden-implementation 场景 + - 已完成一轮 precise reflected invoker provider 合同边界回归: + - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 现新增 request / stream 两条回归,明确当 handler 仍需走 `PreciseReflectedRegistrationSpec` 时,generator 即使检测到 invoker provider runtime 合同,也不会错误发射 descriptor、枚举接口或静态 invoker 桥接 + - 本轮接受了一条只读 subagent 的“继续评估 precise reflected + provider 发射”候选思路,但主线程复核后确认该候选并不存在可安全放宽的 `typeof(request/response)` 子集,因此收敛为“锁定当前排除边界”的测试批次,而不是修改生产 generator 逻辑 - 当前相对 `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 元数据的组合输出 @@ -276,6 +279,15 @@ CQRS 迁移与收敛。 - `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.Core/GFramework.Core.csproj -c Release` - 结果:通过 - 备注:`0 warning / 0 error`;确认 `CqrsRuntimeModule` 接线变更未引入 `GFramework.Core` 模块构建问题 @@ -309,6 +321,6 @@ CQRS 迁移与收敛。 ## 下一步 -1. 在保持 branch diff 明显低于 `50 files` 的前提下,继续挑选下一批低风险 `dispatch/invoker` 收敛切片,并优先考虑 request / stream provider 的诊断、入口或测试补强 +1. 在保持 branch diff 明显低于 `50 files` 的前提下,继续挑选下一批低风险 `dispatch/invoker` 收敛切片,并优先考虑 request / stream provider 的诊断、入口或 runtime / generator 合同测试补强 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 df1b4a7c..6a7d04e2 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,39 @@ ## 2026-04-30 +### 阶段:precise reflected invoker provider 合同边界回归(CQRS-REWRITE-RP-071) + +- 在 `RP-070` 提交后继续按 `gframework-batch-boot 50` 执行;当前已提交 branch diff 仍为 `24 files`,headroom 充足,因此继续下一批 generator-only 合同收敛 +- 本轮先接受一条只读 subagent 的候选建议,评估是否可把 `PreciseReflectedRegistrationSpec` 的某个安全子集也纳入 request / stream provider 发射 +- 主线程复核 `TryCreatePreciseReflectedRegistration(...)`、`CreateRequestInvokerEmissions(...)` / `CreateStreamInvokerEmissions(...)` 与现有 precise 测试素材后确认: + - precise reflected 分支之所以存在,正是因为 handler interface 的请求或响应类型无法完全通过 `typeof(...)` 稳定表达 + - 当前 provider descriptor 合同需要直接发射 `typeof(requestType)` / `typeof(responseType)`;因此不存在可无条件放宽的“安全子集” + - 本轮最终不改生产 generator,而是把这条边界显式固化到回归测试,避免后续误把不存在的子集当成已支持能力 +- 主线程已完成: + - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增两条回归,分别锁定 request / stream 的 precise reflected 注册不会发射 invoker provider 元数据 + - 同一组定向测试同时复核 hidden-implementation + visible-interface 场景仍会继续发射 provider 元数据,确保“允许发射”和“继续排除”的边界没有被本轮测试收紧弄混 + +### 验证(RP-071) + +- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `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 +- `git diff --name-only origin/main...HEAD | wc -l` + - 结果:通过 + - 备注:当前相对 `origin/main` 的已提交 branch diff 仍为 `24 files` +- `git diff --numstat origin/main...HEAD` + - 结果:通过 + - 备注:当前相对 `origin/main` 的工作分支累计 diff 为 `1793 changed lines` + +### 当前下一步(RP-071) + +1. 先提交本轮 generator 合同边界回归,保持恢复点、trace 与已验证测试状态一致 +2. 继续挑选下一批低风险切片,优先考虑 request / stream provider 的 runtime 或 generator 诊断边界,而不是贸然扩大 precise reflected 支持面 +3. 若下一批仍可拆分为非冲突文件,再恢复只读 / 写入 subagent 的分工方式压低主线程上下文 ### 阶段:hidden-implementation generated invoker runtime 回归补强(CQRS-REWRITE-RP-070) - 在 `5a77e2fb` 提交后补齐 active `ai-plan` 恢复入口,继续按 `gframework-batch-boot 50` 执行,基线仍为当前本地 `origin/main` From 502f65239cb6ecde360482dad75b9b747d855cad Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:53:05 +0800 Subject: [PATCH 09/14] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E5=85=85=20provid?= =?UTF-8?q?er=20gate=20=E5=90=88=E5=90=8C=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 request 与 stream gate 回归,锁定 runtime 合同不完整时不会发射 invoker provider 元数据 - 更新 CQRS 重写恢复点到 RP-072,并记录定向验证与 helper 收敛 --- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 129 ++++++++++++++++++ .../todos/cqrs-rewrite-migration-tracking.md | 13 +- .../traces/cqrs-rewrite-migration-trace.md | 23 ++++ 3 files changed, 163 insertions(+), 2 deletions(-) diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 45de1b58..1254e273 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -2945,6 +2945,56 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当 runtime 缺少 ICqrsRequestInvokerProvider 时, + /// 生成器会整体跳过 request invoker provider 元数据发射,而不是输出半套 descriptor 成员。 + /// + [Test] + public void Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Provider_Interface() + { + var generatedSource = RunGenerator(RemoveBlock( + RequestInvokerProviderSource, + "public interface ICqrsRequestInvokerProvider", + "public interface IEnumeratesCqrsRequestInvokerDescriptors")); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry")); + Assert.That(generatedSource, Does.Not.Contain("ICqrsRequestInvokerProvider")); + Assert.That(generatedSource, Does.Not.Contain("IEnumeratesCqrsRequestInvokerDescriptors")); + Assert.That(generatedSource, Does.Not.Contain("CqrsRequestInvokerDescriptorEntry(")); + Assert.That(generatedSource, Does.Not.Contain("InvokeRequestHandler0")); + }); + } + + /// + /// 验证当 runtime 缺少 IEnumeratesCqrsRequestInvokerDescriptors 时, + /// 生成器不会只发射 request provider 的部分成员,而是整体保持不生成 provider 元数据。 + /// + [Test] + public void Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Descriptor_Enumerator() + { + var generatedSource = RunGenerator(RemoveBlock( + RequestInvokerProviderSource, + "public interface IEnumeratesCqrsRequestInvokerDescriptors", + "public sealed class CqrsRequestInvokerDescriptor")); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry")); + Assert.That(generatedSource, Does.Not.Contain("ICqrsRequestInvokerProvider")); + Assert.That(generatedSource, Does.Not.Contain("IEnumeratesCqrsRequestInvokerDescriptors")); + Assert.That(generatedSource, Does.Not.Contain("CqrsRequestInvokerDescriptorEntry(")); + Assert.That(generatedSource, Does.Not.Contain("InvokeRequestHandler0")); + }); + } + /// /// 验证当 request handler 仍需走 precise reflected 注册时, /// 生成器即使检测到 request invoker provider runtime 合同,也不会错误发射无法稳定表达隐藏请求/响应类型的 provider 元数据。 @@ -3056,6 +3106,56 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当 runtime 缺少 ICqrsStreamInvokerProvider 时, + /// 生成器会整体跳过 stream invoker provider 元数据发射,而不是保留孤立的 descriptor 成员。 + /// + [Test] + public void Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Provider_Interface() + { + var generatedSource = RunGenerator(RemoveBlock( + StreamInvokerProviderSource, + "public interface ICqrsStreamInvokerProvider", + "public interface IEnumeratesCqrsStreamInvokerDescriptors")); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry")); + Assert.That(generatedSource, Does.Not.Contain("ICqrsStreamInvokerProvider")); + Assert.That(generatedSource, Does.Not.Contain("IEnumeratesCqrsStreamInvokerDescriptors")); + Assert.That(generatedSource, Does.Not.Contain("CqrsStreamInvokerDescriptorEntry(")); + Assert.That(generatedSource, Does.Not.Contain("InvokeStreamHandler0")); + }); + } + + /// + /// 验证当 runtime 缺少 IEnumeratesCqrsStreamInvokerDescriptors 时, + /// 生成器不会只发射 stream provider 的局部成员,而是整体保持不生成 provider 元数据。 + /// + [Test] + public void Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Enumerator() + { + var generatedSource = RunGenerator(RemoveBlock( + StreamInvokerProviderSource, + "public interface IEnumeratesCqrsStreamInvokerDescriptors", + "public sealed class CqrsStreamInvokerDescriptor")); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry")); + Assert.That(generatedSource, Does.Not.Contain("ICqrsStreamInvokerProvider")); + Assert.That(generatedSource, Does.Not.Contain("IEnumeratesCqrsStreamInvokerDescriptors")); + Assert.That(generatedSource, Does.Not.Contain("CqrsStreamInvokerDescriptorEntry(")); + Assert.That(generatedSource, Does.Not.Contain("InvokeStreamHandler0")); + }); + } + /// /// 验证当 stream handler 仍需走 precise reflected 注册时, /// 生成器即使检测到 stream invoker provider runtime 合同,也不会错误发射无法稳定表达隐藏请求/响应类型的 provider 元数据。 @@ -3129,6 +3229,35 @@ public class CqrsHandlerRegistryGeneratorTests return execution.GeneratedSources[0].content; } + /// + /// 从测试输入源码中移除两个稳定标记之间的整段合同定义, + /// 避免回归用例依赖三引号字符串中的精确缩进。 + /// + /// 原始测试源码。 + /// 待移除代码块的起始标记。 + /// 待移除代码块之后紧邻的下一个稳定标记。 + /// 移除指定代码块后的新源码。 + private static string RemoveBlock(string source, string startMarker, string endMarker) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(startMarker); + ArgumentNullException.ThrowIfNull(endMarker); + + var startIndex = source.IndexOf(startMarker, StringComparison.Ordinal); + if (startIndex < 0) + { + throw new InvalidOperationException("The requested start marker was not found in the generator test input."); + } + + var endIndex = source.IndexOf(endMarker, startIndex, StringComparison.Ordinal); + if (endIndex < 0) + { + throw new InvalidOperationException("The requested end marker was not found in the generator test input."); + } + + return source.Remove(startIndex, endIndex - startIndex); + } + /// /// 统计生成源码中某个固定片段的出现次数,用于锁定程序集级 fallback 特性的发射个数。 /// 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 b82c7199..e0c247ec 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-071` +- 恢复点编号:`CQRS-REWRITE-RP-072` - 当前阶段:`Phase 8` - 当前焦点: - 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md` @@ -71,6 +71,9 @@ CQRS 迁移与收敛。 - 已完成一轮 precise reflected invoker provider 合同边界回归: - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 现新增 request / stream 两条回归,明确当 handler 仍需走 `PreciseReflectedRegistrationSpec` 时,generator 即使检测到 invoker provider runtime 合同,也不会错误发射 descriptor、枚举接口或静态 invoker 桥接 - 本轮接受了一条只读 subagent 的“继续评估 precise reflected + provider 发射”候选思路,但主线程复核后确认该候选并不存在可安全放宽的 `typeof(request/response)` 子集,因此收敛为“锁定当前排除边界”的测试批次,而不是修改生产 generator 逻辑 + - 已完成一轮 invoker provider gate 合同回归: + - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 现新增四条回归,分别锁定 request / stream 在缺少 `ICqrsRequestInvokerProvider`、`IEnumeratesCqrsRequestInvokerDescriptors`、`ICqrsStreamInvokerProvider` 或 `IEnumeratesCqrsStreamInvokerDescriptors` 时,generator 都会整体跳过对应 provider 元数据发射 + - 本轮最初采用固定源码片段替换来裁剪测试输入,但因三引号字符串缩进差异导致 helper 过脆;当前已收敛为按稳定起止标记移除源码块的 `RemoveBlock(...)` helper,避免 gate 回归依赖精确空格对齐 - 当前相对 `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 元数据的组合输出 @@ -288,6 +291,12 @@ CQRS 迁移与收敛。 - `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.Core/GFramework.Core.csproj -c Release` - 结果:通过 - 备注:`0 warning / 0 error`;确认 `CqrsRuntimeModule` 接线变更未引入 `GFramework.Core` 模块构建问题 @@ -321,6 +330,6 @@ CQRS 迁移与收敛。 ## 下一步 -1. 在保持 branch diff 明显低于 `50 files` 的前提下,继续挑选下一批低风险 `dispatch/invoker` 收敛切片,并优先考虑 request / stream provider 的诊断、入口或 runtime / generator 合同测试补强 +1. 在保持 branch diff 明显低于 `50 files` 的前提下,继续挑选下一批低风险 `dispatch/invoker` 收敛切片,并优先考虑 request / stream provider 的 runtime 失败边界或 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 6a7d04e2..91f372d4 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,29 @@ ## 2026-04-30 +### 阶段:invoker provider gate 合同回归(CQRS-REWRITE-RP-072) + +- 在 `RP-071` 提交后继续按 `gframework-batch-boot 50` 执行;当前 branch diff 相对 `origin/main` 仍为 `24 files`,未接近主要 stop condition,因此继续追加一轮 test-only generator 合同回归 +- 本轮接受一条只读 subagent 建议,把下一批进一步收敛为“runtime 合同不完整时不发射 provider 元数据”的单文件测试波次 +- 主线程已完成: + - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增四条 gate 回归,分别锁定 request / stream 在缺少 provider 接口或缺少 descriptor 枚举接口时,都会整体跳过元数据发射 + - 初版实现曾使用整段源码片段替换来删减测试输入,但因三引号字符串缩进差异导致 helper 匹配失败;随后改为按稳定起止标记移除源码块的 `RemoveBlock(...)` helper,使测试意图与输入格式解耦 + - 同一组定向验证同时保留 request / stream happy-path 两条既有回归,确认 gate 收紧后不会误伤原本完整合同下的 provider 发射 + +### 验证(RP-072) + +- `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 + +### 当前下一步(RP-072) + +1. 先提交本轮 generator gate 合同回归与恢复点更新 +2. 重新复算 branch diff 后,再决定是否继续推进 request / stream provider 的 runtime 失败边界测试 +3. 若继续下一批,优先保持 test-only 或极小写集,避免在接近阈值前扩散到新的生产模块 + ### 阶段:precise reflected invoker provider 合同边界回归(CQRS-REWRITE-RP-071) - 在 `RP-070` 提交后继续按 `gframework-batch-boot 50` 执行;当前已提交 branch diff 仍为 `24 files`,headroom 充足,因此继续下一批 generator-only 合同收敛 From 10915942240b7b074886746d34554d8589cf1d6e Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:59:19 +0800 Subject: [PATCH 10/14] =?UTF-8?q?fix(cqrs):=20=E6=94=B6=E6=95=9B=20generat?= =?UTF-8?q?ed=20invoker=20=E5=BC=82=E5=B8=B8=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 request 与 stream generated invoker 签名不兼容时冒出 ArgumentException 的行为,统一包装为 InvalidOperationException - 补充对应 runtime 回归测试并更新 CQRS 重写恢复点到 RP-073 --- ...qrsGeneratedRequestInvokerProviderTests.cs | 333 ++++++++++++++++++ GFramework.Cqrs/Internal/CqrsDispatcher.cs | 36 +- .../todos/cqrs-rewrite-migration-tracking.md | 14 +- .../traces/cqrs-rewrite-migration-trace.md | 25 ++ 4 files changed, 397 insertions(+), 11 deletions(-) diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs index 2d2725b2..e260b380 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs @@ -185,6 +185,318 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Assert.That(results, Is.EqualTo([300, 301])); } + /// + /// 验证当 generated request invoker provider 返回实例方法时, + /// dispatcher 会显式拒绝该描述符,而不是在后续绑定阶段静默接受非法合同。 + /// + [Test] + public void SendAsync_Should_Throw_When_Generated_Request_Invoker_Is_Not_Static() + { + var generatedAssembly = CreateGeneratedAssembly( + typeof(NonStaticRequestInvokerProviderRegistry), + "GFramework.Cqrs.Tests.Cqrs.NonStaticRequestInvokerAssembly, Version=1.0.0.0"); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + 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")); + } + + /// + /// 验证当 generated request invoker provider 返回与 dispatcher 委托签名不兼容的方法时, + /// dispatcher 会显式抛出契约错误。 + /// + [Test] + public void SendAsync_Should_Throw_When_Generated_Request_Invoker_Is_Incompatible() + { + var generatedAssembly = CreateGeneratedAssembly( + typeof(IncompatibleRequestInvokerProviderRegistry), + "GFramework.Cqrs.Tests.Cqrs.IncompatibleRequestInvokerAssembly, Version=1.0.0.0"); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + 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("incompatible invoker")); + } + + /// + /// 验证当 generated stream invoker provider 返回实例方法时, + /// dispatcher 会在首次建流时显式拒绝该描述符。 + /// + [Test] + public void CreateStream_Should_Throw_When_Generated_Stream_Invoker_Is_Not_Static() + { + var generatedAssembly = CreateGeneratedAssembly( + typeof(NonStaticStreamInvokerProviderRegistry), + "GFramework.Cqrs.Tests.Cqrs.NonStaticStreamInvokerAssembly, Version=1.0.0.0"); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + 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")); + } + + /// + /// 验证当 generated stream invoker provider 返回与 dispatcher 委托签名不兼容的方法时, + /// dispatcher 会显式抛出契约错误。 + /// + [Test] + public void CreateStream_Should_Throw_When_Generated_Stream_Invoker_Is_Incompatible() + { + var generatedAssembly = CreateGeneratedAssembly( + typeof(IncompatibleStreamInvokerProviderRegistry), + "GFramework.Cqrs.Tests.Cqrs.IncompatibleStreamInvokerAssembly, Version=1.0.0.0"); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + 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("incompatible invoker")); + } + + /// + /// 模拟返回实例 request invoker 方法的 generated registry。 + /// + private sealed class NonStaticRequestInvokerProviderRegistry : + ICqrsHandlerRegistry, + 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) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(IRequestHandler), + typeof(GeneratedRequestInvokerRequestHandler)); + } + + /// + public bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsRequestInvokerDescriptor? descriptor) + { + if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string)) + { + descriptor = DescriptorEntry.Descriptor; + return true; + } + + descriptor = null; + return false; + } + + /// + public IReadOnlyList GetDescriptors() + { + return [DescriptorEntry]; + } + + private ValueTask InvokeGenerated(object handler, object request, CancellationToken cancellationToken) + { + return ValueTask.FromResult(string.Empty); + } + } + + /// + /// 模拟返回不兼容 request invoker 方法的 generated registry。 + /// + private sealed class IncompatibleRequestInvokerProviderRegistry : + ICqrsHandlerRegistry, + ICqrsRequestInvokerProvider, + IEnumeratesCqrsRequestInvokerDescriptors + { + private static readonly CqrsRequestInvokerDescriptorEntry DescriptorEntry = new( + typeof(GeneratedRequestInvokerRequest), + typeof(string), + new CqrsRequestInvokerDescriptor( + typeof(IRequestHandler), + typeof(IncompatibleRequestInvokerProviderRegistry).GetMethod( + nameof(InvokeGenerated), + BindingFlags.NonPublic | BindingFlags.Static)!)); + + /// + 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) + { + if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string)) + { + descriptor = DescriptorEntry.Descriptor; + return true; + } + + descriptor = null; + return false; + } + + /// + public IReadOnlyList GetDescriptors() + { + return [DescriptorEntry]; + } + + private static string InvokeGenerated(object handler, object request) + { + return string.Empty; + } + } + + /// + /// 模拟返回实例 stream invoker 方法的 generated registry。 + /// + private sealed class NonStaticStreamInvokerProviderRegistry : + ICqrsHandlerRegistry, + 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) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(IStreamRequestHandler), + typeof(GeneratedStreamInvokerRequestHandler)); + } + + /// + public bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsStreamInvokerDescriptor? descriptor) + { + if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int)) + { + descriptor = DescriptorEntry.Descriptor; + return true; + } + + descriptor = null; + return false; + } + + /// + public IReadOnlyList GetDescriptors() + { + return [DescriptorEntry]; + } + + private object InvokeGenerated(object handler, object request, CancellationToken cancellationToken) + { + return Array.Empty().ToAsyncEnumerable(); + } + } + + /// + /// 模拟返回不兼容 stream invoker 方法的 generated registry。 + /// + private sealed class IncompatibleStreamInvokerProviderRegistry : + ICqrsHandlerRegistry, + ICqrsStreamInvokerProvider, + IEnumeratesCqrsStreamInvokerDescriptors + { + private static readonly CqrsStreamInvokerDescriptorEntry DescriptorEntry = new( + typeof(GeneratedStreamInvokerRequest), + typeof(int), + new CqrsStreamInvokerDescriptor( + typeof(IStreamRequestHandler), + typeof(IncompatibleStreamInvokerProviderRegistry).GetMethod( + nameof(InvokeGenerated), + BindingFlags.NonPublic | BindingFlags.Static)!)); + + /// + 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) + { + if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int)) + { + descriptor = DescriptorEntry.Descriptor; + return true; + } + + descriptor = null; + return false; + } + + /// + public IReadOnlyList GetDescriptors() + { + return [DescriptorEntry]; + } + + private static object InvokeGenerated(object handler, object request) + { + return Array.Empty().ToAsyncEnumerable(); + } + } + /// /// 创建带有 generated request invoker registry 元数据的程序集替身。 /// @@ -215,6 +527,27 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests return generatedAssembly; } + /// + /// 创建带有指定 generated registry 元数据的程序集替身。 + /// + /// 测试 registry 类型。 + /// 模拟程序集全名。 + /// 可用于 registrar 注册流程的程序集替身。 + private static Mock CreateGeneratedAssembly(Type registryType, string assemblyFullName) + { + ArgumentNullException.ThrowIfNull(registryType); + ArgumentException.ThrowIfNullOrWhiteSpace(assemblyFullName); + + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns(assemblyFullName); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(registryType)]); + return generatedAssembly; + } + /// /// 创建带有 hidden implementation request invoker registry 元数据的程序集替身。 /// diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index 17cbb67a..0fa91f76 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -256,14 +256,23 @@ internal sealed class CqrsDispatcher( $"Generated CQRS request invoker provider returned a non-static invoker method for request type {requestType.FullName} and response type {typeof(TResponse).FullName}."); } - if (Delegate.CreateDelegate(typeof(RequestInvoker), descriptor.InvokerMethod) is not - RequestInvoker invoker) + try + { + if (Delegate.CreateDelegate(typeof(RequestInvoker), descriptor.InvokerMethod) is not + RequestInvoker invoker) + { + throw new InvalidOperationException( + $"Generated CQRS request invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {typeof(TResponse).FullName}."); + } + + return new RequestInvokerDescriptor(descriptor.HandlerType, invoker); + } + catch (ArgumentException exception) { throw new InvalidOperationException( - $"Generated CQRS request invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {typeof(TResponse).FullName}."); + $"Generated CQRS request invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {typeof(TResponse).FullName}.", + exception); } - - return new RequestInvokerDescriptor(descriptor.HandlerType, invoker); } /// @@ -328,13 +337,22 @@ internal sealed class CqrsDispatcher( $"Generated CQRS stream invoker provider returned a non-static invoker method for request type {requestType.FullName} and response type {responseType.FullName}."); } - if (Delegate.CreateDelegate(typeof(StreamInvoker), descriptor.InvokerMethod) is not StreamInvoker invoker) + try + { + if (Delegate.CreateDelegate(typeof(StreamInvoker), descriptor.InvokerMethod) is not StreamInvoker invoker) + { + throw new InvalidOperationException( + $"Generated CQRS stream invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {responseType.FullName}."); + } + + return new StreamInvokerDescriptor(descriptor.HandlerType, invoker); + } + catch (ArgumentException exception) { throw new InvalidOperationException( - $"Generated CQRS stream invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {responseType.FullName}."); + $"Generated CQRS stream invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {responseType.FullName}.", + exception); } - - return new StreamInvokerDescriptor(descriptor.HandlerType, invoker); } /// 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 e0c247ec..271ff6fc 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-072` +- 恢复点编号:`CQRS-REWRITE-RP-073` - 当前阶段:`Phase 8` - 当前焦点: - 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md` @@ -74,6 +74,10 @@ CQRS 迁移与收敛。 - 已完成一轮 invoker provider gate 合同回归: - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 现新增四条回归,分别锁定 request / stream 在缺少 `ICqrsRequestInvokerProvider`、`IEnumeratesCqrsRequestInvokerDescriptors`、`ICqrsStreamInvokerProvider` 或 `IEnumeratesCqrsStreamInvokerDescriptors` 时,generator 都会整体跳过对应 provider 元数据发射 - 本轮最初采用固定源码片段替换来裁剪测试输入,但因三引号字符串缩进差异导致 helper 过脆;当前已收敛为按稳定起止标记移除源码块的 `RemoveBlock(...)` helper,避免 gate 回归依赖精确空格对齐 + - 已完成一轮 generated invoker provider runtime 失败边界修复: + - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 现新增 request / stream 两组 `non-static invoker` 与 `incompatible invoker` 回归,锁定 dispatcher 在首次绑定阶段会显式拒绝非法 generated descriptor + - `GFramework.Cqrs/Internal/CqrsDispatcher.cs` 现把 `Delegate.CreateDelegate(...)` 抛出的 `ArgumentException` 统一包装为已有 XML 文档承诺的 `InvalidOperationException`,保持 request / stream 两条错误消息语义一致 + - 本轮顺手为新增异步断言补齐 `ConfigureAwait(false)`,消除新测试引入的 `MA0004` warning - 当前相对 `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 元数据的组合输出 @@ -297,6 +301,12 @@ CQRS 迁移与收敛。 - `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.Core/GFramework.Core.csproj -c Release` - 结果:通过 - 备注:`0 warning / 0 error`;确认 `CqrsRuntimeModule` 接线变更未引入 `GFramework.Core` 模块构建问题 @@ -330,6 +340,6 @@ CQRS 迁移与收敛。 ## 下一步 -1. 在保持 branch diff 明显低于 `50 files` 的前提下,继续挑选下一批低风险 `dispatch/invoker` 收敛切片,并优先考虑 request / stream provider 的 runtime 失败边界或 generator gate 合同补强 +1. 在保持 branch diff 明显低于 `50 files` 的前提下,继续挑选下一批低风险 `dispatch/invoker` 收敛切片,并优先考虑 request / stream provider 的剩余 runtime 失败边界或 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 91f372d4..674ca8a1 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,31 @@ ## 2026-04-30 +### 阶段:generated invoker provider runtime 失败边界修复(CQRS-REWRITE-RP-073) + +- 在 `RP-072` 提交后继续按 `gframework-batch-boot 50` 执行;当前 branch diff 相对 `origin/main` 仍为 `24 files`,文件阈值 headroom 依然充足,因此继续推进下一批 runtime 失败边界回归 +- 本轮原计划只补 `CqrsGeneratedRequestInvokerProviderTests` 的 request / stream 非 happy-path 回归,但定向测试首轮直接暴露出一个真实 runtime 缺口: + - `CqrsDispatcher.CreateRequestInvokerDescriptor(...)` 与 `CreateStreamInvokerDescriptor(...)` 的 XML 文档和消息语义都承诺会抛 `InvalidOperationException` + - 实际实现先调用 `Delegate.CreateDelegate(...)`,当 invoker 签名不兼容时会直接冒出 `ArgumentException`,导致文档承诺与运行时行为不一致 +- 主线程已完成: + - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 新增 request / stream 两组 `non-static invoker` 与 `incompatible invoker` 回归,并保留 request / stream happy-path 作为同批守护断言 + - `GFramework.Cqrs/Internal/CqrsDispatcher.cs` 现对 request / stream 两条 descriptor 创建路径统一捕获 `ArgumentException`,并转换成带原有错误消息的 `InvalidOperationException` + - 新增异步断言已补齐 `ConfigureAwait(false)`,避免测试批次自身引入 `MA0004` analyzer warning + +### 验证(RP-073) + +- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` + - 结果:通过 + - 备注:并行执行 build/test 时曾出现 `MSB3026` 输出文件竞争噪音;无真实编译失败,也未引入新增 analyzer warning +- `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 + +### 当前下一步(RP-073) + +1. 先提交本轮 runtime 失败边界修复与恢复点更新 +2. 重新复算 branch diff 后,再判断是否继续推进剩余 provider 失败边界或在接近阈值前停下 +3. 若继续下一批,优先保持单文件或双文件写集,避免在本轮后段扩散 review 面积 + ### 阶段:invoker provider gate 合同回归(CQRS-REWRITE-RP-072) - 在 `RP-071` 提交后继续按 `gframework-batch-boot 50` 执行;当前 branch diff 相对 `origin/main` 仍为 `24 files`,未接近主要 stop condition,因此继续追加一轮 test-only generator 合同回归 From 8b36626266d6691eb092aaf52c794cb1297f0ac7 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:04:10 +0800 Subject: [PATCH 11/14] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E5=85=85=20provid?= =?UTF-8?q?er=20fallback=20=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 non-enumerating request 与 stream provider 回归,锁定 dispatcher 会继续回退到反射路径 - 更新 CQRS 重写恢复点到 RP-074,并记录定向验证结果 --- ...qrsGeneratedRequestInvokerProviderTests.cs | 122 ++++++++++++++++++ .../todos/cqrs-rewrite-migration-tracking.md | 13 +- .../traces/cqrs-rewrite-migration-trace.md | 21 +++ 3 files changed, 154 insertions(+), 2 deletions(-) diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs index e260b380..3c81fbfb 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs @@ -185,6 +185,46 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Assert.That(results, Is.EqualTo([300, 301])); } + /// + /// 验证当 registry 只暴露 request invoker provider 接口、但不提供可枚举描述符契约时, + /// dispatcher 仍会回退到既有反射路径,而不是错误依赖未预热的 generated metadata。 + /// + [Test] + public async Task SendAsync_Should_Fall_Back_To_Runtime_Path_When_Request_Provider_Does_Not_Enumerate_Descriptors() + { + var generatedAssembly = CreateGeneratedAssembly( + typeof(NonEnumeratingRequestInvokerProviderRegistry), + "GFramework.Cqrs.Tests.Cqrs.NonEnumeratingRequestInvokerAssembly, 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")); + } + + /// + /// 验证当 registry 只暴露 stream invoker provider 接口、但不提供可枚举描述符契约时, + /// dispatcher 仍会回退到既有流式反射路径。 + /// + [Test] + public async Task CreateStream_Should_Fall_Back_To_Runtime_Path_When_Stream_Provider_Does_Not_Enumerate_Descriptors() + { + var generatedAssembly = CreateGeneratedAssembly( + typeof(NonEnumeratingStreamInvokerProviderRegistry), + "GFramework.Cqrs.Tests.Cqrs.NonEnumeratingStreamInvokerAssembly, 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])); + } + /// /// 验证当 generated request invoker provider 返回实例方法时, /// dispatcher 会显式拒绝该描述符,而不是在后续绑定阶段静默接受非法合同。 @@ -497,6 +537,88 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests } } + /// + /// 模拟只暴露 request provider 接口、但不暴露描述符枚举契约的 generated registry。 + /// + private sealed class NonEnumeratingRequestInvokerProviderRegistry : + ICqrsHandlerRegistry, + ICqrsRequestInvokerProvider + { + private static readonly CqrsRequestInvokerDescriptor Descriptor = new( + typeof(IRequestHandler), + typeof(GeneratedRequestInvokerProviderRegistry).GetMethod( + "InvokeGenerated", + BindingFlags.NonPublic | BindingFlags.Static)!); + + /// + 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) + { + if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string)) + { + descriptor = Descriptor; + return true; + } + + descriptor = null; + return false; + } + } + + /// + /// 模拟只暴露 stream provider 接口、但不暴露描述符枚举契约的 generated registry。 + /// + private sealed class NonEnumeratingStreamInvokerProviderRegistry : + ICqrsHandlerRegistry, + ICqrsStreamInvokerProvider + { + private static readonly CqrsStreamInvokerDescriptor Descriptor = new( + typeof(IStreamRequestHandler), + typeof(GeneratedStreamInvokerProviderRegistry).GetMethod( + "InvokeGenerated", + BindingFlags.NonPublic | BindingFlags.Static)!); + + /// + 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) + { + if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int)) + { + descriptor = Descriptor; + return true; + } + + descriptor = null; + return false; + } + } + /// /// 创建带有 generated request invoker registry 元数据的程序集替身。 /// 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 271ff6fc..1908139c 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-073` +- 恢复点编号:`CQRS-REWRITE-RP-074` - 当前阶段:`Phase 8` - 当前焦点: - 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md` @@ -78,6 +78,9 @@ CQRS 迁移与收敛。 - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 现新增 request / stream 两组 `non-static invoker` 与 `incompatible invoker` 回归,锁定 dispatcher 在首次绑定阶段会显式拒绝非法 generated descriptor - `GFramework.Cqrs/Internal/CqrsDispatcher.cs` 现把 `Delegate.CreateDelegate(...)` 抛出的 `ArgumentException` 统一包装为已有 XML 文档承诺的 `InvalidOperationException`,保持 request / stream 两条错误消息语义一致 - 本轮顺手为新增异步断言补齐 `ConfigureAwait(false)`,消除新测试引入的 `MA0004` warning + - 已完成一轮 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 的合同 - 当前相对 `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 元数据的组合输出 @@ -307,6 +310,12 @@ CQRS 迁移与收敛。 - `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` 模块构建问题 @@ -340,6 +349,6 @@ CQRS 迁移与收敛。 ## 下一步 -1. 在保持 branch diff 明显低于 `50 files` 的前提下,继续挑选下一批低风险 `dispatch/invoker` 收敛切片,并优先考虑 request / stream provider 的剩余 runtime 失败边界或 generator gate 合同补强 +1. 在保持 branch diff 明显低于 `50 files` 的前提下,继续挑选下一批低风险 `dispatch/invoker` 收敛切片,并优先考虑 request / stream provider 的剩余 runtime 失败边界、缓存预热边界或 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 674ca8a1..4140cff1 100644 --- a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md +++ b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md @@ -2,6 +2,27 @@ ## 2026-04-30 +### 阶段:non-enumerating provider reflection fallback 回归(CQRS-REWRITE-RP-074) + +- 在 `RP-073` 提交后继续按 `gframework-batch-boot 50` 执行;当前 branch diff 相对 `origin/main` 仍远低于 `50 files` 阈值,因此继续追加一轮单文件 runtime contract 回归 +- 本轮接受只读 subagent 的收敛建议,把切片限定为“provider 已注册但未向 dispatcher 可枚举地贡献 descriptor”时的 fallback 语义 +- 主线程已完成: + - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 新增 request / stream 两条回归,锁定仅实现 `ICqrsRequestInvokerProvider` / `ICqrsStreamInvokerProvider`、但未实现 `IEnumeratesCqrs*InvokerDescriptors` 的 registry 仍会让 dispatch 回退到既有反射路径 + - 当前回归刻意不修改 `CqrsDispatcher` 或 `CqrsHandlerRegistrar`:它只把现有实现和注释里已经隐含的“descriptor cache 预热优先于 provider 显式查询”语义提升为可执行合同 + +### 验证(RP-074) + +- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"` + - 结果:通过,`14/14` passed + +### 当前下一步(RP-074) + +1. 先提交本轮 non-enumerating provider 回归与恢复点更新 +2. 重新复算 branch diff 后,再判断是否继续推进 provider 的空枚举 descriptor 边界或在本轮阈值前停下 +3. 若继续下一批,优先保持单文件测试写集,不扩散到新的模块 + ### 阶段:generated invoker provider runtime 失败边界修复(CQRS-REWRITE-RP-073) - 在 `RP-072` 提交后继续按 `gframework-batch-boot 50` 执行;当前 branch diff 相对 `origin/main` 仍为 `24 files`,文件阈值 headroom 依然充足,因此继续推进下一批 runtime 失败边界回归 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 12/14] =?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 发射与诊断边界 | ## 继续阅读 From 9296def108b9ea8581e9ac5eba3397728a15bed4 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 17:50:30 +0800 Subject: [PATCH 13/14] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E9=BD=90=20stream?= =?UTF-8?q?=20invoker=20gate=20=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 补充 stream invoker descriptor 与 descriptor entry 缺失时整体跳过 provider 元数据的生成器回归 - 优化测试辅助重命名逻辑,精确模拟 metadata name 缺失而不破坏其余合同编译 - 更新 cqrs-rewrite 跟踪与追踪,记录 PR #307 follow-up 的恢复点和验证结果 --- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 112 ++++++++++++++++++ .../todos/cqrs-rewrite-migration-tracking.md | 5 +- .../traces/cqrs-rewrite-migration-trace.md | 24 ++++ 3 files changed, 140 insertions(+), 1 deletion(-) diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 1254e273..277b0ee7 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -3156,6 +3156,58 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当 runtime 缺少 CqrsStreamInvokerDescriptor 时, + /// 生成器不会继续发射依赖描述符类型的 stream provider 元数据。 + /// + [Test] + public void Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Type() + { + var source = RenameTypeIdentifier( + StreamInvokerProviderSource, + "CqrsStreamInvokerDescriptor", + "MissingCqrsStreamInvokerDescriptor"); + var generatedSource = RunGenerator(source); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry")); + Assert.That(generatedSource, Does.Not.Contain("ICqrsStreamInvokerProvider")); + Assert.That(generatedSource, Does.Not.Contain("IEnumeratesCqrsStreamInvokerDescriptors")); + Assert.That(generatedSource, Does.Not.Contain("CqrsStreamInvokerDescriptorEntry(")); + Assert.That(generatedSource, Does.Not.Contain("InvokeStreamHandler0")); + }); + } + + /// + /// 验证当 runtime 缺少 CqrsStreamInvokerDescriptorEntry 时, + /// 生成器不会继续保留 stream provider 的枚举接口或静态 invoker 元数据。 + /// + [Test] + public void Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Entry_Type() + { + var source = RenameTypeIdentifier( + StreamInvokerProviderSource, + "CqrsStreamInvokerDescriptorEntry", + "MissingCqrsStreamInvokerDescriptorEntry"); + var generatedSource = RunGenerator(source); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry")); + Assert.That(generatedSource, Does.Not.Contain("ICqrsStreamInvokerProvider")); + Assert.That(generatedSource, Does.Not.Contain("IEnumeratesCqrsStreamInvokerDescriptors")); + Assert.That(generatedSource, Does.Not.Contain("CqrsStreamInvokerDescriptorEntry(")); + Assert.That(generatedSource, Does.Not.Contain("InvokeStreamHandler0")); + }); + } + /// /// 验证当 stream handler 仍需走 precise reflected 注册时, /// 生成器即使检测到 stream invoker provider runtime 合同,也不会错误发射无法稳定表达隐藏请求/响应类型的 provider 元数据。 @@ -3258,6 +3310,66 @@ public class CqrsHandlerRegistryGeneratorTests return source.Remove(startIndex, endIndex - startIndex); } + /// + /// 仅按完整类型标识符重命名测试输入中的合同类型,避免误伤共享前缀的其他类型名。 + /// + /// 原始测试源码。 + /// 原始合同类型名。 + /// 替换后的占位类型名。 + /// 完成精确类型重命名后的源码。 + private static string RenameTypeIdentifier(string source, string originalTypeName, string replacementTypeName) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(originalTypeName); + ArgumentNullException.ThrowIfNull(replacementTypeName); + + var result = new System.Text.StringBuilder(source.Length); + var currentIndex = 0; + + while (currentIndex < source.Length) + { + var matchIndex = source.IndexOf(originalTypeName, currentIndex, StringComparison.Ordinal); + if (matchIndex < 0) + { + result.Append(source, currentIndex, source.Length - currentIndex); + break; + } + + result.Append(source, currentIndex, matchIndex - currentIndex); + + if (IsIdentifierBoundary(source, matchIndex - 1) && + IsIdentifierBoundary(source, matchIndex + originalTypeName.Length)) + { + result.Append(replacementTypeName); + } + else + { + result.Append(originalTypeName); + } + + currentIndex = matchIndex + originalTypeName.Length; + } + + return result.ToString(); + } + + /// + /// 判断给定位置是否位于 C# 标识符边界,用于避免把共享前缀的其他类型名一并改写。 + /// + /// 待检查的完整源码。 + /// 边界位置;允许落在字符串两端之外。 + /// 若当前位置不在标识符内部,则返回 + private static bool IsIdentifierBoundary(string source, int index) + { + if (index < 0 || index >= source.Length) + { + return true; + } + + var character = source[index]; + return !char.IsLetterOrDigit(character) && character != '_'; + } + /// /// 统计生成源码中某个固定片段的出现次数,用于锁定程序集级 fallback 特性的发射个数。 /// 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 ee254359..06cc5b02 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-075` +- 恢复点编号:`CQRS-REWRITE-RP-076` - 当前阶段:`Phase 8` - 当前焦点: - 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md` @@ -74,6 +74,9 @@ CQRS 迁移与收敛。 - 已完成一轮 invoker provider gate 合同回归: - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 现新增四条回归,分别锁定 request / stream 在缺少 `ICqrsRequestInvokerProvider`、`IEnumeratesCqrsRequestInvokerDescriptors`、`ICqrsStreamInvokerProvider` 或 `IEnumeratesCqrsStreamInvokerDescriptors` 时,generator 都会整体跳过对应 provider 元数据发射 - 本轮最初采用固定源码片段替换来裁剪测试输入,但因三引号字符串缩进差异导致 helper 过脆;当前已收敛为按稳定起止标记移除源码块的 `RemoveBlock(...)` helper,避免 gate 回归依赖精确空格对齐 + - 已完成一轮 stream invoker descriptor gate 合同补强: + - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 现额外新增两条 stream gate 回归,分别锁定 runtime 缺少 `CqrsStreamInvokerDescriptor` 或 `CqrsStreamInvokerDescriptorEntry` 时,generator 同样会整体跳过 stream provider 元数据发射 + - 本轮补强直接对应 `CqrsHandlerRegistryGenerator` 中 `supportsStreamInvokerProvider` 的四项合同探测,避免此前只覆盖 provider / enumerator 缺失而漏掉 descriptor 两条分支 - 已完成一轮 generated invoker provider runtime 失败边界修复: - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 现新增 request / stream 两组 `non-static invoker` 与 `incompatible invoker` 回归,锁定 dispatcher 在首次绑定阶段会显式拒绝非法 generated descriptor - `GFramework.Cqrs/Internal/CqrsDispatcher.cs` 现把 `Delegate.CreateDelegate(...)` 抛出的 `ArgumentException` 统一包装为已有 XML 文档承诺的 `InvalidOperationException`,保持 request / stream 两条错误消息语义一致 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 8a3d6cde..95bac3aa 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,30 @@ ## 2026-04-30 +### 阶段:PR #307 stream invoker gate 回归补强(CQRS-REWRITE-RP-076) + +- 继续沿用 `$gframework-pr-review` 对 `PR #307` 的 latest-head review triage,只处理本地仍成立且写集可控的 generator regression gap +- 主线程复核 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs:88-92` 后确认:`supportsStreamInvokerProvider` 依赖四项合同,但现有测试只覆盖 `ICqrsStreamInvokerProvider` 与 `IEnumeratesCqrsStreamInvokerDescriptors` 缺失分支,确实遗漏 `CqrsStreamInvokerDescriptor` / `CqrsStreamInvokerDescriptorEntry` +- 本轮实现收敛: + - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增两条 `RemoveBlock(...)` 回归,分别移除 `CqrsStreamInvokerDescriptor` 与 `CqrsStreamInvokerDescriptorEntry` 合同定义 + - 新回归继续锁定统一结果:当 stream invoker runtime 合同四者缺一时,generated registry 不会残留 provider 接口、descriptor entry 枚举或静态 invoker 桥接 + - active tracking 已把恢复点推进到 `RP-076`,避免 PR review 结论只体现在测试代码里 + +### 验证(RP-076) + +- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - 备注:首轮并发跑 build/test 时出现过 `MSB3248` / `MSB3026` 输出文件占用噪音;按仓库规则改为串行复核后,本轮 authoritative build 结果为干净通过 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "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.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Entry_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` + - 结果:通过,`5/5` passed + - 备注:新增两条 descriptor gate 回归与既有 stream happy-path 一并通过,确认 `supportsStreamInvokerProvider` 的四项合同缺一不可 + +### 当前下一步(RP-076) + +1. 提交本轮 `PR #307` stream gate 合同补强与 `ai-plan` 恢复点更新 +2. 后续若继续处理 review,优先清点 request 侧是否也存在同构遗漏,再决定是否追加同批对称测试 +3. 保持忽略工作区里无关的 `.gitignore` 本地改动,不把它混入本轮提交 + ### 阶段:PR #307 review follow-up 收敛(CQRS-REWRITE-RP-075) - 在 `RP-074` 后继续沿用 `gframework-batch-boot 50` 的低风险切片策略,本轮只处理 `$gframework-pr-review` 对当前 `PR #307` 仍然成立的本地问题 From 26314dba5ec921a3571593337946dc2c4c8731ab Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 18:47:55 +0800 Subject: [PATCH 14/14] =?UTF-8?q?docs(cqrs-rewrite):=20=E6=94=B6=E6=95=9B?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=E5=85=A5=E5=8F=A3=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 active tracking,仅保留 RP-076、PR #307、活跃风险、权威验证与下一推荐步骤 - 重构 active trace,仅保留当前阶段决策、验证结果与后续恢复方向 - 补充 RP-062 至 RP-076 的 trace 归档,承接迁出的历史阶段上下文 --- ...qrs-rewrite-history-rp062-through-rp076.md | 34 ++ .../todos/cqrs-rewrite-migration-tracking.md | 276 ++--------- .../traces/cqrs-rewrite-migration-trace.md | 438 +----------------- 3 files changed, 83 insertions(+), 665 deletions(-) create mode 100644 ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-rp062-through-rp076.md diff --git a/ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-rp062-through-rp076.md b/ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-rp062-through-rp076.md new file mode 100644 index 00000000..8af5d36d --- /dev/null +++ b/ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-rp062-through-rp076.md @@ -0,0 +1,34 @@ +# CQRS 重写迁移追踪归档(RP-062 至 RP-076) + +## 说明 + +- 本文件承接从 active trace 中迁出的 `RP-062` 至 `RP-076` 阶段细节。 +- `boot` 默认恢复入口应回到 `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md`,不要从本归档直接挑选旧阶段作为当前恢复点。 + +## 覆盖范围 + +- `CQRS-REWRITE-RP-062` 至 `CQRS-REWRITE-RP-076` +- 对应 active trace 清理前的 `2026-04-29` 至 `2026-04-30` 阶段记录 + +## 归档摘要 + +- `RP-062`:`PR #305` review follow-up 收敛,补齐并发测试、logger provider 恢复、真实上下文注入与 tracking/trace 细节修正 +- `RP-063`:`CQRS vs Mediator` 结构化评估归档 +- `RP-064`:notification publisher seam 最小实现与回归补齐 +- `RP-065`:`Mediator` 历史测试命名与目录收口 +- `RP-066`:legacy `ICqrsRuntime` alias compatibility slice 收敛 +- `RP-067`:generated request invoker provider 最小落地 +- `RP-068`:generated stream invoker provider 最小落地 +- `RP-069`:generated invoker 在 hidden-implementation + visible-interface 场景下的发射范围补强 +- `RP-070`:hidden-implementation generated invoker runtime 回归补强 +- `RP-071`:precise reflected invoker provider 合同边界回归 +- `RP-072`:request / stream provider gate 合同回归 +- `RP-073`:generated invoker provider runtime 失败边界修复 +- `RP-074`:non-enumerating provider reflection fallback 回归 +- `RP-075`:`PR #307` review follow-up 收敛,补齐 descriptor 合同防御、空枚举回退与文档口径 +- `RP-076`:stream invoker gate 四项 runtime 合同分支补强,并最终将 active tracking / trace 收敛为单一恢复入口 + +## 备注 + +- `RP-063` 至 `RP-074` 的详细命令级验证仍以 `archive/todos/cqrs-rewrite-validation-history-rp063-through-rp074.md` 为准。 +- `RP-075` 与 `RP-076` 的权威验证结论已同步沉淀到 active tracking / trace,后续若需追溯阶段细节,应同时参考对应测试文件、提交记录与本归档摘要。 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 06cc5b02..fff5461f 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 @@ -9,264 +9,52 @@ CQRS 迁移与收敛。 - 恢复点编号:`CQRS-REWRITE-RP-076` - 当前阶段:`Phase 8` -- 当前焦点: - - 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md` - - 当前评估结论已明确:`GFramework.Cqrs` 已完成对外部 `Mediator` 的生产级替代,但仓库内部旧总线 API、 - 兼容 seam、fallback 旧语义与测试命名仍未完全收口 - - 当前评估结论已明确:相对 `ai-libs/Mediator`,框架已吸收统一消息模型、generator 优先注册与热路径缓存思路, - 但仍未完整吸收 publisher 策略抽象、细粒度 pipeline、telemetry / diagnostics / benchmark 体系与 runtime 主体生成 - - 下一阶段建议优先级已收敛为:`notification publisher seam`、`dispatch/invoker 生成前移`、`pipeline 分层扩展`、 - `可观测性 seam` 与 `benchmark / allocation baseline` - - 当前功能历史已归档,active 跟踪仅保留 `Phase 8` 主线的恢复入口 - - 已完成一轮 notification publisher seam 最小落地:`GFramework.Cqrs` 新增 `INotificationPublisher`、 - `NotificationPublishContext` 与默认 `SequentialNotificationPublisher` - - `CqrsDispatcher` 现会在解析当前通知处理器集合后,把执行顺序委托给 publisher seam;默认行为仍保持 - “零处理器静默完成、顺序执行、首错即停” - - `CqrsRuntimeFactory`、`CqrsRuntimeModule` 与 `GFramework.Tests.Common.CqrsTestRuntime` 现支持在 runtime 创建前复用 - 容器里已显式注册的 `INotificationPublisher` - - 已补充 `CqrsNotificationPublisherTests`,覆盖自定义 publisher 接管、上下文注入、零处理器静默完成、首错即停,以及 - `RegisterInfrastructure` 默认接线复用预注册 publisher 的回归 - - 已完成一轮 `Mediator` 测试命名收口: - - `MediatorAdvancedFeaturesTests` -> `CqrsArchitectureContextAdvancedFeaturesTests` - - `MediatorArchitectureIntegrationTests` -> `CqrsArchitectureContextIntegrationTests` - - `MediatorComprehensiveTests` -> `ArchitectureContextComprehensiveTests` - - `GFramework.Cqrs.Tests` 中这三份历史测试现已统一迁入 `Cqrs/` 目录,并将命名空间、类名、中文注释与嵌套测试类型中的 - `Mediator` 语义收口为 `CQRS` / `ArchitectureContext` - - 已补充 `ArchitectureContextTests` 并发 lazy-resolution 回归,锁定 `PublishAsync(...)` 与 `CreateStream(...)` - 在并发首次访问时也只会解析一次 `ICqrsRuntime` - - 已完成一轮 `LegacyICqrsRuntime` compatibility slice 收口: - - `CqrsRuntimeModule` 与 `GFramework.Tests.Common.CqrsTestRuntime` 现把 legacy alias 注册收敛到显式 helper - - `MicrosoftDiContainerTests` 已补充“只预注册正式 `ICqrsRuntime` seam 时,也会回填 legacy alias 且保持同实例”的回归 - - `GFramework.Core.Abstractions/README.md`、`docs/zh-CN/abstractions/core-abstractions.md` 与 - `docs/zh-CN/core/cqrs.md` 现已明确:旧命名空间下的 `ICqrsRuntime` 仅作为 compatibility alias 保留, - 新代码应直接依赖 `GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime` - - 已完成一轮 `dispatch/invoker` 生成前移的最小 request 切片: - - `GFramework.Cqrs` 新增 `ICqrsRequestInvokerProvider`、`IEnumeratesCqrsRequestInvokerDescriptors`、 - `CqrsRequestInvokerDescriptor` 与 `CqrsRequestInvokerDescriptorEntry` - - generated registry 若实现 request invoker provider 契约,`CqrsHandlerRegistrar` 现会在激活 registry 后把 provider 注册进容器, - 并把 provider 枚举出的 request invoker 描述符写入 dispatcher 的进程级弱缓存 - - `CqrsDispatcher` 现会在首次创建 request dispatch binding 时优先命中 generated request invoker 描述符; - 未命中时仍回退到既有 `MakeGenericMethod + Delegate.CreateDelegate` 路径 - - `GFramework.Cqrs.Tests` 已补充 `CqrsGeneratedRequestInvokerProviderTests`,锁定 registrar 接线和 dispatcher 消费 generated invoker 的最小语义 - - `GFramework.SourceGenerators.Tests` 已补充 generator 回归,锁定当 runtime 暴露新契约时,generated registry 会额外发射 request invoker provider 成员与 invoker 方法 - - 已完成一轮 `dispatch/invoker` 生成前移的最小 stream 切片: - - `GFramework.Cqrs` 新增 `ICqrsStreamInvokerProvider`、`IEnumeratesCqrsStreamInvokerDescriptors`、 - `CqrsStreamInvokerDescriptor` 与 `CqrsStreamInvokerDescriptorEntry` - - generated registry 若实现 stream invoker provider 契约,`CqrsHandlerRegistrar` 现会在激活 registry 后把 provider 注册进容器, - 并把 provider 枚举出的 stream invoker 描述符写入 dispatcher 的进程级弱缓存 - - `CqrsDispatcher` 现会在首次创建 stream dispatch binding 时优先命中 generated stream invoker 描述符; - 未命中时仍回退到既有 `MakeGenericMethod + Delegate.CreateDelegate` 流式 binding 路径 - - `GFramework.Cqrs.Tests` 已扩充 `CqrsGeneratedRequestInvokerProviderTests`,锁定 registrar 接线和 dispatcher 消费 generated stream invoker 的最小语义 - - `GFramework.SourceGenerators.Tests` 已补充 generator 回归,锁定当 runtime 暴露新契约时,generated registry 会额外发射 stream invoker provider 成员与 invoker 方法 - - `GFramework.Cqrs/README.md`、`GFramework.Cqrs.SourceGenerators/README.md`、`docs/zh-CN/core/cqrs.md` 与 - `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md` 现已同步说明 generated stream invoker 的接线与回退边界 - - 已完成一轮 generated invoker 发射范围补强: - - `CqrsHandlerRegistryGenerator` 现会把 generated request / stream invoker 的发射范围,从“仅 direct registration”扩大到“实现类型隐藏、但 handler interface 仍可直接表达”的 reflected-implementation registration - - 当前扩展仍刻意避开 `PreciseReflectedRegistrationSpec`,不把隐藏 request/response 类型误拉进 provider 发射,继续保持生成源码可编译边界 - - `GFramework.SourceGenerators.Tests` 已新增两条 hidden-implementation 回归,锁定 request / stream provider 在该场景下都会继续发射 descriptor 与静态 invoker 方法 - - 已完成一轮 hidden-implementation generated invoker runtime 回归补强: - - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 现覆盖“实现类型隐藏、但 handler interface 可见”场景下的 generated request / stream invoker 消费路径 - - `HiddenImplementationGeneratedRequestInvokerProviderRegistry`、`HiddenImplementationGeneratedStreamInvokerProviderRegistry` 与对应 container / handler fixture 现锁定 registrar 接线后,dispatcher 会优先命中 generated descriptor,而不是退回反射 invoker - - 当前 runtime 回归继续保持 `PreciseReflectedRegistrationSpec` 排除边界不变,只验证已允许发射 provider 元数据的 visible-interface hidden-implementation 场景 - - 已完成一轮 precise reflected invoker provider 合同边界回归: - - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 现新增 request / stream 两条回归,明确当 handler 仍需走 `PreciseReflectedRegistrationSpec` 时,generator 即使检测到 invoker provider runtime 合同,也不会错误发射 descriptor、枚举接口或静态 invoker 桥接 - - 本轮接受了一条只读 subagent 的“继续评估 precise reflected + provider 发射”候选思路,但主线程复核后确认该候选并不存在可安全放宽的 `typeof(request/response)` 子集,因此收敛为“锁定当前排除边界”的测试批次,而不是修改生产 generator 逻辑 - - 已完成一轮 invoker provider gate 合同回归: - - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 现新增四条回归,分别锁定 request / stream 在缺少 `ICqrsRequestInvokerProvider`、`IEnumeratesCqrsRequestInvokerDescriptors`、`ICqrsStreamInvokerProvider` 或 `IEnumeratesCqrsStreamInvokerDescriptors` 时,generator 都会整体跳过对应 provider 元数据发射 - - 本轮最初采用固定源码片段替换来裁剪测试输入,但因三引号字符串缩进差异导致 helper 过脆;当前已收敛为按稳定起止标记移除源码块的 `RemoveBlock(...)` helper,避免 gate 回归依赖精确空格对齐 - - 已完成一轮 stream invoker descriptor gate 合同补强: - - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 现额外新增两条 stream gate 回归,分别锁定 runtime 缺少 `CqrsStreamInvokerDescriptor` 或 `CqrsStreamInvokerDescriptorEntry` 时,generator 同样会整体跳过 stream provider 元数据发射 - - 本轮补强直接对应 `CqrsHandlerRegistryGenerator` 中 `supportsStreamInvokerProvider` 的四项合同探测,避免此前只覆盖 provider / enumerator 缺失而漏掉 descriptor 两条分支 - - 已完成一轮 generated invoker provider runtime 失败边界修复: - - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 现新增 request / stream 两组 `non-static invoker` 与 `incompatible invoker` 回归,锁定 dispatcher 在首次绑定阶段会显式拒绝非法 generated descriptor - - `GFramework.Cqrs/Internal/CqrsDispatcher.cs` 现把 `Delegate.CreateDelegate(...)` 抛出的 `ArgumentException` 统一包装为已有 XML 文档承诺的 `InvalidOperationException`,保持 request / stream 两条错误消息语义一致 - - 本轮顺手为新增异步断言补齐 `ConfigureAwait(false)`,消除新测试引入的 `MA0004` warning - - 已完成一轮 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 元数据的组合输出 - - 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据 - - 当 runtime 不支持多实例 fallback 特性或缺少对应构造函数时,mixed fallback 场景仍会整体保守回退到字符串元数据,避免仅部分 handler 走 `Type[]` 时漏掉剩余需按名称恢复的 handlers - - 已完成 request pipeline executor 形状缓存:`CqrsDispatcher` 现会在单个 request binding 内按 `behaviorCount` 复用强类型 pipeline executor,而不是每次 `SendAsync` 都重建整条 `next` 委托链 - - 已补充 dispatcher pipeline executor 缓存与双行为顺序回归,锁定缓存复用后仍保持现有行为执行顺序 - - 已补充 cached request pipeline executor 的上下文刷新回归,锁定 executor 复用时仍会为当次 handler / singleton behavior 重新注入当前 `ArchitectureContext` - - 已补充 cached notification / stream dispatch binding 的上下文刷新回归,锁定 binding 复用时仍会为当次 handler 重新注入当前 `ArchitectureContext` - - 已补充非 `IArchitectureContext` 的 dispatcher 失败语义回归,锁定 context-aware request / notification / stream handler 在注入前置条件不满足时会显式抛出异常 - - 已补充 registrar fallback 失败分支回归,锁定 named fallback 无法解析、named fallback 解析抛异常、direct fallback 跨程序集三类 warning 语义 - - 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke` - - 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物 - - 已修正 pointer / function pointer 泛型合同的错误覆盖:生成器不再为这两类类型发射 precise runtime type 重建代码 - - 已补充非法 CQRS 泛型合同的输入诊断断言,明确 `CS0306` 与 fallback / diagnostic 路径的组合语义 - - 已为 registrar 的 reflection 注册路径补充 handler-interface 元数据缓存,减少跨容器重复注册时的 `GetInterfaces()` 反射 - - 已将 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 内复用 - - 已补齐 `CqrsDispatcherContextValidationTests` 三个上下文校验 handler 的 XML `param` / `returns` 注释,以及 `DispatcherNotificationContextRefreshNotification`、`DispatcherStreamContextRefreshRequest` 的 `DispatchId` XML 参数注释,收敛上一轮 PR review 遗留的文档类 minor feedback - - 已收紧 CQRS / generator 回归测试的脆弱断言:日志断言改为语义匹配,precise runtime type lookup 回归改为锁定数组秩、外部类型查找与“未发射 fallback metadata”这些稳定语义 - - 已为 dispatcher cache / context refresh / pipeline order 三组测试状态容器补齐并发保护,并将 `CqrsDispatcherCacheTests` 标记为 `NonParallelizable`,避免静态缓存与共享快照在并行测试中相互污染 - - 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点 - -## 当前状态摘要 - -- 已完成 `Mediator` 外部依赖移除、CQRS runtime 重建、默认架构接线和显式程序集 handler 注册入口 -- 已完成 `GFramework.Cqrs.Abstractions` / `GFramework.Cqrs` 项目骨架与 runtime seam 收敛 -- 已完成 handler registry generator 的多轮收敛,当前合法 closed handler contract 已统一收敛到更窄的注册路径 -- 已完成一轮公开入口文档与 source-generator 命名空间收口 -- 已完成一轮 `CQRS vs Mediator` 对照评估,确认当前主问题已从“是否能替代外部依赖”转为“框架内部收口与能力深化顺序” -- 已接入 `$gframework-pr-review`,可直接抓取当前分支对应 PR 的 CodeRabbit 评论、checks 和测试结果 +- 当前 PR 锚点:`PR #307` +- 当前结论: + - `GFramework.Cqrs` 已完成对外部 `Mediator` 的生产级替代,当前主线已从“是否可替代”转向“仓库内部收口与能力深化顺序” + - `dispatch/invoker` 生成前移已扩展到 request / stream 路径,当前 `RP-076` 已补齐 stream invoker provider gate 的四项 runtime 合同分支 + - `ai-plan` active 入口现以 `PR #307` 和 `RP-076` 为唯一权威恢复锚点;更早 PR 与阶段细节均以下方归档为准 ## 当前活跃事实 -- `Phase 8` 仍是当前主线,不再回退到 `Phase 7` -- `2026-04-20` 已重新执行 `$gframework-pr-review`: - - 当前分支对应 `PR #261`,状态为 `OPEN` - - latest reviewed commit 当前剩余 `1` 条 open CodeRabbit thread,指向 `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md` 中 `RP-047` 与 `RP-050` 的历史语义冲突 - - 本地已同步修正该追踪歧义:`RP-047` 明确标注为已被 `RP-050` 覆盖,后续不得恢复 `MakePointerType()` precise registration - - 远端测试信号保持通过:最新 CTRF 汇总为 `2118/2118` passed;MegaLinter 仅剩 `dotnet-format` restore failure 预警,当前未提供本地仍然成立的文件级格式问题 -- `2026-04-20` 已完成一轮冷启动反射收敛: - - generated registry 类型首次分析后,会缓存一个可复用的激活工厂,而不是在后续容器注册时重复走 `ConstructorInfo.Invoke` - - 若运行环境不允许动态方法,仍保留原有的反射激活回退,避免阻塞 generated registry 路径 - - `GFramework.Cqrs.Tests` 已补充“私有无参构造 registry 仍可激活”的回归覆盖 -- `2026-04-20` 已完成一轮 generator 覆盖面扩展: - - `CqrsHandlerRegistryGenerator` 现会在 runtime type 建模入口直接拒绝 `IPointerTypeSymbol` 与 `IFunctionPointerTypeSymbol` - - `CanReferenceFromGeneratedRegistry` 不再递归判断 pointer / function pointer 的内部元素,而是统一返回 `false` - - 相关 source-generator 回归已改为区分输入源诊断与生成源诊断,避免把非法泛型合同误判为成功生成 -- `2026-04-20` 已完成一轮 registrar reflection 路径收敛: - - `CqrsHandlerRegistrar` 现会按 `Type` 弱键缓存已筛选且排序好的 supported handler interface 列表 - - 同一 handler 类型跨容器重复注册时,不再重复执行 `GetInterfaces()` 与支持接口筛选 - - `GFramework.Cqrs.Tests` 已补充 registrar 静态缓存隔离与 supported interface 缓存复用回归 -- `2026-04-20` 已完成一轮 registrar 去重路径收敛: - - `CqrsHandlerRegistrar` 现会在单次 reflection 注册流程开始时构建已注册 handler 映射索引 - - 同一批注册中后续 duplicate handler mapping 不再重复线性扫描 `IServiceCollection` - - `GFramework.Cqrs.Tests` 已补充“程序集返回重复 handler 类型时仍只注册一份映射”的回归 -- `2026-04-29` 已完成一轮 generator fallback 元数据收敛: - - `CqrsHandlerRegistryGenerator` 现会探测 runtime 是否同时支持 `params string[]` 与 `params Type[]` 两类 `CqrsReflectionFallbackAttribute` 构造函数 - - 当本轮 fallback handlers 全部可被生成代码直接引用时,生成器会优先发射 `typeof(...)` 形式的程序集级 fallback 元数据,减少运行时 `Assembly.GetType(...)` 回查 - - 当 fallback handlers 中仍存在不能直接引用的实现类型时,生成器继续统一发射字符串元数据,避免 mixed 场景只恢复部分 handlers - - `GFramework.SourceGenerators.Tests` 已补充 runtime 同时暴露两类构造函数时优先选择直接 `Type` 元数据的回归 -- `2026-04-29` 已完成一轮 mixed fallback 元数据拆分: - - `CqrsReflectionFallbackAttribute` 现显式允许 `AllowMultiple = true` - - `CqrsHandlerRegistryGenerator` 现会探测 runtime 是否允许多个 fallback 特性实例 - - 当本轮 fallback 同时包含可直接引用与仅能按名称恢复的 handlers,且 runtime 同时支持 `Type[]`、`string[]` 和多实例特性时,生成器会拆分输出两段 fallback 元数据 - - `GFramework.Cqrs.Tests` 已补充 mixed fallback metadata 回归,锁定 registrar 只对字符串条目执行定向 `Assembly.GetType(...)` - - `GFramework.SourceGenerators.Tests` 已补充 mixed fallback emission 回归,锁定 generator 会输出两个程序集级 fallback 特性实例而不是整体退回字符串 -- `2026-04-29` 已重新执行 `$gframework-pr-review`: - - 当前分支对应 `PR #302`,状态为 `OPEN` - - 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 #305`,状态为 `OPEN` - - 当前抓取到 `9` 条 CodeRabbit open threads、`2` 条 Greptile open threads;远端 CTRF 汇总为 `2214/2214` passed,MegaLinter 仍只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音 - - 本地核对后,已确认以下评论仍然成立并已完成修正:`ArchitectureContextTests` 并发测试失败路径释放、`CqrsGeneratedRequestInvokerProviderTests` 的全局 logger provider 恢复与私有缓存断言解耦、`CqrsArchitectureContextIntegrationTests` 的真实上下文注入断言、`GeneratedRequestInvokerRequest` / `INotificationPublisher` XML 文档、`CqrsHandlerRegistrar` 的 provider 注册顺序、`CqrsTestRuntime` 的 legacy alias 显式失败模式,以及 `cqrs-rewrite` trace 重复标题 - - 对于 `ICqrsRequestInvokerProvider` / generated `TryGetDescriptor(...)` 相关 Greptile 评论,本地评估后未改 dispatcher 热路径语义;改为补齐公开注释与生成器方法级注释,明确默认 runtime 只在注册阶段经 `IEnumeratesCqrsRequestInvokerDescriptors` 预热缓存,`TryGetDescriptor(...)` 保留为显式查询 seam - - 本轮额外修正了 `GFramework.SourceGenerators.Tests` 中先读取 `GeneratedSources[0]` 再断言长度的脆弱顺序,并将 `ArchitectureContextTests` 的并发 orchestration 收敛到公共 helper,消除本轮引入的 `MA0051` warning -- `2026-04-29` 已完成一轮 precise runtime type lookup 的数组回归补强: - - `GFramework.SourceGenerators.Tests` 已新增多维数组、交错数组、外部程序集隐藏元素类型三类回归 - - 当前生成器在 precise runtime type lookup 下已稳定保留数组秩信息,并递归发射交错数组的 `MakeArrayType()` 链 - - 本轮定向测试未暴露数组发射缺陷,因此未改动 fallback 合同选择逻辑,也未调整 direct / named / mixed fallback 排版路径 -- `2026-04-29` 已补齐一轮外部程序集隐藏泛型定义回归覆盖: - - `GFramework.SourceGenerators.Tests` 已新增“外部程序集隐藏泛型定义 + 可见类型实参”的 precise registration 回归 - - 当前生成器会继续为这类 handler 合同发射 `ResolveReferencedAssemblyType(...) + MakeGenericType(...)` 组合,而不是退回字符串 fallback 元数据 - - 本轮定向测试未暴露新的实现缺口,因此未改动 direct / named / mixed fallback 选择逻辑,也未调整 generator runtime type 建模实现 -- `2026-04-29` 已完成一轮缓存工厂闭包收敛: - - `CqrsDispatcher` 现会在 notification / stream / request binding 与 pipeline executor 缓存入口优先使用无捕获工厂 - - `CqrsHandlerRegistrar` 现会在程序集元数据缓存与可加载类型缓存入口复用 `static` 工厂 + 显式状态参数 - - 本轮未改动公开语义,也未修改 fallback 合同与 handler / behavior 生命周期边界 -- `2026-04-29` 已完成一轮 request pipeline executor 形状缓存: - - `CqrsDispatcher` 现会继续按 `requestType + responseType` 缓存 request dispatch binding,并在 binding 内按 `behaviorCount` 缓存强类型 pipeline executor - - 每次分发只绑定当前 handler / behaviors 实例,不缓存容器解析结果,因此不改变 transient 生命周期与上下文注入语义 - - `GFramework.Cqrs.Tests` 已补充 executor 首次创建 / 后续复用与双行为顺序回归 -- `2026-04-29` 已完成一轮 cached executor 上下文刷新回归补强: - - `GFramework.Cqrs.Tests` 已新增 `DispatcherPipelineContextRefresh*` 测试替身,分别记录 request handler 与 pipeline behavior 在每次分发中实际观察到的实例身份与 `ArchitectureContext` - - `CqrsDispatcherCacheTests` 现明确断言:同一个 cached request pipeline executor 在重复分发时会继续命中同一 executor 形状,但不会跨分发保留旧上下文 - - 本轮定向测试未暴露新的 runtime 缺口,因此没有改动 `GFramework.Cqrs/Internal/CqrsDispatcher.cs` -- `2026-04-29` 已完成一轮 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` 已完成一轮 dispatcher 上下文前置条件失败语义回归: - - `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 已通过公开工厂 `CqrsRuntimeFactory.CreateRuntime(...)` 锁定默认 dispatcher 的失败语义 - - 当 context-aware request / notification / stream handler 遇到仅实现 `ICqrsContext`、但未实现 `IArchitectureContext` 的上下文时,dispatcher 会在调用前显式抛出 `InvalidOperationException` - - 本轮只补测试,不改 runtime 实现与文档口径 -- `2026-04-29` 已接受一轮 delegated registrar fallback 失败分支测试: - - `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs` 已覆盖 named fallback 无法解析、named fallback 解析抛异常、direct fallback 跨程序集三类 warning 语义 - - 主线程已复核该新文件并重新执行定向测试,确认当前 registrar 在 fallback 元数据失效时仍保持“跳过条目 + 记录告警”的既有语义 -- `2026-04-29` 已接受一轮 delegated 叶子级 fallback 合同测试: - - `GFramework.Cqrs.Tests/Cqrs/CqrsReflectionFallbackAttributeTests.cs` 已锁定空 marker、字符串 fallback 名称去空/去重/排序、直接 `Type` fallback 去空/去重/排序与空参数数组防御语义 - - 当前 runtime 读取程序集级 fallback 元数据时所依赖的 attribute 归一化合同,现已有独立叶子级测试文件覆盖 -- `2026-04-29` 已完成一轮 CQRS 入口文档对齐: - - `GFramework.Cqrs/README.md`、`docs/zh-CN/core/cqrs.md` 与 `docs/zh-CN/api-reference/index.md` 现已明确 generated registry 优先、targeted fallback 补齐剩余 handler 的当前语义 -- `2026-04-29` 已完成一轮 generator pointer runtime-reconstruction 残留清理: - - `CqrsHandlerRegistryGenerator` 的运行时类型引用模型已移除不可达的 pointer 子结构 - - `SourceEmission` 不再保留 `MakePointerType()` 源码发射分支,`RuntimeTypeReferences` 也已删掉对应的外部程序集递归扫描死代码 - - pointer / function pointer 的拒绝语义保持不变,direct / named / mixed fallback 逻辑未改动 - - 当前工作区相对 `origin/main` 的累计 diff 已达到 `14 files`,仍低于本轮 `gframework-batch-boot 50` 的主要 stop condition -- `2026-04-30` 已完成一轮 `CQRS vs Mediator` 结构化评估: - - 生产依赖与默认 runtime 接线层面,`GFramework.Cqrs` 已完成对外部 `Mediator` 的替代 - - 仓库内部收口层面,旧 `Command` / `Query` API、`LegacyICqrsRuntime` 别名、fallback 空 marker 兼容语义与 - `Mediator` 测试命名仍然存在 - - 设计吸收层面,当前已吸收统一消息模型、generator 优先注册与反射收敛思路;仍未完整吸收 publisher 策略抽象、 - stream / exception pipeline、telemetry / diagnostics / benchmark 体系与 runtime 主体生成 - - 详细结论与证据已归档到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md` -- `2026-04-30` 已接受两条只读 subagent 结论并完成 notification publisher seam 最小实现: - - 相对 `ai-libs/Mediator`,本轮只吸收 notification publisher 的策略接缝,不照搬 `NotificationHandlers` 包装、 - 并行 publisher 或异常聚合语义 - - 当前 seam 刻意保持在默认 runtime 内部:`ICqrsRuntime.PublishAsync(...)` 外形不变,dispatcher 仍负责 handler 解析与 - `IContextAware` 上下文注入 - - 用户若需替换通知发布策略,只需在 runtime 创建前向容器显式注册 `INotificationPublisher` -- `2026-04-30` 已接受三条 worker 切片并完成一轮测试命名收口: - - 三个 worker 分别独立拥有一份 `GFramework.Cqrs.Tests/Mediator/*.cs` 文件,主线程只做集成验证与后续追踪更新 - - 当前分支已不再保留 `GFramework.Cqrs.Tests/Mediator/` 目录下的生产内涵测试,相关文件均迁移到 `GFramework.Cqrs.Tests/Cqrs/` - - 本轮没有修改测试行为,只收口命名、注释、局部变量与嵌套测试类型语义 -- 当前主线优先级: - - dispatch/invoker 反射占比继续下降,并优先评估生成前移方案 - - 基于已落地 publisher seam,继续评估是否需要公开配置面、并行策略或 telemetry decorator - - package / facade / 兼容层继续收口 - - pipeline 分层扩展、可观测性 seam 与 benchmark baseline 进入中期候选 +- 当前分支对应 `PR #307`,状态为 `OPEN` +- latest-head review 仍以 `ai-plan` 恢复文档收敛为主要待闭环项;代码与测试侧的本地有效问题已收敛 +- 远端 `CTRF` 最新汇总为 `2247/2247` passed +- `MegaLinter` 当前只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,尚未提供本地仍成立的文件级格式诊断 ## 当前风险 -- 当前 `dotnet build GFramework.sln -c Release` 在 WSL 环境仍会受顶层 `GFramework.csproj` 的 Windows NuGet fallback 配置影响 -- 当前 `GFramework.Cqrs.Tests` 仍直接引用 `GFramework.Core`,说明测试已按模块意图拆分,但 runtime 物理迁移尚未完全切断依赖 -- 当前对外替代已基本完成,但若不单独规划旧 `Command` / `Query`、`LegacyICqrsRuntime` 与测试命名的收口顺序, - 后续仍会持续混淆“生产替代已完成”与“仓库内部收口未完成”这两个不同结论 +- 顶层 `GFramework.sln` / `GFramework.csproj` 在 WSL 下仍可能受 Windows NuGet fallback 配置影响,完整 solution 级验证成本高于模块级验证 +- 仓库内部仍保留旧 `Command` / `Query` API、`LegacyICqrsRuntime` alias 与部分历史命名语义,后续若不继续分批收口,容易混淆“对外替代已完成”与“内部收口未完成” +- 若继续扩大 generated invoker 覆盖面,需要持续区分“可静态表达的合同”与 `PreciseReflectedRegistrationSpec` 等仍需保守回退的场景 + +## 最近权威验证 + +- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json` + - 结果:通过 + - 备注:确认当前分支对应 `PR #307`,本轮剩余 open AI feedback 主要集中在 `ai-plan` 收敛 +- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "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.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Entry_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` + - 结果:通过,`5/5` passed + +## 下一推荐步骤 + +1. 继续处理 `PR #307` 的剩余 review 收尾,优先保持 `ai-plan` active 入口与 trace 的单一锚点一致 +2. 若继续推进代码切片,优先复核 request 侧是否存在与 stream gate 对称的生成合同遗漏,再决定是否补同批 generator 回归 +3. 在进入下一批 runtime / generator 收敛前,保持最小 Release build 或 targeted test 作为权威验证 ## 活跃文档 - 历史跟踪归档:[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) +- `RP-062` 至 `RP-076` trace 归档:[cqrs-rewrite-history-rp062-through-rp076.md](../archive/traces/cqrs-rewrite-history-rp062-through-rp076.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) -## 验证说明 +## 说明 -- `RP-043` 之前的详细阶段记录、定向验证命令和阶段性决策均已移入主题内归档 -- `RP-046` 至 `RP-062` 的历史验证命令与阶段性结果已移入验证归档,active tracking 只保留当前恢复入口需要的最新验证 -- `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 #307`,状态为 `OPEN`;当前仍有 `9` 条 CodeRabbit open thread,本轮只接受其中经本地复核后仍成立的合同防御、文档和 tracking 建议 -- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - - 结果:通过 - - 备注:`0 warning / 0 error`;确认描述符前置防御、XML 文档与文档修正未引入 `GFramework.Cqrs` 模块告警 -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"` - - 结果:通过 - - 备注:`16/16` passed;确认非静态 invoker 回退语义、空 descriptor 枚举回退,以及既有 generated / incompatible 分支均保持通过 - -## 下一步 - -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 与专门测试,可暂时移出最高优先级 +- `PR #261`、`PR #302`、`PR #305` 及更早阶段的详细过程已不再作为 active 恢复入口;如需追溯,以对应归档文件为准 +- active tracking 仅保留当前恢复点、当前风险、最近权威验证与下一推荐步骤,避免 `boot` 落到历史阶段细节 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 95bac3aa..b46a73ea 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,435 +2,31 @@ ## 2026-04-30 -### 阶段:PR #307 stream invoker gate 回归补强(CQRS-REWRITE-RP-076) +### 阶段:PR #307 active 入口收敛(CQRS-REWRITE-RP-076) -- 继续沿用 `$gframework-pr-review` 对 `PR #307` 的 latest-head review triage,只处理本地仍成立且写集可控的 generator regression gap -- 主线程复核 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs:88-92` 后确认:`supportsStreamInvokerProvider` 依赖四项合同,但现有测试只覆盖 `ICqrsStreamInvokerProvider` 与 `IEnumeratesCqrsStreamInvokerDescriptors` 缺失分支,确实遗漏 `CqrsStreamInvokerDescriptor` / `CqrsStreamInvokerDescriptorEntry` -- 本轮实现收敛: - - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增两条 `RemoveBlock(...)` 回归,分别移除 `CqrsStreamInvokerDescriptor` 与 `CqrsStreamInvokerDescriptorEntry` 合同定义 - - 新回归继续锁定统一结果:当 stream invoker runtime 合同四者缺一时,generated registry 不会残留 provider 接口、descriptor entry 枚举或静态 invoker 桥接 - - active tracking 已把恢复点推进到 `RP-076`,避免 PR review 结论只体现在测试代码里 +- 继续沿用 `$gframework-pr-review` 对 `PR #307` 做 latest-head triage,本轮只处理仍成立的 `ai-plan` 恢复入口问题 +- 主线程确认当前远端权威信号: + - 当前分支对应 `PR #307`,状态为 `OPEN` + - 远端 `CTRF` 最新汇总为 `2247/2247` passed + - `MegaLinter` 仅剩 `dotnet-format` 的 `Restore operation failed` 环境噪音 + - 仍未闭环的 review 重点集中在 `cqrs-rewrite` active tracking / trace 仍保留过多历史锚点,而非新的运行时代码缺陷 +- 本轮决策: + - 将 active tracking 收敛为单一恢复入口,只保留 `RP-076`、`PR #307`、活跃风险、最近权威验证与下一推荐步骤 + - 将 active trace 收敛为当前阶段的关键事实与决策,不再在默认恢复入口中保留 `RP-062` 之后的长阶段流水账 + - 新增 `archive/traces/cqrs-rewrite-history-rp062-through-rp076.md` 承接 `RP-062` 至 `RP-076` 的详细 trace 历史,保持旧阶段仍可追溯 ### 验证(RP-076) +- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json` + - 结果:通过 + - 备注:确认 `PR #307` 的当前 review 重点已收敛到 `ai-plan` 文档收尾 - `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` - 结果:通过,`0 warning / 0 error` - - 备注:首轮并发跑 build/test 时出现过 `MSB3248` / `MSB3026` 输出文件占用噪音;按仓库规则改为串行复核后,本轮 authoritative build 结果为干净通过 - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "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.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Entry_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` - 结果:通过,`5/5` passed - - 备注:新增两条 descriptor gate 回归与既有 stream happy-path 一并通过,确认 `supportsStreamInvokerProvider` 的四项合同缺一不可 ### 当前下一步(RP-076) -1. 提交本轮 `PR #307` stream gate 合同补强与 `ai-plan` 恢复点更新 -2. 后续若继续处理 review,优先清点 request 侧是否也存在同构遗漏,再决定是否追加同批对称测试 -3. 保持忽略工作区里无关的 `.gitignore` 本地改动,不把它混入本轮提交 - -### 阶段: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 回归 -- 本轮接受只读 subagent 的收敛建议,把切片限定为“provider 已注册但未向 dispatcher 可枚举地贡献 descriptor”时的 fallback 语义 -- 主线程已完成: - - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 新增 request / stream 两条回归,锁定仅实现 `ICqrsRequestInvokerProvider` / `ICqrsStreamInvokerProvider`、但未实现 `IEnumeratesCqrs*InvokerDescriptors` 的 registry 仍会让 dispatch 回退到既有反射路径 - - 当前回归刻意不修改 `CqrsDispatcher` 或 `CqrsHandlerRegistrar`:它只把现有实现和注释里已经隐含的“descriptor cache 预热优先于 provider 显式查询”语义提升为可执行合同 - -### 验证(RP-074) - -- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` - - 结果:通过,`0 warning / 0 error` -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"` - - 结果:通过,`14/14` passed - -### 当前下一步(RP-074) - -1. 先提交本轮 non-enumerating provider 回归与恢复点更新 -2. 重新复算 branch diff 后,再判断是否继续推进 provider 的空枚举 descriptor 边界或在本轮阈值前停下 -3. 若继续下一批,优先保持单文件测试写集,不扩散到新的模块 - -### 阶段:generated invoker provider runtime 失败边界修复(CQRS-REWRITE-RP-073) - -- 在 `RP-072` 提交后继续按 `gframework-batch-boot 50` 执行;当前 branch diff 相对 `origin/main` 仍为 `24 files`,文件阈值 headroom 依然充足,因此继续推进下一批 runtime 失败边界回归 -- 本轮原计划只补 `CqrsGeneratedRequestInvokerProviderTests` 的 request / stream 非 happy-path 回归,但定向测试首轮直接暴露出一个真实 runtime 缺口: - - `CqrsDispatcher.CreateRequestInvokerDescriptor(...)` 与 `CreateStreamInvokerDescriptor(...)` 的 XML 文档和消息语义都承诺会抛 `InvalidOperationException` - - 实际实现先调用 `Delegate.CreateDelegate(...)`,当 invoker 签名不兼容时会直接冒出 `ArgumentException`,导致文档承诺与运行时行为不一致 -- 主线程已完成: - - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 新增 request / stream 两组 `non-static invoker` 与 `incompatible invoker` 回归,并保留 request / stream happy-path 作为同批守护断言 - - `GFramework.Cqrs/Internal/CqrsDispatcher.cs` 现对 request / stream 两条 descriptor 创建路径统一捕获 `ArgumentException`,并转换成带原有错误消息的 `InvalidOperationException` - - 新增异步断言已补齐 `ConfigureAwait(false)`,避免测试批次自身引入 `MA0004` analyzer warning - -### 验证(RP-073) - -- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` - - 结果:通过 - - 备注:并行执行 build/test 时曾出现 `MSB3026` 输出文件竞争噪音;无真实编译失败,也未引入新增 analyzer warning -- `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 - -### 当前下一步(RP-073) - -1. 先提交本轮 runtime 失败边界修复与恢复点更新 -2. 重新复算 branch diff 后,再判断是否继续推进剩余 provider 失败边界或在接近阈值前停下 -3. 若继续下一批,优先保持单文件或双文件写集,避免在本轮后段扩散 review 面积 - -### 阶段:invoker provider gate 合同回归(CQRS-REWRITE-RP-072) - -- 在 `RP-071` 提交后继续按 `gframework-batch-boot 50` 执行;当前 branch diff 相对 `origin/main` 仍为 `24 files`,未接近主要 stop condition,因此继续追加一轮 test-only generator 合同回归 -- 本轮接受一条只读 subagent 建议,把下一批进一步收敛为“runtime 合同不完整时不发射 provider 元数据”的单文件测试波次 -- 主线程已完成: - - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增四条 gate 回归,分别锁定 request / stream 在缺少 provider 接口或缺少 descriptor 枚举接口时,都会整体跳过元数据发射 - - 初版实现曾使用整段源码片段替换来删减测试输入,但因三引号字符串缩进差异导致 helper 匹配失败;随后改为按稳定起止标记移除源码块的 `RemoveBlock(...)` helper,使测试意图与输入格式解耦 - - 同一组定向验证同时保留 request / stream happy-path 两条既有回归,确认 gate 收紧后不会误伤原本完整合同下的 provider 发射 - -### 验证(RP-072) - -- `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 - -### 当前下一步(RP-072) - -1. 先提交本轮 generator gate 合同回归与恢复点更新 -2. 重新复算 branch diff 后,再决定是否继续推进 request / stream provider 的 runtime 失败边界测试 -3. 若继续下一批,优先保持 test-only 或极小写集,避免在接近阈值前扩散到新的生产模块 - -### 阶段:precise reflected invoker provider 合同边界回归(CQRS-REWRITE-RP-071) - -- 在 `RP-070` 提交后继续按 `gframework-batch-boot 50` 执行;当前已提交 branch diff 仍为 `24 files`,headroom 充足,因此继续下一批 generator-only 合同收敛 -- 本轮先接受一条只读 subagent 的候选建议,评估是否可把 `PreciseReflectedRegistrationSpec` 的某个安全子集也纳入 request / stream provider 发射 -- 主线程复核 `TryCreatePreciseReflectedRegistration(...)`、`CreateRequestInvokerEmissions(...)` / `CreateStreamInvokerEmissions(...)` 与现有 precise 测试素材后确认: - - precise reflected 分支之所以存在,正是因为 handler interface 的请求或响应类型无法完全通过 `typeof(...)` 稳定表达 - - 当前 provider descriptor 合同需要直接发射 `typeof(requestType)` / `typeof(responseType)`;因此不存在可无条件放宽的“安全子集” - - 本轮最终不改生产 generator,而是把这条边界显式固化到回归测试,避免后续误把不存在的子集当成已支持能力 -- 主线程已完成: - - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增两条回归,分别锁定 request / stream 的 precise reflected 注册不会发射 invoker provider 元数据 - - 同一组定向测试同时复核 hidden-implementation + visible-interface 场景仍会继续发射 provider 元数据,确保“允许发射”和“继续排除”的边界没有被本轮测试收紧弄混 - -### 验证(RP-071) - -- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - 结果:通过,`0 warning / 0 error` -- `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 -- `git diff --name-only origin/main...HEAD | wc -l` - - 结果:通过 - - 备注:当前相对 `origin/main` 的已提交 branch diff 仍为 `24 files` -- `git diff --numstat origin/main...HEAD` - - 结果:通过 - - 备注:当前相对 `origin/main` 的工作分支累计 diff 为 `1793 changed lines` - -### 当前下一步(RP-071) - -1. 先提交本轮 generator 合同边界回归,保持恢复点、trace 与已验证测试状态一致 -2. 继续挑选下一批低风险切片,优先考虑 request / stream provider 的 runtime 或 generator 诊断边界,而不是贸然扩大 precise reflected 支持面 -3. 若下一批仍可拆分为非冲突文件,再恢复只读 / 写入 subagent 的分工方式压低主线程上下文 -### 阶段:hidden-implementation generated invoker runtime 回归补强(CQRS-REWRITE-RP-070) - -- 在 `5a77e2fb` 提交后补齐 active `ai-plan` 恢复入口,继续按 `gframework-batch-boot 50` 执行,基线仍为当前本地 `origin/main` -- 当前已提交 branch diff 复算为 `24 files / 1754 changed lines`,仍低于主要 stop condition,因此本轮只补 runtime 回归与恢复点,不改 generator / runtime 生产实现 -- 本轮关键目标是把 `RP-069` 已落地的 hidden-implementation provider 发射范围补强,继续向 runtime 消费侧闭环,避免 active tracking 只记录了 generator 侧验证 -- 主线程已完成: - - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 新增 hidden-implementation + visible-interface 的 request / stream runtime 回归 - - `HiddenImplementationGeneratedRequestInvokerProviderRegistry`、`HiddenImplementationGeneratedStreamInvokerProviderRegistry` 与对应 container fixture 已被纳入同一组 provider 消费测试,锁定 registrar 接线与 dispatcher 优先命中 generated descriptor 的语义 - - 当前测试仍保持 `PreciseReflectedRegistrationSpec` 排除边界不变,不把隐藏 request/response 类型场景错误抬升为 runtime 支持承诺 - -### 验证(RP-070) - -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"` - - 结果:通过,`8/8` passed -- `git diff --name-only origin/main...HEAD | wc -l` - - 结果:通过 - - 备注:当前相对 `origin/main` 的已提交 branch diff 为 `24 files` -- `git diff --numstat origin/main...HEAD` - - 结果:通过 - - 备注:当前相对 `origin/main` 的已提交 branch diff 为 `1754 changed lines` - -### 当前下一步(RP-070) - -1. 先提交本轮 `ai-plan` 恢复点更新,保持 batch 追踪与已提交代码状态一致 -2. 在剩余 headroom 内继续选择下一批低风险 `dispatch/invoker` 收敛切片,优先考虑 request / stream provider 的诊断、入口或测试补强 -3. 如下一批写集仍可拆分,再用只读 / 写入 subagent 分离非冲突切片,继续降低主线程上下文压力 - -### 阶段:generated stream invoker provider 最小落地(CQRS-REWRITE-RP-068) - -- 继续按 `gframework-batch-boot 50` 执行,基线仍为当前本地 `origin/main` -- 本轮开始前,`origin/main` 已追平到当前 `HEAD`;因此 branch diff 重新归零,主 stop condition 仍为“相对 `origin/main` 接近 `50 files`” -- 当前批次沿用上一轮 request invoker provider 的设计形状,只做 stream 路径的最小对称扩展,避免把 notification publisher seam、pipeline 或 telemetry 一并卷入 -- 本轮切片拆分: - - worker:`GFramework.Cqrs/README.md`、`docs/zh-CN/core/cqrs.md`、`docs/zh-CN/source-generators/cqrs-handler-registry-generator.md` - - worker:`GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` - - 主线程:`GFramework.Cqrs/Internal/CqrsDispatcher.cs`、`GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs`、 - `GFramework.Cqrs/*.cs` 新增 stream provider 契约、`GFramework.Cqrs.SourceGenerators/Cqrs/*`、 - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` -- 主线程关键设计调整: - - 继续保持 dispatcher 的 stream binding 静态缓存只依赖 `requestType + responseType`,不回调具体容器实例 - - stream provider 与 request provider 一样在 registrar 注册阶段一次性枚举 descriptor,并写入 dispatcher 的进程级弱缓存 - - generated registry 同时实现 request 与 stream 两组 descriptor 枚举契约时,改用显式接口实现 `GetDescriptors()`,避免同名方法冲突 -- 已完成实现: - - `GFramework.Cqrs` 新增 `ICqrsStreamInvokerProvider`、`IEnumeratesCqrsStreamInvokerDescriptors`、 - `CqrsStreamInvokerDescriptor` 与 `CqrsStreamInvokerDescriptorEntry` - - `CqrsHandlerRegistrar` 新增 stream provider 接线与 descriptor 登记路径 - - `CqrsDispatcher` 新增 generated stream invoker 弱缓存,并在 `CreateStream(...)` 首次创建 stream binding 时优先消费 generated stream invoker 元数据 - - `CqrsHandlerRegistryGenerator` 新增 stream invoker registration 建模、descriptor 发射、显式枚举接口实现与 `InvokeStreamHandler{n}(...)` 静态桥接方法 - - `GFramework.Cqrs.Tests` 新增 `GeneratedStreamInvokerProviderRegistry`、`GeneratedStreamInvokerRequest`、`GeneratedStreamInvokerRequestHandler`,并扩充 `CqrsGeneratedRequestInvokerProviderTests` - - `GFramework.Cqrs.SourceGenerators/README.md` 额外补齐模块级 README,对齐 generated stream invoker 语义 -- worker 产出已接受: - - 文档切片已把 request / stream invoker provider 作为并列 reader-facing 语义写入公开文档 - - generator 测试切片已补齐 stream invoker provider fixture 与断言;主线程根据最终实现把 request / stream 的 `GetDescriptors()` 断言统一收敛到显式接口实现版本 - -### 验证(RP-068) - -- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - - 结果:通过,`0 warning / 0 error` -- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - 结果:通过,`0 warning / 0 error` -- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` - - 结果:通过,`0 warning / 0 error` -- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` - - 结果:通过,`0 warning / 0 error` -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"` - - 结果:通过,`4/4` passed -- `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 -- `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` - - 结果:通过 -- `git diff --name-only origin/main...HEAD | wc -l` - - 结果:通过 - - 备注:当前相对 `origin/main` 的已提交 branch diff 为 `4 files` -- `git diff --numstat origin/main...HEAD` - - 结果:通过 - - 备注:当前相对 `origin/main` 的已提交 branch diff 为 `217 changed lines` - -### 当前下一步(RP-068) - -1. 在保持 branch diff 远低于 `50 files` 阈值的前提下,继续评估下一个低风险 `dispatch/invoker` 收敛切片 -2. 优先候选仍是 notification 路径是否值得引入同类 generated invoker seam,或继续补强 request / stream provider 的公开 API 入口与诊断语义 -3. 下一批落地前先提交当前 stream provider 批次,避免未提交改动持续堆叠 - -### 阶段:generated invoker reflected-implementation 发射范围补强(CQRS-REWRITE-RP-069) - -- 在 `RP-068` 提交后,重新复算 branch diff,相对 `origin/main` 升至 `20 files / 1015 changed lines`,仍明显低于 `gframework-batch-boot 50` 的 stop condition,因此继续下一批 -- 本轮目标只收敛 source generator,不扩散到 runtime 或公开文档:把 generated request / stream invoker 的发射范围从“仅 direct registration”扩大到“实现类型隐藏、但 handler interface 可直接表达”的 reflected-implementation registration -- 接受只读 subagent 结论后确认: - - 现有分类阶段已经为 reflected-implementation registration 保留了 request / stream invoker registration 元数据 - - 真正缺口只在 `CreateRequestInvokerEmissions(...)` 与 `CreateStreamInvokerEmissions(...)` 仍只遍历 `DirectRegistrations` - - `PreciseReflectedRegistrationSpec` 继续排除在 provider 发射范围外,避免隐藏 request/response 类型导致生成源码不可编译 -- 主线程已完成: - - `ReflectedImplementationRegistrationSpec` 显式承载 request / stream invoker registration 元数据 - - `CreateRequestInvokerEmissions(...)` 与 `CreateStreamInvokerEmissions(...)` 现会同时消费 reflected-implementation registration - - `GFramework.SourceGenerators.Tests` 已新增 hidden-implementation + visible-interface 两条 provider 回归 -- 本轮不改 runtime:dispatcher / registrar 对 generated provider 的消费语义保持不变,变化只在 generator 愿意发射更多可安全静态表达的 descriptor - -### 验证(RP-069) - -- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - 结果:通过,`0 warning / 0 error` -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Generates_Direct_Interface_Registrations_For_Hidden_Implementation_When_Handler_Interface_Is_Public|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` - - 结果:通过,`3/3` passed -- `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 - -### 当前下一步(RP-069) - -1. 提交当前 generator-only 批次,继续保持每个低风险切片可独立回滚与审查 -2. 继续评估下一个能明显降低反射占比、但不需要同时改动 runtime 语义的切片 - -### 阶段:generated request invoker provider 最小落地(CQRS-REWRITE-RP-067) - -- 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main` -- 在 `RP-066` 提交后复算 branch diff,相对 `origin/main` 增长到 `22 files`,仍明显低于 `50 files` stop condition,因此继续下一批 -- 本轮 critical path 保持在主线程,本地完成 `dispatch/invoker` 生成前移的最小 request 切片;尝试委派 source-generator 测试给 worker 时因 subagent 名额已满失败,因此主线程直接接管该测试修改 -- 本轮关键设计调整: - - 不按 `requestType.Assembly` 做 provider 发现,避免“请求定义在 A、handler 与 generated registry 在 B”时漏掉 generated invoker - - generated registry 若实现 `ICqrsRequestInvokerProvider`,registrar 会在激活 registry 后把 provider 注册进容器,并通过 `IEnumeratesCqrsRequestInvokerDescriptors` 把描述符写入 dispatcher 的进程级弱缓存 - - dispatcher 首次创建 request dispatch binding 时只按 `requestType + responseType` 读取静态弱缓存,不依赖具体容器实例;未命中时仍走既有反射创建路径 -- 已完成实现: - - `GFramework.Cqrs` 新增 `ICqrsRequestInvokerProvider`、`IEnumeratesCqrsRequestInvokerDescriptors`、 - `CqrsRequestInvokerDescriptor` 与 `CqrsRequestInvokerDescriptorEntry` - - `CqrsHandlerRegistrar` 现会识别 generated registry 的 request invoker provider 能力,并登记 provider 与 request invoker 描述符 - - `CqrsDispatcher` 新增 generated request invoker 弱缓存,并在 request binding 创建时优先消费该元数据 - - `CqrsHandlerRegistryGenerator` 在 runtime 合同可用时,会让 generated registry 额外实现 request invoker provider 相关接口,并发射 descriptor 列表、`TryGetDescriptor(...)`、`GetDescriptors()` 与 request invoker 静态方法 -- 已补充测试: - - `CqrsGeneratedRequestInvokerProviderTests` 锁定 registrar 会注册 generated request invoker provider,且 dispatcher 走 generated invoker 后会返回 `generated:` 前缀结果 - - `CqrsHandlerRegistryGeneratorTests` 锁定 generated source 会包含 request invoker provider 接口、descriptor 条目与 `InvokeRequestHandler0(...)` 方法 - -### 验证(RP-067) - -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsHandlerRegistrarTests|FullyQualifiedName~CqrsDispatcherCacheTests"` - - 结果:通过,`22/22` passed -- `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` passed -- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - - 结果:通过,`0 warning / 0 error` - -### 当前下一步(RP-067) - -1. 评估 notification / stream invoker 是否值得沿同一 provider 模式继续前移,或先补 request provider 的公开说明与诊断语义 -2. 继续在保持 branch diff 低于阈值的前提下推进下一批;当前相对 `origin/main` 的 branch diff 为 `22 files` - -### 阶段:LegacyICqrsRuntime compatibility slice 收口(CQRS-REWRITE-RP-066) - -- 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main` -- 在 `RP-065` 之后复算 branch diff,相对 `origin/main` 仍为 `19 files`,明显低于 `50 files` stop condition,因此继续下一批 -- 本轮按“关键路径本地、非冲突文档委派”的方式拆成两个切片: - - worker:`GFramework.Core.Abstractions/README.md`、`docs/zh-CN/abstractions/core-abstractions.md`、`docs/zh-CN/core/cqrs.md` - - 主线程:`GFramework.Core/Services/Modules/CqrsRuntimeModule.cs`、`GFramework.Tests.Common/CqrsTestRuntime.cs`、`GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs` -- 接受只读 subagent 结论后,将 `LegacyICqrsRuntime` 定位为“容器兼容层”,明确本轮不删除别名、不改 dispatcher 主体、不与旧 `Command` / `Query` API 清理混做 -- 主线程已完成: - - `CqrsRuntimeModule` 把 legacy alias 注册收敛到 `RegisterLegacyRuntimeAlias(...)` helper,并在 XML 文档里明确新旧服务类型解析到同一 runtime 实例 - - `CqrsTestRuntime.RegisterInfrastructure(...)` 现也通过同名 helper 补齐 legacy alias;当容器只预注册正式 `ICqrsRuntime` seam 时,会在幂等接线时回填旧命名空间 alias - - `MicrosoftDiContainerTests` 新增 `RegisterInfrastructure_Should_Backfill_Legacy_Cqrs_Runtime_Alias_With_The_Same_Instance`,锁定“只存在正式 seam 时也会补旧 alias,且两者仍指向同一实例”的兼容合同 -- worker 已完成文档收口: - - `GFramework.Core.Abstractions/README.md` - - `docs/zh-CN/abstractions/core-abstractions.md` - - `docs/zh-CN/core/cqrs.md` - - 三处文档都已明确:`GFramework.Core.Abstractions.Cqrs.ICqrsRuntime` 只是旧命名空间下保留的 compatibility alias,新代码应依赖 `GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime` - -### 验证(RP-066) - -- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"` - - 结果:通过,`42/42` passed -- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` - - 结果:通过,`0 warning / 0 error` - -### 当前下一步(RP-066) - -1. 在保持 branch diff 低于阈值的前提下,回到 `dispatch/invoker` 生成前移主线 -2. 优先尝试只覆盖 request 路径的 generated invoker/provider 最小切片,避免一次卷入 notification / stream / pipeline executor -3. 下一次 batch 结束后继续复算 branch diff,确认距 `50 files` stop condition 的剩余 headroom - -### 阶段:测试命名收口与 ArchitectureContext lazy-resolution 回归(CQRS-REWRITE-RP-065) - -- 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main` -- `22f608eb` 之后复算 branch diff,相对 `origin/main` 已达到 `18 files`,仍明显低于 `50 files` stop condition,因此继续下一批 -- 本轮拆成四个互不冲突切片: - - worker 1:`MediatorAdvancedFeaturesTests.cs` - - worker 2:`MediatorArchitectureIntegrationTests.cs` - - worker 3:`MediatorComprehensiveTests.cs` - - 主线程:`GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs` -- 三个 worker 均只收口单文件命名与注释语义,并把测试文件迁移到 `GFramework.Cqrs.Tests/Cqrs/` -- 主线程新增 `ArchitectureContextTests` 并发 lazy-resolution 回归,锁定: - - `PublishAsync(...)` 在并发首次访问时只解析一次 `ICqrsRuntime` - - `CreateStream(...)` 在并发首次访问时只解析一次 `ICqrsRuntime` -- 集成后已确认三份测试文件中不再残留 `GFramework.Cqrs.Tests.Mediator` 命名空间或 `Mediator` 语义命名 - -### 验证(RP-065) - -- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` - - 结果:通过,`0 warning / 0 error` -- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests"` - - 结果:通过,`22/22` passed - -### 当前下一步(RP-065) - -1. 继续 `Phase 8` 主线,回到 `dispatch/invoker` 生成前移或 `LegacyICqrsRuntime` 收口的下一个低风险切片 -2. 在下一次 batch 结束后复算 branch diff,确认距 `50 files` stop condition 的剩余 headroom - -### 阶段:notification publisher seam 最小落地(CQRS-REWRITE-RP-064) - -- 本轮按 `gframework-batch-boot 50` 继续 `cqrs-rewrite`,基线使用本地现有 `origin/main` -- 当前 branch diff 相对 `origin/main` 开始时仅 `3 files / 164 lines`,远低于 `50 files` stop condition,因此继续推进真实代码切片 -- 主线程锁定 `notification publisher seam` 为本轮最低风险高收益切片,并保持关键路径在本地实现 -- 接受两条只读 subagent 结论: - - 对照 `ai-libs/Mediator` 后,只吸收 notification publisher 策略接缝,不在本轮引入并行 publisher、异常聚合或公开配置面 - - 现有仓库测试需要锁定的兼容语义是:零处理器静默完成、顺序执行、首错即停、上下文逐次注入 -- 已完成实现: - - `GFramework.Cqrs` 新增 `INotificationPublisher`、`NotificationPublishContext`、 - `DelegatingNotificationPublishContext` 与默认 `SequentialNotificationPublisher` - - `CqrsDispatcher.PublishAsync(...)` 改为解析 handlers 后构造发布上下文,并委托给 publisher seam 执行 - - `CqrsRuntimeFactory`、`CqrsRuntimeModule` 与 `GFramework.Tests.Common.CqrsTestRuntime` 现会在 runtime 创建前复用容器里已注册的 `INotificationPublisher` - - `GFramework.Cqrs.Tests` 新增 `CqrsNotificationPublisherTests`,覆盖自定义 publisher、上下文注入、零处理器、首错即停与默认接线复用 - - `GFramework.Cqrs/README.md` 与 `docs/zh-CN/core/cqrs.md` 已同步说明默认通知语义与可替换 seam -- 中途验证曾因并行 .NET 构建产生输出文件锁噪音;已改为串行重跑并获取干净结果 - -### 验证(RP-064) - -- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - - 结果:通过,`0 warning / 0 error` -- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` - - 结果:通过,`0 warning / 0 error` -- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsNotificationPublisherTests"` - - 结果:通过,`5/5` passed -- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"` - - 结果:通过,`41/41` passed -- `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` - - 结果:通过 - -### 当前下一步(RP-064) - -1. 评估 notification publisher seam 的第二阶段是否需要公开配置面、并行 publisher 或 telemetry decorator -2. 把 `dispatch/invoker` 生成前移重新拉回 `Phase 8` 主线,作为下一个实现切片 - -### 阶段:CQRS vs Mediator 评估归档(CQRS-REWRITE-RP-063) - -- 本轮按用户要求使用 `gframework-boot` 启动上下文后,先完成 `cqrs-rewrite` 现状核对,再并行对照 - `GFramework.Cqrs` 与 `ai-libs/Mediator` -- 只读评估结论已归档到 `ai-plan/public/cqrs-rewrite/archive/todos/cqrs-vs-mediator-assessment-rp063.md` -- 本轮关键判断: - - `GFramework.Cqrs` 已完成对外部 `Mediator` 作为生产 runtime 依赖的替代 - - 当前尚未完成的是仓库内部旧 `Command` / `Query` API、兼容 seam、fallback 旧语义与测试命名的收口 - - 当前已吸收 `Mediator` 的统一消息模型、generator 优先注册与热路径缓存思路 - - 当前仍未完整吸收 publisher 策略抽象、细粒度 pipeline、telemetry / diagnostics / benchmark 体系与 runtime 主体生成 -- 本轮把默认下一步从“继续盯 PR thread”调整为“围绕 publisher seam 与 dispatch/invoker 生成前移做下一轮设计收敛” - -### 验证(RP-063) - -- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - - 结果:通过,`0 warning / 0 error` - -## 活跃事实 - -- 当前主题仍处于 `Phase 8` -- 当前主题的主问题已从“是否完成外部依赖替代”转为“内部兼容层收口顺序与下一轮能力深化优先级” -- 已完成阶段的详细执行历史不再留在 active trace;默认恢复入口只保留当前恢复点、活跃事实、风险与下一步 - -## 当前风险 - -- 当前 `dotnet build GFramework.sln -c Release` 在 WSL 环境仍会受顶层 `GFramework.csproj` 的 Windows NuGet fallback 配置影响 -- 若不把“生产替代完成”与“仓库内部收口完成”分开记录,后续很容易重复争论当前 CQRS 迁移是否已经完成 - -## Archive Context - -- 当前评估归档: - - `ai-plan/public/cqrs-rewrite/archive/todos/cqrs-vs-mediator-assessment-rp063.md` -- 历史 trace 归档: - - `ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-through-rp043.md` - - `ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-rp046-through-rp061.md` - -## 当前下一步 - -1. 补一轮最小 Release 构建验证,确认本次 `ai-plan` 与评估文档更新未引入仓库级异常 -2. 以 `notification publisher seam` 与 `dispatch/invoker` 生成前移为优先对象,形成下一轮可执行设计 +1. 继续按 `PR #307` 的 latest-head review 收尾,优先保持 active tracking 与 active trace 的单一锚点一致 +2. 若继续推进代码切片,先复核 request 侧是否仍存在与 stream invoker gate 对称的生成合同遗漏 +3. 进入下一批前继续使用最小 Release build 或 targeted test 作为权威验证,避免把环境噪音误判为代码问题