diff --git a/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs b/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs index 0eb6a43f..c7b62e2f 100644 --- a/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs +++ b/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs @@ -15,7 +15,7 @@ namespace GFramework.Core.Abstractions.Architectures; /// /// /// 旧的 GFramework.Core.Abstractions.CommandGFramework.Core.Abstractions.Query 契约会继续通过原有 Command/Query Executor 路径执行,以保证存量代码兼容。 -/// 新的 GFramework.Core.Abstractions.Cqrs 契约由内置 CQRS dispatcher 统一处理,支持 request pipeline、notification publish 与 stream request。 +/// 新的 GFramework.Cqrs.Abstractions.Cqrs 契约由内置 CQRS dispatcher 统一处理,支持 request pipeline、notification publish 与 stream request。 /// 新功能优先使用 与对应的 CQRS Command/Query 重载;迁移旧代码时可先保留旧入口,再逐步替换为 CQRS 请求模型。 /// public interface IArchitectureContext @@ -175,7 +175,7 @@ public interface IArchitectureContext /// 要发送的 CQRS 查询。 /// 查询结果。 /// - /// 这是迁移后的推荐查询入口。新查询应优先实现 GFramework.Core.Abstractions.Cqrs.Query.IQuery<TResponse>。 + /// 这是迁移后的推荐查询入口。新查询应优先实现 GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse>。 /// TResponse SendQuery(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query); diff --git a/GFramework.Core.Abstractions/Logging/LoggerFactoryResolver.cs b/GFramework.Core.Abstractions/Logging/LoggerFactoryResolver.cs new file mode 100644 index 00000000..fdffe5b2 --- /dev/null +++ b/GFramework.Core.Abstractions/Logging/LoggerFactoryResolver.cs @@ -0,0 +1,281 @@ +namespace GFramework.Core.Abstractions.Logging; + +/// +/// 提供全局日志工厂访问入口。 +/// +/// +/// 该类型位于抽象层,是为了让上层模块可以在不依赖 GFramework.Core 实现程序集的前提下 +/// 获取日志记录器。默认 provider 会优先通过反射解析 GFramework.Core 中的控制台实现, +/// 若宿主未加载该程序集,则退回到静默 provider,避免抽象层形成实现层循环依赖。 +/// +public static class LoggerFactoryResolver +{ + private static readonly object ProviderLock = new(); + + private static string DefaultProviderTypeName = + "GFramework.Core.Logging.ConsoleLoggerFactoryProvider, GFramework.Core"; + + private static ILoggerFactoryProvider? _provider; + + /// + /// 获取或设置当前日志工厂提供程序。 + /// + /// + /// 读取与赋值都会通过同一把锁串行化,确保并发调用方观察到确定的 provider 引用。 + /// 当调用方未显式赋值时,会在首次访问时尝试解析默认实现;若解析失败,则退回静默 provider。 + /// + /// + /// 当赋值为 时抛出。 + /// + public static ILoggerFactoryProvider Provider + { + get + { + lock (ProviderLock) + { + _provider ??= CreateDefaultProvider(); + return _provider; + } + } + set + { + var provider = value ?? throw new ArgumentNullException(nameof(value)); + + lock (ProviderLock) + { + _provider = provider; + } + } + } + + /// + /// 获取或设置新创建日志记录器的最小日志级别。 + /// + /// + /// 该属性直接代理到当前 ,确保调用方调整级别后立即影响后续创建的日志器。 + /// + public static LogLevel MinLevel + { + get => Provider.MinLevel; + set => Provider.MinLevel = value; + } + + private static ILoggerFactoryProvider CreateDefaultProvider() + { + try + { + if (Type.GetType(DefaultProviderTypeName, throwOnError: false) is { } providerType && + Activator.CreateInstance(providerType) is ILoggerFactoryProvider provider) + { + provider.MinLevel = LogLevel.Info; + return provider; + } + } + catch (Exception) + { + // The default provider is optional. Any load or activation failure must degrade to the silent provider so + // abstractions-only hosts can continue bootstrapping without the concrete logging assembly. + } + + return new SilentLoggerFactoryProvider(); + } + + /// + /// 当宿主未提供默认日志实现时使用的静默 provider。 + /// + private sealed class SilentLoggerFactoryProvider : ILoggerFactoryProvider + { + public LogLevel MinLevel { get; set; } = LogLevel.Info; + + public ILogger CreateLogger(string name) + { + return new SilentLogger(name); + } + } + + /// + /// 默认日志实现不可用时的 no-op 日志器。 + /// + private sealed class SilentLogger(string name) : ILogger + { + public string Name() + { + return name; + } + + public bool IsTraceEnabled() + { + return false; + } + + public bool IsDebugEnabled() + { + return false; + } + + public bool IsInfoEnabled() + { + return false; + } + + public bool IsWarnEnabled() + { + return false; + } + + public bool IsErrorEnabled() + { + return false; + } + + public bool IsFatalEnabled() + { + return false; + } + + public bool IsEnabledForLevel(LogLevel level) + { + return false; + } + + public void Trace(string msg) + { + } + + public void Trace(string format, object arg) + { + } + + public void Trace(string format, object arg1, object arg2) + { + } + + public void Trace(string format, params object[] arguments) + { + } + + public void Trace(string msg, Exception t) + { + } + + public void Debug(string msg) + { + } + + public void Debug(string format, object arg) + { + } + + public void Debug(string format, object arg1, object arg2) + { + } + + public void Debug(string format, params object[] arguments) + { + } + + public void Debug(string msg, Exception t) + { + } + + public void Info(string msg) + { + } + + public void Info(string format, object arg) + { + } + + public void Info(string format, object arg1, object arg2) + { + } + + public void Info(string format, params object[] arguments) + { + } + + public void Info(string msg, Exception t) + { + } + + public void Warn(string msg) + { + } + + public void Warn(string format, object arg) + { + } + + public void Warn(string format, object arg1, object arg2) + { + } + + public void Warn(string format, params object[] arguments) + { + } + + public void Warn(string msg, Exception t) + { + } + + public void Error(string msg) + { + } + + public void Error(string format, object arg) + { + } + + public void Error(string format, object arg1, object arg2) + { + } + + public void Error(string format, params object[] arguments) + { + } + + public void Error(string msg, Exception t) + { + } + + public void Fatal(string msg) + { + } + + public void Fatal(string format, object arg) + { + } + + public void Fatal(string format, object arg1, object arg2) + { + } + + public void Fatal(string format, params object[] arguments) + { + } + + public void Fatal(string msg, Exception t) + { + } + + public void Log(LogLevel level, string message) + { + } + + public void Log(LogLevel level, string format, object arg) + { + } + + public void Log(LogLevel level, string format, object arg1, object arg2) + { + } + + public void Log(LogLevel level, string format, params object[] arguments) + { + } + + public void Log(LogLevel level, string message, Exception exception) + { + } + } +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs index 67255b41..ddec08c8 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs @@ -1,8 +1,8 @@ using System.Reflection; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Logging; +using GFramework.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Tests.Architectures; diff --git a/GFramework.Core.Tests/Architectures/ArchitectureComponentRegistryBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureComponentRegistryBehaviorTests.cs index 583c30c8..f6cbdafa 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureComponentRegistryBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureComponentRegistryBehaviorTests.cs @@ -1,11 +1,11 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Enums; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; using GFramework.Core.Architectures; using GFramework.Core.Logging; -using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Tests.Architectures; @@ -714,4 +714,4 @@ public class ArchitectureComponentRegistryBehaviorTests return _context; } } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs index 87978cd2..cdcde44d 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs @@ -5,6 +5,7 @@ using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Enums; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Query; using GFramework.Core.Abstractions.Systems; diff --git a/GFramework.Core.Tests/Architectures/ArchitectureInitializationPipelineTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureInitializationPipelineTests.cs index 717e1b18..10945ad2 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureInitializationPipelineTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureInitializationPipelineTests.cs @@ -1,9 +1,9 @@ using GFramework.Core.Abstractions.Enums; using GFramework.Core.Abstractions.Events; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Environment; using GFramework.Core.Logging; -using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Tests.Architectures; @@ -185,4 +185,4 @@ public class ArchitectureInitializationPipelineTests private sealed class BootstrapMarker { } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs index b0ef7262..943e2bfd 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs @@ -2,12 +2,12 @@ using System.Reflection; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Enums; using GFramework.Core.Abstractions.Lifecycle; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; using GFramework.Core.Architectures; using GFramework.Core.Logging; -using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Tests.Architectures; @@ -460,4 +460,4 @@ public class ArchitectureLifecycleBehaviorTests return _context; } } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs index cfe0db79..493f3590 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs @@ -1,4 +1,5 @@ using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Utility; using GFramework.Core.Architectures; using GFramework.Core.Logging; diff --git a/GFramework.Core.Tests/Architectures/PriorityServiceTests.cs b/GFramework.Core.Tests/Architectures/PriorityServiceTests.cs index 24fd2681..0499a4ab 100644 --- a/GFramework.Core.Tests/Architectures/PriorityServiceTests.cs +++ b/GFramework.Core.Tests/Architectures/PriorityServiceTests.cs @@ -1,5 +1,6 @@ using System.Reflection; using GFramework.Core.Abstractions.Bases; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; @@ -244,4 +245,4 @@ public class PriorityTestUtilityC : IPriorityTestUtility, IPrioritized public int Priority => 30; } -#endregion \ No newline at end of file +#endregion diff --git a/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs b/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs new file mode 100644 index 00000000..64f69f02 --- /dev/null +++ b/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs @@ -0,0 +1,76 @@ +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Command; +using GFramework.Cqrs.Abstractions.Cqrs.Notification; +using GFramework.Cqrs.Abstractions.Cqrs.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Request; +using GFramework.Cqrs.Command; +using GFramework.Cqrs.Notification; +using GFramework.Cqrs.Query; +using GFramework.Cqrs.Request; + +namespace GFramework.Core.Tests.Cqrs; + +/// +/// 锁定 CQRS 基础消息类型在 runtime 拆分后的公开命名空间与程序集兼容性。 +/// +[TestFixture] +public sealed class CqrsPublicNamespaceCompatibilityTests +{ + /// + /// 验证基础消息类型继续暴露在历史公开 CQRS 命名空间(GFramework.Cqrs.*),同时由独立 runtime 程序集承载实现。 + /// + [Test] + public void Base_Message_Types_Should_Live_In_Cqrs_Namespaces_And_Runtime_Assembly() + { + Assert.Multiple(() => + { + AssertLegacyType(typeof(CommandBase), "GFramework.Cqrs.Command"); + AssertLegacyType(typeof(QueryBase), "GFramework.Cqrs.Query"); + AssertLegacyType(typeof(RequestBase), "GFramework.Cqrs.Request"); + AssertLegacyType(typeof(NotificationBase), "GFramework.Cqrs.Notification"); + }); + } + + /// + /// 验证旧的 GFramework.Core 程序集限定名仍可解析到迁移后的 runtime 实现类型。 + /// + [Test] + public void Type_Forwarding_Should_Resolve_Cqrs_Types_From_Core_Assembly() + { + Assert.Multiple(() => + { + AssertForwardedType("GFramework.Cqrs.Command.CommandBase`2, GFramework.Core"); + AssertForwardedType("GFramework.Cqrs.Query.QueryBase`2, GFramework.Core"); + AssertForwardedType("GFramework.Cqrs.Request.RequestBase`2, GFramework.Core"); + AssertForwardedType("GFramework.Cqrs.Notification.NotificationBase`1, GFramework.Core"); + }); + } + + private static void AssertLegacyType(Type type, string expectedNamespace) + { + Assert.Multiple(() => + { + Assert.That(type.Namespace, Is.EqualTo(expectedNamespace)); + Assert.That(type.Assembly.GetName().Name, Is.EqualTo("GFramework.Cqrs")); + }); + } + + private static void AssertForwardedType(string assemblyQualifiedTypeName) + { + var resolvedType = Type.GetType(assemblyQualifiedTypeName, throwOnError: false); + + Assert.Multiple(() => + { + Assert.That(resolvedType, Is.Not.Null); + Assert.That(resolvedType!.Assembly.GetName().Name, Is.EqualTo("GFramework.Cqrs")); + }); + } + + private sealed record TestCommandInput : ICommandInput; + + private sealed record TestQueryInput : IQueryInput; + + private sealed record TestRequestInput : IRequestInput; + + private sealed record TestNotificationInput : INotificationInput; +} diff --git a/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs b/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs index b25806f0..a6cc927f 100644 --- a/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs +++ b/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs @@ -38,10 +38,10 @@ public class MediatorCompatibilityDeprecationTests "Use GFramework.Core.Extensions.ContextAwareCqrsExtensions instead."); AssertLegacyType( typeof(ContextAwareMediatorCommandExtensions), - "Use GFramework.Core.Extensions.ContextAwareCqrsCommandExtensions instead."); + "Use GFramework.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead."); AssertLegacyType( typeof(ContextAwareMediatorQueryExtensions), - "Use GFramework.Core.Extensions.ContextAwareCqrsQueryExtensions instead."); + "Use GFramework.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead."); AssertLegacyType( typeof(MediatorCoroutineExtensions), "Use GFramework.Core.Coroutine.Extensions.CqrsCoroutineExtensions instead."); diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index 0e03059e..4104ccb1 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -1,6 +1,7 @@ using System.Reflection; using GFramework.Core.Abstractions.Bases; using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Tests.Cqrs; diff --git a/GFramework.Core.Tests/Logging/LoggerFactoryTests.cs b/GFramework.Core.Tests/Logging/LoggerFactoryTests.cs index 8ba7ac91..d175b32f 100644 --- a/GFramework.Core.Tests/Logging/LoggerFactoryTests.cs +++ b/GFramework.Core.Tests/Logging/LoggerFactoryTests.cs @@ -1,18 +1,19 @@ using System.IO; +using System.Reflection; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Logging; -using NUnit.Framework; namespace GFramework.Core.Tests.Logging; /// -/// 测试LoggerFactory相关功能的测试类 +/// 测试 LoggerFactory 相关功能的测试类。 /// [TestFixture] +[NonParallelizable] public class LoggerFactoryTests { /// - /// 测试ConsoleLoggerFactory的GetLogger方法是否返回ConsoleLogger实例 + /// 测试 ConsoleLoggerFactory 的 GetLogger 方法是否返回 ConsoleLogger 实例。 /// [Test] public void ConsoleLoggerFactory_GetLogger_ShouldReturnConsoleLogger() @@ -26,7 +27,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactory使用不同名称获取不同的logger实例 + /// 测试 ConsoleLoggerFactory 使用不同名称获取不同的 logger 实例。 /// [Test] public void ConsoleLoggerFactory_GetLogger_WithDifferentNames_ShouldReturnDifferentLoggers() @@ -40,7 +41,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactory使用默认最小级别时的行为(默认为Info级别) + /// 测试 ConsoleLoggerFactory 使用默认最小级别时的行为。 /// [Test] public void ConsoleLoggerFactory_GetLogger_WithDefaultMinLevel_ShouldUseInfo() @@ -51,7 +52,6 @@ public class LoggerFactoryTests var stringWriter = new StringWriter(); var testLogger = new ConsoleLogger("TestLogger", LogLevel.Info, stringWriter, false); - // 验证Debug消息不会被记录,但Info消息会被记录 testLogger.Debug("Debug message"); testLogger.Info("Info message"); @@ -61,7 +61,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactoryProvider创建logger时使用提供者的最小级别设置 + /// 测试 ConsoleLoggerFactoryProvider 创建 logger 时使用提供者的最小级别设置。 /// [Test] public void ConsoleLoggerFactoryProvider_CreateLogger_ShouldReturnLoggerWithProviderMinLevel() @@ -72,7 +72,6 @@ public class LoggerFactoryTests var stringWriter = new StringWriter(); var testLogger = new ConsoleLogger("TestLogger", LogLevel.Debug, stringWriter, false); - // 验证Debug消息会被记录,但Trace消息不会被记录 testLogger.Debug("Debug message"); testLogger.Trace("Trace message"); @@ -82,7 +81,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactoryProvider创建logger时使用提供的名称 + /// 测试 ConsoleLoggerFactoryProvider 创建 logger 时使用提供的名称。 /// [Test] public void ConsoleLoggerFactoryProvider_CreateLogger_ShouldUseProvidedName() @@ -94,7 +93,7 @@ public class LoggerFactoryTests } /// - /// 测试LoggerFactoryResolver的Provider属性是否有默认值 + /// 测试 LoggerFactoryResolver 的 Provider 属性是否有默认值。 /// [Test] public void LoggerFactoryResolver_Provider_ShouldHaveDefaultValue() @@ -104,7 +103,7 @@ public class LoggerFactoryTests } /// - /// 测试LoggerFactoryResolver的Provider属性可以被更改 + /// 测试 LoggerFactoryResolver 的 Provider 属性可以被更改。 /// [Test] public void LoggerFactoryResolver_Provider_CanBeChanged() @@ -120,7 +119,7 @@ public class LoggerFactoryTests } /// - /// 测试LoggerFactoryResolver的MinLevel属性是否有默认值 + /// 测试 LoggerFactoryResolver 的 MinLevel 属性是否有默认值。 /// [Test] public void LoggerFactoryResolver_MinLevel_ShouldHaveDefaultValue() @@ -129,7 +128,7 @@ public class LoggerFactoryTests } /// - /// 测试LoggerFactoryResolver的MinLevel属性可以被更改 + /// 测试 LoggerFactoryResolver 的 MinLevel 属性可以被更改。 /// [Test] public void LoggerFactoryResolver_MinLevel_CanBeChanged() @@ -144,7 +143,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactoryProvider的MinLevel属性是否有默认值 + /// 测试 ConsoleLoggerFactoryProvider 的 MinLevel 属性是否有默认值。 /// [Test] public void ConsoleLoggerFactoryProvider_MinLevel_ShouldHaveDefaultValue() @@ -155,7 +154,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactoryProvider的MinLevel属性可以被更改 + /// 测试 ConsoleLoggerFactoryProvider 的 MinLevel 属性可以被更改。 /// [Test] public void ConsoleLoggerFactoryProvider_MinLevel_CanBeChanged() @@ -168,7 +167,7 @@ public class LoggerFactoryTests } /// - /// 测试LoggerFactoryResolver的Provider创建logger时使用提供者设置 + /// 测试 LoggerFactoryResolver 的 Provider 创建 logger 时使用提供者设置。 /// [Test] public void LoggerFactoryResolver_Provider_CreateLogger_ShouldUseProviderSettings() @@ -183,7 +182,6 @@ public class LoggerFactoryTests var stringWriter = new StringWriter(); var testLogger = new ConsoleLogger("TestLogger", LogLevel.Warning, stringWriter, false); - // 验证Warn消息会被记录,但Info消息不会被记录 testLogger.Warn("Warn message"); testLogger.Info("Info message"); @@ -195,7 +193,7 @@ public class LoggerFactoryTests } /// - /// 测试LoggerFactoryResolver的MinLevel属性影响新创建的logger + /// 测试 LoggerFactoryResolver 的 MinLevel 属性影响新创建的 logger。 /// [Test] public void LoggerFactoryResolver_MinLevel_AffectsNewLoggers() @@ -210,7 +208,6 @@ public class LoggerFactoryTests var stringWriter = new StringWriter(); var testLogger = new ConsoleLogger("TestLogger", LogLevel.Error, stringWriter, false); - // 验证Error消息会被记录,但Warn消息不会被记录 testLogger.Error("Error message"); testLogger.Warn("Warn message"); @@ -222,7 +219,93 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactory创建的多个logger实例是独立的 + /// 验证默认 provider 激活失败时会回退到静默 provider。 + /// + [Test] + public void + LoggerFactoryResolver_Provider_Should_Fall_Back_To_SilentProvider_When_DefaultProvider_Activation_Fails() + { + var originalProvider = LoggerFactoryResolver.Provider; + var originalTypeName = GetDefaultProviderTypeName(); + + try + { + ResetProvider(); + SetDefaultProviderTypeName(typeof(ThrowingLoggerFactoryProvider).AssemblyQualifiedName!); + + var provider = LoggerFactoryResolver.Provider; + var logger = provider.CreateLogger("Fallback"); + + Assert.Multiple(() => + { + Assert.That(provider.GetType().Name, Is.EqualTo("SilentLoggerFactoryProvider")); + Assert.That(provider.MinLevel, Is.EqualTo(LogLevel.Info)); + Assert.That(logger.IsEnabledForLevel(LogLevel.Error), Is.False); + }); + } + finally + { + SetDefaultProviderTypeName(originalTypeName); + LoggerFactoryResolver.Provider = originalProvider; + } + } + + /// + /// 验证并发首次访问默认 provider 时只会创建一个实例,并向所有调用方返回相同引用。 + /// + [Test] + public async Task + LoggerFactoryResolver_Provider_Should_Create_A_Single_Default_Instance_When_Accessed_Concurrently() + { + var originalProvider = LoggerFactoryResolver.Provider; + var originalTypeName = GetDefaultProviderTypeName(); + + try + { + BlockingLoggerFactoryProvider.Reset(); + ResetProvider(); + SetDefaultProviderTypeName(typeof(BlockingLoggerFactoryProvider).AssemblyQualifiedName!); + + var startGate = new ManualResetEventSlim(false); + var tasks = Enumerable.Range(0, 8) + .Select(_ => Task.Run(() => + { + startGate.Wait(); + return LoggerFactoryResolver.Provider; + })) + .ToArray(); + + startGate.Set(); + + Assert.That( + SpinWait.SpinUntil( + () => BlockingLoggerFactoryProvider.ConstructionCount >= 1, + TimeSpan.FromSeconds(2)), + Is.True, + "The test provider should start construction after concurrent access begins."); + + BlockingLoggerFactoryProvider.ReleaseConstruction(); + + var providers = await Task.WhenAll(tasks); + + Assert.Multiple(() => + { + Assert.That(BlockingLoggerFactoryProvider.ConstructionCount, Is.EqualTo(1)); + Assert.That(providers.Distinct().Count(), Is.EqualTo(1)); + Assert.That(LoggerFactoryResolver.Provider, Is.SameAs(providers[0])); + }); + } + finally + { + BlockingLoggerFactoryProvider.ReleaseConstruction(); + BlockingLoggerFactoryProvider.Reset(); + SetDefaultProviderTypeName(originalTypeName); + LoggerFactoryResolver.Provider = originalProvider; + } + } + + /// + /// 测试 ConsoleLoggerFactory 创建的多个 logger 实例是独立的。 /// [Test] public void ConsoleLoggerFactory_MultipleLoggers_ShouldBeIndependent() @@ -236,7 +319,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactoryProvider的MinLevel不会影响已创建的logger + /// 测试 ConsoleLoggerFactoryProvider 的 MinLevel 不会影响已创建的 logger。 /// [Test] public void ConsoleLoggerFactoryProvider_MinLevel_DoesNotAffectCreatedLogger() @@ -247,7 +330,6 @@ public class LoggerFactoryTests var stringWriter = new StringWriter(); var testLogger = new ConsoleLogger("TestLogger", LogLevel.Error, stringWriter, false); - // 验证Error和Fatal消息都会被记录 testLogger.Error("Error message"); testLogger.Fatal("Fatal message"); @@ -255,4 +337,114 @@ public class LoggerFactoryTests Assert.That(output, Does.Contain("Error message")); Assert.That(output, Does.Contain("Fatal message")); } -} \ No newline at end of file + + private static string GetDefaultProviderTypeName() + { + return (string)GetResolverField("DefaultProviderTypeName").GetValue(null)!; + } + + private static void SetDefaultProviderTypeName(string typeName) + { + GetResolverField("DefaultProviderTypeName").SetValue(null, typeName); + } + + private static void ResetProvider() + { + GetResolverField("_provider").SetValue(null, null); + } + + private static FieldInfo GetResolverField(string fieldName) + { + return typeof(LoggerFactoryResolver).GetField( + fieldName, + BindingFlags.NonPublic | BindingFlags.Static) + ?? throw new InvalidOperationException( + $"Failed to locate LoggerFactoryResolver.{fieldName}."); + } + + /// + /// 用于触发默认 provider 激活失败回退路径的测试桩。 + /// + public sealed class ThrowingLoggerFactoryProvider : ILoggerFactoryProvider + { + /// + /// 初始化一个始终抛出异常的 provider。 + /// + /// 始终抛出,用于覆盖回退路径。 + public ThrowingLoggerFactoryProvider() + { + throw new InvalidOperationException("Simulated provider activation failure."); + } + + /// + /// 获取或设置最小日志级别。 + /// + public LogLevel MinLevel { get; set; } = LogLevel.Info; + + /// + /// 创建日志器。 + /// + /// 日志器名称。 + /// 该测试桩永远不会成功创建日志器。 + /// 始终抛出,因为该方法不应被调用。 + public ILogger CreateLogger(string name) + { + throw new NotSupportedException(); + } + } + + /// + /// 用于验证并发首次初始化路径只创建单个 provider 实例的测试桩。 + /// + public sealed class BlockingLoggerFactoryProvider : ILoggerFactoryProvider + { + private static int _constructionCount; + private static ManualResetEventSlim _constructionGate = new(false); + + /// + /// 初始化一个会阻塞构造完成的 provider,用于放大并发首次访问竞争窗口。 + /// + public BlockingLoggerFactoryProvider() + { + Interlocked.Increment(ref _constructionCount); + _constructionGate.Wait(TimeSpan.FromSeconds(5)); + } + + /// + /// 获取已经发生的构造次数。 + /// + public static int ConstructionCount => Volatile.Read(ref _constructionCount); + + /// + /// 获取或设置最小日志级别。 + /// + public LogLevel MinLevel { get; set; } = LogLevel.Info; + + /// + /// 创建测试日志器。 + /// + /// 日志器名称。 + /// 带有当前最小级别设置的控制台日志器。 + public ILogger CreateLogger(string name) + { + return new ConsoleLogger(name, MinLevel, TextWriter.Null, false); + } + + /// + /// 重置该测试桩的并发观测状态。 + /// + public static void Reset() + { + _constructionGate = new ManualResetEventSlim(false); + Interlocked.Exchange(ref _constructionCount, 0); + } + + /// + /// 释放当前被阻塞的 provider 构造过程。 + /// + public static void ReleaseConstruction() + { + _constructionGate.Set(); + } + } +} diff --git a/GFramework.Core.Tests/State/StateMachineSystemTests.cs b/GFramework.Core.Tests/State/StateMachineSystemTests.cs index 0ea62de6..2c234238 100644 --- a/GFramework.Core.Tests/State/StateMachineSystemTests.cs +++ b/GFramework.Core.Tests/State/StateMachineSystemTests.cs @@ -1,5 +1,6 @@ using System.Reflection; using GFramework.Core.Abstractions.Enums; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.State; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Architectures; @@ -373,4 +374,4 @@ public class TestStateV5_2 : IState } } -#endregion \ No newline at end of file +#endregion diff --git a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs index 4ab0692b..24490239 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using GFramework.Core.Abstractions.Rule; using GFramework.Cqrs.Abstractions.Cqrs.Command; +using GFramework.Cqrs.Extensions; namespace GFramework.Core.Extensions; @@ -11,7 +12,7 @@ namespace GFramework.Core.Extensions; /// [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete( - "Use GFramework.Core.Extensions.ContextAwareCqrsCommandExtensions instead. This compatibility alias will be removed in a future major version.")] + "Use GFramework.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead. This compatibility alias will be removed in a future major version.")] public static class ContextAwareMediatorCommandExtensions { /// diff --git a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs index f2294930..c7aec1b6 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using GFramework.Core.Abstractions.Rule; using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Extensions; namespace GFramework.Core.Extensions; diff --git a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs index 61b02cb9..cf0b4513 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using GFramework.Core.Abstractions.Rule; using GFramework.Cqrs.Abstractions.Cqrs.Query; +using GFramework.Cqrs.Extensions; namespace GFramework.Core.Extensions; @@ -11,7 +12,7 @@ namespace GFramework.Core.Extensions; /// [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete( - "Use GFramework.Core.Extensions.ContextAwareCqrsQueryExtensions instead. This compatibility alias will be removed in a future major version.")] + "Use GFramework.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead. This compatibility alias will be removed in a future major version.")] public static class ContextAwareMediatorQueryExtensions { /// diff --git a/GFramework.Core/GFramework.Core.csproj b/GFramework.Core/GFramework.Core.csproj index f3e41eab..2535d4e9 100644 --- a/GFramework.Core/GFramework.Core.csproj +++ b/GFramework.Core/GFramework.Core.csproj @@ -10,6 +10,7 @@ + diff --git a/GFramework.Core/Logging/LoggerFactoryResolver.cs b/GFramework.Core/Logging/LoggerFactoryResolver.cs deleted file mode 100644 index c15378d1..00000000 --- a/GFramework.Core/Logging/LoggerFactoryResolver.cs +++ /dev/null @@ -1,26 +0,0 @@ -using GFramework.Core.Abstractions.Logging; - -namespace GFramework.Core.Logging; - -/// -/// 日志工厂提供程序解析器,用于管理和提供日志工厂提供程序实例 -/// -public static class LoggerFactoryResolver -{ - /// - /// 获取或设置当前的日志工厂提供程序 - /// - /// - /// 日志工厂提供程序实例,默认为控制台日志工厂提供程序 - /// - public static ILoggerFactoryProvider Provider { get; set; } - = new ConsoleLoggerFactoryProvider(); - - /// - /// 获取或设置日志记录的最小级别 - /// - /// - /// 日志级别枚举值,默认为Info级别 - /// - public static LogLevel MinLevel { get; set; } = LogLevel.Info; -} \ No newline at end of file diff --git a/GFramework.Core/Properties/TypeForwarders.cs b/GFramework.Core/Properties/TypeForwarders.cs new file mode 100644 index 00000000..2c260462 --- /dev/null +++ b/GFramework.Core/Properties/TypeForwarders.cs @@ -0,0 +1,12 @@ +using System.Runtime.CompilerServices; +using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs.Command; +using GFramework.Cqrs.Notification; +using GFramework.Cqrs.Query; +using GFramework.Cqrs.Request; + +[assembly: TypeForwardedTo(typeof(LoggerFactoryResolver))] +[assembly: TypeForwardedTo(typeof(CommandBase<,>))] +[assembly: TypeForwardedTo(typeof(QueryBase<,>))] +[assembly: TypeForwardedTo(typeof(RequestBase<,>))] +[assembly: TypeForwardedTo(typeof(NotificationBase<>))] diff --git a/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs index 5ca7a909..3fe37558 100644 --- a/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs +++ b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs @@ -1,8 +1,8 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; -using GFramework.Core.Cqrs.Internal; -using GFramework.Core.Logging; +using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Services.Modules; @@ -37,11 +37,12 @@ public sealed class CqrsRuntimeModule : IServiceModule { ArgumentNullException.ThrowIfNull(container); - var dispatcherLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsDispatcher)); - var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(DefaultCqrsHandlerRegistrar)); + var dispatcherLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher"); + var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar"); - container.Register(new CqrsDispatcher(container, dispatcherLogger)); - container.Register(new DefaultCqrsHandlerRegistrar(container, registrarLogger)); + container.Register(CqrsRuntimeFactory.CreateRuntime(container, dispatcherLogger)); + container.Register( + CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger)); } /// diff --git a/GFramework.Cqrs.Tests/Cqrs/AbstractCqrsHandlerContextTests.cs b/GFramework.Cqrs.Tests/Cqrs/AbstractCqrsHandlerContextTests.cs new file mode 100644 index 00000000..58e86f89 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/AbstractCqrsHandlerContextTests.cs @@ -0,0 +1,74 @@ +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Command; +using GFramework.Cqrs.Cqrs.Command; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 验证 CQRS handler 基类在脱离 dispatcher 使用时会显式失败,并在注入上下文后保持可观察行为。 +/// +[TestFixture] +internal sealed class AbstractCqrsHandlerContextTests +{ + /// + /// 验证新的轻量 handler 基类不会再偷偷回退到全局 GameContext。 + /// + [Test] + public void GetContext_Should_Throw_When_Handler_Has_Not_Been_Initialized_By_Runtime() + { + var handler = new TestCommandHandler(); + + var exception = Assert.Throws(() => ((IContextAware)handler).GetContext()); + + Assert.That( + exception!.Message, + Does.Contain("has not been initialized").IgnoreCase); + } + + /// + /// 验证 runtime 注入上下文后,派生 handler 可以继续访问 Context 并收到 OnContextReady 回调。 + /// + [Test] + public async Task Handle_Should_Observe_Injected_Context_And_OnContextReady_Callback() + { + var handler = new TestCommandHandler(); + var context = new Mock(MockBehavior.Strict).Object; + + ((IContextAware)handler).SetContext(context); + await handler.Handle(new TestCommand(), CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(handler.OnContextReadyCallCount, Is.EqualTo(1)); + Assert.That(handler.LastObservedContext, Is.SameAs(context)); + }); + } + + /// + /// 用于验证上下文注入行为的最小 CQRS 命令。 + /// + private sealed record TestCommand : ICommand; + + /// + /// 暴露基类上下文访问与初始化回调的测试处理器。 + /// + private sealed class TestCommandHandler : AbstractCommandHandler + { + public int OnContextReadyCallCount { get; private set; } + + public IArchitectureContext? LastObservedContext { get; private set; } + + protected override void OnContextReady() + { + OnContextReadyCallCount++; + } + + public override ValueTask Handle(TestCommand command, CancellationToken cancellationToken) + { + LastObservedContext = Context; + return ValueTask.FromResult(Unit.Value); + } + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index 360fe97b..3100ce84 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -1,4 +1,3 @@ -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Ioc; diff --git a/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs index 257809f0..fe8f4413 100644 --- a/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs @@ -1,3 +1,4 @@ +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Ioc; using GFramework.Core.Logging; diff --git a/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs index e403735e..728f005a 100644 --- a/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs @@ -1,4 +1,5 @@ using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Command; using GFramework.Core.Ioc; diff --git a/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs index b0b510d6..423b1c9b 100644 --- a/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs @@ -1,5 +1,6 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Events; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Command; using GFramework.Core.Environment; diff --git a/GFramework.Core/Cqrs/Command/CommandBase.cs b/GFramework.Cqrs/Command/CommandBase.cs similarity index 53% rename from GFramework.Core/Cqrs/Command/CommandBase.cs rename to GFramework.Cqrs/Command/CommandBase.cs index d8232608..486e4136 100644 --- a/GFramework.Core/Cqrs/Command/CommandBase.cs +++ b/GFramework.Cqrs/Command/CommandBase.cs @@ -13,19 +13,23 @@ using GFramework.Cqrs.Abstractions.Cqrs.Command; -namespace GFramework.Core.Cqrs.Command; +namespace GFramework.Cqrs.Command; /// -/// 表示一个基础命令类,用于处理带有输入和响应的命令模式实现。 -/// 该类实现了 ICommand<TResponse> 接口,提供了通用的命令结构。 +/// 为携带输入模型的 CQRS 命令提供统一基类。 /// -/// 命令输入数据的类型 -/// 命令执行后返回结果的类型 -/// 命令执行所需的输入数据 -public abstract class CommandBase(TInput input) : ICommand where TInput : ICommandInput +/// 命令输入类型。 +/// 命令响应类型。 +/// 命令执行所需的输入对象。 +/// +/// 该类型继续保留在历史公开命名空间中,以避免调用方因 runtime 程序集拆分而批量修改继承层次。 +/// 具体实现现由 GFramework.Cqrs 程序集承载,并通过 type forward 维持旧程序集兼容性。 +/// +public abstract class CommandBase(TInput input) : ICommand + where TInput : ICommandInput { /// - /// 获取命令的输入数据。 + /// 获取命令执行时携带的输入对象。 /// public TInput Input => input; } diff --git a/GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs b/GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs similarity index 71% rename from GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs rename to GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs index 7230f53d..63896c92 100644 --- a/GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs +++ b/GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs @@ -11,19 +11,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Diagnostics; using GFramework.Core.Abstractions.Logging; -using GFramework.Core.Logging; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Behaviors; +namespace GFramework.Cqrs.Cqrs.Behaviors; /// -/// 日志记录行为类,用于在CQRS管道中记录请求处理的日志信息 -/// 实现IPipelineBehavior接口,为请求处理提供日志记录功能 +/// 在 CQRS 请求管道中记录请求开始、完成、取消与失败日志。 /// -/// 请求类型,必须实现IRequest接口 -/// 响应类型 +/// 请求类型。 +/// 响应类型。 +/// +/// 该行为已迁移到 GFramework.Cqrs.Cqrs.Behaviors 命名空间, +/// 实现位于 GFramework.Cqrs 程序集,用于承载 CQRS runtime 细节并与旧层解耦。 +/// public sealed class LoggingBehavior : IPipelineBehavior where TRequest : IRequest { @@ -31,13 +32,12 @@ public sealed class LoggingBehavior : IPipelineBehavior)); /// - /// 处理请求并记录日志 - /// 在请求处理前后记录调试信息,处理异常时记录错误日志 + /// 执行日志包装后的下一段请求处理逻辑。 /// - /// 要处理的请求消息 - /// 下一个处理委托,用于继续管道执行 - /// 取消令牌,用于取消操作 - /// 处理结果的ValueTask + /// 当前请求消息。 + /// 后续处理委托。 + /// 取消令牌。 + /// 请求处理结果。 public async ValueTask Handle( TRequest message, MessageHandlerDelegate next, diff --git a/GFramework.Core/Cqrs/Behaviors/PerformanceBehavior.cs b/GFramework.Cqrs/Cqrs/Behaviors/PerformanceBehavior.cs similarity index 61% rename from GFramework.Core/Cqrs/Behaviors/PerformanceBehavior.cs rename to GFramework.Cqrs/Cqrs/Behaviors/PerformanceBehavior.cs index 35ab2978..1d13319d 100644 --- a/GFramework.Core/Cqrs/Behaviors/PerformanceBehavior.cs +++ b/GFramework.Cqrs/Cqrs/Behaviors/PerformanceBehavior.cs @@ -11,33 +11,34 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Diagnostics; using GFramework.Core.Abstractions.Logging; -using GFramework.Core.Logging; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Behaviors; +namespace GFramework.Cqrs.Cqrs.Behaviors; /// -/// 性能监控行为类,用于监控CQRS请求的执行时间 -/// 实现IPipelineBehavior接口,检测并记录执行时间过长的请求 +/// 在 CQRS 请求管道中监控处理耗时,并对长耗时请求发出告警。 /// -/// 请求类型,必须实现IRequest接口 -/// 响应类型 +/// 请求类型。 +/// 响应类型。 +/// +/// 该行为保留现有公开命名空间以维持消费端兼容性,但实现已迁入 GFramework.Cqrs 程序集。 +/// public sealed class PerformanceBehavior : IPipelineBehavior where TRequest : IRequest { + private const double SlowRequestThresholdMilliseconds = 500; + private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(PerformanceBehavior)); /// - /// 处理请求并监控执行时间 - /// 使用Stopwatch测量请求处理耗时,超过500ms时记录警告日志 + /// 统计当前请求处理耗时,并在超阈值时记录警告日志。 /// - /// 要处理的请求消息 - /// 下一个处理委托,用于继续管道执行 - /// 取消令牌,用于取消操作 - /// 处理结果的ValueTask + /// 当前请求消息。 + /// 后续处理委托。 + /// 取消令牌。 + /// 请求处理结果。 public async ValueTask Handle( TRequest message, MessageHandlerDelegate next, @@ -53,11 +54,10 @@ public sealed class PerformanceBehavior : IPipelineBehavior { var elapsed = Stopwatch.GetElapsedTime(start); - if (elapsed.TotalMilliseconds > 500) + if (elapsed.TotalMilliseconds > SlowRequestThresholdMilliseconds) { var requestName = typeof(TRequest).Name; - _logger.Warn( - $"Long Running Request: {requestName} ({elapsed.TotalMilliseconds:F2} ms)"); + _logger.Warn($"Long Running Request: {requestName} ({elapsed.TotalMilliseconds:F2} ms)"); } } } diff --git a/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs b/GFramework.Cqrs/Cqrs/Command/AbstractCommandHandler.cs similarity index 83% rename from GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs rename to GFramework.Cqrs/Cqrs/Command/AbstractCommandHandler.cs index d7ebb117..825737bf 100644 --- a/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs +++ b/GFramework.Cqrs/Cqrs/Command/AbstractCommandHandler.cs @@ -11,19 +11,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Command; -namespace GFramework.Core.Cqrs.Command; +namespace GFramework.Cqrs.Cqrs.Command; /// /// 抽象命令处理器基类 -/// 继承自 ContextAwareBase 并实现 IRequestHandler 接口,为具体的命令处理器提供基础功能。 +/// 继承自轻量 CQRS 上下文基类并实现 IRequestHandler 接口,为具体的命令处理器提供基础功能。 /// 框架会在每次分发前注入当前架构上下文,因此派生类可以通过 Context 访问架构级服务。 /// /// 命令类型 -public abstract class AbstractCommandHandler : ContextAwareBase, IRequestHandler +public abstract class AbstractCommandHandler : CqrsContextAwareHandlerBase, IRequestHandler where TCommand : ICommand { /// @@ -38,12 +37,13 @@ public abstract class AbstractCommandHandler : ContextAwareBase, IRequ /// /// 抽象命令处理器基类(带返回值版本) -/// 继承自 ContextAwareBase 并实现 IRequestHandler 接口,为具体的命令处理器提供基础功能。 +/// 继承自轻量 CQRS 上下文基类并实现 IRequestHandler 接口,为具体的命令处理器提供基础功能。 /// 支持泛型命令和结果类型,框架会在每次分发前注入当前架构上下文。 /// /// 命令类型,必须实现ICommand接口 /// 命令执行结果类型 -public abstract class AbstractCommandHandler : ContextAwareBase, IRequestHandler +public abstract class AbstractCommandHandler : CqrsContextAwareHandlerBase, + IRequestHandler where TCommand : ICommand { /// diff --git a/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs b/GFramework.Cqrs/Cqrs/Command/AbstractStreamCommandHandler.cs similarity index 92% rename from GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs rename to GFramework.Cqrs/Cqrs/Command/AbstractStreamCommandHandler.cs index 223a9cc5..cb4ccb6b 100644 --- a/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs +++ b/GFramework.Cqrs/Cqrs/Command/AbstractStreamCommandHandler.cs @@ -11,15 +11,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Command; -namespace GFramework.Core.Cqrs.Command; +namespace GFramework.Cqrs.Cqrs.Command; /// /// 抽象流式命令处理器基类。 -/// 继承自 并实现 , +/// 继承自轻量 CQRS 上下文基类并实现 , /// 为具体的流式命令处理器提供基础功能。 /// /// 流式命令类型,必须实现 @@ -32,7 +31,7 @@ namespace GFramework.Core.Cqrs.Command; /// 传入 的取消令牌同时约束流的创建与后续枚举, /// 派生类应在启动阶段和每次生成响应前尊重取消请求,避免在调用方停止枚举后继续执行后台工作。 /// -public abstract class AbstractStreamCommandHandler : ContextAwareBase, +public abstract class AbstractStreamCommandHandler : CqrsContextAwareHandlerBase, IStreamRequestHandler where TCommand : IStreamCommand { diff --git a/GFramework.Cqrs/Cqrs/CqrsContextAwareHandlerBase.cs b/GFramework.Cqrs/Cqrs/CqrsContextAwareHandlerBase.cs new file mode 100644 index 00000000..73e25ff1 --- /dev/null +++ b/GFramework.Cqrs/Cqrs/CqrsContextAwareHandlerBase.cs @@ -0,0 +1,59 @@ +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Rule; + +namespace GFramework.Cqrs.Cqrs; + +/// +/// 为 CQRS 处理器提供最小化的上下文感知基类实现。 +/// +/// +/// 该基类只承接 CQRS runtime 在分发前注入的 , +/// 不再像 ContextAwareBase 那样回退到 GameContext 全局查找。 +/// 这样可以让 GFramework.Cqrs 保持对 GFramework.Core 运行时实现的零依赖, +/// 同时在处理器被错误地脱离 dispatcher 使用时以显式异常快速失败。 +/// +public abstract class CqrsContextAwareHandlerBase : IContextAware +{ + private IArchitectureContext? _context; + + /// + /// 获取当前分发周期内已注入的架构上下文。 + /// + /// + /// 当前处理器尚未被 CQRS runtime 注入上下文。 + /// + protected IArchitectureContext Context => _context ?? throw new InvalidOperationException( + "The CQRS handler context has not been initialized. Ensure the handler is invoked through the CQRS runtime."); + + /// + /// 由 runtime 在分发前注入当前架构上下文。 + /// + /// 当前架构上下文。 + void IContextAware.SetContext(IArchitectureContext context) + { + ArgumentNullException.ThrowIfNull(context); + + _context = context; + OnContextReady(); + } + + /// + /// 获取当前处理器实例已绑定的架构上下文。 + /// + /// 当前分发周期内的架构上下文。 + IArchitectureContext IContextAware.GetContext() + { + return Context; + } + + /// + /// 当上下文注入完成后执行额外初始化。 + /// + /// + /// 该钩子保留与旧 ContextAwareBase 相近的扩展点, + /// 便于处理器在迁移后继续承接分发前的派生类初始化逻辑。 + /// + protected virtual void OnContextReady() + { + } +} diff --git a/GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs b/GFramework.Cqrs/Cqrs/Notification/AbstractNotificationHandler.cs similarity index 85% rename from GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs rename to GFramework.Cqrs/Cqrs/Notification/AbstractNotificationHandler.cs index da6f5281..16246d16 100644 --- a/GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs +++ b/GFramework.Cqrs/Cqrs/Notification/AbstractNotificationHandler.cs @@ -11,18 +11,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Notification; +namespace GFramework.Cqrs.Cqrs.Notification; /// /// 抽象通知处理器基类 -/// 继承自ContextAwareBase并实现INotificationHandler接口,为具体的通知处理器提供基础功能 +/// 继承自轻量 CQRS 上下文基类并实现INotificationHandler接口,为具体的通知处理器提供基础功能 /// 用于处理CQRS模式中的通知消息,支持异步处理 /// /// 通知类型,必须实现INotification接口 -public abstract class AbstractNotificationHandler : ContextAwareBase, INotificationHandler +public abstract class AbstractNotificationHandler : CqrsContextAwareHandlerBase, + INotificationHandler where TNotification : INotification { /// diff --git a/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs b/GFramework.Cqrs/Cqrs/Query/AbstractQueryHandler.cs similarity index 83% rename from GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs rename to GFramework.Cqrs/Cqrs/Query/AbstractQueryHandler.cs index 85c86425..5096f4b7 100644 --- a/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs +++ b/GFramework.Cqrs/Cqrs/Query/AbstractQueryHandler.cs @@ -11,20 +11,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Query; -namespace GFramework.Core.Cqrs.Query; +namespace GFramework.Cqrs.Cqrs.Query; /// /// 抽象查询处理器基类 -/// 继承自 ContextAwareBase 并实现 IRequestHandler 接口,为具体的查询处理器提供基础功能。 +/// 继承自轻量 CQRS 上下文基类并实现 IRequestHandler 接口,为具体的查询处理器提供基础功能。 /// 框架会在每次分发前注入当前架构上下文,因此派生类可以通过 Context 访问架构级服务。 /// /// 查询类型,必须实现IQuery接口 /// 查询结果类型 -public abstract class AbstractQueryHandler : ContextAwareBase, IRequestHandler +public abstract class AbstractQueryHandler : CqrsContextAwareHandlerBase, + IRequestHandler where TQuery : IQuery { /// diff --git a/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs b/GFramework.Cqrs/Cqrs/Query/AbstractStreamQueryHandler.cs similarity index 50% rename from GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs rename to GFramework.Cqrs/Cqrs/Query/AbstractStreamQueryHandler.cs index 9695dc42..f532f80d 100644 --- a/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs +++ b/GFramework.Cqrs/Cqrs/Query/AbstractStreamQueryHandler.cs @@ -11,29 +11,30 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Query; -namespace GFramework.Core.Cqrs.Query; +namespace GFramework.Cqrs.Cqrs.Query; /// -/// 抽象流式查询处理器基类 -/// 继承自ContextAwareBase并实现IStreamQueryHandler接口,为具体的流式查询处理器提供基础功能 -/// 支持流式处理查询并产生异步可枚举的响应序列,适用于大数据量或实时数据查询场景 +/// 为流式查询处理器提供共享的 CQRS 上下文访问基类。 /// -/// 流式查询类型,必须实现IStreamQuery接口 -/// 流式查询响应元素类型 -public abstract class AbstractStreamQueryHandler : ContextAwareBase, +/// 流式查询类型,必须实现 +/// 流式查询响应元素类型。 +/// +/// 该基类复用 的上下文注入能力,并实现 +/// 契约,让派生类只需聚焦于结果流的生成逻辑。 +/// 适用于需要逐步产出大量结果或长生命周期响应流的查询场景。 +/// +public abstract class AbstractStreamQueryHandler : CqrsContextAwareHandlerBase, IStreamRequestHandler where TQuery : IStreamQuery { /// - /// 处理流式查询并返回异步可枚举的响应序列 - /// 由具体的流式查询处理器子类实现流式查询处理逻辑 + /// 处理流式查询并返回异步可枚举的响应序列。 /// - /// 要处理的流式查询对象 - /// 取消令牌,用于取消流式查询操作 - /// 异步可枚举的响应序列,每个元素类型为TResponse + /// 要处理的流式查询对象。 + /// 用于停止结果流生成的取消令牌。 + /// 按需生成的异步响应序列。 public abstract IAsyncEnumerable Handle(TQuery query, CancellationToken cancellationToken); } diff --git a/GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs b/GFramework.Cqrs/Cqrs/Request/AbstractRequestHandler.cs similarity index 86% rename from GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs rename to GFramework.Cqrs/Cqrs/Request/AbstractRequestHandler.cs index f26c7cfa..8343576b 100644 --- a/GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs +++ b/GFramework.Cqrs/Cqrs/Request/AbstractRequestHandler.cs @@ -11,17 +11,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Request; +namespace GFramework.Cqrs.Cqrs.Request; /// /// 抽象请求处理器基类,用于处理不返回具体响应的请求 -/// 继承自ContextAwareBase并实现IRequestHandler接口 +/// 继承自轻量 CQRS 上下文基类并实现IRequestHandler接口 /// /// 请求类型,必须实现IRequest[Unit]接口 -public abstract class AbstractRequestHandler : ContextAwareBase, IRequestHandler +public abstract class AbstractRequestHandler : CqrsContextAwareHandlerBase, IRequestHandler where TRequest : IRequest { /// @@ -35,11 +34,11 @@ public abstract class AbstractRequestHandler : ContextAwareBase, IRequ /// /// 抽象请求处理器基类,用于处理需要返回具体响应的请求 -/// 继承自ContextAwareBase并实现IRequestHandler接口 +/// 继承自轻量 CQRS 上下文基类并实现IRequestHandler接口 /// /// 请求类型,必须实现IRequest[TResponse]接口 /// 响应类型 -public abstract class AbstractRequestHandler : ContextAwareBase, +public abstract class AbstractRequestHandler : CqrsContextAwareHandlerBase, IRequestHandler where TRequest : IRequest { /// diff --git a/GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs b/GFramework.Cqrs/Cqrs/Request/AbstractStreamRequestHandler.cs similarity index 88% rename from GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs rename to GFramework.Cqrs/Cqrs/Request/AbstractStreamRequestHandler.cs index 2cbf438d..42b968f4 100644 --- a/GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs +++ b/GFramework.Cqrs/Cqrs/Request/AbstractStreamRequestHandler.cs @@ -11,19 +11,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Request; +namespace GFramework.Cqrs.Cqrs.Request; /// /// 抽象流式请求处理器基类 -/// 继承自ContextAwareBase并实现IStreamRequestHandler接口,为具体的流式请求处理器提供基础功能 +/// 继承自轻量 CQRS 上下文基类并实现IStreamRequestHandler接口,为具体的流式请求处理器提供基础功能 /// 支持流式处理请求并产生异步可枚举的响应序列,适用于需要逐步返回结果的请求处理场景 /// /// 流式请求类型,必须实现IStreamRequest接口 /// 流式请求响应元素类型 -public abstract class AbstractStreamRequestHandler : ContextAwareBase, +public abstract class AbstractStreamRequestHandler : CqrsContextAwareHandlerBase, IStreamRequestHandler where TRequest : IStreamRequest { diff --git a/GFramework.Core.Abstractions/Cqrs/CqrsHandlerRegistryAttribute.cs b/GFramework.Cqrs/CqrsHandlerRegistryAttribute.cs similarity index 94% rename from GFramework.Core.Abstractions/Cqrs/CqrsHandlerRegistryAttribute.cs rename to GFramework.Cqrs/CqrsHandlerRegistryAttribute.cs index e073fe5c..a64875f3 100644 --- a/GFramework.Core.Abstractions/Cqrs/CqrsHandlerRegistryAttribute.cs +++ b/GFramework.Cqrs/CqrsHandlerRegistryAttribute.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs; /// /// 声明程序集内可供运行时直接调用的 CQRS 处理器注册器类型。 diff --git a/GFramework.Cqrs/CqrsRuntimeFactory.cs b/GFramework.Cqrs/CqrsRuntimeFactory.cs new file mode 100644 index 00000000..0a0f86ce --- /dev/null +++ b/GFramework.Cqrs/CqrsRuntimeFactory.cs @@ -0,0 +1,51 @@ +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Internal; + +namespace GFramework.Cqrs; + +/// +/// 提供 CQRS runtime 默认实现的跨程序集创建入口。 +/// +/// +/// 需要在不暴露内部实现细节的前提下接入默认 CQRS runtime, +/// 因此通过该工厂返回抽象接口,而不是直接公开内部 dispatcher / registrar 类型。 +/// +public static class CqrsRuntimeFactory +{ + /// + /// 创建默认 CQRS runtime 分发器。 + /// + /// 目标依赖注入容器。 + /// 用于 runtime 诊断的日志器。 + /// 默认 CQRS runtime。 + /// + /// 。 + /// + public static ICqrsRuntime CreateRuntime(IIocContainer container, ILogger logger) + { + ArgumentNullException.ThrowIfNull(container); + ArgumentNullException.ThrowIfNull(logger); + + return new CqrsDispatcher(container, logger); + } + + /// + /// 创建默认 CQRS 处理器注册器。 + /// + /// 目标依赖注入容器。 + /// 用于注册阶段诊断的日志器。 + /// 默认 CQRS handler registrar。 + /// + /// 。 + /// + public static ICqrsHandlerRegistrar CreateHandlerRegistrar(IIocContainer container, ILogger logger) + { + ArgumentNullException.ThrowIfNull(container); + ArgumentNullException.ThrowIfNull(logger); + + return new DefaultCqrsHandlerRegistrar(container, logger); + } +} diff --git a/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs b/GFramework.Cqrs/Extensions/ContextAwareCqrsCommandExtensions.cs similarity index 98% rename from GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs rename to GFramework.Cqrs/Extensions/ContextAwareCqrsCommandExtensions.cs index f99fae25..b3c2bde8 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs +++ b/GFramework.Cqrs/Extensions/ContextAwareCqrsCommandExtensions.cs @@ -1,7 +1,7 @@ using GFramework.Core.Abstractions.Rule; using GFramework.Cqrs.Abstractions.Cqrs.Command; -namespace GFramework.Core.Extensions; +namespace GFramework.Cqrs.Extensions; /// /// 提供对 接口的 CQRS 命令扩展方法。 diff --git a/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs b/GFramework.Cqrs/Extensions/ContextAwareCqrsExtensions.cs similarity index 99% rename from GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs rename to GFramework.Cqrs/Extensions/ContextAwareCqrsExtensions.cs index 6db156f5..ef5eb247 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs +++ b/GFramework.Cqrs/Extensions/ContextAwareCqrsExtensions.cs @@ -1,7 +1,7 @@ using GFramework.Core.Abstractions.Rule; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Extensions; +namespace GFramework.Cqrs.Extensions; /// /// 提供对 接口的 CQRS 统一扩展方法。 diff --git a/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs b/GFramework.Cqrs/Extensions/ContextAwareCqrsQueryExtensions.cs similarity index 98% rename from GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs rename to GFramework.Cqrs/Extensions/ContextAwareCqrsQueryExtensions.cs index 40ec0f7d..21c1e952 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs +++ b/GFramework.Cqrs/Extensions/ContextAwareCqrsQueryExtensions.cs @@ -1,7 +1,7 @@ using GFramework.Core.Abstractions.Rule; using GFramework.Cqrs.Abstractions.Cqrs.Query; -namespace GFramework.Core.Extensions; +namespace GFramework.Cqrs.Extensions; /// /// 提供对 接口的 CQRS 查询扩展方法。 diff --git a/GFramework.Cqrs/GFramework.Cqrs.csproj b/GFramework.Cqrs/GFramework.Cqrs.csproj index 9f002283..86323d6e 100644 --- a/GFramework.Cqrs/GFramework.Cqrs.csproj +++ b/GFramework.Cqrs/GFramework.Cqrs.csproj @@ -11,6 +11,7 @@ + diff --git a/GFramework.Cqrs/GlobalUsings.cs b/GFramework.Cqrs/GlobalUsings.cs new file mode 100644 index 00000000..97f2d13a --- /dev/null +++ b/GFramework.Cqrs/GlobalUsings.cs @@ -0,0 +1,7 @@ +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Threading; +global using System.Threading.Tasks; +global using Microsoft.Extensions.DependencyInjection; +global using System.Diagnostics; diff --git a/GFramework.Core.Abstractions/Cqrs/ICqrsHandlerRegistry.cs b/GFramework.Cqrs/ICqrsHandlerRegistry.cs similarity index 95% rename from GFramework.Core.Abstractions/Cqrs/ICqrsHandlerRegistry.cs rename to GFramework.Cqrs/ICqrsHandlerRegistry.cs index 91af6be7..db3775de 100644 --- a/GFramework.Core.Abstractions/Cqrs/ICqrsHandlerRegistry.cs +++ b/GFramework.Cqrs/ICqrsHandlerRegistry.cs @@ -1,6 +1,6 @@ using GFramework.Core.Abstractions.Logging; -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs; /// /// 定义由源码生成器产出的 CQRS 处理器注册器契约。 diff --git a/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs similarity index 99% rename from GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs rename to GFramework.Cqrs/Internal/CqrsDispatcher.cs index 0cae34c4..dafea402 100644 --- a/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -7,7 +7,7 @@ using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Rule; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Internal; +namespace GFramework.Cqrs.Internal; /// /// GFramework 自有 CQRS 运行时分发器。 diff --git a/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs similarity index 99% rename from GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs rename to GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index 65b1cbcf..85a95509 100644 --- a/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -1,10 +1,9 @@ using System.Reflection; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Internal; +namespace GFramework.Cqrs.Internal; /// /// 在架构初始化期间扫描并注册 CQRS 处理器。 diff --git a/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs similarity index 96% rename from GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs rename to GFramework.Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs index ddf0c06a..70d33562 100644 --- a/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs @@ -3,7 +3,7 @@ using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Internal; +namespace GFramework.Cqrs.Internal; /// /// 默认的 CQRS 处理器注册器实现。 diff --git a/GFramework.Core/Cqrs/Notification/NotificationBase.cs b/GFramework.Cqrs/Notification/NotificationBase.cs similarity index 55% rename from GFramework.Core/Cqrs/Notification/NotificationBase.cs rename to GFramework.Cqrs/Notification/NotificationBase.cs index 05db2de7..653a22f2 100644 --- a/GFramework.Core/Cqrs/Notification/NotificationBase.cs +++ b/GFramework.Cqrs/Notification/NotificationBase.cs @@ -14,18 +14,22 @@ using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Notification; -namespace GFramework.Core.Cqrs.Notification; +namespace GFramework.Cqrs.Notification; /// -/// 表示一个基础通知类,用于处理带有输入的通知模式实现。 -/// 该类实现了 INotification 接口,提供了通用的通知结构。 +/// 为携带输入模型的 CQRS 通知提供统一基类。 /// -/// 通知输入数据的类型,必须实现 INotificationInput 接口 -/// 通知执行所需的输入数据 -public abstract class NotificationBase(TInput input) : INotification where TInput : INotificationInput +/// 通知输入类型,必须实现 +/// 通知广播时携带的输入对象。 +/// +/// 该类型继续保留在历史公开命名空间中,以避免调用方因 runtime 程序集拆分而批量修改继承层次。 +/// 具体实现现由 GFramework.Cqrs 程序集承载,并通过 type forward 维持旧程序集兼容性。 +/// +public abstract class NotificationBase(TInput input) : INotification + where TInput : INotificationInput { /// - /// 获取通知的输入数据。 + /// 获取通知广播时携带的输入对象。 /// public TInput Input => input; } diff --git a/GFramework.Core/Cqrs/Query/QueryBase.cs b/GFramework.Cqrs/Query/QueryBase.cs similarity index 52% rename from GFramework.Core/Cqrs/Query/QueryBase.cs rename to GFramework.Cqrs/Query/QueryBase.cs index 759b8df1..4da82bf3 100644 --- a/GFramework.Core/Cqrs/Query/QueryBase.cs +++ b/GFramework.Cqrs/Query/QueryBase.cs @@ -13,19 +13,23 @@ using GFramework.Cqrs.Abstractions.Cqrs.Query; -namespace GFramework.Core.Cqrs.Query; +namespace GFramework.Cqrs.Query; /// -/// 表示一个基础查询类,用于处理带有输入和响应的查询模式实现。 -/// 该类实现 IQuery<TResponse> 接口,提供了通用的查询结构。 +/// 为携带输入模型的 CQRS 查询提供统一基类。 /// -/// 查询输入数据的类型,必须实现 IQueryInput 接口 -/// 查询执行后返回结果的类型 -/// 查询执行所需的输入数据 -public abstract class QueryBase(TInput input) : IQuery where TInput : IQueryInput +/// 查询输入类型,必须实现 +/// 查询响应类型。 +/// 查询执行所需的输入对象。 +/// +/// 该类型继续保留在历史公开命名空间中,以避免调用方因 runtime 程序集拆分而批量修改继承层次。 +/// 具体实现现由 GFramework.Cqrs 程序集承载,并通过 type forward 维持旧程序集兼容性。 +/// +public abstract class QueryBase(TInput input) : IQuery + where TInput : IQueryInput { /// - /// 获取查询的输入数据。 + /// 获取查询执行时携带的输入对象。 /// public TInput Input => input; } diff --git a/GFramework.Core/Cqrs/Request/RequestBase.cs b/GFramework.Cqrs/Request/RequestBase.cs similarity index 53% rename from GFramework.Core/Cqrs/Request/RequestBase.cs rename to GFramework.Cqrs/Request/RequestBase.cs index 5ff18a04..08ceabe4 100644 --- a/GFramework.Core/Cqrs/Request/RequestBase.cs +++ b/GFramework.Cqrs/Request/RequestBase.cs @@ -14,19 +14,23 @@ using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Request; -namespace GFramework.Core.Cqrs.Request; +namespace GFramework.Cqrs.Request; /// -/// 表示一个基础请求类,用于处理带有输入和响应的请求模式实现。 -/// 该类实现了 IRequest<TResponse> 接口,提供了通用的请求结构。 +/// 为携带输入模型的通用 CQRS 请求提供统一基类。 /// -/// 请求输入数据的类型,必须实现 IRequestInput 接口 -/// 请求执行后返回结果的类型 -/// 请求执行所需的输入数据 -public abstract class RequestBase(TInput input) : IRequest where TInput : IRequestInput +/// 请求输入类型,必须实现 +/// 请求响应类型。 +/// 请求执行所需的输入对象。 +/// +/// 该类型继续保留在历史公开命名空间中,以避免调用方因 runtime 程序集拆分而批量修改继承层次。 +/// 具体实现现由 GFramework.Cqrs 程序集承载,并通过 type forward 维持旧程序集兼容性。 +/// +public abstract class RequestBase(TInput input) : IRequest + where TInput : IRequestInput { /// - /// 获取请求的输入数据。 + /// 获取请求执行时携带的输入对象。 /// public TInput Input => input; } diff --git a/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs b/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs index 6805d2e7..15ee4561 100644 --- a/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs +++ b/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs @@ -1,10 +1,10 @@ using GFramework.Core.Abstractions.Rule; using GFramework.Core.Coroutine; using GFramework.Core.Coroutine.Extensions; -using GFramework.Core.Extensions; using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Command; using GFramework.Cqrs.Abstractions.Cqrs.Query; +using GFramework.Cqrs.Extensions; namespace GFramework.Godot.Coroutine; diff --git a/GFramework.SourceGenerators.Common/Constants/PathContests.cs b/GFramework.SourceGenerators.Common/Constants/PathContests.cs index 3facd8b6..a9416fbe 100644 --- a/GFramework.SourceGenerators.Common/Constants/PathContests.cs +++ b/GFramework.SourceGenerators.Common/Constants/PathContests.cs @@ -15,6 +15,11 @@ public static class PathContests /// public const string CoreNamespace = $"{BaseNamespace}.Core"; + /// + /// GFramework CQRS runtime 命名空间 + /// + public const string CqrsNamespace = $"{BaseNamespace}.Cqrs"; + /// /// GFramework Godot模块命名空间 /// @@ -45,4 +50,9 @@ public static class PathContests /// GFramework核心抽象层命名空间 /// public const string CoreAbstractionsNamespace = $"{CoreNamespace}.Abstractions"; -} \ No newline at end of file + + /// + /// GFramework CQRS 抽象层命名空间 + /// + public const string CqrsAbstractionsNamespace = $"{CqrsNamespace}.Abstractions"; +} diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index e2b7ffb1..0392ac8a 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -38,7 +38,7 @@ public class CqrsHandlerRegistryGeneratorTests } } - namespace GFramework.Core.Abstractions.Cqrs + namespace GFramework.Cqrs.Abstractions.Cqrs { public interface IRequest { } public interface INotification { } @@ -47,7 +47,10 @@ public class CqrsHandlerRegistryGeneratorTests public interface IRequestHandler where TRequest : IRequest { } public interface INotificationHandler where TNotification : INotification { } public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + namespace GFramework.Cqrs + { public interface ICqrsHandlerRegistry { void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); @@ -62,7 +65,7 @@ public class CqrsHandlerRegistryGeneratorTests namespace TestApp { - using GFramework.Core.Abstractions.Cqrs; + using GFramework.Cqrs.Abstractions.Cqrs; public sealed record PingQuery() : IRequest; public sealed record DomainEvent() : INotification; @@ -78,11 +81,11 @@ public class CqrsHandlerRegistryGeneratorTests // #nullable enable - [assembly: global::GFramework.Core.Abstractions.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] + [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] namespace GFramework.Generated.Cqrs; - internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Core.Abstractions.Cqrs.ICqrsHandlerRegistry + internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry { public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) { @@ -93,19 +96,19 @@ public class CqrsHandlerRegistryGeneratorTests global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( services, - typeof(global::GFramework.Core.Abstractions.Cqrs.IRequestHandler), + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), typeof(global::TestApp.AlphaQueryHandler)); - logger.Debug("Registered CQRS handler TestApp.AlphaQueryHandler as GFramework.Core.Abstractions.Cqrs.IRequestHandler."); + logger.Debug("Registered CQRS handler TestApp.AlphaQueryHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( services, - typeof(global::GFramework.Core.Abstractions.Cqrs.IStreamRequestHandler), + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler), typeof(global::TestApp.StreamHandler)); - logger.Debug("Registered CQRS handler TestApp.StreamHandler as GFramework.Core.Abstractions.Cqrs.IStreamRequestHandler."); + logger.Debug("Registered CQRS handler TestApp.StreamHandler as GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler."); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( services, - typeof(global::GFramework.Core.Abstractions.Cqrs.INotificationHandler), + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler), typeof(global::TestApp.ZetaNotificationHandler)); - logger.Debug("Registered CQRS handler TestApp.ZetaNotificationHandler as GFramework.Core.Abstractions.Cqrs.INotificationHandler."); + logger.Debug("Registered CQRS handler TestApp.ZetaNotificationHandler as GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler."); } } @@ -143,7 +146,7 @@ public class CqrsHandlerRegistryGeneratorTests } } - namespace GFramework.Core.Abstractions.Cqrs + namespace GFramework.Cqrs.Abstractions.Cqrs { public interface IRequest { } public interface INotification { } @@ -152,7 +155,10 @@ public class CqrsHandlerRegistryGeneratorTests public interface IRequestHandler where TRequest : IRequest { } public interface INotificationHandler where TNotification : INotification { } public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + namespace GFramework.Cqrs + { public interface ICqrsHandlerRegistry { void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); @@ -167,7 +173,7 @@ public class CqrsHandlerRegistryGeneratorTests namespace TestApp { - using GFramework.Core.Abstractions.Cqrs; + using GFramework.Cqrs.Abstractions.Cqrs; public sealed record VisibleRequest() : IRequest; diff --git a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 39c3aadb..65aad69a 100644 --- a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -8,13 +8,17 @@ namespace GFramework.SourceGenerators.Cqrs; [Generator] public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator { - private const string CqrsNamespace = $"{PathContests.CoreAbstractionsNamespace}.Cqrs"; + private const string CqrsContractsNamespace = $"{PathContests.CqrsAbstractionsNamespace}.Cqrs"; + private const string CqrsRuntimeNamespace = PathContests.CqrsNamespace; private const string LoggingNamespace = $"{PathContests.CoreAbstractionsNamespace}.Logging"; - private const string IRequestHandlerMetadataName = $"{CqrsNamespace}.IRequestHandler`2"; - private const string INotificationHandlerMetadataName = $"{CqrsNamespace}.INotificationHandler`1"; - private const string IStreamRequestHandlerMetadataName = $"{CqrsNamespace}.IStreamRequestHandler`2"; - private const string ICqrsHandlerRegistryMetadataName = $"{CqrsNamespace}.ICqrsHandlerRegistry"; - private const string CqrsHandlerRegistryAttributeMetadataName = $"{CqrsNamespace}.CqrsHandlerRegistryAttribute"; + private const string IRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IRequestHandler`2"; + private const string INotificationHandlerMetadataName = $"{CqrsContractsNamespace}.INotificationHandler`1"; + private const string IStreamRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IStreamRequestHandler`2"; + private const string ICqrsHandlerRegistryMetadataName = $"{CqrsRuntimeNamespace}.ICqrsHandlerRegistry"; + + private const string CqrsHandlerRegistryAttributeMetadataName = + $"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute"; + private const string ILoggerMetadataName = $"{LoggingNamespace}.ILogger"; private const string IServiceCollectionMetadataName = "Microsoft.Extensions.DependencyInjection.IServiceCollection"; private const string GeneratedNamespace = "GFramework.Generated.Cqrs"; @@ -273,7 +277,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine("#nullable enable"); builder.AppendLine(); builder.Append("[assembly: global::"); - builder.Append(CqrsNamespace); + builder.Append(CqrsRuntimeNamespace); builder.Append(".CqrsHandlerRegistryAttribute(typeof(global::"); builder.Append(GeneratedNamespace); builder.Append('.'); @@ -287,7 +291,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.Append("internal sealed class "); builder.Append(GeneratedTypeName); builder.Append(" : global::"); - builder.Append(CqrsNamespace); + builder.Append(CqrsRuntimeNamespace); builder.AppendLine(".ICqrsHandlerRegistry"); builder.AppendLine("{"); builder.Append( diff --git a/GFramework.Tests.Common/CqrsTestRuntime.cs b/GFramework.Tests.Common/CqrsTestRuntime.cs index 57d4effd..e7c971db 100644 --- a/GFramework.Tests.Common/CqrsTestRuntime.cs +++ b/GFramework.Tests.Common/CqrsTestRuntime.cs @@ -4,10 +4,10 @@ using System.Reflection; 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; +using GFramework.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Command; namespace GFramework.Tests.Common; @@ -20,9 +20,11 @@ namespace GFramework.Tests.Common; /// public static class CqrsTestRuntime { - private static readonly Type CqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly + private static readonly Assembly CqrsRuntimeAssembly = typeof(CommandBase<,>).Assembly; + + private static readonly Type CqrsHandlerRegistrarType = CqrsRuntimeAssembly .GetType( - "GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar", + "GFramework.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)!; private static readonly MethodInfo RegisterHandlersMethod = CqrsHandlerRegistrarType @@ -40,47 +42,11 @@ public static class CqrsTestRuntime ?? throw new InvalidOperationException( "Failed to locate CqrsHandlerRegistrar.RegisterHandlers."); - private static readonly Type CqrsDispatcherType = typeof(ArchitectureContext).Assembly - .GetType( - "GFramework.Core.Cqrs.Internal.CqrsDispatcher", - throwOnError: true)!; - - 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)!; - - 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。 /// /// 目标测试容器。 /// - /// 反射调用底层 CQRS runtime 或注册器构造函数失败时抛出。 /// /// 这使仅使用 的测试环境也能观察与生产路径一致的 runtime 行为, /// 而无需完整启动服务模块管理器。 @@ -92,16 +58,15 @@ public static class CqrsTestRuntime if (container.Get() is null) { - var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger(CqrsDispatcherType.Name); - var runtime = (ICqrsRuntime)CqrsDispatcherConstructor.Invoke([container, runtimeLogger]); + var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher"); + var runtime = CqrsRuntimeFactory.CreateRuntime(container, runtimeLogger); container.Register(runtime); } if (container.Get() is null) { - var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger(DefaultCqrsHandlerRegistrarType.Name); - var registrar = - (ICqrsHandlerRegistrar)DefaultCqrsHandlerRegistrarConstructor.Invoke([container, registrarLogger]); + var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar"); + var registrar = CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger); container.Register(registrar); } } diff --git a/GFramework.Tests.Common/GFramework.Tests.Common.csproj b/GFramework.Tests.Common/GFramework.Tests.Common.csproj index 51f8fba9..ddd8c02c 100644 --- a/GFramework.Tests.Common/GFramework.Tests.Common.csproj +++ b/GFramework.Tests.Common/GFramework.Tests.Common.csproj @@ -10,6 +10,7 @@ +