diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs index 835930d0..1b8ec488 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs @@ -43,6 +43,7 @@ namespace GFramework.Core.Tests.Architectures; /// - GetUtility方法 - 获取未注册工具时抛出异常 /// - GetEnvironment方法 - 获取环境对象 /// +[NonParallelizable] [TestFixture] public class ArchitectureContextTests { @@ -150,13 +151,20 @@ public class ArchitectureContextTests { LegacyBridgePipelineTracker.Reset(); var testQuery = new LegacyArchitectureBridgeQuery(); - var bridgeContext = CreateFrozenBridgeContext(); + var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer); - var result = bridgeContext.SendQuery(testQuery); + try + { + var result = bridgeContext.SendQuery(testQuery); - Assert.That(result, Is.EqualTo(24)); - Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext)); - Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1)); + Assert.That(result, Is.EqualTo(24)); + Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext)); + Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1)); + } + finally + { + bridgeContainer.Dispose(); + } } /// @@ -190,13 +198,20 @@ public class ArchitectureContextTests { LegacyBridgePipelineTracker.Reset(); var testCommand = new LegacyArchitectureBridgeCommand(); - var bridgeContext = CreateFrozenBridgeContext(); + var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer); - bridgeContext.SendCommand(testCommand); + try + { + bridgeContext.SendCommand(testCommand); - Assert.That(testCommand.Executed, Is.True); - Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext)); - Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1)); + Assert.That(testCommand.Executed, Is.True); + Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext)); + Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1)); + } + finally + { + bridgeContainer.Dispose(); + } } /// @@ -230,13 +245,20 @@ public class ArchitectureContextTests { LegacyBridgePipelineTracker.Reset(); var testCommand = new LegacyArchitectureBridgeCommandWithResult(); - var bridgeContext = CreateFrozenBridgeContext(); + var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer); - var result = bridgeContext.SendCommand(testCommand); + try + { + var result = bridgeContext.SendCommand(testCommand); - Assert.That(result, Is.EqualTo(42)); - Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext)); - Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1)); + Assert.That(result, Is.EqualTo(42)); + Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext)); + Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1)); + } + finally + { + bridgeContainer.Dispose(); + } } /// @@ -247,22 +269,30 @@ public class ArchitectureContextTests { LegacyBridgePipelineTracker.Reset(); var testQuery = new LegacyArchitectureBridgeAsyncQuery(); - var bridgeContext = CreateFrozenBridgeContext(); + var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer); - var result = await bridgeContext.SendQueryAsync(testQuery).ConfigureAwait(false); + try + { + var result = await bridgeContext.SendQueryAsync(testQuery).ConfigureAwait(false); - Assert.That(result, Is.EqualTo(64)); - Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext)); - Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1)); + Assert.That(result, Is.EqualTo(64)); + Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext)); + Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1)); + } + finally + { + bridgeContainer.Dispose(); + } } /// /// 为需要验证统一 CQRS pipeline 的用例创建一个已冻结的最小 bridge 上下文。 /// + /// 返回承载当前 bridge 上下文的冻结容器,供测试在 finally 中显式释放。 /// 能够执行 legacy bridge request 且会 materialize open-generic pipeline behavior 的上下文。 - private static ArchitectureContext CreateFrozenBridgeContext() + private static ArchitectureContext CreateFrozenBridgeContext(out MicrosoftDiContainer container) { - var container = new MicrosoftDiContainer(); + container = new MicrosoftDiContainer(); RegisterLegacyBridgeHandlers(container); new CqrsRuntimeModule().Register(container); container.ExecuteServicesHook(services => diff --git a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs index 4c0b86f8..f803bb56 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs @@ -15,6 +15,7 @@ namespace GFramework.Core.Tests.Architectures; /// 验证 Architecture 通过 ArchitectureModules 暴露出的模块安装与 CQRS 行为注册能力。 /// 这些测试覆盖模块安装回调和请求管道行为接入,确保模块管理器仍然保持可观察行为不变。 /// +[NonParallelizable] [TestFixture] public class ArchitectureModulesBehaviorTests { diff --git a/GFramework.Core.Tests/Command/CommandExecutorTests.cs b/GFramework.Core.Tests/Command/CommandExecutorTests.cs index 869b7384..21c6bc27 100644 --- a/GFramework.Core.Tests/Command/CommandExecutorTests.cs +++ b/GFramework.Core.Tests/Command/CommandExecutorTests.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using GFramework.Core.Command; +using GFramework.Core.Tests.Architectures; namespace GFramework.Core.Tests.Command; @@ -75,6 +76,59 @@ public class CommandExecutorTests Assert.Throws(() => _commandExecutor.Send(null!)); } + /// + /// 验证 legacy 同步命令桥接会在线程池上等待 runtime, + /// 避免直接继承调用方当前的同步上下文。 + /// + [Test] + public void Send_Should_Bridge_Through_Runtime_Without_Reusing_Caller_SynchronizationContext() + { + var runtime = new RecordingCqrsRuntime(); + var executor = new CommandExecutor(runtime); + var command = new ContextAwareLegacyCommand(); + var expectedContext = new TestArchitectureContextBaseStub(); + ((GFramework.Core.Abstractions.Rule.IContextAware)command).SetContext(expectedContext); + var originalContext = SynchronizationContext.Current; + + try + { + SynchronizationContext.SetSynchronizationContext(new TestLegacySynchronizationContext()); + + executor.Send(command); + + Assert.Multiple(() => + { + Assert.That(runtime.LastRequest, Is.TypeOf()); + Assert.That(runtime.ObservedSynchronizationContextType, Is.Null); + }); + } + finally + { + SynchronizationContext.SetSynchronizationContext(originalContext); + } + } + + /// + /// 验证 legacy 带返回值命令桥接也会保留上下文注入与返回值语义。 + /// + [Test] + public void Send_WithResult_Should_Bridge_Through_Runtime_And_Preserve_Context() + { + var runtime = new RecordingCqrsRuntime(static _ => 123); + var executor = new CommandExecutor(runtime); + var command = new ContextAwareLegacyCommandWithResult(123); + var expectedContext = new TestArchitectureContextBaseStub(); + ((GFramework.Core.Abstractions.Rule.IContextAware)command).SetContext(expectedContext); + + var result = executor.Send(command); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(123)); + Assert.That(runtime.LastRequest, Is.TypeOf()); + }); + } + /// /// 测试SendAsync方法执行异步命令 /// @@ -122,4 +176,11 @@ public class CommandExecutorTests { Assert.ThrowsAsync(() => _commandExecutor.SendAsync(null!)); } + + /// + /// 为同步 bridge 测试提供最小架构上下文替身。 + /// + private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase + { + } } diff --git a/GFramework.Core.Tests/Command/ContextAwareLegacyCommand.cs b/GFramework.Core.Tests/Command/ContextAwareLegacyCommand.cs new file mode 100644 index 00000000..ffd47b4f --- /dev/null +++ b/GFramework.Core.Tests/Command/ContextAwareLegacyCommand.cs @@ -0,0 +1,31 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Command; +using GFramework.Core.Rule; + +namespace GFramework.Core.Tests.Command; + +/// +/// 为 提供可观察上下文注入的 legacy 命令。 +/// +internal sealed class ContextAwareLegacyCommand : ContextAwareBase, ICommand +{ + /// + /// 获取执行期间观察到的架构上下文。 + /// + public IArchitectureContext? ObservedContext { get; private set; } + + /// + /// 获取命令是否已经执行。 + /// + public bool Executed { get; private set; } + + /// + public void Execute() + { + Executed = true; + ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext(); + } +} diff --git a/GFramework.Core.Tests/Command/ContextAwareLegacyCommandWithResult.cs b/GFramework.Core.Tests/Command/ContextAwareLegacyCommandWithResult.cs new file mode 100644 index 00000000..21712f73 --- /dev/null +++ b/GFramework.Core.Tests/Command/ContextAwareLegacyCommandWithResult.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Command; +using GFramework.Core.Rule; + +namespace GFramework.Core.Tests.Command; + +/// +/// 为 提供可观察上下文注入的带返回值 legacy 命令。 +/// +internal sealed class ContextAwareLegacyCommandWithResult(int result) : ContextAwareBase, ICommand +{ + /// + /// 获取执行期间观察到的架构上下文。 + /// + public IArchitectureContext? ObservedContext { get; private set; } + + /// + public int Execute() + { + ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext(); + return result; + } +} diff --git a/GFramework.Core.Tests/Command/RecordingCqrsRuntime.cs b/GFramework.Core.Tests/Command/RecordingCqrsRuntime.cs new file mode 100644 index 00000000..d85df9a0 --- /dev/null +++ b/GFramework.Core.Tests/Command/RecordingCqrsRuntime.cs @@ -0,0 +1,66 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Core.Tests.Command; + +/// +/// 记录 bridge 执行线程与收到请求的最小 CQRS runtime 测试替身。 +/// +internal sealed class RecordingCqrsRuntime(Func? responseFactory = null) : ICqrsRuntime +{ + private static readonly Func DefaultResponseFactory = _ => null; + + private readonly Func _responseFactory = responseFactory ?? DefaultResponseFactory; + + /// + /// 获取最近一次 观察到的同步上下文类型。 + /// + public Type? ObservedSynchronizationContextType { get; private set; } + + /// + /// 获取最近一次收到的请求实例。 + /// + public object? LastRequest { get; private set; } + + /// + public ValueTask SendAsync( + ICqrsContext context, + IRequest request, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(request); + + ObservedSynchronizationContextType = SynchronizationContext.Current?.GetType(); + LastRequest = request; + + object? response = request switch + { + IRequest => Unit.Value, + _ => _responseFactory(request) + }; + + return ValueTask.FromResult((TResponse)response!); + } + + /// + public ValueTask PublishAsync( + ICqrsContext context, + TNotification notification, + CancellationToken cancellationToken = default) + where TNotification : INotification + { + throw new NotSupportedException(); + } + + /// + public IAsyncEnumerable CreateStream( + ICqrsContext context, + IStreamRequest request, + CancellationToken cancellationToken = default) + { + throw new NotSupportedException(); + } +} diff --git a/GFramework.Core.Tests/Command/TestLegacySynchronizationContext.cs b/GFramework.Core.Tests/Command/TestLegacySynchronizationContext.cs new file mode 100644 index 00000000..21f3aba6 --- /dev/null +++ b/GFramework.Core.Tests/Command/TestLegacySynchronizationContext.cs @@ -0,0 +1,11 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +namespace GFramework.Core.Tests.Command; + +/// +/// 为 legacy 同步 bridge 回归测试提供可识别的同步上下文占位类型。 +/// +internal sealed class TestLegacySynchronizationContext : SynchronizationContext +{ +} diff --git a/GFramework.Core.Tests/Cqrs/LegacyAsyncCommandDispatchRequestHandlerTests.cs b/GFramework.Core.Tests/Cqrs/LegacyAsyncCommandDispatchRequestHandlerTests.cs new file mode 100644 index 00000000..4d648ef1 --- /dev/null +++ b/GFramework.Core.Tests/Cqrs/LegacyAsyncCommandDispatchRequestHandlerTests.cs @@ -0,0 +1,87 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Core.Abstractions.Command; +using GFramework.Core.Abstractions.Rule; +using GFramework.Core.Cqrs; +using GFramework.Core.Rule; +using GFramework.Core.Tests.Architectures; + +namespace GFramework.Core.Tests.Cqrs; + +/// +/// 验证 legacy 异步无返回值命令 bridge handler 的取消语义。 +/// +[TestFixture] +public class LegacyAsyncCommandDispatchRequestHandlerTests +{ + /// + /// 验证当取消令牌在执行前已触发时,handler 不会启动底层 legacy 命令。 + /// + [Test] + public void Handle_Should_Throw_Without_Executing_Command_When_Cancellation_Is_Already_Requested() + { + var handler = new LegacyAsyncCommandDispatchRequestHandler(); + var command = new ProbeAsyncCommand(Task.CompletedTask); + using var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.Cancel(); + + Assert.ThrowsAsync( + async () => await handler.Handle( + new LegacyAsyncCommandDispatchRequest(command), + cancellationTokenSource.Token) + .AsTask() + .ConfigureAwait(false)); + Assert.That(command.ExecutionCount, Is.Zero); + } + + /// + /// 验证当底层 legacy 命令正在运行时,handler 会通过 WaitAsync 及时向调用方暴露取消。 + /// + [Test] + public async Task Handle_Should_Observe_Cancellation_While_Command_Is_Running() + { + var completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var handler = new LegacyAsyncCommandDispatchRequestHandler(); + var command = new ProbeAsyncCommand(completionSource.Task); + using var cancellationTokenSource = new CancellationTokenSource(); + ((IContextAware)handler).SetContext(new TestArchitectureContextBaseStub()); + + var handleTask = handler.Handle( + new LegacyAsyncCommandDispatchRequest(command), + cancellationTokenSource.Token) + .AsTask(); + + cancellationTokenSource.Cancel(); + + Assert.That( + async () => await handleTask.ConfigureAwait(false), + Throws.InstanceOf()); + Assert.That(command.ExecutionCount, Is.EqualTo(1)); + } + + /// + /// 为 handler 取消测试提供可控完成时机的异步命令替身。 + /// + private sealed class ProbeAsyncCommand(Task executionTask) : ContextAwareBase, IAsyncCommand + { + /// + /// 获取底层命令逻辑的触发次数。 + /// + public int ExecutionCount { get; private set; } + + /// + public Task ExecuteAsync() + { + ExecutionCount++; + return executionTask; + } + } + + /// + /// 为 handler 取消测试提供最小架构上下文替身。 + /// + private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase + { + } +} diff --git a/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs b/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs index 434c8600..8ac9f002 100644 --- a/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs +++ b/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 using GFramework.Core.Query; +using GFramework.Core.Tests.Architectures; +using GFramework.Core.Tests.Command; namespace GFramework.Core.Tests.Query; @@ -138,4 +140,32 @@ public class AsyncQueryExecutorTests Assert.That(result1, Is.EqualTo(20)); Assert.That(result2, Is.EqualTo(40)); } + + /// + /// 验证 legacy 异步查询桥接会保留上下文注入,并通过 runtime 返回结果。 + /// + [Test] + public async Task SendAsync_Should_Bridge_Through_Runtime_And_Preserve_Context() + { + var runtime = new RecordingCqrsRuntime(static _ => 64); + var executor = new AsyncQueryExecutor(runtime); + var query = new ContextAwareLegacyAsyncQuery(64); + var expectedContext = new TestArchitectureContextBaseStub(); + ((GFramework.Core.Abstractions.Rule.IContextAware)query).SetContext(expectedContext); + + var result = await executor.SendAsync(query); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(64)); + Assert.That(runtime.LastRequest, Is.TypeOf()); + }); + } + + /// + /// 为异步 bridge 测试提供最小架构上下文替身。 + /// + private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase + { + } } diff --git a/GFramework.Core.Tests/Query/ContextAwareLegacyAsyncQuery.cs b/GFramework.Core.Tests/Query/ContextAwareLegacyAsyncQuery.cs new file mode 100644 index 00000000..77b6ef02 --- /dev/null +++ b/GFramework.Core.Tests/Query/ContextAwareLegacyAsyncQuery.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Query; +using GFramework.Core.Rule; + +namespace GFramework.Core.Tests.Query; + +/// +/// 为 提供可观察上下文注入的 legacy 异步查询。 +/// +internal sealed class ContextAwareLegacyAsyncQuery(int result) : ContextAwareBase, IAsyncQuery +{ + /// + /// 获取执行期间观察到的架构上下文。 + /// + public IArchitectureContext? ObservedContext { get; private set; } + + /// + public Task DoAsync() + { + ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext(); + return Task.FromResult(result); + } +} diff --git a/GFramework.Core.Tests/Query/ContextAwareLegacyQuery.cs b/GFramework.Core.Tests/Query/ContextAwareLegacyQuery.cs new file mode 100644 index 00000000..ad50babe --- /dev/null +++ b/GFramework.Core.Tests/Query/ContextAwareLegacyQuery.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Query; +using GFramework.Core.Rule; + +namespace GFramework.Core.Tests.Query; + +/// +/// 为 提供可观察上下文注入的 legacy 查询。 +/// +internal sealed class ContextAwareLegacyQuery(int result) : ContextAwareBase, IQuery +{ + /// + /// 获取执行期间观察到的架构上下文。 + /// + public IArchitectureContext? ObservedContext { get; private set; } + + /// + public int Do() + { + ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext(); + return result; + } +} diff --git a/GFramework.Core.Tests/Query/QueryExecutorTests.cs b/GFramework.Core.Tests/Query/QueryExecutorTests.cs index fc2415cc..e5adaf79 100644 --- a/GFramework.Core.Tests/Query/QueryExecutorTests.cs +++ b/GFramework.Core.Tests/Query/QueryExecutorTests.cs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 using GFramework.Core.Query; +using GFramework.Core.Tests.Architectures; +using GFramework.Core.Tests.Command; namespace GFramework.Core.Tests.Query; @@ -61,4 +63,44 @@ public class QueryExecutorTests Assert.That(result, Is.EqualTo("Result: 10")); } + + /// + /// 验证 legacy 同步查询桥接会在线程池上等待 runtime, + /// 避免直接复用调用方的同步上下文。 + /// + [Test] + public void Send_Should_Bridge_Through_Runtime_Without_Reusing_Caller_SynchronizationContext() + { + var runtime = new RecordingCqrsRuntime(static _ => 24); + var executor = new QueryExecutor(runtime); + var query = new ContextAwareLegacyQuery(24); + var expectedContext = new TestArchitectureContextBaseStub(); + ((GFramework.Core.Abstractions.Rule.IContextAware)query).SetContext(expectedContext); + var originalContext = SynchronizationContext.Current; + + try + { + SynchronizationContext.SetSynchronizationContext(new TestLegacySynchronizationContext()); + + var result = executor.Send(query); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(24)); + Assert.That(runtime.LastRequest, Is.TypeOf()); + Assert.That(runtime.ObservedSynchronizationContextType, Is.Null); + }); + } + finally + { + SynchronizationContext.SetSynchronizationContext(originalContext); + } + } + + /// + /// 为同步 bridge 测试提供最小架构上下文替身。 + /// + private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase + { + } } diff --git a/GFramework.Core/Architectures/ArchitectureContext.cs b/GFramework.Core/Architectures/ArchitectureContext.cs index e143c957..f314a0b8 100644 --- a/GFramework.Core/Architectures/ArchitectureContext.cs +++ b/GFramework.Core/Architectures/ArchitectureContext.cs @@ -112,7 +112,8 @@ public class ArchitectureContext : IArchitectureContext /// 响应结果 public TResponse SendRequest(IRequest request) { - return SendRequestAsync(request).AsTask().GetAwaiter().GetResult(); + ArgumentNullException.ThrowIfNull(request); + return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, request); } /// @@ -197,7 +198,8 @@ public class ArchitectureContext : IArchitectureContext /// 查询结果 public TResponse SendQuery(global::GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query) { - return SendQueryAsync(query).AsTask().GetAwaiter().GetResult(); + ArgumentNullException.ThrowIfNull(query); + return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, query); } /// @@ -403,7 +405,8 @@ public class ArchitectureContext : IArchitectureContext /// 命令执行结果 public TResponse SendCommand(global::GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command) { - return SendCommandAsync(command).AsTask().GetAwaiter().GetResult(); + ArgumentNullException.ThrowIfNull(command); + return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, command); } /// diff --git a/GFramework.Core/Command/CommandExecutor.cs b/GFramework.Core/Command/CommandExecutor.cs index fd401c9d..83d6ad80 100644 --- a/GFramework.Core/Command/CommandExecutor.cs +++ b/GFramework.Core/Command/CommandExecutor.cs @@ -1,9 +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; using GFramework.Cqrs.Abstractions.Cqrs; using IAsyncCommand = GFramework.Core.Abstractions.Command.IAsyncCommand; @@ -78,9 +76,11 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec { ArgumentNullException.ThrowIfNull(command); - if (TryResolveDispatchContext(command, out var context)) + var cqrsRuntime = _runtime; + + if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, command, out var context)) { - return _runtime.SendAsync(context, new LegacyAsyncCommandDispatchRequest(command)).AsTask(); + return cqrsRuntime.SendAsync(context, new LegacyAsyncCommandDispatchRequest(command)).AsTask(); } return command.ExecuteAsync(); @@ -97,9 +97,11 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec { ArgumentNullException.ThrowIfNull(command); - if (TryResolveDispatchContext(command, out var context)) + var cqrsRuntime = _runtime; + + if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, command, out var context)) { - return BridgeAsyncCommandWithResultAsync(_runtime, context, command); + return BridgeAsyncCommandWithResultAsync(cqrsRuntime, context, command); } return command.ExecuteAsync(); @@ -119,12 +121,14 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec where TTarget : class where TRequest : IRequest { - if (!TryResolveDispatchContext(target, out var context)) + var cqrsRuntime = _runtime; + + if (!LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, target, out var context)) { return false; } - _runtime.SendAsync(context, requestFactory(target)).AsTask().GetAwaiter().GetResult(); + LegacyCqrsDispatchHelper.SendSynchronously(cqrsRuntime, context, requestFactory(target)); return true; } @@ -145,13 +149,15 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec where TTarget : class where TRequest : IRequest { - if (!TryResolveDispatchContext(target, out var context)) + var cqrsRuntime = _runtime; + + if (!LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, target, out var context)) { result = default; return false; } - var boxedResult = _runtime.SendAsync(context, requestFactory(target)).AsTask().GetAwaiter().GetResult(); + var boxedResult = LegacyCqrsDispatchHelper.SendSynchronously(cqrsRuntime, context, requestFactory(target)); result = (TResult)boxedResult!; return true; } @@ -177,33 +183,4 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec .ConfigureAwait(false); return (TResult)boxedResult!; } - - /// - /// 解析当前 legacy 目标对象应该绑定到哪个架构上下文。 - /// - /// 即将执行的 legacy 目标对象。 - /// 命中时返回可用于 CQRS runtime 的架构上下文。 - /// 如果既接入了 runtime 且目标对象提供了上下文,则返回 - [MemberNotNullWhen(true, nameof(_runtime))] - private bool TryResolveDispatchContext( - object target, - out GFramework.Core.Abstractions.Architectures.IArchitectureContext context) - { - context = null!; - - if (_runtime is null || target is not IContextAware contextAware) - { - return false; - } - - try - { - context = contextAware.GetContext(); - return true; - } - catch (InvalidOperationException) - { - return false; - } - } } diff --git a/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequestHandler.cs b/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequestHandler.cs index e34e9de3..4e0b56ca 100644 --- a/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequestHandler.cs +++ b/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequestHandler.cs @@ -17,8 +17,10 @@ internal sealed class LegacyAsyncCommandDispatchRequestHandler CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); + // Legacy ExecuteAsync contract does not accept CancellationToken; use WaitAsync so the caller can still observe cancellation promptly. + cancellationToken.ThrowIfCancellationRequested(); PrepareTarget(request.Command); - await request.Command.ExecuteAsync().ConfigureAwait(false); + await request.Command.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false); return Unit.Value; } } diff --git a/GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs b/GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs new file mode 100644 index 00000000..5fb6fec0 --- /dev/null +++ b/GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs @@ -0,0 +1,94 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Core.Cqrs; + +/// +/// 为 legacy Core CQRS bridge 提供共享的上下文解析与同步兼容辅助逻辑。 +/// +/// +/// 旧的同步 Command/Query 入口仍需要阻塞等待统一 返回结果。 +/// 这里统一通过 把等待动作切换到线程池, +/// 避免直接占用调用方的 导致 legacy 同步入口与异步 pipeline 互相卡死。 +/// +internal static class LegacyCqrsDispatchHelper +{ + /// + /// 解析当前 legacy 目标对象是否能够绑定到统一 CQRS runtime 的架构上下文。 + /// + /// 当前执行器可用的统一 CQRS runtime。 + /// 即将执行的 legacy 目标对象。 + /// 命中时返回可用于 CQRS runtime 的架构上下文。 + /// + /// 当 可用且 能稳定提供 + /// 时返回 ;否则返回 。 + /// + internal static bool TryResolveDispatchContext( + [NotNullWhen(true)] ICqrsRuntime? runtime, + object target, + out IArchitectureContext context) + { + ArgumentNullException.ThrowIfNull(target); + + context = null!; + + if (runtime is null || target is not IContextAware contextAware) + { + return false; + } + + try + { + context = contextAware.GetContext(); + return true; + } + catch (InvalidOperationException) + { + return false; + } + } + + /// + /// 同步等待统一 CQRS runtime 完成无返回值请求。 + /// + /// 负责分发当前请求的统一 CQRS runtime。 + /// 当前架构上下文。 + /// 要同步等待的请求。 + internal static void SendSynchronously( + ICqrsRuntime runtime, + IArchitectureContext context, + IRequest request) + { + ArgumentNullException.ThrowIfNull(runtime); + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(request); + + Task.Run(() => runtime.SendAsync(context, request).AsTask()).GetAwaiter().GetResult(); + } + + /// + /// 同步等待统一 CQRS runtime 完成带返回值请求,并返回实际响应。 + /// + /// 请求响应类型。 + /// 负责分发当前请求的统一 CQRS runtime。 + /// 当前架构上下文。 + /// 要同步等待的请求。 + /// 统一 CQRS runtime 返回的响应结果。 + internal static TResponse SendSynchronously( + ICqrsRuntime runtime, + IArchitectureContext context, + IRequest request) + { + ArgumentNullException.ThrowIfNull(runtime); + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(request); + + return Task.Run(() => runtime.SendAsync(context, request).AsTask()).GetAwaiter().GetResult(); + } +} diff --git a/GFramework.Core/Query/AsyncQueryExecutor.cs b/GFramework.Core/Query/AsyncQueryExecutor.cs index f59b63f9..4890d9d0 100644 --- a/GFramework.Core/Query/AsyncQueryExecutor.cs +++ b/GFramework.Core/Query/AsyncQueryExecutor.cs @@ -1,9 +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; using GFramework.Cqrs.Abstractions.Cqrs; @@ -31,9 +29,11 @@ public sealed class AsyncQueryExecutor(ICqrsRuntime? runtime = null) : IAsyncQue { ArgumentNullException.ThrowIfNull(query); - if (TryResolveDispatchContext(query, out var context)) + var cqrsRuntime = _runtime; + + if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, query, out var context)) { - return BridgeAsyncQueryAsync(_runtime, context, query); + return BridgeAsyncQueryAsync(cqrsRuntime, context, query); } return query.DoAsync(); @@ -61,32 +61,4 @@ public sealed class AsyncQueryExecutor(ICqrsRuntime? runtime = null) : IAsyncQue return (TResult)boxedResult!; } - /// - /// 解析当前 legacy 查询应该绑定到哪个架构上下文。 - /// - /// 即将执行的 legacy 查询对象。 - /// 命中时返回可用于 CQRS runtime 的架构上下文。 - /// 如果既接入了 runtime 且查询对象提供了上下文,则返回 - [MemberNotNullWhen(true, nameof(_runtime))] - private bool TryResolveDispatchContext( - object query, - out GFramework.Core.Abstractions.Architectures.IArchitectureContext context) - { - context = null!; - - if (_runtime is null || query is not IContextAware contextAware) - { - return false; - } - - try - { - context = contextAware.GetContext(); - return true; - } - catch (InvalidOperationException) - { - return false; - } - } } diff --git a/GFramework.Core/Query/QueryExecutor.cs b/GFramework.Core/Query/QueryExecutor.cs index 2ecf0960..516644bc 100644 --- a/GFramework.Core/Query/QueryExecutor.cs +++ b/GFramework.Core/Query/QueryExecutor.cs @@ -1,9 +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; using GFramework.Cqrs.Abstractions.Cqrs; @@ -31,53 +29,30 @@ public sealed class QueryExecutor(ICqrsRuntime? runtime = null) : IQueryExecutor /// /// 查询结果的类型。 /// 要执行的查询对象,必须实现 IQuery<TResult> 接口。 - /// 查询执行的结果,类型为 TResult。 + /// 查询执行成功后还原出的 结果。 + /// + /// 统一 CQRS runtime 返回 ,但 为值类型。 + /// + /// + /// 统一 CQRS runtime 返回的装箱结果无法转换为 。 + /// public TResult Send(IQuery query) { ArgumentNullException.ThrowIfNull(query); - if (TryResolveDispatchContext(query, out var context)) + var cqrsRuntime = _runtime; + + if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, query, out var context)) { - var boxedResult = _runtime.SendAsync( - context, - new LegacyQueryDispatchRequest( - query, - () => query.Do())) - .AsTask() - .GetAwaiter() - .GetResult(); + var boxedResult = LegacyCqrsDispatchHelper.SendSynchronously( + cqrsRuntime, + context, + new LegacyQueryDispatchRequest( + query, + () => query.Do())); return (TResult)boxedResult!; } return query.Do(); } - - /// - /// 解析当前 legacy 查询应该绑定到哪个架构上下文。 - /// - /// 即将执行的 legacy 查询对象。 - /// 命中时返回可用于 CQRS runtime 的架构上下文。 - /// 如果既接入了 runtime 且查询对象提供了上下文,则返回 - [MemberNotNullWhen(true, nameof(_runtime))] - private bool TryResolveDispatchContext( - object query, - out GFramework.Core.Abstractions.Architectures.IArchitectureContext context) - { - context = null!; - - if (_runtime is null || query is not IContextAware contextAware) - { - return false; - } - - try - { - context = contextAware.GetContext(); - return true; - } - catch (InvalidOperationException) - { - return false; - } - } } diff --git a/GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs index efb376e6..fe3bfcf2 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs @@ -28,6 +28,9 @@ public interface ICqrsRuntime /// /// 该契约允许调用方传入任意 , /// 但默认运行时在需要向处理器或行为注入框架上下文时,仍要求该上下文同时实现 IArchitectureContext。 + /// 为了兼容 legacy 同步入口,ArchitectureContextQueryExecutorCommandExecutor + /// 可能会在后台线程上同步等待该异步结果;实现者与 pipeline 行为不应依赖调用方的 + /// ,并应优先在内部异步链路上使用 ConfigureAwait(false)。 /// ValueTask SendAsync( ICqrsContext context, 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 ab97559e..1911872f 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-094` +- 恢复点编号:`CQRS-REWRITE-RP-095` - 当前阶段:`Phase 8` - 当前 PR 锚点:`PR #334` - 当前结论: @@ -29,8 +29,9 @@ CQRS 迁移与收敛。 - 当前 `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` 未吸收能力差距复核 - - 当前 `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` 与其他更早阶段细节均以下方归档或说明为准 + - `RP-094` 已按 `PR #334` latest-head review 收口 legacy bridge 的测试注册方式、模块运行时依赖契约、异步取消语义、XML 文档缺口与兼容文档回退边界 + - 当前 `RP-095` 已继续收口 `PR #334` 剩余 review:把 legacy 同步 bridge 的阻塞等待统一切到线程池隔离 helper、补齐 `ArchitectureContext` / executor 共享 dispatch helper、修正 bridge fixture 的并行与容器释放约束,并为 runtime bridge 与 async void command cancellation 增补回归测试 +- `ai-plan` active 入口现以 `RP-095` 为最新恢复锚点;`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准 ## 当前活跃事实 @@ -42,9 +43,13 @@ 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 时直接执行”的回退路径,用于不依赖容器的隔离单元测试 +- `LegacyCqrsDispatchHelper` 现统一负责 runtime dispatch context 解析,以及 legacy 同步 bridge 对 `ICqrsRuntime.SendAsync(...)` 的线程池隔离等待 +- `ArchitectureContext`、`CommandExecutor`、`QueryExecutor` 的同步 CQRS/legacy bridge 入口不再直接在调用线程上阻塞 `SendAsync(...).GetAwaiter().GetResult()` - `GFramework.Core.Tests` 现通过 `InternalsVisibleTo("GFramework.Core.Tests")` 直接实例化内部 bridge handler,不再依赖字符串反射装配测试桥接注册 +- 使用 `LegacyBridgePipelineTracker` 的 `ArchitectureContextTests` 与 `ArchitectureModulesBehaviorTests` 现都显式标记为 `NonParallelizable` +- `ArchitectureContextTests.CreateFrozenBridgeContext(...)` 现把冻结容器所有权显式交回调用方,并在每个 bridge 用例的 `finally` 中释放 - `CommandExecutorModule`、`QueryExecutorModule`、`AsyncQueryExecutorModule` 现改为 `GetRequired()` 并在 XML 文档里显式声明注册顺序契约,避免 runtime 缺失时静默回退 -- `LegacyAsyncQueryDispatchRequestHandler` 与 `LegacyAsyncCommandResultDispatchRequestHandler` 现通过 `ThrowIfCancellationRequested()` + `WaitAsync(cancellationToken)` 显式保留调用方取消可见性 +- `LegacyAsyncQueryDispatchRequestHandler`、`LegacyAsyncCommandResultDispatchRequestHandler`、`LegacyAsyncCommandDispatchRequestHandler` 现都通过 `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` @@ -55,7 +60,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 重新索引的状态差异或低价值建议 +- `PR #334` 当前 latest-head open AI feedback 经过本轮本地复核与修复后,应主要剩余待 GitHub 重新索引的状态差异或已实质关闭但未 resolve 的 thread ## 当前风险 @@ -127,6 +132,13 @@ CQRS 迁移与收敛。 - 结果:通过 - `git diff --check` - 结果:通过 +- `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|FullyQualifiedName~LegacyAsyncCommandDispatchRequestHandlerTests"` + - 结果:通过,`54/54` passed + - 备注:覆盖 legacy 同步 bridge 的同步上下文隔离、bridge fixture 容器释放,以及 async void command cancellation 可见性 +- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check` + - 结果:通过 ## 下一推荐步骤 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 25255efc..900e06f3 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,28 @@ ## 2026-05-07 +### 阶段:PR #334 legacy bridge sync follow-up(CQRS-REWRITE-RP-095) + +- 再次使用 `$gframework-pr-review` 抓取 `feat/cqrs-optimization` 对应的 `PR #334` latest-head review,并只保留本地复核后仍成立的问题: + - `QueryExecutor` / `CommandExecutor` 新增的同步 bridge 仍直接阻塞 `ICqrsRuntime.SendAsync(...)`,在调用方存在 `SynchronizationContext` 时容易放大 sync-over-async 死锁面 + - `QueryExecutor` / `CommandExecutor` / `AsyncQueryExecutor` 各自保留一份相同的 dispatch-context 解析逻辑,仍有漂移风险 + - `ArchitectureContextTests` 的 bridge fixture 依然共享静态 tracker 且未显式声明非并行;冻结容器所有权也未交还给调用方释放 + - `LegacyAsyncCommandDispatchRequestHandler` 仍未沿用另两个 async bridge handler 的取消可见性模式 +- 本轮主线程决策: + - 新增 `GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs`,统一收口 legacy bridge 的 dispatch-context 解析,以及同步 bridge 对 `ICqrsRuntime.SendAsync(...)` 的线程池隔离等待 + - 将 `QueryExecutor`、`CommandExecutor`、`AsyncQueryExecutor` 的重复 helper 改为复用共享 helper,并把 `ArchitectureContext` 的同步 CQRS 包装入口一并切换到同一阻塞策略,避免留下半修状态 + - 为 `ICqrsRuntime.SendAsync(...)` 补充 ``,显式说明 legacy 同步入口会在后台线程上等待该异步契约,处理链路不应依赖调用方 `SynchronizationContext` + - 把 `ArchitectureContextTests`、`ArchitectureModulesBehaviorTests` 标记为 `NonParallelizable`,并让 `CreateFrozenBridgeContext(...)` 把冻结容器通过 `out` 参数返还给每个测试在 `finally` 中释放 + - 为 `LegacyAsyncCommandDispatchRequestHandler` 增补 `ThrowIfCancellationRequested()` + `WaitAsync(cancellationToken)`,与另外两个 async bridge handler 保持一致 + - 新增回归测试覆盖同步 bridge 的 `SynchronizationContext` 隔离、legacy async command handler 的取消语义,以及 async/sync bridge request 的 request-type 命中 +- 本轮权威验证: + - `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check` + - 结果:通过 + - `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|FullyQualifiedName~LegacyAsyncCommandDispatchRequestHandlerTests"` + - 结果:通过,`54/54` passed + ### 阶段:PR #334 legacy bridge / 文档 review 收尾(CQRS-REWRITE-RP-094) - 使用 `$gframework-pr-review` 抓取当前分支公开 PR,确认 `feat/cqrs-optimization` 当前对应 `PR #334`