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`