feat(cqrs): 添加CQRS处理器基类和测试运行时支持

- 引入AbstractCommandHandler、AbstractQueryHandler等各类处理器基类
- 实现CqrsContextAwareHandlerBase提供上下文感知功能
- 添加CqrsTestRuntime为测试项目提供CQRS运行时访问入口
- 创建AbstractCqrsHandlerContextTests验证上下文注入行为
- 支持命令、查询、通知及流式处理的各种抽象基类实现
This commit is contained in:
GeWuYou 2026-04-15 20:34:08 +08:00
parent a80ff59631
commit e36c80229a
10 changed files with 162 additions and 33 deletions

View File

@ -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;
/// <summary>
/// 验证 CQRS handler 基类在脱离 dispatcher 使用时会显式失败,并在注入上下文后保持可观察行为。
/// </summary>
[TestFixture]
internal sealed class AbstractCqrsHandlerContextTests
{
/// <summary>
/// 验证新的轻量 handler 基类不会再偷偷回退到全局 GameContext。
/// </summary>
[Test]
public void GetContext_Should_Throw_When_Handler_Has_Not_Been_Initialized_By_Runtime()
{
var handler = new TestCommandHandler();
var exception = Assert.Throws<InvalidOperationException>(() => ((IContextAware)handler).GetContext());
Assert.That(
exception!.Message,
Does.Contain("has not been initialized").IgnoreCase);
}
/// <summary>
/// 验证 runtime 注入上下文后,派生 handler 可以继续访问 Context 并收到 OnContextReady 回调。
/// </summary>
[Test]
public async Task Handle_Should_Observe_Injected_Context_And_OnContextReady_Callback()
{
var handler = new TestCommandHandler();
var context = new Mock<IArchitectureContext>(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));
});
}
/// <summary>
/// 用于验证上下文注入行为的最小 CQRS 命令。
/// </summary>
private sealed record TestCommand : ICommand<Unit>;
/// <summary>
/// 暴露基类上下文访问与初始化回调的测试处理器。
/// </summary>
private sealed class TestCommandHandler : AbstractCommandHandler<TestCommand>
{
public int OnContextReadyCallCount { get; private set; }
public IArchitectureContext? LastObservedContext { get; private set; }
protected override void OnContextReady()
{
OnContextReadyCallCount++;
}
public override ValueTask<Unit> Handle(TestCommand command, CancellationToken cancellationToken)
{
LastObservedContext = Context;
return ValueTask.FromResult(Unit.Value);
}
}
}

View File

