Compare commits

...

20 Commits

Author SHA1 Message Date
gewuyou
3dbe1053fb
Merge pull request #223 from GeWuYou/refactor/cqrs-architecture-decoupling-todo-4
feat(core): 扩展 CQRS 处理器注册 API 并完善文档
2026-04-15 13:21:31 +08:00
GeWuYou
a01ec8d29c fix(ci): 修复PR扫描的基线和头版本配置
- 将基础提交哈希从 github.event.before 更新为 github.event.pull_request.base.sha
- 将当前提交哈希从 github.sha 更新为 github.event.pull_request.head.sha
- 确保PR工作流正确比较基线和目标分支的差异
2026-04-15 13:20:31 +08:00
coderabbitai[bot]
2329cba3a6
fix: apply CodeRabbit auto-fixes
Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
2026-04-15 05:07:59 +00:00
GeWuYou
49df81e46f refactor(tests): 重构 CQRS 处理程序测试架构
- 移除自定义测试架构类,改用现有的 SyncTestArchitecture
- 将 RegisterCqrsHandlersFromAssembly 测试方法中的架构创建逻辑提取为统一方法
- 更新重复程序集注册去重测试,验证不同 Assembly 实例但相同程序集键的情况
- 简化测试架构初始化逻辑,使用 AddPostRegistrationHook 替代自定义配置
- 调整注释文档以反映新的测试架构创建方式
- 移除 GitHub 工作流中对 main 分支的限制条件
2026-04-15 12:59:12 +08:00
GeWuYou
340b6cae90 chore(ci): 更新GitHub Actions工作流配置
- 移除push触发器,仅保留pull request触发CI构建测试
- 添加CodeQL静态代码分析工作流,支持安全漏洞检测
- 配置每日凌晨2点定时执行CodeQL分析
- 设置.NET 8.0.x运行时环境支持
- 启用C#语言自动构建模式进行代码扫描
2026-04-15 12:51:59 +08:00
GeWuYou
0cd1e9e83a feat(ci): 添加CI/CD工作流和CQRS命令接口
- 配置CI构建和测试工作流,支持多.NET版本和并发测试
- 添加CodeQL静态代码分析工作流
- 实现自动版本递增和标签创建工作流
- 定义CQRS命令接口规范,包括响应式和流式命令
- 为架构测试添加空值参数异常文档注释
2026-04-15 12:47:22 +08:00
GeWuYou
27266d037d feat(arch): 添加架构基础类和依赖注入容器实现
- 创建 Architecture 基类提供系统、模型、工具等组件的注册与管理功能
- 实现架构生命周期管理、初始化流程控制和阶段转换功能
- 添加 ArchitectureModules 模块管理器负责 CQRS 行为注册和模块安装
- 实现 MicrosoftDiContainer 依赖注入容器适配器
- 支持单例、瞬态、作用域服务注册和工厂方法注册
- 添加 CQRS 请求管道行为和处理器注册功能
- 实现线程安全的读写锁保护容器操作
- 提供服务获取、排序和优先级管理功能
2026-04-15 12:38:45 +08:00
GeWuYou
4db7923512 docs(core): 添加 CQRS 架构模式详细文档
- 完整介绍 CQRS 核心概念包括命令、查询、处理器和分发器
- 提供命令和查询的定义与实现示例代码
- 详细介绍处理器编写方法和注册流程
- 说明管道行为(Behaviors)的使用方式
- 展示通知(Notification)和流式处理功能
- 提供最佳实践和常见问题解决方案
- 包含完整的 API 参考和用法示例
2026-04-15 11:44:44 +08:00
gewuyou
f59e8f7a1f
Merge pull request #222 from GeWuYou/refactor/cqrs-architecture-decoupling-todo-3
Refactor/Deprecate Mediator alias and introduce source-generated CQRS handler registry
2026-04-15 11:20:19 +08:00
GeWuYou
7a6f966601 feat(cqrs): 添加 CQRS 处理器注册生成器
- 实现 CqrsHandlerRegistryGenerator 源代码生成器
- 支持 IRequestHandler、INotificationHandler 和 IStreamRequestHandler 接口的处理器注册
- 生成程序集级别的 CQRS 处理器注册器以减少运行时反射开销
- 添加对请求、通知和流处理器的稳定顺序注册支持
- 实现对私有嵌套处理器的检测和回退机制
- 提供字符串字面量转义功能以避免生成代码中的语法错误
- 添加完整的单元测试验证生成器的功能和边界条件
2026-04-15 11:12:36 +08:00
GeWuYou
fd64423741 docs(core): 添加 CQRS 架构模式完整文档
- 新增 CQRS 核心概念介绍,包括命令、查询、处理器和分发器
- 添加基本用法示例,展示命令和查询的定义与发送流程
- 实现高级功能文档,涵盖请求、通知、管道行为和流式处理
- 提供最佳实践指南,明确命令查询分离和验证行为使用方式
- 增加常见问题解答,解释 Command/Query 区别和错误处理方案
- 新增 CQRS 处理器自动注册实现,支持源码生成和反射扫描
- 添加单元测试验证处理器注册顺序和容错行为
- 更新项目 AI 代理说明文档,完善模块依赖关系图
2026-04-15 10:21:20 +08:00
GeWuYou
295496e90f docs(core): 添加 CQRS 文档并实现架构模块管理
- 添加完整的 CQRS 中文文档,涵盖命令、查询、处理器、管道行为等核心概念
- 实现 ArchitectureModules 类用于管理架构模块安装和 CQRS 行为注册
- 重构 Architecture 类为协调器模式,委托给专门的管理器组件
- 添加 RegisterCqrsPipelineBehavior 方法替代旧的 RegisterMediatorBehavior
- 标记旧的扩展方法为 Obsolete 并提供新的兼容性别名
- 实现模块化架构组件注册和生命周期管理功能
2026-04-15 09:49:26 +08:00
gewuyou
779c521a20
Merge pull request #221 from GeWuYou/refactor/cqrs-architecture-decoupling-todo-2
Replace Mediator pattern with CQRS pipeline behavior registration
2026-04-15 09:12:46 +08:00
GeWuYou
c0e2e9a640 docs(coroutine): 更新CqrsCoroutineExtensions文档
- 添加了TaskCanceledException异常说明文档
- 详细描述了命令调度取消时的异常情况
- 补充了底层命令调度相关的异常处理说明
2026-04-15 09:05:22 +08:00
GeWuYou
4c0a99d24c fix(coroutine): 优化协程扩展中的异常处理机制
- 添加 TaskCanceledException 映射以统一取消状态处理
- 保留原始异常调用栈以避免调试时丢失异常来源
- 优先解包业务异常以避免直接暴露 AggregateException
- 使用 ExceptionDispatchInfo.Capture 确保异常栈信息完整
2026-04-15 08:25:52 +08:00
GeWuYou
96ffd49b31 fix(coroutine): 更新CQRS协程扩展的异常处理文档
- 将TaskCanceledException更改为Exception以反映实际抛出的异常类型
- 更新异常描述以准确说明在未提供onError时的行为
- 修正文档以反映底层原始异常的传递机制
2026-04-15 08:24:48 +08:00
GeWuYou
5a2981a557 feat(cqrs): 添加 CQRS 命令协程扩展功能
- 实现 CqrsCoroutineExtensions 扩展类,提供协程方式发送 CQRS 命令的功能
- 添加 SendCommandCoroutine 方法支持命令异步执行与异常处理
- 实现取消操作的特殊处理逻辑,区分取消、失败和成功状态
- 添加 ContextAwareCqrsCommandExtensions 扩展类,提供同步和异步命令发送方法
- 增加对 TaskCanceledException 的专门处理机制
- 完善相关单元测试,验证取消操作的异常处理行为
2026-04-15 08:18:27 +08:00
GeWuYou
088f02d586 docs(core): 添加 CQRS 文档并完善相关扩展方法
- 新增 CQRS 核心概念、命令查询处理器使用指南
- 添加管道行为、流式处理和最佳实践说明
- 实现 CQRS 协程扩展方法支持异步命令执行
- 添加 ContextAware 接口的 CQRS 命令查询扩展
- 集成 Microsoft DI 容器依赖注入支持
- 补充架构模块行为测试验证功能完整性
- 扩展 GameContext 测试用例提高代码覆盖率
2026-04-15 07:34:01 +08:00
GeWuYou
115fe65e88 docs(core): 添加 CQRS 和核心框架文档
- 新增 CQRS 模块详细文档,介绍命令查询职责分离模式
- 添加核心框架架构概述和五层架构设计说明
- 补充快速开始指南和最佳实践建议
- 完善包说明和组件联动机制介绍
- 添加架构生命周期管理和模块化设计说明
2026-04-14 22:54:27 +08:00
GeWuYou
5c112f8545 docs(core): 添加 CQRS 和核心框架文档
- 新增 CQRS 详细文档,介绍命令查询职责分离模式
- 添加核心框架概述文档,包含架构图和快速开始指南
- 详细介绍五层架构设计和组件联动机制
- 提供完整的最佳实践和设计理念说明
- 添加架构生命周期管理和模块化设计说明
2026-04-14 22:30:59 +08:00
41 changed files with 2298 additions and 337 deletions

View File

@ -5,6 +5,8 @@ on:
workflows: ["CI - Build & Test"]
types:
- completed
branches:
- main
workflow_dispatch:
concurrency:
group: auto-tag-main
@ -13,15 +15,15 @@ concurrency:
jobs:
auto-tag:
if: >
github.ref == 'refs/heads/main' &&
(
(
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
contains(github.event.workflow_run.head_commit.message, '[release ci]')
)
||
github.event_name == 'workflow_dispatch'
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
contains(github.event.workflow_run.head_commit.message, '[release ci]')
)
||
(
github.event_name == 'workflow_dispatch' &&
github.ref == 'refs/heads/main'
)
runs-on: ubuntu-latest
@ -61,4 +63,4 @@ jobs:
fi
git tag -a "$TAG" -m "Auto tag $TAG"
git push "https://x-access-token:${PAT}@github.com/${{ github.repository }}.git" "$TAG"
git push "https://x-access-token:${PAT}@github.com/${{ github.repository }}.git" "$TAG"

View File

@ -1,12 +1,10 @@
# CI/CD工作流配置构建和测试.NET项目
# 该工作流push到main/master分支或创建pull request时触发
# 该工作流在创建或更新面向任意分支的 pull request 时触发
name: CI - Build & Test
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
branches: [ '**' ]
permissions:
contents: read
@ -69,9 +67,9 @@ jobs:
# 扫描路径,. 表示扫描整个仓库
path: .
# 基础提交哈希,用于与当前提交进行比较
base: ${{ github.event.before }}
base: ${{ github.event.pull_request.base.sha }}
# 当前提交哈希,作为扫描的目标版本
head: ${{ github.sha }}
head: ${{ github.event.pull_request.head.sha }}
# 构建和测试 job并行执行
build-and-test:

View File

@ -4,14 +4,11 @@ name: "CodeQL"
# 触发事件配置
# 在以下情况下触发工作流:
# 1. 推送到main分支时
# 2. 针对main分支的拉取请求时
# 3. 每天凌晨2点执行一次
# 1. 针对任意分支的拉取请求时
# 2. 每天凌晨2点执行一次
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
branches: [ '**' ]
schedule:
- cron: '0 2 * * *'

View File

@ -73,7 +73,9 @@ Architecture 负责统一生命周期编排,核心阶段包括:
### CQRS
命令与查询分离支持同步与异步执行。Mediator 模式通过源码生成器集成,以减少模板代码并保持调用路径清晰。
命令与查询分离,支持同步与异步执行。当前版本内建自有 CQRS runtime、行为管道和 handler 自动注册;公开 API 里仍保留少量历史
`Mediator` 命名以兼容旧调用点,但这些别名已进入正式弃用周期:新代码应使用 `Cqrs` 命名入口,旧别名会继续兼容一段时间并计划在未来
major 版本中移除。
### EventBus
@ -103,6 +105,8 @@ Architecture 负责统一生命周期编排,核心阶段包括:
- `PriorityGenerator` (`[Priority]`): 生成优先级比较相关实现。
- `EnumExtensionsGenerator` (`[GenerateEnumExtensions]`): 生成枚举扩展能力。
- `ContextAwareGenerator` (`[ContextAware]`): 自动实现 `IContextAware` 相关样板逻辑。
- `CqrsHandlerRegistryGenerator`: 为消费端程序集生成 CQRS handler 注册器,运行时优先使用生成产物,无法覆盖时回退到反射扫描;非默认程序集可通过
`RegisterCqrsHandlersFromAssembly(...)` / `RegisterCqrsHandlersFromAssemblies(...)` 显式接入同一路径。
这些生成器的目标是减少重复代码,同时保持框架层 API 的一致性与可维护性。

View File

@ -1,8 +1,9 @@
using System.ComponentModel;
using System.Reflection;
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;
@ -73,15 +74,46 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia
void RegisterUtility<T>(Action<T>? onCreated = null) where T : class, IUtility;
/// <summary>
/// 注册中介行为管道
/// 用于配置Mediator框架的行为拦截和处理逻辑。
/// 注册 CQRS 请求管道行为。
/// 既支持实现 <c>IPipelineBehavior&lt;,&gt;</c> 的开放泛型行为类型,
/// 也支持绑定到单一请求/响应对的封闭行为类型。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
void RegisterCqrsPipelineBehavior<TBehavior>()
where TBehavior : class;
/// <summary>
/// 注册 CQRS 请求管道行为。
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
/// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。
/// 既支持实现 <c>IPipelineBehavior&lt;,&gt;</c> 的开放泛型行为类型,
/// 也支持绑定到单一请求/响应对的封闭行为类型。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete(
"Use RegisterCqrsPipelineBehavior<TBehavior>() instead. This compatibility alias will be removed in a future major version.")]
void RegisterMediatorBehavior<TBehavior>()
where TBehavior : class;
/// <summary>
/// 从指定程序集显式注册 CQRS 处理器。
/// 当处理器位于默认架构程序集之外的模块或扩展程序集中时,可在初始化阶段调用该入口接入对应程序集。
/// </summary>
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
/// <exception cref="ArgumentNullException"><paramref name="assembly" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">当前架构的底层容器已冻结,无法继续注册处理器。</exception>
void RegisterCqrsHandlersFromAssembly(Assembly assembly);
/// <summary>
/// 从多个程序集显式注册 CQRS 处理器。
/// 该入口会对程序集集合去重,适用于统一接入多个扩展包或模块程序集。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">当前架构的底层容器已冻结,无法继续注册处理器。</exception>
void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies);
/// <summary>
/// 安装架构模块
/// </summary>
@ -101,4 +133,4 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia
/// </summary>
/// <returns>表示异步等待操作的任务</returns>
Task WaitUntilReadyAsync();
}
}

