fix(cqrs): 拆分处理器注册长方法

- 优化 CqrsHandlerRegistrar 的 generated registry 激活与 fallback 解析拆分

- 保持原有日志文本、缓存策略与 reflection fallback 语义不变

- 更新 analyzer warning reduction 的 active tracking 与 trace,记录 RP-002 验证结果
This commit is contained in:
GeWuYou 2026-04-21 07:46:00 +08:00
parent 5175f00178
commit 5c7870ca3e
3 changed files with 239 additions and 86 deletions

View File

@ -88,63 +88,14 @@ internal static class CqrsHandlerRegistrar
if (registryTypes.Count == 0)
return GeneratedRegistrationResult.NoGeneratedRegistry();
var registries = new List<ICqrsHandlerRegistry>(registryTypes.Count);
foreach (var registryType in registryTypes)
{
var activationMetadata = RegistryActivationMetadataCache.GetOrAdd(
registryType,
AnalyzeRegistryActivation);
if (!TryCreateGeneratedRegistries(registryTypes, assemblyName, logger, out var registries))
return GeneratedRegistrationResult.NoGeneratedRegistry();
if (!activationMetadata.ImplementsRegistryContract)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}.");
return GeneratedRegistrationResult.NoGeneratedRegistry();
}
if (activationMetadata.IsAbstract)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract.");
return GeneratedRegistrationResult.NoGeneratedRegistry();
}
if (activationMetadata.Factory is null)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not expose an accessible parameterless constructor.");
return GeneratedRegistrationResult.NoGeneratedRegistry();
}
var registry = activationMetadata.Factory();
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);
}
var reflectionFallbackMetadata = assemblyMetadata.ReflectionFallbackMetadata;
if (reflectionFallbackMetadata is not null)
{
if (reflectionFallbackMetadata.HasExplicitTypes)
{
logger.Debug(
$"Generated CQRS registry for assembly {assemblyName} requested targeted reflection fallback for {reflectionFallbackMetadata.Types.Count} unsupported handler type(s).");
}
else
{
logger.Debug(
$"Generated CQRS registry for assembly {assemblyName} requested full reflection fallback for unsupported handlers.");
}
return GeneratedRegistrationResult.WithReflectionFallback(reflectionFallbackMetadata);
}
return GeneratedRegistrationResult.FullyHandled();
RegisterGeneratedRegistries(services, registries, assemblyName, logger);
return BuildGeneratedRegistrationResult(
assemblyMetadata.ReflectionFallbackMetadata,
assemblyName,
logger);
}
catch (Exception exception)
{
@ -186,12 +137,138 @@ internal static class CqrsHandlerRegistrar
// Request/notification handlers receive context injection before every dispatch.
// Transient registration avoids sharing mutable Context across concurrent requests.
services.AddTransient(handlerInterface, implementationType);
logger.Debug(
logger.Debug(
$"Registered CQRS handler {implementationType.FullName} as {handlerInterface.FullName}.");
}
}
}
/// <summary>
/// 激活当前程序集声明的所有 generated registry若任一 registry 不满足运行时契约,则整批回退到反射扫描。
/// </summary>
/// <param name="registryTypes">程序集声明的 generated registry 类型列表。</param>
/// <param name="assemblyName">用于诊断的程序集稳定名称。</param>
/// <param name="logger">日志记录器。</param>
/// <param name="registries">成功激活后的 registry 实例。</param>
/// <returns>当全部 registry 都可安全激活时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
private static bool TryCreateGeneratedRegistries(
IReadOnlyList<Type> registryTypes,
string assemblyName,
ILogger logger,
out IReadOnlyList<ICqrsHandlerRegistry> registries)
{
var activatedRegistries = new List<ICqrsHandlerRegistry>(registryTypes.Count);
foreach (var registryType in registryTypes)
{
if (!TryCreateGeneratedRegistry(registryType, assemblyName, logger, out var registry))
{
registries = Array.Empty<ICqrsHandlerRegistry>();
return false;
}
activatedRegistries.Add(registry);
}
registries = activatedRegistries;
return true;
}
/// <summary>
/// 激活单个 generated registry并在契约不满足时输出与原先完全一致的回退诊断。
/// </summary>
/// <param name="registryType">要分析的 generated registry 类型。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
/// <param name="registry">激活成功后的 registry 实例。</param>
/// <returns>当 registry 可安全使用时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
private static bool TryCreateGeneratedRegistry(
Type registryType,
string assemblyName,
ILogger logger,
out ICqrsHandlerRegistry registry)
{
var activationMetadata = RegistryActivationMetadataCache.GetOrAdd(
registryType,
AnalyzeRegistryActivation);
if (!activationMetadata.ImplementsRegistryContract)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}.");
registry = null!;
return false;
}
if (activationMetadata.IsAbstract)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract.");
registry = null!;
return false;
}
if (activationMetadata.Factory is null)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not expose an accessible parameterless constructor.");
registry = null!;
return false;
}
registry = activationMetadata.Factory();
return true;
}
/// <summary>
/// 调用所有已激活的 generated registry 完成 CQRS handler 注册,并保留稳定的调试日志顺序。
/// </summary>
/// <param name="services">目标服务集合。</param>
/// <param name="registries">已通过契约校验的 registry 实例。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
private static void RegisterGeneratedRegistries(
IServiceCollection services,
IReadOnlyList<ICqrsHandlerRegistry> registries,
string assemblyName,
ILogger logger)
{
foreach (var registry in registries)
{
logger.Debug(
$"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}.");
registry.Register(services, logger);
}
}
/// <summary>
/// 将 generated registry 的 fallback 元数据转换为统一的注册结果,并记录下一阶段是定向补扫还是整程序集扫描。
/// </summary>
/// <param name="reflectionFallbackMetadata">生成注册器声明的反射补扫元数据。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
/// <returns>描述 generated registry 是否已完全处理当前程序集的结果对象。</returns>
private static GeneratedRegistrationResult BuildGeneratedRegistrationResult(
ReflectionFallbackMetadata? reflectionFallbackMetadata,
string assemblyName,
ILogger logger)
{
if (reflectionFallbackMetadata is null)
return GeneratedRegistrationResult.FullyHandled();
if (reflectionFallbackMetadata.HasExplicitTypes)
{
logger.Debug(
$"Generated CQRS registry for assembly {assemblyName} requested targeted reflection fallback for {reflectionFallbackMetadata.Types.Count} unsupported handler type(s).");
}
else
{
logger.Debug(
$"Generated CQRS registry for assembly {assemblyName} requested full reflection fallback for unsupported handlers.");
}
return GeneratedRegistrationResult.WithReflectionFallback(reflectionFallbackMetadata);
}
/// <summary>
/// 获取指定实现类型上所有受支持的 CQRS handler 接口,并缓存筛选与排序结果。
/// </summary>
@ -255,6 +332,29 @@ internal static class CqrsHandlerRegistrar
return null;
var resolvedTypes = new List<Type>();
AppendDirectFallbackTypes(fallbackAttributes, resolvedTypes, assemblyName, logger);
AppendNamedFallbackTypes(assembly, fallbackAttributes, resolvedTypes, assemblyName, logger);
return new ReflectionFallbackMetadata(
resolvedTypes
.Distinct()
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
.ToArray());
}
/// <summary>
/// 追加 attribute 里直接携带的 fallback 类型,并过滤掉跨程序集误声明的条目。
/// </summary>
/// <param name="fallbackAttributes">当前程序集上的 fallback attribute 集合。</param>
/// <param name="resolvedTypes">待补充的已解析类型集合。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
private static void AppendDirectFallbackTypes(
IReadOnlyList<CqrsReflectionFallbackAttribute> fallbackAttributes,
ICollection<Type> resolvedTypes,
string assemblyName,
ILogger logger)
{
foreach (var fallbackType in fallbackAttributes
.SelectMany(static attribute => attribute.FallbackHandlerTypes)
.Where(static type => type is not null)
@ -273,37 +373,65 @@ internal static class CqrsHandlerRegistrar
resolvedTypes.Add(fallbackType);
}
}
/// <summary>
/// 追加 attribute 里以类型名声明的 fallback 条目,并保留逐项失败的诊断能力。
/// </summary>
/// <param name="assembly">当前待解析的程序集。</param>
/// <param name="fallbackAttributes">当前程序集上的 fallback attribute 集合。</param>
/// <param name="resolvedTypes">待补充的已解析类型集合。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
private static void AppendNamedFallbackTypes(
Assembly assembly,
IReadOnlyList<CqrsReflectionFallbackAttribute> fallbackAttributes,
ICollection<Type> resolvedTypes,
string assemblyName,
ILogger logger)
{
foreach (var typeName in fallbackAttributes
.SelectMany(static attribute => attribute.FallbackHandlerTypeNames)
.Where(static name => !string.IsNullOrWhiteSpace(name))
.Distinct(StringComparer.Ordinal)
.OrderBy(static name => name, StringComparer.Ordinal))
{
try
{
var type = assembly.GetType(typeName, throwOnError: false, ignoreCase: false);
if (type is null)
{
logger.Warn(
$"Generated CQRS reflection fallback type {typeName} could not be resolved in assembly {assemblyName}. Skipping targeted fallback entry.");
continue;
}
TryAppendNamedFallbackType(assembly, resolvedTypes, assemblyName, typeName, logger);
}
}
resolvedTypes.Add(type);
}
catch (Exception exception)
/// <summary>
/// 解析并追加单个按名称声明的 fallback 类型,同时保留“找不到”与“加载异常”两类不同日志语义。
/// </summary>
/// <param name="assembly">当前待解析的程序集。</param>
/// <param name="resolvedTypes">待补充的已解析类型集合。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="typeName">要解析的完整类型名。</param>
/// <param name="logger">日志记录器。</param>
private static void TryAppendNamedFallbackType(
Assembly assembly,
ICollection<Type> resolvedTypes,
string assemblyName,
string typeName,
ILogger logger)
{
try
{
var type = assembly.GetType(typeName, throwOnError: false, ignoreCase: false);
if (type is null)
{
logger.Warn(
$"Generated CQRS reflection fallback type {typeName} failed to load in assembly {assemblyName}: {exception.Message}");
$"Generated CQRS reflection fallback type {typeName} could not be resolved in assembly {assemblyName}. Skipping targeted fallback entry.");
return;
}
}
return new ReflectionFallbackMetadata(
resolvedTypes
.Distinct()
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
.ToArray());
resolvedTypes.Add(type);
}
catch (Exception exception)
{
logger.Warn(
$"Generated CQRS reflection fallback type {typeName} failed to load in assembly {assemblyName}: {exception.Message}");
}
}
/// <summary>

