diff --git a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs index ab54bd54..e1687ff9 100644 --- a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs +++ b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs @@ -101,6 +101,8 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia /// 当处理器位于默认架构程序集之外的模块或扩展程序集中时,可在初始化阶段调用该入口接入对应程序集。 /// /// 包含 CQRS 处理器或生成注册器的程序集。 + /// + /// 当前架构的底层容器已冻结,无法继续注册处理器。 void RegisterCqrsHandlersFromAssembly(Assembly assembly); /// @@ -108,6 +110,8 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia /// 该入口会对程序集集合去重,适用于统一接入多个扩展包或模块程序集。 /// /// 要接入的程序集集合。 + /// + /// 当前架构的底层容器已冻结,无法继续注册处理器。 void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies); /// diff --git a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs index b1908020..3149b3c4 100644 --- a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs +++ b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs @@ -115,6 +115,8 @@ public interface IIocContainer : IContextAware /// 运行时会优先使用程序集级源码生成注册器;若不存在可用注册器,则自动回退到反射扫描。 /// /// 包含 CQRS 处理器或生成注册器的程序集。 + /// + /// 容器已冻结,无法继续注册 CQRS 处理器。 void RegisterCqrsHandlersFromAssembly(Assembly assembly); /// @@ -122,6 +124,8 @@ public interface IIocContainer : IContextAware /// 容器会按稳定程序集键去重,避免默认启动路径与扩展模块重复接入同一程序集时产生重复 handler 映射。 /// /// 要接入的程序集集合。 + /// + /// 容器已冻结,无法继续注册 CQRS 处理器。 void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies); diff --git a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs index ae0bbad0..23725e7c 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs @@ -18,9 +18,10 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests [SetUp] public void SetUp() { + _previousLoggerFactoryProvider = LoggerFactoryResolver.Provider; LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); GameContext.Clear(); - AdditionalAssemblyNotificationHandler.Reset(); + AdditionalAssemblyNotificationHandlerState.Reset(); } /// @@ -29,10 +30,15 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests [TearDown] public void TearDown() { - AdditionalAssemblyNotificationHandler.Reset(); + AdditionalAssemblyNotificationHandlerState.Reset(); GameContext.Clear(); + LoggerFactoryResolver.Provider = _previousLoggerFactoryProvider + ?? throw new InvalidOperationException( + "LoggerFactoryResolver.Provider should be captured during setup."); } + private ILoggerFactoryProvider? _previousLoggerFactoryProvider; + /// /// 验证显式声明的额外程序集会在初始化阶段接入当前架构容器。 /// @@ -44,11 +50,16 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests target.RegisterCqrsHandlersFromAssembly(generatedAssembly.Object)); await architecture.InitializeAsync(); - await architecture.Context.PublishAsync(new AdditionalAssemblyNotification()); + try + { + await architecture.Context.PublishAsync(new AdditionalAssemblyNotification()); - Assert.That(AdditionalAssemblyNotificationHandler.InvocationCount, Is.EqualTo(1)); - - await architecture.DestroyAsync(); + Assert.That(AdditionalAssemblyNotificationHandlerState.InvocationCount, Is.EqualTo(1)); + } + finally + { + await architecture.DestroyAsync(); + } } /// @@ -65,11 +76,16 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests }); await architecture.InitializeAsync(); - await architecture.Context.PublishAsync(new AdditionalAssemblyNotification()); + try + { + await architecture.Context.PublishAsync(new AdditionalAssemblyNotification()); - Assert.That(AdditionalAssemblyNotificationHandler.InvocationCount, Is.EqualTo(1)); - - await architecture.DestroyAsync(); + Assert.That(AdditionalAssemblyNotificationHandlerState.InvocationCount, Is.EqualTo(1)); + } + finally + { + await architecture.DestroyAsync(); + } } /// @@ -111,25 +127,26 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests public sealed record AdditionalAssemblyNotification : INotification; /// -/// 由模拟扩展程序集的生成注册器挂入当前容器的通知处理器。 +/// 记录模拟扩展程序集通知处理器的执行次数。 /// -public sealed class AdditionalAssemblyNotificationHandler : INotificationHandler +public static class AdditionalAssemblyNotificationHandlerState { + private static int _invocationCount; + /// /// 获取当前测试进程中该处理器的执行次数。 /// - public static int InvocationCount { get; private set; } + /// + /// 该计数器通过原子读写维护,以支持 NUnit 并行执行环境中的并发访问。 + /// + public static int InvocationCount => Volatile.Read(ref _invocationCount); /// /// 记录一次通知处理,供测试断言显式程序集接入后的运行时行为。 /// - /// 通知实例。 - /// 取消令牌。 - /// 已完成任务。 - public ValueTask Handle(AdditionalAssemblyNotification notification, CancellationToken cancellationToken) + public static void RecordInvocation() { - InvocationCount++; - return ValueTask.CompletedTask; + Interlocked.Increment(ref _invocationCount); } /// @@ -137,7 +154,7 @@ public sealed class AdditionalAssemblyNotificationHandler : INotificationHandler /// public static void Reset() { - InvocationCount = 0; + Interlocked.Exchange(ref _invocationCount, 0); } } @@ -156,10 +173,25 @@ internal sealed class AdditionalAssemblyNotificationHandlerRegistry : ICqrsHandl ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(logger); - services - .AddTransient, - AdditionalAssemblyNotificationHandler>(); + services.AddTransient>(_ => CreateHandler()); logger.Debug( - $"Registered CQRS handler {typeof(AdditionalAssemblyNotificationHandler).FullName} as {typeof(INotificationHandler).FullName}."); + $"Registered CQRS handler proxy for {typeof(INotificationHandler).FullName}."); + } + + /// + /// 创建一个仅供显式程序集注册路径使用的动态通知处理器。 + /// + /// 用于记录通知触发次数的测试替身处理器。 + private static INotificationHandler CreateHandler() + { + var handler = new Mock>(); + handler + .Setup(target => target.Handle(It.IsAny(), It.IsAny())) + .Returns(() => + { + AdditionalAssemblyNotificationHandlerState.RecordInvocation(); + return ValueTask.CompletedTask; + }); + return handler.Object; } } diff --git a/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs b/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs index 66f7869c..b224804d 100644 --- a/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs +++ b/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs @@ -186,11 +186,21 @@ public class TestArchitectureWithRegistry : IArchitecture throw new NotImplementedException(); } + /// + /// 测试替身未实现显式程序集 CQRS 处理器接入入口。 + /// + /// 包含 CQRS 处理器或生成注册器的程序集。 + /// 该测试替身不参与 CQRS 程序集接入路径验证。 public void RegisterCqrsHandlersFromAssembly(Assembly assembly) { throw new NotImplementedException(); } + /// + /// 测试替身未实现显式程序集 CQRS 处理器接入入口。 + /// + /// 要接入的程序集集合。 + /// 该测试替身不参与 CQRS 程序集接入路径验证。 public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) { throw new NotImplementedException(); @@ -327,11 +337,21 @@ public class TestArchitectureWithoutRegistry : IArchitecture throw new NotImplementedException(); } + /// + /// 测试替身未实现显式程序集 CQRS 处理器接入入口。 + /// + /// 包含 CQRS 处理器或生成注册器的程序集。 + /// 该测试替身不参与 CQRS 程序集接入路径验证。 public void RegisterCqrsHandlersFromAssembly(Assembly assembly) { throw new NotImplementedException(); } + /// + /// 测试替身未实现显式程序集 CQRS 处理器接入入口。 + /// + /// 要接入的程序集集合。 + /// 该测试替身不参与 CQRS 程序集接入路径验证。 public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) { throw new NotImplementedException(); diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index 0621bcfc..b4e5af67 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -1,7 +1,9 @@ using System.Reflection; using GFramework.Core.Abstractions.Bases; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Ioc; using GFramework.Core.Logging; +using GFramework.Core.Tests.Cqrs; using GFramework.Core.Tests.Systems; namespace GFramework.Core.Tests.Ioc; @@ -306,6 +308,34 @@ public class MicrosoftDiContainerTests Assert.That(_container.Contains(), Is.False); } + /// + /// 测试清空容器后可以重新接入同一程序集中的 CQRS 处理器。 + /// + [Test] + public void Clear_Should_Reset_Cqrs_Assembly_Deduplication_State() + { + var assembly = typeof(CqrsHandlerRegistrarTests).Assembly; + + _container.RegisterCqrsHandlersFromAssembly(assembly); + Assert.That( + _container.GetServicesUnsafe.Any(static descriptor => + descriptor.ServiceType == typeof(INotificationHandler)), + Is.True); + + _container.Clear(); + Assert.That( + _container.GetServicesUnsafe.Any(static descriptor => + descriptor.ServiceType == typeof(INotificationHandler)), + Is.False); + + _container.RegisterCqrsHandlersFromAssembly(assembly); + + Assert.That( + _container.GetServicesUnsafe.Any(static descriptor => + descriptor.ServiceType == typeof(INotificationHandler)), + Is.True); + } + /// /// 测试冻结容器以防止进一步注册的功能 /// @@ -676,4 +706,4 @@ public sealed class PrioritizedService : IPrioritizedService, IMixedService public sealed class NonPrioritizedService : IMixedService { public string? Name { get; set; } -} \ No newline at end of file +} diff --git a/GFramework.Core/Architectures/Architecture.cs b/GFramework.Core/Architectures/Architecture.cs index 0fb361a4..8d2582aa 100644 --- a/GFramework.Core/Architectures/Architecture.cs +++ b/GFramework.Core/Architectures/Architecture.cs @@ -175,6 +175,8 @@ public abstract class Architecture : IArchitecture /// 该入口适用于把拆分到其他模块或扩展包程序集中的 handlers 接入当前架构。 /// /// 包含 CQRS 处理器或生成注册器的程序集。 + /// + /// 当前架构的底层容器已冻结,无法继续注册处理器。 public void RegisterCqrsHandlersFromAssembly(Assembly assembly) { _modules.RegisterCqrsHandlersFromAssembly(assembly); @@ -185,6 +187,8 @@ public abstract class Architecture : IArchitecture /// 适用于在初始化阶段批量接入多个扩展程序集,并沿用容器的去重策略避免重复注册。 /// /// 要接入的程序集集合。 + /// + /// 当前架构的底层容器已冻结,无法继续注册处理器。 public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) { _modules.RegisterCqrsHandlersFromAssemblies(assemblies); diff --git a/GFramework.Core/Architectures/ArchitectureModules.cs b/GFramework.Core/Architectures/ArchitectureModules.cs index 8e659fa4..f5d2a55d 100644 --- a/GFramework.Core/Architectures/ArchitectureModules.cs +++ b/GFramework.Core/Architectures/ArchitectureModules.cs @@ -44,8 +44,11 @@ internal sealed class ArchitectureModules( /// 该入口用于把默认架构程序集之外的扩展处理器接入当前架构容器。 /// /// 包含 CQRS 处理器或生成注册器的程序集。 + /// + /// 底层容器已冻结,无法继续注册处理器。 public void RegisterCqrsHandlersFromAssembly(Assembly assembly) { + ArgumentNullException.ThrowIfNull(assembly); logger.Debug($"Registering CQRS handlers from assembly: {assembly.FullName ?? assembly.GetName().Name}"); services.Container.RegisterCqrsHandlersFromAssembly(assembly); } @@ -55,8 +58,11 @@ internal sealed class ArchitectureModules( /// 它会复用容器级去重逻辑,避免模块重复接入相同程序集时重复注册 handler。 /// /// 要接入的程序集集合。 + /// + /// 底层容器已冻结,无法继续注册处理器。 public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) { + ArgumentNullException.ThrowIfNull(assemblies); logger.Debug("Registering CQRS handlers from additional assemblies."); services.Container.RegisterCqrsHandlersFromAssemblies(assemblies); } diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index b3dfcb03..712a41c1 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -384,6 +384,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 从指定程序集显式注册 CQRS 处理器。 /// /// 包含 CQRS 处理器或生成注册器的程序集。 + /// + /// 容器已冻结,无法继续注册 CQRS 处理器。 public void RegisterCqrsHandlersFromAssembly(Assembly assembly) { ArgumentNullException.ThrowIfNull(assembly); @@ -395,6 +397,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 同一程序集只会被接入一次,避免默认启动路径与扩展模块重复注册相同 handlers。 /// /// 要接入的程序集集合。 + /// + /// 容器已冻结,无法继续注册 CQRS 处理器。 public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) { ArgumentNullException.ThrowIfNull(assemblies); @@ -803,6 +807,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) GetServicesUnsafe.Clear(); _registeredInstances.Clear(); + _registeredCqrsHandlerAssemblyKeys.Clear(); _provider = null; _logger.Info("Container cleared"); }