View File

@ -0,0 +1,18 @@
namespace GFramework.Core.Abstractions.Cqrs;
/// <summary>
/// 声明程序集内可供运行时直接调用的 CQRS 处理器注册器类型。
/// </summary>
/// <remarks>
/// 该特性通常由源码生成器自动添加到消费端程序集。
/// 运行时读取到该特性后,会优先实例化对应的 <see cref="ICqrsHandlerRegistry" />
/// 以常量时间获取处理器注册映射,而不是遍历程序集中的全部类型。
/// </remarks>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class CqrsHandlerRegistryAttribute(Type registryType) : Attribute
{
/// <summary>
/// 获取承载 CQRS 处理器注册逻辑的注册器类型。
/// </summary>
public Type RegistryType { get; } = registryType ?? throw new ArgumentNullException(nameof(registryType));
}

View File

@ -0,0 +1,20 @@
using GFramework.Core.Abstractions.Logging;
namespace GFramework.Core.Abstractions.Cqrs;
/// <summary>
/// 定义由源码生成器产出的 CQRS 处理器注册器契约。
/// </summary>
/// <remarks>
/// 运行时会优先调用实现该接口的程序集级注册器,以避免在冷启动阶段对整个程序集执行反射扫描。
/// 当目标程序集没有生成注册器,或生成注册器因兼容性原因不可用时,运行时仍会回退到反射扫描路径。
/// </remarks>
public interface ICqrsHandlerRegistry
{
/// <summary>
/// 将当前程序集中的 CQRS 处理器映射注册到目标服务集合。
/// </summary>
/// <param name="services">承载处理器映射的服务集合。</param>
/// <param name="logger">用于记录注册诊断信息的日志器。</param>
void Register(IServiceCollection services, ILogger logger);
}

View File

@ -26,6 +26,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Mediator.Abstractions" Version="3.0.2"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.5"/>
</ItemGroup>
</Project>

View File

@ -16,4 +16,5 @@ global using System.Collections.Generic;
global using System.Runtime;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
global using System.Threading.Tasks;
global using Microsoft.Extensions.DependencyInjection;

View File

@ -1,6 +1,7 @@
using GFramework.Core.Abstractions.Rule;
using System.ComponentModel;
using System.Reflection;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Abstractions.Systems;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Abstractions.Ioc;
@ -90,13 +91,43 @@ public interface IIocContainer : IContextAware
void RegisterFactory<TService>(Func<IServiceProvider, TService> factory) where TService : class;
/// <summary>
/// 注册中介行为管道
/// 用于配置Mediator框架的行为拦截和处理逻辑
/// 注册 CQRS 请求管道行为。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
void RegisterCqrsPipelineBehavior<TBehavior>()
where TBehavior : class;
/// <summary>
/// 注册 CQRS 请求管道行为。
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
/// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete(
"Use RegisterCqrsPipelineBehavior<TBehavior>() instead. This compatibility alias will be removed in a future major version.")]
void RegisterMediatorBehavior<TBehavior>()
where TBehavior : class;
/// <summary>
/// 从指定程序集显式注册 CQRS 处理器。
/// 该入口适用于处理器不位于默认架构程序集中的场景,例如扩展包、模块程序集或拆分后的业务程序集。
/// 运行时会优先使用程序集级源码生成注册器;若不存在可用注册器,则自动回退到反射扫描。
/// </summary>
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
/// <exception cref="ArgumentNullException"><paramref name="assembly" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">容器已冻结,无法继续注册 CQRS 处理器。</exception>
void RegisterCqrsHandlersFromAssembly(Assembly assembly);
/// <summary>
/// 从多个程序集显式注册 CQRS 处理器。
/// 容器会按稳定程序集键去重,避免默认启动路径与扩展模块重复接入同一程序集时产生重复 handler 映射。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">容器已冻结,无法继续注册 CQRS 处理器。</exception>
void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies);
/// <summary>
/// 配置服务
@ -227,4 +258,4 @@ public interface IIocContainer : IContextAware
IServiceScope CreateScope();
#endregion
}
}

View File

@ -0,0 +1,200 @@
using System.Reflection;
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Architectures;
using GFramework.Core.Logging;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 验证架构初始化阶段可以显式接入默认程序集之外的 CQRS handlers。
/// </summary>
[TestFixture]
public sealed class ArchitectureAdditionalCqrsHandlersTests
{
private ILoggerFactoryProvider? _previousLoggerFactoryProvider;
/// <summary>
/// 初始化日志工厂和共享测试状态。
/// </summary>
[SetUp]
public void SetUp()
{
_previousLoggerFactoryProvider = LoggerFactoryResolver.Provider;
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
GameContext.Clear();
AdditionalAssemblyNotificationHandlerState.Reset();
}
/// <summary>
/// 清理测试过程中写入的共享状态。
/// </summary>
[TearDown]
public void TearDown()
{
AdditionalAssemblyNotificationHandlerState.Reset();
GameContext.Clear();
LoggerFactoryResolver.Provider = _previousLoggerFactoryProvider
?? throw new InvalidOperationException(
"LoggerFactoryResolver.Provider should be captured during setup.");
}
/// <summary>
/// 验证显式声明的额外程序集会在初始化阶段接入当前架构容器。
/// </summary>
/// <returns>The asynchronous test task.</returns>
[Test]
public async Task RegisterCqrsHandlersFromAssembly_Should_Register_Handlers_From_Explicit_Assembly()
{
var generatedAssembly = CreateGeneratedHandlerAssembly();
var architecture = CreateArchitecture(target =>
target.RegisterCqrsHandlersFromAssembly(generatedAssembly.Object));
await architecture.InitializeAsync();
try
{
await architecture.Context.PublishAsync(new AdditionalAssemblyNotification());
Assert.That(AdditionalAssemblyNotificationHandlerState.InvocationCount, Is.EqualTo(1));
}
finally
{
await architecture.DestroyAsync();
}
}
/// <summary>
/// 验证不同 <see cref="Assembly" /> 实例只要解析到相同程序集键,就不会向容器重复写入相同 handler 映射。
/// </summary>
/// <returns>The asynchronous test task.</returns>
[Test]
public async Task RegisterCqrsHandlersFromAssembly_Should_Deduplicate_Repeated_Assembly_Registration()
{
var generatedAssemblyA = CreateGeneratedHandlerAssembly();
var generatedAssemblyB = CreateGeneratedHandlerAssembly();
var architecture = CreateArchitecture(target =>
{
target.RegisterCqrsHandlersFromAssembly(generatedAssemblyA.Object);
target.RegisterCqrsHandlersFromAssemblies([generatedAssemblyB.Object]);
});
await architecture.InitializeAsync();
try
{
await architecture.Context.PublishAsync(new AdditionalAssemblyNotification());
Assert.That(AdditionalAssemblyNotificationHandlerState.InvocationCount, Is.EqualTo(1));
}
finally
{
await architecture.DestroyAsync();
}
}
/// <summary>
/// 创建一个仅暴露程序集级 CQRS registry 元数据的 mocked Assembly。
/// 该测试替身模拟“扩展程序集已经挂接 source-generator运行时只需显式接入该程序集”的真实路径。
/// </summary>
/// <returns>包含程序集级 handler registry 元数据的 mocked Assembly。</returns>
private static Mock<Assembly> CreateGeneratedHandlerAssembly()
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Architectures.ExplicitAdditionalHandlers, Version=1.0.0.0");
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(AdditionalAssemblyNotificationHandlerRegistry))]);
return generatedAssembly;
}
/// <summary>
/// 创建复用现有测试架构基建的测试架构,并在注册阶段后执行额外程序集接入逻辑。
/// </summary>
/// <param name="configure">初始化阶段执行的额外 CQRS 程序集接入逻辑。</param>
/// <returns>带有注册后钩子的测试架构实例。</returns>
private static SyncTestArchitecture CreateArchitecture(Action<TestArchitectureBase> configure)
{
var architecture = new SyncTestArchitecture();
architecture.AddPostRegistrationHook(configure);
return architecture;
}
}
/// <summary>
/// 用于验证额外程序集接入是否成功的测试通知。
/// </summary>
public sealed record AdditionalAssemblyNotification : INotification;
/// <summary>
/// 记录模拟扩展程序集通知处理器的执行次数。
/// </summary>
public static class AdditionalAssemblyNotificationHandlerState
{
private static int _invocationCount;
/// <summary>
/// 获取当前测试进程中该处理器的执行次数。
/// </summary>
/// <remarks>
/// 该计数器通过原子读写维护,以支持 NUnit 并行执行环境中的并发访问。
/// </remarks>
public static int InvocationCount => Volatile.Read(ref _invocationCount);
/// <summary>
/// 记录一次通知处理,供测试断言显式程序集接入后的运行时行为。
/// </summary>
public static void RecordInvocation()
{
Interlocked.Increment(ref _invocationCount);
}
/// <summary>
/// 清理共享计数器,避免测试间相互污染。
/// </summary>
public static void Reset()
{
Interlocked.Exchange(ref _invocationCount, 0);
}
}
/// <summary>
/// 模拟由 source-generator 为扩展程序集生成的 CQRS handler registry。
/// </summary>
internal sealed class AdditionalAssemblyNotificationHandlerRegistry : ICqrsHandlerRegistry
{
/// <summary>
/// 将扩展程序集中的通知处理器映射写入服务集合。
/// </summary>
/// <param name="services">目标服务集合。</param>
/// <param name="logger">日志记录器。</param>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="services" /> 或 <paramref name="logger" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient<INotificationHandler<AdditionalAssemblyNotification>>(_ => CreateHandler());
logger.Debug(
$"Registered CQRS handler proxy for {typeof(INotificationHandler<AdditionalAssemblyNotification>).FullName}.");
}
/// <summary>
/// 创建一个仅供显式程序集注册路径使用的动态通知处理器。
/// </summary>
/// <returns>用于记录通知触发次数的测试替身处理器。</returns>
private static INotificationHandler<AdditionalAssemblyNotification> CreateHandler()
{
var handler = new Mock<INotificationHandler<AdditionalAssemblyNotification>>();
handler
.Setup(target => target.Handle(It.IsAny<AdditionalAssemblyNotification>(), It.IsAny<CancellationToken>()))
.Returns(() =>
{
AdditionalAssemblyNotificationHandlerState.RecordInvocation();
return ValueTask.CompletedTask;
});
return handler.Object;
}
}

View File

