fix(core): 收口 legacy cqrs bridge 评审问题

- 修复 legacy bridge 测试装配与清理流程,改用 InternalsVisibleTo 和显式 handler 注册,补齐共享计数器重置与生命周期说明

- 优化 CommandExecutor、QueryExecutor 与相关模块的 runtime 契约,补充 XML 文档、nullable 注解和显式依赖解析

- 更新 legacy 异步 bridge 的取消语义、兼容文档回退边界以及 cqrs-rewrite active tracking/trace
This commit is contained in:
gewuyou 2026-05-07 17:54:05 +08:00
parent d7293aa475
commit 6056159866
24 changed files with 240 additions and 90 deletions

View File

@ -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
{
/// <summary>
/// 初始化测试所需的容器与默认服务实例。
/// </summary>
[SetUp]
public void SetUp()
{
@ -78,6 +82,16 @@ public class ArchitectureContextTests
_context = new ArchitectureContext(_container);
}
/// <summary>
/// 释放当前测试创建的容器,并清理 legacy bridge 共享计数状态。
/// </summary>
[TearDown]
public void TearDown()
{
LegacyBridgePipelineTracker.Reset();
_container?.Dispose();
}
private AsyncQueryExecutor? _asyncQueryBus;
private CommandExecutor? _commandBus;
private MicrosoftDiContainer? _container;
@ -258,34 +272,19 @@ public class ArchitectureContextTests
}
/// <summary>
/// 通过反射把 GFramework.Core 内部的 legacy bridge handler 实例预先注册成可见的实例绑定。
/// 把 GFramework.Core 内部的 legacy bridge handler 实例预先注册成可见的实例绑定。
/// </summary>
/// <param name="container">目标测试容器。</param>
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());
}
/// <summary>

View File

@ -37,6 +37,7 @@ public class ArchitectureModulesBehaviorTests
{
GameContext.Clear();
TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount = 0;
LegacyBridgePipelineTracker.Reset();
}
/// <summary>
@ -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<InstalledByModuleUtility>(), 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<InstalledByModuleUtility>(), Is.Not.Null);
});
}
finally
{
await architecture.DestroyAsync();
}
}
/// <summary>
@ -70,16 +75,20 @@ public class ArchitectureModulesBehaviorTests
target.RegisterCqrsPipelineBehavior<TrackingPipelineBehavior<ModuleBehaviorRequest, string>>());
await architecture.InitializeAsync();
var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest());
Assert.Multiple(() =>
try
{
Assert.That(response, Is.EqualTo("handled"));
Assert.That(TrackingPipelineBehavior<ModuleBehaviorRequest, string>.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<ModuleBehaviorRequest, string>.InvocationCount, Is.EqualTo(1));
});
}
finally
{
await architecture.DestroyAsync();
}
}
/// <summary>
@ -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();
}
}
/// <summary>

View File

@ -2,12 +2,19 @@
// SPDX-License-Identifier: Apache-2.0
using System.Threading;
using GFramework.Core.Cqrs;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 为 legacy bridge pipeline 回归测试保存跨泛型闭包共享的计数状态。
/// </summary>
/// <remarks>
/// 该计数器通过 <see cref="Interlocked.Increment(ref int)" /> 原子递增,并使用
/// <see cref="Volatile" /> 读写,因此单次读写操作本身是线程安全的。
/// 由于状态在同一进程内跨 fixture 共享,所有使用它的测试都必须在清理阶段调用 <see cref="Reset" />
/// 以避免并行或失败测试把旧计数泄露给后续断言。
/// </remarks>
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);
}

View File

