feat(arch): 添加架构上下文实现及完整测试

- 实现 ArchitectureContext 类,提供对系统、模型、工具等组件的访问
- 集成 CQRS runtime,支持命令、查询、事件的执行管理
- 添加服务缓存机制,优化容器解析性能
- 实现并发安全的 CQRS runtime 懒加载
- 提供同步和异步的请求处理方法
- 支持优先级排序的服务实例获取
- 添加完整的单元测试覆盖构造函数、查询、命令、事件等功能
- 配置测试项目依赖和全局引用
- 实现共享的 CQRS 测试运行时支持
This commit is contained in:
GeWuYou 2026-04-15 15:57:08 +08:00
parent 932235e8cc
commit e2001766cb
8 changed files with 118 additions and 144 deletions

View File

@ -1,8 +1,10 @@
using System.Reflection;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Environment;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Abstractions.Systems;
@ -14,6 +16,7 @@ using GFramework.Core.Events;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Core.Query;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Tests.Architectures;
@ -298,6 +301,69 @@ public class ArchitectureContextTests
Assert.That(environment, Is.Not.Null);
Assert.That(environment, Is.InstanceOf<IEnvironment>());
}
/// <summary>
/// 测试 CQRS runtime 在并发首次访问时只会从容器解析一次。
/// </summary>
[Test]
public async Task SendRequestAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently()
{
using var startGate = new ManualResetEventSlim(false);
using var allowResolutionToComplete = new ManualResetEventSlim(false);
var resolutionCallCount = 0;
var runtime = new Mock<ICqrsRuntime>(MockBehavior.Strict);
var container = new Mock<IIocContainer>(MockBehavior.Strict);
runtime.Setup(mockRuntime => mockRuntime.SendAsync(
It.IsAny<IArchitectureContext>(),
It.IsAny<IRequest<int>>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<int>(42));
container.Setup(mockContainer => mockContainer.Get<ICqrsRuntime>())
.Returns(() =>
{
Interlocked.Increment(ref resolutionCallCount);
allowResolutionToComplete.Wait();
return runtime.Object;
});
var context = new ArchitectureContext(container.Object);
var requests = Enumerable.Range(0, 16)
.Select(_ => Task.Run(async () =>
{
startGate.Wait();
return await context.SendRequestAsync(new TestCqrsRequest());
}))
.ToArray();
startGate.Set();
Assert.That(
SpinWait.SpinUntil(() => Volatile.Read(ref resolutionCallCount) > 0, TimeSpan.FromSeconds(1)),
Is.True,
"Expected at least one CQRS runtime resolution attempt.");
// 留出一个短暂窗口,让并发首次访问都在 runtime 尚未发布前抵达同一初始化点。
await Task.Delay(50);
allowResolutionToComplete.Set();
var responses = await Task.WhenAll(requests);
Assert.That(responses, Has.All.EqualTo(42));
Assert.That(resolutionCallCount, Is.EqualTo(1));
container.Verify(mockContainer => mockContainer.Get<ICqrsRuntime>(), Times.Once);
runtime.Verify(
mockRuntime => mockRuntime.SendAsync(
It.IsAny<IArchitectureContext>(),
It.IsAny<IRequest<int>>(),
It.IsAny<CancellationToken>()),
Times.Exactly(requests.Length));
}
private sealed class TestCqrsRequest : IRequest<int>
{
}
}
#region Test Classes

View File

