refactor(cqrs): 收敛处理器重复映射判定

- 优化 CqrsHandlerRegistrar 使用本地映射索引替代重复线性扫描
- 补充 重复 handler 类型输入仍只注册一份映射的回归测试
- 更新 cqrs-rewrite 跟踪与 trace 到 RP-049
This commit is contained in:
gewuyou 2026-04-20 10:37:36 +08:00 committed by GeWuYou
parent 110666d06b
commit db65249315
4 changed files with 63 additions and 18 deletions

View File

@ -488,6 +488,33 @@ internal sealed class CqrsHandlerRegistrarTests
handlerAssembly.Verify(static assembly => assembly.GetTypes(), Times.Once); handlerAssembly.Verify(static assembly => assembly.GetTypes(), Times.Once);
} }
/// <summary>
/// 验证当程序集枚举结果包含重复 handler 类型时registrar 仍只会写入一份 handler 映射。
/// </summary>
[Test]
public void RegisterHandlers_Should_Skip_Duplicate_Handler_Mappings_When_Assembly_Returns_Duplicate_Types()
{
var handlerType = typeof(AlphaDeterministicNotificationHandler);
var handlerAssembly = new Mock<Assembly>();
handlerAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Cqrs.DuplicateHandlerMappingsAssembly, Version=1.0.0.0");
handlerAssembly
.Setup(static assembly => assembly.GetTypes())
.Returns([handlerType, handlerType]);
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, handlerAssembly.Object);
var registrations = container.GetServicesUnsafe
.Where(static descriptor =>
descriptor.ServiceType == typeof(INotificationHandler<DeterministicOrderNotification>) &&
descriptor.ImplementationType == typeof(AlphaDeterministicNotificationHandler))
.ToArray();
Assert.That(registrations, Has.Length.EqualTo(1));
}
/// <summary> /// <summary>
/// 清空本测试依赖的 registrar 静态缓存,避免跨用例共享进程级状态导致断言漂移。 /// 清空本测试依赖的 registrar 静态缓存,避免跨用例共享进程级状态导致断言漂移。
/// </summary> /// </summary>

View File

