refactor(core): 统一旧版命令查询到Cqrs运行时

- 重构 Core 兼容命令查询入口,使 legacy SendCommand/SendQuery 通过内部 bridge request 复用统一 CQRS runtime

- 新增 legacy bridge handler 与真实启动路径回归测试,验证默认架构初始化会自动接入统一 pipeline

- 更新 Core 与 CQRS 文档及 cqrs-rewrite 跟踪,记录 Mediator 尚未吸收的能力差距与后续收口方向
This commit is contained in:
gewuyou 2026-05-07 17:20:14 +08:00
parent 017e689abd
commit d7293aa475
36 changed files with 1042 additions and 44 deletions

View File

@ -15,6 +15,7 @@ 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;
@ -71,6 +72,8 @@ public class ArchitectureContextTests
_container.RegisterPlurality(_queryBus);
_container.RegisterPlurality(_asyncQueryBus);
_container.RegisterPlurality(_environment);
new CqrsRuntimeModule().Register(_container);
RegisterLegacyBridgeHandlers(_container);
_context = new ArchitectureContext(_container);
}
@ -124,6 +127,24 @@ public class ArchitectureContextTests
Assert.That(result, Is.EqualTo(42));
}
/// <summary>
/// 测试 legacy 查询通过 <see cref="ArchitectureContext" /> 发送时会进入统一 CQRS pipeline
/// 并把当前架构上下文注入到查询对象。
/// </summary>
[Test]
public void SendQuery_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context()
{
LegacyBridgePipelineTracker.Reset();
var testQuery = new LegacyArchitectureBridgeQuery();
var bridgeContext = CreateFrozenBridgeContext();
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));
}
/// <summary>
/// 测试SendQuery方法在查询为null时应抛出ArgumentNullException
/// </summary>
@ -146,6 +167,24 @@ public class ArchitectureContextTests
Assert.That(testCommand.Executed, Is.True);
}
/// <summary>
/// 测试 legacy 命令通过 <see cref="ArchitectureContext" /> 发送时会进入统一 CQRS pipeline
/// 并把当前架构上下文注入到命令对象。
/// </summary>
[Test]
public void SendCommand_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context()
{
LegacyBridgePipelineTracker.Reset();
var testCommand = new LegacyArchitectureBridgeCommand();
var bridgeContext = CreateFrozenBridgeContext();
bridgeContext.SendCommand(testCommand);
Assert.That(testCommand.Executed, Is.True);
Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
}
/// <summary>
/// 测试SendCommand方法在命令为null时应抛出ArgumentNullException
/// </summary>
@ -168,6 +207,87 @@ public class ArchitectureContextTests
Assert.That(result, Is.EqualTo(123));
}
/// <summary>
/// 测试 legacy 带返回值命令通过 <see cref="ArchitectureContext" /> 发送时会进入统一 CQRS pipeline
/// 并保持原始返回值语义。
/// </summary>
[Test]
public void SendCommand_WithResult_Should_Bridge_Through_CqrsRuntime()
{
LegacyBridgePipelineTracker.Reset();
var testCommand = new LegacyArchitectureBridgeCommandWithResult();
var bridgeContext = CreateFrozenBridgeContext();
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));
}
/// <summary>
/// 测试 legacy 异步查询通过 <see cref="ArchitectureContext" /> 发送时也会进入统一 CQRS pipeline。
/// </summary>
[Test]
public async Task SendQueryAsync_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context()
{
LegacyBridgePipelineTracker.Reset();
var testQuery = new LegacyArchitectureBridgeAsyncQuery();
var bridgeContext = CreateFrozenBridgeContext();
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));
}
/// <summary>
/// 为需要验证统一 CQRS pipeline 的用例创建一个已冻结的最小 bridge 上下文。
/// </summary>
/// <returns>能够执行 legacy bridge request 且会 materialize open-generic pipeline behavior 的上下文。</returns>
private static ArchitectureContext CreateFrozenBridgeContext()
{
var container = new MicrosoftDiContainer();
RegisterLegacyBridgeHandlers(container);
new CqrsRuntimeModule().Register(container);
container.ExecuteServicesHook(services =>
services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LegacyBridgeTrackingPipelineBehavior<,>)));
container.Freeze();
return new ArchitectureContext(container);
}
/// <summary>
/// 通过反射把 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);
}
}
/// <summary>
/// 测试SendCommand方法带返回值在命令为null时应抛出ArgumentNullException
/// </summary>

View File

@ -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;
@ -80,6 +82,36 @@ public class ArchitectureModulesBehaviorTests
await architecture.DestroyAsync();
}
/// <summary>
/// 验证默认架构初始化路径会自动扫描 Core 程序集里的 legacy bridge handler
/// 使旧 <c>SendCommand</c> / <c>SendQuery</c> 入口也能进入统一 CQRS pipeline。
/// </summary>
[Test]
public async Task InitializeAsync_Should_AutoRegister_LegacyBridgeHandlers_For_Default_Core_Assemblies()
{
LegacyBridgePipelineTracker.Reset();
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(() =>
{
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));
});
await architecture.DestroyAsync();
}
/// <summary>
/// 用于测试模块行为的最小架构实现。
/// </summary>
@ -94,6 +126,27 @@ public class ArchitectureModulesBehaviorTests
}
}
/// <summary>
/// 通过公开初始化入口注册测试 pipeline behavior 的最小架构,
/// 用于验证默认 Core 程序集扫描是否会自动接入 legacy bridge handler。
/// </summary>
private sealed class LegacyBridgeArchitecture : Architecture
{
/// <summary>
/// 在容器钩子阶段注册 open-generic pipeline behavior
/// 以便 bridge request 走真实的架构初始化与 handler 自动扫描链路。
/// </summary>
public override Action<IServiceCollection>? Configurator => services =>
services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LegacyBridgeTrackingPipelineBehavior<,>));
/// <summary>
/// 保持空初始化,让测试只聚焦默认 CQRS 接线与 legacy bridge handler 自动发现。
/// </summary>
protected override void OnInitialize()
{
}
}
/// <summary>
/// 记录模块安装调用情况的测试模块。
/// </summary>