@ -59,6 +59,28 @@ public class ArchitectureModulesBehaviorTests
/// 验证注册的 CQRS 行为会参与请求管道执行。
/// </summary>
[Test]
public async Task RegisterCqrsPipelineBehavior_Should_Apply_Pipeline_Behavior_To_Request()
{
var architecture = new ModuleTestArchitecture(target =>
target.RegisterCqrsPipelineBehavior<TrackingPipelineBehavior<ModuleBehaviorRequest, string>>());
await architecture.InitializeAsync();
var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest());
Assert.Multiple(() =>
{
Assert.That(response, Is.EqualTo("handled"));
Assert.That(TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount, Is.EqualTo(1));
});
await architecture.DestroyAsync();
}
/// <summary>
/// 验证兼容别名 <c>RegisterMediatorBehavior</c> 仍会把 CQRS 行为接入请求管道。
/// </summary>
[Test]
public async Task RegisterMediatorBehavior_Should_Apply_Pipeline_Behavior_To_Request()
{
var architecture = new ModuleTestArchitecture(target =>

View File

@ -1,5 +1,6 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Abstractions.Environment;
using GFramework.Core.Abstractions.Events;
using GFramework.Core.Abstractions.Ioc;
@ -13,7 +14,6 @@ using GFramework.Core.Environment;
using GFramework.Core.Events;
using GFramework.Core.Ioc;
using GFramework.Core.Query;
using Mediator;
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
namespace GFramework.Core.Tests.Architectures;
@ -34,6 +34,10 @@ namespace GFramework.Core.Tests.Architectures;
[TestFixture]
public class ArchitectureServicesTests
{
private TestArchitectureContextV3? _context;
private ArchitectureServices? _services;
[SetUp]
public void SetUp()
{
@ -41,9 +45,6 @@ public class ArchitectureServicesTests
_context = new TestArchitectureContextV3();
}
private ArchitectureServices? _services;
private TestArchitectureContextV3? _context;
private void RegisterBuiltInServices()
{
_services!.ModuleManager.RegisterBuiltInModules(_services.Container);
@ -347,61 +348,59 @@ public class TestArchitectureContextV3 : IArchitectureContext
{
}
public ValueTask<TResponse> SendRequestAsync<TResponse>(global::GFramework.Core.Abstractions.Cqrs.IRequest<TResponse> request,
public ValueTask<TResponse> SendRequestAsync<TResponse>(IRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public TResponse SendRequest<TResponse>(global::GFramework.Core.Abstractions.Cqrs.IRequest<TResponse> request)
public TResponse SendRequest<TResponse>(IRequest<TResponse> request)
{
throw new NotImplementedException();
}
public ValueTask<TResponse> SendCommandAsync<TResponse>(
global::GFramework.Core.Abstractions.Cqrs.Command.ICommand<TResponse> command,
public ValueTask<TResponse> SendCommandAsync<TResponse>(Abstractions.Cqrs.Command.ICommand<TResponse> command,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public TResponse SendCommand<TResponse>(global::GFramework.Core.Abstractions.Cqrs.Command.ICommand<TResponse> command)
public TResponse SendCommand<TResponse>(Abstractions.Cqrs.Command.ICommand<TResponse> command)
{
throw new NotImplementedException();
}
public ValueTask<TResponse> SendQueryAsync<TResponse>(
global::GFramework.Core.Abstractions.Cqrs.Query.IQuery<TResponse> query,
public ValueTask<TResponse> SendQueryAsync<TResponse>(Abstractions.Cqrs.Query.IQuery<TResponse> query,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public TResponse SendQuery<TResponse>(global::GFramework.Core.Abstractions.Cqrs.Query.IQuery<TResponse> query)
public TResponse SendQuery<TResponse>(Abstractions.Cqrs.Query.IQuery<TResponse> query)
{
throw new NotImplementedException();
}
public ValueTask PublishAsync<TNotification>(TNotification notification,
CancellationToken cancellationToken = default) where TNotification : global::GFramework.Core.Abstractions.Cqrs.INotification
CancellationToken cancellationToken = default) where TNotification : INotification
{
throw new NotImplementedException();
}
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(
global::GFramework.Core.Abstractions.Cqrs.IStreamRequest<TResponse> request,
IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public ValueTask SendAsync<TCommand>(TCommand command, CancellationToken cancellationToken = default)
where TCommand : global::GFramework.Core.Abstractions.Cqrs.IRequest<global::GFramework.Core.Abstractions.Cqrs.Unit>
where TCommand : IRequest<Unit>
{
throw new NotImplementedException();
}
public ValueTask<TResponse> SendAsync<TResponse>(global::GFramework.Core.Abstractions.Cqrs.IRequest<TResponse> command,
public ValueTask<TResponse> SendAsync<TResponse>(IRequest<TResponse> command,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();

View File

@ -1,5 +1,6 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Abstractions.Environment;
using GFramework.Core.Abstractions.Events;
using GFramework.Core.Abstractions.Ioc;
@ -13,7 +14,6 @@ using GFramework.Core.Environment;
using GFramework.Core.Events;
using GFramework.Core.Ioc;
using GFramework.Core.Query;
using Mediator;
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
namespace GFramework.Core.Tests.Architectures;
@ -394,61 +394,136 @@ public class TestArchitectureContext : IArchitectureContext
{
}
public ValueTask<TResponse> SendRequestAsync<TResponse>(global::GFramework.Core.Abstractions.Cqrs.IRequest<TResponse> request,
/// <summary>
/// 测试桩:异步发送统一 CQRS 请求。
/// </summary>
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="request">要发送的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public ValueTask<TResponse> SendRequestAsync<TResponse>(IRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public TResponse SendRequest<TResponse>(global::GFramework.Core.Abstractions.Cqrs.IRequest<TResponse> request)
/// <summary>
/// 测试桩:同步发送统一 CQRS 请求。
/// </summary>
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="request">要发送的请求。</param>
/// <returns>请求响应。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public TResponse SendRequest<TResponse>(IRequest<TResponse> request)
{
throw new NotImplementedException();
}
public ValueTask<TResponse> SendCommandAsync<TResponse>(
global::GFramework.Core.Abstractions.Cqrs.Command.ICommand<TResponse> command,
/// <summary>
/// 测试桩:异步发送 CQRS 命令并返回响应。
/// </summary>
/// <typeparam name="TResponse">命令响应类型。</typeparam>
/// <param name="command">要发送的命令。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>命令响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public ValueTask<TResponse> SendCommandAsync<TResponse>(Abstractions.Cqrs.Command.ICommand<TResponse> command,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public TResponse SendCommand<TResponse>(global::GFramework.Core.Abstractions.Cqrs.Command.ICommand<TResponse> command)
/// <summary>
/// 测试桩:同步发送 CQRS 命令并返回响应。
/// </summary>
/// <typeparam name="TResponse">命令响应类型。</typeparam>
/// <param name="command">要发送的命令。</param>
/// <returns>命令响应。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public TResponse SendCommand<TResponse>(Abstractions.Cqrs.Command.ICommand<TResponse> command)
{
throw new NotImplementedException();
}
public ValueTask<TResponse> SendQueryAsync<TResponse>(
global::GFramework.Core.Abstractions.Cqrs.Query.IQuery<TResponse> query,
/// <summary>
/// 测试桩:异步发送 CQRS 查询并返回结果。
/// </summary>
/// <typeparam name="TResponse">查询结果类型。</typeparam>
/// <param name="query">要发送的查询。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>查询结果任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public ValueTask<TResponse> SendQueryAsync<TResponse>(Abstractions.Cqrs.Query.IQuery<TResponse> query,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public TResponse SendQuery<TResponse>(global::GFramework.Core.Abstractions.Cqrs.Query.IQuery<TResponse> query)
/// <summary>
/// 测试桩:同步发送 CQRS 查询并返回结果。
/// </summary>
/// <typeparam name="TResponse">查询结果类型。</typeparam>
/// <param name="query">要发送的查询。</param>
/// <returns>查询结果。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public TResponse SendQuery<TResponse>(Abstractions.Cqrs.Query.IQuery<TResponse> query)
{
throw new NotImplementedException();
}
/// <summary>
/// 测试桩:异步发布 CQRS 通知。
/// </summary>
/// <typeparam name="TNotification">通知类型。</typeparam>
/// <param name="notification">要发布的通知。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>通知发布任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public ValueTask PublishAsync<TNotification>(TNotification notification,
CancellationToken cancellationToken = default) where TNotification : global::GFramework.Core.Abstractions.Cqrs.INotification
CancellationToken cancellationToken = default) where TNotification : INotification
{
throw new NotImplementedException();
}
/// <summary>
/// 测试桩:创建 CQRS 流式请求响应序列。
/// </summary>
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
/// <param name="request">流式请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>异步响应流。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(
global::GFramework.Core.Abstractions.Cqrs.IStreamRequest<TResponse> request,
IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
/// <summary>
/// 测试桩:异步发送无返回值 CQRS 命令。
/// </summary>
/// <typeparam name="TCommand">命令类型。</typeparam>
/// <param name="command">要发送的命令。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>命令发送任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public ValueTask SendAsync<TCommand>(TCommand command, CancellationToken cancellationToken = default)
where TCommand : global::GFramework.Core.Abstractions.Cqrs.IRequest<global::GFramework.Core.Abstractions.Cqrs.Unit>
where TCommand : IRequest<Unit>
{
throw new NotImplementedException();
}
public ValueTask<TResponse> SendAsync<TResponse>(global::GFramework.Core.Abstractions.Cqrs.IRequest<TResponse> command,
/// <summary>
/// 测试桩:异步发送带返回值的 CQRS 请求。
/// </summary>
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="command">要发送的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public ValueTask<TResponse> SendAsync<TResponse>(IRequest<TResponse> command,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
@ -468,7 +543,7 @@ public class TestArchitectureContext : IArchitectureContext
/// <typeparam name="TResult">返回值类型</typeparam>
/// <param name="command">命令对象</param>
/// <returns>命令执行结果</returns>
public TResult SendCommand<TResult>(Abstractions.Command.ICommand<TResult> command)
public TResult SendCommand<TResult>(ICommand<TResult> command)
{
return default!;
}
@ -489,7 +564,7 @@ public class TestArchitectureContext : IArchitectureContext
/// <typeparam name="TResult">查询结果类型</typeparam>
/// <param name="query">查询对象</param>
/// <returns>查询结果</returns>
public TResult SendQuery<TResult>(Abstractions.Query.IQuery<TResult> query)
public TResult SendQuery<TResult>(IQuery<TResult> query)
{
return default!;
}

View File

@ -1,3 +1,4 @@
using System.Reflection;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Lifecycle;
@ -5,7 +6,6 @@ using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Architectures;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Tests.Architectures;
@ -181,11 +181,37 @@ public class TestArchitectureWithRegistry : IArchitecture
throw new NotImplementedException();
}
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
public void RegisterCqrsPipelineBehavior<TBehavior>() where TBehavior : class
{
throw new NotImplementedException();
}
/// <summary>
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
/// </summary>
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
{
throw new NotImplementedException();
}
/// <summary>
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
{
throw new NotImplementedException();
}
[Obsolete("Use RegisterCqrsPipelineBehavior<TBehavior>() instead.")]
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
{
RegisterCqrsPipelineBehavior<TBehavior>();
}
public IArchitectureModule InstallModule(IArchitectureModule module)
{
throw new NotImplementedException();
@ -306,11 +332,37 @@ public class TestArchitectureWithoutRegistry : IArchitecture
throw new NotImplementedException();
}
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
public void RegisterCqrsPipelineBehavior<TBehavior>() where TBehavior : class
{
throw new NotImplementedException();
}
/// <summary>
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
/// </summary>
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
{
throw new NotImplementedException();
}
/// <summary>
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
{
throw new NotImplementedException();
}
[Obsolete("Use RegisterCqrsPipelineBehavior<TBehavior>() instead.")]
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
{
RegisterCqrsPipelineBehavior<TBehavior>();
}
public IArchitectureModule InstallModule(IArchitectureModule module)
{
throw new NotImplementedException();
@ -363,4 +415,4 @@ public class TestArchitectureWithoutRegistry : IArchitecture
public void RegisterLifecycleHook(IArchitectureLifecycleHook hook)
{
}
}
}

View File

@ -0,0 +1,174 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Cqrs.Extensions;
namespace GFramework.Core.Tests.Coroutine;
/// <summary>
/// <see cref="CqrsCoroutineExtensions" /> 的单元测试类。
/// 验证新的 CQRS 协程扩展直接走框架内建 CQRS runtime
/// 并确保协程对命令调度异常的传播行为保持稳定。
/// </summary>
[TestFixture]
public class CqrsCoroutineExtensionsTests
{
/// <summary>
/// 验证SendCommandCoroutine应该返回IEnumerator<IYieldInstruction>
/// </summary>
[Test]
public void SendCommandCoroutine_Should_Return_IEnumerator_Of_YieldInstruction()
{
var command = new TestCommand("Test");
var contextAware = new TestContextAware();
contextAware.MockContext
.Setup(ctx => ctx.SendAsync(command, It.IsAny<CancellationToken>()))
.Returns(ValueTask.CompletedTask);
var coroutine = CqrsCoroutineExtensions.SendCommandCoroutine(contextAware, command);
Assert.That(coroutine, Is.InstanceOf<IEnumerator<IYieldInstruction>>());
}
/// <summary>
/// 验证 SendCommandCoroutine 在底层命令调度失败时会重新抛出原始异常。
/// </summary>
[Test]
public void SendCommandCoroutine_Should_Rethrow_Inner_Exception_When_Command_Fails()
{
var command = new TestCommand("Test");
var contextAware = new TestContextAware();
var expectedException = new InvalidOperationException("Command failed.");
contextAware.MockContext
.Setup(ctx => ctx.SendAsync(command, It.IsAny<CancellationToken>()))
.Returns(new ValueTask(Task.FromException(expectedException)));
var coroutine = CqrsCoroutineExtensions.SendCommandCoroutine(contextAware, command);
Assert.That(coroutine.MoveNext(), Is.True);
var exception = Assert.Throws<InvalidOperationException>(() => coroutine.MoveNext());
Assert.That(exception, Is.SameAs(expectedException));
}
/// <summary>
/// 验证 SendCommandCoroutine 在提供错误回调时也会传递解包后的原始异常,
/// 避免回调路径暴露 <see cref="AggregateException" />。
/// </summary>
[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<CancellationToken>()))
.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));
}
/// <summary>
/// 验证 SendCommandCoroutine 在底层命令被取消且未提供错误回调时会抛出取消异常。
/// </summary>
[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<CancellationToken>()))
.Returns(new ValueTask(Task.FromCanceled(cancellationTokenSource.Token)));
var coroutine = CqrsCoroutineExtensions.SendCommandCoroutine(contextAware, command);
Assert.That(coroutine.MoveNext(), Is.True);
Assert.Throws<TaskCanceledException>(() => coroutine.MoveNext());
}
/// <summary>
/// 验证 SendCommandCoroutine 在底层命令被取消且提供错误回调时会把取消异常转发给回调。
/// </summary>
[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<CancellationToken>()))
.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<TaskCanceledException>());
}
/// <summary>
/// 测试用的简单命令类
/// </summary>
private sealed record TestCommand(string Data) : IRequest<Unit>;
/// <summary>
/// 上下文感知基类的模拟实现
/// </summary>
private sealed class TestContextAware : IContextAware
{
/// <summary>
/// 提供可配置的架构上下文 Mock。
/// </summary>
public Mock<IArchitectureContext> MockContext { get; } = new();
/// <summary>
/// 获取当前架构上下文。
/// </summary>
/// <returns>用于 CQRS 调用的架构上下文实例。</returns>
public IArchitectureContext GetContext()
{
return MockContext.Object;
}
/// <summary>
/// 设置架构上下文。
/// </summary>
/// <param name="context">要设置的架构上下文。</param>
public void SetContext(IArchitectureContext context)
{
}
}
}

View File

@ -1,104 +0,0 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Coroutine.Extensions;
using Mediator;
using Moq;
namespace GFramework.Core.Tests.Coroutine;
/// <summary>
/// MediatorCoroutineExtensions的单元测试类
/// 测试Mediator模式与协程集成的扩展方法
/// 注意:由于 Mediator 使用源生成器,本测试类主要验证接口和参数验证
/// </summary>
[TestFixture]
public class MediatorCoroutineExtensionsTests
{
/// <summary>
/// 测试用的简单命令类
/// </summary>
private class TestCommand : IRequest<Unit>
{
public string Data { get; set; } = string.Empty;
}
/// <summary>
/// 测试用的简单事件类
/// </summary>
private class TestEvent
{
public string Data { get; set; } = string.Empty;
}
/// <summary>
/// 上下文感知基类的模拟实现
/// </summary>
private class TestContextAware : IContextAware
{
public readonly Mock<IArchitectureContext> _mockContext = new();
public IArchitectureContext GetContext()
{
return _mockContext.Object;
}
public void SetContext(IArchitectureContext context)
{
}
}
/// <summary>
/// 验证SendCommandCoroutine应该返回IEnumerator<IYieldInstruction>
/// </summary>
[Test]
public void SendCommandCoroutine_Should_Return_IEnumerator_Of_YieldInstruction()
{
var command = new TestCommand { Data = "Test" };
var contextAware = new TestContextAware();
// 创建 mediator 模拟
var mediatorMock = new Mock<IMediator>();
contextAware._mockContext
.Setup(ctx => ctx.GetService<IMediator>())
.Returns(mediatorMock.Object);
var coroutine = MediatorCoroutineExtensions.SendCommandCoroutine(contextAware, command);
Assert.That(coroutine, Is.InstanceOf<IEnumerator<IYieldInstruction>>());
}
/// <summary>
/// 验证SendCommandCoroutine应该在mediator为null时抛出NullReferenceException
/// </summary>
[Test]
public void SendCommandCoroutine_Should_Throw_When_Mediator_Null()
{
var command = new TestCommand { Data = "Test" };
var contextAware = new TestContextAware();
// 设置上下文服务以返回null mediator
contextAware._mockContext
.Setup(ctx => ctx.GetService<IMediator>())
.Returns((IMediator?)null);
// 创建协程
var coroutine = MediatorCoroutineExtensions.SendCommandCoroutine(contextAware, command);
// 调用 MoveNext 时应该抛出 NullReferenceException
Assert.Throws<NullReferenceException>(() => coroutine.MoveNext());
}
}

View File

@ -115,6 +115,80 @@ internal sealed class CqrsHandlerRegistrarTests
LoggerFactoryResolver.Provider = originalProvider;
}
}
/// <summary>
/// 验证当程序集提供源码生成的注册器时,运行时会优先使用该注册器而不是反射扫描类型列表。
/// </summary>
[Test]
public void RegisterHandlers_Should_Use_Generated_Registry_When_Available()
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Cqrs.GeneratedRegistryAssembly, Version=1.0.0.0");
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(GeneratedNotificationHandlerRegistry))]);
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var handlers = container.GetAll<INotificationHandler<GeneratedRegistryNotification>>();
Assert.That(
handlers.Select(static handler => handler.GetType()),
Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)]));
}
/// <summary>
/// 验证当生成注册器元数据损坏时,运行时会记录告警并回退到反射扫描路径。
/// </summary>
[Test]
public void RegisterHandlers_Should_Fall_Back_To_Reflection_When_Generated_Registry_Is_Invalid()
{
var originalProvider = LoggerFactoryResolver.Provider;
var capturingProvider = new CapturingLoggerFactoryProvider(LogLevel.Warning);
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Cqrs.InvalidGeneratedRegistryAssembly, Version=1.0.0.0");
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(string))]);
generatedAssembly
.Setup(static assembly => assembly.GetTypes())
.Returns([typeof(AlphaDeterministicNotificationHandler)]);
LoggerFactoryResolver.Provider = capturingProvider;
try
{
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var handlers = container.GetAll<INotificationHandler<DeterministicOrderNotification>>();
var warningLogs = capturingProvider.Loggers
.SelectMany(static logger => logger.Logs)
.Where(static log => log.Level == LogLevel.Warning)
.ToList();
Assert.Multiple(() =>
{
Assert.That(
handlers.Select(static handler => handler.GetType()),
Is.EqualTo([typeof(AlphaDeterministicNotificationHandler)]));
Assert.That(
warningLogs.Any(log =>
log.Message.Contains("does not implement", StringComparison.Ordinal)),
Is.True);
});
}
finally
{
LoggerFactoryResolver.Provider = originalProvider;
}
}
}
/// <summary>
@ -219,3 +293,48 @@ internal sealed class CapturingLoggerFactoryProvider : ILoggerFactoryProvider
return logger;
}
}
/// <summary>
/// 用于验证生成注册器路径的通知消息。
/// </summary>
internal sealed record GeneratedRegistryNotification : INotification;
/// <summary>
/// 由模拟的源码生成注册器显式注册的通知处理器。
/// </summary>
internal sealed class GeneratedRegistryNotificationHandler : INotificationHandler<GeneratedRegistryNotification>
{
/// <summary>
/// 处理生成注册器测试中的通知。
/// </summary>
/// <param name="notification">通知实例。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>已完成任务。</returns>
public ValueTask Handle(GeneratedRegistryNotification notification, CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
}
/// <summary>
/// 模拟源码生成器为某个程序集生成的 CQRS 处理器注册器。
/// </summary>
internal sealed class GeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry
{
/// <summary>
/// 将测试通知处理器注册到目标服务集合。
/// </summary>
/// <param name="services">承载处理器映射的服务集合。</param>
/// <param name="logger">用于记录注册诊断的日志器。</param>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(INotificationHandler<GeneratedRegistryNotification>),
typeof(GeneratedRegistryNotificationHandler));
logger.Debug(
$"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler<GeneratedRegistryNotification>).FullName}.");
}
}