@ -1,119 +0,0 @@
using System.Reflection;
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Architectures;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Tests;
/// <summary>
/// 为测试项目提供对 CQRS 处理器真实注册入口的受控访问。
/// </summary>
/// <remarks>
/// 测试应通过该入口驱动注册流程,而不是直接反射调用注册器的私有辅助方法,
/// 这样可以覆盖生产启动路径中的程序集去重、日志记录与容错恢复行为。
/// </remarks>
internal static class CqrsTestRuntime
{
private static readonly Type CqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly
.GetType(
"GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar",
throwOnError: true)!
?? throw new InvalidOperationException(
"Failed to locate CqrsHandlerRegistrar type.");
private static readonly MethodInfo RegisterHandlersMethod = CqrsHandlerRegistrarType
.GetMethod(
"RegisterHandlers",
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Static,
binder: null,
[
typeof(IIocContainer),
typeof(IEnumerable<Assembly>),
typeof(ILogger)
],
modifiers: null)
?? throw new InvalidOperationException(
"Failed to locate CqrsHandlerRegistrar.RegisterHandlers.");
private static readonly Type CqrsDispatcherType = typeof(ArchitectureContext).Assembly
.GetType(
"GFramework.Core.Cqrs.Internal.CqrsDispatcher",
throwOnError: true)!
?? throw new InvalidOperationException(
"Failed to locate CqrsDispatcher type.");
private static readonly ConstructorInfo CqrsDispatcherConstructor = CqrsDispatcherType.GetConstructor(
BindingFlags.Instance |
BindingFlags.Public |
BindingFlags.NonPublic,
binder: null,
[
typeof(IIocContainer),
typeof(ILogger)
],
modifiers: null)
?? throw new InvalidOperationException(
"Failed to locate CqrsDispatcher constructor.");
private static readonly Type DefaultCqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly
.GetType(
"GFramework.Core.Cqrs.Internal.DefaultCqrsHandlerRegistrar",
throwOnError: true)!
?? throw new InvalidOperationException(
"Failed to locate DefaultCqrsHandlerRegistrar type.");
private static readonly ConstructorInfo DefaultCqrsHandlerRegistrarConstructor =
DefaultCqrsHandlerRegistrarType.GetConstructor(
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
binder: null,
[
typeof(IIocContainer),
typeof(ILogger)
],
modifiers: null)
?? throw new InvalidOperationException(
"Failed to locate DefaultCqrsHandlerRegistrar constructor.");
/// <summary>
/// 为裸测试容器补齐默认 CQRS runtime seam。
/// 这使仅使用 <see cref="MicrosoftDiContainer" /> 的测试环境也能观察与生产路径一致的 runtime 行为,
/// 而无需完整启动服务模块管理器。
/// </summary>
/// <param name="container">目标测试容器。</param>
internal static void RegisterInfrastructure(MicrosoftDiContainer container)
{
ArgumentNullException.ThrowIfNull(container);
var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher");
var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime));
var runtime = (ICqrsRuntime)CqrsDispatcherConstructor.Invoke([container, runtimeLogger]);
var registrar =
(ICqrsHandlerRegistrar)DefaultCqrsHandlerRegistrarConstructor.Invoke([container, registrarLogger]);
container.Register<ICqrsRuntime>(runtime);
container.Register<ICqrsHandlerRegistrar>(registrar);
}
/// <summary>
/// 通过与生产代码一致的注册入口扫描并注册指定程序集中的 CQRS 处理器。
/// </summary>
/// <param name="container">承载处理器映射的测试容器。</param>
/// <param name="assemblies">要扫描的程序集集合。</param>
internal static void RegisterHandlers(MicrosoftDiContainer container, params Assembly[] assemblies)
{
ArgumentNullException.ThrowIfNull(container);
ArgumentNullException.ThrowIfNull(assemblies);
RegisterInfrastructure(container);
var logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime));
RegisterHandlersMethod.Invoke(
null,
[container, assemblies.Where(static assembly => assembly is not null).Distinct().ToArray(), logger]);
}
}

View File

@ -18,6 +18,7 @@
<ItemGroup>
<PackageReference Include="Scriban" Version="7.1.0" />
<Compile Include="..\tests\Shared\CqrsTestRuntime.cs" Link="Shared\CqrsTestRuntime.cs"/>
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj"/>
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/>
<ProjectReference Include="..\GFramework.SourceGenerators.Abstractions\GFramework.SourceGenerators.Abstractions.csproj"/>

View File

@ -16,6 +16,7 @@ global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
global using GFramework.Tests.Shared;
global using NUnit.Framework;
global using NUnit.Compatibility;
global using GFramework.Core.Systems;

View File