View File

@ -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;
/// <summary>
/// 用于验证 legacy 异步查询桥接时也会显式注入当前架构上下文。
/// </summary>
public sealed class LegacyArchitectureBridgeAsyncQuery : ContextAwareBase, IAsyncQuery<int>
{
/// <summary>
/// 获取执行期间观察到的上下文实例。
/// </summary>
public IArchitectureContext? ObservedContext { get; private set; }
/// <summary>
/// 执行异步查询并返回测试结果。
/// </summary>
public Task<int> DoAsync()
{
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
return Task.FromResult(64);
}
}

View File

@ -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;
/// <summary>
/// 用于验证 legacy 命令桥接时会把当前 <see cref="IArchitectureContext" /> 注入到命令对象。
/// </summary>
public sealed class LegacyArchitectureBridgeCommand : ContextAwareBase, ICommand
{
/// <summary>
/// 获取执行期间观察到的上下文实例。
/// </summary>
public IArchitectureContext? ObservedContext { get; private set; }
/// <summary>
/// 获取当前命令是否已经执行。
/// </summary>
public bool Executed { get; private set; }
/// <summary>
/// 执行命令并记录 bridge handler 注入的上下文。
/// </summary>
public void Execute()
{
Executed = true;
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
}
}

View File

@ -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;
/// <summary>
/// 用于验证 legacy 带返回值命令桥接时会沿用统一 runtime。
/// </summary>
public sealed class LegacyArchitectureBridgeCommandWithResult : ContextAwareBase, ICommand<int>
{
/// <summary>
/// 获取执行期间观察到的上下文实例。
/// </summary>
public IArchitectureContext? ObservedContext { get; private set; }
/// <summary>
/// 执行命令并返回测试结果。
/// </summary>
public int Execute()
{
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
return 42;
}
}

View File

@ -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;
/// <summary>
/// 用于验证 legacy 查询桥接时会把当前 <see cref="IArchitectureContext" /> 注入到查询对象。
/// </summary>
public sealed class LegacyArchitectureBridgeQuery : ContextAwareBase, IQuery<int>
{
/// <summary>
/// 获取执行期间观察到的上下文实例。
/// </summary>
public IArchitectureContext? ObservedContext { get; private set; }
/// <summary>
/// 执行查询并返回测试结果。
/// </summary>
public int Do()
{
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
return 24;
}
}

View File

@ -0,0 +1,41 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using System.Threading;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 为 legacy bridge pipeline 回归测试保存跨泛型闭包共享的计数状态。
/// </summary>
public static class LegacyBridgePipelineTracker
{
private static int _invocationCount;
/// <summary>
/// 获取当前进程内被识别为 legacy bridge request 的 pipeline 命中次数。
/// </summary>
public static int InvocationCount => Volatile.Read(ref _invocationCount);
/// <summary>
/// 重置计数器。
/// </summary>
public static void Reset()
{
Volatile.Write(ref _invocationCount, 0);
}
/// <summary>
/// 若当前请求类型属于 Core legacy bridge request则记录一次命中。
/// </summary>
public static void Record(Type requestType)
{
ArgumentNullException.ThrowIfNull(requestType);
if (string.Equals(requestType.Namespace, "GFramework.Core.Cqrs", StringComparison.Ordinal) &&
requestType.Name.Contains("Legacy", StringComparison.Ordinal))
{
Interlocked.Increment(ref _invocationCount);
}
}
}

View File

@ -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;
/// <summary>
/// 记录 legacy Core CQRS bridge request 是否经过统一 CQRS pipeline 的测试行为。
/// </summary>
public sealed class LegacyBridgeTrackingPipelineBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
/// <inheritdoc />
public async ValueTask<TResponse> Handle(
TRequest message,
MessageHandlerDelegate<TRequest, TResponse> next,
CancellationToken cancellationToken)
{
LegacyBridgePipelineTracker.Record(typeof(TRequest));
return await next(message, cancellationToken).ConfigureAwait(false);
}
}

View File

