mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
feat(arch): 添加架构上下文实现及完整测试
- 实现 ArchitectureContext 类,提供对系统、模型、工具等组件的访问 - 集成 CQRS runtime,支持命令、查询、事件的执行管理 - 添加服务缓存机制,优化容器解析性能 - 实现并发安全的 CQRS runtime 懒加载 - 提供同步和异步的请求处理方法 - 支持优先级排序的服务实例获取 - 添加完整的单元测试覆盖构造函数、查询、命令、事件等功能 - 配置测试项目依赖和全局引用 - 实现共享的 CQRS 测试运行时支持
This commit is contained in:
parent
932235e8cc
commit
e2001766cb
@ -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
|
||||
@ -442,4 +508,4 @@ public class TestEventV2
|
||||
public int Data { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
@ -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"/>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
/// 获取指定类型的服务实例,如果缓存中存在则直接返回,否则从容器中获取并缓存
|
||||
|
||||
@ -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"/>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.");
|
||||
.GetType(
|
||||
"GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar",
|
||||
throwOnError: true)!;
|
||||
|
||||
private static readonly MethodInfo RegisterHandlersMethod = CqrsHandlerRegistrarType
|
||||
.GetMethod(
|
||||
@ -40,11 +38,9 @@ internal static class CqrsTestRuntime
|
||||
"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.");
|
||||
.GetType(
|
||||
"GFramework.Core.Cqrs.Internal.CqrsDispatcher",
|
||||
throwOnError: true)!;
|
||||
|
||||
private static readonly ConstructorInfo CqrsDispatcherConstructor = CqrsDispatcherType.GetConstructor(
|
||||
BindingFlags.Instance |
|
||||
@ -60,11 +56,9 @@ internal static class CqrsTestRuntime
|
||||
"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.");
|
||||
.GetType(
|
||||
"GFramework.Core.Cqrs.Internal.DefaultCqrsHandlerRegistrar",
|
||||
throwOnError: true)!;
|
||||
|
||||
private static readonly ConstructorInfo DefaultCqrsHandlerRegistrarConstructor =
|
||||
DefaultCqrsHandlerRegistrarType.GetConstructor(
|
||||
Loading…
x
Reference in New Issue
Block a user