View File

@ -0,0 +1,99 @@
using System.ComponentModel;
using System.Reflection;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Architectures;
using GFramework.Core.Coroutine.Extensions;
using GFramework.Core.Ioc;
namespace GFramework.Core.Tests.Cqrs;
/// <summary>
/// 锁定历史 Mediator 兼容入口的正式弃用策略。
/// 这些测试确保旧 API 不仅保留行为兼容,还会通过编译期提示和 IntelliSense 隐藏引导调用方迁移到新的 CQRS 命名。
/// </summary>
[TestFixture]
public class MediatorCompatibilityDeprecationTests
{
/// <summary>
/// 验证公开兼容方法仍可用,但已被显式标记为未来移除的旧别名。
/// </summary>
[Test]
public void Legacy_Public_Methods_Should_Be_Obsolete_And_Hidden_From_Editor_Browsing()
{
AssertLegacyMethod(typeof(IArchitecture), nameof(IArchitecture.RegisterMediatorBehavior));
AssertLegacyMethod(typeof(IIocContainer), nameof(IIocContainer.RegisterMediatorBehavior));
AssertLegacyMethod(typeof(Architecture), nameof(Architecture.RegisterMediatorBehavior));
AssertLegacyMethod(typeof(MicrosoftDiContainer), nameof(MicrosoftDiContainer.RegisterMediatorBehavior));
}
/// <summary>
/// 验证历史扩展类型会把迁移目标写入弃用说明,并从 IntelliSense 主路径隐藏。
/// </summary>
[Test]
public void Legacy_Extension_Types_Should_Be_Obsolete_And_Hidden_From_Editor_Browsing()
{
AssertLegacyType(
typeof(ContextAwareMediatorExtensions),
"Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsExtensions instead.");
AssertLegacyType(
typeof(ContextAwareMediatorCommandExtensions),
"Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead.");
AssertLegacyType(
typeof(ContextAwareMediatorQueryExtensions),
"Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead.");
AssertLegacyType(
typeof(MediatorCoroutineExtensions),
"Use GFramework.Core.Cqrs.Extensions.CqrsCoroutineExtensions instead.");
}
/// <summary>
/// 断言方法级兼容 API 具备统一的弃用元数据。
/// </summary>
/// <param name="declaringType">声明该方法的类型。</param>
/// <param name="methodName">方法名称。</param>
private static void AssertLegacyMethod(Type declaringType, string methodName)
{
var method = declaringType
.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Single(candidate => candidate.Name == methodName);
var obsoleteAttribute = method.GetCustomAttribute<ObsoleteAttribute>();
var editorBrowsableAttribute = method.GetCustomAttribute<EditorBrowsableAttribute>();
Assert.Multiple(() =>
{
Assert.That(obsoleteAttribute, Is.Not.Null);
Assert.That(
obsoleteAttribute!.Message,
Does.Contain("Use RegisterCqrsPipelineBehavior<TBehavior>() instead."));
Assert.That(
obsoleteAttribute.Message,
Does.Contain("removed in a future major version"));
Assert.That(editorBrowsableAttribute, Is.Not.Null);
Assert.That(editorBrowsableAttribute!.State, Is.EqualTo(EditorBrowsableState.Never));
});
}
/// <summary>
/// 断言类型级兼容扩展具备统一的弃用元数据。
/// </summary>
/// <param name="type">兼容扩展类型。</param>
/// <param name="expectedReplacementHint">期望的迁移提示。</param>
private static void AssertLegacyType(Type type, string expectedReplacementHint)
{
var obsoleteAttribute = type.GetCustomAttribute<ObsoleteAttribute>();
var editorBrowsableAttribute = type.GetCustomAttribute<EditorBrowsableAttribute>();
Assert.Multiple(() =>
{
Assert.That(obsoleteAttribute, Is.Not.Null);
Assert.That(obsoleteAttribute!.Message, Does.Contain(expectedReplacementHint));
Assert.That(
obsoleteAttribute.Message,
Does.Contain("removed in a future major version"));
Assert.That(editorBrowsableAttribute, Is.Not.Null);
Assert.That(editorBrowsableAttribute!.State, Is.EqualTo(EditorBrowsableState.Never));
});
}
}

View File

@ -10,11 +10,6 @@
<WarningLevel>0</WarningLevel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Mediator.Abstractions" Version="3.0.2"/>
<PackageReference Include="Mediator.SourceGenerator" Version="3.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0"/>
<PackageReference Include="Moq" Version="4.20.72"/>
<PackageReference Include="NUnit" Version="4.5.1"/>

View File

@ -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;
global using GFramework.Core.Abstractions.Property;
global using Microsoft.Extensions.DependencyInjection;
global using Moq;

View File

@ -1,7 +1,9 @@
using System.Reflection;
using GFramework.Core.Abstractions.Bases;
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Core.Tests.Cqrs;
using GFramework.Core.Tests.Systems;
namespace GFramework.Core.Tests.Ioc;
@ -306,6 +308,34 @@ public class MicrosoftDiContainerTests
Assert.That(_container.Contains<TestService>(), Is.False);
}
/// <summary>
/// 测试清空容器后可以重新接入同一程序集中的 CQRS 处理器。
/// </summary>
[Test]
public void Clear_Should_Reset_Cqrs_Assembly_Deduplication_State()
{
var assembly = typeof(CqrsHandlerRegistrarTests).Assembly;
_container.RegisterCqrsHandlersFromAssembly(assembly);
Assert.That(
_container.GetServicesUnsafe.Any(static descriptor =>
descriptor.ServiceType == typeof(INotificationHandler<DeterministicOrderNotification>)),
Is.True);
_container.Clear();
Assert.That(
_container.GetServicesUnsafe.Any(static descriptor =>
descriptor.ServiceType == typeof(INotificationHandler<DeterministicOrderNotification>)),
Is.False);
_container.RegisterCqrsHandlersFromAssembly(assembly);
Assert.That(
_container.GetServicesUnsafe.Any(static descriptor =>
descriptor.ServiceType == typeof(INotificationHandler<DeterministicOrderNotification>)),
Is.True);
}
/// <summary>
/// 测试冻结容器以防止进一步注册的功能
/// </summary>
@ -676,4 +706,4 @@ public sealed class PrioritizedService : IPrioritizedService, IMixedService
public sealed class NonPrioritizedService : IMixedService
{
public string? Name { get; set; }
}
}

View File

@ -1,3 +1,5 @@
using System.ComponentModel;
using System.Reflection;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Environment;
@ -7,7 +9,6 @@ using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Environment;
using GFramework.Core.Logging;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Architectures;
@ -146,14 +147,51 @@ public abstract class Architecture : IArchitecture
#region Module Management
/// <summary>
/// 注册中介行为管道
/// 用于配置Mediator框架的行为拦截和处理逻辑。
/// 注册 CQRS 请求管道行为。
/// 可以传入开放泛型行为类型,也可以传入绑定到特定请求的封闭行为类型。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
public void RegisterCqrsPipelineBehavior<TBehavior>() where TBehavior : class
{
_modules.RegisterCqrsPipelineBehavior<TBehavior>();
}
/// <summary>
/// 注册 CQRS 请求管道行为。
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
/// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete(
"Use RegisterCqrsPipelineBehavior<TBehavior>() instead. This compatibility alias will be removed in a future major version.")]
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
{
_modules.RegisterMediatorBehavior<TBehavior>();
RegisterCqrsPipelineBehavior<TBehavior>();
}
/// <summary>
/// 从指定程序集显式注册 CQRS 处理器。
/// 该入口适用于把拆分到其他模块或扩展包程序集中的 handlers 接入当前架构。
/// </summary>
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
/// <exception cref="ArgumentNullException"><paramref name="assembly" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">当前架构的底层容器已冻结,无法继续注册处理器。</exception>
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
{
_modules.RegisterCqrsHandlersFromAssembly(assembly);
}
/// <summary>
/// 从多个程序集显式注册 CQRS 处理器。
/// 适用于在初始化阶段批量接入多个扩展程序集,并沿用容器的去重策略避免重复注册。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">当前架构的底层容器已冻结,无法继续注册处理器。</exception>
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
{
_modules.RegisterCqrsHandlersFromAssemblies(assemblies);
}
/// <summary>
@ -328,4 +366,4 @@ public abstract class Architecture : IArchitecture
}
#endregion
}
}

View File

