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;
+ }
+ }
+ }
}