@ -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<Unit>
{
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<object?>
{
if (!TryResolveDispatchContext(target, out var context) || _runtime is null)
if (!TryResolveDispatchContext(target, out var context))
{
result = default;
return false;
@ -159,14 +160,16 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
/// 通过统一 CQRS runtime 异步执行 legacy 带返回值命令,并把装箱结果还原为目标类型。
/// </summary>
/// <typeparam name="TResult">命令返回值类型。</typeparam>
/// <param name="runtime">负责调度当前 bridge request 的统一 CQRS runtime。</param>
/// <param name="context">当前架构上下文。</param>
/// <param name="command">要桥接的 legacy 命令。</param>
/// <returns>命令执行结果。</returns>
private async Task<TResult> BridgeAsyncCommandWithResultAsync<TResult>(
private static async Task<TResult> BridgeAsyncCommandWithResultAsync<TResult>(
ICqrsRuntime runtime,
GFramework.Core.Abstractions.Architectures.IArchitectureContext context,
IAsyncCommand<TResult> 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
/// <param name="target">即将执行的 legacy 目标对象。</param>
/// <param name="context">命中时返回可用于 CQRS runtime 的架构上下文。</param>
/// <returns>如果既接入了 runtime 且目标对象提供了上下文,则返回 <see langword="true" />。</returns>
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!;

View File

@ -9,11 +9,17 @@ namespace GFramework.Core.Cqrs;
/// <summary>
/// 包装 legacy 异步无返回值命令,使其能够通过自有 CQRS runtime 调度。
/// </summary>
/// <param name="command">当前 bridge request 代理的 legacy 异步命令实例。</param>
internal sealed class LegacyAsyncCommandDispatchRequest(CoreCommand.IAsyncCommand command)
: LegacyCqrsDispatchRequestBase(command), IRequest<Unit>
: LegacyCqrsDispatchRequestBase(ValidateCommand(command)), IRequest<Unit>
{
/// <summary>
/// 获取当前 bridge request 代理的异步命令实例。
/// </summary>
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));
}
}

View File

@ -8,6 +8,8 @@ namespace GFramework.Core.Cqrs;
/// <summary>
/// 包装 legacy 异步带返回值命令,使其能够通过自有 CQRS runtime 调度。
/// </summary>
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 命令目标实例。</param>
/// <param name="executeAsync">封装 legacy 异步命令执行逻辑并返回装箱结果的委托。</param>
internal sealed class LegacyAsyncCommandResultDispatchRequest(object target, Func<Task<object?>> executeAsync)
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
{
@ -16,5 +18,6 @@ internal sealed class LegacyAsyncCommandResultDispatchRequest(object target, Fun
/// <summary>
/// 异步执行底层 legacy 命令并返回装箱后的结果。
/// </summary>
/// <returns>表示异步执行结果的任务;任务结果为底层 legacy 命令返回的装箱值。</returns>
public Task<object?> ExecuteAsync() => _executeAsync();
}

View File

@ -17,7 +17,9 @@ internal sealed class LegacyAsyncCommandResultDispatchRequestHandler
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
// Legacy ExecuteAsync contract does not accept CancellationToken; use WaitAsync so the caller can observe cancellation promptly.
cancellationToken.ThrowIfCancellationRequested();
PrepareTarget(request.Target);
return await request.ExecuteAsync().ConfigureAwait(false);
return await request.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
}
}

View File

@ -8,6 +8,8 @@ namespace GFramework.Core.Cqrs;
/// <summary>
/// 包装 legacy 异步查询,使其能够通过自有 CQRS runtime 调度。
/// </summary>
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 查询目标实例。</param>
/// <param name="executeAsync">封装 legacy 异步查询执行逻辑并返回装箱结果的委托。</param>
internal sealed class LegacyAsyncQueryDispatchRequest(object target, Func<Task<object?>> executeAsync)
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
{
@ -16,5 +18,6 @@ internal sealed class LegacyAsyncQueryDispatchRequest(object target, Func<Task<o
/// <summary>
/// 异步执行底层 legacy 查询并返回装箱后的结果。
/// </summary>
/// <returns>表示异步执行结果的任务;任务结果为底层 legacy 查询返回的装箱值。</returns>
public Task<object?> ExecuteAsync() => _executeAsync();
}

View File

@ -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);
}
}

View File

@ -9,11 +9,17 @@ namespace GFramework.Core.Cqrs;
/// <summary>
/// 包装 legacy 无返回值命令,使其能够通过自有 CQRS runtime 调度。
/// </summary>
/// <param name="command">当前 bridge request 代理的 legacy 命令实例。</param>
internal sealed class LegacyCommandDispatchRequest(CoreCommand.ICommand command)
: LegacyCqrsDispatchRequestBase(command), IRequest<Unit>
: LegacyCqrsDispatchRequestBase(ValidateCommand(command)), IRequest<Unit>
{
/// <summary>
/// 获取当前 bridge request 代理的命令实例。
/// </summary>
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));
}
}

View File

