From 295496e90f04c6eae484c20e794bbcd01ca868f4 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 09:49:26 +0800 Subject: [PATCH 1/3] =?UTF-8?q?docs(core):=20=E6=B7=BB=E5=8A=A0=20CQRS=20?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=B9=B6=E5=AE=9E=E7=8E=B0=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加完整的 CQRS 中文文档,涵盖命令、查询、处理器、管道行为等核心概念 - 实现 ArchitectureModules 类用于管理架构模块安装和 CQRS 行为注册 - 重构 Architecture 类为协调器模式,委托给专门的管理器组件 - 添加 RegisterCqrsPipelineBehavior 方法替代旧的 RegisterMediatorBehavior - 标记旧的扩展方法为 Obsolete 并提供新的兼容性别名 - 实现模块化架构组件注册和生命周期管理功能 --- CLAUDE.md | 3 +- .../Architectures/IArchitecture.cs | 6 +- .../Ioc/IIocContainer.cs | 8 +- .../MediatorCompatibilityDeprecationTests.cs | 99 +++++++++++++++++++ GFramework.Core/Architectures/Architecture.cs | 6 +- .../Architectures/ArchitectureModules.cs | 6 +- .../Extensions/MediatorCoroutineExtensions.cs | 6 +- .../ContextAwareMediatorCommandExtensions.cs | 6 +- .../ContextAwareMediatorExtensions.cs | 6 +- .../ContextAwareMediatorQueryExtensions.cs | 6 +- GFramework.Core/Ioc/MicrosoftDiContainer.cs | 6 +- docs/zh-CN/core/cqrs.md | 3 +- 12 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs diff --git a/CLAUDE.md b/CLAUDE.md index 1962098f..e2e656c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,7 +74,8 @@ Architecture 负责统一生命周期编排,核心阶段包括: ### CQRS 命令与查询分离,支持同步与异步执行。当前版本内建自有 CQRS runtime、行为管道和 handler 自动注册;公开 API 里仍保留少量历史 -`Mediator` 命名以兼容旧调用点。 +`Mediator` 命名以兼容旧调用点,但这些别名已进入正式弃用周期:新代码应使用 `Cqrs` 命名入口,旧别名会继续兼容一段时间并计划在未来 +major 版本中移除。 ### EventBus diff --git a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs index b344717d..16386d5a 100644 --- a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs +++ b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using GFramework.Core.Abstractions.Lifecycle; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Systems; @@ -84,11 +85,14 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia /// /// 注册 CQRS 请求管道行为。 /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。 /// 既支持实现 IPipelineBehavior<,> 的开放泛型行为类型, /// 也支持绑定到单一请求/响应对的封闭行为类型。 /// /// 行为类型,必须是引用类型 - [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete( + "Use RegisterCqrsPipelineBehavior() instead. This compatibility alias will be removed in a future major version.")] void RegisterMediatorBehavior() where TBehavior : class; diff --git a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs index c56b71fa..aead5a9d 100644 --- a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs +++ b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs @@ -1,4 +1,5 @@ -using GFramework.Core.Abstractions.Rule; +using System.ComponentModel; +using GFramework.Core.Abstractions.Rule; using GFramework.Core.Abstractions.Systems; using Microsoft.Extensions.DependencyInjection; @@ -99,9 +100,12 @@ public interface IIocContainer : IContextAware /// /// 注册 CQRS 请求管道行为。 /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。 /// /// 行为类型,必须是引用类型 - [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete( + "Use RegisterCqrsPipelineBehavior() instead. This compatibility alias will be removed in a future major version.")] void RegisterMediatorBehavior() where TBehavior : class; diff --git a/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs b/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs new file mode 100644 index 00000000..2e3bdbca --- /dev/null +++ b/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs @@ -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; + +/// +/// 锁定历史 Mediator 兼容入口的正式弃用策略。 +/// 这些测试确保旧 API 不仅保留行为兼容,还会通过编译期提示和 IntelliSense 隐藏引导调用方迁移到新的 CQRS 命名。 +/// +[TestFixture] +public class MediatorCompatibilityDeprecationTests +{ + /// + /// 验证公开兼容方法仍可用,但已被显式标记为未来移除的旧别名。 + /// + [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)); + } + + /// + /// 验证历史扩展类型会把迁移目标写入弃用说明,并从 IntelliSense 主路径隐藏。 + /// + [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."); + } + + /// + /// 断言方法级兼容 API 具备统一的弃用元数据。 + /// + /// 声明该方法的类型。 + /// 方法名称。 + 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(); + var editorBrowsableAttribute = method.GetCustomAttribute(); + + Assert.Multiple(() => + { + Assert.That(obsoleteAttribute, Is.Not.Null); + Assert.That( + obsoleteAttribute!.Message, + Does.Contain("Use RegisterCqrsPipelineBehavior() 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)); + }); + } + + /// + /// 断言类型级兼容扩展具备统一的弃用元数据。 + /// + /// 兼容扩展类型。 + /// 期望的迁移提示。 + private static void AssertLegacyType(Type type, string expectedReplacementHint) + { + var obsoleteAttribute = type.GetCustomAttribute(); + var editorBrowsableAttribute = type.GetCustomAttribute(); + + 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)); + }); + } +} diff --git a/GFramework.Core/Architectures/Architecture.cs b/GFramework.Core/Architectures/Architecture.cs index ec571ee8..0395347d 100644 --- a/GFramework.Core/Architectures/Architecture.cs +++ b/GFramework.Core/Architectures/Architecture.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Enums; using GFramework.Core.Abstractions.Environment; @@ -157,9 +158,12 @@ public abstract class Architecture : IArchitecture /// /// 注册 CQRS 请求管道行为。 /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。 /// /// 行为类型,必须是引用类型 - [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete( + "Use RegisterCqrsPipelineBehavior() instead. This compatibility alias will be removed in a future major version.")] public void RegisterMediatorBehavior() where TBehavior : class { RegisterCqrsPipelineBehavior(); diff --git a/GFramework.Core/Architectures/ArchitectureModules.cs b/GFramework.Core/Architectures/ArchitectureModules.cs index 7563e276..a0c1b149 100644 --- a/GFramework.Core/Architectures/ArchitectureModules.cs +++ b/GFramework.Core/Architectures/ArchitectureModules.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Logging; @@ -26,9 +27,12 @@ internal sealed class ArchitectureModules( /// /// 注册 CQRS 请求管道行为。 /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。 /// /// 行为类型,必须是引用类型 - [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete( + "Use RegisterCqrsPipelineBehavior() instead. This compatibility alias will be removed in a future major version.")] public void RegisterMediatorBehavior() where TBehavior : class { RegisterCqrsPipelineBehavior(); diff --git a/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs index 91ed0238..f955f175 100644 --- a/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs +++ b/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs @@ -11,6 +11,7 @@ // 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; @@ -21,8 +22,11 @@ namespace GFramework.Core.Coroutine.Extensions; /// /// 提供 CQRS 命令与协程集成的扩展方法。 /// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// -[Obsolete("Use GFramework.Core.Cqrs.Extensions.CqrsCoroutineExtensions instead.")] +[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 { /// diff --git a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs index 9fc9311b..85f9776e 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Abstractions.Rule; using GFramework.Core.Cqrs.Extensions; @@ -7,8 +8,11 @@ namespace GFramework.Core.Extensions; /// /// 提供对 接口的 CQRS 命令扩展方法。 /// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// -[Obsolete("Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead.")] +[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 { /// diff --git a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs index 4b63e223..68b130ce 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; using GFramework.Core.Cqrs.Extensions; @@ -7,8 +8,11 @@ namespace GFramework.Core.Extensions; /// /// 提供对 接口的 CQRS 统一接口扩展方法。 /// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// -[Obsolete("Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsExtensions instead.")] +[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 { /// diff --git a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs index be2d70e1..d7fada4a 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Abstractions.Rule; using GFramework.Core.Cqrs.Extensions; @@ -7,8 +8,11 @@ namespace GFramework.Core.Extensions; /// /// 提供对 接口的 CQRS 查询扩展方法。 /// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// -[Obsolete("Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead.")] +[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 { /// diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 69b46720..3b0396c1 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using GFramework.Core.Abstractions.Bases; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; @@ -360,9 +361,12 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// /// 注册 CQRS 请求管道行为。 /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。 /// /// 行为类型,必须是引用类型 - [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete( + "Use RegisterCqrsPipelineBehavior() instead. This compatibility alias will be removed in a future major version.")] public void RegisterMediatorBehavior() where TBehavior : class { RegisterCqrsPipelineBehavior(); diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 3effaa86..a38fbdfa 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -223,7 +223,8 @@ public class GameArchitecture : Architecture 如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是依赖默认扫描。 `RegisterCqrsPipelineBehavior()` 是推荐入口;旧的 `RegisterMediatorBehavior()` -仅作为兼容名称保留。当前接口支持两种形式: +仅作为兼容名称保留,当前已标记为 `Obsolete` 并从 IntelliSense 主路径隐藏,计划在未来 major 版本中移除。 +`ContextAwareMediator*Extensions` 与 `MediatorCoroutineExtensions` 也遵循同样的弃用节奏。当前接口支持两种形式: - 开放泛型行为,例如 `LoggingBehavior<,>`,用于匹配所有请求 - 封闭行为类型,例如某个只服务于单一请求的 `SpecialBehavior` From fd6442374175803cebff9cbe37ad6f4dc49cfa6e Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:21:20 +0800 Subject: [PATCH 2/3] =?UTF-8?q?docs(core):=20=E6=B7=BB=E5=8A=A0=20CQRS=20?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=E6=A8=A1=E5=BC=8F=E5=AE=8C=E6=95=B4=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CQRS 核心概念介绍,包括命令、查询、处理器和分发器 - 添加基本用法示例,展示命令和查询的定义与发送流程 - 实现高级功能文档,涵盖请求、通知、管道行为和流式处理 - 提供最佳实践指南,明确命令查询分离和验证行为使用方式 - 增加常见问题解答,解释 Command/Query 区别和错误处理方案 - 新增 CQRS 处理器自动注册实现,支持源码生成和反射扫描 - 添加单元测试验证处理器注册顺序和容错行为 - 更新项目 AI 代理说明文档,完善模块依赖关系图 --- CLAUDE.md | 1 + .../Cqrs/CqrsHandlerRegistryAttribute.cs | 18 + .../Cqrs/ICqrsHandlerRegistry.cs | 20 ++ .../Cqrs/CqrsHandlerRegistrarTests.cs | 119 +++++++ .../Cqrs/Internal/CqrsHandlerRegistrar.cs | 77 ++++- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 195 +++++++++++ .../Cqrs/CqrsHandlerRegistryGenerator.cs | 311 ++++++++++++++++++ docs/zh-CN/core/cqrs.md | 8 +- 8 files changed, 746 insertions(+), 3 deletions(-) create mode 100644 GFramework.Core.Abstractions/Cqrs/CqrsHandlerRegistryAttribute.cs create mode 100644 GFramework.Core.Abstractions/Cqrs/ICqrsHandlerRegistry.cs create mode 100644 GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs create mode 100644 GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs diff --git a/CLAUDE.md b/CLAUDE.md index e2e656c3..4ea95597 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,6 +105,7 @@ major 版本中移除。 - `PriorityGenerator` (`[Priority]`): 生成优先级比较相关实现。 - `EnumExtensionsGenerator` (`[GenerateEnumExtensions]`): 生成枚举扩展能力。 - `ContextAwareGenerator` (`[ContextAware]`): 自动实现 `IContextAware` 相关样板逻辑。 +- `CqrsHandlerRegistryGenerator`: 为消费端程序集生成 CQRS handler 注册器,运行时优先使用生成产物,无法覆盖时回退到反射扫描。 这些生成器的目标是减少重复代码,同时保持框架层 API 的一致性与可维护性。 diff --git a/GFramework.Core.Abstractions/Cqrs/CqrsHandlerRegistryAttribute.cs b/GFramework.Core.Abstractions/Cqrs/CqrsHandlerRegistryAttribute.cs new file mode 100644 index 00000000..e073fe5c --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/CqrsHandlerRegistryAttribute.cs @@ -0,0 +1,18 @@ +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 声明程序集内可供运行时直接调用的 CQRS 处理器注册器类型。 +/// +/// +/// 该特性通常由源码生成器自动添加到消费端程序集。 +/// 运行时读取到该特性后,会优先实例化对应的 , +/// 以常量时间获取处理器注册映射,而不是遍历程序集中的全部类型。 +/// +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +public sealed class CqrsHandlerRegistryAttribute(Type registryType) : Attribute +{ + /// + /// 获取承载 CQRS 处理器注册逻辑的注册器类型。 + /// + public Type RegistryType { get; } = registryType ?? throw new ArgumentNullException(nameof(registryType)); +} diff --git a/GFramework.Core.Abstractions/Cqrs/ICqrsHandlerRegistry.cs b/GFramework.Core.Abstractions/Cqrs/ICqrsHandlerRegistry.cs new file mode 100644 index 00000000..91af6be7 --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/ICqrsHandlerRegistry.cs @@ -0,0 +1,20 @@ +using GFramework.Core.Abstractions.Logging; + +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 定义由源码生成器产出的 CQRS 处理器注册器契约。 +/// +/// +/// 运行时会优先调用实现该接口的程序集级注册器,以避免在冷启动阶段对整个程序集执行反射扫描。 +/// 当目标程序集没有生成注册器,或生成注册器因兼容性原因不可用时,运行时仍会回退到反射扫描路径。 +/// +public interface ICqrsHandlerRegistry +{ + /// + /// 将当前程序集中的 CQRS 处理器映射注册到目标服务集合。 + /// + /// 承载处理器映射的服务集合。 + /// 用于记录注册诊断信息的日志器。 + void Register(IServiceCollection services, ILogger logger); +} diff --git a/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index 28b1fb32..7748227c 100644 --- a/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -115,6 +115,80 @@ internal sealed class CqrsHandlerRegistrarTests LoggerFactoryResolver.Provider = originalProvider; } } + + /// + /// 验证当程序集提供源码生成的注册器时,运行时会优先使用该注册器而不是反射扫描类型列表。 + /// + [Test] + public void RegisterHandlers_Should_Use_Generated_Registry_When_Available() + { + var generatedAssembly = new Mock(); + 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>(); + + Assert.That( + handlers.Select(static handler => handler.GetType()), + Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)])); + } + + /// + /// 验证当生成注册器元数据损坏时,运行时会记录告警并回退到反射扫描路径。 + /// + [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(); + 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>(); + 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; + } + } } /// @@ -219,3 +293,48 @@ internal sealed class CapturingLoggerFactoryProvider : ILoggerFactoryProvider return logger; } } + +/// +/// 用于验证生成注册器路径的通知消息。 +/// +internal sealed record GeneratedRegistryNotification : INotification; + +/// +/// 由模拟的源码生成注册器显式注册的通知处理器。 +/// +internal sealed class GeneratedRegistryNotificationHandler : INotificationHandler +{ + /// + /// 处理生成注册器测试中的通知。 + /// + /// 通知实例。 + /// 取消令牌。 + /// 已完成任务。 + public ValueTask Handle(GeneratedRegistryNotification notification, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } +} + +/// +/// 模拟源码生成器为某个程序集生成的 CQRS 处理器注册器。 +/// +internal sealed class GeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry +{ + /// + /// 将测试通知处理器注册到目标服务集合。 + /// + /// 承载处理器映射的服务集合。 + /// 用于记录注册诊断的日志器。 + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(INotificationHandler), + typeof(GeneratedRegistryNotificationHandler)); + logger.Debug( + $"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler).FullName}."); + } +} diff --git a/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs index 957c0981..189ae8f0 100644 --- a/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -7,7 +7,8 @@ namespace GFramework.Core.Cqrs.Internal; /// /// 在架构初始化期间扫描并注册 CQRS 处理器。 -/// 首批实现采用运行时反射扫描,优先满足“无需额外注册步骤即可工作”的迁移目标。 +/// 运行时会优先尝试使用源码生成的程序集级注册器,以减少冷启动阶段的反射开销; +/// 当目标程序集没有生成注册器,或注册器不可用时,再回退到运行时反射扫描。 /// 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); } } + /// + /// 优先使用程序集级源码生成注册器完成 CQRS 映射注册。 + /// + /// 目标服务集合。 + /// 当前要处理的程序集。 + /// 日志记录器。 + /// 当成功使用生成注册器时返回 ;否则返回 + private static bool TryRegisterGeneratedHandlers(IServiceCollection services, Assembly assembly, ILogger logger) + { + var assemblyName = GetAssemblySortKey(assembly); + + try + { + var registryTypes = assembly + .GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), inherit: false) + .OfType() + .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(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; + } + } + /// /// 注册单个程序集里的所有 CQRS 处理器映射。 /// diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs new file mode 100644 index 00000000..b04d8165 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -0,0 +1,195 @@ +using GFramework.SourceGenerators.Cqrs; +using GFramework.SourceGenerators.Tests.Core; + +namespace GFramework.SourceGenerators.Tests.Cqrs; + +/// +/// 验证 CQRS 处理器注册生成器的输出与回退边界。 +/// +[TestFixture] +public class CqrsHandlerRegistryGeneratorTests +{ + /// + /// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 + /// + [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 { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + + 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; + public sealed record DomainEvent() : INotification; + public sealed record NumberStream() : IStreamRequest; + + public sealed class ZetaNotificationHandler : INotificationHandler { } + public sealed class AlphaQueryHandler : IRequestHandler { } + public sealed class StreamHandler : IStreamRequestHandler { } + } + """; + + const string expected = """ + // + #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), + typeof(global::TestApp.AlphaQueryHandler)); + logger.Debug("Registered CQRS handler TestApp.AlphaQueryHandler as GFramework.Core.Abstractions.Cqrs.IRequestHandler."); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + typeof(global::GFramework.Core.Abstractions.Cqrs.IStreamRequestHandler), + typeof(global::TestApp.StreamHandler)); + logger.Debug("Registered CQRS handler TestApp.StreamHandler as GFramework.Core.Abstractions.Cqrs.IStreamRequestHandler."); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + typeof(global::GFramework.Core.Abstractions.Cqrs.INotificationHandler), + typeof(global::TestApp.ZetaNotificationHandler)); + logger.Debug("Registered CQRS handler TestApp.ZetaNotificationHandler as GFramework.Core.Abstractions.Cqrs.INotificationHandler."); + } + } + + """; + + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", expected)); + } + + /// + /// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器会放弃产出并让运行时回退到反射扫描。 + /// + [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 { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + + 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; + + public sealed class Container + { + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenHandler : IRequestHandler { } + } + + public sealed class VisibleHandler : IRequestHandler { } + } + """; + + var test = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { source } + }, + DisabledDiagnostics = { "GF_Common_Trace_001" } + }; + + await test.RunAsync(); + } +} diff --git a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs new file mode 100644 index 00000000..59db7c77 --- /dev/null +++ b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -0,0 +1,311 @@ +using GFramework.SourceGenerators.Common.Constants; + +namespace GFramework.SourceGenerators.Cqrs; + +/// +/// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。 +/// +[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"; + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterSourceOutput( + context.CompilationProvider, + static (productionContext, compilation) => Execute(productionContext, compilation)); + } + + private static void Execute(SourceProductionContext context, Compilation compilation) + { + var requestHandlerType = compilation.GetTypeByMetadataName(IRequestHandlerMetadataName); + var notificationHandlerType = compilation.GetTypeByMetadataName(INotificationHandlerMetadataName); + var streamHandlerType = compilation.GetTypeByMetadataName(IStreamRequestHandlerMetadataName); + var registryInterfaceType = compilation.GetTypeByMetadataName(ICqrsHandlerRegistryMetadataName); + var registryAttributeType = compilation.GetTypeByMetadataName(CqrsHandlerRegistryAttributeMetadataName); + var loggerType = compilation.GetTypeByMetadataName(ILoggerMetadataName); + var serviceCollectionType = compilation.GetTypeByMetadataName(IServiceCollectionMetadataName); + + if (requestHandlerType is null || + notificationHandlerType is null || + streamHandlerType is null || + registryInterfaceType is null || + registryAttributeType is null || + loggerType is null || + serviceCollectionType is null) + { + return; + } + + var registrations = CollectRegistrations( + compilation.Assembly.GlobalNamespace, + requestHandlerType, + notificationHandlerType, + streamHandlerType, + 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 CollectRegistrations( + INamespaceSymbol rootNamespace, + INamedTypeSymbol requestHandlerType, + INamedTypeSymbol notificationHandlerType, + INamedTypeSymbol streamHandlerType, + out bool hasUnsupportedConcreteHandler) + { + var registrations = new List(); + hasUnsupportedConcreteHandler = false; + + foreach (var type in EnumerateTypes(rootNamespace)) + { + if (!IsConcreteHandlerType(type)) + continue; + + var handlerInterfaces = type.AllInterfaces + .Where(interfaceType => IsSupportedHandlerInterface( + interfaceType, + requestHandlerType, + notificationHandlerType, + streamHandlerType)) + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToList(); + + if (handlerInterfaces.Count == 0) + continue; + + if (!CanReferenceFromGeneratedRegistry(type) || + handlerInterfaces.Any(interfaceType => !CanReferenceFromGeneratedRegistry(interfaceType))) + { + hasUnsupportedConcreteHandler = true; + return []; + } + + var implementationTypeDisplayName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var implementationLogName = GetLogDisplayName(type); + + foreach (var handlerInterface in handlerInterfaces) + { + registrations.Add(new HandlerRegistrationSpec( + handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + implementationTypeDisplayName, + GetLogDisplayName(handlerInterface), + implementationLogName)); + } + } + + 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 IEnumerable EnumerateTypes(INamespaceSymbol namespaceSymbol) + { + foreach (var member in namespaceSymbol.GetMembers()) + { + switch (member) + { + case INamespaceSymbol childNamespace: + foreach (var type in EnumerateTypes(childNamespace)) + yield return type; + + break; + + case INamedTypeSymbol namedType: + foreach (var type in EnumerateTypes(namedType)) + yield return type; + + break; + } + } + } + + private static IEnumerable EnumerateTypes(INamedTypeSymbol typeSymbol) + { + yield return typeSymbol; + + foreach (var nestedType in typeSymbol.GetTypeMembers()) + { + foreach (var descendant in EnumerateTypes(nestedType)) + yield return descendant; + } + } + + 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, + INamedTypeSymbol requestHandlerType, + INamedTypeSymbol notificationHandlerType, + INamedTypeSymbol streamHandlerType) + { + if (!interfaceType.IsGenericType) + return false; + + var definition = interfaceType.OriginalDefinition; + return SymbolEqualityComparer.Default.Equals(definition, requestHandlerType) || + SymbolEqualityComparer.Default.Equals(definition, notificationHandlerType) || + SymbolEqualityComparer.Default.Equals(definition, streamHandlerType); + } + + 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 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 registrations) + { + var builder = new StringBuilder(); + builder.AppendLine("// "); + 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("\"", "\\\""); + } + + private readonly record struct HandlerRegistrationSpec( + string HandlerInterfaceDisplayName, + string ImplementationTypeDisplayName, + string HandlerInterfaceLogName, + string ImplementationLogName); +} diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index a38fbdfa..5c0a2203 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -204,7 +204,7 @@ public async Task> GetHighScores() ### 注册处理器 -在架构中注册 CQRS 行为;默认会自动扫描当前架构所在程序集和 `GFramework.Core` 程序集中的处理器: +在架构中注册 CQRS 行为;默认会自动接入当前架构所在程序集和 `GFramework.Core` 程序集中的处理器: ```csharp public class GameArchitecture : Architecture @@ -220,7 +220,11 @@ public class GameArchitecture : Architecture } ``` -如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是依赖默认扫描。 +当前版本会优先使用源码生成的程序集级 handler registry 来注册“当前业务程序集”里的处理器; +如果该程序集没有生成注册器,或者包含生成代码无法合法引用的处理器类型,则会自动回退到运行时反射扫描。 +`GFramework.Core` 等未挂接该生成器的程序集仍会继续走反射扫描。 + +如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是只依赖默认接入范围。 `RegisterCqrsPipelineBehavior()` 是推荐入口;旧的 `RegisterMediatorBehavior()` 仅作为兼容名称保留,当前已标记为 `Obsolete` 并从 IntelliSense 主路径隐藏,计划在未来 major 版本中移除。 From 7a6f966601d235abbd20fb2b8470409b6636ddec Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:12:36 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0=20CQRS=20?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=99=A8=E6=B3=A8=E5=86=8C=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 CqrsHandlerRegistryGenerator 源代码生成器 - 支持 IRequestHandler、INotificationHandler 和 IStreamRequestHandler 接口的处理器注册 - 生成程序集级别的 CQRS 处理器注册器以减少运行时反射开销 - 添加对请求、通知和流处理器的稳定顺序注册支持 - 实现对私有嵌套处理器的检测和回退机制 - 提供字符串字面量转义功能以避免生成代码中的语法错误 - 添加完整的单元测试验证生成器的功能和边界条件 --- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 20 ++ .../Cqrs/CqrsHandlerRegistryGenerator.cs | 279 ++++++++++++------ 2 files changed, 203 insertions(+), 96 deletions(-) diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index b04d8165..e2b7ffb1 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1,3 +1,4 @@ +using System.Reflection; using GFramework.SourceGenerators.Cqrs; using GFramework.SourceGenerators.Tests.Core; @@ -192,4 +193,23 @@ public class CqrsHandlerRegistryGeneratorTests await test.RunAsync(); } + + /// + /// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。 + /// + [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)); + } } diff --git a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 59db7c77..39c3aadb 100644 --- a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -24,38 +24,93 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator /// 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( - context.CompilationProvider, - static (productionContext, compilation) => Execute(productionContext, compilation)); + generationEnabled.Combine(handlerCandidates), + static (productionContext, pair) => Execute(productionContext, pair.Left, pair.Right)); } - private static void Execute(SourceProductionContext context, Compilation compilation) + private static bool HasRequiredTypes(Compilation compilation) { - var requestHandlerType = compilation.GetTypeByMetadataName(IRequestHandlerMetadataName); - var notificationHandlerType = compilation.GetTypeByMetadataName(INotificationHandlerMetadataName); - var streamHandlerType = compilation.GetTypeByMetadataName(IStreamRequestHandlerMetadataName); - var registryInterfaceType = compilation.GetTypeByMetadataName(ICqrsHandlerRegistryMetadataName); - var registryAttributeType = compilation.GetTypeByMetadataName(CqrsHandlerRegistryAttributeMetadataName); - var loggerType = compilation.GetTypeByMetadataName(ILoggerMetadataName); - var serviceCollectionType = compilation.GetTypeByMetadataName(IServiceCollectionMetadataName); + 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; + } - if (requestHandlerType is null || - notificationHandlerType is null || - streamHandlerType is null || - registryInterfaceType is null || - registryAttributeType is null || - loggerType is null || - serviceCollectionType is null) + private static bool IsHandlerCandidate(SyntaxNode node) + { + return node is TypeDeclarationSyntax { - return; + 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.Empty, + true); } - var registrations = CollectRegistrations( - compilation.Assembly.GlobalNamespace, - requestHandlerType, - notificationHandlerType, - streamHandlerType, - out var hasUnsupportedConcreteHandler); + var implementationLogName = GetLogDisplayName(type); + var registrations = ImmutableArray.CreateBuilder(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 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 @@ -67,50 +122,33 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator } private static List CollectRegistrations( - INamespaceSymbol rootNamespace, - INamedTypeSymbol requestHandlerType, - INamedTypeSymbol notificationHandlerType, - INamedTypeSymbol streamHandlerType, + ImmutableArray candidates, out bool hasUnsupportedConcreteHandler) { var registrations = new List(); hasUnsupportedConcreteHandler = false; - foreach (var type in EnumerateTypes(rootNamespace)) + // 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(StringComparer.Ordinal); + + foreach (var candidate in candidates) { - if (!IsConcreteHandlerType(type)) + if (candidate is null) continue; - var handlerInterfaces = type.AllInterfaces - .Where(interfaceType => IsSupportedHandlerInterface( - interfaceType, - requestHandlerType, - notificationHandlerType, - streamHandlerType)) - .OrderBy(GetTypeSortKey, StringComparer.Ordinal) - .ToList(); - - if (handlerInterfaces.Count == 0) - continue; - - if (!CanReferenceFromGeneratedRegistry(type) || - handlerInterfaces.Any(interfaceType => !CanReferenceFromGeneratedRegistry(interfaceType))) + if (candidate.Value.HasUnsupportedConcreteHandler) { hasUnsupportedConcreteHandler = true; return []; } - var implementationTypeDisplayName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - var implementationLogName = GetLogDisplayName(type); + uniqueCandidates[candidate.Value.ImplementationTypeDisplayName] = candidate.Value; + } - foreach (var handlerInterface in handlerInterfaces) - { - registrations.Add(new HandlerRegistrationSpec( - handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - implementationTypeDisplayName, - GetLogDisplayName(handlerInterface), - implementationLogName)); - } + foreach (var candidate in uniqueCandidates.Values) + { + registrations.AddRange(candidate.Registrations); } registrations.Sort(static (left, right) => @@ -127,38 +165,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return registrations; } - private static IEnumerable EnumerateTypes(INamespaceSymbol namespaceSymbol) - { - foreach (var member in namespaceSymbol.GetMembers()) - { - switch (member) - { - case INamespaceSymbol childNamespace: - foreach (var type in EnumerateTypes(childNamespace)) - yield return type; - - break; - - case INamedTypeSymbol namedType: - foreach (var type in EnumerateTypes(namedType)) - yield return type; - - break; - } - } - } - - private static IEnumerable EnumerateTypes(INamedTypeSymbol typeSymbol) - { - yield return typeSymbol; - - foreach (var nestedType in typeSymbol.GetTypeMembers()) - { - foreach (var descendant in EnumerateTypes(nestedType)) - yield return descendant; - } - } - private static bool IsConcreteHandlerType(INamedTypeSymbol type) { return type.TypeKind is TypeKind.Class or TypeKind.Struct && @@ -177,19 +183,15 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return false; } - private static bool IsSupportedHandlerInterface( - INamedTypeSymbol interfaceType, - INamedTypeSymbol requestHandlerType, - INamedTypeSymbol notificationHandlerType, - INamedTypeSymbol streamHandlerType) + private static bool IsSupportedHandlerInterface(INamedTypeSymbol interfaceType) { if (!interfaceType.IsGenericType) return false; - var definition = interfaceType.OriginalDefinition; - return SymbolEqualityComparer.Default.Equals(definition, requestHandlerType) || - SymbolEqualityComparer.Default.Equals(definition, notificationHandlerType) || - SymbolEqualityComparer.Default.Equals(definition, streamHandlerType); + 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) @@ -229,6 +231,31 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator or Accessibility.ProtectedOrInternal; } + private static string GetFullyQualifiedMetadataName(INamedTypeSymbol type) + { + var nestedTypes = new Stack(); + 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); @@ -300,7 +327,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator private static string EscapeStringLiteral(string value) { return value.Replace("\\", "\\\\") - .Replace("\"", "\\\""); + .Replace("\"", "\\\"") + .Replace("\n", "\\n") + .Replace("\r", "\\r"); } private readonly record struct HandlerRegistrationSpec( @@ -308,4 +337,62 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator string ImplementationTypeDisplayName, string HandlerInterfaceLogName, string ImplementationLogName); + + private readonly struct HandlerCandidateAnalysis : IEquatable + { + public HandlerCandidateAnalysis( + string implementationTypeDisplayName, + ImmutableArray registrations, + bool hasUnsupportedConcreteHandler) + { + ImplementationTypeDisplayName = implementationTypeDisplayName; + Registrations = registrations; + HasUnsupportedConcreteHandler = hasUnsupportedConcreteHandler; + } + + public string ImplementationTypeDisplayName { get; } + + public ImmutableArray 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; + } + } + } }