diff --git a/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs b/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs index d5f85f1d..67e9537d 100644 --- a/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs +++ b/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs @@ -92,6 +92,53 @@ public class CqrsCoroutineExtensionsTests Assert.That(capturedException, Is.SameAs(expectedException)); } + /// + /// 验证 SendCommandCoroutine 在底层命令被取消且未提供错误回调时会抛出取消异常。 + /// + [Test] + public void SendCommandCoroutine_Should_Throw_TaskCanceledException_When_Command_Is_Canceled() + { + var command = new TestCommand("Test"); + var contextAware = new TestContextAware(); + using var cancellationTokenSource = new CancellationTokenSource(); + + cancellationTokenSource.Cancel(); + contextAware.MockContext + .Setup(ctx => ctx.SendAsync(command, It.IsAny())) + .Returns(new ValueTask(Task.FromCanceled(cancellationTokenSource.Token))); + + var coroutine = CqrsCoroutineExtensions.SendCommandCoroutine(contextAware, command); + + Assert.That(coroutine.MoveNext(), Is.True); + Assert.Throws(() => coroutine.MoveNext()); + } + + /// + /// 验证 SendCommandCoroutine 在底层命令被取消且提供错误回调时会把取消异常转发给回调。 + /// + [Test] + public void SendCommandCoroutine_Should_Forward_TaskCanceledException_To_Error_Handler_When_Command_Is_Canceled() + { + var command = new TestCommand("Test"); + var contextAware = new TestContextAware(); + using var cancellationTokenSource = new CancellationTokenSource(); + Exception? capturedException = null; + + cancellationTokenSource.Cancel(); + contextAware.MockContext + .Setup(ctx => ctx.SendAsync(command, It.IsAny())) + .Returns(new ValueTask(Task.FromCanceled(cancellationTokenSource.Token))); + + 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.TypeOf()); + } + /// /// 测试用的简单命令类 /// @@ -102,13 +149,24 @@ public class CqrsCoroutineExtensionsTests /// private sealed class TestContextAware : IContextAware { + /// + /// 提供可配置的架构上下文 Mock。 + /// public Mock MockContext { get; } = new(); + /// + /// 获取当前架构上下文。 + /// + /// 用于 CQRS 调用的架构上下文实例。 public IArchitectureContext GetContext() { return MockContext.Object; } + /// + /// 设置架构上下文。 + /// + /// 要设置的架构上下文。 public void SetContext(IArchitectureContext context) { } diff --git a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs index 40367ff8..74782793 100644 --- a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs +++ b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs @@ -1,3 +1,4 @@ +using System.Runtime.ExceptionServices; using GFramework.Core.Abstractions.Coroutine; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; @@ -22,9 +23,12 @@ public static class CqrsCoroutineExtensions /// /// 当 时抛出。 /// + /// + /// 当底层命令调度被取消且未提供 时抛出。 + /// /// /// 当底层命令调度失败时,该扩展会把底层异常解包后传给 , - /// 或在未提供回调时重新抛出同一个异常实例,避免两条失败路径暴露不同的异常类型。 + /// 在取消时则统一暴露 ,避免成功、失败与取消三种完成状态被混淆。 /// public static IEnumerator SendCommandCoroutine( this IContextAware contextAware, @@ -39,6 +43,18 @@ public static class CqrsCoroutineExtensions yield return task.AsCoroutineInstruction(); + if (task.IsCanceled) + { + var canceledException = new TaskCanceledException(task); + if (onError != null) + { + onError.Invoke(canceledException); + yield break; + } + + ExceptionDispatchInfo.Capture(canceledException).Throw(); + } + if (!task.IsFaulted) yield break; @@ -46,6 +62,6 @@ public static class CqrsCoroutineExtensions if (onError != null) onError.Invoke(exception); else - throw exception; + ExceptionDispatchInfo.Capture(exception).Throw(); } } diff --git a/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs index 233228ed..b71669ee 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs @@ -6,6 +6,9 @@ namespace GFramework.Core.Cqrs.Extensions; /// /// 提供对 接口的 CQRS 命令扩展方法。 /// +/// +/// 该扩展类将命令分发统一路由到架构上下文中的 CQRS 运行时。 +/// public static class ContextAwareCqrsCommandExtensions { /// @@ -18,6 +21,9 @@ public static class ContextAwareCqrsCommandExtensions /// /// 当 时抛出。 /// + /// + /// 同步方法仅用于兼容同步调用链;新代码建议优先使用异步版本。 + /// public static TResponse SendCommand(this IContextAware contextAware, ICommand command) { ArgumentNullException.ThrowIfNull(contextAware); @@ -37,6 +43,9 @@ public static class ContextAwareCqrsCommandExtensions /// /// 当 时抛出。 /// + /// + /// 该方法直接返回底层 ,避免额外的 async 状态机分配。 + /// public static ValueTask SendCommandAsync( this IContextAware contextAware, ICommand command,