@ -1,8 +1,6 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Environment;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Cqrs.Internal;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Architectures;
@ -23,7 +21,7 @@ internal sealed class ArchitectureBootstrapper(
/// 因为用户初始化逻辑通常会立即访问事件总线、查询执行器或环境对象。
/// </summary>
/// <param name="existingContext">调用方已经提供的上下文;如果为空则创建默认上下文。</param>
/// <param name="configurator">可选的容器配置委托,用于接入 Mediator 等扩展服务。</param>
/// <param name="configurator">可选的容器配置委托,用于接入额外服务或覆盖默认依赖绑定。</param>
/// <param name="asyncMode">是否以异步模式初始化服务模块。</param>
/// <returns>已绑定到当前架构类型的架构上下文。</returns>
public async Task<IArchitectureContext> PrepareForInitializationAsync(
@ -100,10 +98,11 @@ internal sealed class ArchitectureBootstrapper(
private void ConfigureServices(IArchitectureContext context, Action<IServiceCollection>? configurator)
{
services.SetContext(context);
CqrsHandlerRegistrar.RegisterHandlers(
services.Container,
[architectureType.Assembly, typeof(ArchitectureContext).Assembly],
logger);
services.Container.RegisterCqrsHandlersFromAssemblies(
[
architectureType.Assembly,
typeof(ArchitectureContext).Assembly
]);
if (configurator is null)
logger.Debug("No external service configurator provided. Using built-in CQRS runtime registration only.");

View File

@ -1,3 +1,5 @@
using System.ComponentModel;
using System.Reflection;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Logging;
@ -5,7 +7,7 @@ namespace GFramework.Core.Architectures;
/// <summary>
/// 架构模块管理器
/// 负责管理架构模块的安装和中介行为注册
/// 负责管理架构模块的安装和 CQRS 行为注册
/// </summary>
internal sealed class ArchitectureModules(
IArchitecture architecture,
@ -13,15 +15,56 @@ internal sealed class ArchitectureModules(
ILogger logger)
{
/// <summary>
/// 注册中介行为管道
/// 用于配置Mediator框架的行为拦截和处理逻辑。
/// 注册 CQRS 请求管道行为。
/// 支持开放泛型行为类型和针对单一请求的封闭行为类型。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
public void RegisterCqrsPipelineBehavior<TBehavior>() where TBehavior : class
{
logger.Debug($"Registering CQRS pipeline behavior: {typeof(TBehavior).Name}");
services.Container.RegisterCqrsPipelineBehavior<TBehavior>();
}
/// <summary>
/// 注册 CQRS 请求管道行为。
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
/// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete(
"Use RegisterCqrsPipelineBehavior<TBehavior>() instead. This compatibility alias will be removed in a future major version.")]
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
{
logger.Debug($"Registering mediator behavior: {typeof(TBehavior).Name}");
services.Container.RegisterMediatorBehavior<TBehavior>();
RegisterCqrsPipelineBehavior<TBehavior>();
}
/// <summary>
/// 从指定程序集显式注册 CQRS 处理器。
/// 该入口用于把默认架构程序集之外的扩展处理器接入当前架构容器。
/// </summary>
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
/// <exception cref="ArgumentNullException"><paramref name="assembly" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">底层容器已冻结,无法继续注册处理器。</exception>
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
{
ArgumentNullException.ThrowIfNull(assembly);
logger.Debug($"Registering CQRS handlers from assembly: {assembly.FullName ?? assembly.GetName().Name}");
services.Container.RegisterCqrsHandlersFromAssembly(assembly);
}
/// <summary>
/// 从多个程序集显式注册 CQRS 处理器。
/// 它会复用容器级去重逻辑,避免模块重复接入相同程序集时重复注册 handler。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">底层容器已冻结,无法继续注册处理器。</exception>
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
{
ArgumentNullException.ThrowIfNull(assemblies);
logger.Debug("Registering CQRS handlers from additional assemblies.");
services.Container.RegisterCqrsHandlersFromAssemblies(assemblies);
}
/// <summary>
@ -37,4 +80,4 @@ internal sealed class ArchitectureModules(
logger.Info($"Module installed: {name}");
return module;
}
}
}

View File

@ -0,0 +1,73 @@
using System.Runtime.ExceptionServices;
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Coroutine.Extensions;
namespace GFramework.Core.Cqrs.Extensions;
/// <summary>
/// 提供 CQRS 命令与协程集成的扩展方法。
/// 这些扩展直接走架构上下文的内建 CQRS runtime不依赖外部 Mediator 服务。
/// </summary>
public static class CqrsCoroutineExtensions
{
/// <summary>
/// 以协程方式发送无返回值 CQRS 命令并处理可能的异常。
/// </summary>
/// <typeparam name="TCommand">命令类型。</typeparam>
/// <param name="contextAware">上下文感知对象,用于获取架构上下文。</param>
/// <param name="command">要发送的命令对象。</param>
/// <param name="onError">发生异常时的回调处理函数。</param>
/// <returns>协程枚举器,用于协程执行。</returns>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="contextAware" /> 或 <paramref name="command" /> 为 <see langword="null" /> 时抛出。
/// </exception>
/// <exception cref="TaskCanceledException">
/// 当底层命令调度被取消且未提供 <paramref name="onError" /> 时抛出。
/// </exception>
/// <exception cref="Exception">
/// 当底层命令调度失败且未提供 <paramref name="onError" /> 时,抛出底层原始异常。
/// </exception>
/// <remarks>
/// 当底层命令调度失败时,该扩展会把底层异常解包后传给 <paramref name="onError" />
/// 在取消时则统一暴露 <see cref="TaskCanceledException" />,避免成功、失败与取消三种完成状态被混淆。
/// </remarks>
public static IEnumerator<IYieldInstruction> SendCommandCoroutine<TCommand>(
this IContextAware contextAware,
TCommand command,
Action<Exception>? onError = null)
where TCommand : IRequest<Unit>
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(command);
var task = contextAware.GetContext().SendAsync(command).AsTask();
yield return task.AsCoroutineInstruction();
if (task.IsCanceled)
{
// 取消态与成功态区分:协程层统一映射为 TaskCanceledException。
var canceledException = new TaskCanceledException(task);
if (onError != null)
{
onError.Invoke(canceledException);
yield break;
}
// 保留原始抛出栈,避免调试时丢失异常来源。
ExceptionDispatchInfo.Capture(canceledException).Throw();
}
if (!task.IsFaulted)
yield break;
// 优先解包业务异常,避免直接暴露 AggregateException。
var exception = task.Exception!.InnerException ?? task.Exception;
if (onError != null)
onError.Invoke(exception);
else
// 继续保留原始栈信息。
ExceptionDispatchInfo.Capture(exception).Throw();
}
}

View File

@ -11,22 +11,28 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.ComponentModel;
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Abstractions.Rule;
using Mediator;
using GFramework.Core.Cqrs.Extensions;
namespace GFramework.Core.Coroutine.Extensions;
/// <summary>
/// 提供Mediator模式与协程集成的扩展方法。
/// 包含发送命令和等待事件的协程实现。
/// 提供 CQRS 命令与协程集成的扩展方法。
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="GFramework.Core.Cqrs.Extensions.CqrsCoroutineExtensions" />。
/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete(
"Use GFramework.Core.Cqrs.Extensions.CqrsCoroutineExtensions instead. This compatibility alias will be removed in a future major version.")]
public static class MediatorCoroutineExtensions
{
/// <summary>
/// 以协程方式发送命令并处理可能的异常。
/// 以协程方式发送无返回值 CQRS 命令并处理可能的异常。
/// </summary>
/// <typeparam name="TCommand">命令的类型</typeparam>
/// <typeparam name="TCommand">命令的类型</typeparam>
/// <param name="contextAware">上下文感知对象,用于获取服务</param>
/// <param name="command">要发送的命令对象</param>
/// <param name="onError">发生异常时的回调处理函数</param>
@ -35,20 +41,8 @@ public static class MediatorCoroutineExtensions
this IContextAware contextAware,
TCommand command,
Action<Exception>? onError = null)
where TCommand : notnull
where TCommand : IRequest<Unit>
{
var mediator = contextAware
.GetContext()
.GetService<IMediator>()!;
var task = mediator.Send(command).AsTask();
yield return task.AsCoroutineInstruction();
if (!task.IsFaulted) yield break;
if (onError != null)
onError.Invoke(task.Exception!);
else
throw task.Exception!.InnerException ?? task.Exception;
return CqrsCoroutineExtensions.SendCommandCoroutine(contextAware, command, onError);
}
}
}

View File

@ -7,7 +7,8 @@ namespace GFramework.Core.Cqrs.Internal;
/// <summary>
/// 在架构初始化期间扫描并注册 CQRS 处理器。
/// 首批实现采用运行时反射扫描,优先满足“无需 AddMediator 即可工作”的迁移目标。
/// 运行时会优先尝试使用源码生成的程序集级注册器,以减少冷启动阶段的反射开销;
/// 当目标程序集没有生成注册器,或注册器不可用时,再回退到运行时反射扫描。
/// </summary>
internal static class CqrsHandlerRegistrar
{
@ -31,10 +32,84 @@ internal static class CqrsHandlerRegistrar
.Distinct()
.OrderBy(GetAssemblySortKey, StringComparer.Ordinal))
{
if (TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger))
continue;
RegisterAssemblyHandlers(container.GetServicesUnsafe, assembly, logger);
}
}
/// <summary>
/// 优先使用程序集级源码生成注册器完成 CQRS 映射注册。
/// </summary>
/// <param name="services">目标服务集合。</param>
/// <param name="assembly">当前要处理的程序集。</param>
/// <param name="logger">日志记录器。</param>
/// <returns>当成功使用生成注册器时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
private static bool TryRegisterGeneratedHandlers(IServiceCollection services, Assembly assembly, ILogger logger)
{
var assemblyName = GetAssemblySortKey(assembly);
try
{
var registryTypes = assembly
.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), inherit: false)
.OfType<CqrsHandlerRegistryAttribute>()
.Select(static attribute => attribute.RegistryType)
.Where(static type => type is not null)
.Distinct()
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
.ToList();
if (registryTypes.Count == 0)
return false;
var registries = new List<ICqrsHandlerRegistry>(registryTypes.Count);
foreach (var registryType in registryTypes)
{
if (!typeof(ICqrsHandlerRegistry).IsAssignableFrom(registryType))
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}.");
return false;
}
if (registryType.IsAbstract)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract.");
return false;
}
if (Activator.CreateInstance(registryType, nonPublic: true) is not ICqrsHandlerRegistry registry)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it could not be instantiated.");
return false;
}
registries.Add(registry);
}
foreach (var registry in registries)
{
logger.Debug(
$"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}.");
registry.Register(services, logger);
}
return true;
}
catch (Exception exception)
{
logger.Warn(
$"Generated CQRS handler registry discovery failed for assembly {assemblyName}. Falling back to reflection scan.");
logger.Warn(
$"Failed to use generated CQRS handler registry for assembly {assemblyName}: {exception.Message}");
return false;
}
}
/// <summary>
/// 注册单个程序集里的所有 CQRS 处理器映射。
/// </summary>

View File

@ -0,0 +1,59 @@
using GFramework.Core.Abstractions.Cqrs.Command;
using GFramework.Core.Abstractions.Rule;
namespace GFramework.Core.Cqrs.Extensions;
/// <summary>
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 命令扩展方法。
/// </summary>
/// <remarks>
/// 该扩展类将命令分发统一路由到架构上下文中的 CQRS 运行时。
/// </remarks>
public static class ContextAwareCqrsCommandExtensions
{
/// <summary>
/// 发送命令的同步版本(不推荐,仅用于兼容同步调用链)。
/// </summary>
/// <typeparam name="TResponse">命令响应类型。</typeparam>
/// <param name="contextAware">实现 <see cref="IContextAware" /> 接口的对象。</param>
/// <param name="command">要发送的命令对象。</param>
/// <returns>命令执行结果。</returns>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="contextAware" /> 或 <paramref name="command" /> 为 <see langword="null" /> 时抛出。
/// </exception>
/// <remarks>
/// 同步方法仅用于兼容同步调用链;新代码建议优先使用异步版本。
/// </remarks>
public static TResponse SendCommand<TResponse>(this IContextAware contextAware, ICommand<TResponse> command)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(command);
return contextAware.GetContext().SendCommand(command);
}
/// <summary>
/// 异步发送命令并返回结果。
/// </summary>
/// <typeparam name="TResponse">命令响应类型。</typeparam>
/// <param name="contextAware">实现 <see cref="IContextAware" /> 接口的对象。</param>
/// <param name="command">要发送的命令对象。</param>
/// <param name="cancellationToken">取消令牌,用于取消操作。</param>
/// <returns>包含命令执行结果的 <see cref="ValueTask{TResult}" />。</returns>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="contextAware" /> 或 <paramref name="command" /> 为 <see langword="null" /> 时抛出。
/// </exception>
/// <remarks>
/// 该方法直接返回底层 <see cref="ValueTask{TResult}" />,避免额外的 async 状态机分配。
/// </remarks>
public static ValueTask<TResponse> SendCommandAsync<TResponse>(
this IContextAware contextAware,
ICommand<TResponse> command,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(command);
return contextAware.GetContext().SendCommandAsync(command, cancellationToken);
}
}

View File

@ -0,0 +1,141 @@
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Abstractions.Rule;
namespace GFramework.Core.Cqrs.Extensions;
/// <summary>
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 统一扩展方法。
/// 这些扩展直接委托给架构上下文的内建 CQRS runtime作为新的中性命名入口。
/// </summary>
public static class ContextAwareCqrsExtensions
{
/// <summary>
/// 发送请求(统一处理 Command/Query
/// </summary>
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="contextAware">实现 <see cref="IContextAware" /> 接口的对象。</param>
/// <param name="request">要发送的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求结果。</returns>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="contextAware" /> 或 <paramref name="request" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public static ValueTask<TResponse> SendRequestAsync<TResponse>(
this IContextAware contextAware,
IRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(request);
return contextAware.GetContext().SendRequestAsync(request, cancellationToken);
}
/// <summary>
/// 发送请求(同步版本,不推荐)。
/// </summary>
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="contextAware">实现 <see cref="IContextAware" /> 接口的对象。</param>
/// <param name="request">要发送的请求。</param>
/// <returns>请求结果。</returns>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="contextAware" /> 或 <paramref name="request" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public static TResponse SendRequest<TResponse>(this IContextAware contextAware, IRequest<TResponse> request)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(request);
return contextAware.GetContext().SendRequest(request);
}
/// <summary>
/// 发布通知(一对多事件)。
/// </summary>
/// <typeparam name="TNotification">通知类型。</typeparam>
/// <param name="contextAware">实现 <see cref="IContextAware" /> 接口的对象。</param>
/// <param name="notification">要发布的通知。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>异步任务。</returns>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="contextAware" /> 或 <paramref name="notification" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public static ValueTask PublishAsync<TNotification>(
this IContextAware contextAware,
TNotification notification,
CancellationToken cancellationToken = default)
where TNotification : INotification
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(notification);
return contextAware.GetContext().PublishAsync(notification, cancellationToken);
}
/// <summary>
/// 创建流式请求。
/// </summary>
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="contextAware">实现 <see cref="IContextAware" /> 接口的对象。</param>
/// <param name="request">流式请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>异步响应流。</returns>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="contextAware" /> 或 <paramref name="request" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public static IAsyncEnumerable<TResponse> CreateStream<TResponse>(
this IContextAware contextAware,
IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(request);
return contextAware.GetContext().CreateStream(request, cancellationToken);
}
/// <summary>
/// 发送无返回值命令。
/// </summary>
/// <typeparam name="TCommand">命令类型。</typeparam>
/// <param name="contextAware">实现 <see cref="IContextAware" /> 接口的对象。</param>
/// <param name="command">要发送的命令。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>异步任务。</returns>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="contextAware" /> 或 <paramref name="command" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public static ValueTask SendAsync<TCommand>(
this IContextAware contextAware,
TCommand command,
CancellationToken cancellationToken = default)
where TCommand : IRequest<Unit>
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(command);
return contextAware.GetContext().SendAsync(command, cancellationToken);
}
/// <summary>
/// 发送带返回值命令。
/// </summary>
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="contextAware">实现 <see cref="IContextAware" /> 接口的对象。</param>
/// <param name="command">要发送的命令。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>命令执行结果。</returns>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="contextAware" /> 或 <paramref name="command" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public static ValueTask<TResponse> SendAsync<TResponse>(
this IContextAware contextAware,
IRequest<TResponse> command,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(command);
return contextAware.GetContext().SendAsync(command, cancellationToken);
}
}