@ -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;
/// <summary>
/// 抽象命令处理器基类
/// 继承自 ContextAwareBase 并实现 IRequestHandler 接口,为具体的命令处理器提供基础功能。
/// 继承自轻量 CQRS 上下文基类并实现 IRequestHandler 接口,为具体的命令处理器提供基础功能。
/// 框架会在每次分发前注入当前架构上下文,因此派生类可以通过 Context 访问架构级服务。
/// </summary>
/// <typeparam name="TCommand">命令类型</typeparam>
public abstract class AbstractCommandHandler<TCommand> : ContextAwareBase, IRequestHandler<TCommand, Unit>
public abstract class AbstractCommandHandler<TCommand> : CqrsContextAwareHandlerBase, IRequestHandler<TCommand, Unit>
where TCommand : ICommand<Unit>
{
/// <summary>
@ -38,12 +37,13 @@ public abstract class AbstractCommandHandler<TCommand> : ContextAwareBase, IRequ
/// <summary>
/// 抽象命令处理器基类(带返回值版本)
/// 继承自 ContextAwareBase 并实现 IRequestHandler 接口,为具体的命令处理器提供基础功能。
/// 继承自轻量 CQRS 上下文基类并实现 IRequestHandler 接口,为具体的命令处理器提供基础功能。
/// 支持泛型命令和结果类型,框架会在每次分发前注入当前架构上下文。
/// </summary>
/// <typeparam name="TCommand">命令类型必须实现ICommand接口</typeparam>
/// <typeparam name="TResult">命令执行结果类型</typeparam>
public abstract class AbstractCommandHandler<TCommand, TResult> : ContextAwareBase, IRequestHandler<TCommand, TResult>
public abstract class AbstractCommandHandler<TCommand, TResult> : CqrsContextAwareHandlerBase,
IRequestHandler<TCommand, TResult>
where TCommand : ICommand<TResult>
{
/// <summary>

View File

@ -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;
/// <summary>
/// 抽象流式命令处理器基类。
/// 继承自 <see cref="ContextAwareBase" /> 并实现 <see cref="IStreamRequestHandler{TRequest,TResponse}" />
/// 继承自轻量 CQRS 上下文基类并实现 <see cref="IStreamRequestHandler{TRequest,TResponse}" />
/// 为具体的流式命令处理器提供基础功能。
/// </summary>
/// <typeparam name="TCommand">流式命令类型,必须实现 <see cref="IStreamCommand{TResponse}" />。</typeparam>
@ -32,7 +31,7 @@ namespace GFramework.Core.Cqrs.Command;
/// 传入 <see cref="Handle" /> 的取消令牌同时约束流的创建与后续枚举,
/// 派生类应在启动阶段和每次生成响应前尊重取消请求,避免在调用方停止枚举后继续执行后台工作。
/// </remarks>
public abstract class AbstractStreamCommandHandler<TCommand, TResponse> : ContextAwareBase,
public abstract class AbstractStreamCommandHandler<TCommand, TResponse> : CqrsContextAwareHandlerBase,
IStreamRequestHandler<TCommand, TResponse>
where TCommand : IStreamCommand<TResponse>
{

View File

@ -0,0 +1,59 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Rule;
namespace GFramework.Cqrs.Cqrs;
/// <summary>
/// 为 CQRS 处理器提供最小化的上下文感知基类实现。
/// </summary>
/// <remarks>
/// 该基类只承接 CQRS runtime 在分发前注入的 <see cref="IArchitectureContext" />
/// 不再像 <c>ContextAwareBase</c> 那样回退到 <c>GameContext</c> 全局查找。
/// 这样可以让 <c>GFramework.Cqrs</c> 保持对 <c>GFramework.Core</c> 运行时实现的零依赖,
/// 同时在处理器被错误地脱离 dispatcher 使用时以显式异常快速失败。
/// </remarks>
public abstract class CqrsContextAwareHandlerBase : IContextAware
{
private IArchitectureContext? _context;
/// <summary>
/// 获取当前分发周期内已注入的架构上下文。
/// </summary>
/// <exception cref="InvalidOperationException">
/// 当前处理器尚未被 CQRS runtime 注入上下文。
/// </exception>
protected IArchitectureContext Context => _context ?? throw new InvalidOperationException(
"The CQRS handler context has not been initialized. Ensure the handler is invoked through the CQRS runtime.");
/// <summary>
/// 由 runtime 在分发前注入当前架构上下文。
/// </summary>
/// <param name="context">当前架构上下文。</param>
void IContextAware.SetContext(IArchitectureContext context)
{
ArgumentNullException.ThrowIfNull(context);
_context = context;
OnContextReady();
}
/// <summary>
/// 获取当前处理器实例已绑定的架构上下文。
/// </summary>
/// <returns>当前分发周期内的架构上下文。</returns>
IArchitectureContext IContextAware.GetContext()
{
return Context;
}
/// <summary>
/// 当上下文注入完成后执行额外初始化。
/// </summary>
/// <remarks>
/// 该钩子保留与旧 <c>ContextAwareBase</c> 相近的扩展点,
/// 便于处理器在迁移后继续承接分发前的派生类初始化逻辑。
/// </remarks>
protected virtual void OnContextReady()
{
}
}

View File

@ -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;
/// <summary>
/// 抽象通知处理器基类
/// 继承自ContextAwareBase并实现INotificationHandler接口为具体的通知处理器提供基础功能
/// 继承自轻量 CQRS 上下文基类并实现INotificationHandler接口为具体的通知处理器提供基础功能
/// 用于处理CQRS模式中的通知消息支持异步处理
/// </summary>
/// <typeparam name="TNotification">通知类型必须实现INotification接口</typeparam>
public abstract class AbstractNotificationHandler<TNotification> : ContextAwareBase, INotificationHandler<TNotification>
public abstract class AbstractNotificationHandler<TNotification> : CqrsContextAwareHandlerBase,
INotificationHandler<TNotification>
where TNotification : INotification
{
/// <summary>

View File

@ -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;
/// <summary>
/// 抽象查询处理器基类
/// 继承自 ContextAwareBase 并实现 IRequestHandler 接口,为具体的查询处理器提供基础功能。
/// 继承自轻量 CQRS 上下文基类并实现 IRequestHandler 接口,为具体的查询处理器提供基础功能。
/// 框架会在每次分发前注入当前架构上下文,因此派生类可以通过 Context 访问架构级服务。
/// </summary>
/// <typeparam name="TQuery">查询类型必须实现IQuery接口</typeparam>
/// <typeparam name="TResult">查询结果类型</typeparam>
public abstract class AbstractQueryHandler<TQuery, TResult> : ContextAwareBase, IRequestHandler<TQuery, TResult>
public abstract class AbstractQueryHandler<TQuery, TResult> : CqrsContextAwareHandlerBase,
IRequestHandler<TQuery, TResult>
where TQuery : IQuery<TResult>
{
/// <summary>

View File

@ -11,20 +11,19 @@
// 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;
/// <summary>
/// 抽象流式查询处理器基类
/// 继承自ContextAwareBase并实现IStreamQueryHandler接口为具体的流式查询处理器提供基础功能
/// 继承自轻量 CQRS 上下文基类并实现IStreamQueryHandler接口为具体的流式查询处理器提供基础功能
/// 支持流式处理查询并产生异步可枚举的响应序列,适用于大数据量或实时数据查询场景
/// </summary>
/// <typeparam name="TQuery">流式查询类型必须实现IStreamQuery接口</typeparam>
/// <typeparam name="TResponse">流式查询响应元素类型</typeparam>
public abstract class AbstractStreamQueryHandler<TQuery, TResponse> : ContextAwareBase,
public abstract class AbstractStreamQueryHandler<TQuery, TResponse> : CqrsContextAwareHandlerBase,
IStreamRequestHandler<TQuery, TResponse>
where TQuery : IStreamQuery<TResponse>
{

View File

@ -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;
/// <summary>
/// 抽象请求处理器基类,用于处理不返回具体响应的请求
/// 继承自ContextAwareBase并实现IRequestHandler接口
/// 继承自轻量 CQRS 上下文基类并实现IRequestHandler接口
/// </summary>
/// <typeparam name="TRequest">请求类型必须实现IRequest[Unit]接口</typeparam>
public abstract class AbstractRequestHandler<TRequest> : ContextAwareBase, IRequestHandler<TRequest, Unit>
public abstract class AbstractRequestHandler<TRequest> : CqrsContextAwareHandlerBase, IRequestHandler<TRequest, Unit>
where TRequest : IRequest<Unit>
{
/// <summary>
@ -35,11 +34,11 @@ public abstract class AbstractRequestHandler<TRequest> : ContextAwareBase, IRequ
/// <summary>
/// 抽象请求处理器基类,用于处理需要返回具体响应的请求
/// 继承自ContextAwareBase并实现IRequestHandler接口
/// 继承自轻量 CQRS 上下文基类并实现IRequestHandler接口
/// </summary>
/// <typeparam name="TRequest">请求类型必须实现IRequest[TResponse]接口</typeparam>
/// <typeparam name="TResponse">响应类型</typeparam>
public abstract class AbstractRequestHandler<TRequest, TResponse> : ContextAwareBase,
public abstract class AbstractRequestHandler<TRequest, TResponse> : CqrsContextAwareHandlerBase,
IRequestHandler<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
/// <summary>

View File

@ -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;
/// <summary>
/// 抽象流式请求处理器基类
/// 继承自ContextAwareBase并实现IStreamRequestHandler接口为具体的流式请求处理器提供基础功能
/// 继承自轻量 CQRS 上下文基类并实现IStreamRequestHandler接口为具体的流式请求处理器提供基础功能
/// 支持流式处理请求并产生异步可枚举的响应序列,适用于需要逐步返回结果的请求处理场景
/// </summary>
/// <typeparam name="TRequest">流式请求类型必须实现IStreamRequest接口</typeparam>
/// <typeparam name="TResponse">流式请求响应元素类型</typeparam>
public abstract class AbstractStreamRequestHandler<TRequest, TResponse> : ContextAwareBase,
public abstract class AbstractStreamRequestHandler<TRequest, TResponse> : CqrsContextAwareHandlerBase,
IStreamRequestHandler<TRequest, TResponse>
where TRequest : IStreamRequest<TResponse>
{

View File

@ -25,7 +25,7 @@ public static class CqrsTestRuntime
private static readonly Type CqrsHandlerRegistrarType = CqrsRuntimeAssembly
.GetType(
"GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar",
"GFramework.Cqrs.Internal.CqrsHandlerRegistrar",
throwOnError: true)!;
private static readonly MethodInfo RegisterHandlersMethod = CqrsHandlerRegistrarType