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.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 89% rename from GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs rename to GFramework.Cqrs/Cqrs/Query/AbstractStreamQueryHandler.cs index 9695dc42..7d301009 100644 --- a/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs +++ b/GFramework.Cqrs/Cqrs/Query/AbstractStreamQueryHandler.cs @@ -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; /// /// 抽象流式查询处理器基类 -/// 继承自ContextAwareBase并实现IStreamQueryHandler接口,为具体的流式查询处理器提供基础功能 +/// 继承自轻量 CQRS 上下文基类并实现IStreamQueryHandler接口,为具体的流式查询处理器提供基础功能 /// 支持流式处理查询并产生异步可枚举的响应序列,适用于大数据量或实时数据查询场景 /// /// 流式查询类型,必须实现IStreamQuery接口 /// 流式查询响应元素类型 -public abstract class AbstractStreamQueryHandler : ContextAwareBase, +public abstract class AbstractStreamQueryHandler : CqrsContextAwareHandlerBase, IStreamRequestHandler where TQuery : IStreamQuery { 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.Tests.Common/CqrsTestRuntime.cs b/GFramework.Tests.Common/CqrsTestRuntime.cs index f5913c6e..f044bbc8 100644 --- a/GFramework.Tests.Common/CqrsTestRuntime.cs +++ b/GFramework.Tests.Common/CqrsTestRuntime.cs @@ -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