diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs index 00684cb9..4b5da422 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs @@ -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()); } + + /// + /// 测试 CQRS runtime 在并发首次访问时只会从容器解析一次。 + /// + [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(MockBehavior.Strict); + var container = new Mock(MockBehavior.Strict); + + runtime.Setup(mockRuntime => mockRuntime.SendAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(new ValueTask(42)); + + container.Setup(mockContainer => mockContainer.Get()) + .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(), Times.Once); + runtime.Verify( + mockRuntime => mockRuntime.SendAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny()), + Times.Exactly(requests.Length)); + } + + private sealed class TestCqrsRequest : IRequest + { + } } #region Test Classes @@ -442,4 +508,4 @@ public class TestEventV2 public int Data { get; init; } } -#endregion \ No newline at end of file +#endregion diff --git a/GFramework.Core.Tests/CqrsTestRuntime.cs b/GFramework.Core.Tests/CqrsTestRuntime.cs deleted file mode 100644 index 3f8a7813..00000000 --- a/GFramework.Core.Tests/CqrsTestRuntime.cs +++ /dev/null @@ -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; - -/// -/// 为测试项目提供对 CQRS 处理器真实注册入口的受控访问。 -/// -/// -/// 测试应通过该入口驱动注册流程,而不是直接反射调用注册器的私有辅助方法, -/// 这样可以覆盖生产启动路径中的程序集去重、日志记录与容错恢复行为。 -/// -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), - 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."); - - /// - /// 为裸测试容器补齐默认 CQRS runtime seam。 - /// 这使仅使用 的测试环境也能观察与生产路径一致的 runtime 行为, - /// 而无需完整启动服务模块管理器。 - /// - /// 目标测试容器。 - 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(runtime); - container.Register(registrar); - } - - /// - /// 通过与生产代码一致的注册入口扫描并注册指定程序集中的 CQRS 处理器。 - /// - /// 承载处理器映射的测试容器。 - /// 要扫描的程序集集合。 - 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]); - } -} diff --git a/GFramework.Core.Tests/GFramework.Core.Tests.csproj b/GFramework.Core.Tests/GFramework.Core.Tests.csproj index 97663fe0..c455f570 100644 --- a/GFramework.Core.Tests/GFramework.Core.Tests.csproj +++ b/GFramework.Core.Tests/GFramework.Core.Tests.csproj @@ -18,6 +18,7 @@ + diff --git a/GFramework.Core.Tests/GlobalUsings.cs b/GFramework.Core.Tests/GlobalUsings.cs index fe9b7de1..daaaf5b7 100644 --- a/GFramework.Core.Tests/GlobalUsings.cs +++ b/GFramework.Core.Tests/GlobalUsings.cs @@ -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; diff --git a/GFramework.Core/Architectures/ArchitectureContext.cs b/GFramework.Core/Architectures/ArchitectureContext.cs index e52960bf..9b6d7dc2 100644 --- a/GFramework.Core/Architectures/ArchitectureContext.cs +++ b/GFramework.Core/Architectures/ArchitectureContext.cs @@ -17,19 +17,48 @@ namespace GFramework.Core.Architectures; /// /// 架构上下文类,提供对系统、模型、工具等组件的访问以及命令、查询、事件的执行管理 /// -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 _cqrsRuntime; private readonly ConcurrentDictionary _serviceCache = new(); - private ICqrsRuntime? _cqrsRuntime; + + /// + /// 初始化新的架构上下文,并绑定其依赖容器。 + /// + /// + /// 当前架构使用的 IOC 容器。 + /// CQRS runtime 与其他框架服务会通过该容器延迟解析,以避免在上下文构造阶段强制拉起整条运行时链路。 + /// + /// + public ArchitectureContext(IIocContainer container) + { + _container = container ?? throw new ArgumentNullException(nameof(container)); + _cqrsRuntime = new Lazy( + ResolveCqrsRuntime, + LazyThreadSafetyMode.ExecutionAndPublication); + } #region CQRS Integration /// - /// 获取 CQRS runtime seam(延迟初始化)。 + /// 获取 CQRS runtime seam。 /// - private ICqrsRuntime CqrsRuntime => _cqrsRuntime ??= - _container.Get() ?? throw new InvalidOperationException("ICqrsRuntime not registered"); + /// + /// 该实例会在首次访问时从容器解析,并通过 保证并发场景下只执行一次初始化, + /// 避免多个请求线程重复触发同一个 runtime 的容器解析。 + /// + private ICqrsRuntime CqrsRuntime => _cqrsRuntime.Value; + + /// + /// 从容器解析当前架构上下文依赖的 CQRS runtime。 + /// + /// 已注册的 CQRS runtime 实例。 + /// 容器中未注册 + private ICqrsRuntime ResolveCqrsRuntime() + { + return _container.Get() ?? throw new InvalidOperationException("ICqrsRuntime not registered"); + } /// /// 获取指定类型的服务实例,如果缓存中存在则直接返回,否则从容器中获取并缓存 diff --git a/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj b/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj index 09cded18..6c05a59d 100644 --- a/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj +++ b/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj @@ -17,6 +17,7 @@ + diff --git a/GFramework.Cqrs.Tests/GlobalUsings.cs b/GFramework.Cqrs.Tests/GlobalUsings.cs index d31630ed..9a04018e 100644 --- a/GFramework.Cqrs.Tests/GlobalUsings.cs +++ b/GFramework.Cqrs.Tests/GlobalUsings.cs @@ -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; diff --git a/GFramework.Cqrs.Tests/CqrsTestRuntime.cs b/tests/Shared/CqrsTestRuntime.cs similarity index 76% rename from GFramework.Cqrs.Tests/CqrsTestRuntime.cs rename to tests/Shared/CqrsTestRuntime.cs index 7676f143..a234ff90 100644 --- a/GFramework.Cqrs.Tests/CqrsTestRuntime.cs +++ b/tests/Shared/CqrsTestRuntime.cs @@ -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; /// /// 为测试项目提供对 CQRS 处理器真实注册入口的受控访问。 /// /// -/// 测试应通过该入口驱动注册流程,而不是直接反射调用注册器的私有辅助方法, -/// 这样可以覆盖生产启动路径中的程序集去重、日志记录与容错恢复行为。 +/// 该文件以共享源码的方式同时编译进多个测试项目,确保反射绑定签名、默认 runtime 接线和注册入口行为始终保持一致, +/// 避免测试副本在独立演化后产生隐藏分歧。 /// 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(