diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs index 47ad0d2c..4c33f725 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs @@ -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)); } + /// + /// 测试 legacy 查询通过 发送时会进入统一 CQRS pipeline, + /// 并把当前架构上下文注入到查询对象。 + /// + [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)); + } + /// /// 测试SendQuery方法在查询为null时应抛出ArgumentNullException /// @@ -146,6 +167,24 @@ public class ArchitectureContextTests Assert.That(testCommand.Executed, Is.True); } + /// + /// 测试 legacy 命令通过 发送时会进入统一 CQRS pipeline, + /// 并把当前架构上下文注入到命令对象。 + /// + [Test] + public void SendCommand_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context() + { + LegacyBridgePipelineTracker.Reset(); + var testCommand = new LegacyArchitectureBridgeCommand(); + var bridgeContext = CreateFrozenBridgeContext(); + + bridgeContext.SendCommand(testCommand); + + Assert.That(testCommand.Executed, Is.True); + Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext)); + Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1)); + } + /// /// 测试SendCommand方法在命令为null时应抛出ArgumentNullException /// @@ -168,6 +207,87 @@ public class ArchitectureContextTests Assert.That(result, Is.EqualTo(123)); } + /// + /// 测试 legacy 带返回值命令通过 发送时会进入统一 CQRS pipeline, + /// 并保持原始返回值语义。 + /// + [Test] + public void SendCommand_WithResult_Should_Bridge_Through_CqrsRuntime() + { + LegacyBridgePipelineTracker.Reset(); + var testCommand = new LegacyArchitectureBridgeCommandWithResult(); + var bridgeContext = CreateFrozenBridgeContext(); + + 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)); + } + + /// + /// 测试 legacy 异步查询通过 发送时也会进入统一 CQRS pipeline。 + /// + [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)); + } + + /// + /// 为需要验证统一 CQRS pipeline 的用例创建一个已冻结的最小 bridge 上下文。 + /// + /// 能够执行 legacy bridge request 且会 materialize open-generic pipeline behavior 的上下文。 + 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); + } + + /// + /// 通过反射把 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); + } + } + /// /// 测试SendCommand方法(带返回值)在命令为null时应抛出ArgumentNullException /// diff --git a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs index b9e7b180..ff9880f5 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs @@ -6,6 +6,8 @@ using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Utility; using GFramework.Core.Architectures; using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; +using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Tests.Architectures; @@ -80,6 +82,36 @@ public class ArchitectureModulesBehaviorTests await architecture.DestroyAsync(); } + /// + /// 验证默认架构初始化路径会自动扫描 Core 程序集里的 legacy bridge handler, + /// 使旧 SendCommand / SendQuery 入口也能进入统一 CQRS pipeline。 + /// + [Test] + public async Task InitializeAsync_Should_AutoRegister_LegacyBridgeHandlers_For_Default_Core_Assemblies() + { + LegacyBridgePipelineTracker.Reset(); + var architecture = new LegacyBridgeArchitecture(); + + await architecture.InitializeAsync(); + + 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(); + } + /// /// 用于测试模块行为的最小架构实现。 /// @@ -94,6 +126,27 @@ public class ArchitectureModulesBehaviorTests } } + /// + /// 通过公开初始化入口注册测试 pipeline behavior 的最小架构, + /// 用于验证默认 Core 程序集扫描是否会自动接入 legacy bridge handler。 + /// + private sealed class LegacyBridgeArchitecture : Architecture + { + /// + /// 在容器钩子阶段注册 open-generic pipeline behavior, + /// 以便 bridge request 走真实的架构初始化与 handler 自动扫描链路。 + /// + public override Action? Configurator => services => + services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LegacyBridgeTrackingPipelineBehavior<,>)); + + /// + /// 保持空初始化,让测试只聚焦默认 CQRS 接线与 legacy bridge handler 自动发现。 + /// + protected override void OnInitialize() + { + } + } + /// /// 记录模块安装调用情况的测试模块。 /// diff --git a/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeAsyncQuery.cs b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeAsyncQuery.cs new file mode 100644 index 00000000..864864a9 --- /dev/null +++ b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeAsyncQuery.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Query; +using GFramework.Core.Rule; + +namespace GFramework.Core.Tests.Architectures; + +/// +/// 用于验证 legacy 异步查询桥接时也会显式注入当前架构上下文。 +/// +public sealed class LegacyArchitectureBridgeAsyncQuery : ContextAwareBase, IAsyncQuery +{ + /// + /// 获取执行期间观察到的上下文实例。 + /// + public IArchitectureContext? ObservedContext { get; private set; } + + /// + /// 执行异步查询并返回测试结果。 + /// + public Task DoAsync() + { + ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext(); + return Task.FromResult(64); + } +} diff --git a/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommand.cs b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommand.cs new file mode 100644 index 00000000..f58d646b --- /dev/null +++ b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommand.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Command; +using GFramework.Core.Rule; + +namespace GFramework.Core.Tests.Architectures; + +/// +/// 用于验证 legacy 命令桥接时会把当前 注入到命令对象。 +/// +public sealed class LegacyArchitectureBridgeCommand : ContextAwareBase, ICommand +{ + /// + /// 获取执行期间观察到的上下文实例。 + /// + public IArchitectureContext? ObservedContext { get; private set; } + + /// + /// 获取当前命令是否已经执行。 + /// + public bool Executed { get; private set; } + + /// + /// 执行命令并记录 bridge handler 注入的上下文。 + /// + public void Execute() + { + Executed = true; + ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext(); + } +} diff --git a/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommandWithResult.cs b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommandWithResult.cs new file mode 100644 index 00000000..d97d553a --- /dev/null +++ b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommandWithResult.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Command; +using GFramework.Core.Rule; + +namespace GFramework.Core.Tests.Architectures; + +/// +/// 用于验证 legacy 带返回值命令桥接时会沿用统一 runtime。 +/// +public sealed class LegacyArchitectureBridgeCommandWithResult : ContextAwareBase, ICommand +{ + /// + /// 获取执行期间观察到的上下文实例。 + /// + public IArchitectureContext? ObservedContext { get; private set; } + + /// + /// 执行命令并返回测试结果。 + /// + public int Execute() + { + ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext(); + return 42; + } +} diff --git a/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeQuery.cs b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeQuery.cs new file mode 100644 index 00000000..1f7617a6 --- /dev/null +++ b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeQuery.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Query; +using GFramework.Core.Rule; + +namespace GFramework.Core.Tests.Architectures; + +/// +/// 用于验证 legacy 查询桥接时会把当前 注入到查询对象。 +/// +public sealed class LegacyArchitectureBridgeQuery : ContextAwareBase, IQuery +{ + /// + /// 获取执行期间观察到的上下文实例。 + /// + public IArchitectureContext? ObservedContext { get; private set; } + + /// + /// 执行查询并返回测试结果。 + /// + public int Do() + { + ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext(); + return 24; + } +} diff --git a/GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs b/GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs new file mode 100644 index 00000000..27ce3d4e --- /dev/null +++ b/GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs @@ -0,0 +1,41 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using System.Threading; + +namespace GFramework.Core.Tests.Architectures; + +/// +/// 为 legacy bridge pipeline 回归测试保存跨泛型闭包共享的计数状态。 +/// +public static class LegacyBridgePipelineTracker +{ + private static int _invocationCount; + + /// + /// 获取当前进程内被识别为 legacy bridge request 的 pipeline 命中次数。 + /// + public static int InvocationCount => Volatile.Read(ref _invocationCount); + + /// + /// 重置计数器。 + /// + public static void Reset() + { + Volatile.Write(ref _invocationCount, 0); + } + + /// + /// 若当前请求类型属于 Core legacy bridge request,则记录一次命中。 + /// + public static void Record(Type requestType) + { + ArgumentNullException.ThrowIfNull(requestType); + + if (string.Equals(requestType.Namespace, "GFramework.Core.Cqrs", StringComparison.Ordinal) && + requestType.Name.Contains("Legacy", StringComparison.Ordinal)) + { + Interlocked.Increment(ref _invocationCount); + } + } +} diff --git a/GFramework.Core.Tests/Architectures/LegacyBridgeTrackingPipelineBehavior.cs b/GFramework.Core.Tests/Architectures/LegacyBridgeTrackingPipelineBehavior.cs new file mode 100644 index 00000000..4e719e0e --- /dev/null +++ b/GFramework.Core.Tests/Architectures/LegacyBridgeTrackingPipelineBehavior.cs @@ -0,0 +1,24 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using System.Threading; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Core.Tests.Architectures; + +/// +/// 记录 legacy Core CQRS bridge request 是否经过统一 CQRS pipeline 的测试行为。 +/// +public sealed class LegacyBridgeTrackingPipelineBehavior : IPipelineBehavior + where TRequest : IRequest +{ + /// + public async ValueTask Handle( + TRequest message, + MessageHandlerDelegate next, + CancellationToken cancellationToken) + { + LegacyBridgePipelineTracker.Record(typeof(TRequest)); + return await next(message, cancellationToken).ConfigureAwait(false); + } +} diff --git a/GFramework.Core/Architectures/ArchitectureContext.cs b/GFramework.Core/Architectures/ArchitectureContext.cs index b63a0427..e143c957 100644 --- a/GFramework.Core/Architectures/ArchitectureContext.cs +++ b/GFramework.Core/Architectures/ArchitectureContext.cs @@ -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 /// 查询结果 public TResult SendQuery(IQuery query) { - if (query == null) throw new ArgumentNullException(nameof(query)); - var queryBus = GetOrCache(); - 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!; } /// @@ -192,7 +195,7 @@ public class ArchitectureContext : IArchitectureContext /// 查询响应类型 /// 要发送的查询对象 /// 查询结果 - public TResponse SendQuery(Cqrs.Abstractions.Cqrs.Query.IQuery query) + public TResponse SendQuery(global::GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query) { return SendQueryAsync(query).AsTask().GetAwaiter().GetResult(); } @@ -205,10 +208,13 @@ public class ArchitectureContext : IArchitectureContext /// 查询结果 public async Task SendQueryAsync(IAsyncQuery query) { - if (query == null) throw new ArgumentNullException(nameof(query)); - var asyncQueryBus = GetOrCache(); - 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!; } /// @@ -218,7 +224,7 @@ public class ArchitectureContext : IArchitectureContext /// 要发送的查询对象 /// 取消令牌,用于取消操作 /// 包含查询结果的ValueTask - public async ValueTask SendQueryAsync(Cqrs.Abstractions.Cqrs.Query.IQuery query, + public async ValueTask SendQueryAsync(global::GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(query); @@ -355,7 +361,7 @@ public class ArchitectureContext : IArchitectureContext /// 取消令牌,用于取消操作 /// 包含命令执行结果的ValueTask public async ValueTask SendCommandAsync( - Cqrs.Abstractions.Cqrs.Command.ICommand command, + global::GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand 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(); - if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered"); - await commandBus.SendAsync(command).ConfigureAwait(false); + await SendRequestAsync(new LegacyAsyncCommandDispatchRequest(command)).ConfigureAwait(false); } /// @@ -383,9 +387,12 @@ public class ArchitectureContext : IArchitectureContext public async Task SendCommandAsync(IAsyncCommand command) { ArgumentNullException.ThrowIfNull(command); - var commandBus = GetOrCache(); - 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!; } /// @@ -394,7 +401,7 @@ public class ArchitectureContext : IArchitectureContext /// 命令响应类型 /// 要发送的命令对象 /// 命令执行结果 - public TResponse SendCommand(Cqrs.Abstractions.Cqrs.Command.ICommand command) + public TResponse SendCommand(global::GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand 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(); - commandBus.Send(command); + SendRequest(new LegacyCommandDispatchRequest(command)); } /// @@ -419,9 +425,11 @@ public class ArchitectureContext : IArchitectureContext public TResult SendCommand(ICommand command) { ArgumentNullException.ThrowIfNull(command); - var commandBus = GetOrCache(); - 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 diff --git a/GFramework.Core/Command/CommandExecutor.cs b/GFramework.Core/Command/CommandExecutor.cs index 2d2d7e32..355379e3 100644 --- a/GFramework.Core/Command/CommandExecutor.cs +++ b/GFramework.Core/Command/CommandExecutor.cs @@ -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 接口,提供命令执行的核心功能。 /// -public sealed class CommandExecutor : ICommandExecutor +public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExecutor { + private readonly ICqrsRuntime? _runtime = runtime; + + /// + /// 获取当前执行器是否已接入统一 CQRS runtime。 + /// + /// + /// 当调用方只是直接 new 一个执行器做纯单元测试时,这里允许为空,并回退到 legacy 直接执行路径; + /// 当执行器由架构容器提供给 使用时,应始终传入 runtime, + /// 以便旧入口也复用统一 pipeline 与 handler 调度链路。 + /// + public bool UsesCqrsRuntime => _runtime is not null; + /// /// 发送并执行无返回值的命令 /// @@ -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(); } -} \ No newline at end of file + + /// + /// 尝试通过统一 CQRS runtime 执行当前 legacy 请求。 + /// + /// legacy 目标对象类型。 + /// bridge request 类型。 + /// 即将执行的 legacy 目标对象。 + /// 用于创建 bridge request 的工厂。 + /// 若成功切入 CQRS runtime 则返回 ;否则返回 + private bool TryExecuteThroughCqrsRuntime( + TTarget target, + Func requestFactory) + where TTarget : class + where TRequest : IRequest + { + if (!TryResolveDispatchContext(target, out var context) || _runtime is null) + { + return false; + } + + _runtime.SendAsync(context, requestFactory(target)).AsTask().GetAwaiter().GetResult(); + return true; + } + + /// + /// 尝试通过统一 CQRS runtime 执行当前 legacy 请求,并返回装箱结果。 + /// + /// legacy 目标对象类型。 + /// 预期结果类型。 + /// bridge request 类型。 + /// 即将执行的 legacy 目标对象。 + /// 用于创建 bridge request 的工厂。 + /// 若命中 bridge,则返回执行结果;否则返回默认值。 + /// 若成功切入 CQRS runtime 则返回 ;否则返回 + private bool TryExecuteThroughCqrsRuntime( + TTarget target, + Func requestFactory, + out TResult? result) + where TTarget : class + where TRequest : IRequest + { + 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; + } + + /// + /// 通过统一 CQRS runtime 异步执行 legacy 带返回值命令,并把装箱结果还原为目标类型。 + /// + /// 命令返回值类型。 + /// 当前架构上下文。 + /// 要桥接的 legacy 命令。 + /// 命令执行结果。 + private async Task BridgeAsyncCommandWithResultAsync( + GFramework.Core.Abstractions.Architectures.IArchitectureContext context, + IAsyncCommand command) + { + var boxedResult = await _runtime!.SendAsync( + context, + new LegacyAsyncCommandResultDispatchRequest( + command, + async () => await command.ExecuteAsync().ConfigureAwait(false))) + .ConfigureAwait(false); + return (TResult)boxedResult!; + } + + /// + /// 解析当前 legacy 目标对象应该绑定到哪个架构上下文。 + /// + /// 即将执行的 legacy 目标对象。 + /// 命中时返回可用于 CQRS runtime 的架构上下文。 + /// 如果既接入了 runtime 且目标对象提供了上下文,则返回 + private bool TryResolveDispatchContext(object target, out GFramework.Core.Abstractions.Architectures.IArchitectureContext context) + { + context = null!; + + if (_runtime is null || target is not IContextAware contextAware) + { + return false; + } + + try + { + context = contextAware.GetContext(); + return true; + } + catch (InvalidOperationException) + { + return false; + } + } +} diff --git a/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequest.cs b/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequest.cs new file mode 100644 index 00000000..b69f369b --- /dev/null +++ b/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequest.cs @@ -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; + +/// +/// 包装 legacy 异步无返回值命令,使其能够通过自有 CQRS runtime 调度。 +/// +internal sealed class LegacyAsyncCommandDispatchRequest(CoreCommand.IAsyncCommand command) + : LegacyCqrsDispatchRequestBase(command), IRequest +{ + /// + /// 获取当前 bridge request 代理的异步命令实例。 + /// + public CoreCommand.IAsyncCommand Command { get; } = command ?? throw new ArgumentNullException(nameof(command)); +} diff --git a/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequestHandler.cs b/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequestHandler.cs new file mode 100644 index 00000000..e34e9de3 --- /dev/null +++ b/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequestHandler.cs @@ -0,0 +1,24 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Core.Cqrs; + +/// +/// 处理 legacy 异步无返回值命令的 bridge handler。 +/// +internal sealed class LegacyAsyncCommandDispatchRequestHandler + : LegacyCqrsDispatchHandlerBase, IRequestHandler +{ + /// + public async ValueTask Handle( + LegacyAsyncCommandDispatchRequest request, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + PrepareTarget(request.Command); + await request.Command.ExecuteAsync().ConfigureAwait(false); + return Unit.Value; + } +} diff --git a/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequest.cs b/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequest.cs new file mode 100644 index 00000000..18f4cd5d --- /dev/null +++ b/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequest.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Core.Cqrs; + +/// +/// 包装 legacy 异步带返回值命令,使其能够通过自有 CQRS runtime 调度。 +/// +internal sealed class LegacyAsyncCommandResultDispatchRequest(object target, Func> executeAsync) + : LegacyCqrsDispatchRequestBase(target), IRequest +{ + private readonly Func> _executeAsync = executeAsync ?? throw new ArgumentNullException(nameof(executeAsync)); + + /// + /// 异步执行底层 legacy 命令并返回装箱后的结果。 + /// + public Task ExecuteAsync() => _executeAsync(); +} diff --git a/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequestHandler.cs b/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequestHandler.cs new file mode 100644 index 00000000..d58a661f --- /dev/null +++ b/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequestHandler.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Core.Cqrs; + +/// +/// 处理 legacy 异步带返回值命令的 bridge handler。 +/// +internal sealed class LegacyAsyncCommandResultDispatchRequestHandler + : LegacyCqrsDispatchHandlerBase, IRequestHandler +{ + /// + public async ValueTask Handle( + LegacyAsyncCommandResultDispatchRequest request, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + PrepareTarget(request.Target); + return await request.ExecuteAsync().ConfigureAwait(false); + } +} diff --git a/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequest.cs b/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequest.cs new file mode 100644 index 00000000..113a3a3b --- /dev/null +++ b/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequest.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Core.Cqrs; + +/// +/// 包装 legacy 异步查询,使其能够通过自有 CQRS runtime 调度。 +/// +internal sealed class LegacyAsyncQueryDispatchRequest(object target, Func> executeAsync) + : LegacyCqrsDispatchRequestBase(target), IRequest +{ + private readonly Func> _executeAsync = executeAsync ?? throw new ArgumentNullException(nameof(executeAsync)); + + /// + /// 异步执行底层 legacy 查询并返回装箱后的结果。 + /// + public Task ExecuteAsync() => _executeAsync(); +} diff --git a/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequestHandler.cs b/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequestHandler.cs new file mode 100644 index 00000000..b9bc848e --- /dev/null +++ b/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequestHandler.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Core.Cqrs; + +/// +/// 处理 legacy 异步查询的 bridge handler。 +/// +internal sealed class LegacyAsyncQueryDispatchRequestHandler + : LegacyCqrsDispatchHandlerBase, IRequestHandler +{ + /// + public async ValueTask Handle( + LegacyAsyncQueryDispatchRequest request, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + PrepareTarget(request.Target); + return await request.ExecuteAsync().ConfigureAwait(false); + } +} diff --git a/GFramework.Core/Cqrs/LegacyCommandDispatchRequest.cs b/GFramework.Core/Cqrs/LegacyCommandDispatchRequest.cs new file mode 100644 index 00000000..bbb66713 --- /dev/null +++ b/GFramework.Core/Cqrs/LegacyCommandDispatchRequest.cs @@ -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; + +/// +/// 包装 legacy 无返回值命令,使其能够通过自有 CQRS runtime 调度。 +/// +internal sealed class LegacyCommandDispatchRequest(CoreCommand.ICommand command) + : LegacyCqrsDispatchRequestBase(command), IRequest +{ + /// + /// 获取当前 bridge request 代理的命令实例。 + /// + public CoreCommand.ICommand Command { get; } = command ?? throw new ArgumentNullException(nameof(command)); +} diff --git a/GFramework.Core/Cqrs/LegacyCommandDispatchRequestHandler.cs b/GFramework.Core/Cqrs/LegacyCommandDispatchRequestHandler.cs new file mode 100644 index 00000000..c9cc044e --- /dev/null +++ b/GFramework.Core/Cqrs/LegacyCommandDispatchRequestHandler.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Core.Cqrs; + +/// +/// 处理 legacy 无返回值命令的 bridge handler。 +/// +internal sealed class LegacyCommandDispatchRequestHandler + : LegacyCqrsDispatchHandlerBase, IRequestHandler +{ + /// + public ValueTask Handle(LegacyCommandDispatchRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + PrepareTarget(request.Command); + request.Command.Execute(); + return ValueTask.FromResult(Unit.Value); + } +} diff --git a/GFramework.Core/Cqrs/LegacyCommandResultDispatchRequest.cs b/GFramework.Core/Cqrs/LegacyCommandResultDispatchRequest.cs new file mode 100644 index 00000000..e4c2de40 --- /dev/null +++ b/GFramework.Core/Cqrs/LegacyCommandResultDispatchRequest.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Core.Cqrs; + +/// +/// 包装 legacy 带返回值命令,使其能够通过自有 CQRS runtime 调度。 +/// +internal sealed class LegacyCommandResultDispatchRequest(object target, Func execute) + : LegacyCqrsDispatchRequestBase(target), IRequest +{ + private readonly Func _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + + /// + /// 执行底层 legacy 命令并返回装箱后的结果。 + /// + public object? Execute() => _execute(); +} diff --git a/GFramework.Core/Cqrs/LegacyCommandResultDispatchRequestHandler.cs b/GFramework.Core/Cqrs/LegacyCommandResultDispatchRequestHandler.cs new file mode 100644 index 00000000..c951273d --- /dev/null +++ b/GFramework.Core/Cqrs/LegacyCommandResultDispatchRequestHandler.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Core.Cqrs; + +/// +/// 处理 legacy 带返回值命令的 bridge handler。 +/// +internal sealed class LegacyCommandResultDispatchRequestHandler + : LegacyCqrsDispatchHandlerBase, IRequestHandler +{ + /// + public ValueTask Handle(LegacyCommandResultDispatchRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + PrepareTarget(request.Target); + return ValueTask.FromResult(request.Execute()); + } +} diff --git a/GFramework.Core/Cqrs/LegacyCqrsDispatchHandlerBase.cs b/GFramework.Core/Cqrs/LegacyCqrsDispatchHandlerBase.cs new file mode 100644 index 00000000..1745465f --- /dev/null +++ b/GFramework.Core/Cqrs/LegacyCqrsDispatchHandlerBase.cs @@ -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; + +/// +/// 为 legacy Core CQRS bridge handler 提供共享的上下文注入辅助逻辑。 +/// +internal abstract class LegacyCqrsDispatchHandlerBase : ContextAwareBase +{ + /// + /// 在执行 legacy 命令或查询前,把当前架构上下文显式注入给支持 的目标对象。 + /// + 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); + } + } +} diff --git a/GFramework.Core/Cqrs/LegacyCqrsDispatchRequestBase.cs b/GFramework.Core/Cqrs/LegacyCqrsDispatchRequestBase.cs new file mode 100644 index 00000000..11370c95 --- /dev/null +++ b/GFramework.Core/Cqrs/LegacyCqrsDispatchRequestBase.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +namespace GFramework.Core.Cqrs; + +/// +/// 为 legacy Command / Query 到自有 CQRS runtime 的桥接请求提供共享的目标对象封装。 +/// +internal abstract class LegacyCqrsDispatchRequestBase(object target) +{ + /// + /// 获取当前 bridge request 代理的 legacy 目标对象。 + /// + public object Target { get; } = target ?? throw new ArgumentNullException(nameof(target)); +} diff --git a/GFramework.Core/Cqrs/LegacyQueryDispatchRequest.cs b/GFramework.Core/Cqrs/LegacyQueryDispatchRequest.cs new file mode 100644 index 00000000..4eb2c15f --- /dev/null +++ b/GFramework.Core/Cqrs/LegacyQueryDispatchRequest.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Core.Cqrs; + +/// +/// 包装 legacy 同步查询,使其能够通过自有 CQRS runtime 调度。 +/// +internal sealed class LegacyQueryDispatchRequest(object target, Func execute) + : LegacyCqrsDispatchRequestBase(target), IRequest +{ + private readonly Func _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + + /// + /// 执行底层 legacy 查询并返回装箱后的结果。 + /// + public object? Execute() => _execute(); +} diff --git a/GFramework.Core/Cqrs/LegacyQueryDispatchRequestHandler.cs b/GFramework.Core/Cqrs/LegacyQueryDispatchRequestHandler.cs new file mode 100644 index 00000000..3656869d --- /dev/null +++ b/GFramework.Core/Cqrs/LegacyQueryDispatchRequestHandler.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Core.Cqrs; + +/// +/// 处理 legacy 同步查询的 bridge handler。 +/// +internal sealed class LegacyQueryDispatchRequestHandler + : LegacyCqrsDispatchHandlerBase, IRequestHandler +{ + /// + public ValueTask Handle(LegacyQueryDispatchRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + PrepareTarget(request.Target); + return ValueTask.FromResult(request.Execute()); + } +} diff --git a/GFramework.Core/Query/AsyncQueryExecutor.cs b/GFramework.Core/Query/AsyncQueryExecutor.cs index a80fb1b8..e1b04300 100644 --- a/GFramework.Core/Query/AsyncQueryExecutor.cs +++ b/GFramework.Core/Query/AsyncQueryExecutor.cs @@ -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; /// /// 异步查询总线实现,用于处理异步查询请求 /// -public sealed class AsyncQueryExecutor : IAsyncQueryExecutor +public sealed class AsyncQueryExecutor(ICqrsRuntime? runtime = null) : IAsyncQueryExecutor { + private readonly ICqrsRuntime? _runtime = runtime; + + /// + /// 获取当前执行器是否已接入统一 CQRS runtime。 + /// + public bool UsesCqrsRuntime => _runtime is not null; + /// /// 异步发送查询请求并返回结果 /// @@ -18,8 +28,59 @@ public sealed class AsyncQueryExecutor : IAsyncQueryExecutor /// 包含查询结果的异步任务 public Task SendAsync(IAsyncQuery query) { - // 验证查询参数不为空 ArgumentNullException.ThrowIfNull(query); + + if (TryResolveDispatchContext(query, out var context) && _runtime is not null) + { + return BridgeAsyncQueryAsync(context, query); + } + return query.DoAsync(); } -} \ No newline at end of file + + /// + /// 通过统一 CQRS runtime 异步执行 legacy 查询,并把装箱结果还原为目标类型。 + /// + /// 查询结果类型。 + /// 当前架构上下文。 + /// 要桥接的 legacy 查询。 + /// 查询执行结果。 + private async Task BridgeAsyncQueryAsync( + GFramework.Core.Abstractions.Architectures.IArchitectureContext context, + IAsyncQuery query) + { + var boxedResult = await _runtime!.SendAsync( + context, + new LegacyAsyncQueryDispatchRequest( + query, + async () => await query.DoAsync().ConfigureAwait(false))) + .ConfigureAwait(false); + return (TResult)boxedResult!; + } + + /// + /// 解析当前 legacy 查询应该绑定到哪个架构上下文。 + /// + /// 即将执行的 legacy 查询对象。 + /// 命中时返回可用于 CQRS runtime 的架构上下文。 + /// 如果既接入了 runtime 且查询对象提供了上下文,则返回 + private bool TryResolveDispatchContext(object query, out GFramework.Core.Abstractions.Architectures.IArchitectureContext context) + { + context = null!; + + if (_runtime is null || query is not IContextAware contextAware) + { + return false; + } + + try + { + context = contextAware.GetContext(); + return true; + } + catch (InvalidOperationException) + { + return false; + } + } +} diff --git a/GFramework.Core/Query/QueryExecutor.cs b/GFramework.Core/Query/QueryExecutor.cs index 5d269a6f..c8f2fdd2 100644 --- a/GFramework.Core/Query/QueryExecutor.cs +++ b/GFramework.Core/Query/QueryExecutor.cs @@ -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 接口。 /// 该类是密封的,防止被继承。 /// -public sealed class QueryExecutor : IQueryExecutor +public sealed class QueryExecutor(ICqrsRuntime? runtime = null) : IQueryExecutor { + private readonly ICqrsRuntime? _runtime = runtime; + + /// + /// 获取当前执行器是否已接入统一 CQRS runtime。 + /// + public bool UsesCqrsRuntime => _runtime is not null; + /// /// 执行指定的查询并返回结果。 - /// 该方法通过调用查询对象的 Do 方法来获取结果。 + /// 当查询对象携带可用的架构上下文且执行器已接入统一 runtime 时, + /// 该方法会先把 legacy 查询包装成内部 request 并交给 , + /// 以复用统一的 dispatch / pipeline 入口;否则回退到 legacy 直接执行。 /// /// 查询结果的类型。 /// 要执行的查询对象,必须实现 IQuery<TResult> 接口。 /// 查询执行的结果,类型为 TResult。 public TResult Send(IQuery 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(); } + + /// + /// 解析当前 legacy 查询应该绑定到哪个架构上下文。 + /// + /// 即将执行的 legacy 查询对象。 + /// 命中时返回可用于 CQRS runtime 的架构上下文。 + /// 如果既接入了 runtime 且查询对象提供了上下文,则返回 + private bool TryResolveDispatchContext(object query, out GFramework.Core.Abstractions.Architectures.IArchitectureContext context) + { + context = null!; + + if (_runtime is null || query is not IContextAware contextAware) + { + return false; + } + + try + { + context = contextAware.GetContext(); + return true; + } + catch (InvalidOperationException) + { + return false; + } + } } diff --git a/GFramework.Core/README.md b/GFramework.Core/README.md index 93481ab3..39def5dd 100644 --- a/GFramework.Core/README.md +++ b/GFramework.Core/README.md @@ -15,6 +15,9 @@ - 资源、对象池、日志、协程、并发、环境、配置与本地化 - 服务模块管理、时间提供器与默认的 IoC 容器适配 +标准架构启动路径下,旧 `Command` / `Query` 兼容入口现在会继续保持原有使用方式, +但底层会通过 `GFramework.Cqrs` 的统一 runtime、pipeline 与上下文注入链路执行。 + 它不负责: - 游戏内容配置、Scene / UI / Storage 等游戏层能力 diff --git a/GFramework.Core/Services/Modules/AsyncQueryExecutorModule.cs b/GFramework.Core/Services/Modules/AsyncQueryExecutorModule.cs index 9fb6f787..e1a0524d 100644 --- a/GFramework.Core/Services/Modules/AsyncQueryExecutorModule.cs +++ b/GFramework.Core/Services/Modules/AsyncQueryExecutorModule.cs @@ -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 /// 依赖注入容器实例。 public void Register(IIocContainer container) { - container.RegisterPlurality(new AsyncQueryExecutor()); + ArgumentNullException.ThrowIfNull(container); + container.RegisterPlurality(new AsyncQueryExecutor(container.Get())); } /// @@ -55,4 +57,4 @@ public sealed class AsyncQueryExecutorModule : IServiceModule { return ValueTask.CompletedTask; } -} \ No newline at end of file +} diff --git a/GFramework.Core/Services/Modules/CommandExecutorModule.cs b/GFramework.Core/Services/Modules/CommandExecutorModule.cs index 2ea60049..3d327cda 100644 --- a/GFramework.Core/Services/Modules/CommandExecutorModule.cs +++ b/GFramework.Core/Services/Modules/CommandExecutorModule.cs @@ -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 /// 依赖注入容器实例。 public void Register(IIocContainer container) { - container.RegisterPlurality(new CommandExecutor()); + ArgumentNullException.ThrowIfNull(container); + container.RegisterPlurality(new CommandExecutor(container.Get())); } /// @@ -55,4 +57,4 @@ public sealed class CommandExecutorModule : IServiceModule { return ValueTask.CompletedTask; } -} \ No newline at end of file +} diff --git a/GFramework.Core/Services/Modules/QueryExecutorModule.cs b/GFramework.Core/Services/Modules/QueryExecutorModule.cs index 1f3e8403..40e1eb91 100644 --- a/GFramework.Core/Services/Modules/QueryExecutorModule.cs +++ b/GFramework.Core/Services/Modules/QueryExecutorModule.cs @@ -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 /// 依赖注入容器实例。 public void Register(IIocContainer container) { - container.RegisterPlurality(new QueryExecutor()); + ArgumentNullException.ThrowIfNull(container); + container.RegisterPlurality(new QueryExecutor(container.Get())); } /// @@ -55,4 +57,4 @@ public sealed class QueryExecutorModule : IServiceModule { return ValueTask.CompletedTask; } -} \ No newline at end of file +} 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 c2029a9a..0d6ed018 100644 --- a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md +++ b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md @@ -7,7 +7,7 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-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 ` 当前只产出 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 基线 ## 活跃文档 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 50fb8956..76264060 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,53 @@ ## 2026-05-07 +### 阶段:legacy Core CQRS -> GFramework.Cqrs bridge(CQRS-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 生命周期矩阵 benchmark(CQRS-REWRITE-RP-092) - 使用 `$gframework-batch-boot 50` 启动本轮批次,并按技能要求先复核 `origin/main` 基线与 branch diff: diff --git a/docs/zh-CN/core/command.md b/docs/zh-CN/core/command.md index f5667568..49e74611 100644 --- a/docs/zh-CN/core/command.md +++ b/docs/zh-CN/core/command.md @@ -105,6 +105,9 @@ var reward = this.SendCommand(new GetGoldRewardCommand(new GetGoldRewardInput(3) - `SendCommandAsync(IAsyncCommand)` - `SendCommandAsync(IAsyncCommand)` +在标准架构启动路径中,这些兼容入口底层已经统一改走 `ICqrsRuntime`。 +这意味着历史命令调用链在不改调用方式的前提下,也会复用同一套 pipeline 与上下文注入语义。 + 在 `IContextAware` 对象内,通常直接通过扩展使用: ```csharp diff --git a/docs/zh-CN/core/context.md b/docs/zh-CN/core/context.md index 40cf1fe5..128e27ee 100644 --- a/docs/zh-CN/core/context.md +++ b/docs/zh-CN/core/context.md @@ -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 的主入口。最重要的方法是: diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 2a62250d..389bd6cc 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -224,6 +224,7 @@ RegisterCqrsPipelineBehavior>(); 这里有两个边界需要分开理解: - 旧 `Command` / `Query` 入口仍可用于维护历史调用链 +- 标准 `Architecture` 启动路径下,旧入口现在会通过内部 bridge request 复用同一个 `ICqrsRuntime` - 旧命名空间下的 `ICqrsRuntime` 只是为了兼容既有引用而保留的 alias;面向新代码时,应直接使用 `GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime` @@ -232,6 +233,15 @@ RegisterCqrsPipelineBehavior>(); - 在维护历史代码:允许继续使用旧 Command / Query - 在写新功能或新模块:优先使用 CQRS +相对 `ai-libs/Mediator`,当前 `GFramework.Cqrs` 仍有几类能力差距尚未完全吸收: + +- `IMediator` / `ISender` / `IPublisher` 风格的一等 facade 公开入口 +- telemetry / tracing / metrics 的运行时与生成器配置面 +- 独立的 stream pipeline 行为体系 +- 更丰富的 notification publisher 策略 +- 更强的生成器配置与诊断公开面 +- 生命周期 / 缓存策略的显式公开配置面 + ## 源码阅读入口 如果你需要直接回到源码确认 CQRS 契约,建议按下面这几组入口阅读: diff --git a/docs/zh-CN/core/query.md b/docs/zh-CN/core/query.md index 10e58ac9..e01cde2f 100644 --- a/docs/zh-CN/core/query.md +++ b/docs/zh-CN/core/query.md @@ -83,6 +83,9 @@ var count = this.SendQuery( - `SendQuery(IQuery)` - `SendQueryAsync(IAsyncQuery)` +在标准架构启动路径中,这些兼容入口底层同样会转到统一 `ICqrsRuntime`。 +因此历史查询对象仍保持原始 `SendQuery(...)` / `SendQueryAsync(...)` 用法,但会共享新版 request pipeline 与上下文注入链路。 + 在 `IContextAware` 对象内部,通常直接使用 `GFramework.Core.Extensions` 里的扩展: ```csharp