From 088f02d586a9cff9fcbde478f64cb1e16f957d17 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Wed, 15 Apr 2026 07:34:01 +0800
Subject: [PATCH] =?UTF-8?q?docs(core):=20=E6=B7=BB=E5=8A=A0=20CQRS=20?=
=?UTF-8?q?=E6=96=87=E6=A1=A3=E5=B9=B6=E5=AE=8C=E5=96=84=E7=9B=B8=E5=85=B3?=
=?UTF-8?q?=E6=89=A9=E5=B1=95=E6=96=B9=E6=B3=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 CQRS 核心概念、命令查询处理器使用指南
- 添加管道行为、流式处理和最佳实践说明
- 实现 CQRS 协程扩展方法支持异步命令执行
- 添加 ContextAware 接口的 CQRS 命令查询扩展
- 集成 Microsoft DI 容器依赖注入支持
- 补充架构模块行为测试验证功能完整性
- 扩展 GameContext 测试用例提高代码覆盖率
---
.../Architectures/IArchitecture.cs | 1 +
.../Ioc/IIocContainer.cs | 1 +
.../ArchitectureModulesBehaviorTests.cs | 22 ++++++
.../Architectures/GameContextTests.cs | 77 +++++++++++++++++++
.../Coroutine/CqrsCoroutineExtensionsTests.cs | 26 +++++++
GFramework.Core.Tests/GlobalUsings.cs | 4 +-
.../Extensions/CqrsCoroutineExtensions.cs | 12 ++-
.../ContextAwareCqrsCommandExtensions.cs | 6 ++
.../Extensions/ContextAwareCqrsExtensions.cs | 18 +++++
.../ContextAwareCqrsQueryExtensions.cs | 6 ++
GFramework.Core/GlobalUsings.cs | 3 +-
.../ContextAwareCoroutineExtensions.cs | 11 ++-
docs/zh-CN/core/cqrs.md | 20 ++---
13 files changed, 190 insertions(+), 17 deletions(-)
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)