feat(cqrs): 添加CQRS扩展方法兼容层和日志工厂解析器

- 新增ContextAwareMediatorCommandExtensions提供命令扩展方法兼容性支持
- 新增ContextAwareMediatorQueryExtensions提供查询扩展方法兼容性支持
- 添加LoggerFactoryResolver实现全局日志工厂访问入口
- 实现TypeForwarders将核心类型转发到正确程序集
- 添加MediatorCompatibilityDeprecationTests验证弃用策略
- 扩展LoggerFactoryTests覆盖并发初始化和回退逻辑
- 迁移CommandBase到Core.Cqrs.Command命名空间
- 移动LoggingBehavior到GFramework.Cqrs.Cqrs.Behaviors
- 添加AbstractStreamQueryHandler支持流式查询处理
- 创建NotificationBase提供通知基类实现
This commit is contained in:
GeWuYou 2026-04-15 22:27:09 +08:00
parent 18337c5995
commit 005c32d84f
15 changed files with 409 additions and 80 deletions

View File

@ -10,19 +10,42 @@ namespace GFramework.Core.Abstractions.Logging;
/// </remarks>
public static class LoggerFactoryResolver
{
private const string DefaultProviderTypeName =
private static readonly object ProviderLock = new();
private static string DefaultProviderTypeName =
"GFramework.Core.Logging.ConsoleLoggerFactoryProvider, GFramework.Core";
private static ILoggerFactoryProvider? _provider;
/// <summary>
/// 获取或设置当前日志工厂提供程序。
/// </summary>
/// <remarks>
/// 读取与赋值都会通过同一把锁串行化,确保并发调用方观察到确定的 provider 引用。
/// 当调用方未显式赋值时,会在首次访问时尝试解析默认实现;若解析失败,则退回静默 provider。
/// </remarks>
/// <exception cref="ArgumentNullException">
/// 当赋值为 <see langword="null" /> 时抛出。
/// </exception>
public static ILoggerFactoryProvider Provider
{
get => field ??= CreateDefaultProvider();
set => field = value ?? throw new ArgumentNullException(nameof(value));
get
{
lock (ProviderLock)
{
_provider ??= CreateDefaultProvider();
return _provider;
}
}
set
{
var provider = value ?? throw new ArgumentNullException(nameof(value));
lock (ProviderLock)
{
_provider = provider;
}
}
}
/// <summary>
@ -39,11 +62,19 @@ public static class LoggerFactoryResolver
private static ILoggerFactoryProvider CreateDefaultProvider()
{
if (Type.GetType(DefaultProviderTypeName, throwOnError: false) is { } providerType &&
Activator.CreateInstance(providerType) is ILoggerFactoryProvider provider)
try
{
provider.MinLevel = LogLevel.Info;
return provider;
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();

View File

@ -0,0 +1,76 @@
using GFramework.Core.Cqrs.Command;
using GFramework.Core.Cqrs.Notification;
using GFramework.Core.Cqrs.Query;
using GFramework.Core.Cqrs.Request;
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;
namespace GFramework.Core.Tests.Cqrs;
/// <summary>
/// 锁定 CQRS 基础消息类型在 runtime 拆分后的公开命名空间与程序集兼容性。
/// </summary>
[TestFixture]
public sealed class CqrsPublicNamespaceCompatibilityTests
{
/// <summary>
/// 验证基础消息类型继续暴露在历史 Core.Cqrs 命名空间,同时由独立 runtime 程序集承载实现。
/// </summary>
[Test]
public void Base_Message_Types_Should_Remain_In_Legacy_Namespaces_While_Living_In_Runtime_Assembly()
{
Assert.Multiple(() =>
{
AssertLegacyType(typeof(CommandBase<TestCommandInput, Unit>), "GFramework.Core.Cqrs.Command");
AssertLegacyType(typeof(QueryBase<TestQueryInput, string>), "GFramework.Core.Cqrs.Query");
AssertLegacyType(typeof(RequestBase<TestRequestInput, string>), "GFramework.Core.Cqrs.Request");
AssertLegacyType(typeof(NotificationBase<TestNotificationInput>), "GFramework.Core.Cqrs.Notification");
});
}
/// <summary>
/// 验证旧的 GFramework.Core 程序集限定名仍可解析到迁移后的 runtime 实现类型。
/// </summary>
[Test]
public void GFramework_Core_Assembly_Should_Forward_Legacy_Base_Types_To_Runtime_Assembly()
{
Assert.Multiple(() =>
{
AssertForwardedType("GFramework.Core.Cqrs.Command.CommandBase`2, GFramework.Core");
AssertForwardedType("GFramework.Core.Cqrs.Query.QueryBase`2, GFramework.Core");
AssertForwardedType("GFramework.Core.Cqrs.Request.RequestBase`2, GFramework.Core");
AssertForwardedType("GFramework.Core.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: true);
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;
}

View File

@ -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.");

View File

@ -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;
/// <summary>
/// 测试LoggerFactory相关功能的测试类
/// 测试 LoggerFactory 相关功能的测试类
/// </summary>
[TestFixture]
[NonParallelizable]
public class LoggerFactoryTests
{
/// <summary>
/// 测试ConsoleLoggerFactory的GetLogger方法是否返回ConsoleLogger实例
/// 测试 ConsoleLoggerFactory GetLogger 方法是否返回 ConsoleLogger 实例
/// </summary>
[Test]
public void ConsoleLoggerFactory_GetLogger_ShouldReturnConsoleLogger()
@ -26,7 +27,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试ConsoleLoggerFactory使用不同名称获取不同的logger实例
/// 测试 ConsoleLoggerFactory 使用不同名称获取不同的 logger 实例
/// </summary>
[Test]
public void ConsoleLoggerFactory_GetLogger_WithDifferentNames_ShouldReturnDifferentLoggers()
@ -40,7 +41,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试ConsoleLoggerFactory使用默认最小级别时的行为默认为Info级别
/// 测试 ConsoleLoggerFactory 使用默认最小级别时的行为。
/// </summary>
[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
}
/// <summary>
/// 测试ConsoleLoggerFactoryProvider创建logger时使用提供者的最小级别设置
/// 测试 ConsoleLoggerFactoryProvider 创建 logger 时使用提供者的最小级别设置
/// </summary>
[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
}
/// <summary>
/// 测试ConsoleLoggerFactoryProvider创建logger时使用提供的名称
/// 测试 ConsoleLoggerFactoryProvider 创建 logger 时使用提供的名称
/// </summary>
[Test]
public void ConsoleLoggerFactoryProvider_CreateLogger_ShouldUseProvidedName()
@ -94,7 +93,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试LoggerFactoryResolver的Provider属性是否有默认值
/// 测试 LoggerFactoryResolver Provider 属性是否有默认值
/// </summary>
[Test]
public void LoggerFactoryResolver_Provider_ShouldHaveDefaultValue()
@ -104,7 +103,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试LoggerFactoryResolver的Provider属性可以被更改
/// 测试 LoggerFactoryResolver Provider 属性可以被更改
/// </summary>
[Test]
public void LoggerFactoryResolver_Provider_CanBeChanged()
@ -120,7 +119,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试LoggerFactoryResolver的MinLevel属性是否有默认值
/// 测试 LoggerFactoryResolver MinLevel 属性是否有默认值
/// </summary>
[Test]
public void LoggerFactoryResolver_MinLevel_ShouldHaveDefaultValue()
@ -129,7 +128,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试LoggerFactoryResolver的MinLevel属性可以被更改
/// 测试 LoggerFactoryResolver MinLevel 属性可以被更改
/// </summary>
[Test]
public void LoggerFactoryResolver_MinLevel_CanBeChanged()
@ -144,7 +143,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试ConsoleLoggerFactoryProvider的MinLevel属性是否有默认值
/// 测试 ConsoleLoggerFactoryProvider MinLevel 属性是否有默认值
/// </summary>
[Test]
public void ConsoleLoggerFactoryProvider_MinLevel_ShouldHaveDefaultValue()
@ -155,7 +154,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试ConsoleLoggerFactoryProvider的MinLevel属性可以被更改
/// 测试 ConsoleLoggerFactoryProvider MinLevel 属性可以被更改
/// </summary>
[Test]
public void ConsoleLoggerFactoryProvider_MinLevel_CanBeChanged()
@ -168,7 +167,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试LoggerFactoryResolver的Provider创建logger时使用提供者设置
/// 测试 LoggerFactoryResolver Provider 创建 logger 时使用提供者设置
/// </summary>
[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
}
/// <summary>
/// 测试LoggerFactoryResolver的MinLevel属性影响新创建的logger
/// 测试 LoggerFactoryResolver MinLevel 属性影响新创建的 logger
/// </summary>
[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
}
/// <summary>
/// 测试ConsoleLoggerFactory创建的多个logger实例是独立的
/// 验证默认 provider 激活失败时会回退到静默 provider。
/// </summary>
[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;
}
}
/// <summary>
/// 验证并发首次访问默认 provider 时只会创建一个实例,并向所有调用方返回相同引用。
/// </summary>
[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;
}
}
/// <summary>
/// 测试 ConsoleLoggerFactory 创建的多个 logger 实例是独立的。
/// </summary>
[Test]
public void ConsoleLoggerFactory_MultipleLoggers_ShouldBeIndependent()
@ -236,7 +319,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试ConsoleLoggerFactoryProvider的MinLevel不会影响已创建的logger
/// 测试 ConsoleLoggerFactoryProvider MinLevel 不会影响已创建的 logger
/// </summary>
[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"));
}
}
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}.");
}
/// <summary>
/// 用于触发默认 provider 激活失败回退路径的测试桩。
/// </summary>
public sealed class ThrowingLoggerFactoryProvider : ILoggerFactoryProvider
{
/// <summary>
/// 初始化一个始终抛出异常的 provider。
/// </summary>
/// <exception cref="InvalidOperationException">始终抛出,用于覆盖回退路径。</exception>
public ThrowingLoggerFactoryProvider()
{
throw new InvalidOperationException("Simulated provider activation failure.");
}
/// <summary>
/// 获取或设置最小日志级别。
/// </summary>
public LogLevel MinLevel { get; set; } = LogLevel.Info;
/// <summary>
/// 创建日志器。
/// </summary>
/// <param name="name">日志器名称。</param>
/// <returns>该测试桩永远不会成功创建日志器。</returns>
/// <exception cref="NotSupportedException">始终抛出,因为该方法不应被调用。</exception>
public ILogger CreateLogger(string name)
{
throw new NotSupportedException();
}
}
/// <summary>
/// 用于验证并发首次初始化路径只创建单个 provider 实例的测试桩。
/// </summary>
public sealed class BlockingLoggerFactoryProvider : ILoggerFactoryProvider
{
private static int _constructionCount;
private static ManualResetEventSlim _constructionGate = new(false);
/// <summary>
/// 初始化一个会阻塞构造完成的 provider用于放大并发首次访问竞争窗口。
/// </summary>
public BlockingLoggerFactoryProvider()
{
Interlocked.Increment(ref _constructionCount);
_constructionGate.Wait(TimeSpan.FromSeconds(5));
}
/// <summary>
/// 获取已经发生的构造次数。
/// </summary>
public static int ConstructionCount => Volatile.Read(ref _constructionCount);
/// <summary>
/// 获取或设置最小日志级别。
/// </summary>
public LogLevel MinLevel { get; set; } = LogLevel.Info;
/// <summary>
/// 创建测试日志器。
/// </summary>
/// <param name="name">日志器名称。</param>
/// <returns>带有当前最小级别设置的控制台日志器。</returns>
public ILogger CreateLogger(string name)
{
return new ConsoleLogger(name, MinLevel, TextWriter.Null, false);
}
/// <summary>
/// 重置该测试桩的并发观测状态。
/// </summary>
public static void Reset()
{
_constructionGate = new ManualResetEventSlim(false);
Interlocked.Exchange(ref _constructionCount, 0);
}
/// <summary>
/// 释放当前被阻塞的 provider 构造过程。
/// </summary>
public static void ReleaseConstruction()
{
_constructionGate.Set();
}
}
}

View File

@ -12,7 +12,7 @@ namespace GFramework.Core.Extensions;
/// </summary>
[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
{
/// <summary>

View File

@ -12,7 +12,7 @@ namespace GFramework.Core.Extensions;
/// </summary>
[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
{
/// <summary>

View File

@ -1,4 +1,12 @@
using System.Runtime.CompilerServices;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Cqrs.Command;
using GFramework.Core.Cqrs.Notification;
using GFramework.Core.Cqrs.Query;
using GFramework.Core.Cqrs.Request;
[assembly: TypeForwardedTo(typeof(LoggerFactoryResolver))]
[assembly: TypeForwardedTo(typeof(CommandBase<,>))]
[assembly: TypeForwardedTo(typeof(QueryBase<,>))]
[assembly: TypeForwardedTo(typeof(RequestBase<,>))]
[assembly: TypeForwardedTo(typeof(NotificationBase<>))]

View File

@ -13,19 +13,23 @@
using GFramework.Cqrs.Abstractions.Cqrs.Command;
namespace GFramework.Cqrs.Command;
namespace GFramework.Core.Cqrs.Command;
/// <summary>
/// 表示一个基础命令类,用于处理带有输入和响应的命令模式实现。
/// 该类实现了 ICommand&lt;TResponse&gt; 接口,提供了通用的命令结构。
/// 为携带输入模型的 CQRS 命令提供统一基类。
/// </summary>
/// <typeparam name="TInput">命令输入数据的类型</typeparam>
/// <typeparam name="TResponse">命令执行后返回结果的类型</typeparam>
/// <param name="input">命令执行所需的输入数据</param>
public abstract class CommandBase<TInput, TResponse>(TInput input) : ICommand<TResponse> where TInput : ICommandInput
/// <typeparam name="TInput">命令输入类型。</typeparam>
/// <typeparam name="TResponse">命令响应类型。</typeparam>
/// <param name="input">命令执行所需的输入对象。</param>
/// <remarks>
/// 该类型继续保留在历史公开命名空间中,以避免调用方因 runtime 程序集拆分而批量修改继承层次。
/// 具体实现现由 <c>GFramework.Cqrs</c> 程序集承载,并通过 type forward 维持旧程序集兼容性。
/// </remarks>
public abstract class CommandBase<TInput, TResponse>(TInput input) : ICommand<TResponse>
where TInput : ICommandInput
{
/// <summary>
/// 获取命令的输入数据。
/// 获取命令执行时携带的输入对象
/// </summary>
public TInput Input => input;
}

View File

@ -22,8 +22,8 @@ namespace GFramework.Cqrs.Cqrs.Behaviors;
/// <typeparam name="TRequest">请求类型。</typeparam>
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <remarks>
/// 该行为保留在 <c>GFramework.Core.Cqrs.Behaviors</c> 命名空间以兼容现有调用点
/// 但实现已迁入 <c>GFramework.Cqrs</c> 程序集,避免继续由 <c>GFramework.Core</c> 承载 CQRS runtime 细节
/// 该行为已迁移到 <c>GFramework.Cqrs.Cqrs.Behaviors</c> 命名空间
/// 实现位于 <c>GFramework.Cqrs</c> 程序集,用于承载 CQRS runtime 细节并与旧层解耦
/// </remarks>
public sealed class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>

View File

@ -17,22 +17,24 @@ using GFramework.Cqrs.Abstractions.Cqrs.Query;
namespace GFramework.Cqrs.Cqrs.Query;
/// <summary>
/// 抽象流式查询处理器基类
/// 继承自轻量 CQRS 上下文基类并实现IStreamQueryHandler接口为具体的流式查询处理器提供基础功能
/// 支持流式处理查询并产生异步可枚举的响应序列,适用于大数据量或实时数据查询场景
/// 为流式查询处理器提供共享的 CQRS 上下文访问基类。
/// </summary>
/// <typeparam name="TQuery">流式查询类型必须实现IStreamQuery接口</typeparam>
/// <typeparam name="TResponse">流式查询响应元素类型</typeparam>
/// <typeparam name="TQuery">流式查询类型,必须实现 <see cref="IStreamQuery{TResponse}" />。</typeparam>
/// <typeparam name="TResponse">流式查询响应元素类型。</typeparam>
/// <remarks>
/// 该基类复用 <see cref="CqrsContextAwareHandlerBase" /> 的上下文注入能力,并实现
/// <see cref="IStreamRequestHandler{TQuery,TResponse}" /> 契约,让派生类只需聚焦于结果流的生成逻辑。
/// 适用于需要逐步产出大量结果或长生命周期响应流的查询场景。
/// </remarks>
public abstract class AbstractStreamQueryHandler<TQuery, TResponse> : CqrsContextAwareHandlerBase,
IStreamRequestHandler<TQuery, TResponse>
where TQuery : IStreamQuery<TResponse>
{
/// <summary>
/// 处理流式查询并返回异步可枚举的响应序列
/// 由具体的流式查询处理器子类实现流式查询处理逻辑
/// 处理流式查询并返回异步可枚举的响应序列。
/// </summary>
/// <param name="query">要处理的流式查询对象</param>
/// <param name="cancellationToken">取消令牌,用于取消流式查询操作</param>
/// <returns>异步可枚举的响应序列每个元素类型为TResponse</returns>
/// <param name="query">要处理的流式查询对象</param>
/// <param name="cancellationToken">用于停止结果流生成的取消令牌。</param>
/// <returns>按需生成的异步响应序列。</returns>
public abstract IAsyncEnumerable<TResponse> Handle(TQuery query, CancellationToken cancellationToken);
}

View File

@ -21,6 +21,9 @@ public static class CqrsRuntimeFactory
/// <param name="container">目标依赖注入容器。</param>
/// <param name="logger">用于 runtime 诊断的日志器。</param>
/// <returns>默认 CQRS runtime。</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="container" /> 或 <paramref name="logger" /> 为 <see langword="null" />。
/// </exception>
public static ICqrsRuntime CreateRuntime(IIocContainer container, ILogger logger)
{
ArgumentNullException.ThrowIfNull(container);
@ -35,6 +38,9 @@ public static class CqrsRuntimeFactory
/// <param name="container">目标依赖注入容器。</param>
/// <param name="logger">用于注册阶段诊断的日志器。</param>
/// <returns>默认 CQRS handler registrar。</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="container" /> 或 <paramref name="logger" /> 为 <see langword="null" />。
/// </exception>
public static ICqrsHandlerRegistrar CreateHandlerRegistrar(IIocContainer container, ILogger logger)
{
ArgumentNullException.ThrowIfNull(container);

View File

@ -14,18 +14,22 @@
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs.Notification;
namespace GFramework.Cqrs.Notification;
namespace GFramework.Core.Cqrs.Notification;
/// <summary>
/// 表示一个基础通知类,用于处理带有输入的通知模式实现。
/// 该类实现了 INotification 接口,提供了通用的通知结构。
/// 为携带输入模型的 CQRS 通知提供统一基类。
/// </summary>
/// <typeparam name="TInput">通知输入数据的类型,必须实现 INotificationInput 接口</typeparam>
/// <param name="input">通知执行所需的输入数据</param>
public abstract class NotificationBase<TInput>(TInput input) : INotification where TInput : INotificationInput
/// <typeparam name="TInput">通知输入类型,必须实现 <see cref="INotificationInput" />。</typeparam>
/// <param name="input">通知广播时携带的输入对象。</param>
/// <remarks>
/// 该类型继续保留在历史公开命名空间中,以避免调用方因 runtime 程序集拆分而批量修改继承层次。
/// 具体实现现由 <c>GFramework.Cqrs</c> 程序集承载,并通过 type forward 维持旧程序集兼容性。
/// </remarks>
public abstract class NotificationBase<TInput>(TInput input) : INotification
where TInput : INotificationInput
{
/// <summary>
/// 获取通知的输入数据。
/// 获取通知广播时携带的输入对象
/// </summary>
public TInput Input => input;
}

View File

@ -13,19 +13,23 @@
using GFramework.Cqrs.Abstractions.Cqrs.Query;
namespace GFramework.Cqrs.Query;
namespace GFramework.Core.Cqrs.Query;
/// <summary>
/// 表示一个基础查询类,用于处理带有输入和响应的查询模式实现。
/// 该类实现 IQuery&lt;TResponse&gt; 接口,提供了通用的查询结构。
/// 为携带输入模型的 CQRS 查询提供统一基类。
/// </summary>
/// <typeparam name="TInput">查询输入数据的类型,必须实现 IQueryInput 接口</typeparam>
/// <typeparam name="TResponse">查询执行后返回结果的类型</typeparam>
/// <param name="input">查询执行所需的输入数据</param>
public abstract class QueryBase<TInput, TResponse>(TInput input) : IQuery<TResponse> where TInput : IQueryInput
/// <typeparam name="TInput">查询输入类型,必须实现 <see cref="IQueryInput" />。</typeparam>
/// <typeparam name="TResponse">查询响应类型。</typeparam>
/// <param name="input">查询执行所需的输入对象。</param>
/// <remarks>
/// 该类型继续保留在历史公开命名空间中,以避免调用方因 runtime 程序集拆分而批量修改继承层次。
/// 具体实现现由 <c>GFramework.Cqrs</c> 程序集承载,并通过 type forward 维持旧程序集兼容性。
/// </remarks>
public abstract class QueryBase<TInput, TResponse>(TInput input) : IQuery<TResponse>
where TInput : IQueryInput
{
/// <summary>
/// 获取查询的输入数据。
/// 获取查询执行时携带的输入对象
/// </summary>
public TInput Input => input;
}

View File

@ -14,19 +14,23 @@
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs.Request;
namespace GFramework.Cqrs.Request;
namespace GFramework.Core.Cqrs.Request;
/// <summary>
/// 表示一个基础请求类,用于处理带有输入和响应的请求模式实现。
/// 该类实现了 IRequest&lt;TResponse&gt; 接口,提供了通用的请求结构。
/// 为携带输入模型的通用 CQRS 请求提供统一基类。
/// </summary>
/// <typeparam name="TInput">请求输入数据的类型,必须实现 IRequestInput 接口</typeparam>
/// <typeparam name="TResponse">请求执行后返回结果的类型</typeparam>
/// <param name="input">请求执行所需的输入数据</param>
public abstract class RequestBase<TInput, TResponse>(TInput input) : IRequest<TResponse> where TInput : IRequestInput
/// <typeparam name="TInput">请求输入类型,必须实现 <see cref="IRequestInput" />。</typeparam>
/// <typeparam name="TResponse">请求响应类型。</typeparam>
/// <param name="input">请求执行所需的输入对象。</param>
/// <remarks>
/// 该类型继续保留在历史公开命名空间中,以避免调用方因 runtime 程序集拆分而批量修改继承层次。
/// 具体实现现由 <c>GFramework.Cqrs</c> 程序集承载,并通过 type forward 维持旧程序集兼容性。
/// </remarks>
public abstract class RequestBase<TInput, TResponse>(TInput input) : IRequest<TResponse>
where TInput : IRequestInput
{
/// <summary>
/// 获取请求的输入数据。
/// 获取请求执行时携带的输入对象
/// </summary>
public TInput Input => input;
}

View File

@ -4,11 +4,10 @@ using System.Reflection;
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Cqrs.Command;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Command;
namespace GFramework.Tests.Common;
@ -48,7 +47,6 @@ public static class CqrsTestRuntime
/// </summary>
/// <param name="container">目标测试容器。</param>
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
/// <exception cref="TargetInvocationException">反射调用底层 CQRS runtime 或注册器构造函数失败时抛出。</exception>
/// <remarks>
/// 这使仅使用 <see cref="MicrosoftDiContainer" /> 的测试环境也能观察与生产路径一致的 runtime 行为,
/// 而无需完整启动服务模块管理器。