From ff04a4fbad9f44952db996adb44d0c7bcf8f594b Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 7 May 2026 10:03:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(core):=20=E8=A1=A5=E9=BD=90=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E9=94=80=E6=AF=81=E5=90=8E=E7=9A=84=E4=B8=8A=E4=B8=8B?= =?UTF-8?q?=E6=96=87=E8=A7=A3=E7=BB=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 Architecture 销毁后 GameContext 仍保留活动上下文的问题 - 补充生命周期回归测试并验证失败初始化后的解绑路径 - 收口生成器文档中的多架构表述并更新 ai-plan 追踪 --- .../ArchitectureLifecycleBehaviorTests.cs | 63 +++++++++++++++++++ GFramework.Core/Architectures/Architecture.cs | 11 +++- .../todos/single-context-priority-tracking.md | 8 ++- .../traces/single-context-priority-trace.md | 14 ++++- .../context-get-generator.md | 8 +-- 5 files changed, 97 insertions(+), 7 deletions(-) diff --git a/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs index cae82837..e5eb0e1f 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs @@ -6,11 +6,13 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Enums; using GFramework.Core.Abstractions.Lifecycle; using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Abstractions.Rule; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; using GFramework.Core.Architectures; using GFramework.Core.Logging; +using GFramework.Core.Rule; namespace GFramework.Core.Tests.Architectures; @@ -181,6 +183,60 @@ public class ArchitectureLifecycleBehaviorTests })); } + /// + /// 验证架构销毁后会解除全局 GameContext 绑定。 + /// 该回归测试用于防止已销毁架构继续充当默认上下文回退入口。 + /// + [Test] + public async Task DestroyAsync_Should_Unbind_Context_From_GameContext() + { + var architecture = new PhaseTrackingArchitecture(); + + await architecture.InitializeAsync(); + + Assert.That(GameContext.GetByType(architecture.GetType()), Is.SameAs(architecture.Context)); + + await architecture.DestroyAsync(); + + Assert.Throws(() => GameContext.GetByType(architecture.GetType())); + Assert.Throws(() => GameContext.GetFirstArchitectureContext()); + } + + /// + /// 验证失败初始化后的销毁同样会解除全局上下文绑定。 + /// + [Test] + public async Task DestroyAsync_After_FailedInitialization_Should_Unbind_Context_From_GameContext() + { + var destroyOrder = new List(); + var architecture = new FailingInitializationArchitecture(destroyOrder); + + var exception = Assert.ThrowsAsync(() => architecture.InitializeAsync()); + Assert.That(exception, Is.Not.Null); + Assert.That(GameContext.GetByType(architecture.GetType()), Is.SameAs(architecture.Context)); + + await architecture.DestroyAsync(); + + Assert.Throws(() => GameContext.GetByType(architecture.GetType())); + Assert.Throws(() => GameContext.GetFirstArchitectureContext()); + } + + /// + /// 验证销毁后的新 ContextAware 实例不会再通过全局回退命中过期上下文。 + /// + [Test] + public async Task DestroyAsync_Should_Prevent_New_ContextAware_Fallback_From_Using_Destroyed_Context() + { + var architecture = new PhaseTrackingArchitecture(); + + await architecture.InitializeAsync(); + await architecture.DestroyAsync(); + + IContextAware probe = new LifecycleContextAwareProbe(); + + Assert.Throws(() => probe.GetContext()); + } + /// /// 验证启用 AllowLateRegistration 时,生命周期层会立即初始化后注册的组件,而不是继续沿用初始化期的拒绝策略。 /// 由于公共架构 API 在 Ready 之后会先触发容器限制,此回归测试直接覆盖生命周期协作者的对齐逻辑。 @@ -232,6 +288,13 @@ public class ArchitectureLifecycleBehaviorTests } } + /// + /// 仅用于验证销毁后全局上下文回退是否仍然泄漏的最小 ContextAware 探针。 + /// + private sealed class LifecycleContextAwareProbe : ContextAwareBase + { + } + /// /// 在初始化时注册可销毁组件的测试架构。 /// diff --git a/GFramework.Core/Architectures/Architecture.cs b/GFramework.Core/Architectures/Architecture.cs index 8bb71c62..7fa66b1d 100644 --- a/GFramework.Core/Architectures/Architecture.cs +++ b/GFramework.Core/Architectures/Architecture.cs @@ -361,7 +361,16 @@ public abstract class Architecture : IArchitecture /// public virtual async ValueTask DestroyAsync() { - await _lifecycle.DestroyAsync().ConfigureAwait(false); + try + { + await _lifecycle.DestroyAsync().ConfigureAwait(false); + } + finally + { + // 架构初始化时会把当前实例绑定到 GameContext;销毁后必须解除该全局回退入口, + // 避免后续惰性 ContextAware 调用继续命中过期的运行时上下文。 + GameContext.Unbind(GetType()); + } } /// diff --git a/ai-plan/public/single-context-priority/todos/single-context-priority-tracking.md b/ai-plan/public/single-context-priority/todos/single-context-priority-tracking.md index 623a6c4e..b25cc74f 100644 --- a/ai-plan/public/single-context-priority/todos/single-context-priority-tracking.md +++ b/ai-plan/public/single-context-priority/todos/single-context-priority-tracking.md @@ -16,6 +16,7 @@ - `GameContext` 已从“字典首个枚举值即默认上下文”收敛为“单活动上下文 + 类型别名兼容查找”;同一全局上下文表不再允许并存两个不同上下文实例 - `MicrosoftDiContainer` 的预冻结 `Get()` / `Get(Type)` 已改为复用实例可见性收集逻辑,和 `GetAll*` 的实例暴露规则保持一致 - `IIocContainer` XML 文档已明确预冻结查询与 `Contains()` 的契约边界,避免把注册阶段查询误读为完整 DI 激活语义 + - `Architecture.DestroyAsync()` 现会在生命周期销毁完成后显式解除 `GameContext` 绑定,防止已销毁架构继续充当默认上下文回退入口 - 当前分支从 `main` 创建,已完成 `git pull --ff-only origin main` ## 当前活跃事实 @@ -33,6 +34,7 @@ - `GameContext` 是公开静态入口,任何“允许多个不同上下文并存”的现有测试都需要按单活动上下文语义重写 - `Contains()` 在预冻结阶段目前更接近“是否存在注册”,不等同于“是否能立即解析实例”;本轮若不改其行为,需要在文档和测试中明确这一点 - `ResolveCqrsRegistrationService()` 仍要求注册阶段对 `ICqrsRegistrationService` 可见的是实例绑定;若后续改成工厂或实现类型注册,需要额外设计注册阶段激活 helper +- 现有解绑逻辑通过 `Architecture.GetType()` 移除初始化期绑定;若后续引入更多显式上下文别名,需同步评估是否要在销毁时额外移除这些别名 ## 最近权威验证 @@ -46,8 +48,12 @@ - 结果:通过,`92/92` passed - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` - 结果:通过,`0 warning / 0 error` +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureLifecycleBehaviorTests|FullyQualifiedName~SyncArchitectureTests|FullyQualifiedName~AsyncArchitectureTests|FullyQualifiedName~ArchitectureInitializationPipelineTests|FullyQualifiedName~ContextAwareTests"` + - 结果:通过,`32/32` passed +- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` + - 结果:再次通过,`0 warning / 0 error` ## 下一推荐步骤 -1. 若后续继续推进,可评估 `Architecture` 销毁阶段是否要显式解绑 `GameContext` 当前活动上下文 +1. 若后续继续推进,可评估是否要把 `GameContext.ArchitectureReadOnlyDictionary` 标记为兼容层,并收口其公开使用面 2. 若 CQRS runtime seam 计划改成工厂式注册,再单独补“注册阶段激活 helper”的设计与测试 diff --git a/ai-plan/public/single-context-priority/traces/single-context-priority-trace.md b/ai-plan/public/single-context-priority/traces/single-context-priority-trace.md index 6ae13dcd..44273c0b 100644 --- a/ai-plan/public/single-context-priority/traces/single-context-priority-trace.md +++ b/ai-plan/public/single-context-priority/traces/single-context-priority-trace.md @@ -27,6 +27,18 @@ - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~GameContextTests|FullyQualifiedName~ContextProviderTests|FullyQualifiedName~ContextAwareTests|FullyQualifiedName~MicrosoftDiContainerTests|FullyQualifiedName~IocContainerLifetimeTests|FullyQualifiedName~ArchitectureInitializationPipelineTests"` 通过,`92/92` passed - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` 通过,`0 warning / 0 error` +### 阶段:销毁闭环与文档收口完成(SINGLE-CONTEXT-PRIORITY-RP-002) + +- 实现摘要: + - `Architecture.DestroyAsync()` 新增 `finally` 解绑,确保销毁完成后自动从 `GameContext` 移除当前架构类型绑定 + - `ArchitectureLifecycleBehaviorTests` 新增销毁解绑、失败初始化后解绑、以及销毁后新 `ContextAwareBase` 实例不再回退到过期上下文的回归测试 + - `docs/zh-CN/source-generators/context-get-generator.md` 已把“多架构场景”改写为“自定义上下文来源”,收口对全局多架构并存的暗示 +- 测试与验证: + - `python3 scripts/license-header.py --check` 通过 + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureLifecycleBehaviorTests|FullyQualifiedName~SyncArchitectureTests|FullyQualifiedName~AsyncArchitectureTests|FullyQualifiedName~ArchitectureInitializationPipelineTests|FullyQualifiedName~ContextAwareTests"` 通过,`32/32` passed + - 首次并发执行 `dotnet test` 与 `dotnet build` 时出现 `bin/Release` 文件占用导致的 MSBuild copy 冲突;按仓库规则改为单独重跑直接命令后结果通过 + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` 单独重跑通过,`0 warning / 0 error` + ### 当前下一步 -1. 若后续要进一步彻底移除全局回退,可单独评估 `Architecture` 销毁解绑与 `GameContext` 公开别名字典的收口策略 +1. 若后续要进一步彻底移除全局回退,可单独评估 `GameContext` 公开别名字典的收口策略与生成器默认 provider 的进一步简化空间 diff --git a/docs/zh-CN/source-generators/context-get-generator.md b/docs/zh-CN/source-generators/context-get-generator.md index b6c62237..1f6d347d 100644 --- a/docs/zh-CN/source-generators/context-get-generator.md +++ b/docs/zh-CN/source-generators/context-get-generator.md @@ -651,7 +651,7 @@ public partial class GameController - 运行时条件分支控制的注册 - 反射、配置驱动或外部程序集动态注册 -- 无法唯一判定组件归属架构的多架构场景 +- 需要自定义 provider 或外部切换逻辑才能判定上下文来源的场景 ## 高级场景 @@ -685,9 +685,9 @@ public partial class Controller } ``` -### 多架构场景 +### 自定义上下文来源 -在多架构场景中,可以通过 `SetContextProvider` 切换架构: +当默认的当前活动上下文不适用时,可以通过 `SetContextProvider` 显式切换上下文来源: ```csharp [ContextAware] @@ -698,7 +698,7 @@ public partial class GameController public static void SetArchitecture(IArchitecture architecture) { - // 切换架构提供者 + // 显式切换当前类型使用的上下文来源 SetContextProvider(new CustomContextProvider(architecture)); } }