From 5a2981a5577740e8cf61bad8bec98636913e7196 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Wed, 15 Apr 2026 08:18:27 +0800
Subject: [PATCH] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0=20CQRS=20?=
=?UTF-8?q?=E5=91=BD=E4=BB=A4=E5=8D=8F=E7=A8=8B=E6=89=A9=E5=B1=95=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现 CqrsCoroutineExtensions 扩展类,提供协程方式发送 CQRS 命令的功能
- 添加 SendCommandCoroutine 方法支持命令异步执行与异常处理
- 实现取消操作的特殊处理逻辑,区分取消、失败和成功状态
- 添加 ContextAwareCqrsCommandExtensions 扩展类,提供同步和异步命令发送方法
- 增加对 TaskCanceledException 的专门处理机制
- 完善相关单元测试,验证取消操作的异常处理行为
---
.../Coroutine/CqrsCoroutineExtensionsTests.cs | 58 +++++++++++++++++++
.../Extensions/CqrsCoroutineExtensions.cs | 20 ++++++-
.../ContextAwareCqrsCommandExtensions.cs | 9 +++
3 files changed, 85 insertions(+), 2 deletions(-)
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,