@ -165,6 +165,7 @@ internal static class CqrsHandlerRegistrar
ILogger logger, ILogger logger,
ReflectionFallbackMetadata? reflectionFallbackMetadata) ReflectionFallbackMetadata? reflectionFallbackMetadata)
{ {
var registeredMappings = CreateRegisteredHandlerMappings(services);
foreach (var implementationType in GetCandidateHandlerTypes(assembly, logger, reflectionFallbackMetadata) foreach (var implementationType in GetCandidateHandlerTypes(assembly, logger, reflectionFallbackMetadata)
.Where(IsConcreteHandlerType)) .Where(IsConcreteHandlerType))
{ {
@ -175,7 +176,7 @@ internal static class CqrsHandlerRegistrar
foreach (var handlerInterface in handlerInterfaces) foreach (var handlerInterface in handlerInterfaces)
{ {
if (IsHandlerMappingAlreadyRegistered(services, handlerInterface, implementationType)) if (!registeredMappings.Add(new HandlerMapping(handlerInterface, implementationType)))
{ {
logger.Debug( logger.Debug(
$"Skipping duplicate CQRS handler {implementationType.FullName} as {handlerInterface.FullName}."); $"Skipping duplicate CQRS handler {implementationType.FullName} as {handlerInterface.FullName}.");
@ -209,6 +210,21 @@ internal static class CqrsHandlerRegistrar
.ToArray()); .ToArray());
} }
/// <summary>
/// 根据当前服务集合创建已注册 handler 映射的快速索引,避免 reflection fallback 路径重复线性扫描服务描述符。
/// </summary>
/// <param name="services">当前容器的服务描述符集合。</param>
/// <returns>已存在的 handler 映射集合。</returns>
private static HashSet<HandlerMapping> CreateRegisteredHandlerMappings(IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
return services
.Where(static descriptor => descriptor.ImplementationType is not null)
.Select(static descriptor => new HandlerMapping(descriptor.ServiceType, descriptor.ImplementationType!))
.ToHashSet();
}
/// <summary> /// <summary>
/// 根据生成器提供的 fallback 清单或整程序集扫描结果,获取本轮要注册的候选处理器类型。 /// 根据生成器提供的 fallback 清单或整程序集扫描结果,获取本轮要注册的候选处理器类型。
/// </summary> /// </summary>
@ -455,21 +471,6 @@ internal static class CqrsHandlerRegistrar
definition == typeof(IStreamRequestHandler<,>); definition == typeof(IStreamRequestHandler<,>);
} }
/// <summary>
/// 判断同一 handler 映射是否已经由生成注册器或先前扫描步骤写入服务集合。
/// </summary>
private static bool IsHandlerMappingAlreadyRegistered(
IServiceCollection services,
Type handlerInterface,
Type implementationType)
{
// 这里保持线性扫描,避免为常见的小到中等规模程序集长期维护额外索引。
// 若未来大型服务集合出现热点,可在更高层批处理中引入 HashSet<(Type, Type)> 做 O(1) 去重。
return services.Any(descriptor =>
descriptor.ServiceType == handlerInterface &&
descriptor.ImplementationType == implementationType);
}
/// <summary> /// <summary>
/// 生成程序集排序键,保证跨运行环境的处理器注册顺序稳定。 /// 生成程序集排序键,保证跨运行环境的处理器注册顺序稳定。
/// </summary> /// </summary>
@ -486,6 +487,8 @@ internal static class CqrsHandlerRegistrar
return type.FullName ?? type.Name; return type.FullName ?? type.Name;
} }
private readonly record struct HandlerMapping(Type ServiceType, Type ImplementationType);
private readonly record struct GeneratedRegistrationResult( private readonly record struct GeneratedRegistrationResult(
bool UsedGeneratedRegistry, bool UsedGeneratedRegistry,
bool RequiresReflectionFallback, bool RequiresReflectionFallback,

View File

@ -7,7 +7,7 @@ CQRS 迁移与收敛。
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`CQRS-REWRITE-RP-048` - 恢复点编号:`CQRS-REWRITE-RP-049`
- 当前阶段:`Phase 8` - 当前阶段:`Phase 8`
- 当前焦点: - 当前焦点:
- 当前功能历史已归档active 跟踪仅保留 `Phase 8` 主线的恢复入口 - 当前功能历史已归档active 跟踪仅保留 `Phase 8` 主线的恢复入口
@ -16,6 +16,7 @@ CQRS 迁移与收敛。
- 已补充 pointer 响应类型的 precise runtime type 生成,避免这类 handler 再退回程序集级 reflection fallback - 已补充 pointer 响应类型的 precise runtime type 生成,避免这类 handler 再退回程序集级 reflection fallback
- 已收紧 function pointer 签名的可直接生成判定,仅在其返回值与参数类型都可安全引用时才走静态注册路径 - 已收紧 function pointer 签名的可直接生成判定,仅在其返回值与参数类型都可安全引用时才走静态注册路径
- 已为 registrar 的 reflection 注册路径补充 handler-interface 元数据缓存,减少跨容器重复注册时的 `GetInterfaces()` 反射 - 已为 registrar 的 reflection 注册路径补充 handler-interface 元数据缓存,减少跨容器重复注册时的 `GetInterfaces()` 反射
- 已将 registrar 的重复映射判定从线性扫描 `IServiceCollection` 收敛为本地映射索引,减少 fallback 注册路径的重复查找
- 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点 - 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点
## 当前状态摘要 ## 当前状态摘要
@ -45,6 +46,10 @@ CQRS 迁移与收敛。
- `CqrsHandlerRegistrar` 现会按 `Type` 弱键缓存已筛选且排序好的 supported handler interface 列表 - `CqrsHandlerRegistrar` 现会按 `Type` 弱键缓存已筛选且排序好的 supported handler interface 列表
- 同一 handler 类型跨容器重复注册时,不再重复执行 `GetInterfaces()` 与支持接口筛选 - 同一 handler 类型跨容器重复注册时,不再重复执行 `GetInterfaces()` 与支持接口筛选
- `GFramework.Cqrs.Tests` 已补充 registrar 静态缓存隔离与 supported interface 缓存复用回归 - `GFramework.Cqrs.Tests` 已补充 registrar 静态缓存隔离与 supported interface 缓存复用回归
- `2026-04-20` 已完成一轮 registrar 去重路径收敛:
- `CqrsHandlerRegistrar` 现会在单次 reflection 注册流程开始时构建已注册 handler 映射索引
- 同一批注册中后续 duplicate handler mapping 不再重复线性扫描 `IServiceCollection`
- `GFramework.Cqrs.Tests` 已补充“程序集返回重复 handler 类型时仍只注册一份映射”的回归
- 当前主线优先级: - 当前主线优先级:
- generator 覆盖面继续扩大 - generator 覆盖面继续扩大
- dispatch/invoker 反射占比继续下降 - dispatch/invoker 反射占比继续下降
@ -74,7 +79,7 @@ CQRS 迁移与收敛。
- 备注:`14/14` 测试通过;本轮覆盖 pointer precise registration 与 function pointer fallback 边界 - 备注:`14/14` 测试通过;本轮覆盖 pointer precise registration 与 function pointer fallback 边界
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
- 结果:通过 - 结果:通过
- 备注:`10/10` 测试通过;本轮覆盖 registrar 的 supported handler interface 缓存 - 备注:`11/11` 测试通过;本轮覆盖 registrar 的 supported handler interface 缓存与 duplicate mapping 去重路径
## 下一步 ## 下一步

View File

@ -2,6 +2,16 @@
## 2026-04-20 ## 2026-04-20
### 阶段registrar duplicate mapping 索引收敛CQRS-REWRITE-RP-049
- 已将 `CqrsHandlerRegistrar` 的重复 handler mapping 判定从逐条线性扫描 `IServiceCollection` 收敛为单次构建的本地映射索引
- reflection fallback 或重复类型输入场景下,后续 duplicate mapping 判定改为 `HashSet` 命中,不再重复遍历已有服务描述符
- `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs` 已补充“程序集枚举返回重复 handler 类型时仍只注册一份映射”的回归
- 定向验证已通过:
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
- `11/11` passed
- 当前沙箱限制 MSBuild named pipe因此验证在提权环境下执行
### 阶段registrar handler-interface 反射缓存CQRS-REWRITE-RP-048 ### 阶段registrar handler-interface 反射缓存CQRS-REWRITE-RP-048
- 已在 `CqrsHandlerRegistrar` 中新增按 `Type` 弱键缓存的 supported handler interface 元数据reflection 注册路径现会复用已筛选且排序好的接口列表 - 已在 `CqrsHandlerRegistrar` 中新增按 `Type` 弱键缓存的 supported handler interface 元数据reflection 注册路径现会复用已筛选且排序好的接口列表