diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs
index 47ad0d2c..1b8ec488 100644
--- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs
+++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs
@@ -10,11 +10,13 @@ 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;
using GFramework.Core.Logging;
using GFramework.Core.Query;
+using GFramework.Core.Services.Modules;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Tests.Architectures;
@@ -41,9 +43,13 @@ namespace GFramework.Core.Tests.Architectures;
/// - GetUtility方法 - 获取未注册工具时抛出异常
/// - GetEnvironment方法 - 获取环境对象
///
+[NonParallelizable]
[TestFixture]
public class ArchitectureContextTests
{
+ ///
+ /// 初始化测试所需的容器与默认服务实例。
+ ///
[SetUp]
public void SetUp()
{
@@ -71,10 +77,22 @@ public class ArchitectureContextTests
_container.RegisterPlurality(_queryBus);
_container.RegisterPlurality(_asyncQueryBus);
_container.RegisterPlurality(_environment);
+ new CqrsRuntimeModule().Register(_container);
+ RegisterLegacyBridgeHandlers(_container);
_context = new ArchitectureContext(_container);
}
+ ///
+ /// 释放当前测试创建的容器,并清理 legacy bridge 共享计数状态。
+ ///
+ [TearDown]
+ public void TearDown()
+ {
+ LegacyBridgePipelineTracker.Reset();
+ _container?.Dispose();
+ }
+
private AsyncQueryExecutor? _asyncQueryBus;
private CommandExecutor? _commandBus;
private MicrosoftDiContainer? _container;
@@ -124,6 +142,31 @@ public class ArchitectureContextTests
Assert.That(result, Is.EqualTo(42));
}
+ ///
+ /// 测试 legacy 查询通过 发送时会进入统一 CQRS pipeline,
+ /// 并把当前架构上下文注入到查询对象。
+ ///
+ [Test]
+ public void SendQuery_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context()
+ {
+ LegacyBridgePipelineTracker.Reset();
+ var testQuery = new LegacyArchitectureBridgeQuery();
+ var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
+
+ 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));
+ }
+ finally
+ {
+ bridgeContainer.Dispose();
+ }
+ }
+
///
/// 测试SendQuery方法在查询为null时应抛出ArgumentNullException
///
@@ -146,6 +189,31 @@ public class ArchitectureContextTests
Assert.That(testCommand.Executed, Is.True);
}
+ ///
+ /// 测试 legacy 命令通过 发送时会进入统一 CQRS pipeline,
+ /// 并把当前架构上下文注入到命令对象。
+ ///
+ [Test]
+ public void SendCommand_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context()
+ {
+ LegacyBridgePipelineTracker.Reset();
+ var testCommand = new LegacyArchitectureBridgeCommand();
+ var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
+
+ try
+ {
+ bridgeContext.SendCommand(testCommand);
+
+ Assert.That(testCommand.Executed, Is.True);
+ Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
+ Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
+ }
+ finally
+ {
+ bridgeContainer.Dispose();
+ }
+ }
+
///
/// 测试SendCommand方法在命令为null时应抛出ArgumentNullException
///
@@ -168,6 +236,87 @@ public class ArchitectureContextTests
Assert.That(result, Is.EqualTo(123));
}
+ ///
+ /// 测试 legacy 带返回值命令通过 发送时会进入统一 CQRS pipeline,
+ /// 并保持原始返回值语义。
+ ///
+ [Test]
+ public void SendCommand_WithResult_Should_Bridge_Through_CqrsRuntime()
+ {
+ LegacyBridgePipelineTracker.Reset();
+ var testCommand = new LegacyArchitectureBridgeCommandWithResult();
+ var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
+
+ 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));
+ }
+ finally
+ {
+ bridgeContainer.Dispose();
+ }
+ }
+
+ ///
+ /// 测试 legacy 异步查询通过 发送时也会进入统一 CQRS pipeline。
+ ///
+ [Test]
+ public async Task SendQueryAsync_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context()
+ {
+ LegacyBridgePipelineTracker.Reset();
+ var testQuery = new LegacyArchitectureBridgeAsyncQuery();
+ var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
+
+ 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));
+ }
+ finally
+ {
+ bridgeContainer.Dispose();
+ }
+ }
+
+ ///
+ /// 为需要验证统一 CQRS pipeline 的用例创建一个已冻结的最小 bridge 上下文。
+ ///
+ /// 返回承载当前 bridge 上下文的冻结容器,供测试在 finally 中显式释放。
+ /// 能够执行 legacy bridge request 且会 materialize open-generic pipeline behavior 的上下文。
+ private static ArchitectureContext CreateFrozenBridgeContext(out MicrosoftDiContainer container)
+ {
+ container = new MicrosoftDiContainer();
+ RegisterLegacyBridgeHandlers(container);
+ new CqrsRuntimeModule().Register(container);
+ container.ExecuteServicesHook(services =>
+ services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LegacyBridgeTrackingPipelineBehavior<,>)));
+ container.Freeze();
+ return new ArchitectureContext(container);
+ }
+
+ ///
+ /// 把 GFramework.Core 内部的 legacy bridge handler 实例预先注册成可见的实例绑定。
+ ///
+ /// 目标测试容器。
+ private static void RegisterLegacyBridgeHandlers(MicrosoftDiContainer container)
+ {
+ ArgumentNullException.ThrowIfNull(container);
+
+ container.RegisterPlurality(new LegacyCommandDispatchRequestHandler());
+ container.RegisterPlurality(new LegacyCommandResultDispatchRequestHandler());
+ container.RegisterPlurality(new LegacyAsyncCommandDispatchRequestHandler());
+ container.RegisterPlurality(new LegacyAsyncCommandResultDispatchRequestHandler());
+ container.RegisterPlurality(new LegacyQueryDispatchRequestHandler());
+ container.RegisterPlurality(new LegacyAsyncQueryDispatchRequestHandler());
+ }
+
///
/// 测试SendCommand方法(带返回值)在命令为null时应抛出ArgumentNullException
///
diff --git a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs
index b9e7b180..f803bb56 100644
--- a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs
+++ b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs
@@ -6,6 +6,8 @@ using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Architectures;
using GFramework.Core.Logging;
+using GFramework.Cqrs.Abstractions.Cqrs;
+using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Tests.Architectures;
@@ -13,6 +15,7 @@ namespace GFramework.Core.Tests.Architectures;
/// 验证 Architecture 通过 ArchitectureModules 暴露出的模块安装与 CQRS 行为注册能力。
/// 这些测试覆盖模块安装回调和请求管道行为接入,确保模块管理器仍然保持可观察行为不变。
///
+[NonParallelizable]
[TestFixture]
public class ArchitectureModulesBehaviorTests
{
@@ -35,6 +38,7 @@ public class ArchitectureModulesBehaviorTests
{
GameContext.Clear();
TrackingPipelineBehavior.InvocationCount = 0;
+ LegacyBridgePipelineTracker.Reset();
}
///
@@ -47,15 +51,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();
+ }
}
///
@@ -68,16 +76,54 @@ 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();
+ }
+ }
+
+ ///
+ /// 验证默认架构初始化路径会自动扫描 Core 程序集里的 legacy bridge handler,
+ /// 使旧 SendCommand / SendQuery 入口也能进入统一 CQRS pipeline。
+ ///
+ [Test]
+ public async Task InitializeAsync_Should_AutoRegister_LegacyBridgeHandlers_For_Default_Core_Assemblies()
+ {
+ LegacyBridgePipelineTracker.Reset();
+ var architecture = new LegacyBridgeArchitecture();
+
+ await architecture.InitializeAsync();
+ try
+ {
+ var query = new LegacyArchitectureBridgeQuery();
+ var command = new LegacyArchitectureBridgeCommand();
+
+ 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();
+ }
}
///
@@ -94,6 +140,27 @@ public class ArchitectureModulesBehaviorTests
}
}
+ ///
+ /// 通过公开初始化入口注册测试 pipeline behavior 的最小架构,
+ /// 用于验证默认 Core 程序集扫描是否会自动接入 legacy bridge handler。
+ ///
+ private sealed class LegacyBridgeArchitecture : Architecture
+ {
+ ///
+ /// 在容器钩子阶段注册 open-generic pipeline behavior,
+ /// 以便 bridge request 走真实的架构初始化与 handler 自动扫描链路。
+ ///
+ public override Action? Configurator => services =>
+ services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LegacyBridgeTrackingPipelineBehavior<,>));
+
+ ///
+ /// 保持空初始化,让测试只聚焦默认 CQRS 接线与 legacy bridge handler 自动发现。
+ ///
+ protected override void OnInitialize()
+ {
+ }
+ }
+
///
/// 记录模块安装调用情况的测试模块。
///
diff --git a/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeAsyncQuery.cs b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeAsyncQuery.cs
new file mode 100644
index 00000000..864864a9
--- /dev/null
+++ b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeAsyncQuery.cs
@@ -0,0 +1,28 @@
+// 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.Architectures;
+
+///
+/// 用于验证 legacy 异步查询桥接时也会显式注入当前架构上下文。
+///
+public sealed class LegacyArchitectureBridgeAsyncQuery : ContextAwareBase, IAsyncQuery
+{
+ ///
+ /// 获取执行期间观察到的上下文实例。
+ ///
+ public IArchitectureContext? ObservedContext { get; private set; }
+
+ ///
+ /// 执行异步查询并返回测试结果。
+ ///
+ public Task DoAsync()
+ {
+ ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
+ return Task.FromResult(64);
+ }
+}
diff --git a/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommand.cs b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommand.cs
new file mode 100644
index 00000000..f58d646b
--- /dev/null
+++ b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommand.cs
@@ -0,0 +1,33 @@
+// 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.Architectures;
+
+///
+/// 用于验证 legacy 命令桥接时会把当前 注入到命令对象。
+///
+public sealed class LegacyArchitectureBridgeCommand : ContextAwareBase, ICommand
+{
+ ///
+ /// 获取执行期间观察到的上下文实例。
+ ///
+ public IArchitectureContext? ObservedContext { get; private set; }
+
+ ///
+ /// 获取当前命令是否已经执行。
+ ///
+ public bool Executed { get; private set; }
+
+ ///
+ /// 执行命令并记录 bridge handler 注入的上下文。
+ ///
+ public void Execute()
+ {
+ Executed = true;
+ ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
+ }
+}
diff --git a/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommandWithResult.cs b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommandWithResult.cs
new file mode 100644
index 00000000..d97d553a
--- /dev/null
+++ b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommandWithResult.cs
@@ -0,0 +1,28 @@
+// 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.Architectures;
+
+///
+/// 用于验证 legacy 带返回值命令桥接时会沿用统一 runtime。
+///
+public sealed class LegacyArchitectureBridgeCommandWithResult : ContextAwareBase, ICommand
+{
+ ///
+ /// 获取执行期间观察到的上下文实例。
+ ///
+ public IArchitectureContext? ObservedContext { get; private set; }
+
+ ///
+ /// 执行命令并返回测试结果。
+ ///
+ public int Execute()
+ {
+ ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
+ return 42;
+ }
+}
diff --git a/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeQuery.cs b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeQuery.cs
new file mode 100644
index 00000000..1f7617a6
--- /dev/null
+++ b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeQuery.cs
@@ -0,0 +1,28 @@
+// 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.Architectures;
+
+///
+/// 用于验证 legacy 查询桥接时会把当前 注入到查询对象。
+///
+public sealed class LegacyArchitectureBridgeQuery : ContextAwareBase, IQuery
+{
+ ///
+ /// 获取执行期间观察到的上下文实例。
+ ///
+ public IArchitectureContext? ObservedContext { get; private set; }
+
+ ///
+ /// 执行查询并返回测试结果。
+ ///
+ public int Do()
+ {
+ ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
+ return 24;
+ }
+}
diff --git a/GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs b/GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs
new file mode 100644
index 00000000..599e8277
--- /dev/null
+++ b/GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs
@@ -0,0 +1,47 @@
+// Copyright (c) 2025-2026 GeWuYou
+// 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;
+
+ ///
+ /// 获取当前进程内被识别为 legacy bridge request 的 pipeline 命中次数。
+ ///
+ public static int InvocationCount => Volatile.Read(ref _invocationCount);
+
+ ///
+ /// 重置计数器。
+ ///
+ public static void Reset()
+ {
+ Volatile.Write(ref _invocationCount, 0);
+ }
+
+ ///
+ /// 若当前请求类型属于 Core legacy bridge request,则记录一次命中。
+ ///
+ public static void Record(Type requestType)
+ {
+ ArgumentNullException.ThrowIfNull(requestType);
+
+ if (typeof(LegacyCqrsDispatchRequestBase).IsAssignableFrom(requestType))
+ {
+ Interlocked.Increment(ref _invocationCount);
+ }
+ }
+}
diff --git a/GFramework.Core.Tests/Architectures/LegacyBridgeTrackingPipelineBehavior.cs b/GFramework.Core.Tests/Architectures/LegacyBridgeTrackingPipelineBehavior.cs
new file mode 100644
index 00000000..4e719e0e
--- /dev/null
+++ b/GFramework.Core.Tests/Architectures/LegacyBridgeTrackingPipelineBehavior.cs
@@ -0,0 +1,24 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Threading;
+using GFramework.Cqrs.Abstractions.Cqrs;
+
+namespace GFramework.Core.Tests.Architectures;
+
+///
+/// 记录 legacy Core CQRS bridge request 是否经过统一 CQRS pipeline 的测试行为。
+///
+public sealed class LegacyBridgeTrackingPipelineBehavior : IPipelineBehavior
+ where TRequest : IRequest
+{
+ ///
+ public async ValueTask Handle(
+ TRequest message,
+ MessageHandlerDelegate next,
+ CancellationToken cancellationToken)
+ {
+ LegacyBridgePipelineTracker.Record(typeof(TRequest));
+ return await next(message, cancellationToken).ConfigureAwait(false);
+ }
+}
diff --git a/GFramework.Core.Tests/Command/CommandExecutorTests.cs b/GFramework.Core.Tests/Command/CommandExecutorTests.cs
index 869b7384..dd08cf80 100644
--- a/GFramework.Core.Tests/Command/CommandExecutorTests.cs
+++ b/GFramework.Core.Tests/Command/CommandExecutorTests.cs
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Command;
+using GFramework.Core.Rule;
+using GFramework.Core.Tests.Architectures;
namespace GFramework.Core.Tests.Command;
@@ -75,6 +77,95 @@ public class CommandExecutorTests
Assert.Throws(() => _commandExecutor.Send(null!));
}
+ ///
+ /// 验证当 legacy 命令没有可用上下文时,会安全回退到本地直接执行路径。
+ ///
+ [Test]
+ public void Send_Should_Fall_Back_To_Legacy_Execution_When_Context_IsMissing()
+ {
+ var runtime = new RecordingCqrsRuntime();
+ var executor = new CommandExecutor(runtime);
+ var command = new MissingContextLegacyCommand();
+
+ executor.Send(command);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(command.Executed, Is.True);
+ Assert.That(runtime.LastRequest, Is.Null);
+ });
+ }
+
+ ///
+ /// 验证非“缺上下文”类型的 不会被 bridge 回退逻辑误吞掉。
+ ///
+ [Test]
+ public void Send_Should_Propagate_InvalidOperationException_When_ContextAware_Target_Throws_Unexpected_Error()
+ {
+ var runtime = new RecordingCqrsRuntime();
+ var executor = new CommandExecutor(runtime);
+ var command = new ThrowingLegacyCommand();
+
+ Assert.That(
+ () => executor.Send(command),
+ Throws.InvalidOperationException.With.Message.EqualTo(ThrowingLegacyCommand.ExceptionMessage));
+ Assert.That(runtime.LastRequest, Is.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());
+ Assert.That(command.ObservedContext, Is.SameAs(expectedContext));
+ });
+ }
+
///
/// 测试SendAsync方法执行异步命令
///
@@ -122,4 +213,65 @@ public class CommandExecutorTests
{
Assert.ThrowsAsync(() => _commandExecutor.SendAsync(null!));
}
+
+ ///
+ /// 为同步 bridge 测试提供最小架构上下文替身。
+ ///
+ private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
+ {
+ }
+
+ ///
+ /// 用于验证缺少上下文时仍会走本地 fallback 的测试命令。
+ ///
+ private sealed class MissingContextLegacyCommand : GFramework.Core.Abstractions.Rule.IContextAware, GFramework.Core.Abstractions.Command.ICommand
+ {
+ ///
+ /// 获取命令是否已经执行。
+ ///
+ public bool Executed { get; private set; }
+
+ ///
+ public void SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+ }
+
+ ///
+ public GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext()
+ {
+ throw new InvalidOperationException("Architecture context has not been set. Call SetContext before accessing the context.");
+ }
+
+ ///
+ public void Execute()
+ {
+ Executed = true;
+ }
+ }
+
+ ///
+ /// 用于验证 bridge 上下文解析不会吞掉意外运行时错误的测试命令。
+ ///
+ private sealed class ThrowingLegacyCommand : GFramework.Core.Abstractions.Rule.IContextAware, GFramework.Core.Abstractions.Command.ICommand
+ {
+ internal const string ExceptionMessage = "Unexpected context failure.";
+
+ ///
+ public void SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+ }
+
+ ///
+ public GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext()
+ {
+ throw new InvalidOperationException(ExceptionMessage);
+ }
+
+ ///
+ public void Execute()
+ {
+ }
+ }
}
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..5dd07317
--- /dev/null
+++ b/GFramework.Core.Tests/Command/RecordingCqrsRuntime.cs
@@ -0,0 +1,207 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using GFramework.Core.Abstractions.Rule;
+using GFramework.Core.Cqrs;
+using GFramework.Cqrs.Abstractions.Cqrs;
+
+namespace GFramework.Core.Tests.Command;
+
+///
+/// 记录 bridge 执行线程与收到请求的最小 CQRS runtime 测试替身。
+///
+internal sealed class RecordingCqrsRuntime(Func