@ -8,6 +8,8 @@ namespace GFramework.Core.Cqrs;
/// <summary>
/// 包装 legacy 带返回值命令,使其能够通过自有 CQRS runtime 调度。
/// </summary>
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 命令目标实例。</param>
/// <param name="execute">封装 legacy 命令执行逻辑并返回装箱结果的委托。</param>
internal sealed class LegacyCommandResultDispatchRequest(object target, Func<object?> execute)
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
{
@ -16,5 +18,6 @@ internal sealed class LegacyCommandResultDispatchRequest(object target, Func<obj
/// <summary>
/// 执行底层 legacy 命令并返回装箱后的结果。
/// </summary>
/// <returns>底层 legacy 命令执行后的装箱结果;若命令语义无返回值则为 <see langword="null" />。</returns>
public object? Execute() => _execute();
}

View File

@ -14,6 +14,11 @@ internal abstract class LegacyCqrsDispatchHandlerBase : ContextAwareBase
/// <summary>
/// 在执行 legacy 命令或查询前,把当前架构上下文显式注入给支持 <see cref="IContextAware" /> 的目标对象。
/// </summary>
/// <param name="target">即将执行的 legacy 目标对象。</param>
/// <exception cref="ArgumentNullException"><paramref name="target" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">
/// 目标对象实现了 <see cref="IContextAware" />,但当前 handler 还没有可用的架构上下文。
/// </exception>
protected void PrepareTarget(object target)
{
ArgumentNullException.ThrowIfNull(target);

View File

@ -6,6 +6,7 @@ namespace GFramework.Core.Cqrs;
/// <summary>
/// 为 legacy Command / Query 到自有 CQRS runtime 的桥接请求提供共享的目标对象封装。
/// </summary>
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 目标对象。</param>
internal abstract class LegacyCqrsDispatchRequestBase(object target)
{
/// <summary>

View File

@ -8,6 +8,8 @@ namespace GFramework.Core.Cqrs;
/// <summary>
/// 包装 legacy 同步查询,使其能够通过自有 CQRS runtime 调度。
/// </summary>
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 查询目标实例。</param>
/// <param name="execute">封装 legacy 查询执行逻辑并返回装箱结果的委托。</param>
internal sealed class LegacyQueryDispatchRequest(object target, Func<object?> execute)
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
{
@ -16,5 +18,6 @@ internal sealed class LegacyQueryDispatchRequest(object target, Func<object?> ex
/// <summary>
/// 执行底层 legacy 查询并返回装箱后的结果。
/// </summary>
/// <returns>底层 legacy 查询执行后的装箱结果;若查询无返回值则为 <see langword="null" />。</returns>
public object? Execute() => _execute();
}

View File

@ -0,0 +1,6 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("GFramework.Core.Tests")]

View File

@ -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<TResult>(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 查询,并把装箱结果还原为目标类型。
/// </summary>
/// <typeparam name="TResult">查询结果类型。</typeparam>
/// <param name="runtime">负责调度当前 bridge request 的统一 CQRS runtime。</param>
/// <param name="context">当前架构上下文。</param>
/// <param name="query">要桥接的 legacy 查询。</param>
/// <returns>查询执行结果。</returns>
private async Task<TResult> BridgeAsyncQueryAsync<TResult>(
private static async Task<TResult> BridgeAsyncQueryAsync<TResult>(
ICqrsRuntime runtime,
GFramework.Core.Abstractions.Architectures.IArchitectureContext context,
IAsyncQuery<TResult> 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
/// <param name="query">即将执行的 legacy 查询对象。</param>
/// <param name="context">命中时返回可用于 CQRS runtime 的架构上下文。</param>
/// <returns>如果既接入了 runtime 且查询对象提供了上下文,则返回 <see langword="true" />。</returns>
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!;

View File

@ -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
/// <param name="query">即将执行的 legacy 查询对象。</param>
/// <param name="context">命中时返回可用于 CQRS runtime 的架构上下文。</param>
/// <returns>如果既接入了 runtime 且查询对象提供了上下文,则返回 <see langword="true" />。</returns>
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!;

View File

@ -33,11 +33,19 @@ public sealed class AsyncQueryExecutorModule : IServiceModule
/// 注册异步查询执行器到依赖注入容器。
/// 创建异步查询执行器实例并将其注册为多例服务。
/// </summary>
/// <param name="container">依赖注入容器实例。</param>
/// <param name="container">承载异步查询执行器与 CQRS runtime 的依赖注入容器实例。</param>
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">
/// 容器中尚未注册唯一的 <see cref="ICqrsRuntime" /> 实例,无法构建统一 runtime 版本的异步查询执行器。
/// </exception>
/// <remarks>
/// 该模块会在注册阶段立即解析 <see cref="ICqrsRuntime" />,因此
/// <see cref="CqrsRuntimeModule" /> 必须先于当前模块完成注册。
/// </remarks>
public void Register(IIocContainer container)
{
ArgumentNullException.ThrowIfNull(container);
container.RegisterPlurality(new AsyncQueryExecutor(container.Get<ICqrsRuntime>()));
container.RegisterPlurality(new AsyncQueryExecutor(container.GetRequired<ICqrsRuntime>()));
}
/// <summary>

View File

@ -33,11 +33,19 @@ public sealed class CommandExecutorModule : IServiceModule
/// 注册命令执行器到依赖注入容器。
/// 创建命令执行器实例并将其注册为多例服务。
/// </summary>
/// <param name="container">依赖注入容器实例。</param>
/// <param name="container">承载命令执行器与 CQRS runtime 的依赖注入容器实例。</param>
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">
/// 容器中尚未注册唯一的 <see cref="ICqrsRuntime" /> 实例,无法构建统一 runtime 版本的命令执行器。
/// </exception>
/// <remarks>
/// 该模块会在注册阶段立即解析 <see cref="ICqrsRuntime" />,因此
/// <see cref="CqrsRuntimeModule" /> 必须先于当前模块完成注册。
/// </remarks>
public void Register(IIocContainer container)
{
ArgumentNullException.ThrowIfNull(container);
container.RegisterPlurality(new CommandExecutor(container.Get<ICqrsRuntime>()));
container.RegisterPlurality(new CommandExecutor(container.GetRequired<ICqrsRuntime>()));
}
/// <summary>

View File

@ -33,11 +33,19 @@ public sealed class QueryExecutorModule : IServiceModule
/// 注册查询执行器到依赖注入容器。
/// 创建查询执行器实例并将其注册为多例服务。
/// </summary>
/// <param name="container">依赖注入容器实例。</param>
/// <param name="container">承载查询执行器与 CQRS runtime 的依赖注入容器实例。</param>
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">
/// 容器中尚未注册唯一的 <see cref="ICqrsRuntime" /> 实例,无法构建统一 runtime 版本的查询执行器。
/// </exception>
/// <remarks>
/// 该模块会在注册阶段立即解析 <see cref="ICqrsRuntime" />,因此
/// <see cref="CqrsRuntimeModule" /> 必须先于当前模块完成注册。
/// </remarks>
public void Register(IIocContainer container)
{
ArgumentNullException.ThrowIfNull(container);
container.RegisterPlurality(new QueryExecutor(container.Get<ICqrsRuntime>()));
container.RegisterPlurality(new QueryExecutor(container.GetRequired<ICqrsRuntime>()));
}
/// <summary>

View File

@ -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<ICqrsRuntime>()` 并在 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 <temp-dir>` 当前只产出 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` 兼容入口的职责
## 活跃文档

View File

@ -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<ICqrsRuntime>()` 的隐式注册顺序,但此前既未显式失败,也未写进 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` 增补 `<remarks>`,并改用 `typeof(LegacyCqrsDispatchRequestBase).IsAssignableFrom(requestType)` 识别 bridge request
- 为 `LegacyAsyncQueryDispatchRequestHandler` / `LegacyAsyncCommandResultDispatchRequestHandler` 加入预取消检查与 `WaitAsync(cancellationToken)`
- 将三个 executor module 改为 `GetRequired<ICqrsRuntime>()`,同时在 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 bridgeCQRS-REWRITE-RP-093
- 延续 `$gframework-batch-boot 50`,本轮明确不只盯 benchmark而是同时处理两个目标

View File

@ -107,6 +107,7 @@ var reward = this.SendCommand(new GetGoldRewardCommand(new GetGoldRewardInput(3)
在标准架构启动路径中,这些兼容入口底层已经统一改走 `ICqrsRuntime`
这意味着历史命令调用链在不改调用方式的前提下,也会复用同一套 pipeline 与上下文注入语义。
只有在你直接 `new CommandExecutor()` 做隔离测试,且没有提供 `ICqrsRuntime` 时,才会回退到 legacy 直接执行;此时不会注入统一 pipeline也不会额外补上下文桥接链路。
`IContextAware` 对象内,通常直接通过扩展使用:

View File

@ -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 入口