Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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;
Expand Down Expand Up @@ -71,6 +72,8 @@ public void SetUp()
_container.RegisterPlurality(_queryBus);
_container.RegisterPlurality(_asyncQueryBus);
_container.RegisterPlurality(_environment);
new CqrsRuntimeModule().Register(_container);
RegisterLegacyBridgeHandlers(_container);

_context = new ArchitectureContext(_container);
}
Expand Down Expand Up @@ -124,6 +127,24 @@ public void SendQuery_Should_ReturnResult_When_Query_IsValid()
Assert.That(result, Is.EqualTo(42));
}

/// <summary>
/// 测试 legacy 查询通过 <see cref="ArchitectureContext" /> 发送时会进入统一 CQRS pipeline,
/// 并把当前架构上下文注入到查询对象。
/// </summary>
[Test]
public void SendQuery_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context()
{
LegacyBridgePipelineTracker.Reset();
var testQuery = new LegacyArchitectureBridgeQuery();
var bridgeContext = CreateFrozenBridgeContext();

var result = bridgeContext.SendQuery(testQuery);

Assert.That(result, Is.EqualTo(24));
Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// <summary>
/// 测试SendQuery方法在查询为null时应抛出ArgumentNullException
/// </summary>
Expand All @@ -146,6 +167,24 @@ public void SendCommand_Should_ExecuteCommand_When_Command_IsValid()
Assert.That(testCommand.Executed, Is.True);
}

/// <summary>
/// 测试 legacy 命令通过 <see cref="ArchitectureContext" /> 发送时会进入统一 CQRS pipeline,
/// 并把当前架构上下文注入到命令对象。
/// </summary>
[Test]
public void SendCommand_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context()
{
LegacyBridgePipelineTracker.Reset();
var testCommand = new LegacyArchitectureBridgeCommand();
var bridgeContext = CreateFrozenBridgeContext();

bridgeContext.SendCommand(testCommand);

Assert.That(testCommand.Executed, Is.True);
Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
}

/// <summary>
/// 测试SendCommand方法在命令为null时应抛出ArgumentNullException
/// </summary>
Expand All @@ -168,6 +207,87 @@ public void SendCommand_WithResult_Should_ReturnResult_When_Command_IsValid()
Assert.That(result, Is.EqualTo(123));
}

/// <summary>
/// 测试 legacy 带返回值命令通过 <see cref="ArchitectureContext" /> 发送时会进入统一 CQRS pipeline,
/// 并保持原始返回值语义。
/// </summary>
[Test]
public void SendCommand_WithResult_Should_Bridge_Through_CqrsRuntime()
{
LegacyBridgePipelineTracker.Reset();
var testCommand = new LegacyArchitectureBridgeCommandWithResult();
var bridgeContext = CreateFrozenBridgeContext();

var result = bridgeContext.SendCommand(testCommand);

Assert.That(result, Is.EqualTo(42));
Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
}

/// <summary>
/// 测试 legacy 异步查询通过 <see cref="ArchitectureContext" /> 发送时也会进入统一 CQRS pipeline。
/// </summary>
[Test]
public async Task SendQueryAsync_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context()
{
LegacyBridgePipelineTracker.Reset();
var testQuery = new LegacyArchitectureBridgeAsyncQuery();
var bridgeContext = CreateFrozenBridgeContext();

var result = await bridgeContext.SendQueryAsync(testQuery).ConfigureAwait(false);

Assert.That(result, Is.EqualTo(64));
Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
}

/// <summary>
/// 为需要验证统一 CQRS pipeline 的用例创建一个已冻结的最小 bridge 上下文。
/// </summary>
/// <returns>能够执行 legacy bridge request 且会 materialize open-generic pipeline behavior 的上下文。</returns>
private static ArchitectureContext CreateFrozenBridgeContext()
{
var container = new MicrosoftDiContainer();
RegisterLegacyBridgeHandlers(container);
new CqrsRuntimeModule().Register(container);
container.ExecuteServicesHook(services =>
services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LegacyBridgeTrackingPipelineBehavior<,>)));
container.Freeze();
return new ArchitectureContext(container);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