View File

@ -0,0 +1,50 @@
using GFramework.Core.Abstractions.Cqrs.Query;
using GFramework.Core.Abstractions.Rule;
namespace GFramework.Core.Cqrs.Extensions;
/// <summary>
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 查询扩展方法。
/// </summary>
public static class ContextAwareCqrsQueryExtensions
{
/// <summary>
/// 发送查询的同步版本(不推荐,仅用于兼容同步调用链)。
/// </summary>
/// <typeparam name="TResponse">查询响应类型。</typeparam>
/// <param name="contextAware">实现 <see cref="IContextAware" /> 接口的对象。</param>
/// <param name="query">要发送的查询对象。</param>
/// <returns>查询结果。</returns>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="contextAware" /> 或 <paramref name="query" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public static TResponse SendQuery<TResponse>(this IContextAware contextAware, IQuery<TResponse> query)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(query);
return contextAware.GetContext().SendQuery(query);
}
/// <summary>
/// 异步发送查询并返回结果。
/// </summary>
/// <typeparam name="TResponse">查询响应类型。</typeparam>
/// <param name="contextAware">实现 <see cref="IContextAware" /> 接口的对象。</param>
/// <param name="query">要发送的查询对象。</param>
/// <param name="cancellationToken">取消令牌,用于取消操作。</param>
/// <returns>包含查询结果的 <see cref="ValueTask{TResult}" />。</returns>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="contextAware" /> 或 <paramref name="query" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public static ValueTask<TResponse> SendQueryAsync<TResponse>(
this IContextAware contextAware,
IQuery<TResponse> query,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(query);
return contextAware.GetContext().SendQueryAsync(query, cancellationToken);
}
}

View File

@ -1,11 +1,18 @@
using GFramework.Core.Abstractions.Rule;
using System.ComponentModel;
using GFramework.Core.Abstractions.Cqrs.Command;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Cqrs.Extensions;
namespace GFramework.Core.Extensions;
/// <summary>
/// 提供对 IContextAware 接口的 CQRS 命令扩展方法。
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 命令扩展方法。
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="GFramework.Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions" />。
/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete(
"Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead. This compatibility alias will be removed in a future major version.")]
public static class ContextAwareMediatorCommandExtensions
{
/// <summary>
@ -19,11 +26,7 @@ public static class ContextAwareMediatorCommandExtensions
public static TResponse SendCommand<TResponse>(this IContextAware contextAware,
ICommand<TResponse> command)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(command);
var context = contextAware.GetContext();
return context.SendCommand(command);
return ContextAwareCqrsCommandExtensions.SendCommand(contextAware, command);
}
/// <summary>
@ -38,10 +41,9 @@ public static class ContextAwareMediatorCommandExtensions
public static ValueTask<TResponse> SendCommandAsync<TResponse>(this IContextAware contextAware,
ICommand<TResponse> command, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(command);
var context = contextAware.GetContext();
return context.SendCommandAsync(command, cancellationToken);
return ContextAwareCqrsCommandExtensions.SendCommandAsync(
contextAware,
command,
cancellationToken);
}
}

View File

@ -1,11 +1,18 @@
using GFramework.Core.Abstractions.Rule;
using System.ComponentModel;
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Cqrs.Extensions;
namespace GFramework.Core.Extensions;
/// <summary>
/// 提供对 IContextAware 接口的 CQRS 统一接口扩展方法。
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 统一接口扩展方法。
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="GFramework.Core.Cqrs.Extensions.ContextAwareCqrsExtensions" />。
/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete(
"Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsExtensions instead. This compatibility alias will be removed in a future major version.")]
public static class ContextAwareMediatorExtensions
{
/// <summary>
@ -20,11 +27,10 @@ public static class ContextAwareMediatorExtensions
public static ValueTask<TResponse> SendRequestAsync<TResponse>(this IContextAware contextAware,
IRequest<TResponse> request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(request);
var context = contextAware.GetContext();
return context.SendRequestAsync(request, cancellationToken);
return ContextAwareCqrsExtensions.SendRequestAsync(
contextAware,
request,
cancellationToken);
}
/// <summary>
@ -38,11 +44,7 @@ public static class ContextAwareMediatorExtensions
public static TResponse SendRequest<TResponse>(this IContextAware contextAware,
IRequest<TResponse> request)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(request);
var context = contextAware.GetContext();
return context.SendRequest(request);
return ContextAwareCqrsExtensions.SendRequest(contextAware, request);
}
/// <summary>
@ -58,11 +60,10 @@ public static class ContextAwareMediatorExtensions
TNotification notification, CancellationToken cancellationToken = default)
where TNotification : INotification
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(notification);
var context = contextAware.GetContext();
return context.PublishAsync(notification, cancellationToken);
return ContextAwareCqrsExtensions.PublishAsync(
contextAware,
notification,
cancellationToken);
}
/// <summary>
@ -77,11 +78,10 @@ public static class ContextAwareMediatorExtensions
public static IAsyncEnumerable<TResponse> CreateStream<TResponse>(this IContextAware contextAware,
IStreamRequest<TResponse> request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(request);
var context = contextAware.GetContext();
return context.CreateStream(request, cancellationToken);
return ContextAwareCqrsExtensions.CreateStream(
contextAware,
request,
cancellationToken);
}
/// <summary>
@ -97,11 +97,10 @@ public static class ContextAwareMediatorExtensions
CancellationToken cancellationToken = default)
where TCommand : IRequest<Unit>
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(command);
var context = contextAware.GetContext();
return context.SendAsync(command, cancellationToken);
return ContextAwareCqrsExtensions.SendAsync(
contextAware,
command,
cancellationToken);
}
/// <summary>
@ -116,10 +115,9 @@ public static class ContextAwareMediatorExtensions
public static ValueTask<TResponse> SendAsync<TResponse>(this IContextAware contextAware,
IRequest<TResponse> command, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(command);
var context = contextAware.GetContext();
return context.SendAsync(command, cancellationToken);
return ContextAwareCqrsExtensions.SendAsync(
contextAware,
command,
cancellationToken);
}
}

View File

@ -1,11 +1,18 @@
using GFramework.Core.Abstractions.Rule;
using System.ComponentModel;
using GFramework.Core.Abstractions.Cqrs.Query;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Cqrs.Extensions;
namespace GFramework.Core.Extensions;
/// <summary>
/// 提供对 IContextAware 接口的 CQRS 查询扩展方法。
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 查询扩展方法。
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="GFramework.Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions" />。
/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete(
"Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead. This compatibility alias will be removed in a future major version.")]
public static class ContextAwareMediatorQueryExtensions
{
/// <summary>
@ -18,11 +25,7 @@ public static class ContextAwareMediatorQueryExtensions
/// <exception cref="ArgumentNullException">当 contextAware 或 query 为 null 时抛出</exception>
public static TResponse SendQuery<TResponse>(this IContextAware contextAware, IQuery<TResponse> query)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(query);
var context = contextAware.GetContext();
return context.SendQuery(query);
return ContextAwareCqrsQueryExtensions.SendQuery(contextAware, query);
}
/// <summary>
@ -37,10 +40,9 @@ public static class ContextAwareMediatorQueryExtensions
public static ValueTask<TResponse> SendQueryAsync<TResponse>(this IContextAware contextAware,
IQuery<TResponse> query, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(query);
var context = contextAware.GetContext();
return context.SendQueryAsync(query, cancellationToken);
return ContextAwareCqrsQueryExtensions.SendQueryAsync(
contextAware,
query,
cancellationToken);
}
}

View File

@ -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;
global using System.Threading.Channels;
global using Microsoft.Extensions.DependencyInjection;

View File

@ -1,11 +1,13 @@
using System.ComponentModel;
using System.Reflection;
using GFramework.Core.Abstractions.Bases;
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Cqrs.Internal;
using GFramework.Core.Logging;
using GFramework.Core.Rule;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Ioc;
@ -56,6 +58,12 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// </summary>
private readonly HashSet<object> _registeredInstances = [];
/// <summary>
/// 已接入 CQRS handler 注册流程的程序集键集合。
/// 使用稳定字符串键而不是 Assembly 引用本身,以避免默认路径和显式扩展路径使用不同 Assembly 对象时重复注册。
/// </summary>
private readonly HashSet<string> _registeredCqrsHandlerAssemblyKeys = new(StringComparer.Ordinal);
/// <summary>
/// 日志记录器,用于记录容器操作日志
/// </summary>
@ -310,13 +318,12 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <summary>
/// 注册中介行为管道
/// 用于配置Mediator框架的行为拦截和处理逻辑。
/// 注册 CQRS 请求管道行为。
/// 同时支持开放泛型行为类型和已闭合的具体行为类型,
/// 以兼容通用行为和针对单一请求的专用行为两种注册方式。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
public void RegisterCqrsPipelineBehavior<TBehavior>() where TBehavior : class
{
_lock.EnterWriteLock();
try
@ -351,7 +358,75 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
}
_logger.Debug($"Mediator behavior registered: {behaviorType.Name}");
_logger.Debug($"CQRS pipeline behavior registered: {behaviorType.Name}");
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// 注册 CQRS 请求管道行为。
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
/// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete(
"Use RegisterCqrsPipelineBehavior<TBehavior>() instead. This compatibility alias will be removed in a future major version.")]
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
{
RegisterCqrsPipelineBehavior<TBehavior>();
}
/// <summary>
/// 从指定程序集显式注册 CQRS 处理器。
/// </summary>
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
/// <exception cref="ArgumentNullException"><paramref name="assembly" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">容器已冻结,无法继续注册 CQRS 处理器。</exception>
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
{
ArgumentNullException.ThrowIfNull(assembly);
RegisterCqrsHandlersFromAssemblies([assembly]);
}
/// <summary>
/// 从多个程序集显式注册 CQRS 处理器。
/// 同一程序集只会被接入一次,避免默认启动路径与扩展模块重复注册相同 handlers。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">容器已冻结,无法继续注册 CQRS 处理器。</exception>
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
{
ArgumentNullException.ThrowIfNull(assemblies);
_lock.EnterWriteLock();
try
{
ThrowIfFrozen();
var processedAssemblyKeys = new HashSet<string>(StringComparer.Ordinal);
foreach (var assembly in assemblies
.Where(static assembly => assembly is not null)
.OrderBy(GetCqrsAssemblyRegistrationKey, StringComparer.Ordinal))
{
var assemblyKey = GetCqrsAssemblyRegistrationKey(assembly);
if (!processedAssemblyKeys.Add(assemblyKey))
continue;
if (_registeredCqrsHandlerAssemblyKeys.Contains(assemblyKey))
{
_logger.Debug(
$"Skipping CQRS handler registration for assembly {assemblyKey} because it was already registered.");
continue;
}
CqrsHandlerRegistrar.RegisterHandlers(this, [assembly], _logger);
_registeredCqrsHandlerAssemblyKeys.Add(assemblyKey);
}
}
finally
{
@ -732,6 +807,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
GetServicesUnsafe.Clear();
_registeredInstances.Clear();
_registeredCqrsHandlerAssemblyKeys.Clear();
_provider = null;
_logger.Info("Container cleared");
}
@ -803,5 +879,16 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
}
/// <summary>
/// 生成 CQRS handler 注册用的稳定程序集键。
/// 该键需要同时兼顾真实程序集与测试中使用的 mocked Assembly避免仅靠引用比较导致重复接入。
/// </summary>
/// <param name="assembly">目标程序集。</param>
/// <returns>稳定的程序集标识字符串。</returns>
private static string GetCqrsAssemblyRegistrationKey(Assembly assembly)
{
return assembly.FullName ?? assembly.GetName().Name ?? assembly.ToString();
}
#endregion
}

View File

