diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c9bfcf89..defe6c71 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -166,6 +166,12 @@ jobs:
--logger "trx;LogFileName=sg-$RANDOM.trx" \
--results-directory TestResults &
+ dotnet test GFramework.Cqrs.Tests \
+ -c Release \
+ --no-build \
+ --logger "trx;LogFileName=cqrs-$RANDOM.trx" \
+ --results-directory TestResults &
+
dotnet test GFramework.Ecs.Arch.Tests \
-c Release \
--no-build \
diff --git a/GFramework.Core.Tests/Cqrs/ContainerRegistrationFixtures.cs b/GFramework.Core.Tests/Cqrs/ContainerRegistrationFixtures.cs
new file mode 100644
index 00000000..f01af4de
--- /dev/null
+++ b/GFramework.Core.Tests/Cqrs/ContainerRegistrationFixtures.cs
@@ -0,0 +1,36 @@
+// Copyright (c) 2026 GeWuYou
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace GFramework.Core.Tests.Cqrs;
+
+///
+/// 为容器层测试提供可扫描的最小通知夹具。
+///
+internal sealed record DeterministicOrderNotification : INotification;
+
+///
+/// 供容器注册测试验证程序集扫描结果的通知处理器。
+///
+internal sealed class DeterministicOrderNotificationHandler : INotificationHandler
+{
+ ///
+ /// 无副作用地消费通知。
+ ///
+ /// 通知实例。
+ /// 取消令牌。
+ /// 已完成任务。
+ public ValueTask Handle(DeterministicOrderNotification notification, CancellationToken cancellationToken)
+ {
+ return ValueTask.CompletedTask;
+ }
+}
diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs
index 7126a3ad..6ae0747c 100644
--- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs
+++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs
@@ -315,7 +315,7 @@ public class MicrosoftDiContainerTests
[Test]
public void Clear_Should_Reset_Cqrs_Assembly_Deduplication_State()
{
- var assembly = typeof(CqrsHandlerRegistrarTests).Assembly;
+ var assembly = typeof(DeterministicOrderNotification).Assembly;
_container.RegisterCqrsHandlersFromAssembly(assembly);
Assert.That(
diff --git a/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs b/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs
index 5d59f8a6..dca76290 100644
--- a/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs
+++ b/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs
@@ -1,4 +1,5 @@
using System.Reflection;
+using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Logging;
diff --git a/GFramework.Core/GFramework.Core.csproj b/GFramework.Core/GFramework.Core.csproj
index c450b44c..f3e41eab 100644
--- a/GFramework.Core/GFramework.Core.csproj
+++ b/GFramework.Core/GFramework.Core.csproj
@@ -9,6 +9,7 @@
true
+
diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs
index c094647d..49ce44f2 100644
--- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs
+++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs
@@ -1,6 +1,7 @@
using System.ComponentModel;
using System.Reflection;
using GFramework.Core.Abstractions.Bases;
+using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Systems;
diff --git a/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs b/GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs
similarity index 100%
rename from GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs
rename to GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs
diff --git a/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs
similarity index 99%
rename from GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs
rename to GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs
index 7748227c..05a8b0c7 100644
--- a/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs
+++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs
@@ -1,4 +1,3 @@
-using System.Reflection;
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Architectures;
diff --git a/GFramework.Cqrs.Tests/CqrsTestRuntime.cs b/GFramework.Cqrs.Tests/CqrsTestRuntime.cs
new file mode 100644
index 00000000..f9bb143b
--- /dev/null
+++ b/GFramework.Cqrs.Tests/CqrsTestRuntime.cs
@@ -0,0 +1,117 @@
+using GFramework.Core.Abstractions.Cqrs;
+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 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,
+ binder: null,
+ [
+ typeof(IIocContainer),
+ typeof(IEnumerable),
+ typeof(ILogger)
+ ],
+ modifiers: null)
+ ?? throw new InvalidOperationException(
+ "Failed to locate CqrsHandlerRegistrar.RegisterHandlers.");
+
+ private static readonly Type CqrsDispatcherType = typeof(ArchitectureContext).Assembly
+ .GetType(
+ "GFramework.Core.Cqrs.Internal.CqrsDispatcher",
+ throwOnError: true)!
+ ?? throw new InvalidOperationException(
+ "Failed to locate CqrsDispatcher type.");
+
+ private static readonly ConstructorInfo CqrsDispatcherConstructor = CqrsDispatcherType.GetConstructor(
+ BindingFlags.Instance |
+ BindingFlags.Public |
+ BindingFlags.NonPublic,
+ binder: null,
+ [
+ typeof(IIocContainer),
+ typeof(ILogger)
+ ],
+ modifiers: null)
+ ?? throw new InvalidOperationException(
+ "Failed to locate CqrsDispatcher constructor.");
+
+ private static readonly Type DefaultCqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly
+ .GetType(
+ "GFramework.Core.Cqrs.Internal.DefaultCqrsHandlerRegistrar",
+ throwOnError: true)!
+ ?? throw new InvalidOperationException(
+ "Failed to locate DefaultCqrsHandlerRegistrar type.");
+
+ private static readonly ConstructorInfo DefaultCqrsHandlerRegistrarConstructor =
+ DefaultCqrsHandlerRegistrarType.GetConstructor(
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
+ binder: null,
+ [
+ typeof(IIocContainer),
+ typeof(ILogger)
+ ],
+ modifiers: null)
+ ?? throw new InvalidOperationException(
+ "Failed to locate DefaultCqrsHandlerRegistrar constructor.");
+
+ ///
+ /// 为裸测试容器补齐默认 CQRS runtime seam。
+ /// 这使仅使用 的测试环境也能观察与生产路径一致的 runtime 行为,
+ /// 而无需完整启动服务模块管理器。
+ ///
+ /// 目标测试容器。
+ internal static void RegisterInfrastructure(MicrosoftDiContainer container)
+ {
+ ArgumentNullException.ThrowIfNull(container);
+
+ var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher");
+ var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime));
+ var runtime = (ICqrsRuntime)CqrsDispatcherConstructor.Invoke([container, runtimeLogger]);
+ var registrar =
+ (ICqrsHandlerRegistrar)DefaultCqrsHandlerRegistrarConstructor.Invoke([container, registrarLogger]);
+
+ container.Register(runtime);
+ container.Register(registrar);
+ }
+
+ ///
+ /// 通过与生产代码一致的注册入口扫描并注册指定程序集中的 CQRS 处理器。
+ ///
+ /// 承载处理器映射的测试容器。
+ /// 要扫描的程序集集合。
+ internal static void RegisterHandlers(MicrosoftDiContainer container, params Assembly[] assemblies)
+ {
+ ArgumentNullException.ThrowIfNull(container);
+ ArgumentNullException.ThrowIfNull(assemblies);
+
+ RegisterInfrastructure(container);
+
+ var logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime));
+ RegisterHandlersMethod.Invoke(
+ null,
+ [container, assemblies.Where(static assembly => assembly is not null).Distinct().ToArray(), logger]);
+ }
+}
diff --git a/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj b/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj
new file mode 100644
index 00000000..acbcc536
--- /dev/null
+++ b/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net10.0
+ $(TestTargetFrameworks)
+ disable
+ enable
+ false
+ true
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/GFramework.Cqrs.Tests/GlobalUsings.cs b/GFramework.Cqrs.Tests/GlobalUsings.cs
new file mode 100644
index 00000000..d31630ed
--- /dev/null
+++ b/GFramework.Cqrs.Tests/GlobalUsings.cs
@@ -0,0 +1,26 @@
+// Copyright (c) 2026 GeWuYou
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+global using System;
+global using System.Collections;
+global using System.Collections.Generic;
+global using System.Diagnostics;
+global using System.Linq;
+global using System.Reflection;
+global using System.Runtime.CompilerServices;
+global using System.Threading;
+global using System.Threading.Tasks;
+global using Microsoft.Extensions.DependencyInjection;
+global using Moq;
+global using NUnit.Compatibility;
+global using NUnit.Framework;
diff --git a/GFramework.Cqrs.Tests/Logging/TestLogger.cs b/GFramework.Cqrs.Tests/Logging/TestLogger.cs
new file mode 100644
index 00000000..aaf65d22
--- /dev/null
+++ b/GFramework.Cqrs.Tests/Logging/TestLogger.cs
@@ -0,0 +1,56 @@
+// Copyright (c) 2026 GeWuYou
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using GFramework.Core.Abstractions.Logging;
+using GFramework.Core.Logging;
+
+namespace GFramework.Core.Tests.Logging;
+
+///
+/// 供 CQRS 测试项目复用的最小日志记录器实现。
+///
+public sealed class TestLogger : AbstractLogger
+{
+ ///
+ /// 初始化测试日志记录器。
+ ///
+ /// 日志名称。
+ /// 最小日志级别。
+ public TestLogger(string? name = null, LogLevel minLevel = LogLevel.Info) : base(name, minLevel)
+ {
+ }
+
+ ///
+ /// 获取当前测试期间捕获到的日志条目。
+ ///
+ public List Logs { get; } = [];
+
+ ///
+ /// 将日志写入内存,供断言使用。
+ ///
+ /// 日志级别。
+ /// 日志消息。
+ /// 关联异常。
+ protected override void Write(LogLevel level, string message, Exception? exception)
+ {
+ Logs.Add(new LogEntry(level, message, exception));
+ }
+
+ ///
+ /// 表示单条测试日志记录。
+ ///
+ /// 日志级别。
+ /// 日志消息。
+ /// 关联异常。
+ public sealed record LogEntry(LogLevel Level, string Message, Exception? Exception);
+}
diff --git a/GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs
similarity index 99%
rename from GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs
rename to GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs
index 2dc2503a..cadb340b 100644
--- a/GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs
+++ b/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs
@@ -1,5 +1,3 @@
-using System.Diagnostics;
-using System.Reflection;
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Architectures;
using GFramework.Core.Ioc;
diff --git a/GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs
similarity index 99%
rename from GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs
rename to GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs
index e176cce5..7d73a45e 100644
--- a/GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs
+++ b/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs
@@ -1,5 +1,3 @@
-using System.Diagnostics;
-using System.Reflection;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Architectures;
diff --git a/GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs
similarity index 99%
rename from GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs
rename to GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs
index 27dfed5c..cdaf9af6 100644
--- a/GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs
+++ b/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs
@@ -1,6 +1,3 @@
-using System.Diagnostics;
-using System.Reflection;
-using System.Runtime.CompilerServices;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Abstractions.Events;
diff --git a/GFramework.csproj b/GFramework.csproj
index 76f9a088..679cbf64 100644
--- a/GFramework.csproj
+++ b/GFramework.csproj
@@ -63,6 +63,9 @@
+
+
+
@@ -104,6 +107,9 @@
+
+
+
@@ -131,6 +137,9 @@
+
+
+
diff --git a/GFramework.sln b/GFramework.sln
index 67d53c69..088d63d3 100644
--- a/GFramework.sln
+++ b/GFramework.sln
@@ -42,6 +42,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs.Abstraction
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs", "GFramework.Cqrs\GFramework.Cqrs.csproj", "{E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs.Tests", "GFramework.Cqrs.Tests\GFramework.Cqrs.Tests.csproj", "{29037A55-9A89-425C-AB33-D44872B2E601}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -304,6 +306,18 @@ Global
{E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x64.Build.0 = Release|Any CPU
{E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x86.ActiveCfg = Release|Any CPU
{E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x86.Build.0 = Release|Any CPU
+ {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|x64.Build.0 = Debug|Any CPU
+ {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|x86.Build.0 = Debug|Any CPU
+ {29037A55-9A89-425C-AB33-D44872B2E601}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {29037A55-9A89-425C-AB33-D44872B2E601}.Release|Any CPU.Build.0 = Release|Any CPU
+ {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x64.ActiveCfg = Release|Any CPU
+ {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x64.Build.0 = Release|Any CPU
+ {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x86.ActiveCfg = Release|Any CPU
+ {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE