diff --git a/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs
index 43a6272a..28b1fb32 100644
--- a/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs
+++ b/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs
@@ -14,18 +14,7 @@ namespace GFramework.Core.Tests.Cqrs;
[TestFixture]
internal sealed class CqrsHandlerRegistrarTests
{
- private static readonly MethodInfo RecoverLoadableTypesMethod = typeof(ArchitectureContext).Assembly
- .GetType(
- "GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar",
- throwOnError: true)!
- .GetMethod("RecoverLoadableTypes",
- BindingFlags.NonPublic |
- BindingFlags.Static)!
- ?? throw new InvalidOperationException(
- "Failed to locate CqrsHandlerRegistrar.RecoverLoadableTypes.");
-
private MicrosoftDiContainer? _container;
-
private ArchitectureContext? _context;
///
@@ -40,8 +29,7 @@ internal sealed class CqrsHandlerRegistrarTests
_container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(
_container,
- typeof(CqrsHandlerRegistrarTests).Assembly,
- typeof(ArchitectureContext).Assembly);
+ typeof(CqrsHandlerRegistrarTests).Assembly);
_container.Freeze();
_context = new ArchitectureContext(_container);
@@ -79,28 +67,53 @@ internal sealed class CqrsHandlerRegistrarTests
/// 验证部分类型加载失败时仍能保留可加载类型,并记录诊断日志。
///
[Test]
- public void RecoverLoadableTypes_Should_Return_Loadable_Types_And_Log_Warnings()
+ public void RegisterHandlers_Should_Register_Loadable_Types_And_Log_Warnings_When_Assembly_Load_Partially_Fails()
{
- var logger = new TestLogger(nameof(CqrsHandlerRegistrarTests), LogLevel.Warning);
+ var originalProvider = LoggerFactoryResolver.Provider;
+ var capturingProvider = new CapturingLoggerFactoryProvider(LogLevel.Warning);
var reflectionTypeLoadException = new ReflectionTypeLoadException(
[typeof(AlphaDeterministicNotificationHandler), null],
[new TypeLoadException("Missing optional dependency for registrar test.")]);
+ var partiallyLoadableAssembly = new Mock();
+ partiallyLoadableAssembly
+ .SetupGet(static assembly => assembly.FullName)
+ .Returns("GFramework.Core.Tests.Cqrs.PartiallyLoadableAssembly, Version=1.0.0.0");
+ partiallyLoadableAssembly
+ .Setup(static assembly => assembly.GetTypes())
+ .Throws(reflectionTypeLoadException);
- var recoveredTypes = (IReadOnlyList)RecoverLoadableTypesMethod.Invoke(
- null,
- [typeof(CqrsHandlerRegistrarTests).Assembly, reflectionTypeLoadException, logger])!;
-
- Assert.Multiple(() =>
+ LoggerFactoryResolver.Provider = capturingProvider;
+ try
{
- Assert.That(recoveredTypes, Is.EqualTo([typeof(AlphaDeterministicNotificationHandler)]));
- Assert.That(logger.Logs.Count(log => log.Level == LogLevel.Warning), Is.GreaterThanOrEqualTo(2));
- Assert.That(
- logger.Logs.Any(log => log.Message.Contains("partially failed", StringComparison.Ordinal)),
- Is.True);
- Assert.That(
- logger.Logs.Any(log => log.Message.Contains("Missing optional dependency", StringComparison.Ordinal)),
- Is.True);
- });
+ var container = new MicrosoftDiContainer();
+ CqrsTestRuntime.RegisterHandlers(container, partiallyLoadableAssembly.Object);
+ container.Freeze();
+
+ var handlers = container.GetAll>();
+ var warningLogs = capturingProvider.Loggers
+ .SelectMany(static logger => logger.Logs)
+ .Where(static log => log.Level == LogLevel.Warning)
+ .ToList();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(
+ handlers.Select(static handler => handler.GetType()),
+ Is.EqualTo([typeof(AlphaDeterministicNotificationHandler)]));
+ Assert.That(warningLogs.Count, Is.GreaterThanOrEqualTo(2));
+ Assert.That(
+ warningLogs.Any(log => log.Message.Contains("partially failed", StringComparison.Ordinal)),
+ Is.True);
+ Assert.That(
+ warningLogs.Any(log =>
+ log.Message.Contains("Missing optional dependency", StringComparison.Ordinal)),
+ Is.True);
+ });
+ }
+ finally
+ {
+ LoggerFactoryResolver.Provider = originalProvider;
+ }
}
}
@@ -163,3 +176,46 @@ internal sealed class AlphaDeterministicNotificationHandler : INotificationHandl
return ValueTask.CompletedTask;
}
}
+
+///
+/// 为 CQRS 注册测试捕获真实启动路径中创建的日志记录器。
+///
+///
+/// 处理器注册入口会分别为测试运行时、容器和注册器创建日志器。
+/// 该提供程序统一保留这些测试日志器,以便断言警告是否经由公开入口真正发出。
+///
+internal sealed class CapturingLoggerFactoryProvider : ILoggerFactoryProvider
+{
+ private readonly List _loggers = [];
+
+ ///
+ /// 使用指定的最小日志级别初始化一个新的捕获型日志工厂提供程序。
+ ///
+ /// 要应用到新建测试日志器的最小日志级别。
+ public CapturingLoggerFactoryProvider(LogLevel minLevel = LogLevel.Info)
+ {
+ MinLevel = minLevel;
+ }
+
+ ///
+ /// 获取通过当前提供程序创建的全部测试日志器。
+ ///
+ public IReadOnlyList Loggers => _loggers;
+
+ ///
+ /// 获取或设置新建测试日志器的最小日志级别。
+ ///
+ public LogLevel MinLevel { get; set; }
+
+ ///
+ /// 创建一个测试日志器并将其纳入捕获集合。
+ ///
+ /// 日志记录器名称。
+ /// 用于后续断言的测试日志器。
+ public ILogger CreateLogger(string name)
+ {
+ var logger = new TestLogger(name, MinLevel);
+ _loggers.Add(logger);
+ return logger;
+ }
+}
diff --git a/GFramework.Core.Tests/CqrsTestRuntime.cs b/GFramework.Core.Tests/CqrsTestRuntime.cs
index e2801cc8..e9664925 100644
--- a/GFramework.Core.Tests/CqrsTestRuntime.cs
+++ b/GFramework.Core.Tests/CqrsTestRuntime.cs
@@ -1,24 +1,49 @@
using System.Reflection;
+using GFramework.Core.Abstractions.Ioc;
+using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Architectures;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
namespace GFramework.Core.Tests;
+///
+/// 为测试项目提供对 CQRS 处理器真实注册入口的受控访问。
+///
+///
+/// 测试应通过该入口驱动注册流程,而不是直接反射调用注册器的私有辅助方法,
+/// 这样可以覆盖生产启动路径中的程序集去重、日志记录与容错恢复行为。
+///
internal static class CqrsTestRuntime
{
- private static readonly MethodInfo RegisterHandlersMethod = typeof(ArchitectureContext).Assembly
- .GetType(
- "GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar",
- throwOnError: true)!
+ 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)!
+ BindingFlags.Static,
+ binder: null,
+ [
+ typeof(IIocContainer),
+ typeof(IEnumerable),
+ typeof(ILogger)
+ ],
+ modifiers: null)
?? throw new InvalidOperationException(
"Failed to locate CqrsHandlerRegistrar.RegisterHandlers.");
- public static void RegisterHandlers(MicrosoftDiContainer container, params Assembly[] assemblies)
+ ///
+ /// 通过与生产代码一致的注册入口扫描并注册指定程序集中的 CQRS 处理器。
+ ///
+ /// 承载处理器映射的测试容器。
+ /// 要扫描的程序集集合。
+ internal static void RegisterHandlers(MicrosoftDiContainer container, params Assembly[] assemblies)
{
ArgumentNullException.ThrowIfNull(container);
ArgumentNullException.ThrowIfNull(assemblies);
diff --git a/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs b/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs
index 84e8e029..847563c4 100644
--- a/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs
+++ b/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs
@@ -18,22 +18,30 @@ using GFramework.Core.Rule;
namespace GFramework.Core.Cqrs.Command;
///
-/// 抽象流式命令处理器基类
-/// 继承自 ContextAwareBase 并实现 IStreamRequestHandler 接口,为具体的流式命令处理器提供基础功能。
-/// 支持流式处理命令并产生异步可枚举的响应序列,框架会在每次创建流前注入当前架构上下文。
+/// 抽象流式命令处理器基类。
+/// 继承自 并实现 ,
+/// 为具体的流式命令处理器提供基础功能。
///
-/// 流式命令类型,必须实现IStreamCommand接口
-/// 流式命令响应元素类型
+/// 流式命令类型,必须实现 。
+/// 流式命令响应元素类型。
+///
+/// 框架会在每次调用 CreateStream 进入实际处理逻辑前,为当前处理器实例注入架构上下文,
+/// 因此派生类只能在 执行期间及其返回的异步枚举序列内假定 Context 可用。
+/// 默认注册器会将流式命令处理器注册为瞬态服务,以避免同一个上下文感知实例在多个流或并发请求之间复用。
+/// 派生类不应缓存处理器实例,也不应把依赖当前上下文的可变状态泄漏到流外部。
+/// 传入 的取消令牌同时约束流的创建与后续枚举,
+/// 派生类应在启动阶段和每次生成响应前尊重取消请求,避免在调用方停止枚举后继续执行后台工作。
+///
public abstract class AbstractStreamCommandHandler : ContextAwareBase,
IStreamRequestHandler
where TCommand : IStreamCommand
{
///
- /// 处理流式命令并返回异步可枚举的响应序列
- /// 由具体的流式命令处理器子类实现流式处理逻辑
+ /// 处理流式命令并返回异步可枚举的响应序列。
+ /// 由具体的流式命令处理器子类实现流式处理逻辑。
///
- /// 要处理的流式命令对象
- /// 取消令牌,用于取消流式处理操作
- /// 异步可枚举的响应序列,每个元素类型为TResponse
+ /// 要处理的流式命令对象。
+ /// 取消令牌,用于取消流式处理操作。
+ /// 异步可枚举的响应序列,每个元素类型为 。
public abstract IAsyncEnumerable Handle(TCommand command, CancellationToken cancellationToken);
}