@ -4,7 +4,7 @@ using GFramework.Core.Abstractions.Cqrs.Query;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Coroutine;
using GFramework.Core.Coroutine.Extensions;
using GFramework.Core.Extensions;
using GFramework.Core.Cqrs.Extensions;
namespace GFramework.Godot.Coroutine;
@ -29,8 +29,8 @@ public static class ContextAwareCoroutineExtensions
string? tag = null,
CancellationToken cancellationToken = default)
{
return contextAware
.SendCommandAsync(command, cancellationToken)
return ContextAwareCqrsCommandExtensions
.SendCommandAsync(contextAware, command, cancellationToken)
.AsTask()
.ToCoroutineEnumerator()
.RunCoroutine(segment, tag);
@ -53,8 +53,8 @@ public static class ContextAwareCoroutineExtensions
string? tag = null,
CancellationToken cancellationToken = default)
{
return contextAware
.SendCommandAsync(command, cancellationToken)
return ContextAwareCqrsCommandExtensions
.SendCommandAsync(contextAware, command, cancellationToken)
.AsTask()
.ToCoroutineEnumerator()
.RunCoroutine(segment, tag);
@ -77,8 +77,8 @@ public static class ContextAwareCoroutineExtensions
string? tag = null,
CancellationToken cancellationToken = default)
{
return contextAware
.SendQueryAsync(query, cancellationToken)
return ContextAwareCqrsQueryExtensions
.SendQueryAsync(contextAware, query, cancellationToken)
.AsTask()
.ToCoroutineEnumerator()
.RunCoroutine(segment, tag);
@ -100,8 +100,8 @@ public static class ContextAwareCoroutineExtensions
string? tag = null,
CancellationToken cancellationToken = default)
{
return contextAware
.PublishAsync(notification, cancellationToken)
return ContextAwareCqrsExtensions
.PublishAsync(contextAware, notification, cancellationToken)
.AsTask()
.ToCoroutineEnumerator()
.RunCoroutine(segment, tag);

View File

@ -0,0 +1,215 @@
using System.Reflection;
using GFramework.SourceGenerators.Cqrs;
using GFramework.SourceGenerators.Tests.Core;
namespace GFramework.SourceGenerators.Tests.Cqrs;
/// <summary>
/// 验证 CQRS 处理器注册生成器的输出与回退边界。
/// </summary>
[TestFixture]
public class CqrsHandlerRegistryGeneratorTests
{
/// <summary>
/// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。
/// </summary>
[Test]
public async Task Generates_Assembly_Level_Cqrs_Handler_Registry()
{
const string source = """
using System;
using System.Collections.Generic;
namespace Microsoft.Extensions.DependencyInjection
{
public interface IServiceCollection { }
public static class ServiceCollectionServiceExtensions
{
public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
}
}
namespace GFramework.Core.Abstractions.Logging
{
public interface ILogger
{
void Debug(string msg);
}
}
namespace GFramework.Core.Abstractions.Cqrs
{
public interface IRequest<TResponse> { }
public interface INotification { }
public interface IStreamRequest<TResponse> { }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
public interface ICqrsHandlerRegistry
{
void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class CqrsHandlerRegistryAttribute : Attribute
{
public CqrsHandlerRegistryAttribute(Type registryType) { }
}
}
namespace TestApp
{
using GFramework.Core.Abstractions.Cqrs;
public sealed record PingQuery() : IRequest<string>;
public sealed record DomainEvent() : INotification;
public sealed record NumberStream() : IStreamRequest<int>;
public sealed class ZetaNotificationHandler : INotificationHandler<DomainEvent> { }
public sealed class AlphaQueryHandler : IRequestHandler<PingQuery, string> { }
public sealed class StreamHandler : IStreamRequestHandler<NumberStream, int> { }
}
""";
const string expected = """
// <auto-generated />
#nullable enable
[assembly: global::GFramework.Core.Abstractions.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))]
namespace GFramework.Generated.Cqrs;
internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Core.Abstractions.Cqrs.ICqrsHandlerRegistry
{
public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger)
{
if (services is null)
throw new global::System.ArgumentNullException(nameof(services));
if (logger is null)
throw new global::System.ArgumentNullException(nameof(logger));
global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient(
services,
typeof(global::GFramework.Core.Abstractions.Cqrs.IRequestHandler<global::TestApp.PingQuery, string>),
typeof(global::TestApp.AlphaQueryHandler));
logger.Debug("Registered CQRS handler TestApp.AlphaQueryHandler as GFramework.Core.Abstractions.Cqrs.IRequestHandler<TestApp.PingQuery, string>.");
global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient(
services,
typeof(global::GFramework.Core.Abstractions.Cqrs.IStreamRequestHandler<global::TestApp.NumberStream, int>),
typeof(global::TestApp.StreamHandler));
logger.Debug("Registered CQRS handler TestApp.StreamHandler as GFramework.Core.Abstractions.Cqrs.IStreamRequestHandler<TestApp.NumberStream, int>.");
global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient(
services,
typeof(global::GFramework.Core.Abstractions.Cqrs.INotificationHandler<global::TestApp.DomainEvent>),
typeof(global::TestApp.ZetaNotificationHandler));
logger.Debug("Registered CQRS handler TestApp.ZetaNotificationHandler as GFramework.Core.Abstractions.Cqrs.INotificationHandler<TestApp.DomainEvent>.");
}
}
""";
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
source,
("CqrsHandlerRegistry.g.cs", expected));
}
/// <summary>
/// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器会放弃产出并让运行时回退到反射扫描。
/// </summary>
[Test]
public async Task Skips_Generation_When_Assembly_Contains_Private_Nested_Handler()
{
const string source = """
using System;
namespace Microsoft.Extensions.DependencyInjection
{
public interface IServiceCollection { }
public static class ServiceCollectionServiceExtensions
{
public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
}
}
namespace GFramework.Core.Abstractions.Logging
{
public interface ILogger
{
void Debug(string msg);
}
}
namespace GFramework.Core.Abstractions.Cqrs
{
public interface IRequest<TResponse> { }
public interface INotification { }
public interface IStreamRequest<TResponse> { }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
public interface ICqrsHandlerRegistry
{
void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class CqrsHandlerRegistryAttribute : Attribute
{
public CqrsHandlerRegistryAttribute(Type registryType) { }
}
}
namespace TestApp
{
using GFramework.Core.Abstractions.Cqrs;
public sealed record VisibleRequest() : IRequest<string>;
public sealed class Container
{
private sealed record HiddenRequest() : IRequest<string>;
private sealed class HiddenHandler : IRequestHandler<HiddenRequest, string> { }
}
public sealed class VisibleHandler : IRequestHandler<VisibleRequest, string> { }
}
""";
var test = new CSharpSourceGeneratorTest<CqrsHandlerRegistryGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" }
};
await test.RunAsync();
}
/// <summary>
/// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。
/// </summary>
[Test]
public void Escape_String_Literal_Handles_Control_Characters()
{
var method = typeof(CqrsHandlerRegistryGenerator).GetMethod(
"EscapeStringLiteral",
BindingFlags.NonPublic | BindingFlags.Static);
Assert.That(method, Is.Not.Null);
const string input = "line1\r\nline2\\\"";
const string expected = "line1\\r\\nline2\\\\\\\"";
var escaped = method!.Invoke(null, [input]) as string;
Assert.That(escaped, Is.EqualTo(expected));
}
}

View File

@ -0,0 +1,398 @@
using GFramework.SourceGenerators.Common.Constants;
namespace GFramework.SourceGenerators.Cqrs;
/// <summary>
/// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。
/// </summary>
[Generator]
public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
{
private const string CqrsNamespace = $"{PathContests.CoreAbstractionsNamespace}.Cqrs";
private const string LoggingNamespace = $"{PathContests.CoreAbstractionsNamespace}.Logging";
private const string IRequestHandlerMetadataName = $"{CqrsNamespace}.IRequestHandler`2";
private const string INotificationHandlerMetadataName = $"{CqrsNamespace}.INotificationHandler`1";
private const string IStreamRequestHandlerMetadataName = $"{CqrsNamespace}.IStreamRequestHandler`2";
private const string ICqrsHandlerRegistryMetadataName = $"{CqrsNamespace}.ICqrsHandlerRegistry";
private const string CqrsHandlerRegistryAttributeMetadataName = $"{CqrsNamespace}.CqrsHandlerRegistryAttribute";
private const string ILoggerMetadataName = $"{LoggingNamespace}.ILogger";
private const string IServiceCollectionMetadataName = "Microsoft.Extensions.DependencyInjection.IServiceCollection";
private const string GeneratedNamespace = "GFramework.Generated.Cqrs";
private const string GeneratedTypeName = "__GFrameworkGeneratedCqrsHandlerRegistry";
private const string HintName = "CqrsHandlerRegistry.g.cs";
/// <inheritdoc />
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var generationEnabled = context.CompilationProvider
.Select(static (compilation, _) => HasRequiredTypes(compilation));
// Restrict semantic analysis to type declarations that can actually contribute implemented interfaces.
var handlerCandidates = context.SyntaxProvider.CreateSyntaxProvider(
static (node, _) => IsHandlerCandidate(node),
static (syntaxContext, _) => TransformHandlerCandidate(syntaxContext))
.Where(static candidate => candidate is not null)
.Collect();
context.RegisterSourceOutput(
generationEnabled.Combine(handlerCandidates),
static (productionContext, pair) => Execute(productionContext, pair.Left, pair.Right));
}
private static bool HasRequiredTypes(Compilation compilation)
{
return compilation.GetTypeByMetadataName(IRequestHandlerMetadataName) is not null &&
compilation.GetTypeByMetadataName(INotificationHandlerMetadataName) is not null &&
compilation.GetTypeByMetadataName(IStreamRequestHandlerMetadataName) is not null &&
compilation.GetTypeByMetadataName(ICqrsHandlerRegistryMetadataName) is not null &&
compilation.GetTypeByMetadataName(CqrsHandlerRegistryAttributeMetadataName) is not null &&
compilation.GetTypeByMetadataName(ILoggerMetadataName) is not null &&
compilation.GetTypeByMetadataName(IServiceCollectionMetadataName) is not null;
}
private static bool IsHandlerCandidate(SyntaxNode node)
{
return node is TypeDeclarationSyntax
{
BaseList.Types.Count: > 0
};
}
private static HandlerCandidateAnalysis? TransformHandlerCandidate(GeneratorSyntaxContext context)
{
if (context.Node is not TypeDeclarationSyntax typeDeclaration)
return null;
if (context.SemanticModel.GetDeclaredSymbol(typeDeclaration) is not INamedTypeSymbol type)
return null;
if (!IsConcreteHandlerType(type))
return null;
var handlerInterfaces = type.AllInterfaces
.Where(IsSupportedHandlerInterface)
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
.ToImmutableArray();
if (handlerInterfaces.IsDefaultOrEmpty)
return null;
var implementationTypeDisplayName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
if (!CanReferenceFromGeneratedRegistry(type) ||
handlerInterfaces.Any(interfaceType => !CanReferenceFromGeneratedRegistry(interfaceType)))
{
return new HandlerCandidateAnalysis(
implementationTypeDisplayName,
ImmutableArray<HandlerRegistrationSpec>.Empty,
true);
}
var implementationLogName = GetLogDisplayName(type);
var registrations = ImmutableArray.CreateBuilder<HandlerRegistrationSpec>(handlerInterfaces.Length);
foreach (var handlerInterface in handlerInterfaces)
{
registrations.Add(new HandlerRegistrationSpec(
handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
implementationTypeDisplayName,
GetLogDisplayName(handlerInterface),
implementationLogName));
}
return new HandlerCandidateAnalysis(
implementationTypeDisplayName,
registrations.MoveToImmutable(),
false);
}
private static void Execute(SourceProductionContext context, bool generationEnabled,
ImmutableArray<HandlerCandidateAnalysis?> candidates)
{
if (!generationEnabled)
return;
var registrations = CollectRegistrations(candidates, out var hasUnsupportedConcreteHandler);
// If the assembly contains handlers that generated code cannot legally reference
// (for example private nested handlers), keep the runtime on the reflection path
// so registration behavior remains complete instead of silently dropping handlers.
if (hasUnsupportedConcreteHandler || registrations.Count == 0)
return;
context.AddSource(HintName, GenerateSource(registrations));
}
private static List<HandlerRegistrationSpec> CollectRegistrations(
ImmutableArray<HandlerCandidateAnalysis?> candidates,
out bool hasUnsupportedConcreteHandler)
{
var registrations = new List<HandlerRegistrationSpec>();
hasUnsupportedConcreteHandler = false;
// Partial declarations surface the same symbol through multiple syntax nodes.
// Collapse them by implementation type so generated registrations stay stable and duplicate-free.
var uniqueCandidates = new Dictionary<string, HandlerCandidateAnalysis>(StringComparer.Ordinal);
foreach (var candidate in candidates)
{
if (candidate is null)
continue;
if (candidate.Value.HasUnsupportedConcreteHandler)
{
hasUnsupportedConcreteHandler = true;
return [];
}
uniqueCandidates[candidate.Value.ImplementationTypeDisplayName] = candidate.Value;
}
foreach (var candidate in uniqueCandidates.Values)
{
registrations.AddRange(candidate.Registrations);
}
registrations.Sort(static (left, right) =>
{
var implementationComparison = StringComparer.Ordinal.Compare(
left.ImplementationLogName,
right.ImplementationLogName);
return implementationComparison != 0
? implementationComparison
: StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName);
});
return registrations;
}
private static bool IsConcreteHandlerType(INamedTypeSymbol type)
{
return type.TypeKind is TypeKind.Class or TypeKind.Struct &&
!type.IsAbstract &&
!ContainsGenericParameters(type);
}
private static bool ContainsGenericParameters(INamedTypeSymbol type)
{
for (var current = type; current is not null; current = current.ContainingType)
{
if (current.TypeParameters.Length > 0)
return true;
}
return false;
}
private static bool IsSupportedHandlerInterface(INamedTypeSymbol interfaceType)
{
if (!interfaceType.IsGenericType)
return false;
var definitionMetadataName = GetFullyQualifiedMetadataName(interfaceType.OriginalDefinition);
return string.Equals(definitionMetadataName, IRequestHandlerMetadataName, StringComparison.Ordinal) ||
string.Equals(definitionMetadataName, INotificationHandlerMetadataName, StringComparison.Ordinal) ||
string.Equals(definitionMetadataName, IStreamRequestHandlerMetadataName, StringComparison.Ordinal);
}
private static bool CanReferenceFromGeneratedRegistry(ITypeSymbol type)
{
switch (type)
{
case IArrayTypeSymbol arrayType:
return CanReferenceFromGeneratedRegistry(arrayType.ElementType);
case INamedTypeSymbol namedType:
if (!IsTypeChainAccessible(namedType))
return false;
return namedType.TypeArguments.All(CanReferenceFromGeneratedRegistry);
case IPointerTypeSymbol pointerType:
return CanReferenceFromGeneratedRegistry(pointerType.PointedAtType);
case ITypeParameterSymbol:
return false;
default:
return true;
}
}
private static bool IsTypeChainAccessible(INamedTypeSymbol type)
{
for (var current = type; current is not null; current = current.ContainingType)
{
if (!IsSymbolAccessible(current))
return false;
}
return true;
}
private static bool IsSymbolAccessible(ISymbol symbol)
{
return symbol.DeclaredAccessibility is Accessibility.Public or Accessibility.Internal
or Accessibility.ProtectedOrInternal;
}
private static string GetFullyQualifiedMetadataName(INamedTypeSymbol type)
{
var nestedTypes = new Stack<string>();
for (var current = type; current is not null; current = current.ContainingType)
{
nestedTypes.Push(current.MetadataName);
}
var builder = new StringBuilder();
if (!type.ContainingNamespace.IsGlobalNamespace)
{
builder.Append(type.ContainingNamespace.ToDisplayString());
builder.Append('.');
}
while (nestedTypes.Count > 0)
{
builder.Append(nestedTypes.Pop());
if (nestedTypes.Count > 0)
builder.Append('.');
}
return builder.ToString();
}
private static string GetTypeSortKey(ITypeSymbol type)
{
return type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
}
private static string GetLogDisplayName(ITypeSymbol type)
{
return GetTypeSortKey(type).Replace("global::", string.Empty);
}
private static string GenerateSource(IReadOnlyList<HandlerRegistrationSpec> registrations)
{
var builder = new StringBuilder();
builder.AppendLine("// <auto-generated />");
builder.AppendLine("#nullable enable");
builder.AppendLine();
builder.Append("[assembly: global::");
builder.Append(CqrsNamespace);
builder.Append(".CqrsHandlerRegistryAttribute(typeof(global::");
builder.Append(GeneratedNamespace);
builder.Append('.');
builder.Append(GeneratedTypeName);
builder.AppendLine("))]");
builder.AppendLine();
builder.Append("namespace ");
builder.Append(GeneratedNamespace);
builder.AppendLine(";");
builder.AppendLine();
builder.Append("internal sealed class ");
builder.Append(GeneratedTypeName);
builder.Append(" : global::");
builder.Append(CqrsNamespace);
builder.AppendLine(".ICqrsHandlerRegistry");
builder.AppendLine("{");
builder.Append(
" public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::");
builder.Append(LoggingNamespace);
builder.AppendLine(".ILogger logger)");
builder.AppendLine(" {");
builder.AppendLine(" if (services is null)");
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(services));");
builder.AppendLine(" if (logger is null)");
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(logger));");
builder.AppendLine();
foreach (var registration in registrations)
{
builder.AppendLine(
" global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient(");
builder.AppendLine(" services,");
builder.Append(" typeof(");
builder.Append(registration.HandlerInterfaceDisplayName);
builder.AppendLine("),");
builder.Append(" typeof(");
builder.Append(registration.ImplementationTypeDisplayName);
builder.AppendLine("));");
builder.Append(" logger.Debug(\"Registered CQRS handler ");
builder.Append(EscapeStringLiteral(registration.ImplementationLogName));
builder.Append(" as ");
builder.Append(EscapeStringLiteral(registration.HandlerInterfaceLogName));
builder.AppendLine(".\");");
}
builder.AppendLine(" }");
builder.AppendLine("}");
return builder.ToString();
}
private static string EscapeStringLiteral(string value)
{
return value.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("\n", "\\n")
.Replace("\r", "\\r");
}
private readonly record struct HandlerRegistrationSpec(
string HandlerInterfaceDisplayName,
string ImplementationTypeDisplayName,
string HandlerInterfaceLogName,
string ImplementationLogName);
private readonly struct HandlerCandidateAnalysis : IEquatable<HandlerCandidateAnalysis>
{
public HandlerCandidateAnalysis(
string implementationTypeDisplayName,
ImmutableArray<HandlerRegistrationSpec> registrations,
bool hasUnsupportedConcreteHandler)
{
ImplementationTypeDisplayName = implementationTypeDisplayName;
Registrations = registrations;
HasUnsupportedConcreteHandler = hasUnsupportedConcreteHandler;
}
public string ImplementationTypeDisplayName { get; }
public ImmutableArray<HandlerRegistrationSpec> Registrations { get; }
public bool HasUnsupportedConcreteHandler { get; }
public bool Equals(HandlerCandidateAnalysis other)
{
if (!string.Equals(ImplementationTypeDisplayName, other.ImplementationTypeDisplayName,
StringComparison.Ordinal) ||
HasUnsupportedConcreteHandler != other.HasUnsupportedConcreteHandler ||
Registrations.Length != other.Registrations.Length)
{
return false;
}
for (var index = 0; index < Registrations.Length; index++)
{
if (!Registrations[index].Equals(other.Registrations[index]))
return false;
}
return true;
}
public override bool Equals(object? obj)
{
return obj is HandlerCandidateAnalysis other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
var hashCode = StringComparer.Ordinal.GetHashCode(ImplementationTypeDisplayName);
hashCode = (hashCode * 397) ^ HasUnsupportedConcreteHandler.GetHashCode();
foreach (var registration in Registrations)
{
hashCode = (hashCode * 397) ^ registration.GetHashCode();
}
return hashCode;
}
}
}
}

