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
/// 命令返回值类型。
+ /// 负责调度当前 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
+ /// 表示异步执行结果的任务;任务结果为底层 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 入口