diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs
index ebfa6659..ffbe5196 100644
--- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs
+++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs
@@ -138,6 +138,68 @@ internal sealed class CqrsHandlerRegistrarFallbackFailureTests
});
}
+ ///
+ /// 验证当 generated registry 是抽象类型时,registrar 会记录告警并回退到反射扫描。
+ ///
+ [Test]
+ public void RegisterHandlers_Should_Fall_Back_To_Reflection_When_Generated_Registry_Is_Abstract()
+ {
+ var generatedAssembly = CreateGeneratedRegistryAssembly(
+ "GFramework.Cqrs.Tests.Cqrs.AbstractGeneratedRegistryAssembly, Version=1.0.0.0",
+ typeof(AbstractGeneratedNotificationHandlerRegistry));
+ generatedAssembly
+ .Setup(static assembly => assembly.GetTypes())
+ .Returns([typeof(GeneratedRegistryNotificationHandler)]);
+
+ var container = new MicrosoftDiContainer();
+ CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(
+ GetGeneratedRegistryNotificationHandlerTypes(container),
+ Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)]));
+ Assert.That(
+ GetWarningLogs().Any(log =>
+ log.Message.Contains("because it is abstract", StringComparison.Ordinal)),
+ Is.True);
+ });
+
+ generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Once);
+ }
+
+ ///
+ /// 验证当 generated registry 不暴露可访问无参构造器时,registrar 会记录告警并回退到反射扫描。
+ ///
+ [Test]
+ public void RegisterHandlers_Should_Fall_Back_To_Reflection_When_Generated_Registry_Has_No_Parameterless_Constructor()
+ {
+ var generatedAssembly = CreateGeneratedRegistryAssembly(
+ "GFramework.Cqrs.Tests.Cqrs.NoParameterlessGeneratedRegistryAssembly, Version=1.0.0.0",
+ typeof(ConstructorArgumentNotificationHandlerRegistry));
+ generatedAssembly
+ .Setup(static assembly => assembly.GetTypes())
+ .Returns([typeof(GeneratedRegistryNotificationHandler)]);
+
+ var container = new MicrosoftDiContainer();
+ CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(
+ GetGeneratedRegistryNotificationHandlerTypes(container),
+ Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)]));
+ Assert.That(
+ GetWarningLogs().Any(log =>
+ log.Message.Contains(
+ "does not expose an accessible parameterless constructor",
+ StringComparison.Ordinal)),
+ Is.True);
+ });
+
+ generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Once);
+ }
+
///
/// 创建一个仅通过 generated registry 注册主 handler、并附带指定 fallback 元数据的程序集替身。
///
@@ -161,6 +223,24 @@ internal sealed class CqrsHandlerRegistrarFallbackFailureTests
return generatedAssembly;
}
+ ///
+ /// 创建一个只声明 generated registry attribute 的程序集替身,用于验证 registry 激活失败后的回退行为。
+ ///
+ /// 用于日志与缓存键的程序集名。
+ /// 要暴露给 registrar 的 generated registry 类型。
+ /// 已完成基础接线的程序集 mock。
+ private static Mock CreateGeneratedRegistryAssembly(string assemblyName, Type registryType)
+ {
+ var generatedAssembly = new Mock();
+ generatedAssembly
+ .SetupGet(static assembly => assembly.FullName)
+ .Returns(assemblyName);
+ generatedAssembly
+ .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
+ .Returns([new CqrsHandlerRegistryAttribute(registryType)]);
+ return generatedAssembly;
+ }
+
///
/// 提取容器中针对 generated notification 注册的处理器实现类型。
///
@@ -259,4 +339,55 @@ internal sealed class CqrsHandlerRegistrarFallbackFailureTests
.Where(static log => log.Level == LogLevel.Warning)
.ToArray();
}
+
+ ///
+ /// 模拟 generated registry 被错误声明为抽象类型时的激活失败场景。
+ ///
+ private abstract class AbstractGeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry
+ {
+ ///
+ /// 抽象 registry 即便具备注册逻辑,也不应被运行时实例化。
+ ///
+ /// 承载处理器映射的服务集合。
+ /// 记录注册诊断的日志器。
+ public void Register(IServiceCollection services, ILogger logger)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(logger);
+
+ services.AddTransient(
+ typeof(INotificationHandler),
+ typeof(GeneratedRegistryNotificationHandler));
+ }
+ }
+
+ ///
+ /// 模拟 generated registry 缺少可访问无参构造器时的激活失败场景。
+ ///
+ private sealed class ConstructorArgumentNotificationHandlerRegistry : ICqrsHandlerRegistry
+ {
+ ///
+ /// 初始化一个只能通过额外参数构造的测试 registry。
+ ///
+ /// 用于区分测试场景的占位参数。
+ public ConstructorArgumentNotificationHandlerRegistry(string marker)
+ {
+ ArgumentNullException.ThrowIfNull(marker);
+ }
+
+ ///
+ /// 此实现仅用于满足接口契约;本用例关注的是实例化失败前的回退行为。
+ ///
+ /// 承载处理器映射的服务集合。
+ /// 记录注册诊断的日志器。
+ public void Register(IServiceCollection services, ILogger logger)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(logger);
+
+ services.AddTransient(
+ typeof(INotificationHandler),
+ typeof(GeneratedRegistryNotificationHandler));
+ }
+ }
}
diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs
index 967fc6a8..4fa6b124 100644
--- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs
+++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs
@@ -6,6 +6,7 @@ using GFramework.Core.Architectures;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
+using GFramework.Cqrs.Internal;
using GFramework.Cqrs.Tests.Logging;
namespace GFramework.Cqrs.Tests.Cqrs;
@@ -170,6 +171,74 @@ internal sealed class CqrsHandlerRegistrarTests
Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)]));
}
+ ///
+ /// 验证 direct generated-registry 激活入口在 registry 为抽象类型时会抛出异常,并保留契约告警。
+ ///
+ [Test]
+ public void RegisterGeneratedRegistry_Should_Throw_When_Generated_Registry_Is_Abstract()
+ {
+ var capturingProvider = new CapturingLoggerFactoryProvider(LogLevel.Warning);
+ var logger = capturingProvider.CreateLogger(nameof(CqrsHandlerRegistrarTests));
+ var container = new MicrosoftDiContainer();
+
+ var exception = Assert.Throws(() =>
+ CqrsHandlerRegistrar.RegisterGeneratedRegistry(
+ container,
+ typeof(AbstractGeneratedNotificationHandlerRegistry),
+ logger));
+
+ var warningLogs = capturingProvider.Loggers
+ .SelectMany(static createdLogger => createdLogger.Logs)
+ .Where(static log => log.Level == LogLevel.Warning)
+ .ToArray();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(exception, Is.Not.Null);
+ Assert.That(exception!.Message, Does.Contain(typeof(AbstractGeneratedNotificationHandlerRegistry).FullName));
+ Assert.That(
+ warningLogs.Any(log =>
+ log.Message.Contains("because it is abstract", StringComparison.Ordinal)),
+ Is.True);
+ Assert.That(container.GetServicesUnsafe, Is.Empty);
+ });
+ }
+
+ ///
+ /// 验证 direct generated-registry 激活入口在 registry 缺少无参构造器时会抛出异常,并保留契约告警。
+ ///
+ [Test]
+ public void RegisterGeneratedRegistry_Should_Throw_When_Generated_Registry_Has_No_Parameterless_Constructor()
+ {
+ var capturingProvider = new CapturingLoggerFactoryProvider(LogLevel.Warning);
+ var logger = capturingProvider.CreateLogger(nameof(CqrsHandlerRegistrarTests));
+ var container = new MicrosoftDiContainer();
+
+ var exception = Assert.Throws(() =>
+ CqrsHandlerRegistrar.RegisterGeneratedRegistry(
+ container,
+ typeof(ConstructorArgumentNotificationHandlerRegistry),
+ logger));
+
+ var warningLogs = capturingProvider.Loggers
+ .SelectMany(static createdLogger => createdLogger.Logs)
+ .Where(static log => log.Level == LogLevel.Warning)
+ .ToArray();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(exception, Is.Not.Null);
+ Assert.That(exception!.Message, Does.Contain(typeof(ConstructorArgumentNotificationHandlerRegistry).FullName));
+ Assert.That(
+ warningLogs.Any(log =>
+ log.Message.Contains(
+ "does not expose an accessible parameterless constructor",
+ StringComparison.Ordinal)),
+ Is.True);
+ Assert.That(container.GetServicesUnsafe, Is.Empty);
+ });
+ }
+
///
/// 验证当生成注册器元数据损坏时,运行时会记录告警并回退到反射扫描路径。
///
@@ -695,4 +764,55 @@ internal sealed class CqrsHandlerRegistrarTests
return typeof(CqrsReflectionFallbackAttribute).Assembly
.GetType("GFramework.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)!;
}
+
+ ///
+ /// 模拟被错误声明为抽象类型的 generated registry。
+ ///
+ private abstract class AbstractGeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry
+ {
+ ///
+ /// 抽象 registry 即便具备注册逻辑,也不应被 direct 激活入口实例化。
+ ///
+ /// 承载处理器映射的服务集合。
+ /// 记录注册诊断的日志器。
+ public void Register(IServiceCollection services, ILogger logger)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(logger);
+
+ services.AddTransient(
+ typeof(INotificationHandler),
+ typeof(GeneratedRegistryNotificationHandler));
+ }
+ }
+
+ ///
+ /// 模拟缺少无参构造器的 generated registry。
+ ///
+ private sealed class ConstructorArgumentNotificationHandlerRegistry : ICqrsHandlerRegistry
+ {
+ ///
+ /// 初始化一个只能通过额外参数构造的测试 registry。
+ ///
+ /// 用于区分测试场景的占位参数。
+ public ConstructorArgumentNotificationHandlerRegistry(string marker)
+ {
+ ArgumentNullException.ThrowIfNull(marker);
+ }
+
+ ///
+ /// 此实现仅用于满足接口契约;本用例关注的是构造阶段失败后的异常语义。
+ ///
+ /// 承载处理器映射的服务集合。
+ /// 记录注册诊断的日志器。
+ public void Register(IServiceCollection services, ILogger logger)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(logger);
+
+ services.AddTransient(
+ typeof(INotificationHandler),
+ typeof(GeneratedRegistryNotificationHandler));
+ }
+ }
}