mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-10 11:04:29 +08:00
Merge pull request #334 from GeWuYou/feat/cqrs-optimization
Feat/Implement CQRS runtime integration for legacy compatibility
This commit is contained in:
commit
54b79d99d3
@ -10,11 +10,13 @@ using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Abstractions.Query;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.Command;
|
||||
using GFramework.Core.Cqrs;
|
||||
using GFramework.Core.Environment;
|
||||
using GFramework.Core.Events;
|
||||
using GFramework.Core.Ioc;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Core.Query;
|
||||
using GFramework.Core.Services.Modules;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Tests.Architectures;
|
||||
@ -41,9 +43,13 @@ namespace GFramework.Core.Tests.Architectures;
|
||||
/// - GetUtility方法 - 获取未注册工具时抛出异常
|
||||
/// - GetEnvironment方法 - 获取环境对象
|
||||
/// </summary>
|
||||
[NonParallelizable]
|
||||
[TestFixture]
|
||||
public class ArchitectureContextTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化测试所需的容器与默认服务实例。
|
||||
/// </summary>
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
@ -71,10 +77,22 @@ public class ArchitectureContextTests
|
||||
_container.RegisterPlurality(_queryBus);
|
||||
_container.RegisterPlurality(_asyncQueryBus);
|
||||
_container.RegisterPlurality(_environment);
|
||||
new CqrsRuntimeModule().Register(_container);
|
||||
RegisterLegacyBridgeHandlers(_container);
|
||||
|
||||
_context = new ArchitectureContext(_container);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放当前测试创建的容器,并清理 legacy bridge 共享计数状态。
|
||||
/// </summary>
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
LegacyBridgePipelineTracker.Reset();
|
||||
_container?.Dispose();
|
||||
}
|
||||
|
||||
private AsyncQueryExecutor? _asyncQueryBus;
|
||||
private CommandExecutor? _commandBus;
|
||||
private MicrosoftDiContainer? _container;
|
||||
@ -124,6 +142,31 @@ public class ArchitectureContextTests
|
||||
Assert.That(result, Is.EqualTo(42));
|
||||
}
|
||||
|
||||
/// <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(out var bridgeContainer);
|
||||
|
||||
try
|
||||
{
|
||||
var result = bridgeContext.SendQuery(testQuery);
|
||||
|
||||
Assert.That(result, Is.EqualTo(24));
|
||||
Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
|
||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
bridgeContainer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试SendQuery方法在查询为null时应抛出ArgumentNullException
|
||||
/// </summary>
|
||||
@ -146,6 +189,31 @@ 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(out var bridgeContainer);
|
||||
|
||||
try
|
||||
{
|
||||
bridgeContext.SendCommand(testCommand);
|
||||
|
||||
Assert.That(testCommand.Executed, Is.True);
|
||||
Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
|
||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
bridgeContainer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试SendCommand方法在命令为null时应抛出ArgumentNullException
|
||||
/// </summary>
|
||||
@ -168,6 +236,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(out var bridgeContainer);
|
||||
|
||||
try
|
||||
{
|
||||
var result = bridgeContext.SendCommand(testCommand);
|
||||
|
||||
Assert.That(result, Is.EqualTo(42));
|
||||
Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
|
||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
bridgeContainer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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(out var bridgeContainer);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await bridgeContext.SendQueryAsync(testQuery).ConfigureAwait(false);
|
||||
|
||||
Assert.That(result, Is.EqualTo(64));
|
||||
Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
|
||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
bridgeContainer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为需要验证统一 CQRS pipeline 的用例创建一个已冻结的最小 bridge 上下文。
|
||||
/// </summary>
|
||||
/// <param name="container">返回承载当前 bridge 上下文的冻结容器,供测试在 finally 中显式释放。</param>
|
||||
/// <returns>能够执行 legacy bridge request 且会 materialize open-generic pipeline behavior 的上下文。</returns>
|
||||
private static ArchitectureContext CreateFrozenBridgeContext(out MicrosoftDiContainer container)
|
||||
{
|
||||
container = new MicrosoftDiContainer();
|
||||
RegisterLegacyBridgeHandlers(container);
|
||||
new CqrsRuntimeModule().Register(container);
|
||||
container.ExecuteServicesHook(services =>
|
||||
services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LegacyBridgeTrackingPipelineBehavior<,>)));
|
||||
container.Freeze();
|
||||
return new ArchitectureContext(container);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把 GFramework.Core 内部的 legacy bridge handler 实例预先注册成可见的实例绑定。
|
||||
/// </summary>
|
||||
/// <param name="container">目标测试容器。</param>
|
||||
private static void RegisterLegacyBridgeHandlers(MicrosoftDiContainer container)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
container.RegisterPlurality(new LegacyCommandDispatchRequestHandler());
|
||||
container.RegisterPlurality(new LegacyCommandResultDispatchRequestHandler());
|
||||
container.RegisterPlurality(new LegacyAsyncCommandDispatchRequestHandler());
|
||||
container.RegisterPlurality(new LegacyAsyncCommandResultDispatchRequestHandler());
|
||||
container.RegisterPlurality(new LegacyQueryDispatchRequestHandler());
|
||||
container.RegisterPlurality(new LegacyAsyncQueryDispatchRequestHandler());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试SendCommand方法(带返回值)在命令为null时应抛出ArgumentNullException
|
||||
/// </summary>
|
||||
|
||||
@ -6,6 +6,8 @@ using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Abstractions.Utility;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace GFramework.Core.Tests.Architectures;
|
||||
|
||||
@ -13,6 +15,7 @@ namespace GFramework.Core.Tests.Architectures;
|
||||
/// 验证 Architecture 通过 <c>ArchitectureModules</c> 暴露出的模块安装与 CQRS 行为注册能力。
|
||||
/// 这些测试覆盖模块安装回调和请求管道行为接入,确保模块管理器仍然保持可观察行为不变。
|
||||
/// </summary>
|
||||
[NonParallelizable]
|
||||
[TestFixture]
|
||||
public class ArchitectureModulesBehaviorTests
|
||||
{
|
||||
@ -35,6 +38,7 @@ public class ArchitectureModulesBehaviorTests
|
||||
{
|
||||
GameContext.Clear();
|
||||
TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount = 0;
|
||||
LegacyBridgePipelineTracker.Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -47,15 +51,19 @@ public class ArchitectureModulesBehaviorTests
|
||||
var architecture = new ModuleTestArchitecture(target => target.InstallModule(module));
|
||||
|
||||
await architecture.InitializeAsync();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
try
|
||||
{
|
||||
Assert.That(module.InstalledArchitecture, Is.SameAs(architecture));
|
||||
Assert.That(module.InstallCallCount, Is.EqualTo(1));
|
||||
Assert.That(architecture.Context.GetUtility<InstalledByModuleUtility>(), Is.Not.Null);
|
||||
});
|
||||
|
||||
await architecture.DestroyAsync();
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(module.InstalledArchitecture, Is.SameAs(architecture));
|
||||
Assert.That(module.InstallCallCount, Is.EqualTo(1));
|
||||
Assert.That(architecture.Context.GetUtility<InstalledByModuleUtility>(), Is.Not.Null);
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
await architecture.DestroyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -68,16 +76,54 @@ public class ArchitectureModulesBehaviorTests
|
||||
target.RegisterCqrsPipelineBehavior<TrackingPipelineBehavior<ModuleBehaviorRequest, string>>());
|
||||
|
||||
await architecture.InitializeAsync();
|
||||
|
||||
var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest());
|
||||
|
||||
Assert.Multiple(() =>
|
||||
try
|
||||
{
|
||||
Assert.That(response, Is.EqualTo("handled"));
|
||||
Assert.That(TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount, Is.EqualTo(1));
|
||||
});
|
||||
var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest());
|
||||
|
||||
await architecture.DestroyAsync();
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(response, Is.EqualTo("handled"));
|
||||
Assert.That(TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount, Is.EqualTo(1));
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
await architecture.DestroyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证默认架构初始化路径会自动扫描 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();
|
||||
try
|
||||
{
|
||||
var query = new LegacyArchitectureBridgeQuery();
|
||||
var command = new LegacyArchitectureBridgeCommand();
|
||||
|
||||
var queryResult = architecture.Context.SendQuery(query);
|
||||
architecture.Context.SendCommand(command);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(queryResult, Is.EqualTo(24));
|
||||
Assert.That(query.ObservedContext, Is.SameAs(architecture.Context));
|
||||
Assert.That(command.Executed, Is.True);
|
||||
Assert.That(command.ObservedContext, Is.SameAs(architecture.Context));
|
||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(2));
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
await architecture.DestroyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -94,6 +140,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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Threading;
|
||||
using GFramework.Core.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Tests.Architectures;
|
||||
|
||||
/// <summary>
|
||||
/// 为 legacy bridge pipeline 回归测试保存跨泛型闭包共享的计数状态。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该计数器通过 <see cref="Interlocked.Increment(ref int)" /> 原子递增,并使用
|
||||
/// <see cref="Volatile" /> 读写,因此单次读写操作本身是线程安全的。
|
||||
/// 由于状态在同一进程内跨 fixture 共享,所有使用它的测试都必须在清理阶段调用 <see cref="Reset" />,
|
||||
/// 以避免并行或失败测试把旧计数泄露给后续断言。
|
||||
/// </remarks>
|
||||
public static class LegacyBridgePipelineTracker
|
||||
{
|
||||
private static int _invocationCount;
|
||||
|
||||
/// <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 (typeof(LegacyCqrsDispatchRequestBase).IsAssignableFrom(requestType))
|
||||
{
|
||||
Interlocked.Increment(ref _invocationCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Command;
|
||||
using GFramework.Core.Rule;
|
||||
using GFramework.Core.Tests.Architectures;
|
||||
|
||||
namespace GFramework.Core.Tests.Command;
|
||||
|
||||
@ -75,6 +77,95 @@ public class CommandExecutorTests
|
||||
Assert.Throws<ArgumentNullException>(() => _commandExecutor.Send<int>(null!));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 legacy 命令没有可用上下文时,会安全回退到本地直接执行路径。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Send_Should_Fall_Back_To_Legacy_Execution_When_Context_IsMissing()
|
||||
{
|
||||
var runtime = new RecordingCqrsRuntime();
|
||||
var executor = new CommandExecutor(runtime);
|
||||
var command = new MissingContextLegacyCommand();
|
||||
|
||||
executor.Send(command);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(command.Executed, Is.True);
|
||||
Assert.That(runtime.LastRequest, Is.Null);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证非“缺上下文”类型的 <see cref="InvalidOperationException" /> 不会被 bridge 回退逻辑误吞掉。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Send_Should_Propagate_InvalidOperationException_When_ContextAware_Target_Throws_Unexpected_Error()
|
||||
{
|
||||
var runtime = new RecordingCqrsRuntime();
|
||||
var executor = new CommandExecutor(runtime);
|
||||
var command = new ThrowingLegacyCommand();
|
||||
|
||||
Assert.That(
|
||||
() => executor.Send(command),
|
||||
Throws.InvalidOperationException.With.Message.EqualTo(ThrowingLegacyCommand.ExceptionMessage));
|
||||
Assert.That(runtime.LastRequest, Is.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 legacy 同步命令桥接会在线程池上等待 runtime,
|
||||
/// 避免直接继承调用方当前的同步上下文。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Send_Should_Bridge_Through_Runtime_Without_Reusing_Caller_SynchronizationContext()
|
||||
{
|
||||
var runtime = new RecordingCqrsRuntime();
|
||||
var executor = new CommandExecutor(runtime);
|
||||
var command = new ContextAwareLegacyCommand();
|
||||
var expectedContext = new TestArchitectureContextBaseStub();
|
||||
((GFramework.Core.Abstractions.Rule.IContextAware)command).SetContext(expectedContext);
|
||||
var originalContext = SynchronizationContext.Current;
|
||||
|
||||
try
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(new TestLegacySynchronizationContext());
|
||||
|
||||
executor.Send(command);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(runtime.LastRequest, Is.TypeOf<GFramework.Core.Cqrs.LegacyCommandDispatchRequest>());
|
||||
Assert.That(runtime.ObservedSynchronizationContextType, Is.Null);
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(originalContext);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 legacy 带返回值命令桥接也会保留上下文注入与返回值语义。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Send_WithResult_Should_Bridge_Through_Runtime_And_Preserve_Context()
|
||||
{
|
||||
var runtime = new RecordingCqrsRuntime(static _ => 123);
|
||||
var executor = new CommandExecutor(runtime);
|
||||
var command = new ContextAwareLegacyCommandWithResult(123);
|
||||
var expectedContext = new TestArchitectureContextBaseStub();
|
||||
((GFramework.Core.Abstractions.Rule.IContextAware)command).SetContext(expectedContext);
|
||||
|
||||
var result = executor.Send(command);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(result, Is.EqualTo(123));
|
||||
Assert.That(runtime.LastRequest, Is.TypeOf<GFramework.Core.Cqrs.LegacyCommandResultDispatchRequest>());
|
||||
Assert.That(command.ObservedContext, Is.SameAs(expectedContext));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试SendAsync方法执行异步命令
|
||||
/// </summary>
|
||||
@ -122,4 +213,65 @@ public class CommandExecutorTests
|
||||
{
|
||||
Assert.ThrowsAsync<ArgumentNullException>(() => _commandExecutor.SendAsync<int>(null!));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为同步 bridge 测试提供最小架构上下文替身。
|
||||
/// </summary>
|
||||
private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证缺少上下文时仍会走本地 fallback 的测试命令。
|
||||
/// </summary>
|
||||
private sealed class MissingContextLegacyCommand : GFramework.Core.Abstractions.Rule.IContextAware, GFramework.Core.Abstractions.Command.ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取命令是否已经执行。
|
||||
/// </summary>
|
||||
public bool Executed { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext()
|
||||
{
|
||||
throw new InvalidOperationException("Architecture context has not been set. Call SetContext before accessing the context.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Execute()
|
||||
{
|
||||
Executed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证 bridge 上下文解析不会吞掉意外运行时错误的测试命令。
|
||||
/// </summary>
|
||||
private sealed class ThrowingLegacyCommand : GFramework.Core.Abstractions.Rule.IContextAware, GFramework.Core.Abstractions.Command.ICommand
|
||||
{
|
||||
internal const string ExceptionMessage = "Unexpected context failure.";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext()
|
||||
{
|
||||
throw new InvalidOperationException(ExceptionMessage);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Execute()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
GFramework.Core.Tests/Command/ContextAwareLegacyCommand.cs
Normal file
31
GFramework.Core.Tests/Command/ContextAwareLegacyCommand.cs
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Command;
|
||||
using GFramework.Core.Rule;
|
||||
|
||||
namespace GFramework.Core.Tests.Command;
|
||||
|
||||
/// <summary>
|
||||
/// 为 <see cref="CommandExecutorTests" /> 提供可观察上下文注入的 legacy 命令。
|
||||
/// </summary>
|
||||
internal sealed class ContextAwareLegacyCommand : ContextAwareBase, ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取执行期间观察到的架构上下文。
|
||||
/// </summary>
|
||||
public IArchitectureContext? ObservedContext { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取命令是否已经执行。
|
||||
/// </summary>
|
||||
public bool Executed { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Execute()
|
||||
{
|
||||
Executed = true;
|
||||
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Command;
|
||||
using GFramework.Core.Rule;
|
||||
|
||||
namespace GFramework.Core.Tests.Command;
|
||||
|
||||
/// <summary>
|
||||
/// 为 <see cref="CommandExecutorTests" /> 提供可观察上下文注入的带返回值 legacy 命令。
|
||||
/// </summary>
|
||||
internal sealed class ContextAwareLegacyCommandWithResult(int result) : ContextAwareBase, ICommand<int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取执行期间观察到的架构上下文。
|
||||
/// </summary>
|
||||
public IArchitectureContext? ObservedContext { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Execute()
|
||||
{
|
||||
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
207
GFramework.Core.Tests/Command/RecordingCqrsRuntime.cs
Normal file
207
GFramework.Core.Tests/Command/RecordingCqrsRuntime.cs
Normal file
@ -0,0 +1,207 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
using GFramework.Core.Cqrs;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Tests.Command;
|
||||
|
||||
/// <summary>
|
||||
/// 记录 bridge 执行线程与收到请求的最小 CQRS runtime 测试替身。
|
||||
/// </summary>
|
||||
internal sealed class RecordingCqrsRuntime(Func<object?, object?>? responseFactory = null) : ICqrsRuntime
|
||||
{
|
||||
private static readonly Func<object?, object?> DefaultResponseFactory = _ => null;
|
||||
|
||||
private readonly Func<object?, object?> _responseFactory = responseFactory ?? DefaultResponseFactory;
|
||||
|
||||
/// <summary>
|
||||
/// 获取最近一次 <see cref="SendAsync{TResponse}" /> 观察到的同步上下文类型。
|
||||
/// </summary>
|
||||
public Type? ObservedSynchronizationContextType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最近一次收到的请求实例。
|
||||
/// </summary>
|
||||
public object? LastRequest { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<TResponse> SendAsync<TResponse>(
|
||||
ICqrsContext context,
|
||||
IRequest<TResponse> request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
ObservedSynchronizationContextType = SynchronizationContext.Current?.GetType();
|
||||
LastRequest = request;
|
||||
|
||||
object? response = request switch
|
||||
{
|
||||
LegacyCommandDispatchRequest legacyCommandDispatchRequest => ExecuteLegacyCommand(context, legacyCommandDispatchRequest),
|
||||
LegacyCommandResultDispatchRequest legacyCommandResultDispatchRequest => ExecuteContextAwareRequest(
|
||||
context,
|
||||
legacyCommandResultDispatchRequest.Target,
|
||||
legacyCommandResultDispatchRequest.Execute),
|
||||
LegacyQueryDispatchRequest legacyQueryDispatchRequest => ExecuteContextAwareRequest(
|
||||
context,
|
||||
legacyQueryDispatchRequest.Target,
|
||||
legacyQueryDispatchRequest.Execute),
|
||||
LegacyAsyncCommandDispatchRequest legacyAsyncCommandDispatchRequest => await ExecuteLegacyAsyncCommandAsync(
|
||||
context,
|
||||
legacyAsyncCommandDispatchRequest,
|
||||
cancellationToken).ConfigureAwait(false),
|
||||
LegacyAsyncCommandResultDispatchRequest legacyAsyncCommandResultDispatchRequest => await ExecuteContextAwareRequestAsync(
|
||||
context,
|
||||
legacyAsyncCommandResultDispatchRequest.Target,
|
||||
legacyAsyncCommandResultDispatchRequest.ExecuteAsync,
|
||||
cancellationToken).ConfigureAwait(false),
|
||||
LegacyAsyncQueryDispatchRequest legacyAsyncQueryDispatchRequest => await ExecuteContextAwareRequestAsync(
|
||||
context,
|
||||
legacyAsyncQueryDispatchRequest.Target,
|
||||
legacyAsyncQueryDispatchRequest.ExecuteAsync,
|
||||
cancellationToken).ConfigureAwait(false),
|
||||
IRequest<Unit> => Unit.Value,
|
||||
_ => _responseFactory(request)
|
||||
};
|
||||
|
||||
return ConvertResponse<TResponse>(request, response);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask PublishAsync<TNotification>(
|
||||
ICqrsContext context,
|
||||
TNotification notification,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TNotification : INotification
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(
|
||||
ICqrsContext context,
|
||||
IStreamRequest<TResponse> request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将测试替身工厂返回的装箱结果显式还原为目标类型,并在类型不匹配时给出可诊断异常。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前请求声明的响应类型。</typeparam>
|
||||
/// <param name="request">触发响应工厂的请求实例。</param>
|
||||
/// <param name="response">响应工厂返回的装箱结果。</param>
|
||||
/// <returns>还原后的目标类型响应。</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 响应工厂返回 <see langword="null" /> 或错误类型,导致无法还原为 <typeparamref name="TResponse" />。
|
||||
/// </exception>
|
||||
private static TResponse ConvertResponse<TResponse>(IRequest<TResponse> request, object? response)
|
||||
{
|
||||
if (response is TResponse typedResponse)
|
||||
{
|
||||
return typedResponse;
|
||||
}
|
||||
|
||||
if (response is null && !typeof(TResponse).IsValueType)
|
||||
{
|
||||
return (TResponse)response!;
|
||||
}
|
||||
|
||||
string actualType = response?.GetType().FullName ?? "null";
|
||||
throw new InvalidOperationException(
|
||||
$"RecordingCqrsRuntime 无法将响应类型从 '{actualType}' 转换为 '{typeof(TResponse).FullName}'。"
|
||||
+ $" 请求类型:'{request.GetType().FullName}'。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按 bridge handler 语义为 legacy 无返回值命令注入上下文并执行。
|
||||
/// </summary>
|
||||
/// <param name="context">当前运行时接收到的架构上下文。</param>
|
||||
/// <param name="request">待执行的 legacy 命令桥接请求。</param>
|
||||
/// <returns>桥接后的 <see cref="Unit" /> 响应。</returns>
|
||||
private static Unit ExecuteLegacyCommand(
|
||||
ICqrsContext context,
|
||||
LegacyCommandDispatchRequest request)
|
||||
{
|
||||
PrepareTarget(context, request.Command);
|
||||
request.Command.Execute();
|
||||
return Unit.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按 bridge handler 语义为 legacy 异步无返回值命令注入上下文并执行。
|
||||
/// </summary>
|
||||
/// <param name="context">当前运行时接收到的架构上下文。</param>
|
||||
/// <param name="request">待执行的 legacy 异步命令桥接请求。</param>
|
||||
/// <param name="cancellationToken">调用方传入的取消令牌。</param>
|
||||
/// <returns>表示 bridge 执行完成的异步结果。</returns>
|
||||
private static async Task<Unit> ExecuteLegacyAsyncCommandAsync(
|
||||
ICqrsContext context,
|
||||
LegacyAsyncCommandDispatchRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
PrepareTarget(context, request.Command);
|
||||
await request.Command.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
return Unit.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按 bridge handler 语义为带返回值 legacy 请求注入上下文并执行同步委托。
|
||||
/// </summary>
|
||||
/// <param name="context">当前运行时接收到的架构上下文。</param>
|
||||
/// <param name="target">需要接收上下文注入的 legacy 目标对象。</param>
|
||||
/// <param name="execute">实际执行 legacy 目标逻辑的同步委托。</param>
|
||||
/// <returns>同步执行结果。</returns>
|
||||
private static object? ExecuteContextAwareRequest(
|
||||
ICqrsContext context,
|
||||
object target,
|
||||
Func<object?> execute)
|
||||
{
|
||||
PrepareTarget(context, target);
|
||||
return execute();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按 bridge handler 语义为带返回值 legacy 请求注入上下文并执行异步委托。
|
||||
/// </summary>
|
||||
/// <param name="context">当前运行时接收到的架构上下文。</param>
|
||||
/// <param name="target">需要接收上下文注入的 legacy 目标对象。</param>
|
||||
/// <param name="executeAsync">实际执行 legacy 目标逻辑的异步委托。</param>
|
||||
/// <param name="cancellationToken">调用方传入的取消令牌。</param>
|
||||
/// <returns>异步执行结果。</returns>
|
||||
private static async Task<object?> ExecuteContextAwareRequestAsync(
|
||||
ICqrsContext context,
|
||||
object target,
|
||||
Func<Task<object?>> executeAsync,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
PrepareTarget(context, target);
|
||||
return await executeAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 legacy bridge handler 的上下文注入语义,使测试替身与生产桥接行为保持一致。
|
||||
/// </summary>
|
||||
/// <param name="context">当前运行时接收到的架构上下文。</param>
|
||||
/// <param name="target">即将执行的 legacy 目标对象。</param>
|
||||
private static void PrepareTarget(ICqrsContext context, object target)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
|
||||
if (context is not GFramework.Core.Abstractions.Architectures.IArchitectureContext architectureContext)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"RecordingCqrsRuntime 期望收到 IArchitectureContext,但实际为 '{context.GetType().FullName}'。");
|
||||
}
|
||||
|
||||
if (target is IContextAware contextAware)
|
||||
{
|
||||
contextAware.SetContext(architectureContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
namespace GFramework.Core.Tests.Command;
|
||||
|
||||
/// <summary>
|
||||
/// 为 legacy 同步 bridge 回归测试提供可识别的同步上下文占位类型。
|
||||
/// </summary>
|
||||
internal sealed class TestLegacySynchronizationContext : SynchronizationContext
|
||||
{
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Command;
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
using GFramework.Core.Cqrs;
|
||||
using GFramework.Core.Rule;
|
||||
using GFramework.Core.Tests.Architectures;
|
||||
|
||||
namespace GFramework.Core.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 验证 legacy 异步无返回值命令 bridge handler 的取消语义。
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class LegacyAsyncCommandDispatchRequestHandlerTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证当取消令牌在执行前已触发时,handler 不会启动底层 legacy 命令。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Handle_Should_Throw_Without_Executing_Command_When_Cancellation_Is_Already_Requested()
|
||||
{
|
||||
var handler = new LegacyAsyncCommandDispatchRequestHandler();
|
||||
var command = new ProbeAsyncCommand(Task.CompletedTask);
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
Assert.ThrowsAsync<OperationCanceledException>(
|
||||
async () => await handler.Handle(
|
||||
new LegacyAsyncCommandDispatchRequest(command),
|
||||
cancellationTokenSource.Token)
|
||||
.AsTask()
|
||||
.ConfigureAwait(false));
|
||||
Assert.That(command.ExecutionCount, Is.Zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当底层 legacy 命令正在运行时,handler 会通过 <c>WaitAsync</c> 及时向调用方暴露取消。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Handle_Should_Observe_Cancellation_While_Command_Is_Running()
|
||||
{
|
||||
var completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var handler = new LegacyAsyncCommandDispatchRequestHandler();
|
||||
var command = new ProbeAsyncCommand(completionSource.Task);
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
((IContextAware)handler).SetContext(new TestArchitectureContextBaseStub());
|
||||
|
||||
var handleTask = handler.Handle(
|
||||
new LegacyAsyncCommandDispatchRequest(command),
|
||||
cancellationTokenSource.Token)
|
||||
.AsTask();
|
||||
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
Assert.That(
|
||||
async () => await handleTask.ConfigureAwait(false),
|
||||
Throws.InstanceOf<OperationCanceledException>());
|
||||
Assert.That(command.ExecutionCount, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 handler 取消测试提供可控完成时机的异步命令替身。
|
||||
/// </summary>
|
||||
private sealed class ProbeAsyncCommand(Task executionTask) : ContextAwareBase, IAsyncCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取底层命令逻辑的触发次数。
|
||||
/// </summary>
|
||||
public int ExecutionCount { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ExecuteAsync()
|
||||
{
|
||||
ExecutionCount++;
|
||||
return executionTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 handler 取消测试提供最小架构上下文替身。
|
||||
/// </summary>
|
||||
private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Query;
|
||||
using GFramework.Core.Tests.Architectures;
|
||||
using GFramework.Core.Tests.Command;
|
||||
|
||||
namespace GFramework.Core.Tests.Query;
|
||||
|
||||
@ -138,4 +140,33 @@ public class AsyncQueryExecutorTests
|
||||
Assert.That(result1, Is.EqualTo(20));
|
||||
Assert.That(result2, Is.EqualTo(40));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 legacy 异步查询桥接会保留上下文注入,并通过 runtime 返回结果。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task SendAsync_Should_Bridge_Through_Runtime_And_Preserve_Context()
|
||||
{
|
||||
var runtime = new RecordingCqrsRuntime(static _ => 64);
|
||||
var executor = new AsyncQueryExecutor(runtime);
|
||||
var query = new ContextAwareLegacyAsyncQuery(64);
|
||||
var expectedContext = new TestArchitectureContextBaseStub();
|
||||
((GFramework.Core.Abstractions.Rule.IContextAware)query).SetContext(expectedContext);
|
||||
|
||||
var result = await executor.SendAsync(query);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(result, Is.EqualTo(64));
|
||||
Assert.That(runtime.LastRequest, Is.TypeOf<GFramework.Core.Cqrs.LegacyAsyncQueryDispatchRequest>());
|
||||
Assert.That(query.ObservedContext, Is.SameAs(expectedContext));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为异步 bridge 测试提供最小架构上下文替身。
|
||||
/// </summary>
|
||||
private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
26
GFramework.Core.Tests/Query/ContextAwareLegacyAsyncQuery.cs
Normal file
26
GFramework.Core.Tests/Query/ContextAwareLegacyAsyncQuery.cs
Normal file
@ -0,0 +1,26 @@
|
||||
// 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.Query;
|
||||
|
||||
/// <summary>
|
||||
/// 为 <see cref="AsyncQueryExecutorTests" /> 提供可观察上下文注入的 legacy 异步查询。
|
||||
/// </summary>
|
||||
internal sealed class ContextAwareLegacyAsyncQuery(int result) : ContextAwareBase, IAsyncQuery<int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取执行期间观察到的架构上下文。
|
||||
/// </summary>
|
||||
public IArchitectureContext? ObservedContext { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> DoAsync()
|
||||
{
|
||||
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
}
|
||||
26
GFramework.Core.Tests/Query/ContextAwareLegacyQuery.cs
Normal file
26
GFramework.Core.Tests/Query/ContextAwareLegacyQuery.cs
Normal file
@ -0,0 +1,26 @@
|
||||
// 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.Query;
|
||||
|
||||
/// <summary>
|
||||
/// 为 <see cref="QueryExecutorTests" /> 提供可观察上下文注入的 legacy 查询。
|
||||
/// </summary>
|
||||
internal sealed class ContextAwareLegacyQuery(int result) : ContextAwareBase, IQuery<int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取执行期间观察到的架构上下文。
|
||||
/// </summary>
|
||||
public IArchitectureContext? ObservedContext { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Do()
|
||||
{
|
||||
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Query;
|
||||
using GFramework.Core.Tests.Architectures;
|
||||
using GFramework.Core.Tests.Command;
|
||||
|
||||
namespace GFramework.Core.Tests.Query;
|
||||
|
||||
@ -61,4 +63,44 @@ public class QueryExecutorTests
|
||||
|
||||
Assert.That(result, Is.EqualTo("Result: 10"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 legacy 同步查询桥接会在线程池上等待 runtime,
|
||||
/// 避免直接复用调用方的同步上下文。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Send_Should_Bridge_Through_Runtime_Without_Reusing_Caller_SynchronizationContext()
|
||||
{
|
||||
var runtime = new RecordingCqrsRuntime(static _ => 24);
|
||||
var executor = new QueryExecutor(runtime);
|
||||
var query = new ContextAwareLegacyQuery(24);
|
||||
var expectedContext = new TestArchitectureContextBaseStub();
|
||||
((GFramework.Core.Abstractions.Rule.IContextAware)query).SetContext(expectedContext);
|
||||
var originalContext = SynchronizationContext.Current;
|
||||
|
||||
try
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(new TestLegacySynchronizationContext());
|
||||
|
||||
var result = executor.Send(query);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(result, Is.EqualTo(24));
|
||||
Assert.That(runtime.LastRequest, Is.TypeOf<GFramework.Core.Cqrs.LegacyQueryDispatchRequest>());
|
||||
Assert.That(runtime.ObservedSynchronizationContextType, Is.Null);
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(originalContext);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为同步 bridge 测试提供最小架构上下文替身。
|
||||
/// </summary>
|
||||
private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -111,7 +112,8 @@ public class ArchitectureContext : IArchitectureContext
|
||||
/// <returns>响应结果</returns>
|
||||
public TResponse SendRequest<TResponse>(IRequest<TResponse> request)
|
||||
{
|
||||
return SendRequestAsync(request).AsTask().GetAwaiter().GetResult();
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -180,10 +182,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,9 +196,10 @@ 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();
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, query);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -205,10 +210,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 +226,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 +363,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 +377,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 +389,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,9 +403,10 @@ 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();
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -406,8 +416,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 +428,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
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Command;
|
||||
using GFramework.Core.Cqrs;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using IAsyncCommand = GFramework.Core.Abstractions.Command.IAsyncCommand;
|
||||
|
||||
namespace GFramework.Core.Command;
|
||||
@ -10,8 +12,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 +35,11 @@ public sealed class CommandExecutor : ICommandExecutor
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
|
||||
if (TryExecuteThroughCqrsRuntime(command, static currentCommand => new LegacyCommandDispatchRequest(currentCommand)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
command.Execute();
|
||||
}
|
||||
|
||||
@ -35,6 +54,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 +76,13 @@ public sealed class CommandExecutor : ICommandExecutor
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
|
||||
var cqrsRuntime = _runtime;
|
||||
|
||||
if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, command, out var context))
|
||||
{
|
||||
return cqrsRuntime.SendAsync(context, new LegacyAsyncCommandDispatchRequest(command)).AsTask();
|
||||
}
|
||||
|
||||
return command.ExecuteAsync();
|
||||
}
|
||||
|
||||
@ -61,6 +97,90 @@ public sealed class CommandExecutor : ICommandExecutor
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
|
||||
var cqrsRuntime = _runtime;
|
||||
|
||||
if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, command, out var context))
|
||||
{
|
||||
return BridgeAsyncCommandWithResultAsync(cqrsRuntime, 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>
|
||||
{
|
||||
var cqrsRuntime = _runtime;
|
||||
|
||||
if (!LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, target, out var context))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
LegacyCqrsDispatchHelper.SendSynchronously(cqrsRuntime, context, requestFactory(target));
|
||||
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?>
|
||||
{
|
||||
var cqrsRuntime = _runtime;
|
||||
|
||||
if (!LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, target, out var context))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var boxedResult = LegacyCqrsDispatchHelper.SendSynchronously(cqrsRuntime, context, requestFactory(target));
|
||||
result = (TResult)boxedResult!;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过统一 CQRS runtime 异步执行 legacy 带返回值命令,并把装箱结果还原为目标类型。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">命令返回值类型。</typeparam>
|
||||
/// <param name="runtime">负责调度当前 bridge request 的统一 CQRS runtime。</param>
|
||||
/// <param name="context">当前架构上下文。</param>
|
||||
/// <param name="command">要桥接的 legacy 命令。</param>
|
||||
/// <returns>命令执行结果。</returns>
|
||||
private static async Task<TResult> BridgeAsyncCommandWithResultAsync<TResult>(
|
||||
ICqrsRuntime runtime,
|
||||
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!;
|
||||
}
|
||||
}
|
||||
|
||||
25
GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequest.cs
Normal file
25
GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequest.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// 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>
|
||||
/// <param name="command">当前 bridge request 代理的 legacy 异步命令实例。</param>
|
||||
internal sealed class LegacyAsyncCommandDispatchRequest(CoreCommand.IAsyncCommand command)
|
||||
: LegacyCqrsDispatchRequestBase(ValidateCommand(command)), IRequest<Unit>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前 bridge request 代理的异步命令实例。
|
||||
/// </summary>
|
||||
public CoreCommand.IAsyncCommand Command { get; } = command;
|
||||
|
||||
private static CoreCommand.IAsyncCommand ValidateCommand(CoreCommand.IAsyncCommand command)
|
||||
{
|
||||
return command ?? throw new ArgumentNullException(nameof(command));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
// 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);
|
||||
// Legacy ExecuteAsync contract does not accept CancellationToken; use WaitAsync so the caller can still observe cancellation promptly.
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
PrepareTarget(request.Command);
|
||||
await request.Command.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
@ -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 异步带返回值命令,使其能够通过自有 CQRS runtime 调度。
|
||||
/// </summary>
|
||||
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 命令目标实例。</param>
|
||||
/// <param name="executeAsync">封装 legacy 异步命令执行逻辑并返回装箱结果的委托。</param>
|
||||
internal sealed class LegacyAsyncCommandResultDispatchRequest(object target, Func<Task<object?>> executeAsync)
|
||||
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
|
||||
{
|
||||
private readonly Func<Task<object?>> _executeAsync = executeAsync ?? throw new ArgumentNullException(nameof(executeAsync));
|
||||
|
||||
/// <summary>
|
||||
/// 异步执行底层 legacy 命令并返回装箱后的结果。
|
||||
/// </summary>
|
||||
/// <returns>表示异步执行结果的任务;任务结果为底层 legacy 命令返回的装箱值。</returns>
|
||||
public Task<object?> ExecuteAsync() => _executeAsync();
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
// 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);
|
||||
// Legacy ExecuteAsync contract does not accept CancellationToken; use WaitAsync so the caller can observe cancellation promptly.
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
PrepareTarget(request.Target);
|
||||
return await request.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
23
GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequest.cs
Normal file
23
GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequest.cs
Normal 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 异步查询,使其能够通过自有 CQRS runtime 调度。
|
||||
/// </summary>
|
||||
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 查询目标实例。</param>
|
||||
/// <param name="executeAsync">封装 legacy 异步查询执行逻辑并返回装箱结果的委托。</param>
|
||||
internal sealed class LegacyAsyncQueryDispatchRequest(object target, Func<Task<object?>> executeAsync)
|
||||
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
|
||||
{
|
||||
private readonly Func<Task<object?>> _executeAsync = executeAsync ?? throw new ArgumentNullException(nameof(executeAsync));
|
||||
|
||||
/// <summary>
|
||||
/// 异步执行底层 legacy 查询并返回装箱后的结果。
|
||||
/// </summary>
|
||||
/// <returns>表示异步执行结果的任务;任务结果为底层 legacy 查询返回的装箱值。</returns>
|
||||
public Task<object?> ExecuteAsync() => _executeAsync();
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
// 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);
|
||||
// Legacy DoAsync contract does not accept CancellationToken; use WaitAsync so the caller can observe cancellation promptly.
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
PrepareTarget(request.Target);
|
||||
return await request.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
25
GFramework.Core/Cqrs/LegacyCommandDispatchRequest.cs
Normal file
25
GFramework.Core/Cqrs/LegacyCommandDispatchRequest.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// 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>
|
||||
/// <param name="command">当前 bridge request 代理的 legacy 命令实例。</param>
|
||||
internal sealed class LegacyCommandDispatchRequest(CoreCommand.ICommand command)
|
||||
: LegacyCqrsDispatchRequestBase(ValidateCommand(command)), IRequest<Unit>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前 bridge request 代理的命令实例。
|
||||
/// </summary>
|
||||
public CoreCommand.ICommand Command { get; } = command;
|
||||
|
||||
private static CoreCommand.ICommand ValidateCommand(CoreCommand.ICommand command)
|
||||
{
|
||||
return command ?? throw new ArgumentNullException(nameof(command));
|
||||
}
|
||||
}
|
||||
22
GFramework.Core/Cqrs/LegacyCommandDispatchRequestHandler.cs
Normal file
22
GFramework.Core/Cqrs/LegacyCommandDispatchRequestHandler.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
23
GFramework.Core/Cqrs/LegacyCommandResultDispatchRequest.cs
Normal file
23
GFramework.Core/Cqrs/LegacyCommandResultDispatchRequest.cs
Normal 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 带返回值命令,使其能够通过自有 CQRS runtime 调度。
|
||||
/// </summary>
|
||||
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 命令目标实例。</param>
|
||||
/// <param name="execute">封装 legacy 命令执行逻辑并返回装箱结果的委托。</param>
|
||||
internal sealed class LegacyCommandResultDispatchRequest(object target, Func<object?> execute)
|
||||
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
|
||||
{
|
||||
private readonly Func<object?> _execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
|
||||
/// <summary>
|
||||
/// 执行底层 legacy 命令并返回装箱后的结果。
|
||||
/// </summary>
|
||||
/// <returns>底层 legacy 命令执行后的装箱结果;若命令语义无返回值则为 <see langword="null" />。</returns>
|
||||
public object? Execute() => _execute();
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
33
GFramework.Core/Cqrs/LegacyCqrsDispatchHandlerBase.cs
Normal file
33
GFramework.Core/Cqrs/LegacyCqrsDispatchHandlerBase.cs
Normal file
@ -0,0 +1,33 @@
|
||||
// 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>
|
||||
/// <param name="target">即将执行的 legacy 目标对象。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="target" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 目标对象实现了 <see cref="IContextAware" />,但当前 handler 还没有可用的架构上下文。
|
||||
/// </exception>
|
||||
protected void PrepareTarget(object target)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
116
GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs
Normal file
116
GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs
Normal file
@ -0,0 +1,116 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 为 legacy Core CQRS bridge 提供共享的上下文解析与同步兼容辅助逻辑。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 旧的同步 Command/Query 入口仍需要阻塞等待统一 <see cref="ICqrsRuntime" /> 返回结果。
|
||||
/// 这里统一通过 <see cref="Task.Run(System.Func{System.Threading.Tasks.Task})" /> 把等待动作切换到线程池,
|
||||
/// 避免直接占用调用方的 <see cref="SynchronizationContext" /> 导致 legacy 同步入口与异步 pipeline 互相卡死。
|
||||
/// </remarks>
|
||||
internal static class LegacyCqrsDispatchHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 解析当前 legacy 目标对象是否能够绑定到统一 CQRS runtime 的架构上下文。
|
||||
/// </summary>
|
||||
/// <param name="runtime">当前执行器可用的统一 CQRS runtime。</param>
|
||||
/// <param name="target">即将执行的 legacy 目标对象。</param>
|
||||
/// <param name="context">命中时返回可用于 CQRS runtime 的架构上下文。</param>
|
||||
/// <returns>
|
||||
/// 当 <paramref name="runtime" /> 可用且 <paramref name="target" /> 能稳定提供
|
||||
/// <see cref="IArchitectureContext" /> 时返回 <see langword="true" />;否则返回 <see langword="false" />。
|
||||
/// </returns>
|
||||
internal static bool TryResolveDispatchContext(
|
||||
[NotNullWhen(true)] ICqrsRuntime? runtime,
|
||||
object target,
|
||||
out IArchitectureContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
|
||||
context = null!;
|
||||
|
||||
if (runtime is null || target is not IContextAware contextAware)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
context = contextAware.GetContext();
|
||||
return true;
|
||||
}
|
||||
catch (InvalidOperationException exception) when (IsMissingContextException(exception))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断当前 <see cref="InvalidOperationException" /> 是否表示 legacy 目标尚未具备可桥接的架构上下文。
|
||||
/// </summary>
|
||||
/// <param name="exception">由 <see cref="IContextAware.GetContext" /> 抛出的异常。</param>
|
||||
/// <returns>
|
||||
/// 仅当异常明确表示“上下文尚未设置”或“当前没有活动上下文”时返回 <see langword="true" />;
|
||||
/// 其他运行时错误必须继续向上传播,避免把真实故障误判为可安全回退。
|
||||
/// </returns>
|
||||
private static bool IsMissingContextException(InvalidOperationException exception)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(exception);
|
||||
|
||||
return string.Equals(
|
||||
exception.Message,
|
||||
"Architecture context has not been set. Call SetContext before accessing the context.",
|
||||
StringComparison.Ordinal)
|
||||
|| string.Equals(
|
||||
exception.Message,
|
||||
"No active architecture context is currently bound.",
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同步等待统一 CQRS runtime 完成无返回值请求。
|
||||
/// </summary>
|
||||
/// <param name="runtime">负责分发当前请求的统一 CQRS runtime。</param>
|
||||
/// <param name="context">当前架构上下文。</param>
|
||||
/// <param name="request">要同步等待的请求。</param>
|
||||
internal static void SendSynchronously(
|
||||
ICqrsRuntime runtime,
|
||||
IArchitectureContext context,
|
||||
IRequest<Unit> request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
Task.Run(() => runtime.SendAsync(context, request).AsTask()).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同步等待统一 CQRS runtime 完成带返回值请求,并返回实际响应。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">请求响应类型。</typeparam>
|
||||
/// <param name="runtime">负责分发当前请求的统一 CQRS runtime。</param>
|
||||
/// <param name="context">当前架构上下文。</param>
|
||||
/// <param name="request">要同步等待的请求。</param>
|
||||
/// <returns>统一 CQRS runtime 返回的响应结果。</returns>
|
||||
internal static TResponse SendSynchronously<TResponse>(
|
||||
ICqrsRuntime runtime,
|
||||
IArchitectureContext context,
|
||||
IRequest<TResponse> request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
return Task.Run(() => runtime.SendAsync(context, request).AsTask()).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
16
GFramework.Core/Cqrs/LegacyCqrsDispatchRequestBase.cs
Normal file
16
GFramework.Core/Cqrs/LegacyCqrsDispatchRequestBase.cs
Normal file
@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
namespace GFramework.Core.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 为 legacy Command / Query 到自有 CQRS runtime 的桥接请求提供共享的目标对象封装。
|
||||
/// </summary>
|
||||
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 目标对象。</param>
|
||||
internal abstract class LegacyCqrsDispatchRequestBase(object target)
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前 bridge request 代理的 legacy 目标对象。
|
||||
/// </summary>
|
||||
public object Target { get; } = target ?? throw new ArgumentNullException(nameof(target));
|
||||
}
|
||||
23
GFramework.Core/Cqrs/LegacyQueryDispatchRequest.cs
Normal file
23
GFramework.Core/Cqrs/LegacyQueryDispatchRequest.cs
Normal 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 同步查询,使其能够通过自有 CQRS runtime 调度。
|
||||
/// </summary>
|
||||
/// <param name="target">需要在 bridge handler 中接收上下文注入的 legacy 查询目标实例。</param>
|
||||
/// <param name="execute">封装 legacy 查询执行逻辑并返回装箱结果的委托。</param>
|
||||
internal sealed class LegacyQueryDispatchRequest(object target, Func<object?> execute)
|
||||
: LegacyCqrsDispatchRequestBase(target), IRequest<object?>
|
||||
{
|
||||
private readonly Func<object?> _execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
|
||||
/// <summary>
|
||||
/// 执行底层 legacy 查询并返回装箱后的结果。
|
||||
/// </summary>
|
||||
/// <returns>底层 legacy 查询执行后的装箱结果;若查询无返回值则为 <see langword="null" />。</returns>
|
||||
public object? Execute() => _execute();
|
||||
}
|
||||
21
GFramework.Core/Cqrs/LegacyQueryDispatchRequestHandler.cs
Normal file
21
GFramework.Core/Cqrs/LegacyQueryDispatchRequestHandler.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
6
GFramework.Core/Properties/AssemblyInfo.cs
Normal file
6
GFramework.Core/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,6 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("GFramework.Core.Tests")]
|
||||
@ -2,14 +2,23 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Query;
|
||||
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 +27,38 @@ public sealed class AsyncQueryExecutor : IAsyncQueryExecutor
|
||||
/// <returns>包含查询结果的异步任务</returns>
|
||||
public Task<TResult> SendAsync<TResult>(IAsyncQuery<TResult> query)
|
||||
{
|
||||
// 验证查询参数不为空
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
var cqrsRuntime = _runtime;
|
||||
|
||||
if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, query, out var context))
|
||||
{
|
||||
return BridgeAsyncQueryAsync(cqrsRuntime, context, query);
|
||||
}
|
||||
|
||||
return query.DoAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过统一 CQRS runtime 异步执行 legacy 查询,并把装箱结果还原为目标类型。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">查询结果类型。</typeparam>
|
||||
/// <param name="runtime">负责调度当前 bridge request 的统一 CQRS runtime。</param>
|
||||
/// <param name="context">当前架构上下文。</param>
|
||||
/// <param name="query">要桥接的 legacy 查询。</param>
|
||||
/// <returns>查询执行结果。</returns>
|
||||
private static async Task<TResult> BridgeAsyncQueryAsync<TResult>(
|
||||
ICqrsRuntime runtime,
|
||||
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!;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Abstractions.Query;
|
||||
using GFramework.Core.Cqrs;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Query;
|
||||
|
||||
@ -10,21 +12,47 @@ 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<TResult> 接口。</param>
|
||||
/// <returns>查询执行的结果,类型为 TResult。</returns>
|
||||
/// <returns>查询执行成功后还原出的 <typeparamref name="TResult" /> 结果。</returns>
|
||||
/// <exception cref="NullReferenceException">
|
||||
/// 统一 CQRS runtime 返回 <see langword="null" />,但 <typeparamref name="TResult" /> 为值类型。
|
||||
/// </exception>
|
||||
/// <exception cref="InvalidCastException">
|
||||
/// 统一 CQRS runtime 返回的装箱结果无法转换为 <typeparamref name="TResult" />。
|
||||
/// </exception>
|
||||
public TResult Send<TResult>(IQuery<TResult> query)
|
||||
{
|
||||
// 验证查询参数不为 null,如果为 null 则抛出 ArgumentNullException 异常
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
// 调用查询对象的 Do 方法执行查询并返回结果
|
||||
var cqrsRuntime = _runtime;
|
||||
|
||||
if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, query, out var context))
|
||||
{
|
||||
var boxedResult = LegacyCqrsDispatchHelper.SendSynchronously(
|
||||
cqrsRuntime,
|
||||
context,
|
||||
new LegacyQueryDispatchRequest(
|
||||
query,
|
||||
() => query.Do()));
|
||||
return (TResult)boxedResult!;
|
||||
}
|
||||
|
||||
return query.Do();
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,9 @@
|
||||
- 资源、对象池、日志、协程、并发、环境、配置与本地化
|
||||
- 服务模块管理、时间提供器与默认的 IoC 容器适配
|
||||
|
||||
标准架构启动路径下,旧 `Command` / `Query` 兼容入口现在会继续保持原有使用方式,
|
||||
但底层会通过 `GFramework.Cqrs` 的统一 runtime、pipeline 与上下文注入链路执行。
|
||||
|
||||
它不负责:
|
||||
|
||||
- 游戏内容配置、Scene / UI / Storage 等游戏层能力
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -32,10 +33,19 @@ public sealed class AsyncQueryExecutorModule : IServiceModule
|
||||
/// 注册异步查询执行器到依赖注入容器。
|
||||
/// 创建异步查询执行器实例并将其注册为多例服务。
|
||||
/// </summary>
|
||||
/// <param name="container">依赖注入容器实例。</param>
|
||||
/// <param name="container">承载异步查询执行器与 CQRS runtime 的依赖注入容器实例。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 容器中尚未注册唯一的 <see cref="ICqrsRuntime" /> 实例,无法构建统一 runtime 版本的异步查询执行器。
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// 该模块会在注册阶段立即解析 <see cref="ICqrsRuntime" />,因此
|
||||
/// <see cref="CqrsRuntimeModule" /> 必须先于当前模块完成注册。
|
||||
/// </remarks>
|
||||
public void Register(IIocContainer container)
|
||||
{
|
||||
container.RegisterPlurality(new AsyncQueryExecutor());
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
container.RegisterPlurality(new AsyncQueryExecutor(container.GetRequired<ICqrsRuntime>()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -55,4 +65,4 @@ public sealed class AsyncQueryExecutorModule : IServiceModule
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -32,10 +33,19 @@ public sealed class CommandExecutorModule : IServiceModule
|
||||
/// 注册命令执行器到依赖注入容器。
|
||||
/// 创建命令执行器实例并将其注册为多例服务。
|
||||
/// </summary>
|
||||
/// <param name="container">依赖注入容器实例。</param>
|
||||
/// <param name="container">承载命令执行器与 CQRS runtime 的依赖注入容器实例。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 容器中尚未注册唯一的 <see cref="ICqrsRuntime" /> 实例,无法构建统一 runtime 版本的命令执行器。
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// 该模块会在注册阶段立即解析 <see cref="ICqrsRuntime" />,因此
|
||||
/// <see cref="CqrsRuntimeModule" /> 必须先于当前模块完成注册。
|
||||
/// </remarks>
|
||||
public void Register(IIocContainer container)
|
||||
{
|
||||
container.RegisterPlurality(new CommandExecutor());
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
container.RegisterPlurality(new CommandExecutor(container.GetRequired<ICqrsRuntime>()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -55,4 +65,4 @@ public sealed class CommandExecutorModule : IServiceModule
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -32,10 +33,19 @@ public sealed class QueryExecutorModule : IServiceModule
|
||||
/// 注册查询执行器到依赖注入容器。
|
||||
/// 创建查询执行器实例并将其注册为多例服务。
|
||||
/// </summary>
|
||||
/// <param name="container">依赖注入容器实例。</param>
|
||||
/// <param name="container">承载查询执行器与 CQRS runtime 的依赖注入容器实例。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 容器中尚未注册唯一的 <see cref="ICqrsRuntime" /> 实例,无法构建统一 runtime 版本的查询执行器。
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// 该模块会在注册阶段立即解析 <see cref="ICqrsRuntime" />,因此
|
||||
/// <see cref="CqrsRuntimeModule" /> 必须先于当前模块完成注册。
|
||||
/// </remarks>
|
||||
public void Register(IIocContainer container)
|
||||
{
|
||||
container.RegisterPlurality(new QueryExecutor());
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
container.RegisterPlurality(new QueryExecutor(container.GetRequired<ICqrsRuntime>()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -55,4 +65,4 @@ public sealed class QueryExecutorModule : IServiceModule
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,9 @@ public interface ICqrsRuntime
|
||||
/// <remarks>
|
||||
/// 该契约允许调用方传入任意 <see cref="ICqrsContext" />,
|
||||
/// 但默认运行时在需要向处理器或行为注入框架上下文时,仍要求该上下文同时实现 <c>IArchitectureContext</c>。
|
||||
/// 为了兼容 legacy 同步入口,<c>ArchitectureContext</c>、<c>QueryExecutor</c> 与 <c>CommandExecutor</c>
|
||||
/// 可能会在后台线程上同步等待该异步结果;实现者与 pipeline 行为不应依赖调用方的
|
||||
/// <see cref="SynchronizationContext" />,并应优先在内部异步链路上使用 <c>ConfigureAwait(false)</c>。
|
||||
/// </remarks>
|
||||
ValueTask<TResponse> SendAsync<TResponse>(
|
||||
ICqrsContext context,
|
||||
|
||||
@ -0,0 +1,221 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Columns;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Diagnosers;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using BenchmarkDotNet.Order;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Ioc;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 对比 request steady-state dispatch 在不同 handler 生命周期下的额外开销。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 当前矩阵只覆盖 `Singleton` 与 `Transient`。
|
||||
/// `Scoped` 在两个 runtime 中都依赖显式作用域边界,而当前 benchmark 宿主故意保持“单根容器最小宿主”模型,
|
||||
/// 直接把 scoped 解析压到根作用域会让对照语义失真,因此留到未来有真实 scoped host 基线时再扩展。
|
||||
/// </remarks>
|
||||
[Config(typeof(Config))]
|
||||
public class RequestLifetimeBenchmarks
|
||||
{
|
||||
private MicrosoftDiContainer _container = null!;
|
||||
private ICqrsRuntime _runtime = null!;
|
||||
private ServiceProvider _serviceProvider = null!;
|
||||
private IMediator _mediatr = null!;
|
||||
private BenchmarkRequestHandler _baselineHandler = null!;
|
||||
private BenchmarkRequest _request = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 控制当前 benchmark 使用的 handler 生命周期。
|
||||
/// </summary>
|
||||
[Params(HandlerLifetime.Singleton, HandlerLifetime.Transient)]
|
||||
public HandlerLifetime Lifetime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 可公平比较的 benchmark handler 生命周期集合。
|
||||
/// </summary>
|
||||
public enum HandlerLifetime
|
||||
{
|
||||
/// <summary>
|
||||
/// 复用单个 handler 实例。
|
||||
/// </summary>
|
||||
Singleton,
|
||||
|
||||
/// <summary>
|
||||
/// 每次分发都重新解析新的 handler 实例。
|
||||
/// </summary>
|
||||
Transient
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 request lifetime benchmark 的公共输出格式。
|
||||
/// </summary>
|
||||
private sealed class Config : ManualConfig
|
||||
{
|
||||
public Config()
|
||||
{
|
||||
AddJob(Job.Default);
|
||||
AddColumnProvider(DefaultColumnProviders.Instance);
|
||||
AddColumn(new CustomColumn("Scenario", static (_, _) => "RequestLifetime"));
|
||||
AddDiagnoser(MemoryDiagnoser.Default);
|
||||
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建当前生命周期下的 GFramework 与 MediatR request 对照宿主。
|
||||
/// </summary>
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Fatal
|
||||
};
|
||||
Fixture.Setup($"RequestLifetime/{Lifetime}", handlerCount: 1, pipelineCount: 0);
|
||||
|
||||
_baselineHandler = new BenchmarkRequestHandler();
|
||||
_request = new BenchmarkRequest(Guid.NewGuid());
|
||||
|
||||
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
RegisterGFrameworkHandler(container, Lifetime);
|
||||
});
|
||||
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_container,
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestLifetimeBenchmarks) + "." + Lifetime));
|
||||
|
||||
_serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
configure: null,
|
||||
typeof(RequestLifetimeBenchmarks),
|
||||
static candidateType => candidateType == typeof(BenchmarkRequestHandler),
|
||||
ResolveMediatRLifetime(Lifetime));
|
||||
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放当前生命周期矩阵持有的 benchmark 宿主资源。
|
||||
/// </summary>
|
||||
[GlobalCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接调用 handler,作为不同生命周期矩阵下的 dispatch 额外开销 baseline。
|
||||
/// </summary>
|
||||
[Benchmark(Baseline = true)]
|
||||
public ValueTask<BenchmarkResponse> SendRequest_Baseline()
|
||||
{
|
||||
return _baselineHandler.Handle(_request, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 GFramework.CQRS runtime 发送 request。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public ValueTask<BenchmarkResponse> SendRequest_GFrameworkCqrs()
|
||||
{
|
||||
return _runtime.SendAsync(BenchmarkContext.Instance, _request, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 MediatR 发送 request,作为外部对照。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public Task<BenchmarkResponse> SendRequest_MediatR()
|
||||
{
|
||||
return _mediatr.Send(_request, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按生命周期把 benchmark request handler 注册到 GFramework 容器。
|
||||
/// </summary>
|
||||
/// <param name="container">当前 benchmark 拥有并负责释放的容器。</param>
|
||||
/// <param name="lifetime">待比较的 handler 生命周期。</param>
|
||||
private static void RegisterGFrameworkHandler(MicrosoftDiContainer container, HandlerLifetime lifetime)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
switch (lifetime)
|
||||
{
|
||||
case HandlerLifetime.Singleton:
|
||||
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>, BenchmarkRequestHandler>();
|
||||
return;
|
||||
|
||||
case HandlerLifetime.Transient:
|
||||
container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>, BenchmarkRequestHandler>();
|
||||
return;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 benchmark 生命周期映射为 MediatR 组装所需的 <see cref="ServiceLifetime" />。
|
||||
/// </summary>
|
||||
/// <param name="lifetime">待比较的 handler 生命周期。</param>
|
||||
private static ServiceLifetime ResolveMediatRLifetime(HandlerLifetime lifetime)
|
||||
{
|
||||
return lifetime switch
|
||||
{
|
||||
HandlerLifetime.Singleton => ServiceLifetime.Singleton,
|
||||
HandlerLifetime.Transient => ServiceLifetime.Transient,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark request。
|
||||
/// </summary>
|
||||
/// <param name="Id">请求标识。</param>
|
||||
public sealed record BenchmarkRequest(Guid Id) :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IRequest<BenchmarkResponse>,
|
||||
MediatR.IRequest<BenchmarkResponse>;
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark response。
|
||||
/// </summary>
|
||||
/// <param name="Id">响应标识。</param>
|
||||
public sealed record BenchmarkResponse(Guid Id);
|
||||
|
||||
/// <summary>
|
||||
/// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 request handler。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkRequestHandler :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>,
|
||||
MediatR.IRequestHandler<BenchmarkRequest, BenchmarkResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 GFramework.CQRS request。
|
||||
/// </summary>
|
||||
public ValueTask<BenchmarkResponse> Handle(BenchmarkRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return ValueTask.FromResult(new BenchmarkResponse(request.Id));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 MediatR request。
|
||||
/// </summary>
|
||||
Task<BenchmarkResponse> MediatR.IRequestHandler<BenchmarkRequest, BenchmarkResponse>.Handle(
|
||||
BenchmarkRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(new BenchmarkResponse(request.Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -16,6 +16,8 @@
|
||||
- 运行前输出并校验场景配置
|
||||
- `Messaging/RequestBenchmarks.cs`
|
||||
- direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
|
||||
- `Messaging/RequestLifetimeBenchmarks.cs`
|
||||
- `Singleton / Transient` 两类 handler 生命周期下,direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
|
||||
- `Messaging/RequestPipelineBenchmarks.cs`
|
||||
- `0 / 1 / 4` 个 pipeline 行为下,direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
|
||||
- `Messaging/RequestStartupBenchmarks.cs`
|
||||
@ -39,7 +41,7 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro
|
||||
|
||||
## 后续扩展方向
|
||||
|
||||
- generated invoker provider 与纯反射 dispatch 对比
|
||||
- generated stream invoker provider 与纯反射建流对比
|
||||
- registration / service lifetime 矩阵
|
||||
- request / stream 的真实 source-generator 产物与 handwritten generated provider 对照
|
||||
- stream handler 生命周期矩阵
|
||||
- 带真实显式作用域边界的 scoped host 对照
|
||||
- generated invoker provider 与纯反射 dispatch / 建流对比继续扩展到更多场景
|
||||
|
||||
@ -7,9 +7,9 @@ CQRS 迁移与收敛。
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`CQRS-REWRITE-RP-091`
|
||||
- 恢复点编号:`CQRS-REWRITE-RP-098`
|
||||
- 当前阶段:`Phase 8`
|
||||
- 当前 PR 锚点:`PR #331`
|
||||
- 当前 PR 锚点:`PR #334`
|
||||
- 当前结论:
|
||||
- `GFramework.Cqrs` 已完成对外部 `Mediator` 的生产级替代,当前主线已从“是否可替代”转向“仓库内部收口与能力深化顺序”
|
||||
- `dispatch/invoker` 生成前移已扩展到 request / stream 路径,`RP-077` 已补齐 request invoker provider gate 与 stream gate 对称的 descriptor / descriptor entry runtime 合同回归
|
||||
@ -27,28 +27,58 @@ 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 校验脚本
|
||||
- `ai-plan` active 入口现以 `RP-091` 为最新恢复锚点;`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` 未吸收能力差距复核
|
||||
- `RP-094` 已按 `PR #334` latest-head review 收口 legacy bridge 的测试注册方式、模块运行时依赖契约、异步取消语义、XML 文档缺口与兼容文档回退边界
|
||||
- `RP-095` 已继续收口 `PR #334` 剩余 review:把 legacy 同步 bridge 的阻塞等待统一切到线程池隔离 helper、补齐 `ArchitectureContext` / executor 共享 dispatch helper、修正 bridge fixture 的并行与容器释放约束,并为 runtime bridge 与 async void command cancellation 增补回归测试
|
||||
- `RP-096` 已再次使用 `$gframework-pr-review` 复核 `PR #334` latest-head review,确认仍显示为 open 的 AI threads 在本地代码中已无新增仍成立的运行时 / 测试 / 文档缺陷,剩余差异主要是 GitHub thread 未 resolve 的状态滞后
|
||||
- `RP-097` 已继续收口 `PR #334` latest-head nitpick:为 `AsyncQueryExecutorTests` / `CommandExecutorTests` 补齐可观察的上下文保留断言,并让 `RecordingCqrsRuntime` 在测试替身返回错误响应类型时抛出带请求/类型信息的诊断异常
|
||||
- 当前 `RP-098` 已再次使用 `$gframework-pr-review` 复核 `PR #334` latest-head review,并收口 `LegacyCqrsDispatchHelper.TryResolveDispatchContext(...)` 过宽吞掉 `InvalidOperationException` 的真实运行时诊断退化问题;现在仅把“上下文尚未就绪”视为允许 fallback 的信号,并为 fallback / 异常冒泡分别补齐回归测试
|
||||
- `ai-plan` active 入口现以 `RP-098` 为最新恢复锚点;`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
|
||||
|
||||
## 当前活跃事实
|
||||
|
||||
- 当前分支为 `fix/package-validation-guard`
|
||||
- 当前分支为 `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 时直接执行”的回退路径,用于不依赖容器的隔离单元测试
|
||||
- `LegacyCqrsDispatchHelper` 现统一负责 runtime dispatch context 解析,以及 legacy 同步 bridge 对 `ICqrsRuntime.SendAsync(...)` 的线程池隔离等待
|
||||
- `ArchitectureContext`、`CommandExecutor`、`QueryExecutor` 的同步 CQRS/legacy bridge 入口不再直接在调用线程上阻塞 `SendAsync(...).GetAwaiter().GetResult()`
|
||||
- `GFramework.Core.Tests` 现通过 `InternalsVisibleTo("GFramework.Core.Tests")` 直接实例化内部 bridge handler,不再依赖字符串反射装配测试桥接注册
|
||||
- 使用 `LegacyBridgePipelineTracker` 的 `ArchitectureContextTests` 与 `ArchitectureModulesBehaviorTests` 现都显式标记为 `NonParallelizable`
|
||||
- `ArchitectureContextTests.CreateFrozenBridgeContext(...)` 现把冻结容器所有权显式交回调用方,并在每个 bridge 用例的 `finally` 中释放
|
||||
- `CommandExecutorModule`、`QueryExecutorModule`、`AsyncQueryExecutorModule` 现改为 `GetRequired<ICqrsRuntime>()` 并在 XML 文档里显式声明注册顺序契约,避免 runtime 缺失时静默回退
|
||||
- `LegacyAsyncQueryDispatchRequestHandler`、`LegacyAsyncCommandResultDispatchRequestHandler`、`LegacyAsyncCommandDispatchRequestHandler` 现都通过 `ThrowIfCancellationRequested()` + `WaitAsync(cancellationToken)` 显式保留调用方取消可见性
|
||||
- 相对 `ai-libs/Mediator`,当前仍未完全吸收的能力集中在六类:facade 公开入口、telemetry、stream pipeline、notification publisher 策略、生成器配置与诊断、生命周期/缓存公开配置面
|
||||
- 发布工作流已有 packed modules 校验,但 PR 工作流此前没有等价的 solution pack 产物名单校验
|
||||
- 本地 `dotnet pack GFramework.sln -c Release --no-restore -o <temp-dir>` 当前只产出 14 个预期包,未复现 benchmark `.nupkg`
|
||||
- latest-head review 现仍有少量 open thread,但本地复核后,仍成立的问题已收敛到 benchmark 对照公平性、workflow 输入安全性与 active 文档压缩
|
||||
- `PR #334` 在 `2026-05-07` 的 latest-head review 当前显示 `CodeRabbit 10` / `Greptile 5` 个 open thread;本轮再次复核后确认其中大部分仍是已实质修复但未 resolve 的 stale thread,仅 `LegacyCqrsDispatchHelper.TryResolveDispatchContext(...)` 的异常边界仍需要继续收口
|
||||
- benchmark 场景现统一通过 `BenchmarkHostFactory` 构建最小宿主:GFramework 侧在 runtime 分发前显式 `Freeze()` 容器,MediatR 侧只扫描当前场景需要的 handler / behavior 类型
|
||||
- `RequestStartupBenchmarks` 已恢复 `ColdStart_GFrameworkCqrs` 结果产出,不再命中 `No CQRS request handler registered`
|
||||
- `BenchmarkDotNet` 在当前 agent 沙箱里会因自动生成的 bootstrap 脚本异常失败;同一 `dotnet run --no-build` 命令在沙箱外执行通过,因此本轮以沙箱外结果作为 benchmark 权威验证
|
||||
- 已新增手动触发的 benchmark workflow;默认只验证 benchmark 项目 Release build,只有显式提供过滤器时才执行 BenchmarkDotNet 运行;过滤器输入现通过环境变量传入 shell,避免 workflow_dispatch 输入直接插值到命令行
|
||||
- 远端 `CTRF` 最新汇总为 `2274/2274` passed
|
||||
- 远端 `CTRF` 最新汇总为 `2311/2311` passed(run `#1079`, 2026-05-07)
|
||||
- `MegaLinter` 当前只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,尚未提供本地仍成立的文件级格式诊断
|
||||
- `LegacyCqrsDispatchHelper.TryResolveDispatchContext(...)` 现在只会把“Context 尚未设置”或“当前没有活动上下文”识别为可安全 fallback 的缺上下文信号;其他 `InvalidOperationException` 将继续向上传播,避免把真实运行时故障误判成 legacy 直执行场景
|
||||
- `CommandExecutorTests` 已新增“缺上下文继续 fallback”和“意外 `InvalidOperationException` 必须冒泡”的回归,防止后续再次放宽该异常过滤面
|
||||
- `PR #334` 当前 latest-head open AI feedback 经过本轮本地复核与修复后,应主要剩余待 GitHub 重新索引的状态差异或已实质关闭但未 resolve 的 thread
|
||||
- `GFramework.Core.Tests` 中 legacy bridge 的“保留上下文”回归现在同时断言 bridge request 类型与目标对象执行期观察到的 `IArchitectureContext`
|
||||
- `RecordingCqrsRuntime` 对非 `Unit` 响应已显式校验返回值类型;若测试工厂返回了 `null` 或错误装箱类型,异常会直接指出 request 类型与期望/实际响应类型
|
||||
|
||||
## 当前风险
|
||||
|
||||
- 顶层 `GFramework.sln` / `GFramework.csproj` 在 WSL 下仍可能受 Windows NuGet fallback 配置影响,完整 solution 级验证成本高于模块级验证
|
||||
- 若后续新增 benchmark / example / tooling 项目但未同步校验发布面,solution 级 `dotnet pack` 仍可能在 tag 发布前才暴露异常包
|
||||
- `RequestStartupBenchmarks` 为了量化真正的单次 cold-start,引入了 `InvocationCount=1` / `UnrollFactor=1` 的专用 job;该配置会触发 BenchmarkDotNet 的 `MinIterationTime` 提示,后续若要做稳定基线比较,还需要决定是否引入批量外层循环或自定义 cold-start harness
|
||||
- 当前 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 当成“全部收口完成”
|
||||
- `LegacyBridgePipelineTracker` 仍是进程级静态测试辅助;虽然现在已在相关 fixture 清理阶段重置并补充线程安全说明,但若将来扩大并行 bridge fixture 数量,仍要继续控制共享状态扩散
|
||||
|
||||
## 最近权威验证
|
||||
|
||||
@ -68,6 +98,32 @@ CQRS 迁移与收敛。
|
||||
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- 备注:用于验证本轮 request invoker / pipeline / stream invoker 调整与 benchmark workflow 改动后的 Release 编译结果
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json`
|
||||
- 结果:通过
|
||||
- 备注:确认当前分支对应 `PR #334`;`CodeRabbit` latest review 已 `APPROVED`,但 latest-head 仍显示 `10` 个 open thread、`Greptile` 仍显示 `3` 个 open thread;本地逐项复核后未发现新的仍成立缺陷,最新 CI 测试汇总为 `2311/2311` passed,`MegaLinter` 仅剩 `dotnet-format` restore 环境噪音
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests"`
|
||||
- 结果:通过,`19/19` passed
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
|
||||
- 结果:通过
|
||||
- 备注:确认当前分支对应 `PR #334`;最新 review 仍为 `CodeRabbit APPROVED (2026-05-07T12:20:24Z)`,latest-head 显示 `CodeRabbit 10` / `Greptile 5` open thread;本轮接受并修复的仍成立问题收敛到 `LegacyCqrsDispatchHelper.TryResolveDispatchContext(...)` 的过宽异常吞掉逻辑
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests"`
|
||||
- 结果:通过,`25/25` passed
|
||||
- `python3 scripts/license-header.py --check`
|
||||
- 结果:通过
|
||||
- `git diff --check`
|
||||
- 结果:通过
|
||||
- `python3 scripts/license-header.py --check`
|
||||
- 结果:通过
|
||||
- `git diff --check`
|
||||
- 结果:通过
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output <temporary-json-output>`
|
||||
- 结果:通过
|
||||
- 备注:确认当前分支对应 `PR #331`,本轮 latest-head open AI feedback 已收敛到 `dotnet pack --no-build`、共享包校验脚本跨平台兼容性与 active 文档 PR 锚点同步
|
||||
@ -76,12 +132,52 @@ CQRS 迁移与收敛。
|
||||
- 备注:当前 WSL worktree 需要显式绑定 `GIT_DIR` / `GIT_WORK_TREE` 后运行
|
||||
- `git diff --check`
|
||||
- 结果:通过
|
||||
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
|
||||
- 结果:通过(以沙箱外 `--no-build` 权威结果为准)
|
||||
- 备注:`Singleton` 下 baseline / MediatR / GFramework 均值约 `5.633 ns / 58.687 ns / 301.731 ns`;`Transient` 下约 `5.044 ns / 52.274 ns / 287.863 ns`
|
||||
- `python3 scripts/license-header.py --check`
|
||||
- 结果:通过
|
||||
- 备注:当前 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`
|
||||
- 结果:通过
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
|
||||
- 结果:通过
|
||||
- 备注:确认当前分支对应 `PR #334`;仍有效的 latest-head review 已收敛到 legacy bridge 测试装配、运行时依赖契约、异步取消、XML 文档与兼容文档边界
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- 备注:修复新增 XML 文档 warning 后复跑,当前 `GFramework.Core` 三个 target framework 均已干净通过
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests|FullyQualifiedName~ArchitectureModulesBehaviorTests|FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests"`
|
||||
- 结果:通过,`48/48` passed
|
||||
- 备注:覆盖 legacy bridge 兼容入口、测试装配、执行器 runtime fallback 与相关模块行为
|
||||
- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
|
||||
- 结果:通过
|
||||
- `git diff --check`
|
||||
- 结果:通过
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests|FullyQualifiedName~ArchitectureModulesBehaviorTests|FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests|FullyQualifiedName~LegacyAsyncCommandDispatchRequestHandlerTests"`
|
||||
- 结果:通过,`54/54` passed
|
||||
- 备注:覆盖 legacy 同步 bridge 的同步上下文隔离、bridge fixture 容器释放,以及 async void command cancellation 可见性
|
||||
- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
|
||||
- 结果:通过
|
||||
|
||||
## 下一推荐步骤
|
||||
|
||||
1. 运行 `dotnet pack` 与新的 `scripts/validate-packed-modules.sh`,确认本轮共享校验脚本与 PR workflow 步骤在本地一致通过
|
||||
2. 运行受影响的 Release build / 头部校验,确认 workflow 与脚本改动未引入新的命名、文件头或 shell 语法问题
|
||||
3. 创建修复 PR 时,将重点放在“发布面保护前移到 PR”而不是“扩充 expected package 列表”
|
||||
1. 在 GitHub 上 resolve / reply 已被当前分支实质吸收的 `PR #334` stale review threads,尤其是仍停留在旧 head 上的 CodeRabbit / Greptile open thread;若 head 更新后线程数量继续变化,再用 `$gframework-pr-review` 复核
|
||||
2. 若继续沿用 `$gframework-batch-boot 50` 且优先处理 `Mediator` 能力吸收,下一批建议从 `stream pipeline` 或 `notification publisher` 策略中选择一个独立切片推进
|
||||
3. 若继续收敛 legacy Core CQRS,可评估是否补一个 `IMediator` 风格 facade,而不是继续扩大 `ArchitectureContext` 兼容入口的职责
|
||||
|
||||
## 活跃文档
|
||||
|
||||
|
||||
@ -1,5 +1,216 @@
|
||||
# CQRS 重写迁移追踪
|
||||
|
||||
## 2026-05-07
|
||||
|
||||
### 阶段:PR #334 latest-head helper 异常边界收口(CQRS-REWRITE-RP-098)
|
||||
|
||||
- 再次使用 `$gframework-pr-review` 抓取 `feat/cqrs-optimization` 对应的 `PR #334` latest-head review,并重新核对 `/tmp/current-pr-review.json` 中最新 open thread:
|
||||
- 当前公开 PR 仍为 `PR #334`
|
||||
- `CodeRabbit` 最新 review 在 `2026-05-07T12:20:24Z` 为 `APPROVED`
|
||||
- latest-head 当前显示 `CodeRabbit 10` / `Greptile 5` 个 open thread
|
||||
- 本轮逐条回到本地代码后,确认大多数 open thread 仍是 stale 状态;唯一继续成立的问题集中在 `LegacyCqrsDispatchHelper.TryResolveDispatchContext(...)`:
|
||||
- 该 helper 之前会把 `IContextAware.GetContext()` 抛出的任意 `InvalidOperationException` 都吞掉并回退到 legacy 直执行
|
||||
- 这会把真实运行时故障误判为“上下文未就绪”,导致 bridge 路径悄悄绕过统一 runtime,退化为难以诊断的行为差异
|
||||
- 本轮主线程决策:
|
||||
- 将异常过滤收窄为只接受两类缺上下文信号:`Architecture context has not been set...` 与 `No active architecture context is currently bound.`
|
||||
- 其他 `InvalidOperationException` 一律继续向上传播,避免掩盖容器、生命周期或自定义 `GetContext()` 内的真实错误
|
||||
- 在 `CommandExecutorTests` 中新增两条回归:一条验证缺上下文时仍会 fallback 到 legacy 直执行;一条验证意外 `InvalidOperationException` 不会被 bridge 逻辑静默吞掉
|
||||
- 同步刷新 `cqrs-rewrite` active tracking,把本轮修复记录为新的恢复锚点 `RP-098`
|
||||
- 本轮权威验证:
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
|
||||
- 结果:通过
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests"`
|
||||
- 结果:通过,`25/25` passed
|
||||
- `python3 scripts/license-header.py --check`
|
||||
- 结果:通过
|
||||
- `git diff --check`
|
||||
- 结果:通过
|
||||
|
||||
### 阶段:PR #334 nitpick 测试收尾(CQRS-REWRITE-RP-097)
|
||||
|
||||
- 继续处理 `PR #334` latest-head review 中仍值得本地吸收的轻量 nitpick,范围限定在 legacy bridge 测试可观察性与测试替身诊断质量:
|
||||
- `AsyncQueryExecutorTests.SendAsync_Should_Bridge_Through_Runtime_And_Preserve_Context` 标题声明“保留上下文”,但此前只断言了返回值与 bridge request 类型
|
||||
- `CommandExecutorTests.Send_WithResult_Should_Bridge_Through_Runtime_And_Preserve_Context` 同样缺少可观察的上下文注入断言
|
||||
- `RecordingCqrsRuntime` 直接强转响应对象,若测试工厂回错类型,失败信息不够聚焦
|
||||
- 本轮主线程决策:
|
||||
- 为两个 “Preserve_Context” 用例补齐 `ObservedContext` 与 `expectedContext` 的同一实例断言,使测试标题、注释与断言对象保持一致
|
||||
- 让 `RecordingCqrsRuntime` 通过私有 helper 显式执行响应类型还原;当工厂返回 `null` 或错误装箱类型时,抛出包含 request 类型与期望/实际响应类型的 `InvalidOperationException`
|
||||
- 同步刷新 `cqrs-rewrite` active tracking,把本轮 nitpick 收敛与验证结果记录为新的恢复锚点 `RP-097`
|
||||
- 本轮权威验证:
|
||||
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests"`
|
||||
- 结果:通过,`19/19` passed
|
||||
- `python3 scripts/license-header.py --check`
|
||||
- 结果:通过
|
||||
- `git diff --check`
|
||||
- 结果:通过
|
||||
|
||||
### 阶段:PR #334 latest-head review 复核(CQRS-REWRITE-RP-096)
|
||||
|
||||
- 再次使用 `$gframework-pr-review` 抓取 `feat/cqrs-optimization` 对应的 `PR #334` latest-head review,并读取 `/tmp/current-pr-review.json` 中的 `review_agents`、`latest_commit_review`、`megalinter_report` 与 `test_reports`
|
||||
- 本轮复核结论:
|
||||
- 当前公开 PR 为 `PR #334`,head commit 为 `dc3bd3744e2ceaa557ef03bc991fc88daedb460b`
|
||||
- `CodeRabbit` latest review 在 `2026-05-07T11:46:42Z` 已是 `APPROVED`,但 latest-head 仍显示 `10` 个 open thread;`Greptile` 仍显示 `3` 个 open thread
|
||||
- 逐条回到本地代码后,相关修复已在当前分支落地:`ArchitectureBootstrapper` 已自动扫描 `typeof(ArchitectureContext).Assembly`;`ArchitectureContextTests` / `ArchitectureModulesBehaviorTests` 已标注 `NonParallelizable` 并保证资源释放;`LegacyAsync*DispatchRequestHandler` 已统一补 `ThrowIfCancellationRequested()` + `WaitAsync(cancellationToken)`;`QueryExecutor` / legacy bridge request XML 文档与 `docs/zh-CN/core/command.md` fallback 说明也已齐备
|
||||
- 远端 CTRF 最新测试汇总为 `2311/2311 passed`(run `#1079`),`MegaLinter` 仅剩 `dotnet-format` restore failed 的环境噪音,没有新的文件级诊断
|
||||
- 主线程决策:
|
||||
- 不再为这些 stale open thread 追加新的本地代码改动,避免重复修补已吸收的问题
|
||||
- 仅更新 `cqrs-rewrite` active tracking/trace,把“当前剩余差异主要是 GitHub thread 状态滞后”记录为最新权威事实
|
||||
- 本轮权威验证:
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json`
|
||||
- 结果:通过
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
|
||||
### 阶段:PR #334 legacy bridge sync follow-up(CQRS-REWRITE-RP-095)
|
||||
|
||||
- 再次使用 `$gframework-pr-review` 抓取 `feat/cqrs-optimization` 对应的 `PR #334` latest-head review,并只保留本地复核后仍成立的问题:
|
||||
- `QueryExecutor` / `CommandExecutor` 新增的同步 bridge 仍直接阻塞 `ICqrsRuntime.SendAsync(...)`,在调用方存在 `SynchronizationContext` 时容易放大 sync-over-async 死锁面
|
||||
- `QueryExecutor` / `CommandExecutor` / `AsyncQueryExecutor` 各自保留一份相同的 dispatch-context 解析逻辑,仍有漂移风险
|
||||
- `ArchitectureContextTests` 的 bridge fixture 依然共享静态 tracker 且未显式声明非并行;冻结容器所有权也未交还给调用方释放
|
||||
- `LegacyAsyncCommandDispatchRequestHandler` 仍未沿用另两个 async bridge handler 的取消可见性模式
|
||||
- 本轮主线程决策:
|
||||
- 新增 `GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs`,统一收口 legacy bridge 的 dispatch-context 解析,以及同步 bridge 对 `ICqrsRuntime.SendAsync(...)` 的线程池隔离等待
|
||||
- 将 `QueryExecutor`、`CommandExecutor`、`AsyncQueryExecutor` 的重复 helper 改为复用共享 helper,并把 `ArchitectureContext` 的同步 CQRS 包装入口一并切换到同一阻塞策略,避免留下半修状态
|
||||
- 为 `ICqrsRuntime.SendAsync(...)` 补充 `<remarks>`,显式说明 legacy 同步入口会在后台线程上等待该异步契约,处理链路不应依赖调用方 `SynchronizationContext`
|
||||
- 把 `ArchitectureContextTests`、`ArchitectureModulesBehaviorTests` 标记为 `NonParallelizable`,并让 `CreateFrozenBridgeContext(...)` 把冻结容器通过 `out` 参数返还给每个测试在 `finally` 中释放
|
||||
- 为 `LegacyAsyncCommandDispatchRequestHandler` 增补 `ThrowIfCancellationRequested()` + `WaitAsync(cancellationToken)`,与另外两个 async bridge handler 保持一致
|
||||
- 新增回归测试覆盖同步 bridge 的 `SynchronizationContext` 隔离、legacy async command handler 的取消语义,以及 async/sync bridge request 的 request-type 命中
|
||||
- 本轮权威验证:
|
||||
- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --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~ArchitectureModulesBehaviorTests|FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests|FullyQualifiedName~LegacyAsyncCommandDispatchRequestHandlerTests"`
|
||||
- 结果:通过,`54/54` passed
|
||||
|
||||
### 阶段:PR #334 legacy bridge / 文档 review 收尾(CQRS-REWRITE-RP-094)
|
||||
|
||||
- 使用 `$gframework-pr-review` 抓取当前分支公开 PR,确认 `feat/cqrs-optimization` 当前对应 `PR #334`
|
||||
- latest-head open AI review 复核后,主线程接受并执行的修复集中在六类:
|
||||
- `GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs` 通过字符串字面量反射实例化内部 bridge handler,维护成本高且不利于 rename-safe 重构
|
||||
- `ArchitectureModulesBehaviorTests` 在断言失败路径下未保证 `DestroyAsync()` 执行,且 `TearDown` 未重置 `LegacyBridgePipelineTracker`
|
||||
- `LegacyBridgePipelineTracker` 以静态共享计数器记录 bridge pipeline 命中,但未文档化线程安全语义,且用字符串匹配类型名识别 bridge request
|
||||
- `LegacyAsyncQueryDispatchRequestHandler` / `LegacyAsyncCommandResultDispatchRequestHandler` 丢弃了 runtime 传入的 `CancellationToken`
|
||||
- `CommandExecutorModule` / `QueryExecutorModule` / `AsyncQueryExecutorModule` 依赖 `container.Get<ICqrsRuntime>()` 的隐式注册顺序,但此前既未显式失败,也未写进 API 契约
|
||||
- 多个 legacy bridge request / docs 页面仍缺 XML 文档或回退边界说明
|
||||
- 本轮主线程决策:
|
||||
- 为 `GFramework.Core` 新增 `Properties/AssemblyInfo.cs`,用 `InternalsVisibleTo("GFramework.Core.Tests")` 让测试直接实例化内部 handler
|
||||
- 把 `ArchitectureContextTests.RegisterLegacyBridgeHandlers` 改成显式构造 6 个 handler,移除字符串反射装配
|
||||
- 为 bridge 相关测试补 `TearDown` 清理和 `try/finally` 销毁,减少失败路径资源泄露
|
||||
- 为 `LegacyBridgePipelineTracker` 增补 `<remarks>`,并改用 `typeof(LegacyCqrsDispatchRequestBase).IsAssignableFrom(requestType)` 识别 bridge request
|
||||
- 为 `LegacyAsyncQueryDispatchRequestHandler` / `LegacyAsyncCommandResultDispatchRequestHandler` 加入预取消检查与 `WaitAsync(cancellationToken)`
|
||||
- 将三个 executor module 改为 `GetRequired<ICqrsRuntime>()`,同时在 XML 文档中显式声明 `CqrsRuntimeModule` 的前置注册约束
|
||||
- 为 `CommandExecutor` / `QueryExecutor` / `AsyncQueryExecutor` 的 dispatch-context helper 增加 `[MemberNotNullWhen]`,收敛重复 `_runtime is not null` 判空与 null-forgiving
|
||||
- 补齐 legacy bridge request / handler 的 XML 文档,以及 `docs/zh-CN/core/command.md`、`context.md` 的 fallback 边界说明
|
||||
- 本轮没有跟进的 thread:
|
||||
- `GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.cs` 的 `sealed` 建议属于低价值性能/风格提示,不影响 `PR #334` 的行为正确性
|
||||
- 若 review 在 GitHub 重新索引前仍显示旧 thread,下一轮以最新 head commit 再次抓取为准,不在本地重复造改动
|
||||
- 本轮权威验证:
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests|FullyQualifiedName~ArchitectureModulesBehaviorTests|FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests"`
|
||||
- 结果:通过,`48/48` passed
|
||||
- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
|
||||
- 结果:通过
|
||||
- `git diff --check`
|
||||
- 结果:通过
|
||||
|
||||
### 阶段:legacy Core CQRS -> GFramework.Cqrs 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:
|
||||
- `origin/main` = `2c58d8b6`,提交时间 `2026-05-07 13:24:46 +0800`
|
||||
- 本地 `main` = `c2d22285`,已落后于 remote-tracking ref,因此不作为本轮 batch baseline
|
||||
- 当前 `feat/cqrs-optimization` 相对 `origin/main` 的累计 branch diff 在开工前为 `0 files / 0 lines`
|
||||
- 本轮批次目标:继续推进 `GFramework.Cqrs.Benchmarks`,补一个独立、低风险、可单项目 Release 验证的 request 生命周期对照切片
|
||||
- 主线程先复核现有 benchmark 宿主与 runtime 解析路径后确认:
|
||||
- `RequestBenchmarks` 与 `StreamingBenchmarks` 当前都固定使用单根容器宿主
|
||||
- `MicrosoftDiContainer` 虽支持 `RegisterScoped` / `CreateScope()`,但当前 `CqrsDispatcher` 的 steady-state benchmark 路径直接从根容器解析 handler
|
||||
- 因此若直接把 `Scoped` 注册加入现有 benchmark,会把“根作用域下的 scoped 解析”误当成公平对照,语义不成立
|
||||
- 本轮决策:
|
||||
- 新增 `Messaging/RequestLifetimeBenchmarks.cs`
|
||||
- 生命周期矩阵只覆盖 `Singleton / Transient`
|
||||
- 在 XML 文档与 README 中显式注明:`Scoped` 需要等未来具备真实显式作用域边界的 benchmark host 后再比较
|
||||
- 已修改:
|
||||
- `GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.cs`
|
||||
- `GFramework.Cqrs.Benchmarks/README.md`
|
||||
- `ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md`
|
||||
- `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md`
|
||||
- 预期结果:
|
||||
- `GFramework.Cqrs.Benchmarks` 不再只覆盖“有无 generated provider / startup / pipeline”的维度,也开始覆盖 request steady-state 下的 handler 生命周期成本差异
|
||||
- benchmark 设计继续保持“只加入语义公平的矩阵”,避免把作用域模型不对称的结论写进基线
|
||||
|
||||
### 验证(RP-092)
|
||||
|
||||
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*RequestLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
|
||||
- 结果:通过(沙箱外权威结果)
|
||||
- 备注:当前 agent 沙箱内执行同一 benchmark 会在 BenchmarkDotNet 自动生成 bootstrap 阶段失败;切换到沙箱外后,`restore/build` 自举与 6 个 benchmark case 全部通过
|
||||
- 备注:`Singleton` 下 baseline / MediatR / GFramework 分别约 `5.633 ns / 58.687 ns / 301.731 ns`
|
||||
- 备注:`Transient` 下 baseline / MediatR / GFramework 分别约 `5.044 ns / 52.274 ns / 287.863 ns`
|
||||
- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
|
||||
- 结果:通过
|
||||
- `git diff --check`
|
||||
- 结果:通过
|
||||
|
||||
### 当前下一步(RP-092)
|
||||
|
||||
1. 若 branch diff 仍明显低于 `$gframework-batch-boot 50` 阈值,下一批优先补 `stream handler` 生命周期矩阵,保持 request / stream benchmark 维度对称
|
||||
2. 若准备扩到 `Scoped` 生命周期,先为 benchmark host 设计真实显式作用域基线,再进入运行时对照
|
||||
|
||||
## 2026-05-06
|
||||
|
||||
### 阶段:PR #331 review 收尾补丁(CQRS-REWRITE-RP-091)
|
||||
|
||||
@ -105,6 +105,10 @@ var reward = this.SendCommand(new GetGoldRewardCommand(new GetGoldRewardInput(3)
|
||||
- `SendCommandAsync(IAsyncCommand)`
|
||||
- `SendCommandAsync<TResult>(IAsyncCommand<TResult>)`
|
||||
|
||||
在标准架构启动路径中,这些兼容入口底层已经统一改走 `ICqrsRuntime`。
|
||||
这意味着历史命令调用链在不改调用方式的前提下,也会复用同一套 pipeline 与上下文注入语义。
|
||||
只有在你直接 `new CommandExecutor()` 做隔离测试,且没有提供 `ICqrsRuntime` 时,才会回退到 legacy 直接执行;此时不会注入统一 pipeline,也不会额外补上下文桥接链路。
|
||||
|
||||
在 `IContextAware` 对象内,通常直接通过扩展使用:
|
||||
|
||||
```csharp
|
||||
|
||||
@ -111,6 +111,10 @@ this.SendEvent(new PlayerDiedEvent());
|
||||
|
||||
这部分入口主要用于兼容存量代码。新功能优先看 [cqrs](./cqrs.md)。
|
||||
|
||||
在标准 `Architecture` 初始化路径里,这些旧入口现在会复用同一个 `ICqrsRuntime`:
|
||||
旧 `SendCommand(...)` / `SendQuery(...)` 仍保持原有调用方式,但会经过统一的 request pipeline 与上下文注入链路。
|
||||
只有在你直接 `new CommandExecutor()`、`new QueryExecutor()` 或 `new AsyncQueryExecutor()` 做隔离测试,且没有提供 `ICqrsRuntime` 时,才会回退到 legacy 直接执行;此时 `SendQueryAsync(...)` 兼容入口也会沿用同样的 legacy 路径,而不会进入统一 runtime pipeline。
|
||||
|
||||
## 新 CQRS 入口
|
||||
|
||||
`IArchitectureContext` 也是当前 CQRS runtime 的主入口。最重要的方法是:
|
||||
|
||||
@ -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 契约,建议按下面这几组入口阅读:
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user