View File

@ -7,28 +7,30 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-001`
- 当前阶段:`Phase 1`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-002`
- 当前阶段:`Phase 2`
- 当前焦点:
- 已将旧 `local-plan/` 迁入 `ai-plan/public/analyzer-warning-reduction/`active 入口只保留当前恢复信息
- 基于现有剩余热点,评估 `MA0051``MA0048``MA0046` 与少量 `MA0016` 是否适合继续在同一主线上处理
- 若继续推进,优先选择不引入 API rename、公共契约漂移或 Godot 宿主不稳定测试的切入点
- 已完成 `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs``MA0051` 长方法拆分,保持 generated registry、fallback 与缓存语义不变
- 已确认 `GFramework.Cqrs` 定向构建恢复为 `0 Warning(s)`,该 warning slice 已不再是当前主题的阻塞项
- 下一轮若继续推进,优先从 `GFramework.Core` 剩余的 `MA0051``MA0046` 或低风险 `MA0016` 中只选一个切入点
## 当前状态摘要
- 已完成 `GFramework.Core``GFramework.Cqrs``GFramework.Godot` 与部分 source generator 的低风险 warning 清理
- 已完成多轮 CodeRabbit follow-up 修复,并用定向测试与项目/解决方案构建验证了关键回归风险
- 当前剩余 warning 已集中到长方法、文件/类型命名冲突、delegate 形状和少量公共集合抽象接口问题
- 当前 `GFramework.Cqrs` 的剩余 warning 热点已从 active 入口移除;主题内剩余 warning 主要集中在 `GFramework.Core` 长方法、
文件/类型命名冲突、delegate 形状和少量公共集合抽象接口问题
## 当前活跃事实
- 当前主题仍是 active topic因为剩余结构性 warning 是否继续推进尚未决策
- `RP-001` 的详细实现历史、测试记录和 warning 热点清单已归档到主题内 `archive/`
- `RP-002` 已在不改公共契约的前提下完成 `CqrsHandlerRegistrar` 结构拆分,并通过定向 build/test 验证
- 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射
## 当前风险
- 结构性重构风险:剩余 `MA0051` 与 `MA0048` 可能要求较大的文件拆分或类型重命名
- 结构性重构风险:剩余 `GFramework.Core` 侧 `MA0051` 与 `MA0048` 可能要求较大的文件拆分或类型重命名
- 缓解措施:只在下一轮明确接受结构调整成本时再继续推进,不在恢复点模糊的情况下顺手扩面
- 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定
- 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证
@ -43,10 +45,14 @@
## 验证说明
- `RP-001` 的详细 warning 清理、回归修复与定向验证命令均已迁入主题内历史归档
- `RP-002` 的定向验证结果:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter FullyQualifiedName~CqrsHandlerRegistrarTests -p:RestoreFallbackFolders=`
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
## 下一步
1. 若要继续该主题,先读 active tracking再按需展开历史归档中的 warning 热点与验证记录
2. 从 `MA0051``MA0048``MA0046` 中只选一个结构性切入点继续,不要在同一轮同时扩多个风险面
2. 优先在 `GFramework.Core/Architectures/ArchitectureLifecycle.cs``GFramework.Core/Coroutine/CoroutineScheduler.cs`
`GFramework.Core/Pause/PauseStackManager.cs``MA0051` 中只选一个继续,不要在同一轮同时扩多个风险面
3. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`

