diff --git a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs index 78303a2d..b344717d 100644 --- a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs +++ b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs @@ -2,6 +2,7 @@ using GFramework.Core.Abstractions.Lifecycle; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; +using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Abstractions.Architectures; diff --git a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs index 0e55908a..c56b71fa 100644 --- a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs +++ b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs @@ -1,5 +1,6 @@ using GFramework.Core.Abstractions.Rule; using GFramework.Core.Abstractions.Systems; +using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Abstractions.Ioc; diff --git a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs index c5c0b685..ade4a1a7 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs @@ -77,6 +77,28 @@ public class ArchitectureModulesBehaviorTests await architecture.DestroyAsync(); } + /// + /// 验证兼容别名 RegisterMediatorBehavior 仍会把 CQRS 行为接入请求管道。 + /// + [Test] + public async Task RegisterMediatorBehavior_Should_Apply_Pipeline_Behavior_To_Request() + { + var architecture = new ModuleTestArchitecture(target => + target.RegisterMediatorBehavior>()); + + await architecture.InitializeAsync(); + + var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest()); + + Assert.Multiple(() => + { + Assert.That(response, Is.EqualTo("handled")); + Assert.That(TrackingPipelineBehavior.InvocationCount, Is.EqualTo(1)); + }); + + await architecture.DestroyAsync(); + } + /// /// 用于测试模块行为的最小架构实现。 /// diff --git a/GFramework.Core.Tests/Architectures/GameContextTests.cs b/GFramework.Core.Tests/Architectures/GameContextTests.cs index e11f0892..28e3cc97 100644 --- a/GFramework.Core.Tests/Architectures/GameContextTests.cs +++ b/GFramework.Core.Tests/Architectures/GameContextTests.cs @@ -394,45 +394,106 @@ public class TestArchitectureContext : IArchitectureContext { } + /// + /// 测试桩:异步发送统一 CQRS 请求。 + /// + /// 响应类型。 + /// 要发送的请求。 + /// 取消令牌。 + /// 请求响应任务。 + /// 该测试桩未实现此成员。 public ValueTask SendRequestAsync(IRequest request, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } + /// + /// 测试桩:同步发送统一 CQRS 请求。 + /// + /// 响应类型。 + /// 要发送的请求。 + /// 请求响应。 + /// 该测试桩未实现此成员。 public TResponse SendRequest(IRequest request) { throw new NotImplementedException(); } + /// + /// 测试桩:异步发送 CQRS 命令并返回响应。 + /// + /// 命令响应类型。 + /// 要发送的命令。 + /// 取消令牌。 + /// 命令响应任务。 + /// 该测试桩未实现此成员。 public ValueTask SendCommandAsync(Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } + /// + /// 测试桩:同步发送 CQRS 命令并返回响应。 + /// + /// 命令响应类型。 + /// 要发送的命令。 + /// 命令响应。 + /// 该测试桩未实现此成员。 public TResponse SendCommand(Abstractions.Cqrs.Command.ICommand command) { throw new NotImplementedException(); } + /// + /// 测试桩:异步发送 CQRS 查询并返回结果。 + /// + /// 查询结果类型。 + /// 要发送的查询。 + /// 取消令牌。 + /// 查询结果任务。 + /// 该测试桩未实现此成员。 public ValueTask SendQueryAsync(Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } + /// + /// 测试桩:同步发送 CQRS 查询并返回结果。 + /// + /// 查询结果类型。 + /// 要发送的查询。 + /// 查询结果。 + /// 该测试桩未实现此成员。 public TResponse SendQuery(Abstractions.Cqrs.Query.IQuery query) { throw new NotImplementedException(); } + /// + /// 测试桩:异步发布 CQRS 通知。 + /// + /// 通知类型。 + /// 要发布的通知。 + /// 取消令牌。 + /// 通知发布任务。 + /// 该测试桩未实现此成员。 public ValueTask PublishAsync(TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification { throw new NotImplementedException(); } + /// + /// 测试桩:创建 CQRS 流式请求响应序列。 + /// + /// 流式响应元素类型。 + /// 流式请求。 + /// 取消令牌。 + /// 异步响应流。 + /// 该测试桩未实现此成员。 public IAsyncEnumerable CreateStream( IStreamRequest request, CancellationToken cancellationToken = default) @@ -440,12 +501,28 @@ public class TestArchitectureContext : IArchitectureContext throw new NotImplementedException(); } + /// + /// 测试桩:异步发送无返回值 CQRS 命令。 + /// + /// 命令类型。 + /// 要发送的命令。 + /// 取消令牌。 + /// 命令发送任务。 + /// 该测试桩未实现此成员。 public ValueTask SendAsync(TCommand command, CancellationToken cancellationToken = default) where TCommand : IRequest { throw new NotImplementedException(); } + /// + /// 测试桩:异步发送带返回值的 CQRS 请求。 + /// + /// 响应类型。 + /// 要发送的请求。 + /// 取消令牌。 + /// 请求响应任务。 + /// 该测试桩未实现此成员。 public ValueTask SendAsync(IRequest command, CancellationToken cancellationToken = default) { diff --git a/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs b/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs index 2adba37a..d5f85f1d 100644 --- a/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs +++ b/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs @@ -66,6 +66,32 @@ public class CqrsCoroutineExtensionsTests Assert.That(exception, Is.SameAs(expectedException)); } + /// + /// 验证 SendCommandCoroutine 在提供错误回调时也会传递解包后的原始异常, + /// 避免回调路径暴露 。 + /// + [Test] + public void SendCommandCoroutine_Should_Forward_Inner_Exception_To_Error_Handler() + { + var command = new TestCommand("Test"); + var contextAware = new TestContextAware(); + var expectedException = new InvalidOperationException("Command failed."); + Exception? capturedException = null; + + contextAware.MockContext + .Setup(ctx => ctx.SendAsync(command, It.IsAny())) + .Returns(new ValueTask(Task.FromException(expectedException))); + + var coroutine = CqrsCoroutineExtensions.SendCommandCoroutine( + contextAware, + command, + exception => capturedException = exception); + + Assert.That(coroutine.MoveNext(), Is.True); + Assert.That(coroutine.MoveNext(), Is.False); + Assert.That(capturedException, Is.SameAs(expectedException)); + } + /// /// 测试用的简单命令类 /// diff --git a/GFramework.Core.Tests/GlobalUsings.cs b/GFramework.Core.Tests/GlobalUsings.cs index 18957f6b..fe9b7de1 100644 --- a/GFramework.Core.Tests/GlobalUsings.cs +++ b/GFramework.Core.Tests/GlobalUsings.cs @@ -23,4 +23,6 @@ global using GFramework.Core.Abstractions.StateManagement; global using GFramework.Core.Extensions; global using GFramework.Core.Property; global using GFramework.Core.StateManagement; -global using GFramework.Core.Abstractions.Property; \ No newline at end of file +global using GFramework.Core.Abstractions.Property; +global using Microsoft.Extensions.DependencyInjection; +global using Moq; diff --git a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs index 55e6a67f..40367ff8 100644 --- a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs +++ b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs @@ -19,6 +19,13 @@ public static class CqrsCoroutineExtensions /// 要发送的命令对象。 /// 发生异常时的回调处理函数。 /// 协程枚举器,用于协程执行。 + /// + /// 当 时抛出。 + /// + /// + /// 当底层命令调度失败时,该扩展会把底层异常解包后传给 , + /// 或在未提供回调时重新抛出同一个异常实例,避免两条失败路径暴露不同的异常类型。 + /// public static IEnumerator SendCommandCoroutine( this IContextAware contextAware, TCommand command, @@ -35,9 +42,10 @@ public static class CqrsCoroutineExtensions if (!task.IsFaulted) yield break; + var exception = task.Exception!.InnerException ?? task.Exception; if (onError != null) - onError.Invoke(task.Exception!); + onError.Invoke(exception); else - throw task.Exception!.InnerException ?? task.Exception; + throw exception; } } diff --git a/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs index 5f86f4e5..233228ed 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs @@ -15,6 +15,9 @@ public static class ContextAwareCqrsCommandExtensions /// 实现 接口的对象。 /// 要发送的命令对象。 /// 命令执行结果。 + /// + /// 当 时抛出。 + /// public static TResponse SendCommand(this IContextAware contextAware, ICommand command) { ArgumentNullException.ThrowIfNull(contextAware); @@ -31,6 +34,9 @@ public static class ContextAwareCqrsCommandExtensions /// 要发送的命令对象。 /// 取消令牌,用于取消操作。 /// 包含命令执行结果的 + /// + /// 当 时抛出。 + /// public static ValueTask SendCommandAsync( this IContextAware contextAware, ICommand command, diff --git a/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs b/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs index 64f9d0e1..ab09e689 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs @@ -17,6 +17,9 @@ public static class ContextAwareCqrsExtensions /// 要发送的请求。 /// 取消令牌。 /// 请求结果。 + /// + /// 当 时抛出。 + /// public static ValueTask SendRequestAsync( this IContextAware contextAware, IRequest request, @@ -35,6 +38,9 @@ public static class ContextAwareCqrsExtensions /// 实现 接口的对象。 /// 要发送的请求。 /// 请求结果。 + /// + /// 当 时抛出。 + /// public static TResponse SendRequest(this IContextAware contextAware, IRequest request) { ArgumentNullException.ThrowIfNull(contextAware); @@ -51,6 +57,9 @@ public static class ContextAwareCqrsExtensions /// 要发布的通知。 /// 取消令牌。 /// 异步任务。 + /// + /// 当 时抛出。 + /// public static ValueTask PublishAsync( this IContextAware contextAware, TNotification notification, @@ -71,6 +80,9 @@ public static class ContextAwareCqrsExtensions /// 流式请求。 /// 取消令牌。 /// 异步响应流。 + /// + /// 当 时抛出。 + /// public static IAsyncEnumerable CreateStream( this IContextAware contextAware, IStreamRequest request, @@ -90,6 +102,9 @@ public static class ContextAwareCqrsExtensions /// 要发送的命令。 /// 取消令牌。 /// 异步任务。 + /// + /// 当 时抛出。 + /// public static ValueTask SendAsync( this IContextAware contextAware, TCommand command, @@ -110,6 +125,9 @@ public static class ContextAwareCqrsExtensions /// 要发送的命令。 /// 取消令牌。 /// 命令执行结果。 + /// + /// 当 时抛出。 + /// public static ValueTask SendAsync( this IContextAware contextAware, IRequest command, diff --git a/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs b/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs index 42905215..9906bc3d 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs @@ -15,6 +15,9 @@ public static class ContextAwareCqrsQueryExtensions /// 实现 接口的对象。 /// 要发送的查询对象。 /// 查询结果。 + /// + /// 当 时抛出。 + /// public static TResponse SendQuery(this IContextAware contextAware, IQuery query) { ArgumentNullException.ThrowIfNull(contextAware); @@ -31,6 +34,9 @@ public static class ContextAwareCqrsQueryExtensions /// 要发送的查询对象。 /// 取消令牌,用于取消操作。 /// 包含查询结果的 + /// + /// 当 时抛出。 + /// public static ValueTask SendQueryAsync( this IContextAware contextAware, IQuery query, diff --git a/GFramework.Core/GlobalUsings.cs b/GFramework.Core/GlobalUsings.cs index 8add267e..203366e6 100644 --- a/GFramework.Core/GlobalUsings.cs +++ b/GFramework.Core/GlobalUsings.cs @@ -16,4 +16,5 @@ global using System.Collections.Generic; global using System.Linq; global using System.Threading; global using System.Threading.Tasks; -global using System.Threading.Channels; \ No newline at end of file +global using System.Threading.Channels; +global using Microsoft.Extensions.DependencyInjection; diff --git a/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs b/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs index 5d1a3485..98d57936 100644 --- a/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs +++ b/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs @@ -2,6 +2,9 @@ using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Abstractions.Rule; +using GFramework.Core.Coroutine; +using GFramework.Core.Coroutine.Extensions; +using GFramework.Core.Cqrs.Extensions; namespace GFramework.Godot.Coroutine; @@ -26,7 +29,7 @@ public static class ContextAwareCoroutineExtensions string? tag = null, CancellationToken cancellationToken = default) { - return Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions + return ContextAwareCqrsCommandExtensions .SendCommandAsync(contextAware, command, cancellationToken) .AsTask() .ToCoroutineEnumerator() @@ -50,7 +53,7 @@ public static class ContextAwareCoroutineExtensions string? tag = null, CancellationToken cancellationToken = default) { - return Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions + return ContextAwareCqrsCommandExtensions .SendCommandAsync(contextAware, command, cancellationToken) .AsTask() .ToCoroutineEnumerator() @@ -74,7 +77,7 @@ public static class ContextAwareCoroutineExtensions string? tag = null, CancellationToken cancellationToken = default) { - return Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions + return ContextAwareCqrsQueryExtensions .SendQueryAsync(contextAware, query, cancellationToken) .AsTask() .ToCoroutineEnumerator() @@ -97,7 +100,7 @@ public static class ContextAwareCoroutineExtensions string? tag = null, CancellationToken cancellationToken = default) { - return Core.Cqrs.Extensions.ContextAwareCqrsExtensions + return ContextAwareCqrsExtensions .PublishAsync(contextAware, notification, cancellationToken) .AsTask() .ToCoroutineEnumerator() diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 742d2ef8..3effaa86 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -204,22 +204,24 @@ public async Task> GetHighScores() ### 注册处理器 -在架构中注册 CQRS 行为并让处理器自动扫描注册: +在架构中注册 CQRS 行为;默认会自动扫描当前架构所在程序集和 `GFramework.Core` 程序集中的处理器: ```csharp public class GameArchitecture : Architecture { - protected override void Init() + protected override void OnInitialize() { // 注册通用开放泛型行为 RegisterCqrsPipelineBehavior>(); RegisterCqrsPipelineBehavior>(); - // 处理器会自动通过依赖注入注册 + // 默认只自动扫描当前架构程序集和 GFramework.Core 程序集中的处理器 } } ``` +如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是依赖默认扫描。 + `RegisterCqrsPipelineBehavior()` 是推荐入口;旧的 `RegisterMediatorBehavior()` 仅作为兼容名称保留。当前接口支持两种形式: @@ -338,8 +340,8 @@ public class LoggingBehavior : IPipelineBehavior Handle( TMessage message, - CancellationToken cancellationToken, - MessageHandlerDelegate next) + MessageHandlerDelegate next, + CancellationToken cancellationToken) { var messageName = message.GetType().Name; Console.WriteLine($"[开始] {messageName}"); @@ -358,8 +360,8 @@ public class PerformanceBehavior : IPipelineBehavior Handle( TMessage message, - CancellationToken cancellationToken, - MessageHandlerDelegate next) + MessageHandlerDelegate next, + CancellationToken cancellationToken) { var stopwatch = Stopwatch.StartNew(); @@ -390,8 +392,8 @@ public class ValidationBehavior : IPipelineBehavior Handle( TMessage message, - CancellationToken cancellationToken, - MessageHandlerDelegate next) + MessageHandlerDelegate next, + CancellationToken cancellationToken) { // 验证输入 if (message is IValidatable validatable)