@ -17,19 +17,48 @@ namespace GFramework.Core.Architectures;
/// <summary>
/// 架构上下文类,提供对系统、模型、工具等组件的访问以及命令、查询、事件的执行管理
/// </summary>
public class ArchitectureContext(IIocContainer container) : IArchitectureContext
public class ArchitectureContext : IArchitectureContext
{
private readonly IIocContainer _container = container ?? throw new ArgumentNullException(nameof(container));
private readonly IIocContainer _container;
private readonly Lazy<ICqrsRuntime> _cqrsRuntime;
private readonly ConcurrentDictionary<Type, object> _serviceCache = new();
private ICqrsRuntime? _cqrsRuntime;
/// <summary>
/// 初始化新的架构上下文,并绑定其依赖容器。
/// </summary>
/// <param name="container">
/// 当前架构使用的 IOC 容器。
/// CQRS runtime 与其他框架服务会通过该容器延迟解析,以避免在上下文构造阶段强制拉起整条运行时链路。
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
public ArchitectureContext(IIocContainer container)
{
_container = container ?? throw new ArgumentNullException(nameof(container));
_cqrsRuntime = new Lazy<ICqrsRuntime>(
ResolveCqrsRuntime,
LazyThreadSafetyMode.ExecutionAndPublication);
}
#region CQRS Integration
/// <summary>
/// 获取 CQRS runtime seam延迟初始化
/// 获取 CQRS runtime seam。
/// </summary>
private ICqrsRuntime CqrsRuntime => _cqrsRuntime ??=
_container.Get<ICqrsRuntime>() ?? throw new InvalidOperationException("ICqrsRuntime not registered");
/// <remarks>
/// 该实例会在首次访问时从容器解析,并通过 <see cref="Lazy{T}" /> 保证并发场景下只执行一次初始化,
/// 避免多个请求线程重复触发同一个 runtime 的容器解析。
/// </remarks>
private ICqrsRuntime CqrsRuntime => _cqrsRuntime.Value;
/// <summary>
/// 从容器解析当前架构上下文依赖的 CQRS runtime。
/// </summary>
/// <returns>已注册的 CQRS runtime 实例。</returns>
/// <exception cref="InvalidOperationException">容器中未注册 <see cref="ICqrsRuntime" />。</exception>
private ICqrsRuntime ResolveCqrsRuntime()
{
return _container.Get<ICqrsRuntime>() ?? throw new InvalidOperationException("ICqrsRuntime not registered");
}
/// <summary>
/// 获取指定类型的服务实例,如果缓存中存在则直接返回,否则从容器中获取并缓存

View File

@ -17,6 +17,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="..\tests\Shared\CqrsTestRuntime.cs" Link="Shared\CqrsTestRuntime.cs"/>
<ProjectReference Include="..\GFramework.Cqrs.Abstractions\GFramework.Cqrs.Abstractions.csproj"/>
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj"/>
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/>

View File

@ -20,6 +20,7 @@ global using System.Reflection;
global using System.Runtime.CompilerServices;
global using System.Threading;
global using System.Threading.Tasks;
global using GFramework.Tests.Shared;
global using Microsoft.Extensions.DependencyInjection;
global using Moq;
global using NUnit.Compatibility;

View File

@ -6,23 +6,21 @@ using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests;
namespace GFramework.Tests.Shared;
/// <summary>
/// 为测试项目提供对 CQRS 处理器真实注册入口的受控访问。
/// </summary>
/// <remarks>
/// 测试应通过该入口驱动注册流程,而不是直接反射调用注册器的私有辅助方法
/// 这样可以覆盖生产启动路径中的程序集去重、日志记录与容错恢复行为
/// 该文件以共享源码的方式同时编译进多个测试项目,确保反射绑定签名、默认 runtime 接线和注册入口行为始终保持一致
/// 避免测试副本在独立演化后产生隐藏分歧
/// </remarks>
internal static class CqrsTestRuntime
{
private static readonly Type CqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly
.GetType(
"GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar",
throwOnError: true)!
?? throw new InvalidOperationException(
"Failed to locate CqrsHandlerRegistrar type.");
throwOnError: true)!;
private static readonly MethodInfo RegisterHandlersMethod = CqrsHandlerRegistrarType
.GetMethod(
@ -42,9 +40,7 @@ internal static class CqrsTestRuntime
private static readonly Type CqrsDispatcherType = typeof(ArchitectureContext).Assembly
.GetType(
"GFramework.Core.Cqrs.Internal.CqrsDispatcher",
throwOnError: true)!
?? throw new InvalidOperationException(
"Failed to locate CqrsDispatcher type.");
throwOnError: true)!;
private static readonly ConstructorInfo CqrsDispatcherConstructor = CqrsDispatcherType.GetConstructor(
BindingFlags.Instance |
@ -62,9 +58,7 @@ internal static class CqrsTestRuntime
private static readonly Type DefaultCqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly
.GetType(
"GFramework.Core.Cqrs.Internal.DefaultCqrsHandlerRegistrar",
throwOnError: true)!
?? throw new InvalidOperationException(
"Failed to locate DefaultCqrsHandlerRegistrar type.");
throwOnError: true)!;
private static readonly ConstructorInfo DefaultCqrsHandlerRegistrarConstructor =
DefaultCqrsHandlerRegistrarType.GetConstructor(