diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index c6fd3909..33332ea5 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -88,63 +88,14 @@ internal static class CqrsHandlerRegistrar if (registryTypes.Count == 0) return GeneratedRegistrationResult.NoGeneratedRegistry(); - var registries = new List(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}."); } } } + /// + /// 激活当前程序集声明的所有 generated registry;若任一 registry 不满足运行时契约,则整批回退到反射扫描。 + /// + /// 程序集声明的 generated registry 类型列表。 + /// 用于诊断的程序集稳定名称。 + /// 日志记录器。 + /// 成功激活后的 registry 实例。 + /// 当全部 registry 都可安全激活时返回 ;否则返回 + private static bool TryCreateGeneratedRegistries( + IReadOnlyList registryTypes, + string assemblyName, + ILogger logger, + out IReadOnlyList registries) + { + var activatedRegistries = new List(registryTypes.Count); + foreach (var registryType in registryTypes) + { + if (!TryCreateGeneratedRegistry(registryType, assemblyName, logger, out var registry)) + { + registries = Array.Empty(); + return false; + } + + activatedRegistries.Add(registry); + } + + registries = activatedRegistries; + return true; + } + + /// + /// 激活单个 generated registry,并在契约不满足时输出与原先完全一致的回退诊断。 + /// + /// 要分析的 generated registry 类型。 + /// 当前程序集的稳定名称。 + /// 日志记录器。 + /// 激活成功后的 registry 实例。 + /// 当 registry 可安全使用时返回 ;否则返回 + 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; + } + + /// + /// 调用所有已激活的 generated registry 完成 CQRS handler 注册,并保留稳定的调试日志顺序。 + /// + /// 目标服务集合。 + /// 已通过契约校验的 registry 实例。 + /// 当前程序集的稳定名称。 + /// 日志记录器。 + private static void RegisterGeneratedRegistries( + IServiceCollection services, + IReadOnlyList 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); + } + } + + /// + /// 将 generated registry 的 fallback 元数据转换为统一的注册结果,并记录下一阶段是定向补扫还是整程序集扫描。 + /// + /// 生成注册器声明的反射补扫元数据。 + /// 当前程序集的稳定名称。 + /// 日志记录器。 + /// 描述 generated registry 是否已完全处理当前程序集的结果对象。 + 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); + } + /// /// 获取指定实现类型上所有受支持的 CQRS handler 接口,并缓存筛选与排序结果。 /// @@ -255,6 +332,29 @@ internal static class CqrsHandlerRegistrar return null; var resolvedTypes = new List(); + AppendDirectFallbackTypes(fallbackAttributes, resolvedTypes, assemblyName, logger); + AppendNamedFallbackTypes(assembly, fallbackAttributes, resolvedTypes, assemblyName, logger); + + return new ReflectionFallbackMetadata( + resolvedTypes + .Distinct() + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToArray()); + } + + /// + /// 追加 attribute 里直接携带的 fallback 类型,并过滤掉跨程序集误声明的条目。 + /// + /// 当前程序集上的 fallback attribute 集合。 + /// 待补充的已解析类型集合。 + /// 当前程序集的稳定名称。 + /// 日志记录器。 + private static void AppendDirectFallbackTypes( + IReadOnlyList fallbackAttributes, + ICollection 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); } + } + /// + /// 追加 attribute 里以类型名声明的 fallback 条目,并保留逐项失败的诊断能力。 + /// + /// 当前待解析的程序集。 + /// 当前程序集上的 fallback attribute 集合。 + /// 待补充的已解析类型集合。 + /// 当前程序集的稳定名称。 + /// 日志记录器。 + private static void AppendNamedFallbackTypes( + Assembly assembly, + IReadOnlyList fallbackAttributes, + ICollection 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) + /// + /// 解析并追加单个按名称声明的 fallback 类型,同时保留“找不到”与“加载异常”两类不同日志语义。 + /// + /// 当前待解析的程序集。 + /// 待补充的已解析类型集合。 + /// 当前程序集的稳定名称。 + /// 要解析的完整类型名。 + /// 日志记录器。 + private static void TryAppendNamedFallbackType( + Assembly assembly, + ICollection 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}"); + } } /// diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index 7fa5cf09..3a2b85dd 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -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/` diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index 5e42fb93..d23aca6d 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -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=`,避免把环境问题误判成代码回归