View File

@ -1,5 +1,24 @@
# Analyzer Warning Reduction 追踪
## 2026-04-21
### 阶段CQRS `MA0051` 收口RP-002
- 依据 active tracking 中“先只选一个结构性切入点”的约束,选定 `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs`
作为低风险下一步,因为它已有稳定的 targeted test 覆盖 generated registry、reflection fallback、缓存和重复注册行为
- 将 `TryRegisterGeneratedHandlers` 拆分为 registry 激活、批量注册和 fallback 结果构建三个辅助阶段,同时把
`GetReflectionFallbackMetadata` 的直接类型解析与按名称解析拆开,降低长方法复杂度但不改日志文本与回退语义
- 顺手修正 `RegisterAssemblyHandlers` 内部调试日志的缩进,未改注册顺序、生命周期或服务描述符写入逻辑
- 验证通过:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=`
- 结果:`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter FullyQualifiedName~CqrsHandlerRegistrarTests -p:RestoreFallbackFolders=`
- 结果:`11 Passed``0 Failed`
- 新发现的环境注意事项:
- 当前 WSL worktree 下若不显式传入 `-p:RestoreFallbackFolders=`Linux `dotnet` 会读取不存在的 Windows fallback package
folder 并导致 `ResolvePackageAssets` 失败
- sandbox 内运行 `dotnet` 会因 MSBuild named-pipe 限制失败;需要在提权上下文中执行 .NET 验证
## 2026-04-19
### 阶段local-plan 迁移收口RP-001
@ -28,5 +47,5 @@
### 下一步
1. 后续若继续 analyzer warning reduction只从 `ai-plan/public/analyzer-warning-reduction/` 进入,不再恢复 `local-plan/`
2. 若 active 入口再次积累多轮已完成且已验证阶段,继续按同一模式迁入该主题自己的 `archive/`
1. 若继续 analyzer warning reduction优先回到 `GFramework.Core` 剩余 `MA0051` 热点,并继续保持“单 warning family、单切入点”的节奏
2. 后续所有 WSL 下的 .NET 定向验证命令继续显式附带 `-p:RestoreFallbackFolders=`,避免把环境问题误判成代码回归