View File

@ -1,21 +1,21 @@
---
title: CQRS 与 Mediator
description: CQRS 模式通过 Mediator 实现命令查询职责分离,提供清晰的业务逻辑组织方式
title: CQRS
description: GFramework 内建 CQRS runtime用统一请求分发、通知发布和流式处理组织业务逻辑
---
# CQRS 与 Mediator
# CQRS
## 概述
CQRSCommand Query Responsibility Segregation命令查询职责分离是一种架构模式将数据的读取Query和修改Command操作分离。GFramework
通过集成 Mediator 库实现了 CQRS 模式,提供了类型安全、解耦的业务逻辑处理方式。
当前内建自有 CQRS runtime通过统一的请求分发器、通知发布和流式请求管道提供类型安全、解耦的业务逻辑处理方式。
通过 CQRS你可以将复杂的业务逻辑拆分为独立的命令和查询处理器每个处理器只负责单一职责使代码更易于测试和维护。
**主要特性**
- 命令查询职责分离
- 基于 Mediator 模式的解耦设计
- 内建请求分发与解耦设计
- 支持管道行为Behaviors
- 异步处理支持
- 与架构系统深度集成
@ -72,7 +72,6 @@ public class GetPlayerQuery : QueryBase<GetPlayerInput, PlayerData>
```csharp
using GFramework.Core.CQRS.Command;
using Mediator;
// 命令处理器
public class CreatePlayerCommandHandler : AbstractCommandHandler<CreatePlayerCommand, int>
@ -92,19 +91,19 @@ public class CreatePlayerCommandHandler : AbstractCommandHandler<CreatePlayerCom
}
```
### Mediator中介者
### Dispatcher请求分发器
Mediator 负责将命令/查询路由到对应的处理器:
架构上下文会负责将命令、查询和通知路由到对应的处理器:
```csharp
// 通过 Mediator 发送命令
// 通过架构上下文发送命令
var command = new CreatePlayerCommand(new CreatePlayerInput
{
Name = "Player1",
Level = 1
});
var playerId = await mediator.Send(command);
var playerId = await this.SendAsync(command);
```
## 基本用法
@ -148,15 +147,13 @@ public class SaveGameCommandHandler : AbstractCommandHandler<SaveGameCommand>
// 4. 发送命令
public async Task SaveGame()
{
var mediator = this.GetService<IMediator>();
var command = new SaveGameCommand(new SaveGameInput
{
SlotId = 1,
Data = currentGameData
});
await mediator.Send(command);
await this.SendAsync(command);
}
```
@ -195,37 +192,63 @@ public class GetHighScoresQueryHandler : AbstractQueryHandler<GetHighScoresQuery
// 4. 发送查询
public async Task<List<ScoreData>> GetHighScores()
{
var mediator = this.GetService<IMediator>();
var query = new GetHighScoresQuery(new GetHighScoresInput
{
Count = 10
});
var scores = await mediator.Send(query);
var scores = await this.SendQueryAsync(query);
return scores;
}
```
### 注册处理器
在架构中注册 Mediator 和处理器:
在架构中注册 CQRS 行为;默认会自动接入当前架构所在程序集和 `GFramework.Core` 程序集中的处理器:
```csharp
public class GameArchitecture : Architecture
{
protected override void Init()
protected override void OnInitialize()
{
// 注册通用开放泛型行为
RegisterMediatorBehavior<LoggingBehavior<,>>();
RegisterMediatorBehavior<PerformanceBehavior<,>>();
RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
RegisterCqrsPipelineBehavior<PerformanceBehavior<,>>();
// 处理器会自动通过依赖注入注册
// 默认只自动扫描当前架构程序集和 GFramework.Core 程序集中的处理器
}
}
```
`RegisterMediatorBehavior<TBehavior>()` 同时支持两种形式:
当前版本会优先使用源码生成的程序集级 handler registry 来注册“当前业务程序集”里的处理器;
如果该程序集没有生成注册器,或者包含生成代码无法合法引用的处理器类型,则会自动回退到运行时反射扫描。
`GFramework.Core` 等未挂接该生成器的程序集仍会继续走反射扫描。
如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是只依赖默认接入范围:
```csharp
public class GameArchitecture : Architecture
{
protected override void OnInitialize()
{
RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
RegisterCqrsHandlersFromAssemblies(
[
typeof(InventoryCqrsMarker).Assembly,
typeof(BattleCqrsMarker).Assembly
]);
}
}
```
`RegisterCqrsHandlersFromAssembly(...)` / `RegisterCqrsHandlersFromAssemblies(...)` 会复用与默认启动路径相同的注册逻辑:
优先使用程序集级生成注册器,失败时自动回退到反射扫描;如果同一程序集已经由默认路径或其他模块接入,框架会自动去重,避免重复注册
handler。
`RegisterCqrsPipelineBehavior<TBehavior>()` 是推荐入口;旧的 `RegisterMediatorBehavior<TBehavior>()`
仅作为兼容名称保留,当前已标记为 `Obsolete` 并从 IntelliSense 主路径隐藏,计划在未来 major 版本中移除。
`ContextAwareMediator*Extensions``MediatorCoroutineExtensions` 也遵循同样的弃用节奏。当前接口支持两种形式:
- 开放泛型行为,例如 `LoggingBehavior<,>`,用于匹配所有请求
- 封闭行为类型,例如某个只服务于单一请求的 `SpecialBehavior`
@ -326,7 +349,7 @@ var notification = new PlayerLevelUpNotification(new PlayerLevelUpInput
NewLevel = 10
});
await mediator.Publish(notification);
await this.PublishAsync(notification);
```
### Pipeline Behaviors管道行为
@ -334,16 +357,16 @@ await mediator.Publish(notification);
Behaviors 可以在处理器执行前后添加横切关注点:
```csharp
using Mediator;
using GFramework.Core.Abstractions.Cqrs;
// 日志行为
public class LoggingBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
where TMessage : IMessage
where TMessage : IRequest<TResponse>
{
public async ValueTask<TResponse> Handle(
TMessage message,
CancellationToken cancellationToken,
MessageHandlerDelegate<TMessage, TResponse> next)
MessageHandlerDelegate<TMessage, TResponse> next,
CancellationToken cancellationToken)
{
var messageName = message.GetType().Name;
Console.WriteLine($"[开始] {messageName}");
@ -358,12 +381,12 @@ public class LoggingBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage,
// 性能监控行为
public class PerformanceBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
where TMessage : IMessage
where TMessage : IRequest<TResponse>
{
public async ValueTask<TResponse> Handle(
TMessage message,
CancellationToken cancellationToken,
MessageHandlerDelegate<TMessage, TResponse> next)
MessageHandlerDelegate<TMessage, TResponse> next,
CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
@ -382,20 +405,20 @@ public class PerformanceBehavior<TMessage, TResponse> : IPipelineBehavior<TMessa
}
// 注册行为
RegisterMediatorBehavior<LoggingBehavior<,>>();
RegisterMediatorBehavior<PerformanceBehavior<,>>();
RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
RegisterCqrsPipelineBehavior<PerformanceBehavior<,>>();
```
### 验证行为
```csharp
public class ValidationBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
where TMessage : IMessage
where TMessage : IRequest<TResponse>
{
public async ValueTask<TResponse> Handle(
TMessage message,
CancellationToken cancellationToken,
MessageHandlerDelegate<TMessage, TResponse> next)
MessageHandlerDelegate<TMessage, TResponse> next,
CancellationToken cancellationToken)
{
// 验证输入
if (message is IValidatable validatable)
@ -441,7 +464,7 @@ public class GetAllPlayersStreamQueryHandler : AbstractStreamQueryHandler<GetAll
// 使用流式查询
var query = new GetAllPlayersStreamQuery();
var stream = await mediator.CreateStream(query);
var stream = this.CreateStream(query);
await foreach (var player in stream)
{
@ -476,8 +499,8 @@ await foreach (var player in stream)
4. **使用 Behaviors 处理横切关注点**:日志、性能、验证等
```csharp
RegisterMediatorBehavior<LoggingBehavior<,>>();
RegisterMediatorBehavior<ValidationBehavior<,>>();
RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
RegisterCqrsPipelineBehavior<ValidationBehavior<,>>();
```
5. **保持处理器简单**:一个处理器只做一件事
@ -530,12 +553,12 @@ CalculateDamageRequest
**解答**
- **Notification**:通过 Mediator 发送,处理器在同一请求上下文中执行
- **Notification**:通过内建 CQRS runtime 发送,处理器在同一请求上下文中执行
- **Event**:通过 EventBus 发送,监听器异步执行
```csharp
// Notification: 同步处理
await mediator.Publish(notification); // 等待所有处理器完成
await this.PublishAsync(notification); // 等待所有处理器完成
// Event: 异步处理
this.SendEvent(event); // 立即返回,监听器异步执行
@ -569,15 +592,13 @@ public override async ValueTask<Result> Handle(...)
### 问题:处理器可以调用其他处理器吗?
**解答**
可以,通过 Mediator 发送新的命令或查询:
可以,通过架构上下文继续发送新的命令或查询:
```csharp
public override async ValueTask<Unit> Handle(...)
{
var mediator = this.GetService<IMediator>();
// 调用其他命令
await mediator.Send(new AnotherCommand(...));
await this.SendAsync(new AnotherCommand(...));
return Unit.Value;
}

View File

@ -391,17 +391,17 @@ public class PlayerController : IController
#### 5. ArchitectureModules (模块管理器)
**职责**: 管理架构模块和中介行为
**职责**: 管理架构模块和 CQRS 管道行为
**核心功能**:
- 模块安装 (IArchitectureModule)
- 中介行为注册 (Mediator Behaviors)
- CQRS 管道行为注册(推荐 API 为 `RegisterCqrsPipelineBehavior`
**关键方法**:
- `InstallModule()` - 安装模块
- `RegisterMediatorBehavior<T>()` - 注册中介行为
- `RegisterCqrsPipelineBehavior<T>()` - 注册 CQRS 管道行为
#### 设计优势
@ -672,4 +672,3 @@ public interface IController :
- 添加 `PhaseChanged` 事件,支持阶段监听
**向后兼容**: 所有公共 API 保持不变,现有代码无需修改。