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");
}