From 6056159866aaf4e0cd425b46e319133e412aebaf Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 7 May 2026 17:54:05 +0800 Subject: [PATCH] =?UTF-8?q?fix(core):=20=E6=94=B6=E5=8F=A3=20legacy=20cqrs?= =?UTF-8?q?=20bridge=20=E8=AF=84=E5=AE=A1=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 legacy bridge 测试装配与清理流程,改用 InternalsVisibleTo 和显式 handler 注册,补齐共享计数器重置与生命周期说明 - 优化 CommandExecutor、QueryExecutor 与相关模块的 runtime 契约,补充 XML 文档、nullable 注解和显式依赖解析 - 更新 legacy 异步 bridge 的取消语义、兼容文档回退边界以及 cqrs-rewrite active tracking/trace --- .../Architectures/ArchitectureContextTests.cs | 43 ++++++----- .../ArchitectureModulesBehaviorTests.cs | 75 +++++++++++-------- .../LegacyBridgePipelineTracker.cs | 10 ++- GFramework.Core/Command/CommandExecutor.cs | 22 ++++-- .../Cqrs/LegacyAsyncCommandDispatchRequest.cs | 10 ++- ...LegacyAsyncCommandResultDispatchRequest.cs | 3 + ...syncCommandResultDispatchRequestHandler.cs | 4 +- .../Cqrs/LegacyAsyncQueryDispatchRequest.cs | 3 + .../LegacyAsyncQueryDispatchRequestHandler.cs | 4 +- .../Cqrs/LegacyCommandDispatchRequest.cs | 10 ++- .../LegacyCommandResultDispatchRequest.cs | 3 + .../Cqrs/LegacyCqrsDispatchHandlerBase.cs | 5 ++ .../Cqrs/LegacyCqrsDispatchRequestBase.cs | 1 + .../Cqrs/LegacyQueryDispatchRequest.cs | 3 + GFramework.Core/Properties/AssemblyInfo.cs | 6 ++ GFramework.Core/Query/AsyncQueryExecutor.cs | 16 ++-- GFramework.Core/Query/QueryExecutor.cs | 8 +- .../Modules/AsyncQueryExecutorModule.cs | 12 ++- .../Services/Modules/CommandExecutorModule.cs | 12 ++- .../Services/Modules/QueryExecutorModule.cs | 12 ++- .../todos/cqrs-rewrite-migration-tracking.md | 33 ++++++-- .../traces/cqrs-rewrite-migration-trace.md | 32 ++++++++ docs/zh-CN/core/command.md | 1 + docs/zh-CN/core/context.md | 2 +- 24 files changed, 240 insertions(+), 90 deletions(-) create mode 100644 GFramework.Core/Properties/AssemblyInfo.cs diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs index 4c33f725..835930d0 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs @@ -10,6 +10,7 @@ using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Query; using GFramework.Core.Architectures; using GFramework.Core.Command; +using GFramework.Core.Cqrs; using GFramework.Core.Environment; using GFramework.Core.Events; using GFramework.Core.Ioc; @@ -45,6 +46,9 @@ namespace GFramework.Core.Tests.Architectures; [TestFixture] public class ArchitectureContextTests { + /// + /// 初始化测试所需的容器与默认服务实例。 + /// [SetUp] public void SetUp() { @@ -78,6 +82,16 @@ public class ArchitectureContextTests _context = new ArchitectureContext(_container); } + /// + /// 释放当前测试创建的容器,并清理 legacy bridge 共享计数状态。 + /// + [TearDown] + public void TearDown() + { + LegacyBridgePipelineTracker.Reset(); + _container?.Dispose(); + } + private AsyncQueryExecutor? _asyncQueryBus; private CommandExecutor? _commandBus; private MicrosoftDiContainer? _container; @@ -258,34 +272,19 @@ public class ArchitectureContextTests } /// - /// 通过反射把 GFramework.Core 内部的 legacy bridge handler 实例预先注册成可见的实例绑定。 + /// 把 GFramework.Core 内部的 legacy bridge handler 实例预先注册成可见的实例绑定。 /// /// 目标测试容器。 private static void RegisterLegacyBridgeHandlers(MicrosoftDiContainer container) { ArgumentNullException.ThrowIfNull(container); - string[] handlerTypeNames = - [ - "LegacyCommandDispatchRequestHandler", - "LegacyCommandResultDispatchRequestHandler", - "LegacyAsyncCommandDispatchRequestHandler", - "LegacyAsyncCommandResultDispatchRequestHandler", - "LegacyQueryDispatchRequestHandler", - "LegacyAsyncQueryDispatchRequestHandler" - ]; - - var coreAssembly = typeof(ArchitectureContext).Assembly; - - foreach (var handlerTypeName in handlerTypeNames) - { - var handlerType = coreAssembly.GetType($"GFramework.Core.Cqrs.{handlerTypeName}") - ?? throw new InvalidOperationException($"Bridge handler type '{handlerTypeName}' was not found."); - var handlerInstance = Activator.CreateInstance(handlerType) - ?? throw new InvalidOperationException( - $"Bridge handler type '{handlerType.FullName}' could not be instantiated."); - container.RegisterPlurality(handlerInstance); - } + container.RegisterPlurality(new LegacyCommandDispatchRequestHandler()); + container.RegisterPlurality(new LegacyCommandResultDispatchRequestHandler()); + container.RegisterPlurality(new LegacyAsyncCommandDispatchRequestHandler()); + container.RegisterPlurality(new LegacyAsyncCommandResultDispatchRequestHandler()); + container.RegisterPlurality(new LegacyQueryDispatchRequestHandler()); + container.RegisterPlurality(new LegacyAsyncQueryDispatchRequestHandler()); } /// diff --git a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs index ff9880f5..4c0b86f8 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs @@ -37,6 +37,7 @@ public class ArchitectureModulesBehaviorTests { GameContext.Clear(); TrackingPipelineBehavior.InvocationCount = 0; + LegacyBridgePipelineTracker.Reset(); } /// @@ -49,15 +50,19 @@ public class ArchitectureModulesBehaviorTests var architecture = new ModuleTestArchitecture(target => target.InstallModule(module)); await architecture.InitializeAsync(); - - Assert.Multiple(() => + try { - Assert.That(module.InstalledArchitecture, Is.SameAs(architecture)); - Assert.That(module.InstallCallCount, Is.EqualTo(1)); - Assert.That(architecture.Context.GetUtility(), Is.Not.Null); - }); - - await architecture.DestroyAsync(); + Assert.Multiple(() => + { + Assert.That(module.InstalledArchitecture, Is.SameAs(architecture)); + Assert.That(module.InstallCallCount, Is.EqualTo(1)); + Assert.That(architecture.Context.GetUtility(), Is.Not.Null); + }); + } + finally + { + await architecture.DestroyAsync(); + } } /// @@ -70,16 +75,20 @@ public class ArchitectureModulesBehaviorTests target.RegisterCqrsPipelineBehavior>()); await architecture.InitializeAsync(); - - var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest()); - - Assert.Multiple(() => + try { - Assert.That(response, Is.EqualTo("handled")); - Assert.That(TrackingPipelineBehavior.InvocationCount, Is.EqualTo(1)); - }); + var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest()); - await architecture.DestroyAsync(); + Assert.Multiple(() => + { + Assert.That(response, Is.EqualTo("handled")); + Assert.That(TrackingPipelineBehavior.InvocationCount, Is.EqualTo(1)); + }); + } + finally + { + await architecture.DestroyAsync(); + } } /// @@ -93,23 +102,27 @@ public class ArchitectureModulesBehaviorTests var architecture = new LegacyBridgeArchitecture(); await architecture.InitializeAsync(); - - var query = new LegacyArchitectureBridgeQuery(); - var command = new LegacyArchitectureBridgeCommand(); - - var queryResult = architecture.Context.SendQuery(query); - architecture.Context.SendCommand(command); - - Assert.Multiple(() => + try { - Assert.That(queryResult, Is.EqualTo(24)); - Assert.That(query.ObservedContext, Is.SameAs(architecture.Context)); - Assert.That(command.Executed, Is.True); - Assert.That(command.ObservedContext, Is.SameAs(architecture.Context)); - Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(2)); - }); + var query = new LegacyArchitectureBridgeQuery(); + var command = new LegacyArchitectureBridgeCommand(); - await architecture.DestroyAsync(); + var queryResult = architecture.Context.SendQuery(query); + architecture.Context.SendCommand(command); + + Assert.Multiple(() => + { + Assert.That(queryResult, Is.EqualTo(24)); + Assert.That(query.ObservedContext, Is.SameAs(architecture.Context)); + Assert.That(command.Executed, Is.True); + Assert.That(command.ObservedContext, Is.SameAs(architecture.Context)); + Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(2)); + }); + } + finally + { + await architecture.DestroyAsync(); + } } /// diff --git a/GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs b/GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs index 27ce3d4e..599e8277 100644 --- a/GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs +++ b/GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs @@ -2,12 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 using System.Threading; +using GFramework.Core.Cqrs; namespace GFramework.Core.Tests.Architectures; /// /// 为 legacy bridge pipeline 回归测试保存跨泛型闭包共享的计数状态。 /// +/// +/// 该计数器通过 原子递增,并使用 +/// 读写,因此单次读写操作本身是线程安全的。 +/// 由于状态在同一进程内跨 fixture 共享,所有使用它的测试都必须在清理阶段调用 , +/// 以避免并行或失败测试把旧计数泄露给后续断言。 +/// public static class LegacyBridgePipelineTracker { private static int _invocationCount; @@ -32,8 +39,7 @@ public static class LegacyBridgePipelineTracker { ArgumentNullException.ThrowIfNull(requestType); - if (string.Equals(requestType.Namespace, "GFramework.Core.Cqrs", StringComparison.Ordinal) && - requestType.Name.Contains("Legacy", StringComparison.Ordinal)) + if (typeof(LegacyCqrsDispatchRequestBase).IsAssignableFrom(requestType)) { Interlocked.Increment(ref _invocationCount); } diff --git a/GFramework.Core/Command/CommandExecutor.cs b/GFramework.Core/Command/CommandExecutor.cs index 355379e3..fd401c9d 100644 --- a/GFramework.Core/Command/CommandExecutor.cs +++ b/GFramework.Core/Command/CommandExecutor.cs @@ -1,6 +1,7 @@ // Copyright (c) 2025-2026 GeWuYou // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics.CodeAnalysis; using GFramework.Core.Abstractions.Command; using GFramework.Core.Abstractions.Rule; using GFramework.Core.Cqrs; @@ -77,7 +78,7 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec { ArgumentNullException.ThrowIfNull(command); - if (TryResolveDispatchContext(command, out var context) && _runtime is not null) + if (TryResolveDispatchContext(command, out var context)) { return _runtime.SendAsync(context, new LegacyAsyncCommandDispatchRequest(command)).AsTask(); } @@ -96,9 +97,9 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec { ArgumentNullException.ThrowIfNull(command); - if (TryResolveDispatchContext(command, out var context) && _runtime is not null) + if (TryResolveDispatchContext(command, out var context)) { - return BridgeAsyncCommandWithResultAsync(context, command); + return BridgeAsyncCommandWithResultAsync(_runtime, context, command); } return command.ExecuteAsync(); @@ -118,7 +119,7 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec where TTarget : class where TRequest : IRequest { - if (!TryResolveDispatchContext(target, out var context) || _runtime is null) + if (!TryResolveDispatchContext(target, out var context)) { return false; } @@ -144,7 +145,7 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec where TTarget : class where TRequest : IRequest { - if (!TryResolveDispatchContext(target, out var context) || _runtime is null) + if (!TryResolveDispatchContext(target, out var context)) { result = default; return false; @@ -159,14 +160,16 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec /// 通过统一 CQRS runtime 异步执行 legacy 带返回值命令,并把装箱结果还原为目标类型。 /// /// 命令返回值类型。 + /// 负责调度当前 bridge request 的统一 CQRS runtime。 /// 当前架构上下文。 /// 要桥接的 legacy 命令。 /// 命令执行结果。 - private async Task BridgeAsyncCommandWithResultAsync( + private static async Task BridgeAsyncCommandWithResultAsync( + ICqrsRuntime runtime, GFramework.Core.Abstractions.Architectures.IArchitectureContext context, IAsyncCommand command) { - var boxedResult = await _runtime!.SendAsync( + var boxedResult = await runtime.SendAsync( context, new LegacyAsyncCommandResultDispatchRequest( command, @@ -181,7 +184,10 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec /// 即将执行的 legacy 目标对象。 /// 命中时返回可用于 CQRS runtime 的架构上下文。 /// 如果既接入了 runtime 且目标对象提供了上下文,则返回 - private bool TryResolveDispatchContext(object target, out GFramework.Core.Abstractions.Architectures.IArchitectureContext context) + [MemberNotNullWhen(true, nameof(_runtime))] + private bool TryResolveDispatchContext( + object target, + out GFramework.Core.Abstractions.Architectures.IArchitectureContext context) { context = null!; diff --git a/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequest.cs b/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequest.cs index b69f369b..882f5295 100644 --- a/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequest.cs +++ b/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequest.cs @@ -9,11 +9,17 @@ namespace GFramework.Core.Cqrs; /// /// 包装 legacy 异步无返回值命令,使其能够通过自有 CQRS runtime 调度。 /// +/// 当前 bridge request 代理的 legacy 异步命令实例。 internal sealed class LegacyAsyncCommandDispatchRequest(CoreCommand.IAsyncCommand command) - : LegacyCqrsDispatchRequestBase(command), IRequest + : LegacyCqrsDispatchRequestBase(ValidateCommand(command)), IRequest { /// /// 获取当前 bridge request 代理的异步命令实例。 /// - public CoreCommand.IAsyncCommand Command { get; } = command ?? throw new ArgumentNullException(nameof(command)); + public CoreCommand.IAsyncCommand Command { get; } = command; + + private static CoreCommand.IAsyncCommand ValidateCommand(CoreCommand.IAsyncCommand command) + { + return command ?? throw new ArgumentNullException(nameof(command)); + } } diff --git a/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequest.cs b/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequest.cs index 18f4cd5d..fe374f30 100644 --- a/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequest.cs +++ b/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequest.cs @@ -8,6 +8,8 @@ namespace GFramework.Core.Cqrs; /// /// 包装 legacy 异步带返回值命令,使其能够通过自有 CQRS runtime 调度。 /// +/// 需要在 bridge handler 中接收上下文注入的 legacy 命令目标实例。 +/// 封装 legacy 异步命令执行逻辑并返回装箱结果的委托。 internal sealed class LegacyAsyncCommandResultDispatchRequest(object target, Func> executeAsync) : LegacyCqrsDispatchRequestBase(target), IRequest { @@ -16,5 +18,6 @@ internal sealed class LegacyAsyncCommandResultDispatchRequest(object target, Fun /// /// 异步执行底层 legacy 命令并返回装箱后的结果。 /// + /// 表示异步执行结果的任务;任务结果为底层 legacy 命令返回的装箱值。 public Task ExecuteAsync() => _executeAsync(); } diff --git a/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequestHandler.cs b/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequestHandler.cs index d58a661f..8296cd38 100644 --- a/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequestHandler.cs +++ b/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequestHandler.cs @@ -17,7 +17,9 @@ internal sealed class LegacyAsyncCommandResultDispatchRequestHandler CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); + // Legacy ExecuteAsync contract does not accept CancellationToken; use WaitAsync so the caller can observe cancellation promptly. + cancellationToken.ThrowIfCancellationRequested(); PrepareTarget(request.Target); - return await request.ExecuteAsync().ConfigureAwait(false); + return await request.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false); } } diff --git a/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequest.cs b/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequest.cs index 113a3a3b..679d37b2 100644 --- a/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequest.cs +++ b/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequest.cs @@ -8,6 +8,8 @@ namespace GFramework.Core.Cqrs; /// /// 包装 legacy 异步查询,使其能够通过自有 CQRS runtime 调度。 /// +/// 需要在 bridge handler 中接收上下文注入的 legacy 查询目标实例。 +/// 封装 legacy 异步查询执行逻辑并返回装箱结果的委托。 internal sealed class LegacyAsyncQueryDispatchRequest(object target, Func> executeAsync) : LegacyCqrsDispatchRequestBase(target), IRequest { @@ -16,5 +18,6 @@ internal sealed class LegacyAsyncQueryDispatchRequest(object target, Func /// 异步执行底层 legacy 查询并返回装箱后的结果。 /// + /// 表示异步执行结果的任务;任务结果为底层 legacy 查询返回的装箱值。 public Task ExecuteAsync() => _executeAsync(); } diff --git a/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequestHandler.cs b/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequestHandler.cs index b9bc848e..6aa48590 100644 --- a/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequestHandler.cs +++ b/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequestHandler.cs @@ -17,7 +17,9 @@ internal sealed class LegacyAsyncQueryDispatchRequestHandler CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); + // Legacy DoAsync contract does not accept CancellationToken; use WaitAsync so the caller can observe cancellation promptly. + cancellationToken.ThrowIfCancellationRequested(); PrepareTarget(request.Target); - return await request.ExecuteAsync().ConfigureAwait(false); + return await request.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false); } } diff --git a/GFramework.Core/Cqrs/LegacyCommandDispatchRequest.cs b/GFramework.Core/Cqrs/LegacyCommandDispatchRequest.cs index bbb66713..9010de14 100644 --- a/GFramework.Core/Cqrs/LegacyCommandDispatchRequest.cs +++ b/GFramework.Core/Cqrs/LegacyCommandDispatchRequest.cs @@ -9,11 +9,17 @@ namespace GFramework.Core.Cqrs; /// /// 包装 legacy 无返回值命令,使其能够通过自有 CQRS runtime 调度。 /// +/// 当前 bridge request 代理的 legacy 命令实例。 internal sealed class LegacyCommandDispatchRequest(CoreCommand.ICommand command) - : LegacyCqrsDispatchRequestBase(command), IRequest + : LegacyCqrsDispatchRequestBase(ValidateCommand(command)), IRequest { /// /// 获取当前 bridge request 代理的命令实例。 /// - public CoreCommand.ICommand Command { get; } = command ?? throw new ArgumentNullException(nameof(command)); + public CoreCommand.ICommand Command { get; } = command; + + private static CoreCommand.ICommand ValidateCommand(CoreCommand.ICommand command) + { + return command ?? throw new ArgumentNullException(nameof(command)); + } } diff --git a/GFramework.Core/Cqrs/LegacyCommandResultDispatchRequest.cs b/GFramework.Core/Cqrs/LegacyCommandResultDispatchRequest.cs index e4c2de40..d6e03342 100644 --- a/GFramework.Core/Cqrs/LegacyCommandResultDispatchRequest.cs +++ b/GFramework.Core/Cqrs/LegacyCommandResultDispatchRequest.cs @@ -8,6 +8,8 @@ namespace GFramework.Core.Cqrs; /// /// 包装 legacy 带返回值命令,使其能够通过自有 CQRS runtime 调度。 /// +/// 需要在 bridge handler 中接收上下文注入的 legacy 命令目标实例。 +/// 封装 legacy 命令执行逻辑并返回装箱结果的委托。 internal sealed class LegacyCommandResultDispatchRequest(object target, Func execute) : LegacyCqrsDispatchRequestBase(target), IRequest { @@ -16,5 +18,6 @@ internal sealed class LegacyCommandResultDispatchRequest(object target, Func /// 执行底层 legacy 命令并返回装箱后的结果。 /// + /// 底层 legacy 命令执行后的装箱结果;若命令语义无返回值则为 public object? Execute() => _execute(); } diff --git a/GFramework.Core/Cqrs/LegacyCqrsDispatchHandlerBase.cs b/GFramework.Core/Cqrs/LegacyCqrsDispatchHandlerBase.cs index 1745465f..d2fabccb 100644 --- a/GFramework.Core/Cqrs/LegacyCqrsDispatchHandlerBase.cs +++ b/GFramework.Core/Cqrs/LegacyCqrsDispatchHandlerBase.cs @@ -14,6 +14,11 @@ internal abstract class LegacyCqrsDispatchHandlerBase : ContextAwareBase /// /// 在执行 legacy 命令或查询前,把当前架构上下文显式注入给支持 的目标对象。 /// + /// 即将执行的 legacy 目标对象。 + /// + /// + /// 目标对象实现了 ,但当前 handler 还没有可用的架构上下文。 + /// protected void PrepareTarget(object target) { ArgumentNullException.ThrowIfNull(target); diff --git a/GFramework.Core/Cqrs/LegacyCqrsDispatchRequestBase.cs b/GFramework.Core/Cqrs/LegacyCqrsDispatchRequestBase.cs index 11370c95..8e0d6dfc 100644 --- a/GFramework.Core/Cqrs/LegacyCqrsDispatchRequestBase.cs +++ b/GFramework.Core/Cqrs/LegacyCqrsDispatchRequestBase.cs @@ -6,6 +6,7 @@ namespace GFramework.Core.Cqrs; /// /// 为 legacy Command / Query 到自有 CQRS runtime 的桥接请求提供共享的目标对象封装。 /// +/// 需要在 bridge handler 中接收上下文注入的 legacy 目标对象。 internal abstract class LegacyCqrsDispatchRequestBase(object target) { /// diff --git a/GFramework.Core/Cqrs/LegacyQueryDispatchRequest.cs b/GFramework.Core/Cqrs/LegacyQueryDispatchRequest.cs index 4eb2c15f..d87cf99f 100644 --- a/GFramework.Core/Cqrs/LegacyQueryDispatchRequest.cs +++ b/GFramework.Core/Cqrs/LegacyQueryDispatchRequest.cs @@ -8,6 +8,8 @@ namespace GFramework.Core.Cqrs; /// /// 包装 legacy 同步查询,使其能够通过自有 CQRS runtime 调度。 /// +/// 需要在 bridge handler 中接收上下文注入的 legacy 查询目标实例。 +/// 封装 legacy 查询执行逻辑并返回装箱结果的委托。 internal sealed class LegacyQueryDispatchRequest(object target, Func execute) : LegacyCqrsDispatchRequestBase(target), IRequest { @@ -16,5 +18,6 @@ internal sealed class LegacyQueryDispatchRequest(object target, Func ex /// /// 执行底层 legacy 查询并返回装箱后的结果。 /// + /// 底层 legacy 查询执行后的装箱结果;若查询无返回值则为 public object? Execute() => _execute(); } diff --git a/GFramework.Core/Properties/AssemblyInfo.cs b/GFramework.Core/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..cbf402ed --- /dev/null +++ b/GFramework.Core/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("GFramework.Core.Tests")] diff --git a/GFramework.Core/Query/AsyncQueryExecutor.cs b/GFramework.Core/Query/AsyncQueryExecutor.cs index e1b04300..f59b63f9 100644 --- a/GFramework.Core/Query/AsyncQueryExecutor.cs +++ b/GFramework.Core/Query/AsyncQueryExecutor.cs @@ -1,6 +1,7 @@ // Copyright (c) 2025-2026 GeWuYou // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics.CodeAnalysis; using GFramework.Core.Abstractions.Query; using GFramework.Core.Abstractions.Rule; using GFramework.Core.Cqrs; @@ -30,9 +31,9 @@ public sealed class AsyncQueryExecutor(ICqrsRuntime? runtime = null) : IAsyncQue { ArgumentNullException.ThrowIfNull(query); - if (TryResolveDispatchContext(query, out var context) && _runtime is not null) + if (TryResolveDispatchContext(query, out var context)) { - return BridgeAsyncQueryAsync(context, query); + return BridgeAsyncQueryAsync(_runtime, context, query); } return query.DoAsync(); @@ -42,14 +43,16 @@ public sealed class AsyncQueryExecutor(ICqrsRuntime? runtime = null) : IAsyncQue /// 通过统一 CQRS runtime 异步执行 legacy 查询,并把装箱结果还原为目标类型。 /// /// 查询结果类型。 + /// 负责调度当前 bridge request 的统一 CQRS runtime。 /// 当前架构上下文。 /// 要桥接的 legacy 查询。 /// 查询执行结果。 - private async Task BridgeAsyncQueryAsync( + private static async Task BridgeAsyncQueryAsync( + ICqrsRuntime runtime, GFramework.Core.Abstractions.Architectures.IArchitectureContext context, IAsyncQuery query) { - var boxedResult = await _runtime!.SendAsync( + var boxedResult = await runtime.SendAsync( context, new LegacyAsyncQueryDispatchRequest( query, @@ -64,7 +67,10 @@ public sealed class AsyncQueryExecutor(ICqrsRuntime? runtime = null) : IAsyncQue /// 即将执行的 legacy 查询对象。 /// 命中时返回可用于 CQRS runtime 的架构上下文。 /// 如果既接入了 runtime 且查询对象提供了上下文,则返回 - private bool TryResolveDispatchContext(object query, out GFramework.Core.Abstractions.Architectures.IArchitectureContext context) + [MemberNotNullWhen(true, nameof(_runtime))] + private bool TryResolveDispatchContext( + object query, + out GFramework.Core.Abstractions.Architectures.IArchitectureContext context) { context = null!; diff --git a/GFramework.Core/Query/QueryExecutor.cs b/GFramework.Core/Query/QueryExecutor.cs index c8f2fdd2..2ecf0960 100644 --- a/GFramework.Core/Query/QueryExecutor.cs +++ b/GFramework.Core/Query/QueryExecutor.cs @@ -1,6 +1,7 @@ // Copyright (c) 2025-2026 GeWuYou // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics.CodeAnalysis; using GFramework.Core.Abstractions.Query; using GFramework.Core.Abstractions.Rule; using GFramework.Core.Cqrs; @@ -35,7 +36,7 @@ public sealed class QueryExecutor(ICqrsRuntime? runtime = null) : IQueryExecutor { ArgumentNullException.ThrowIfNull(query); - if (TryResolveDispatchContext(query, out var context) && _runtime is not null) + if (TryResolveDispatchContext(query, out var context)) { var boxedResult = _runtime.SendAsync( context, @@ -57,7 +58,10 @@ public sealed class QueryExecutor(ICqrsRuntime? runtime = null) : IQueryExecutor /// 即将执行的 legacy 查询对象。 /// 命中时返回可用于 CQRS runtime 的架构上下文。 /// 如果既接入了 runtime 且查询对象提供了上下文,则返回 - private bool TryResolveDispatchContext(object query, out GFramework.Core.Abstractions.Architectures.IArchitectureContext context) + [MemberNotNullWhen(true, nameof(_runtime))] + private bool TryResolveDispatchContext( + object query, + out GFramework.Core.Abstractions.Architectures.IArchitectureContext context) { context = null!; diff --git a/GFramework.Core/Services/Modules/AsyncQueryExecutorModule.cs b/GFramework.Core/Services/Modules/AsyncQueryExecutorModule.cs index e1a0524d..2d46354d 100644 --- a/GFramework.Core/Services/Modules/AsyncQueryExecutorModule.cs +++ b/GFramework.Core/Services/Modules/AsyncQueryExecutorModule.cs @@ -33,11 +33,19 @@ public sealed class AsyncQueryExecutorModule : IServiceModule /// 注册异步查询执行器到依赖注入容器。 /// 创建异步查询执行器实例并将其注册为多例服务。 /// - /// 依赖注入容器实例。 + /// 承载异步查询执行器与 CQRS runtime 的依赖注入容器实例。 + /// + /// + /// 容器中尚未注册唯一的 实例,无法构建统一 runtime 版本的异步查询执行器。 + /// + /// + /// 该模块会在注册阶段立即解析 ,因此 + /// 必须先于当前模块完成注册。 + /// public void Register(IIocContainer container) { ArgumentNullException.ThrowIfNull(container); - container.RegisterPlurality(new AsyncQueryExecutor(container.Get())); + container.RegisterPlurality(new AsyncQueryExecutor(container.GetRequired())); } /// diff --git a/GFramework.Core/Services/Modules/CommandExecutorModule.cs b/GFramework.Core/Services/Modules/CommandExecutorModule.cs index 3d327cda..773cdccd 100644 --- a/GFramework.Core/Services/Modules/CommandExecutorModule.cs +++ b/GFramework.Core/Services/Modules/CommandExecutorModule.cs @@ -33,11 +33,19 @@ public sealed class CommandExecutorModule : IServiceModule /// 注册命令执行器到依赖注入容器。 /// 创建命令执行器实例并将其注册为多例服务。 /// - /// 依赖注入容器实例。 + /// 承载命令执行器与 CQRS runtime 的依赖注入容器实例。 + /// + /// + /// 容器中尚未注册唯一的 实例,无法构建统一 runtime 版本的命令执行器。 + /// + /// + /// 该模块会在注册阶段立即解析 ,因此 + /// 必须先于当前模块完成注册。 + /// public void Register(IIocContainer container) { ArgumentNullException.ThrowIfNull(container); - container.RegisterPlurality(new CommandExecutor(container.Get())); + container.RegisterPlurality(new CommandExecutor(container.GetRequired())); } /// diff --git a/GFramework.Core/Services/Modules/QueryExecutorModule.cs b/GFramework.Core/Services/Modules/QueryExecutorModule.cs index 40e1eb91..41edbe22 100644 --- a/GFramework.Core/Services/Modules/QueryExecutorModule.cs +++ b/GFramework.Core/Services/Modules/QueryExecutorModule.cs @@ -33,11 +33,19 @@ public sealed class QueryExecutorModule : IServiceModule /// 注册查询执行器到依赖注入容器。 /// 创建查询执行器实例并将其注册为多例服务。 /// - /// 依赖注入容器实例。 + /// 承载查询执行器与 CQRS runtime 的依赖注入容器实例。 + /// + /// + /// 容器中尚未注册唯一的 实例,无法构建统一 runtime 版本的查询执行器。 + /// + /// + /// 该模块会在注册阶段立即解析 ,因此 + /// 必须先于当前模块完成注册。 + /// public void Register(IIocContainer container) { ArgumentNullException.ThrowIfNull(container); - container.RegisterPlurality(new QueryExecutor(container.Get())); + container.RegisterPlurality(new QueryExecutor(container.GetRequired())); } /// 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 0d6ed018..ab97559e 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,9 +7,9 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-093` +- 恢复点编号:`CQRS-REWRITE-RP-094` - 当前阶段:`Phase 8` -- 当前 PR 锚点:`待创建(当前分支 feat/cqrs-optimization 尚未为 RP-092 建立新 PR)` +- 当前 PR 锚点:`PR #334` - 当前结论: - `GFramework.Cqrs` 已完成对外部 `Mediator` 的生产级替代,当前主线已从“是否可替代”转向“仓库内部收口与能力深化顺序” - `dispatch/invoker` 生成前移已扩展到 request / stream 路径,`RP-077` 已补齐 request invoker provider gate 与 stream gate 对称的 descriptor / descriptor entry runtime 合同回归 @@ -28,8 +28,9 @@ CQRS 迁移与收敛。 - 当前 `RP-090` 已收敛 `PR #326` benchmark review:统一 benchmark 最小宿主构建、冻结 GFramework 容器、限制 MediatR 扫描范围,并恢复 request startup cold-start 对照 - 当前 `RP-091` 已把 benchmark 项目发布面隔离与包清单校验前移到 PR:`GFramework.Cqrs.Benchmarks` 明确保持不可打包,`publish.yml` 与 `ci.yml` 复用同一份 packed-modules 校验脚本 - `RP-092` 已补齐 request handler `Singleton / Transient` 生命周期矩阵 benchmark,并明确把 `Scoped` 对照留到具备真实显式作用域边界的宿主模型后再评估 - - 当前 `RP-093` 已把 `GFramework.Core` 的 legacy `SendCommand` / `SendQuery` 兼容入口收敛到底层统一 `GFramework.Cqrs` runtime,同时补充 `Mediator` 未吸收能力差距复核 -- `ai-plan` active 入口现以 `RP-093` 为最新恢复锚点;`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准 + - `RP-093` 已把 `GFramework.Core` 的 legacy `SendCommand` / `SendQuery` 兼容入口收敛到底层统一 `GFramework.Cqrs` runtime,同时补充 `Mediator` 未吸收能力差距复核 + - 当前 `RP-094` 已按 `PR #334` latest-head review 收口 legacy bridge 的测试注册方式、模块运行时依赖契约、异步取消语义、XML 文档缺口与兼容文档回退边界 +- `ai-plan` active 入口现以 `RP-094` 为最新恢复锚点;`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准 ## 当前活跃事实 @@ -41,6 +42,9 @@ CQRS 迁移与收敛。 - `GFramework.Core` 当前已通过内部 bridge request / handler 把 legacy `ICommand`、`IAsyncCommand`、`IQuery`、`IAsyncQuery` 接到统一 `ICqrsRuntime` - 标准 `Architecture` 初始化路径会自动扫描 `GFramework.Core` 程序集中的 legacy bridge handler,因此旧 `SendCommand(...)` / `SendQuery(...)` 无需改变用法即可进入统一 pipeline - `CommandExecutor`、`QueryExecutor`、`AsyncQueryExecutor` 仍保留“无 runtime 时直接执行”的回退路径,用于不依赖容器的隔离单元测试 +- `GFramework.Core.Tests` 现通过 `InternalsVisibleTo("GFramework.Core.Tests")` 直接实例化内部 bridge handler,不再依赖字符串反射装配测试桥接注册 +- `CommandExecutorModule`、`QueryExecutorModule`、`AsyncQueryExecutorModule` 现改为 `GetRequired()` 并在 XML 文档里显式声明注册顺序契约,避免 runtime 缺失时静默回退 +- `LegacyAsyncQueryDispatchRequestHandler` 与 `LegacyAsyncCommandResultDispatchRequestHandler` 现通过 `ThrowIfCancellationRequested()` + `WaitAsync(cancellationToken)` 显式保留调用方取消可见性 - 相对 `ai-libs/Mediator`,当前仍未完全吸收的能力集中在六类:facade 公开入口、telemetry、stream pipeline、notification publisher 策略、生成器配置与诊断、生命周期/缓存公开配置面 - 发布工作流已有 packed modules 校验,但 PR 工作流此前没有等价的 solution pack 产物名单校验 - 本地 `dotnet pack GFramework.sln -c Release --no-restore -o ` 当前只产出 14 个预期包,未复现 benchmark `.nupkg` @@ -51,6 +55,7 @@ CQRS 迁移与收敛。 - 已新增手动触发的 benchmark workflow;默认只验证 benchmark 项目 Release build,只有显式提供过滤器时才执行 BenchmarkDotNet 运行;过滤器输入现通过环境变量传入 shell,避免 workflow_dispatch 输入直接插值到命令行 - 远端 `CTRF` 最新汇总为 `2274/2274` passed - `MegaLinter` 当前只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,尚未提供本地仍成立的文件级格式诊断 +- `PR #334` 当前 latest-head open AI feedback 已集中到 legacy bridge / 文档收尾;本轮本地修复后,剩余 thread 应主要是待 GitHub 重新索引的状态差异或低价值建议 ## 当前风险 @@ -61,6 +66,7 @@ CQRS 迁移与收敛。 - 仓库内部仍保留旧 `Command` / `Query` API、`LegacyICqrsRuntime` alias 与部分历史命名语义,后续若不继续分批收口,容易混淆“对外替代已完成”与“内部收口未完成” - 若继续扩大 generated invoker 覆盖面,需要持续区分“可静态表达的合同”与 `PreciseReflectedRegistrationSpec` 等仍需保守回退的场景 - legacy bridge 当前只为已有 `Command` / `Query` 兼容入口接到统一 request pipeline;若后续要继续对齐 `Mediator`,仍需要单独设计 stream pipeline、telemetry 与 facade 公开面,而不是把这次 bridge 当成“全部收口完成” +- `LegacyBridgePipelineTracker` 仍是进程级静态测试辅助;虽然现在已在相关 fixture 清理阶段重置并补充线程安全说明,但若将来扩大并行 bridge fixture 数量,仍要继续控制共享状态扩散 ## 最近权威验证 @@ -108,12 +114,25 @@ CQRS 迁移与收敛。 - 结果:通过 - `git diff --check` - 结果:通过 +- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json` + - 结果:通过 + - 备注:确认当前分支对应 `PR #334`;仍有效的 latest-head review 已收敛到 legacy bridge 测试装配、运行时依赖契约、异步取消、XML 文档与兼容文档边界 +- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - 备注:修复新增 XML 文档 warning 后复跑,当前 `GFramework.Core` 三个 target framework 均已干净通过 +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests|FullyQualifiedName~ArchitectureModulesBehaviorTests|FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests"` + - 结果:通过,`48/48` passed + - 备注:覆盖 legacy bridge 兼容入口、测试装配、执行器 runtime fallback 与相关模块行为 +- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check` + - 结果:通过 +- `git diff --check` + - 结果:通过 ## 下一推荐步骤 -1. 若继续沿用 `$gframework-batch-boot 50` 且优先处理 `Mediator` 能力吸收,下一批建议从 `stream pipeline` 或 `notification publisher` 策略中选择一个独立切片推进 -2. 若继续收敛 legacy Core CQRS,可评估是否补一个 `IMediator` 风格 facade,而不是继续扩大 `ArchitectureContext` 兼容入口的职责 -3. 若回到 benchmark 方向,优先补 `stream handler` 生命周期矩阵;若要扩到 `Scoped` 生命周期,先为 benchmark 宿主设计真实显式 scope 基线 +1. 先用新提交和最新 CI 再跑一次 `$gframework-pr-review`,确认 `PR #334` 的 latest-head open threads 是否已实质清空 +2. 若继续沿用 `$gframework-batch-boot 50` 且优先处理 `Mediator` 能力吸收,下一批建议从 `stream pipeline` 或 `notification publisher` 策略中选择一个独立切片推进 +3. 若继续收敛 legacy Core CQRS,可评估是否补一个 `IMediator` 风格 facade,而不是继续扩大 `ArchitectureContext` 兼容入口的职责 ## 活跃文档 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 76264060..25255efc 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,38 @@ ## 2026-05-07 +### 阶段:PR #334 legacy bridge / 文档 review 收尾(CQRS-REWRITE-RP-094) + +- 使用 `$gframework-pr-review` 抓取当前分支公开 PR,确认 `feat/cqrs-optimization` 当前对应 `PR #334` +- latest-head open AI review 复核后,主线程接受并执行的修复集中在六类: + - `GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs` 通过字符串字面量反射实例化内部 bridge handler,维护成本高且不利于 rename-safe 重构 + - `ArchitectureModulesBehaviorTests` 在断言失败路径下未保证 `DestroyAsync()` 执行,且 `TearDown` 未重置 `LegacyBridgePipelineTracker` + - `LegacyBridgePipelineTracker` 以静态共享计数器记录 bridge pipeline 命中,但未文档化线程安全语义,且用字符串匹配类型名识别 bridge request + - `LegacyAsyncQueryDispatchRequestHandler` / `LegacyAsyncCommandResultDispatchRequestHandler` 丢弃了 runtime 传入的 `CancellationToken` + - `CommandExecutorModule` / `QueryExecutorModule` / `AsyncQueryExecutorModule` 依赖 `container.Get()` 的隐式注册顺序,但此前既未显式失败,也未写进 API 契约 + - 多个 legacy bridge request / docs 页面仍缺 XML 文档或回退边界说明 +- 本轮主线程决策: + - 为 `GFramework.Core` 新增 `Properties/AssemblyInfo.cs`,用 `InternalsVisibleTo("GFramework.Core.Tests")` 让测试直接实例化内部 handler + - 把 `ArchitectureContextTests.RegisterLegacyBridgeHandlers` 改成显式构造 6 个 handler,移除字符串反射装配 + - 为 bridge 相关测试补 `TearDown` 清理和 `try/finally` 销毁,减少失败路径资源泄露 + - 为 `LegacyBridgePipelineTracker` 增补 ``,并改用 `typeof(LegacyCqrsDispatchRequestBase).IsAssignableFrom(requestType)` 识别 bridge request + - 为 `LegacyAsyncQueryDispatchRequestHandler` / `LegacyAsyncCommandResultDispatchRequestHandler` 加入预取消检查与 `WaitAsync(cancellationToken)` + - 将三个 executor module 改为 `GetRequired()`,同时在 XML 文档中显式声明 `CqrsRuntimeModule` 的前置注册约束 + - 为 `CommandExecutor` / `QueryExecutor` / `AsyncQueryExecutor` 的 dispatch-context helper 增加 `[MemberNotNullWhen]`,收敛重复 `_runtime is not null` 判空与 null-forgiving + - 补齐 legacy bridge request / handler 的 XML 文档,以及 `docs/zh-CN/core/command.md`、`context.md` 的 fallback 边界说明 +- 本轮没有跟进的 thread: + - `GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.cs` 的 `sealed` 建议属于低价值性能/风格提示,不影响 `PR #334` 的行为正确性 + - 若 review 在 GitHub 重新索引前仍显示旧 thread,下一轮以最新 head commit 再次抓取为准,不在本地重复造改动 +- 本轮权威验证: + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests|FullyQualifiedName~ArchitectureModulesBehaviorTests|FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests"` + - 结果:通过,`48/48` passed + - `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check` + - 结果:通过 + - `git diff --check` + - 结果:通过 + ### 阶段:legacy Core CQRS -> GFramework.Cqrs bridge(CQRS-REWRITE-RP-093) - 延续 `$gframework-batch-boot 50`,本轮明确不只盯 benchmark,而是同时处理两个目标: diff --git a/docs/zh-CN/core/command.md b/docs/zh-CN/core/command.md index 49e74611..25355a72 100644 --- a/docs/zh-CN/core/command.md +++ b/docs/zh-CN/core/command.md @@ -107,6 +107,7 @@ var reward = this.SendCommand(new GetGoldRewardCommand(new GetGoldRewardInput(3) 在标准架构启动路径中,这些兼容入口底层已经统一改走 `ICqrsRuntime`。 这意味着历史命令调用链在不改调用方式的前提下,也会复用同一套 pipeline 与上下文注入语义。 +只有在你直接 `new CommandExecutor()` 做隔离测试,且没有提供 `ICqrsRuntime` 时,才会回退到 legacy 直接执行;此时不会注入统一 pipeline,也不会额外补上下文桥接链路。 在 `IContextAware` 对象内,通常直接通过扩展使用: diff --git a/docs/zh-CN/core/context.md b/docs/zh-CN/core/context.md index 128e27ee..5b604e6c 100644 --- a/docs/zh-CN/core/context.md +++ b/docs/zh-CN/core/context.md @@ -113,7 +113,7 @@ this.SendEvent(new PlayerDiedEvent()); 在标准 `Architecture` 初始化路径里,这些旧入口现在会复用同一个 `ICqrsRuntime`: 旧 `SendCommand(...)` / `SendQuery(...)` 仍保持原有调用方式,但会经过统一的 request pipeline 与上下文注入链路。 -只有在你直接 `new CommandExecutor()`、`new QueryExecutor()` 做隔离测试,且没有提供 runtime 时,才会回退到 legacy 直接执行。 +只有在你直接 `new CommandExecutor()`、`new QueryExecutor()` 或 `new AsyncQueryExecutor()` 做隔离测试,且没有提供 `ICqrsRuntime` 时,才会回退到 legacy 直接执行;此时 `SendQueryAsync(...)` 兼容入口也会沿用同样的 legacy 路径,而不会进入统一 runtime pipeline。 ## 新 CQRS 入口