Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0

using System.Reflection;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Architectures;
using GFramework.Core.Logging;
using GFramework.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Notification;
using Microsoft.Extensions.DependencyInjection;

namespace GFramework.Core.Tests.Architectures;
Expand All @@ -27,6 +30,7 @@ public void SetUp()
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
GameContext.Clear();
AdditionalAssemblyNotificationHandlerState.Reset();
TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount = 0;
TrackingStreamPipelineBehavior<ModuleStreamBehaviorRequest, int>.InvocationCount = 0;
}
Expand All @@ -37,6 +41,7 @@ public void SetUp()
[TearDown]
public void TearDown()
{
AdditionalAssemblyNotificationHandlerState.Reset();
GameContext.Clear();
TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount = 0;
TrackingStreamPipelineBehavior<ModuleStreamBehaviorRequest, int>.InvocationCount = 0;
Expand Down Expand Up @@ -156,6 +161,35 @@ public async Task InitializeAsync_Should_AutoRegister_LegacyBridgeHandlers_For_D
}
}

/// <summary>
/// 验证标准架构启动路径会复用通过 <see cref="Architecture.Configurator" /> 声明的自定义 notification publisher,
/// 而不是在 <see cref="GFramework.Core.Services.Modules.CqrsRuntimeModule" /> 创建 runtime 时提前固化默认顺序策略。
/// </summary>
[Test]
public async Task InitializeAsync_Should_Reuse_Custom_NotificationPublisher_From_Configurator()
{
var generatedAssembly = CreateGeneratedHandlerAssembly();
var architecture = new ConfiguredNotificationPublisherArchitecture(generatedAssembly.Object);

await architecture.InitializeAsync();
try
{
var probe = architecture.Context.GetService<ArchitectureNotificationPublisherProbe>();

await architecture.Context.PublishAsync(new AdditionalAssemblyNotification());

Assert.Multiple(() =>
{
Assert.That(probe.WasCalled, Is.True);
Assert.That(AdditionalAssemblyNotificationHandlerState.InvocationCount, Is.EqualTo(1));
});
}
finally
{
await architecture.DestroyAsync();
}
}

/// <summary>
/// 用于测试模块行为的最小架构实现。
/// </summary>
Expand Down Expand Up @@ -191,6 +225,31 @@ protected override void OnInitialize()
}
}

/// <summary>
/// 通过标准架构启动路径声明自定义 notification publisher 的最小架构。
/// </summary>
private sealed class ConfiguredNotificationPublisherArchitecture(Assembly generatedAssembly) : Architecture
{
/// <summary>
/// 在服务钩子阶段注册 probe 与自定义 publisher,
/// 以模拟真实项目在组合根里通过 <see cref="IServiceCollection" /> 覆盖默认策略的路径。
/// </summary>
public override Action<IServiceCollection>? Configurator => services =>
{
services.AddSingleton<ArchitectureNotificationPublisherProbe>();
services.AddSingleton<INotificationPublisher, ArchitectureTrackingNotificationPublisher>();
};

/// <summary>
/// 在用户初始化阶段显式接入额外程序集里的 notification handler,
/// 让测试聚焦“publisher 是否被复用”,而不是依赖当前测试文件自己的 handler 扫描形状。
/// </summary>
protected override void OnInitialize()
{
RegisterCqrsHandlersFromAssembly(generatedAssembly);
}
}

/// <summary>
/// 记录模块安装调用情况的测试模块。
/// </summary>
Expand Down Expand Up @@ -225,6 +284,69 @@ private sealed class InstalledByModuleUtility : IUtility
{
}

/// <summary>
/// 创建一个仅暴露程序集级 CQRS registry 元数据的 mocked Assembly。
/// 该测试替身模拟扩展程序集已经提供 notification handler registry,而架构只需在初始化时显式接入该程序集。
/// </summary>
/// <returns>包含程序集级 notification handler registry 元数据的 mocked Assembly。</returns>
private static Mock<Assembly> CreateGeneratedHandlerAssembly()
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Architectures.ExplicitAdditionalHandlers, Version=1.0.0.0");
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(AdditionalAssemblyNotificationHandlerRegistry))]);
return generatedAssembly;
}

/// <summary>
/// 记录自定义 notification publisher 是否真正参与了标准架构启动路径下的 publish 调用。
/// </summary>
private sealed class ArchitectureNotificationPublisherProbe
{
/// <summary>
/// 获取 probe 是否已被 publisher 标记为执行过。
/// </summary>
public bool WasCalled { get; private set; }

/// <summary>
/// 记录当前 publish 调用已经命中了自定义 publisher。
/// </summary>
public void MarkCalled()
{
WasCalled = true;
}
}

/// <summary>
/// 依赖容器内 probe 的自定义 notification publisher。
/// 该类型通过显式标记 + 正常转发处理器执行,验证标准架构启动路径不会把自定义策略短路成默认顺序发布器。
/// </summary>
private sealed class ArchitectureTrackingNotificationPublisher(
ArchitectureNotificationPublisherProbe probe) : INotificationPublisher
{
/// <summary>
/// 记录自定义 publisher 已参与当前发布调用,并继续按处理器解析顺序转发执行。
/// </summary>
public async ValueTask PublishAsync<TNotification>(
NotificationPublishContext<TNotification> context,
CancellationToken cancellationToken = default)
where TNotification : INotification
{
ArgumentNullException.ThrowIfNull(context);
cancellationToken.ThrowIfCancellationRequested();

probe.MarkCalled();

foreach (var handler in context.Handlers)
{
await context.InvokeHandlerAsync(handler, cancellationToken).ConfigureAwait(false);
}
}
}

/// <summary>
/// 物化异步流为只读列表,便于断言 stream pipeline 行为的最终可观察结果。
/// </summary>
Expand Down
4 changes: 1 addition & 3 deletions GFramework.Core/Services/Modules/CqrsRuntimeModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using GFramework.Core.Abstractions.Logging;
using GFramework.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Notification;
using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime;

namespace GFramework.Core.Services.Modules;
Expand Down Expand Up @@ -46,8 +45,7 @@ public void Register(IIocContainer container)
var dispatcherLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher");
var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar");
var registrationLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsRegistrationService");
var notificationPublisher = container.Get<INotificationPublisher>();
var runtime = CqrsRuntimeFactory.CreateRuntime(container, dispatcherLogger, notificationPublisher);
var runtime = CqrsRuntimeFactory.CreateRuntime(container, dispatcherLogger);
var registrar = CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger);

container.Register(runtime);
Expand Down
Loading
Loading