GFramework/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs
gewuyou babd132e81 docs(cqrs): 收口批处理剩余文档与追踪
- 新增 NotificationLifetime 基准并补充验证结果

- 更新 CQRS README 与 legacy Command/Query 迁移说明

- 补充 registration fallback 回归测试并同步 ai-plan 恢复点
2026-05-11 13:02:01 +08:00

213 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Tests.Logging;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 验证 CQRS 程序集注册协调器在程序集键去重层面的可观察行为。
/// </summary>
[TestFixture]
internal sealed class CqrsRegistrationServiceTests
{
/// <summary>
/// 验证同一次调用内出现重复程序集键时,底层注册器只会接收到一次注册请求。
/// </summary>
[Test]
public void RegisterHandlers_Should_Register_Duplicate_Assembly_Key_Only_Once_Per_Call()
{
var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug);
var registrar = new Mock<ICqrsHandlerRegistrar>(MockBehavior.Strict);
var duplicateAssemblyA = CreateAssembly("GFramework.Cqrs.Tests.DuplicateAssembly, Version=1.0.0.0");
var duplicateAssemblyB = CreateAssembly("GFramework.Cqrs.Tests.DuplicateAssembly, Version=1.0.0.0");
var expectedAssembly = duplicateAssemblyA.Object;
IEnumerable<Assembly>? registeredAssemblies = null;
registrar
.Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()))
.Callback<IEnumerable<Assembly>>(assemblies => registeredAssemblies = assemblies.ToArray());
var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger);
service.RegisterHandlers([duplicateAssemblyA.Object, duplicateAssemblyB.Object]);
registrar.Verify(
static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()),
Times.Once);
Assert.Multiple(() =>
{
Assert.That(registeredAssemblies, Is.Not.Null);
Assert.That(registeredAssemblies, Is.EqualTo([expectedAssembly]));
Assert.That(logger.Logs, Has.Count.EqualTo(0));
});
}
/// <summary>
/// 验证跨两次调用重复程序集键时,协调器会跳过重复注册并写入 debug 日志。
/// </summary>
[Test]
public void RegisterHandlers_Should_Skip_Already_Registered_Assembly_Key_Across_Calls_And_Log_Debug_Message()
{
var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug);
var registrar = new Mock<ICqrsHandlerRegistrar>(MockBehavior.Strict);
var firstAssembly = CreateAssembly("GFramework.Cqrs.Tests.RegisteredAssembly, Version=1.0.0.0");
var secondAssembly = CreateAssembly("GFramework.Cqrs.Tests.RegisteredAssembly, Version=1.0.0.0");
IEnumerable<Assembly>? registeredAssemblies = null;
registrar
.Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()))
.Callback<IEnumerable<Assembly>>(assemblies => registeredAssemblies = assemblies.ToArray());
var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger);
service.RegisterHandlers([firstAssembly.Object]);
service.RegisterHandlers([secondAssembly.Object]);
registrar.Verify(
static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()),
Times.Once);
Assert.Multiple(() =>
{
Assert.That(registeredAssemblies, Is.EqualTo([firstAssembly.Object]));
var debugMessages = logger.Logs
.Where(static log => log.Level == LogLevel.Debug)
.Select(static log => log.Message)
.ToArray();
Assert.That(debugMessages, Has.Length.EqualTo(1));
Assert.That(
debugMessages[0],
Does.Contain("Skipping CQRS handler registration for assembly"));
Assert.That(
debugMessages[0],
Does.Contain("GFramework.Cqrs.Tests.RegisteredAssembly, Version=1.0.0.0"));
Assert.That(debugMessages[0], Does.Contain("already registered"));
});
}
/// <summary>
/// 验证当 <see cref="Assembly.FullName" /> 缺失时,协调器会退化到 <see cref="AssemblyName.Name" /> 作为稳定程序集键。
/// </summary>
[Test]
public void RegisterHandlers_Should_Fallback_To_Simple_Name_When_Full_Name_Is_Missing()
{
var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug);
var registrar = new Mock<ICqrsHandlerRegistrar>(MockBehavior.Strict);
var firstAssembly = CreateAssembly(
assemblyFullName: null,
assemblySimpleName: "GFramework.Cqrs.Tests.SimpleNameFallback",
assemblyDisplayName: "DisplayName-A");
var secondAssembly = CreateAssembly(
assemblyFullName: null,
assemblySimpleName: "GFramework.Cqrs.Tests.SimpleNameFallback",
assemblyDisplayName: "DisplayName-B");
IEnumerable<Assembly>? registeredAssemblies = null;
registrar
.Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()))
.Callback<IEnumerable<Assembly>>(assemblies => registeredAssemblies = assemblies.ToArray());
var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger);
service.RegisterHandlers([firstAssembly.Object]);
service.RegisterHandlers([secondAssembly.Object]);
registrar.Verify(
static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()),
Times.Once);
Assert.Multiple(() =>
{
Assert.That(registeredAssemblies, Is.EqualTo([firstAssembly.Object]));
var debugMessages = logger.Logs
.Where(static log => log.Level == LogLevel.Debug)
.Select(static log => log.Message)
.ToArray();
Assert.That(debugMessages, Has.Length.EqualTo(1));
Assert.That(debugMessages[0], Does.Contain("GFramework.Cqrs.Tests.SimpleNameFallback"));
Assert.That(debugMessages[0], Does.Not.Contain("DisplayName-B"));
});
}
/// <summary>
/// 验证当 <see cref="Assembly.FullName" /> 与 <see cref="AssemblyName.Name" /> 均缺失时,
/// 协调器会退化到 <see cref="object.ToString" /> 结果作为稳定程序集键。
/// </summary>
[Test]
public void RegisterHandlers_Should_Fallback_To_ToString_When_Full_Name_And_Simple_Name_Are_Missing()
{
var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug);
var registrar = new Mock<ICqrsHandlerRegistrar>(MockBehavior.Strict);
const string assemblyDisplayName = "GFramework.Cqrs.Tests.ToStringFallback";
var firstAssembly = CreateAssembly(
assemblyFullName: null,
assemblySimpleName: null,
assemblyDisplayName: assemblyDisplayName);
var secondAssembly = CreateAssembly(
assemblyFullName: null,
assemblySimpleName: null,
assemblyDisplayName: assemblyDisplayName);
IEnumerable<Assembly>? registeredAssemblies = null;
registrar
.Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()))
.Callback<IEnumerable<Assembly>>(assemblies => registeredAssemblies = assemblies.ToArray());
var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger);
service.RegisterHandlers([firstAssembly.Object]);
service.RegisterHandlers([secondAssembly.Object]);
registrar.Verify(
static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()),
Times.Once);
Assert.Multiple(() =>
{
Assert.That(registeredAssemblies, Is.EqualTo([firstAssembly.Object]));
var debugMessages = logger.Logs
.Where(static log => log.Level == LogLevel.Debug)
.Select(static log => log.Message)
.ToArray();
Assert.That(debugMessages, Has.Length.EqualTo(1));
Assert.That(debugMessages[0], Does.Contain(assemblyDisplayName));
Assert.That(debugMessages[0], Does.Contain("already registered"));
});
}
/// <summary>
/// 创建一个带稳定程序集键的程序集 mock用于模拟不同 <see cref="Assembly" /> 实例表示同一程序集的场景。
/// </summary>
/// <param name="assemblyFullName">要返回的程序集完整名称。</param>
/// <returns>配置好完整名称的程序集 mock。</returns>
private static Mock<Assembly> CreateAssembly(string assemblyFullName)
{
return CreateAssembly(assemblyFullName, assemblySimpleName: null, assemblyDisplayName: assemblyFullName);
}
/// <summary>
/// 创建一个可配置程序集元数据退化路径的程序集 mock用于验证稳定程序集键的回退顺序。
/// </summary>
/// <param name="assemblyFullName">要返回的程序集完整名称;为 <see langword="null" /> 时模拟缺失完整名称。</param>
/// <param name="assemblySimpleName">要返回的程序集简单名称;为 <see langword="null" /> 时模拟缺失简单名称。</param>
/// <param name="assemblyDisplayName">当需要退化到 <see cref="object.ToString" /> 时返回的显示名称。</param>
/// <returns>配置好程序集元数据的程序集 mock。</returns>
private static Mock<Assembly> CreateAssembly(string? assemblyFullName, string? assemblySimpleName, string assemblyDisplayName)
{
var assembly = new Mock<Assembly>();
var assemblyName = new AssemblyName();
assembly
.SetupGet(static currentAssembly => currentAssembly.FullName)
.Returns(assemblyFullName);
assemblyName.Name = assemblySimpleName;
assembly
.Setup(static currentAssembly => currentAssembly.GetName())
.Returns(assemblyName);
assembly
.Setup(static currentAssembly => currentAssembly.ToString())
.Returns(assemblyDisplayName);
return assembly;
}
}