/// <summary>
/// 通过反射把 GFramework.Core 内部的 legacy bridge handler 实例预先注册成可见的实例绑定。
/// </summary>
/// <param name="container">目标测试容器。</param>
private static void RegisterLegacyBridgeHandlers(MicrosoftDiContainer container)
{
ArgumentNullException.ThrowIfNull(container);

string[] handlerTypeNames =
[
"LegacyCommandDispatchRequestHandler",
"LegacyCommandResultDispatchRequestHandler",
"LegacyAsyncCommandDispatchRequestHandler",
"LegacyAsyncCommandResultDispatchRequestHandler",
"LegacyQueryDispatchRequestHandler",
"LegacyAsyncQueryDispatchRequestHandler"
];

var coreAssembly = typeof(ArchitectureContext).Assembly;

foreach (var handlerTypeName in handlerTypeNames)
{
var handlerType = coreAssembly.GetType($"GFramework.Core.Cqrs.{handlerTypeName}")
?? throw new InvalidOperationException($"Bridge handler type '{handlerTypeName}' was not found.");
var handlerInstance = Activator.CreateInstance(handlerType)
?? throw new InvalidOperationException(
$"Bridge handler type '{handlerType.FullName}' could not be instantiated.");
container.RegisterPlurality(handlerInstance);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// <summary>
/// 测试SendCommand方法(带返回值)在命令为null时应抛出ArgumentNullException
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
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;

Expand Down Expand Up @@ -80,6 +82,36 @@ public async Task RegisterCqrsPipelineBehavior_Should_Apply_Pipeline_Behavior_To
await architecture.DestroyAsync();
}

/// <summary>
/// 验证默认架构初始化路径会自动扫描 Core 程序集里的 legacy bridge handler,
/// 使旧 <c>SendCommand</c> / <c>SendQuery</c> 入口也能进入统一 CQRS pipeline。
/// </summary>
[Test]
public async Task InitializeAsync_Should_AutoRegister_LegacyBridgeHandlers_For_Default_Core_Assemblies()
{
LegacyBridgePipelineTracker.Reset();
var architecture = new LegacyBridgeArchitecture();

await architecture.InitializeAsync();

var query = new LegacyArchitectureBridgeQuery();
var command = new LegacyArchitectureBridgeCommand();

var queryResult = architecture.Context.SendQuery(query);
architecture.Context.SendCommand(command);

Assert.Multiple(() =>
{
Assert.That(queryResult, Is.EqualTo(24));
Assert.That(query.ObservedContext, Is.SameAs(architecture.Context));
Assert.That(command.Executed, Is.True);
Assert.That(command.ObservedContext, Is.SameAs(architecture.Context));
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(2));
});

await architecture.DestroyAsync();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// <summary>
/// 用于测试模块行为的最小架构实现。
/// </summary>
Expand All @@ -94,6 +126,27 @@ protected override void OnInitialize()
}
}

/// <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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
41 changes: 41 additions & 0 deletions GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0

using System.Threading;

namespace GFramework.Core.Tests.Architectures;

/// <summary>
/// 为 legacy bridge pipeline 回归测试保存跨泛型闭包共享的计数状态。
/// </summary>
public static class LegacyBridgePipelineTracker
{
private static int _invocationCount;

/// <summary>
/// 获取当前进程内被识别为 legacy bridge request 的 pipeline 命中次数。
/// </summary>
public static int InvocationCount => Volatile.Read(ref _invocationCount);

/// <summary>
/// 重置计数器。
/// </summary>
public static void Reset()
{
Volatile.Write(ref _invocationCount, 0);
}

/// <summary>
/// 若当前请求类型属于 Core legacy bridge request,则记录一次命中。
/// </summary>
public static void Record(Type requestType)
{
ArgumentNullException.ThrowIfNull(requestType);

if (string.Equals(requestType.Namespace, "GFramework.Core.Cqrs", StringComparison.Ordinal) &&
requestType.Name.Contains("Legacy", StringComparison.Ordinal))
{
Interlocked.Increment(ref _invocationCount);
}
}
}
Comment thread
GeWuYou marked this conversation as resolved.
Loading
Loading