diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs index 9652f63b..5895e42f 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs @@ -306,8 +306,6 @@ public class ArchitectureContextTests public async Task SendRequestAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently() { const int workerCount = 8; - var workerStartupTimeout = TimeSpan.FromSeconds(5); - var firstResolutionTimeout = TimeSpan.FromSeconds(5); using var startGate = new ManualResetEventSlim(false); using var allowResolutionToComplete = new ManualResetEventSlim(false); using var workersReady = new CountdownEvent(workerCount); @@ -339,18 +337,11 @@ public class ArchitectureContextTests })) .ToArray(); - Assert.That( - workersReady.Wait(workerStartupTimeout), - Is.True, - "Expected all workers to be ready before releasing start gate."); - startGate.Set(); - - Assert.That( - SpinWait.SpinUntil(() => Volatile.Read(ref resolutionCallCount) > 0, firstResolutionTimeout), - Is.True, - "Expected at least one CQRS runtime resolution attempt."); - - allowResolutionToComplete.Set(); + ReleaseWorkersAfterFirstResolutionAttempt( + workersReady, + startGate, + allowResolutionToComplete, + () => Volatile.Read(ref resolutionCallCount) > 0); var responses = await Task.WhenAll(requests); @@ -372,8 +363,6 @@ public class ArchitectureContextTests public async Task PublishAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently() { const int workerCount = 8; - var workerStartupTimeout = TimeSpan.FromSeconds(5); - var firstResolutionTimeout = TimeSpan.FromSeconds(5); using var startGate = new ManualResetEventSlim(false); using var allowResolutionToComplete = new ManualResetEventSlim(false); using var workersReady = new CountdownEvent(workerCount); @@ -405,18 +394,11 @@ public class ArchitectureContextTests })) .ToArray(); - Assert.That( - workersReady.Wait(workerStartupTimeout), - Is.True, - "Expected all workers to be ready before releasing start gate."); - startGate.Set(); - - Assert.That( - SpinWait.SpinUntil(() => Volatile.Read(ref resolutionCallCount) > 0, firstResolutionTimeout), - Is.True, - "Expected at least one CQRS runtime resolution attempt."); - - allowResolutionToComplete.Set(); + ReleaseWorkersAfterFirstResolutionAttempt( + workersReady, + startGate, + allowResolutionToComplete, + () => Volatile.Read(ref resolutionCallCount) > 0); await Task.WhenAll(notifications).ConfigureAwait(false); @@ -437,8 +419,6 @@ public class ArchitectureContextTests public async Task CreateStream_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently() { const int workerCount = 8; - var workerStartupTimeout = TimeSpan.FromSeconds(5); - var firstResolutionTimeout = TimeSpan.FromSeconds(5); using var startGate = new ManualResetEventSlim(false); using var allowResolutionToComplete = new ManualResetEventSlim(false); using var workersReady = new CountdownEvent(workerCount); @@ -470,18 +450,11 @@ public class ArchitectureContextTests })) .ToArray(); - Assert.That( - workersReady.Wait(workerStartupTimeout), - Is.True, - "Expected all workers to be ready before releasing start gate."); - startGate.Set(); - - Assert.That( - SpinWait.SpinUntil(() => Volatile.Read(ref resolutionCallCount) > 0, firstResolutionTimeout), - Is.True, - "Expected at least one CQRS runtime resolution attempt."); - - allowResolutionToComplete.Set(); + ReleaseWorkersAfterFirstResolutionAttempt( + workersReady, + startGate, + allowResolutionToComplete, + () => Volatile.Read(ref resolutionCallCount) > 0); await Task.WhenAll(streamTasks).ConfigureAwait(false); @@ -509,6 +482,46 @@ public class ArchitectureContextTests } } + /// + /// 释放并发 worker,并确保在断言失败时也能放行首次 runtime 解析。 + /// + /// 用于确认 worker 已就绪的倒计时器。 + /// 用于同时放行 worker 的门闩。 + /// 用于解除首次 runtime 解析阻塞的门闩。 + /// 用于判断当前是否已观察到首次 runtime 解析尝试。 + private static void ReleaseWorkersAfterFirstResolutionAttempt( + CountdownEvent workersReady, + ManualResetEventSlim startGate, + ManualResetEventSlim allowResolutionToComplete, + Func hasObservedResolutionAttempt) + { + ArgumentNullException.ThrowIfNull(workersReady); + ArgumentNullException.ThrowIfNull(startGate); + ArgumentNullException.ThrowIfNull(allowResolutionToComplete); + ArgumentNullException.ThrowIfNull(hasObservedResolutionAttempt); + + var workerStartupTimeout = TimeSpan.FromSeconds(5); + var firstResolutionTimeout = TimeSpan.FromSeconds(5); + + Assert.That( + workersReady.Wait(workerStartupTimeout), + Is.True, + "Expected all workers to be ready before releasing start gate."); + startGate.Set(); + + try + { + Assert.That( + SpinWait.SpinUntil(hasObservedResolutionAttempt, firstResolutionTimeout), + Is.True, + "Expected at least one CQRS runtime resolution attempt."); + } + finally + { + allowResolutionToComplete.Set(); + } + } + /// /// 为 `CreateStream` 并发解析测试提供最小异步流。 /// diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs index a408d508..858c00d9 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs @@ -67,6 +67,23 @@ public sealed partial class CqrsHandlerRegistryGenerator requestInvokerEmissions); } + /// + /// 从 direct handler 注册描述中提取 request invoker 发射计划。 + /// + /// + /// 指示当前 runtime 是否同时暴露 ICqrsRequestInvokerProvider 与 + /// IEnumeratesCqrsRequestInvokerDescriptors 契约;若不支持,则本方法必须返回空结果并让后续发射路径整体跳过。 + /// + /// 已按稳定顺序整理完成的 handler 注册描述。 + /// + /// 由 directRegistration.RequestInvokerRegistration 派生出的 集合。 + /// methodIndex 与其 direct registration 的遍历顺序单调递增, + /// 因而只要上游排序稳定,生成的 invoker 方法名与描述符顺序就跨运行保持稳定。 + /// + /// + /// 缺少 RequestInvokerRegistration 的 direct registration 会被显式跳过,而不会生成半成品 provider 成员; + /// 调用方应把“为什么没有 request invoker registration”对应的诊断留在更早的建模阶段,而不是在源码发射阶段兜底。 + /// private static ImmutableArray CreateRequestInvokerEmissions( bool supportsRequestInvokerProvider, IReadOnlyList registrations) @@ -273,6 +290,17 @@ public sealed partial class CqrsHandlerRegistryGenerator builder.AppendLine(" }"); } + /// + /// 发射 generated registry 的 request invoker provider 成员。 + /// + /// 生成源码构造器。 + /// + /// 来自 的稳定发射计划。 + /// + /// + /// 该输出包含三部分:描述符数组、provider 查询方法,以及与描述符逐项对应的静态 invoker 方法。 + /// 若发射计划为空,调用方应直接跳过整个 provider 分支,而不是输出空的 registry seam。 + /// private static void AppendRequestInvokerProviderMembers( StringBuilder builder, ImmutableArray requestInvokerEmissions) @@ -288,6 +316,15 @@ public sealed partial class CqrsHandlerRegistryGenerator } } + /// + /// 发射 generated registry 的 request invoker 描述符数组。 + /// + /// 生成源码构造器。 + /// 当前要输出的 request invoker 发射计划。 + /// + /// 每个条目都会把请求类型、响应类型和对应的静态 invoker 方法打包成 + /// CqrsRequestInvokerDescriptorEntry,供 registrar 在注册阶段写入 dispatcher 的弱缓存。 + /// private static void AppendRequestInvokerDescriptorArray( StringBuilder builder, ImmutableArray requestInvokerEmissions) @@ -319,6 +356,14 @@ public sealed partial class CqrsHandlerRegistryGenerator builder.AppendLine(" ];"); } + /// + /// 发射 generated registry 对 request invoker provider 契约的实现方法。 + /// + /// 生成源码构造器。 + /// + /// 默认 runtime 真正消费的是 GetDescriptors() 暴露的完整描述符集合,并在注册阶段一次性写入缓存; + /// TryGetDescriptor(...) 保留为显式查询接口,因此这里使用线性扫描即可保持生成代码简单且无额外字典分配。 + /// private static void AppendRequestInvokerProviderMethods(StringBuilder builder) { builder.Append(" public global::System.Collections.Generic.IReadOnlyList + /// 为单个 request invoker 描述符发射对应的静态强类型桥接方法。 + /// + /// 生成源码构造器。 + /// 当前要输出的 invoker 发射计划。 + /// + /// 这些方法的编号与 一一对应, + /// dispatcher 通过描述符里的 把 object 形参桥接回强类型 handler 与 request。 + /// private static void AppendRequestInvokerMethod(StringBuilder builder, RequestInvokerEmissionSpec emission) { builder.Append(" private static global::System.Threading.Tasks.ValueTask<"); diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsArchitectureContextAdvancedFeaturesTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsArchitectureContextAdvancedFeaturesTests.cs index 05674a7b..63fb487b 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsArchitectureContextAdvancedFeaturesTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsArchitectureContextAdvancedFeaturesTests.cs @@ -62,7 +62,7 @@ internal sealed class CqrsArchitectureContextAdvancedFeaturesTests } [Test] - public async Task Request_With_Retry_Behavior_Should_Retry_On_Failure() + public async Task Request_With_Retry_Behavior_Should_Succeed_On_First_Attempt() { // 由于我们没有实现实际的重试行为,简化测试逻辑 TestRetryBehavior.AttemptCount = 0; @@ -132,7 +132,7 @@ internal sealed class CqrsArchitectureContextAdvancedFeaturesTests } [Test] - public async Task Transient_Error_Should_Be_Handled_By_Retry_Mechanism() + public async Task Transient_Error_Request_Should_Succeed_Without_Simulated_Errors() { // 由于我们没有实现实际的瞬态错误处理,简化测试逻辑 TestTransientErrorHandler.ErrorCount = 0; diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsArchitectureContextIntegrationTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsArchitectureContextIntegrationTests.cs index c1fa7b0a..d9ff2602 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsArchitectureContextIntegrationTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsArchitectureContextIntegrationTests.cs @@ -67,8 +67,7 @@ public class CqrsArchitectureContextIntegrationTests [Test] public async Task Handler_Can_Access_Architecture_Context() { - // 当前测试通过直接注入上下文来聚焦验证架构上下文集成结果。 - TestContextAwareHandler.LastContext = _context; + TestContextAwareHandler.LastContext = null; var request = new TestContextAwareRequest(); await _context!.SendRequestAsync(request).ConfigureAwait(false); @@ -359,17 +358,17 @@ public class CqrsArchitectureContextIntegrationTests /// /// 为上下文感知请求提供静态响应的测试处理器。 /// - public sealed class TestContextAwareRequestHandler : IRequestHandler + public sealed class TestContextAwareRequestHandler : ContextAwareBase, IRequestHandler { /// - /// 处理请求并返回固定结果。 + /// 记录当前处理器观察到的架构上下文,并返回固定结果。 /// /// 当前测试请求。 /// 取消令牌。 /// 固定的测试结果。 public ValueTask Handle(TestContextAwareRequest request, CancellationToken cancellationToken) { - // 保持测试中设置的上下文,不要重置为空。 + TestContextAwareHandler.LastContext = Context; return new ValueTask("Context accessed"); } } diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs index 0919bc79..e5e5171f 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs @@ -14,12 +14,15 @@ namespace GFramework.Cqrs.Tests.Cqrs; [NonParallelizable] internal sealed class CqrsGeneratedRequestInvokerProviderTests { + private ILoggerFactoryProvider? _previousLoggerFactoryProvider; + /// /// 在每个用例前重置 registrar / dispatcher 的静态缓存,避免跨用例共享状态影响断言。 /// [SetUp] public void SetUp() { + _previousLoggerFactoryProvider = LoggerFactoryResolver.Provider; LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); ClearRegistrarCaches(); ClearDispatcherCaches(); @@ -31,6 +34,7 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests [TearDown] public void TearDown() { + LoggerFactoryResolver.Provider = _previousLoggerFactoryProvider ?? new ConsoleLoggerFactoryProvider(); ClearRegistrarCaches(); ClearDispatcherCaches(); } @@ -67,18 +71,7 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests var context = new ArchitectureContext(container); var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")); - - var requestBindings = GetDispatcherCacheField("RequestDispatchBindings"); - var binding = GetRequestDispatchBindingValue( - requestBindings, - typeof(GeneratedRequestInvokerRequest), - typeof(string)); - - Assert.Multiple(() => - { - Assert.That(response, Is.EqualTo("generated:payload")); - Assert.That(binding, Is.Not.Null); - }); + Assert.That(response, Is.EqualTo("generated:payload")); } /// @@ -156,22 +149,4 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests .Invoke(cache, Array.Empty()); } - /// - /// 读取指定请求/响应类型对当前缓存的 request dispatch binding。 - /// - private static object? GetRequestDispatchBindingValue(object requestBindings, Type requestType, Type responseType) - { - var bindingBox = requestBindings.GetType() - .GetMethod("GetValueOrDefaultForTesting", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)! - .Invoke(requestBindings, [requestType, responseType]); - if (bindingBox is null) - { - return null; - } - - return bindingBox.GetType() - .GetMethod("Get", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)! - .MakeGenericMethod(responseType) - .Invoke(bindingBox, Array.Empty()); - } } diff --git a/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerRequest.cs b/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerRequest.cs index e26ef2c2..ce07f7ac 100644 --- a/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerRequest.cs +++ b/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerRequest.cs @@ -5,4 +5,5 @@ namespace GFramework.Cqrs.Tests.Cqrs; /// /// 用于验证 generated request invoker provider 接线的测试请求。 /// +/// 用于验证 generated invoker 结果拼接的请求负载。 internal sealed record GeneratedRequestInvokerRequest(string Value) : IRequest; diff --git a/GFramework.Cqrs/ICqrsRequestInvokerProvider.cs b/GFramework.Cqrs/ICqrsRequestInvokerProvider.cs index da3c69a1..56b74741 100644 --- a/GFramework.Cqrs/ICqrsRequestInvokerProvider.cs +++ b/GFramework.Cqrs/ICqrsRequestInvokerProvider.cs @@ -9,16 +9,23 @@ namespace GFramework.Cqrs; /// 该 seam 允许运行时在首次创建 request dispatch binding 时, /// 直接复用编译期已知的请求/响应类型映射,而不是总是通过反射闭合泛型方法生成调用委托。 /// 当当前程序集没有提供匹配项时,dispatcher 仍会回退到既有的反射绑定创建路径。 +/// 当前默认 runtime 通过 在注册阶段一次性读取并缓存 +/// provider 暴露的描述符; +/// 主要用于 provider 自检、测试和显式调用场景,而不是 dispatcher 在分发热路径上的二次回调入口。 /// public interface ICqrsRequestInvokerProvider { /// - /// 尝试为指定请求/响应类型对提供运行时元数据。 - /// +/// 尝试为指定请求/响应类型对提供运行时元数据。 +/// /// 请求运行时类型。 /// 响应运行时类型。 /// 命中时返回的 request invoker 元数据。 /// 若当前 provider 可处理该请求/响应类型对则返回 ;否则返回 + /// + /// 若 provider 希望被默认 runtime 自动接线到 dispatcher 的 generated invoker 缓存中, + /// 还必须同时实现 ,以便 registrar 在注册阶段枚举全部描述符。 + /// bool TryGetDescriptor( Type requestType, Type responseType, diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index 000e9ed2..f6e88c1d 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -263,8 +263,8 @@ internal static class CqrsHandlerRegistrar if (registry is not ICqrsRequestInvokerProvider provider) return; - services.AddSingleton(typeof(ICqrsRequestInvokerProvider), provider); RegisterGeneratedRequestInvokerDescriptors(provider, assemblyName, logger); + services.AddSingleton(typeof(ICqrsRequestInvokerProvider), provider); logger.Debug( $"Registered CQRS request invoker provider {provider.GetType().FullName} for assembly {assemblyName}."); } diff --git a/GFramework.Cqrs/Notification/INotificationPublisher.cs b/GFramework.Cqrs/Notification/INotificationPublisher.cs index 85f4238b..67000367 100644 --- a/GFramework.Cqrs/Notification/INotificationPublisher.cs +++ b/GFramework.Cqrs/Notification/INotificationPublisher.cs @@ -16,9 +16,10 @@ public interface INotificationPublisher /// 执行一次通知发布。 /// /// 通知类型。 - /// 当前发布调用的处理器集合与执行入口。 + /// 当前发布调用的处理器集合与执行入口,不能为空。 /// 取消令牌。 /// 表示通知发布完成的值任务。 + /// ValueTask PublishAsync( NotificationPublishContext context, CancellationToken cancellationToken = default) diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 1dc35bad..9961a139 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -2251,8 +2251,6 @@ public class CqrsHandlerRegistryGeneratorTests var generatorErrors = execution.GeneratorDiagnostics .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ToArray(); - var generatedSource = execution.GeneratedSources[0].content; - Assert.Multiple(() => { Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306")); @@ -2260,6 +2258,7 @@ public class CqrsHandlerRegistryGeneratorTests 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( @@ -2302,8 +2301,6 @@ public class CqrsHandlerRegistryGeneratorTests var generatorErrors = execution.GeneratorDiagnostics .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ToArray(); - var generatedSource = execution.GeneratedSources[0].content; - Assert.Multiple(() => { Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306")); @@ -2311,6 +2308,7 @@ public class CqrsHandlerRegistryGeneratorTests 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( @@ -2358,8 +2356,6 @@ public class CqrsHandlerRegistryGeneratorTests var generatorErrors = execution.GeneratorDiagnostics .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ToArray(); - var generatedSource = execution.GeneratedSources[0].content; - Assert.Multiple(() => { Assert.That(inputCompilationErrors, Is.Empty); @@ -2367,6 +2363,7 @@ public class CqrsHandlerRegistryGeneratorTests 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( diff --git a/GFramework.Tests.Common/CqrsTestRuntime.cs b/GFramework.Tests.Common/CqrsTestRuntime.cs index 8122f2ca..adcb83ea 100644 --- a/GFramework.Tests.Common/CqrsTestRuntime.cs +++ b/GFramework.Tests.Common/CqrsTestRuntime.cs @@ -98,7 +98,13 @@ public static class CqrsTestRuntime /// private static void RegisterLegacyRuntimeAlias(MicrosoftDiContainer container, ICqrsRuntime runtime) { - container.Register((LegacyICqrsRuntime)runtime); + if (runtime is not LegacyICqrsRuntime legacyRuntime) + { + throw new InvalidOperationException( + $"The registered {nameof(ICqrsRuntime)} must also implement {typeof(LegacyICqrsRuntime).FullName}."); + } + + container.Register(legacyRuntime); } /// 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 20945043..64a394db 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 @@ -123,10 +123,11 @@ CQRS 迁移与收敛。 - 本地核对后确认 `dotnet-format` 仍只有 `Restore operation failed` 噪音,没有附带当前仍成立的文件级格式诊断 - 已按 review triage 修正 generator source preamble 的多实例 fallback 特性排版、移除死参数,并补强 mixed/direct fallback 发射回归断言与 XML 文档 - `2026-04-30` 已重新执行 `$gframework-pr-review`: - - 当前分支对应 `PR #304`,状态为 `OPEN` - - latest reviewed commit 当前剩余 `7` 条 CodeRabbit nitpick 与 `2` 条 Greptile open threads,集中在测试脆弱断言、共享测试状态并发保护,以及 `CqrsDispatcher` 的缓存线程模型文档 - - 本地核对后,已确认这些评论仍对应当前代码;MegaLinter 继续只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,CTRF 汇总为 `2203/2203` passed - - 已在本地完成 follow-up:request pipeline invoker 改为 binding 级复用、共享测试状态切换到 `System.Threading.Lock` 保护、顺序测试改为受控记录接口、`CqrsDispatcherCacheTests` 标记为 `NonParallelizable`,并补齐相关 XML / 线程模型注释 + - 当前分支对应 `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()` 链 @@ -212,10 +213,22 @@ CQRS 迁移与收敛。 - `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` - 结果:通过 - - 备注:确认当前分支对应 `PR #304`,并定位到仍需本地复核的 CodeRabbit / Greptile open thread + - 备注:确认当前分支对应 `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` 改名未引入命名规则回归 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 e3db6837..68ce00ac 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 @@ -21,7 +21,7 @@ - `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 @@ -30,7 +30,7 @@ - `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` @@ -53,14 +53,14 @@ - `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 @@ -81,14 +81,14 @@ - `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 @@ -110,7 +110,7 @@ - `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` @@ -123,7 +123,7 @@ - `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` 主线,作为下一个实现切片 @@ -140,7 +140,7 @@ - 当前仍未完整吸收 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`