@ -11,6 +11,7 @@ using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
@ -180,10 +181,12 @@ public class ArchitectureContext : IArchitectureContext
/// <returns>查询结果</returns>
public TResult SendQuery<TResult>(IQuery<TResult> query)
{
if (query == null) throw new ArgumentNullException(nameof(query));
var queryBus = GetOrCache<IQueryExecutor>();
if (queryBus == null) throw new InvalidOperationException("IQueryExecutor not registered");
return queryBus.Send(query);
ArgumentNullException.ThrowIfNull(query);
var boxedResult = SendRequest(
new LegacyQueryDispatchRequest(
query,
() => query.Do()));
return (TResult)boxedResult!;
}
/// <summary>
@ -192,7 +195,7 @@ public class ArchitectureContext : IArchitectureContext
/// <typeparam name="TResponse">查询响应类型</typeparam>
/// <param name="query">要发送的查询对象</param>
/// <returns>查询结果</returns>
public TResponse SendQuery<TResponse>(Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
public TResponse SendQuery<TResponse>(global::GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
{
return SendQueryAsync(query).AsTask().GetAwaiter().GetResult();
}
@ -205,10 +208,13 @@ public class ArchitectureContext : IArchitectureContext
/// <returns>查询结果</returns>
public async Task<TResult> SendQueryAsync<TResult>(IAsyncQuery<TResult> query)
{
if (query == null) throw new ArgumentNullException(nameof(query));
var asyncQueryBus = GetOrCache<IAsyncQueryExecutor>();
if (asyncQueryBus == null) throw new InvalidOperationException("IAsyncQueryExecutor not registered");
return await asyncQueryBus.SendAsync(query).ConfigureAwait(false);
ArgumentNullException.ThrowIfNull(query);
var boxedResult = await SendRequestAsync(
new LegacyAsyncQueryDispatchRequest(
query,
async () => await query.DoAsync().ConfigureAwait(false)))
.ConfigureAwait(false);
return (TResult)boxedResult!;
}
/// <summary>
@ -218,7 +224,7 @@ public class ArchitectureContext : IArchitectureContext
/// <param name="query">要发送的查询对象</param>
/// <param name="cancellationToken">取消令牌,用于取消操作</param>
/// <returns>包含查询结果的ValueTask</returns>
public async ValueTask<TResponse> SendQueryAsync<TResponse>(Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
public async ValueTask<TResponse> SendQueryAsync<TResponse>(global::GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(query);
@ -355,7 +361,7 @@ public class ArchitectureContext : IArchitectureContext
/// <param name="cancellationToken">取消令牌,用于取消操作</param>
/// <returns>包含命令执行结果的ValueTask</returns>
public async ValueTask<TResponse> SendCommandAsync<TResponse>(
Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
global::GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(command);
@ -369,9 +375,7 @@ public class ArchitectureContext : IArchitectureContext
public async Task SendCommandAsync(IAsyncCommand command)
{
ArgumentNullException.ThrowIfNull(command);
var commandBus = GetOrCache<ICommandExecutor>();
if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered");
await commandBus.SendAsync(command).ConfigureAwait(false);
await SendRequestAsync(new LegacyAsyncCommandDispatchRequest(command)).ConfigureAwait(false);
}
/// <summary>
@ -383,9 +387,12 @@ public class ArchitectureContext : IArchitectureContext
public async Task<TResult> SendCommandAsync<TResult>(IAsyncCommand<TResult> command)
{
ArgumentNullException.ThrowIfNull(command);
var commandBus = GetOrCache<ICommandExecutor>();
if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered");
return await commandBus.SendAsync(command).ConfigureAwait(false);
var boxedResult = await SendRequestAsync(
new LegacyAsyncCommandResultDispatchRequest(
command,
async () => await command.ExecuteAsync().ConfigureAwait(false)))
.ConfigureAwait(false);
return (TResult)boxedResult!;
}
/// <summary>
@ -394,7 +401,7 @@ public class ArchitectureContext : IArchitectureContext
/// <typeparam name="TResponse">命令响应类型</typeparam>
/// <param name="command">要发送的命令对象</param>
/// <returns>命令执行结果</returns>
public TResponse SendCommand<TResponse>(Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
public TResponse SendCommand<TResponse>(global::GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
{
return SendCommandAsync(command).AsTask().GetAwaiter().GetResult();
}
@ -406,8 +413,7 @@ public class ArchitectureContext : IArchitectureContext
public void SendCommand(ICommand command)
{
ArgumentNullException.ThrowIfNull(command);
var commandBus = GetOrCache<ICommandExecutor>();
commandBus.Send(command);
SendRequest(new LegacyCommandDispatchRequest(command));
}
/// <summary>
@ -419,9 +425,11 @@ public class ArchitectureContext : IArchitectureContext
public TResult SendCommand<TResult>(ICommand<TResult> command)
{
ArgumentNullException.ThrowIfNull(command);
var commandBus = GetOrCache<ICommandExecutor>();
if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered");
return commandBus.Send(command);
var boxedResult = SendRequest(
new LegacyCommandResultDispatchRequest(
command,
() => command.Execute()));
return (TResult)boxedResult!;
}
#endregion

View File

@ -2,6 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
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;
namespace GFramework.Core.Command;
@ -10,8 +13,20 @@ namespace GFramework.Core.Command;
/// 表示一个命令执行器,用于执行命令操作。
/// 该类实现了 ICommandExecutor 接口,提供命令执行的核心功能。
/// </summary>
public sealed class CommandExecutor : ICommandExecutor
public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExecutor
{
private readonly ICqrsRuntime? _runtime = runtime;
/// <summary>
/// 获取当前执行器是否已接入统一 CQRS runtime。
/// </summary>
/// <remarks>
/// 当调用方只是直接 new 一个执行器做纯单元测试时,这里允许为空,并回退到 legacy 直接执行路径;
/// 当执行器由架构容器提供给 <see cref="Architectures.ArchitectureContext" /> 使用时,应始终传入 runtime
/// 以便旧入口也复用统一 pipeline 与 handler 调度链路。
/// </remarks>
public bool UsesCqrsRuntime => _runtime is not null;
/// <summary>
/// 发送并执行无返回值的命令
/// </summary>
@ -21,6 +36,11 @@ public sealed class CommandExecutor : ICommandExecutor
{
ArgumentNullException.ThrowIfNull(command);
if (TryExecuteThroughCqrsRuntime(command, static currentCommand => new LegacyCommandDispatchRequest(currentCommand)))
{
return;
}
command.Execute();
}
@ -35,6 +55,16 @@ public sealed class CommandExecutor : ICommandExecutor
{
ArgumentNullException.ThrowIfNull(command);
if (TryExecuteThroughCqrsRuntime(
command,
static currentCommand => new LegacyCommandResultDispatchRequest(
currentCommand,
() => currentCommand.Execute()),
out TResult? result))
{
return result!;
}
return command.Execute();
}
@ -47,6 +77,11 @@ public sealed class CommandExecutor : ICommandExecutor
{
ArgumentNullException.ThrowIfNull(command);
if (TryResolveDispatchContext(command, out var context) && _runtime is not null)
{
return _runtime.SendAsync(context, new LegacyAsyncCommandDispatchRequest(command)).AsTask();
}
return command.ExecuteAsync();
}
@ -61,6 +96,108 @@ public sealed class CommandExecutor : ICommandExecutor
{
ArgumentNullException.ThrowIfNull(command);
if (TryResolveDispatchContext(command, out var context) && _runtime is not null)
{
return BridgeAsyncCommandWithResultAsync(context, command);
}
return command.ExecuteAsync();
}
}
/// <summary>
/// 尝试通过统一 CQRS runtime 执行当前 legacy 请求。
/// </summary>
/// <typeparam name="TTarget">legacy 目标对象类型。</typeparam>
/// <typeparam name="TRequest">bridge request 类型。</typeparam>
/// <param name="target">即将执行的 legacy 目标对象。</param>
/// <param name="requestFactory">用于创建 bridge request 的工厂。</param>
/// <returns>若成功切入 CQRS runtime 则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
private bool TryExecuteThroughCqrsRuntime<TTarget, TRequest>(
TTarget target,
Func<TTarget, TRequest> requestFactory)
where TTarget : class
where TRequest : IRequest<Unit>
{
if (!TryResolveDispatchContext(target, out var context) || _runtime is null)
{
return false;
}
_runtime.SendAsync(context, requestFactory(target)).AsTask().GetAwaiter().GetResult();
return true;
}
/// <summary>
/// 尝试通过统一 CQRS runtime 执行当前 legacy 请求,并返回装箱结果。
/// </summary>
/// <typeparam name="TTarget">legacy 目标对象类型。</typeparam>
/// <typeparam name="TResult">预期结果类型。</typeparam>
/// <typeparam name="TRequest">bridge request 类型。</typeparam>
/// <param name="target">即将执行的 legacy 目标对象。</param>
/// <param name="requestFactory">用于创建 bridge request 的工厂。</param>
/// <param name="result">若命中 bridge则返回执行结果否则返回默认值。</param>
/// <returns>若成功切入 CQRS runtime 则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
private bool TryExecuteThroughCqrsRuntime<TTarget, TResult, TRequest>(
TTarget target,
Func<TTarget, TRequest> requestFactory,
out TResult? result)
where TTarget : class
where TRequest : IRequest<object?>
{
if (!TryResolveDispatchContext(target, out var context) || _runtime is null)
{
result = default;
return false;
}
var boxedResult = _runtime.SendAsync(context, requestFactory(target)).AsTask().GetAwaiter().GetResult();
result = (TResult)boxedResult!;
return true;
}
/// <summary>
/// 通过统一 CQRS runtime 异步执行 legacy 带返回值命令,并把装箱结果还原为目标类型。
/// </summary>
/// <typeparam name="TResult">命令返回值类型。</typeparam>
/// <param name="context">当前架构上下文。</param>
/// <param name="command">要桥接的 legacy 命令。</param>
/// <returns>命令执行结果。</returns>
private async Task<TResult> BridgeAsyncCommandWithResultAsync<TResult>(
GFramework.Core.Abstractions.Architectures.IArchitectureContext context,
IAsyncCommand<TResult> command)
{
var boxedResult = await _runtime!.SendAsync(
context,
new LegacyAsyncCommandResultDispatchRequest(
command,
async () => await command.ExecuteAsync().ConfigureAwait(false)))
.ConfigureAwait(false);
return (TResult)boxedResult!;
}
/// <summary>
/// 解析当前 legacy 目标对象应该绑定到哪个架构上下文。
/// </summary>
/// <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)
{
context = null!;
if (_runtime is null || target is not IContextAware contextAware)
{
return false;
}
try
{
context = contextAware.GetContext();
return true;
}
catch (InvalidOperationException)
{
return false;
}
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using CoreCommand = GFramework.Core.Abstractions.Command;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 包装 legacy 异步无返回值命令,使其能够通过自有 CQRS runtime 调度。
/// </summary>
internal sealed class LegacyAsyncCommandDispatchRequest(CoreCommand.IAsyncCommand command)
: LegacyCqrsDispatchRequestBase(command), IRequest<Unit>
{
/// <summary>
/// 获取当前 bridge request 代理的异步命令实例。
/// </summary>
public CoreCommand.IAsyncCommand Command { get; } = command ?? throw new ArgumentNullException(nameof(command));
}

View File

@ -0,0 +1,24 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 处理 legacy 异步无返回值命令的 bridge handler。
/// </summary>
internal sealed class LegacyAsyncCommandDispatchRequestHandler
: LegacyCqrsDispatchHandlerBase, IRequestHandler<LegacyAsyncCommandDispatchRequest, Unit>
{
/// <inheritdoc />
public async ValueTask<Unit> Handle(
LegacyAsyncCommandDispatchRequest request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
PrepareTarget(request.Command);
await request.Command.ExecuteAsync().ConfigureAwait(false);
return Unit.Value;
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 包装 legacy 异步带返回值命令,使其能够通过自有 CQRS runtime 调度。
/// </summary>
internal sealed class LegacyAsyncCommandResultDispatchRequest(object target, Func<Task<object?>> executeAsync)
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
{
private readonly Func<Task<object?>> _executeAsync = executeAsync ?? throw new ArgumentNullException(nameof(executeAsync));
/// <summary>
/// 异步执行底层 legacy 命令并返回装箱后的结果。
/// </summary>
public Task<object?> ExecuteAsync() => _executeAsync();
}

View File

@ -0,0 +1,23 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 处理 legacy 异步带返回值命令的 bridge handler。
/// </summary>
internal sealed class LegacyAsyncCommandResultDispatchRequestHandler
: LegacyCqrsDispatchHandlerBase, IRequestHandler<LegacyAsyncCommandResultDispatchRequest, object?>
{
/// <inheritdoc />
public async ValueTask<object?> Handle(
LegacyAsyncCommandResultDispatchRequest request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
PrepareTarget(request.Target);
return await request.ExecuteAsync().ConfigureAwait(false);
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 包装 legacy 异步查询,使其能够通过自有 CQRS runtime 调度。
/// </summary>
internal sealed class LegacyAsyncQueryDispatchRequest(object target, Func<Task<object?>> executeAsync)
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
{
private readonly Func<Task<object?>> _executeAsync = executeAsync ?? throw new ArgumentNullException(nameof(executeAsync));
/// <summary>
/// 异步执行底层 legacy 查询并返回装箱后的结果。
/// </summary>
public Task<object?> ExecuteAsync() => _executeAsync();
}

View File

@ -0,0 +1,23 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 处理 legacy 异步查询的 bridge handler。
/// </summary>
internal sealed class LegacyAsyncQueryDispatchRequestHandler
: LegacyCqrsDispatchHandlerBase, IRequestHandler<LegacyAsyncQueryDispatchRequest, object?>
{
/// <inheritdoc />
public async ValueTask<object?> Handle(
LegacyAsyncQueryDispatchRequest request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
PrepareTarget(request.Target);
return await request.ExecuteAsync().ConfigureAwait(false);
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using CoreCommand = GFramework.Core.Abstractions.Command;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 包装 legacy 无返回值命令,使其能够通过自有 CQRS runtime 调度。
/// </summary>
internal sealed class LegacyCommandDispatchRequest(CoreCommand.ICommand command)
: LegacyCqrsDispatchRequestBase(command), IRequest<Unit>
{
/// <summary>
/// 获取当前 bridge request 代理的命令实例。
/// </summary>
public CoreCommand.ICommand Command { get; } = command ?? throw new ArgumentNullException(nameof(command));
}

View File

@ -0,0 +1,22 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 处理 legacy 无返回值命令的 bridge handler。
/// </summary>
internal sealed class LegacyCommandDispatchRequestHandler
: LegacyCqrsDispatchHandlerBase, IRequestHandler<LegacyCommandDispatchRequest, Unit>
{
/// <inheritdoc />
public ValueTask<Unit> Handle(LegacyCommandDispatchRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
PrepareTarget(request.Command);
request.Command.Execute();
return ValueTask.FromResult(Unit.Value);
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 包装 legacy 带返回值命令,使其能够通过自有 CQRS runtime 调度。
/// </summary>
internal sealed class LegacyCommandResultDispatchRequest(object target, Func<object?> execute)
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
{
private readonly Func<object?> _execute = execute ?? throw new ArgumentNullException(nameof(execute));
/// <summary>
/// 执行底层 legacy 命令并返回装箱后的结果。
/// </summary>
public object? Execute() => _execute();
}

View File

@ -0,0 +1,21 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 处理 legacy 带返回值命令的 bridge handler。
/// </summary>
internal sealed class LegacyCommandResultDispatchRequestHandler
: LegacyCqrsDispatchHandlerBase, IRequestHandler<LegacyCommandResultDispatchRequest, object?>
{
/// <inheritdoc />
public ValueTask<object?> Handle(LegacyCommandResultDispatchRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
PrepareTarget(request.Target);
return ValueTask.FromResult(request.Execute());
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Rule;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 为 legacy Core CQRS bridge handler 提供共享的上下文注入辅助逻辑。
/// </summary>
internal abstract class LegacyCqrsDispatchHandlerBase : ContextAwareBase
{
/// <summary>
/// 在执行 legacy 命令或查询前,把当前架构上下文显式注入给支持 <see cref="IContextAware" /> 的目标对象。
/// </summary>
protected void PrepareTarget(object target)
{
ArgumentNullException.ThrowIfNull(target);
if (target is IContextAware contextAware)
{
var context = Context ?? throw new InvalidOperationException(
"Legacy CQRS bridge handler requires an active architecture context before executing a context-aware target.");
contextAware.SetContext(context);
}
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
namespace GFramework.Core.Cqrs;
/// <summary>
/// 为 legacy Command / Query 到自有 CQRS runtime 的桥接请求提供共享的目标对象封装。
/// </summary>
internal abstract class LegacyCqrsDispatchRequestBase(object target)
{
/// <summary>
/// 获取当前 bridge request 代理的 legacy 目标对象。
/// </summary>
public object Target { get; } = target ?? throw new ArgumentNullException(nameof(target));
}

View File

@ -0,0 +1,20 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 包装 legacy 同步查询,使其能够通过自有 CQRS runtime 调度。
/// </summary>
internal sealed class LegacyQueryDispatchRequest(object target, Func<object?> execute)
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
{
private readonly Func<object?> _execute = execute ?? throw new ArgumentNullException(nameof(execute));
/// <summary>
/// 执行底层 legacy 查询并返回装箱后的结果。
/// </summary>
public object? Execute() => _execute();
}

View File

@ -0,0 +1,21 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Cqrs;
/// <summary>
/// 处理 legacy 同步查询的 bridge handler。
/// </summary>
internal sealed class LegacyQueryDispatchRequestHandler
: LegacyCqrsDispatchHandlerBase, IRequestHandler<LegacyQueryDispatchRequest, object?>
{
/// <inheritdoc />
public ValueTask<object?> Handle(LegacyQueryDispatchRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
PrepareTarget(request.Target);
return ValueTask.FromResult(request.Execute());
}
}

View File

@ -2,14 +2,24 @@
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Query;
/// <summary>
/// 异步查询总线实现,用于处理异步查询请求
/// </summary>
public sealed class AsyncQueryExecutor : IAsyncQueryExecutor
public sealed class AsyncQueryExecutor(ICqrsRuntime? runtime = null) : IAsyncQueryExecutor
{
private readonly ICqrsRuntime? _runtime = runtime;
/// <summary>
/// 获取当前执行器是否已接入统一 CQRS runtime。
/// </summary>
public bool UsesCqrsRuntime => _runtime is not null;
/// <summary>
/// 异步发送查询请求并返回结果
/// </summary>
@ -18,8 +28,59 @@ public sealed class AsyncQueryExecutor : IAsyncQueryExecutor
/// <returns>包含查询结果的异步任务</returns>
public Task<TResult> SendAsync<TResult>(IAsyncQuery<TResult> query)
{
// 验证查询参数不为空
ArgumentNullException.ThrowIfNull(query);
if (TryResolveDispatchContext(query, out var context) && _runtime is not null)
{
return BridgeAsyncQueryAsync<TResult>(context, query);
}
return query.DoAsync();
}
}
/// <summary>
/// 通过统一 CQRS runtime 异步执行 legacy 查询,并把装箱结果还原为目标类型。
/// </summary>
/// <typeparam name="TResult">查询结果类型。</typeparam>
/// <param name="context">当前架构上下文。</param>
/// <param name="query">要桥接的 legacy 查询。</param>
/// <returns>查询执行结果。</returns>
private async Task<TResult> BridgeAsyncQueryAsync<TResult>(
GFramework.Core.Abstractions.Architectures.IArchitectureContext context,
IAsyncQuery<TResult> query)
{
var boxedResult = await _runtime!.SendAsync(
context,
new LegacyAsyncQueryDispatchRequest(
query,
async () => await query.DoAsync().ConfigureAwait(false)))
.ConfigureAwait(false);
return (TResult)boxedResult!;
}
/// <summary>
/// 解析当前 legacy 查询应该绑定到哪个架构上下文。
/// </summary>
/// <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)
{
context = null!;
if (_runtime is null || query is not IContextAware contextAware)
{
return false;
}
try
{
context = contextAware.GetContext();
return true;
}
catch (InvalidOperationException)
{
return false;
}
}
}

View File

@ -2,6 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Query;
@ -10,21 +13,67 @@ namespace GFramework.Core.Query;
/// QueryExecutor 类负责执行查询操作,实现 IQueryExecutor 接口。
/// 该类是密封的,防止被继承。
/// </summary>
public sealed class QueryExecutor : IQueryExecutor
public sealed class QueryExecutor(ICqrsRuntime? runtime = null) : IQueryExecutor
{
private readonly ICqrsRuntime? _runtime = runtime;
/// <summary>
/// 获取当前执行器是否已接入统一 CQRS runtime。
/// </summary>
public bool UsesCqrsRuntime => _runtime is not null;
/// <summary>
/// 执行指定的查询并返回结果。
/// 该方法通过调用查询对象的 Do 方法来获取结果。
/// 当查询对象携带可用的架构上下文且执行器已接入统一 runtime 时,
/// 该方法会先把 legacy 查询包装成内部 request 并交给 <see cref="ICqrsRuntime" />
/// 以复用统一的 dispatch / pipeline 入口;否则回退到 legacy 直接执行。
/// </summary>
/// <typeparam name="TResult">查询结果的类型。</typeparam>
/// <param name="query">要执行的查询对象,必须实现 IQuery&lt;TResult&gt; 接口。</param>
/// <returns>查询执行的结果,类型为 TResult。</returns>
public TResult Send<TResult>(IQuery<TResult> query)
{
// 验证查询参数不为 null如果为 null 则抛出 ArgumentNullException 异常
ArgumentNullException.ThrowIfNull(query);
// 调用查询对象的 Do 方法执行查询并返回结果
if (TryResolveDispatchContext(query, out var context) && _runtime is not null)
{
var boxedResult = _runtime.SendAsync(
context,
new LegacyQueryDispatchRequest(
query,
() => query.Do()))
.AsTask()
.GetAwaiter()
.GetResult();
return (TResult)boxedResult!;
}
return query.Do();
}
/// <summary>
/// 解析当前 legacy 查询应该绑定到哪个架构上下文。
/// </summary>
/// <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)
{
context = null!;
if (_runtime is null || query is not IContextAware contextAware)
{
return false;
}
try
{
context = contextAware.GetContext();
return true;
}
catch (InvalidOperationException)
{
return false;
}
}
}

View File

@ -15,6 +15,9 @@
- 资源、对象池、日志、协程、并发、环境、配置与本地化
- 服务模块管理、时间提供器与默认的 IoC 容器适配
标准架构启动路径下,旧 `Command` / `Query` 兼容入口现在会继续保持原有使用方式,
但底层会通过 `GFramework.Cqrs` 的统一 runtime、pipeline 与上下文注入链路执行。
它不负责:
- 游戏内容配置、Scene / UI / Storage 等游戏层能力

View File

@ -4,6 +4,7 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Query;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Services.Modules;
@ -35,7 +36,8 @@ public sealed class AsyncQueryExecutorModule : IServiceModule
/// <param name="container">依赖注入容器实例。</param>
public void Register(IIocContainer container)
{
container.RegisterPlurality(new AsyncQueryExecutor());
ArgumentNullException.ThrowIfNull(container);
container.RegisterPlurality(new AsyncQueryExecutor(container.Get<ICqrsRuntime>()));
}
/// <summary>
@ -55,4 +57,4 @@ public sealed class AsyncQueryExecutorModule : IServiceModule
{
return ValueTask.CompletedTask;
}
}
}

View File

@ -4,6 +4,7 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Command;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Services.Modules;
@ -35,7 +36,8 @@ public sealed class CommandExecutorModule : IServiceModule
/// <param name="container">依赖注入容器实例。</param>
public void Register(IIocContainer container)
{
container.RegisterPlurality(new CommandExecutor());
ArgumentNullException.ThrowIfNull(container);
container.RegisterPlurality(new CommandExecutor(container.Get<ICqrsRuntime>()));
}
/// <summary>
@ -55,4 +57,4 @@ public sealed class CommandExecutorModule : IServiceModule
{
return ValueTask.CompletedTask;
}
}
}

View File

@ -4,6 +4,7 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Query;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Services.Modules;
@ -35,7 +36,8 @@ public sealed class QueryExecutorModule : IServiceModule
/// <param name="container">依赖注入容器实例。</param>
public void Register(IIocContainer container)
{
container.RegisterPlurality(new QueryExecutor());
ArgumentNullException.ThrowIfNull(container);
container.RegisterPlurality(new QueryExecutor(container.Get<ICqrsRuntime>()));
}
/// <summary>
@ -55,4 +57,4 @@ public sealed class QueryExecutorModule : IServiceModule
{
return ValueTask.CompletedTask;
}
}
}

View File

@ -7,7 +7,7 @@ CQRS 迁移与收敛。
## 当前恢复点
- 恢复点编号:`CQRS-REWRITE-RP-092`
- 恢复点编号:`CQRS-REWRITE-RP-093`
- 当前阶段:`Phase 8`
- 当前 PR 锚点:`待创建(当前分支 feat/cqrs-optimization 尚未为 RP-092 建立新 PR`
- 当前结论:
@ -27,15 +27,21 @@ CQRS 迁移与收敛。
- 当前 `RP-089` 已补齐 stream invoker reflection / generated-provider 对照,使 generated descriptor 预热收益从 request 扩展到 stream 路径
- 当前 `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` 对照留到具备真实显式作用域边界的宿主模型后再评估
- `ai-plan` active 入口现以 `RP-092` 为最新恢复锚点;`PR #331``PR #326``PR #323``PR #307` 与其他更早阶段细节均以下方归档或说明为准
- `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` 与其他更早阶段细节均以下方归档或说明为准
## 当前活跃事实
- 当前分支为 `feat/cqrs-optimization`
- 本轮 `$gframework-batch-boot 50``origin/main` (`2c58d8b6`, 2026-05-07 13:24:46 +0800) 为基线;本地 `main` (`c2d22285`) 已落后,不作为 branch diff 基线
- 当前分支相对 `origin/main` 的累计 branch diff 为 `4 files / 303 lines`,仍明显低于 `$gframework-batch-boot 50` 的文件阈值
- `GFramework.Cqrs.Benchmarks` 作为 benchmark 基础设施项目,必须持续排除在 NuGet / GitHub Packages 发布集合之外
- `GFramework.Cqrs.Benchmarks` 现已覆盖 request steady-state、pipeline 数量矩阵、startup、request/stream generated invoker以及 request handler `Singleton / Transient` 生命周期矩阵
- `GFramework.Core` 当前已通过内部 bridge request / handler 把 legacy `ICommand``IAsyncCommand``IQuery``IAsyncQuery` 接到统一 `ICqrsRuntime`
- 标准 `Architecture` 初始化路径会自动扫描 `GFramework.Core` 程序集中的 legacy bridge handler因此旧 `SendCommand(...)` / `SendQuery(...)` 无需改变用法即可进入统一 pipeline
- `CommandExecutor``QueryExecutor``AsyncQueryExecutor` 仍保留“无 runtime 时直接执行”的回退路径,用于不依赖容器的隔离单元测试
- 相对 `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`
- latest-head review 现仍有少量 open thread但本地复核后仍成立的问题已收敛到 benchmark 对照公平性、workflow 输入安全性与 active 文档压缩
@ -54,6 +60,7 @@ CQRS 迁移与收敛。
- 当前 benchmark 宿主仍刻意保持“单根容器最小宿主”模型;若要公平比较 `Scoped` handler 生命周期,需要先引入显式 scope 创建与 scope 内首次解析的对照基线
- 仓库内部仍保留旧 `Command` / `Query` API、`LegacyICqrsRuntime` alias 与部分历史命名语义,后续若不继续分批收口,容易混淆“对外替代已完成”与“内部收口未完成”
- 若继续扩大 generated invoker 覆盖面,需要持续区分“可静态表达的合同”与 `PreciseReflectedRegistrationSpec` 等仍需保守回退的场景
- legacy bridge 当前只为已有 `Command` / `Query` 兼容入口接到统一 request pipeline若后续要继续对齐 `Mediator`,仍需要单独设计 stream pipeline、telemetry 与 facade 公开面,而不是把这次 bridge 当成“全部收口完成”
## 最近权威验证
@ -91,12 +98,22 @@ CQRS 迁移与收敛。
- 备注:当前 WSL worktree 需要显式绑定 `GIT_DIR` / `GIT_WORK_TREE`
- `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~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests"`
- 结果:通过,`45/45` passed
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 结果:通过,`1644/1644` passed
- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
- 结果:通过
- `git diff --check`
- 结果:通过
## 下一推荐步骤
1. 若继续沿用 `$gframework-batch-boot 50`,优先补 `stream handler` 生命周期矩阵,与当前 request 生命周期切片保持对称
2. 若要扩到 `Scoped` 生命周期,对 benchmark 宿主先补显式 scope 基线,而不是直接在根容器上解析 scoped handler
3. 若后续继续跑 BenchmarkDotNet本地 agent 环境优先直接使用沙箱外命令,避免再次命中自动生成脚本在沙箱内的 bootstrap 异常
1. 若继续沿用 `$gframework-batch-boot 50` 且优先处理 `Mediator` 能力吸收,下一批建议从 `stream pipeline``notification publisher` 策略中选择一个独立切片推进
2. 若继续收敛 legacy Core CQRS可评估是否补一个 `IMediator` 风格 facade而不是继续扩大 `ArchitectureContext` 兼容入口的职责
3. 若回到 benchmark 方向,优先补 `stream handler` 生命周期矩阵;若要扩到 `Scoped` 生命周期,先为 benchmark 宿主设计真实显式 scope 基线
## 活跃文档

View File

@ -2,6 +2,53 @@
## 2026-05-07
### 阶段legacy Core CQRS -> GFramework.Cqrs bridgeCQRS-REWRITE-RP-093
- 延续 `$gframework-batch-boot 50`,本轮明确不只盯 benchmark而是同时处理两个目标
- 复核 `ai-libs/Mediator` 还有哪些能力尚未被 `GFramework.Cqrs` 吸收
- 验证 `GFramework.Core` 的简单 `Command` / `Query` 兼容入口能否在不改外部用法的前提下,底层统一改走 `GFramework.Cqrs`
- 主线程先完成 `GFramework.Core` bridge 实现收尾与测试修正:
- `ArchitectureContext` 的 legacy `SendCommand(...)` / `SendQuery(...)` / `SendQueryAsync(...)` 现在会创建内部 bridge request并直接通过统一 `ICqrsRuntime` 分发
- `CommandExecutor``QueryExecutor``AsyncQueryExecutor` 在解析到 runtime 且目标对象可提供架构上下文时,也会复用同一条 bridge/runtime 路径
- 为避免破坏不依赖容器的旧测试,执行器仍保留“未接入 runtime 时直接执行”的回退语义
- 新增 `GFramework.Core/Cqrs/Legacy*DispatchRequest*.cs` 与对应 handler把 legacy 命令/查询包装成内部 request
- bridge handler 在执行前会显式把当前 `IArchitectureContext` 注入给 `IContextAware` 目标
- 这让旧调用链在不改 public API 的情况下,也能复用统一 pipeline 与 handler dispatch 语义
- 生产接线结论已经本地复核:
- `CqrsRuntimeModule` 只注册 runtime / registrar / registration service本身不直接手工注册 bridge handler
- 默认生产路径依赖 `ArchitectureBootstrapper.ConfigureServices(...)` 自动调用 `RegisterCqrsHandlersFromAssemblies([architectureType.Assembly, typeof(ArchitectureContext).Assembly])`
- 因此 `GFramework.Core` 程序集中的 internal bridge handler 会在标准架构初始化阶段自动被扫描和注册,不需要业务侧手工补注册
- 为防止以后有人改坏默认扫描范围,本轮额外补了一条更接近真实启动路径的回归:
- `ArchitectureModulesBehaviorTests.InitializeAsync_Should_AutoRegister_LegacyBridgeHandlers_For_Default_Core_Assemblies`
- 该用例通过 `Architecture.Configurator` 注册 open-generic pipeline behavior然后直接走 `Architecture.InitializeAsync()`,验证旧 `SendCommand` / `SendQuery` 兼容入口能命中统一 pipeline
- 只读 subagent 同步完成 `Mediator` 差距复核,接受的结论是六类未完全吸收能力:
- `IMediator` / `ISender` / `IPublisher` 风格 facade
- telemetry / tracing / metrics
- stream pipeline
- notification publisher 策略
- 生成器配置与诊断公开面
- 生命周期 / 缓存公开配置面
- 文档与恢复入口同步更新:
- `docs/zh-CN/core/context.md``command.md``query.md``cqrs.md`
- `GFramework.Core/README.md`
- active tracking / trace 升级到 `RP-093`
### 验证RP-093
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests|FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests"`
- 结果:通过,`45/45` passed
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 结果:通过,`1644/1644` passed
- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
- 结果:通过
- `git diff --check`
- 结果:通过
### 当前下一步RP-093
1. 若继续沿用 `$gframework-batch-boot 50`,优先从 `stream pipeline``notification publisher` 策略中切一块对齐 `Mediator`
2. 若要继续收敛 public seam下一批优先设计 facade而不是继续扩大 `ArchitectureContext` 的兼容职责
### 阶段request handler 生命周期矩阵 benchmarkCQRS-REWRITE-RP-092
- 使用 `$gframework-batch-boot 50` 启动本轮批次,并按技能要求先复核 `origin/main` 基线与 branch diff

View File

@ -105,6 +105,9 @@ var reward = this.SendCommand(new GetGoldRewardCommand(new GetGoldRewardInput(3)
- `SendCommandAsync(IAsyncCommand)`
- `SendCommandAsync<TResult>(IAsyncCommand<TResult>)`
在标准架构启动路径中,这些兼容入口底层已经统一改走 `ICqrsRuntime`
这意味着历史命令调用链在不改调用方式的前提下,也会复用同一套 pipeline 与上下文注入语义。
`IContextAware` 对象内,通常直接通过扩展使用:
```csharp

View File

@ -111,6 +111,10 @@ this.SendEvent(new PlayerDiedEvent());
这部分入口主要用于兼容存量代码。新功能优先看 [cqrs](./cqrs.md)。
在标准 `Architecture` 初始化路径里,这些旧入口现在会复用同一个 `ICqrsRuntime`
`SendCommand(...)` / `SendQuery(...)` 仍保持原有调用方式,但会经过统一的 request pipeline 与上下文注入链路。
只有在你直接 `new CommandExecutor()``new QueryExecutor()` 做隔离测试,且没有提供 runtime 时,才会回退到 legacy 直接执行。
## 新 CQRS 入口
`IArchitectureContext` 也是当前 CQRS runtime 的主入口。最重要的方法是:

View File

@ -224,6 +224,7 @@ RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
这里有两个边界需要分开理解:
- 旧 `Command` / `Query` 入口仍可用于维护历史调用链
- 标准 `Architecture` 启动路径下,旧入口现在会通过内部 bridge request 复用同一个 `ICqrsRuntime`
- 旧命名空间下的 `ICqrsRuntime` 只是为了兼容既有引用而保留的 alias面向新代码时应直接使用
`GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime`
@ -232,6 +233,15 @@ RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
- 在维护历史代码:允许继续使用旧 Command / Query
- 在写新功能或新模块:优先使用 CQRS
相对 `ai-libs/Mediator`,当前 `GFramework.Cqrs` 仍有几类能力差距尚未完全吸收:
- `IMediator` / `ISender` / `IPublisher` 风格的一等 facade 公开入口
- telemetry / tracing / metrics 的运行时与生成器配置面
- 独立的 stream pipeline 行为体系
- 更丰富的 notification publisher 策略
- 更强的生成器配置与诊断公开面
- 生命周期 / 缓存策略的显式公开配置面
## 源码阅读入口
如果你需要直接回到源码确认 CQRS 契约,建议按下面这几组入口阅读:

View File

@ -83,6 +83,9 @@ var count = this.SendQuery(
- `SendQuery<TResult>(IQuery<TResult>)`
- `SendQueryAsync<TResult>(IAsyncQuery<TResult>)`
在标准架构启动路径中,这些兼容入口底层同样会转到统一 `ICqrsRuntime`
因此历史查询对象仍保持原始 `SendQuery(...)` / `SendQueryAsync(...)` 用法,但会共享新版 request pipeline 与上下文注入链路。
`IContextAware` 对象内部,通常直接使用 `GFramework.Core.Extensions` 里的扩展:
```csharp