diff --git a/GFramework.Core.Tests/Pause/PauseStackManagerTests.cs b/GFramework.Core.Tests/Pause/PauseStackManagerTests.cs
index 4705357f..6e7b0581 100644
--- a/GFramework.Core.Tests/Pause/PauseStackManagerTests.cs
+++ b/GFramework.Core.Tests/Pause/PauseStackManagerTests.cs
@@ -394,6 +394,35 @@ public class PauseStackManagerTests
Assert.That(_manager.IsPaused(PauseGroup.Audio), Is.False);
}
+ ///
+ /// 验证销毁时会向所有仍暂停的组补发恢复通知。
+ ///
+ [Test]
+ public async Task DestroyAsync_Should_NotifyResumedGroups()
+ {
+ var resumedGroups = new List();
+ var mockHandler = new MockPauseHandler();
+
+ _manager.RegisterHandler(mockHandler);
+ _manager.OnPauseStateChanged += (_, e) =>
+ {
+ if (!e.IsPaused)
+ {
+ resumedGroups.Add(e.Group);
+ }
+ };
+
+ _manager.Push("Global", PauseGroup.Global);
+ _manager.Push("Gameplay", PauseGroup.Gameplay);
+ mockHandler.Reset();
+
+ await _manager.DestroyAsync();
+
+ Assert.That(mockHandler.CallCount, Is.EqualTo(2));
+ Assert.That(mockHandler.LastIsPaused, Is.False);
+ Assert.That(resumedGroups, Is.EquivalentTo(new[] { PauseGroup.Global, PauseGroup.Gameplay }));
+ }
+
///
/// 验证并发Push是线程安全的
///
@@ -469,4 +498,4 @@ public class PauseStackManagerTests
LastIsPaused = null;
}
}
-}
\ No newline at end of file
+}
diff --git a/GFramework.Core/Architectures/ArchitectureLifecycle.cs b/GFramework.Core/Architectures/ArchitectureLifecycle.cs
index d503f68c..c0fed21f 100644
--- a/GFramework.Core/Architectures/ArchitectureLifecycle.cs
+++ b/GFramework.Core/Architectures/ArchitectureLifecycle.cs
@@ -145,69 +145,34 @@ internal sealed class ArchitectureLifecycle(
{
logger.Info($"Initializing {_pendingInitializableList.Count} components");
- // 按类型分组初始化(保持原有的阶段划分)
- var utilities = _pendingInitializableList.OfType().ToList();
- var models = _pendingInitializableList.OfType().ToList();
- var systems = _pendingInitializableList.OfType().ToList();
+ var initializationPlan = CreateInitializationPlan();
- // 1. 工具初始化阶段
- EnterPhase(ArchitecturePhase.BeforeUtilityInit);
+ await InitializePhaseComponentsAsync(
+ initializationPlan.Utilities,
+ ArchitecturePhase.BeforeUtilityInit,
+ ArchitecturePhase.AfterUtilityInit,
+ "context utilities",
+ "utility",
+ asyncMode)
+ .ConfigureAwait(false);
+ await InitializePhaseComponentsAsync(
+ initializationPlan.Models,
+ ArchitecturePhase.BeforeModelInit,
+ ArchitecturePhase.AfterModelInit,
+ "models",
+ "model",
+ asyncMode)
+ .ConfigureAwait(false);
+ await InitializePhaseComponentsAsync(
+ initializationPlan.Systems,
+ ArchitecturePhase.BeforeSystemInit,
+ ArchitecturePhase.AfterSystemInit,
+ "systems",
+ "system",
+ asyncMode)
+ .ConfigureAwait(false);
- if (utilities.Count != 0)
- {
- logger.Info($"Initializing {utilities.Count} context utilities");
-
- foreach (var utility in utilities)
- {
- logger.Debug($"Initializing utility: {utility.GetType().Name}");
- await InitializeComponentAsync(utility, asyncMode).ConfigureAwait(false);
- }
-
- logger.Info("All context utilities initialized");
- }
-
- EnterPhase(ArchitecturePhase.AfterUtilityInit);
-
- // 2. 模型初始化阶段
- EnterPhase(ArchitecturePhase.BeforeModelInit);
-
- if (models.Count != 0)
- {
- logger.Info($"Initializing {models.Count} models");
-
- foreach (var model in models)
- {
- logger.Debug($"Initializing model: {model.GetType().Name}");
- await InitializeComponentAsync(model, asyncMode).ConfigureAwait(false);
- }
-
- logger.Info("All models initialized");
- }
-
- EnterPhase(ArchitecturePhase.AfterModelInit);
-
- // 3. 系统初始化阶段
- EnterPhase(ArchitecturePhase.BeforeSystemInit);
-
- if (systems.Count != 0)
- {
- logger.Info($"Initializing {systems.Count} systems");
-
- foreach (var system in systems)
- {
- logger.Debug($"Initializing system: {system.GetType().Name}");
- await InitializeComponentAsync(system, asyncMode).ConfigureAwait(false);
- }
-
- logger.Info("All systems initialized");
- }
-
- EnterPhase(ArchitecturePhase.AfterSystemInit);
-
- _pendingInitializableList.Clear();
- _pendingInitializableSet.Clear();
- _initialized = true;
- logger.Info("All components initialized");
+ MarkInitializationCompleted();
}
///
@@ -223,6 +188,67 @@ internal sealed class ArchitectureLifecycle(
component.Initialize();
}
+ ///
+ /// 按架构既有阶段语义把待初始化组件拆分为 utility、model 和 system 三个批次。
+ /// 这样可以在压缩主流程复杂度的同时,继续复用注册顺序和接口类型决定的初始化分层。
+ ///
+ /// 当前待初始化组件的阶段化批次。
+ private InitializationPlan CreateInitializationPlan()
+ {
+ return new InitializationPlan(
+ _pendingInitializableList.OfType().ToList(),
+ _pendingInitializableList.OfType().ToList(),
+ _pendingInitializableList.OfType().ToList());
+ }
+
+ ///
+ /// 执行单个生命周期阶段的批量初始化,并统一维护阶段切换、日志输出和异步初始化策略。
+ ///
+ /// 当前阶段要初始化的组件类型。
+ /// 当前阶段的组件列表。
+ /// 阶段开始前要进入的生命周期状态。
+ /// 阶段结束后要进入的生命周期状态。
+ /// 用于批量日志的组件组名称。
+ /// 用于单个组件日志的组件角色名称。
+ /// 是否允许优先走异步初始化契约。
+ private async Task InitializePhaseComponentsAsync(
+ IReadOnlyList components,
+ ArchitecturePhase beforePhase,
+ ArchitecturePhase afterPhase,
+ string componentGroupName,
+ string componentLogName,
+ bool asyncMode)
+ where TComponent : class, IInitializable
+ {
+ EnterPhase(beforePhase);
+
+ if (components.Count != 0)
+ {
+ logger.Info($"Initializing {components.Count} {componentGroupName}");
+
+ foreach (var component in components)
+ {
+ logger.Debug($"Initializing {componentLogName}: {component.GetType().Name}");
+ await InitializeComponentAsync(component, asyncMode).ConfigureAwait(false);
+ }
+
+ logger.Info($"All {componentGroupName} initialized");
+ }
+
+ EnterPhase(afterPhase);
+ }
+
+ ///
+ /// 在所有阶段初始化完成后清理挂起列表,并把生命周期状态切换到“已初始化”。
+ ///
+ private void MarkInitializationCompleted()
+ {
+ _pendingInitializableList.Clear();
+ _pendingInitializableSet.Clear();
+ _initialized = true;
+ logger.Info("All components initialized");
+ }
+
///
/// 立即初始化在常规初始化批次完成后新增的组件。
/// 当启用 AllowLateRegistration 时,生命周期层需要和注册层保持一致,
@@ -258,6 +284,17 @@ internal sealed class ArchitectureLifecycle(
#endregion
+ ///
+ /// 保存一次完整初始化流程所需的三个阶段批次。
+ ///
+ /// Utility 初始化批次。
+ /// Model 初始化批次。
+ /// System 初始化批次。
+ private readonly record struct InitializationPlan(
+ IReadOnlyList Utilities,
+ IReadOnlyList Models,
+ IReadOnlyList Systems);
+
#region Ready State
///
diff --git a/GFramework.Core/Pause/PauseStackManager.cs b/GFramework.Core/Pause/PauseStackManager.cs
index dd15fe3c..665c2d3a 100644
--- a/GFramework.Core/Pause/PauseStackManager.cs
+++ b/GFramework.Core/Pause/PauseStackManager.cs
@@ -26,69 +26,17 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
public ValueTask DestroyAsync()
{
if (_disposed)
+ {
return ValueTask.CompletedTask;
-
- List pausedGroups;
- IPauseHandler[] handlersSnapshot;
-
- _lock.EnterWriteLock();
- try
- {
- if (_disposed)
- return ValueTask.CompletedTask;
-
- _disposed = true;
-
- // 收集所有当前暂停的组
- pausedGroups = _pauseStacks
- .Where(kvp => kvp.Value.Count > 0)
- .Select(kvp => kvp.Key)
- .ToList();
-
- // 获取处理器快照
- handlersSnapshot = _handlers.ToArray();
-
- // 清理所有数据结构
- _pauseStacks.Clear();
- _tokenMap.Clear();
- _handlers.Clear();
-
- _logger.Debug("PauseStackManager destroyed");
- }
- finally
- {
- _lock.ExitWriteLock();
}
- // 在锁外通知所有之前暂停的组恢复,保持生命周期信号一致
- foreach (var group in pausedGroups)
+ var destroySnapshot = TryBeginDestroy();
+ if (destroySnapshot == null)
{
- _logger.Debug($"Notifying handlers of destruction: Group={group}, IsPaused=false");
-
- foreach (var handler in handlersSnapshot.OrderBy(h => h.Priority))
- {
- try
- {
- handler.OnPauseStateChanged(group, false);
- }
- catch (Exception ex)
- {
- _logger.Error($"Handler {handler.GetType().Name} failed during destruction", ex);
- }
- }
-
- // 触发事件
- try
- {
- RaisePauseStateChanged(group, false);
- }
- catch (Exception ex)
- {
- _logger.Error($"Event subscriber failed during destruction for group {group}", ex);
- }
+ return ValueTask.CompletedTask;
}
- // 释放锁资源
+ NotifyDestroyedGroups(destroySnapshot.Value);
_lock.Dispose();
return ValueTask.CompletedTask;
@@ -163,74 +111,17 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
public bool Pop(PauseToken token)
{
if (!token.IsValid)
+ {
return false;
-
- bool found;
- bool shouldNotify = false;
- PauseGroup notifyGroup = PauseGroup.Global;
-
- _lock.EnterWriteLock();
- try
- {
- ThrowIfDisposed();
-
- if (!_tokenMap.TryGetValue(token.Id, out var entry))
- {
- _logger.Warn($"Attempted to pop invalid/expired token: {token.Id}");
- return false;
- }
-
- var group = entry.Group;
- var stack = _pauseStacks[group];
- var wasPaused = stack.Count > 0;
-
- // 从栈中移除
- var tempStack = new Stack();
- found = false;
-
- while (stack.Count > 0)
- {
- var current = stack.Pop();
- if (current.TokenId == token.Id)
- {
- found = true;
- break;
- }
-
- tempStack.Push(current);
- }
-
- // 恢复栈结构
- while (tempStack.Count > 0)
- {
- stack.Push(tempStack.Pop());
- }
-
- if (found)
- {
- _tokenMap.Remove(token.Id);
- _logger.Debug($"Pause popped: {entry.Reason} (Group: {group}, Remaining: {stack.Count})");
-
- // 状态变化检测:从暂停 → 未暂停
- if (wasPaused && stack.Count == 0)
- {
- shouldNotify = true;
- notifyGroup = group;
- }
- }
- }
- finally
- {
- _lock.ExitWriteLock();
}
- // 在锁外通知处理器,避免死锁
- if (shouldNotify)
+ var result = TryPopEntry(token);
+ if (result.ShouldNotify)
{
- NotifyHandlers(notifyGroup, false);
+ NotifyHandlers(result.NotifyGroup, false);
}
- return found;
+ return result.Found;
}
///
@@ -443,6 +334,200 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
}
}
+ ///
+ /// 采集销毁所需的快照并清空内部状态。
+ ///
+ ///
+ /// 成功进入销毁阶段时返回销毁快照;如果其他线程已先完成销毁,则返回 。
+ ///
+ ///
+ /// 该方法只负责锁内状态迁移,把外部回调与事件派发留到锁外执行,
+ /// 以避免在生命周期结束阶段持锁调用用户代码。
+ ///
+ private DestroySnapshot? TryBeginDestroy()
+ {
+ _lock.EnterWriteLock();
+ try
+ {
+ if (_disposed)
+ {
+ return null;
+ }
+
+ _disposed = true;
+
+ var pausedGroups = CollectPausedGroups();
+ var handlersSnapshot = CreateHandlerSnapshot();
+
+ _pauseStacks.Clear();
+ _tokenMap.Clear();
+ _handlers.Clear();
+
+ _logger.Debug("PauseStackManager destroyed");
+
+ return new DestroySnapshot(pausedGroups, handlersSnapshot);
+ }
+ finally
+ {
+ _lock.ExitWriteLock();
+ }
+ }
+
+ ///
+ /// 在销毁后向所有先前处于暂停状态的分组补发恢复通知。
+ ///
+ /// 销毁阶段采集的分组与处理器快照。
+ private void NotifyDestroyedGroups(DestroySnapshot destroySnapshot)
+ {
+ foreach (var group in destroySnapshot.PausedGroups)
+ {
+ _logger.Debug($"Notifying handlers of destruction: Group={group}, IsPaused=false");
+
+ NotifyHandlersSnapshot(group, false, destroySnapshot.HandlersSnapshot, isDestroying: true);
+ RaiseDestroyStateChanged(group);
+ }
+ }
+
+ ///
+ /// 在锁内执行令牌移除,并返回锁外通知所需的信息。
+ ///
+ /// 要移除的暂停令牌。
+ /// 包含本次弹出结果和后续通知决策的快照。
+ ///
+ /// Pop 支持移除非栈顶令牌,因此这里会先临时转移栈元素,再恢复原有顺序,
+ /// 只在最后一个暂停请求被移除时触发恢复通知。
+ ///
+ private PopResult TryPopEntry(PauseToken token)
+ {
+ _lock.EnterWriteLock();
+ try
+ {
+ ThrowIfDisposed();
+
+ if (!_tokenMap.TryGetValue(token.Id, out var entry))
+ {
+ _logger.Warn($"Attempted to pop invalid/expired token: {token.Id}");
+ return PopResult.NotFound;
+ }
+
+ var stack = _pauseStacks[entry.Group];
+ var wasPaused = stack.Count > 0;
+ var found = RemoveEntryFromStack(stack, token.Id);
+ if (!found)
+ {
+ return PopResult.NotFound;
+ }
+
+ _tokenMap.Remove(token.Id);
+ _logger.Debug($"Pause popped: {entry.Reason} (Group: {entry.Group}, Remaining: {stack.Count})");
+
+ return new PopResult(true, wasPaused && stack.Count == 0, entry.Group);
+ }
+ finally
+ {
+ _lock.ExitWriteLock();
+ }
+ }
+
+ ///
+ /// 从指定暂停栈中移除目标令牌,并保持其他暂停请求的原始顺序。
+ ///
+ /// 要修改的暂停栈。
+ /// 目标令牌标识。
+ /// 如果找到了目标令牌则返回 。
+ private static bool RemoveEntryFromStack(Stack stack, Guid tokenId)
+ {
+ var tempStack = new Stack();
+ var found = false;
+
+ while (stack.Count > 0)
+ {
+ var current = stack.Pop();
+ if (current.TokenId == tokenId)
+ {
+ found = true;
+ break;
+ }
+
+ tempStack.Push(current);
+ }
+
+ while (tempStack.Count > 0)
+ {
+ stack.Push(tempStack.Pop());
+ }
+
+ return found;
+ }
+
+ ///
+ /// 收集当前仍处于暂停状态的分组列表。
+ ///
+ /// 包含所有暂停中的分组的数组。
+ private PauseGroup[] CollectPausedGroups()
+ {
+ return _pauseStacks
+ .Where(kvp => kvp.Value.Count > 0)
+ .Select(kvp => kvp.Key)
+ .ToArray();
+ }
+
+ ///
+ /// 按优先级创建处理器快照,确保锁外通知仍保持确定性顺序。
+ ///
+ /// 已按优先级排序的处理器快照。
+ private IPauseHandler[] CreateHandlerSnapshot()
+ {
+ return _handlers
+ .OrderBy(handler => handler.Priority)
+ .ToArray();
+ }
+
+ ///
+ /// 统一使用给定的处理器快照派发暂停状态变化通知。
+ ///
+ /// 发生状态变化的暂停组。
+ /// 新的暂停状态。
+ /// 要通知的处理器快照。
+ /// 是否处于销毁补发路径。
+ private void NotifyHandlersSnapshot(
+ PauseGroup group,
+ bool isPaused,
+ IReadOnlyList handlersSnapshot,
+ bool isDestroying)
+ {
+ foreach (var handler in handlersSnapshot)
+ {
+ try
+ {
+ handler.OnPauseStateChanged(group, isPaused);
+ }
+ catch (Exception ex)
+ {
+ var message = isDestroying
+ ? $"Handler {handler.GetType().Name} failed during destruction"
+ : $"Handler {handler.GetType().Name} failed";
+ _logger.Error(message, ex);
+ }
+ }
+ }
+
+ ///
+ /// 在销毁路径中独立保护事件通知,避免订阅方异常中断其他分组的恢复信号。
+ ///
+ /// 需要补发恢复事件的暂停组。
+ private void RaiseDestroyStateChanged(PauseGroup group)
+ {
+ try
+ {
+ RaisePauseStateChanged(group, false);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Event subscriber failed during destruction for group {group}", ex);
+ }
+ }
+
///
/// 内部查询暂停状态的方法,不加锁。
///
@@ -467,7 +552,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
_lock.EnterReadLock();
try
{
- handlersSnapshot = _handlers.OrderBy(h => h.Priority).ToArray();
+ handlersSnapshot = CreateHandlerSnapshot();
}
finally
{
@@ -475,17 +560,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
}
// 在锁外遍历快照并通知处理器
- foreach (var handler in handlersSnapshot)
- {
- try
- {
- handler.OnPauseStateChanged(group, isPaused);
- }
- catch (Exception ex)
- {
- _logger.Error($"Handler {handler.GetType().Name} failed", ex);
- }
- }
+ NotifyHandlersSnapshot(group, isPaused, handlersSnapshot, isDestroying: false);
// 触发事件
RaisePauseStateChanged(group, isPaused);
@@ -508,4 +583,25 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
protected override void OnInit()
{
}
-}
\ No newline at end of file
+
+ ///
+ /// 锁内采集的销毁快照,供锁外补发恢复通知使用。
+ ///
+ /// 销毁前仍处于暂停状态的分组。
+ /// 按优先级排序后的处理器快照。
+ private readonly record struct DestroySnapshot(PauseGroup[] PausedGroups, IPauseHandler[] HandlersSnapshot);
+
+ ///
+ /// Pop 操作的锁内结果快照。
+ ///
+ /// 是否成功移除了目标令牌。
+ /// 是否需要在锁外发出恢复通知。
+ /// 需要通知的暂停组。
+ private readonly record struct PopResult(bool Found, bool ShouldNotify, PauseGroup NotifyGroup)
+ {
+ ///
+ /// 表示未找到目标令牌时的默认结果。
+ ///
+ public static PopResult NotFound { get; } = new(false, false, PauseGroup.Global);
+ }
+}
diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs
index c6fd3909..1bef8ef0 100644
--- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs
+++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs
@@ -1,6 +1,7 @@
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
+using System.Diagnostics.CodeAnalysis;
using System.Reflection.Emit;
namespace GFramework.Cqrs.Internal;
@@ -88,63 +89,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 +138,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,
+ [NotNullWhen(true)] 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 +333,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 +374,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..2e7b710f 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,36 @@
## 当前恢复点
-- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-001`
-- 当前阶段:`Phase 1`
+- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-005`
+- 当前阶段:`Phase 5`
- 当前焦点:
- - 已将旧 `local-plan/` 迁入 `ai-plan/public/analyzer-warning-reduction/`,active 入口只保留当前恢复信息
- - 基于现有剩余热点,评估 `MA0051`、`MA0048`、`MA0046` 与少量 `MA0016` 是否适合继续在同一主线上处理
- - 若继续推进,优先选择不引入 API rename、公共契约漂移或 Godot 宿主不稳定测试的切入点
+ - 已完成 `GFramework.Core/Pause/PauseStackManager.cs` 的 `MA0051` 收口:将 `DestroyAsync` 与 `Pop` 拆分为锁内状态迁移、
+ 栈调整和锁外通知三个阶段,同时保持日志、事件与销毁补发语义不变
+ - 已为销毁路径补充 `PauseStackManagerTests.DestroyAsync_Should_NotifyResumedGroups` 回归测试,覆盖“销毁时向所有仍暂停组补发恢复通知”
+ - 下一轮若继续推进,优先在 `CoroutineScheduler` 或 `Store` 的剩余 `MA0051` 中只选一个切入点,不回到已完成的
+ `PauseStackManager`
## 当前状态摘要
- 已完成 `GFramework.Core`、`GFramework.Cqrs`、`GFramework.Godot` 与部分 source generator 的低风险 warning 清理
- 已完成多轮 CodeRabbit follow-up 修复,并用定向测试与项目/解决方案构建验证了关键回归风险
-- 当前剩余 warning 已集中到长方法、文件/类型命名冲突、delegate 形状和少量公共集合抽象接口问题
+- 当前 `PauseStackManager` 的长方法 warning 已从 active 入口移除;主题内剩余 warning 主要集中在
+ `GFramework.Core/Coroutine/CoroutineScheduler.cs`、`GFramework.Core/StateManagement/Store.cs`、文件/类型命名冲突、
+ delegate 形状和少量公共集合抽象接口问题
## 当前活跃事实
- 当前主题仍是 active topic,因为剩余结构性 warning 是否继续推进尚未决策
- `RP-001` 的详细实现历史、测试记录和 warning 热点清单已归档到主题内 `archive/`
+- `RP-002` 已在不改公共契约的前提下完成 `CqrsHandlerRegistrar` 结构拆分,并通过定向 build/test 验证
+- `RP-003` 已在不改生命周期契约的前提下完成 `ArchitectureLifecycle` 初始化主流程拆分,并通过定向 build/test 验证
+- `RP-004` 已完成当前 PR review follow-up:修复 `TryCreateGeneratedRegistry` 的可空 `out` 契约并清理 trace 文档重复标题
+- `RP-005` 已在不改公共 API 的前提下完成 `PauseStackManager` 两个 `MA0051` 的结构拆分,并补充销毁通知回归测试
- 当前工作树分支 `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 +51,25 @@
## 验证说明
- `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=`
+- `RP-003` 的定向验证结果:
+ - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:Summary;WarningsOnly`
+ - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~ArchitectureLifecycleBehaviorTests -p:RestoreFallbackFolders=`
+- `RP-004` 的定向验证结果:
+ - `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)`
+- `RP-005` 的定向验证结果:
+ - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
+ - 结果:`27 Warning(s)`,`0 Error(s)`;`PauseStackManager.cs` 已不再出现在 `MA0051` 列表中
+ - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~PauseStackManagerTests -p:RestoreFallbackFolders=`
+ - 结果:`25 Passed`,`0 Failed`
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
## 下一步
1. 若要继续该主题,先读 active tracking,再按需展开历史归档中的 warning 热点与验证记录
-2. 从 `MA0051`、`MA0048`、`MA0046` 中只选一个结构性切入点继续,不要在同一轮同时扩多个风险面
+2. 优先在 `GFramework.Core/Coroutine/CoroutineScheduler.cs` 与 `GFramework.Core/StateManagement/Store.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..33fb83c8 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,82 @@
# Analyzer Warning Reduction 追踪
+## 2026-04-21 — RP-005
+
+### 阶段:PauseStackManager `MA0051` 收口(RP-005)
+
+- 按 active tracking 中“继续只选一个 `GFramework.Core` 结构性切入点”的约束,本轮选择
+ `GFramework.Core/Pause/PauseStackManager.cs`,因为该文件体量明显小于 `CoroutineScheduler` 和 `Store`,
+ 且已有稳定的 `PauseStackManagerTests` 覆盖暂停栈、跨组独立性、事件通知与并发 `Push/Pop` 行为
+- 先用 `warnings-only` 定向构建确认 `DestroyAsync` 与 `Pop` 仍分别命中 `MA0051`,再把逻辑拆分为:
+ - `TryBeginDestroy`
+ - `NotifyDestroyedGroups`
+ - `TryPopEntry`
+ - `RemoveEntryFromStack`
+- 额外抽出 `CreateHandlerSnapshot` 与 `NotifyHandlersSnapshot`,统一普通通知与销毁补发路径的处理器排序和异常日志,
+ 保持原有“锁内采集快照、锁外调用处理器与事件”的并发策略不变
+- 为销毁路径新增 `DestroyAsync_Should_NotifyResumedGroups`,验证当多个暂停组在销毁前仍为暂停态时,
+ 处理器和事件订阅者都会收到 `IsPaused=false` 的恢复信号
+- 验证通过:
+ - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
+ - 结果:`27 Warning(s)`,`0 Error(s)`;`PauseStackManager.cs` 已不再出现在 `MA0051` 列表
+ - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~PauseStackManagerTests -p:RestoreFallbackFolders=`
+ - 结果:`25 Passed`,`0 Failed`
+- 下一步保持原节奏:只在 `CoroutineScheduler` 或 `Store` 中二选一继续,不与其他 warning 家族混做
+
+## 2026-04-21 — RP-003
+
+### 阶段:Architecture 生命周期 `MA0051` 收口(RP-003)
+
+- 依据 active tracking 中“继续只选一个 `GFramework.Core` 结构性切入点”的约束,选定
+ `GFramework.Core/Architectures/ArchitectureLifecycle.cs`,因为文件体量适中且已有
+ `ArchitectureLifecycleBehaviorTests` 覆盖阶段流转、销毁顺序和 late registration 行为
+- 先用 `warnings-only` 定向构建确认 `ArchitectureLifecycle.InitializeAllComponentsAsync` 仍在报
+ `MA0051`,随后把主流程拆成:
+ - `CreateInitializationPlan`
+ - `InitializePhaseComponentsAsync`
+ - `MarkInitializationCompleted`
+- 保持原有阶段顺序 `Before* -> After*`、批量日志文本和异步初始化策略不变,只压缩主方法长度
+- 修正新增 `InitializationPlan` 记录类型的 XML `` 名称大小写,避免引入文档告警
+- 验证通过:
+ - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:Summary;WarningsOnly`
+ - 结果:`29 Warning(s)`,`0 Error(s)`;`ArchitectureLifecycle.cs` 已不再出现在 warning 列表
+ - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~ArchitectureLifecycleBehaviorTests -p:RestoreFallbackFolders=`
+ - 结果:`6 Passed`,`0 Failed`
+
+## 2026-04-21 — RP-004
+
+### 阶段:PR review follow-up(RP-004)
+
+- 使用 `gframework-pr-review` 抓取当前分支 PR #263 的最新 CodeRabbit review threads、MegaLinter 摘要与 CTRF 测试结果,
+ 只接受仍能在本地工作树复现的 review 点
+- 在 `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs` 中将 `TryCreateGeneratedRegistry` 的 `out` 参数改为
+ `[NotNullWhen(true)] out ICqrsHandlerRegistry?`,移除三处 `null!` 抑制,保持激活失败时的日志文本与回退语义不变
+- 修正 active trace 中重复的 `## 2026-04-21` 二级标题,消除 CodeRabbit 报告的 markdownlint `MD024`
+- 核实 PR 信号后确认:当前 CTRF 报告为 `2134 passed / 0 failed`;MegaLinter 唯一告警来自 CI 环境中的 `dotnet-format`
+ restore 失败,不是本地代码格式问题
+- 验证通过:
+ - `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)`
+
+## 2026-04-21 — RP-002
+
+### 阶段: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 +105,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=`,避免把环境问题误判成代码回归