From e3fa0db9928b4886559674a035d82cc7c38a233f Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 7 May 2026 08:58:09 +0800 Subject: [PATCH 1/3] =?UTF-8?q?refactor(core):=20=E6=94=B6=E6=95=9B?= =?UTF-8?q?=E5=8D=95=E6=B4=BB=E5=8A=A8=E4=B8=8A=E4=B8=8B=E6=96=87=E4=B8=8E?= =?UTF-8?q?=E9=A2=84=E5=86=BB=E7=BB=93=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 收敛 GameContext 为单活动上下文模型并保留类型别名兼容查找 - 统一 MicrosoftDiContainer 预冻结实例读取路径并补充 CQRS 注册阶段提示 - 更新 Core 测试、上下文文档与 ai-plan 追踪记录 --- .../Ioc/IIocContainer.cs | 17 ++++ .../Architectures/ContextProviderTests.cs | 12 +-- .../Architectures/GameContextTests.cs | 89 +++++++++++++------ .../Ioc/MicrosoftDiContainerTests.cs | 66 ++++++++++++++ .../Rule/ContextAwareTests.cs | 15 +--- GFramework.Core/Architectures/GameContext.cs | 68 +++++++++++--- .../Architectures/GameContextProvider.cs | 10 ++- GFramework.Core/Ioc/MicrosoftDiContainer.cs | 32 +++---- GFramework.Core/Rule/ContextAwareBase.cs | 2 +- ai-plan/public/README.md | 6 ++ .../todos/single-context-priority-tracking.md | 53 +++++++++++ .../traces/single-context-priority-trace.md | 32 +++++++ docs/zh-CN/core/context.md | 2 +- docs/zh-CN/core/rule.md | 4 +- .../context-aware-generator.md | 2 +- 15 files changed, 326 insertions(+), 84 deletions(-) create mode 100644 ai-plan/public/single-context-priority/todos/single-context-priority-tracking.md create mode 100644 ai-plan/public/single-context-priority/traces/single-context-priority-trace.md diff --git a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs index 9db0bd3e..44ed3b29 100644 --- a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs +++ b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs @@ -141,6 +141,10 @@ public interface IIocContainer : IContextAware, IDisposable /// /// 期望获取的实例类型 /// 找到的第一个实例;如果未找到则返回 null + /// + /// 在 之前,该查询只保证返回已经物化为实例绑定的服务。 + /// 仅通过工厂或实现类型注册的服务在预冻结阶段可能不可见;若需要完整激活语义,请先冻结容器。 + /// T? Get() where T : class; /// @@ -149,6 +153,10 @@ public interface IIocContainer : IContextAware, IDisposable /// /// 期望获取的实例类型 /// 找到的第一个实例;如果未找到则返回 null + /// + /// 在 之前,该查询只保证返回已经物化为实例绑定的服务。 + /// 仅通过工厂或实现类型注册的服务在预冻结阶段可能不可见;若需要完整激活语义,请先冻结容器。 + /// object? Get(Type type); @@ -174,6 +182,9 @@ public interface IIocContainer : IContextAware, IDisposable /// /// 期望获取的实例类型 /// 所有符合条件的实例列表;如果没有则返回空数组 + /// + /// 在 之前,该查询只会枚举当前已经可见的实例绑定,不会主动执行工厂或创建实现类型。 + /// IReadOnlyList GetAll() where T : class; /// @@ -181,6 +192,9 @@ public interface IIocContainer : IContextAware, IDisposable /// /// 期望获取的实例类型 /// 所有符合条件的实例列表;如果没有则返回空数组 + /// + /// 在 之前,该查询只会枚举当前已经可见的实例绑定,不会主动执行工厂或创建实现类型。 + /// IReadOnlyList GetAll(Type type); @@ -219,6 +233,9 @@ public interface IIocContainer : IContextAware, IDisposable /// /// 要检查的类型 /// 如果容器中包含指定类型的实例则返回true,否则返回false + /// + /// 在 之前,该方法更接近“是否存在对应注册”的检查,而不是完整的 DI 可解析性判断。 + /// bool Contains() where T : class; /// diff --git a/GFramework.Core.Tests/Architectures/ContextProviderTests.cs b/GFramework.Core.Tests/Architectures/ContextProviderTests.cs index bcd9e64c..94bacfbf 100644 --- a/GFramework.Core.Tests/Architectures/ContextProviderTests.cs +++ b/GFramework.Core.Tests/Architectures/ContextProviderTests.cs @@ -9,7 +9,7 @@ namespace GFramework.Core.Tests.Architectures; /// /// ContextProvider 相关类的单元测试 /// 测试内容包括: -/// - GameContextProvider 获取第一个架构上下文 +/// - GameContextProvider 获取当前活动架构上下文 /// - GameContextProvider 尝试获取指定类型的上下文 /// - ScopedContextProvider 获取绑定的上下文 /// - ScopedContextProvider 尝试获取指定类型的上下文 @@ -37,10 +37,10 @@ public class ContextProviderTests } /// - /// 测试 GameContextProvider 是否能正确获取第一个架构上下文 + /// 测试 GameContextProvider 是否能正确获取当前活动架构上下文 /// [Test] - public void GameContextProvider_GetContext_Should_Return_First_Context() + public void GameContextProvider_GetContext_Should_Return_Current_Context() { var context = new TestArchitectureContext(); GameContext.Bind(typeof(TestArchitecture), context); @@ -63,13 +63,13 @@ public class ContextProviderTests } /// - /// 测试 GameContextProvider 的 TryGetContext 方法在找到上下文时返回 true + /// 测试 GameContextProvider 的 TryGetContext 方法在仅绑定架构类型时也能返回 true /// [Test] - public void GameContextProvider_TryGetContext_Should_Return_True_When_Found() + public void GameContextProvider_TryGetContext_Should_Return_True_When_Current_Context_Matches() { var context = new TestArchitectureContext(); - GameContext.Bind(typeof(TestArchitectureContext), context); + GameContext.Bind(typeof(TestArchitecture), context); var provider = new GameContextProvider(); var result = provider.TryGetContext(out var foundContext); diff --git a/GFramework.Core.Tests/Architectures/GameContextTests.cs b/GFramework.Core.Tests/Architectures/GameContextTests.cs index 6005742b..6e3715b2 100644 --- a/GFramework.Core.Tests/Architectures/GameContextTests.cs +++ b/GFramework.Core.Tests/Architectures/GameContextTests.cs @@ -6,20 +6,12 @@ using GFramework.Core.Architectures; namespace GFramework.Core.Tests.Architectures; /// -/// GameContext类的单元测试 +/// GameContext 类的单元测试 /// 测试内容包括: -/// - ArchitectureReadOnlyDictionary在启动时为空 -/// - Bind方法添加上下文到字典 -/// - Bind重复类型时抛出异常 -/// - GetByType返回正确的上下文 -/// - GetByType未找到时抛出异常 -/// - Get泛型方法返回正确的上下文 -/// - TryGet方法在找到时返回true -/// - TryGet方法在未找到时返回false -/// - GetFirstArchitectureContext在存在时返回 -/// - GetFirstArchitectureContext为空时抛出异常 -/// - Unbind移除上下文 -/// - Clear移除所有上下文 +/// - 初始状态为空 +/// - 绑定后可通过架构类型和上下文类型回查 +/// - 不允许并存绑定两个不同上下文实例 +/// - 清理和解绑会同步更新当前活动上下文 /// [TestFixture] public class GameContextTests @@ -81,6 +73,21 @@ public class GameContextTests GameContext.Bind(typeof(TestArchitecture), context2)); } + /// + /// 测试绑定第二个不同的上下文实例时会被拒绝。 + /// + [Test] + public void Bind_WithDifferentContextInstance_Should_ThrowInvalidOperationException() + { + var firstContext = new TestArchitectureContext(); + var secondContext = new TestArchitectureContext(); + + GameContext.Bind(typeof(TestArchitecture), firstContext); + + Assert.Throws(() => + GameContext.Bind(typeof(AnotherTestArchitectureContext), secondContext)); + } + /// /// 测试GetByType方法是否返回正确的上下文 /// @@ -106,13 +113,27 @@ public class GameContextTests } /// - /// 测试Get泛型方法是否返回正确的上下文 + /// 测试 GetByType 支持按当前活动上下文的具体类型回查。 /// [Test] - public void GetGeneric_Should_Return_Correct_Context() + public void GetByType_Should_Return_Current_Context_When_Requested_By_Context_Type() { var context = new TestArchitectureContext(); - GameContext.Bind(typeof(TestArchitectureContext), context); + GameContext.Bind(typeof(TestArchitecture), context); + + var result = GameContext.GetByType(typeof(TestArchitectureContext)); + + Assert.That(result, Is.SameAs(context)); + } + + /// + /// 测试 Get 泛型方法在仅绑定架构类型时也能返回当前上下文 + /// + [Test] + public void GetGeneric_Should_Return_Current_Context_When_Bound_By_Architecture_Type() + { + var context = new TestArchitectureContext(); + GameContext.Bind(typeof(TestArchitecture), context); var result = GameContext.Get(); @@ -120,13 +141,13 @@ public class GameContextTests } /// - /// 测试TryGet方法在找到上下文时是否返回true并正确设置输出参数 + /// 测试 TryGet 方法在仅绑定架构类型时也能找到当前上下文 /// [Test] - public void TryGet_Should_ReturnTrue_When_Found() + public void TryGet_Should_ReturnTrue_When_Bound_By_Architecture_Type() { var context = new TestArchitectureContext(); - GameContext.Bind(typeof(TestArchitectureContext), context); + GameContext.Bind(typeof(TestArchitecture), context); var result = GameContext.TryGet(out TestArchitectureContext? foundContext); @@ -135,7 +156,7 @@ public class GameContextTests } /// - /// 测试TryGet方法在未找到上下文时是否返回false且输出参数为null + /// 测试 TryGet 方法在未找到上下文时是否返回 false 且输出参数为 null /// [Test] public void TryGet_Should_ReturnFalse_When_Not_Found() @@ -171,10 +192,10 @@ public class GameContextTests } /// - /// 测试Unbind方法是否正确移除指定类型的上下文 + /// 测试 Unbind 方法在移除最后一个别名时会清空当前活动上下文 /// [Test] - public void Unbind_Should_Remove_Context() + public void Unbind_Should_Remove_Context_When_Last_Alias_Is_Removed() { var context = new TestArchitectureContext(); GameContext.Bind(typeof(TestArchitecture), context); @@ -185,16 +206,34 @@ public class GameContextTests } /// - /// 测试Clear方法是否正确移除所有上下文 + /// 测试 Unbind 方法在仍有其他别名时保留当前活动上下文 + /// + [Test] + public void Unbind_Should_Keep_Current_Context_When_Another_Alias_Remains() + { + var context = new TestArchitectureContext(); + GameContext.Bind(typeof(TestArchitecture), context); + GameContext.Bind(typeof(TestArchitectureContext), context); + + GameContext.Unbind(typeof(TestArchitecture)); + + Assert.That(GameContext.GetFirstArchitectureContext(), Is.SameAs(context)); + Assert.That(GameContext.ArchitectureReadOnlyDictionary.Count, Is.EqualTo(1)); + } + + /// + /// 测试 Clear 方法是否正确移除所有上下文 /// [Test] public void Clear_Should_Remove_All_Contexts() { - GameContext.Bind(typeof(TestArchitecture), new TestArchitectureContext()); - GameContext.Bind(typeof(TestArchitectureContext), new TestArchitectureContext()); + var context = new TestArchitectureContext(); + GameContext.Bind(typeof(TestArchitecture), context); + GameContext.Bind(typeof(TestArchitectureContext), context); GameContext.Clear(); Assert.That(GameContext.ArchitectureReadOnlyDictionary.Count, Is.EqualTo(0)); + Assert.Throws(() => GameContext.GetFirstArchitectureContext()); } } diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index a2627a06..b1c5e336 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -158,6 +158,21 @@ public class MicrosoftDiContainerTests Assert.That(result, Is.SameAs(instance)); } + /// + /// 测试预冻结阶段通过 RegisterPlurality 注册的接口别名仍可通过 Get 解析到同一实例。 + /// + [Test] + public void Get_Should_Return_RegisterPlurality_Interface_Instance_Before_Freeze() + { + var instance = new TestService(); + + _container.RegisterPlurality(instance); + + var result = _container.Get(); + + Assert.That(result, Is.SameAs(instance)); + } + /// /// 测试当 CQRS 基础设施已手动接线后,再调用处理器注册入口不会重复注册 runtime seam。 /// @@ -278,6 +293,32 @@ public class MicrosoftDiContainerTests Assert.That(results.Count, Is.EqualTo(0)); } + /// + /// 测试预冻结阶段通过实现类型注册的服务不会被当作已物化实例返回。 + /// + [Test] + public void Get_Should_Return_Null_PreFreeze_For_ImplementationType_Registration() + { + _container.RegisterSingleton(); + + var result = _container.Get(); + + Assert.That(result, Is.Null); + } + + /// + /// 测试预冻结阶段通过实现类型注册的服务在 GetAll 中同样不可见。 + /// + [Test] + public void GetAll_Should_Return_Empty_PreFreeze_For_ImplementationType_Registration() + { + _container.RegisterSingleton(); + + var results = _container.GetAll(); + + Assert.That(results, Is.Empty); + } + /// /// 测试容器未冻结时,会折叠“不同服务类型指向同一实例”的兼容别名重复, /// 但会保留同一服务类型的重复显式注册。 @@ -353,6 +394,21 @@ public class MicrosoftDiContainerTests Assert.That(_container.Get(), Is.SameAs(instance)); } + /// + /// 测试预冻结阶段通过 RegisterPlurality 注册的接口别名对 Contains 与 GetAll 都可见。 + /// + [Test] + public void Contains_Should_Return_True_For_RegisterPlurality_Interface_Alias_Before_Freeze() + { + var instance = new TestService(); + _container.RegisterPlurality(instance); + + var services = _container.GetAll(); + + Assert.That(services, Has.Count.EqualTo(1)); + Assert.That(_container.Contains(), Is.True); + } + /// /// 测试当不存在实例时检查包含关系应返回 false 的功能 @@ -430,6 +486,16 @@ public class MicrosoftDiContainerTests Is.True); } + /// + /// 测试 RegisterCqrsHandlersFromAssemblies 会通过注册阶段可见实例解析 CQRS 注册服务。 + /// + [Test] + public void RegisterCqrsHandlersFromAssemblies_Should_Resolve_Registration_Service_When_Registered_As_Instance() + { + Assert.DoesNotThrow(() => + _container.RegisterCqrsHandlersFromAssemblies([typeof(DeterministicOrderNotification).Assembly])); + } + /// /// 测试当程序集集合中包含空元素时,CQRS handler 注册入口会在委托给注册服务前直接失败。 /// diff --git a/GFramework.Core.Tests/Rule/ContextAwareTests.cs b/GFramework.Core.Tests/Rule/ContextAwareTests.cs index eba932f9..dd103f2b 100644 --- a/GFramework.Core.Tests/Rule/ContextAwareTests.cs +++ b/GFramework.Core.Tests/Rule/ContextAwareTests.cs @@ -83,22 +83,15 @@ public class ContextAwareTests } /// - /// 测试 GetContext 方法在未设置上下文时的行为 - /// 验证当内部 Context 为 null 时,GetContext 方法不会抛出异常 - /// 此时应返回第一个架构上下文(在测试环境中验证不抛出异常即可) + /// 测试 GetContext 方法在未设置上下文时会回退到当前活动上下文 /// [Test] - public void GetContext_Should_Return_FirstArchitectureContext_When_Not_Set() + public void GetContext_Should_Return_CurrentArchitectureContext_When_Not_Set() { - // Arrange - 暂时不调用 SetContext,让 Context 为 null IContextAware aware = _contextAware; - // Act - 当 Context 为 null 时,应该返回第一个 Architecture Context - // 由于测试环境中没有实际的 Architecture Context,这里只测试调用不会抛出异常 - // 在实际使用中,当 Context 为 null 时会调用 GameContext.GetFirstArchitectureContext() + var result = aware.GetContext(); - // Assert - 验证在没有设置 Context 时的行为 - // 注意:由于测试环境中可能没有 Architecture Context,这里我们只测试不抛出异常 - Assert.DoesNotThrow(() => aware.GetContext()); + Assert.That(result, Is.SameAs(_mockContext)); } } diff --git a/GFramework.Core/Architectures/GameContext.cs b/GFramework.Core/Architectures/GameContext.cs index 3d1e2f54..a25a07ca 100644 --- a/GFramework.Core/Architectures/GameContext.cs +++ b/GFramework.Core/Architectures/GameContext.cs @@ -2,59 +2,86 @@ // SPDX-License-Identifier: Apache-2.0 using System.Collections.Concurrent; +using System.Threading; using GFramework.Core.Abstractions.Architectures; namespace GFramework.Core.Architectures; /// -/// 游戏上下文管理类,用于管理当前的架构上下文实例 +/// 游戏上下文管理类,用于管理当前活动的架构上下文实例及其兼容类型别名。 /// public static class GameContext { private static readonly ConcurrentDictionary ArchitectureDictionary = new(); + private static IArchitectureContext? _currentArchitectureContext; /// - /// 获取所有已注册的架构上下文的只读字典 + /// 获取所有已注册的架构上下文类型别名映射。 + /// 该只读视图会反映当前并发状态,不保证是稳定快照。 /// public static IReadOnlyDictionary ArchitectureReadOnlyDictionary => ArchitectureDictionary; /// - /// 绑定指定类型的架构上下文到管理器中 + /// 绑定指定类型的架构上下文到管理器中。 + /// 同一时刻只允许存在一个活动上下文实例,但可以为其绑定多个兼容类型别名。 /// /// 架构类型 /// 架构上下文实例 - /// 当指定类型的架构上下文已存在时抛出 + /// + /// 。 + /// + /// 当指定类型的架构上下文已存在,或尝试绑定第二个不同上下文实例时抛出。 public static void Bind(Type architectureType, IArchitectureContext context) { + ArgumentNullException.ThrowIfNull(architectureType); + ArgumentNullException.ThrowIfNull(context); + + var currentContext = Interlocked.CompareExchange(ref _currentArchitectureContext, context, comparand: null); + if (currentContext != null && !ReferenceEquals(currentContext, context)) + throw new InvalidOperationException( + $"GameContext already tracks active context '{currentContext.GetType().Name}'. " + + $"Cannot bind a different context '{context.GetType().Name}'."); + if (!ArchitectureDictionary.TryAdd(architectureType, context)) throw new InvalidOperationException( $"Architecture context for '{architectureType.Name}' already exists"); } /// - /// 获取字典中的第一个架构上下文 + /// 获取当前活动的架构上下文。 + /// 该方法保留原有名称以兼容存量调用方,但语义已经收敛为“当前上下文”,而不是任意字典首项。 /// - /// 返回字典中的第一个架构上下文实例 - /// 当字典为空时抛出 + /// 当前活动的架构上下文实例。 + /// 当当前没有活动上下文时抛出。 public static IArchitectureContext GetFirstArchitectureContext() { - return ArchitectureDictionary.Values.First(); + if (_currentArchitectureContext is { } context) + return context; + + throw new InvalidOperationException("No active architecture context is currently bound."); } /// - /// 根据类型获取对应的架构上下文 + /// 根据类型获取对应的架构上下文。 + /// 兼容层会优先查找显式绑定的类型别名,然后回退到当前上下文的类型兼容判断。 /// /// 要查找的架构类型 /// 返回指定类型的架构上下文实例 + /// /// 当指定类型的架构上下文不存在时抛出 public static IArchitectureContext GetByType(Type type) { + ArgumentNullException.ThrowIfNull(type); + if (ArchitectureDictionary.TryGetValue(type, out var context)) return context; + if (_currentArchitectureContext != null && type.IsInstanceOfType(_currentArchitectureContext)) + return _currentArchitectureContext; + throw new InvalidOperationException( $"Architecture context for '{type.Name}' not found"); } @@ -68,6 +95,9 @@ public static class GameContext /// 当指定类型的架构上下文不存在时抛出 public static T Get() where T : class, IArchitectureContext { + if (_currentArchitectureContext is T currentContext) + return currentContext; + if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx)) return (T)ctx; @@ -84,6 +114,12 @@ public static class GameContext public static bool TryGet(out T? context) where T : class, IArchitectureContext { + if (_currentArchitectureContext is T currentContext) + { + context = currentContext; + return true; + } + if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx)) { context = (T)ctx; @@ -100,7 +136,16 @@ public static class GameContext /// 要移除的架构类型 public static void Unbind(Type architectureType) { - ArchitectureDictionary.TryRemove(architectureType, out _); + ArgumentNullException.ThrowIfNull(architectureType); + + if (!ArchitectureDictionary.TryRemove(architectureType, out var removedContext)) + return; + + if (_currentArchitectureContext == null || !ReferenceEquals(_currentArchitectureContext, removedContext)) + return; + + if (!ArchitectureDictionary.Values.Any(current => ReferenceEquals(current, removedContext))) + Interlocked.CompareExchange(ref _currentArchitectureContext, null, removedContext); } @@ -110,5 +155,6 @@ public static class GameContext public static void Clear() { ArchitectureDictionary.Clear(); + Interlocked.Exchange(ref _currentArchitectureContext, null); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Architectures/GameContextProvider.cs b/GFramework.Core/Architectures/GameContextProvider.cs index 6f82edfd..5725d00b 100644 --- a/GFramework.Core/Architectures/GameContextProvider.cs +++ b/GFramework.Core/Architectures/GameContextProvider.cs @@ -6,12 +6,13 @@ using GFramework.Core.Abstractions.Architectures; namespace GFramework.Core.Architectures; /// -/// 基于 GameContext 的默认上下文提供者 +/// 基于 GameContext 的默认上下文提供者。 +/// 默认只面向当前活动上下文工作,而不是维护多个并存的全局上下文。 /// public sealed class GameContextProvider : IArchitectureContextProvider { /// - /// 获取当前的架构上下文(返回第一个注册的架构上下文) + /// 获取当前的架构上下文。 /// /// 架构上下文实例 public IArchitectureContext GetContext() @@ -20,7 +21,8 @@ public sealed class GameContextProvider : IArchitectureContextProvider } /// - /// 尝试获取指定类型的架构上下文 + /// 尝试获取指定类型的架构上下文。 + /// 若当前活动上下文本身兼容 ,则无需显式类型别名也会返回成功。 /// /// 架构上下文类型 /// 输出的上下文实例 @@ -29,4 +31,4 @@ public sealed class GameContextProvider : IArchitectureContextProvider { return GameContext.TryGet(out context); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index ad362b76..24be6415 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -599,14 +599,17 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 未找到可用的 CQRS 程序集注册协调器实例时抛出。 private ICqrsRegistrationService ResolveCqrsRegistrationService() { - var descriptor = GetServicesUnsafe.LastOrDefault(static service => - service.ServiceType == typeof(ICqrsRegistrationService)); + var registrationService = CollectRegisteredImplementationInstances(typeof(ICqrsRegistrationService)) + .OfType() + .LastOrDefault(); - if (descriptor?.ImplementationInstance is ICqrsRegistrationService registrationService) + if (registrationService != null) return registrationService; const string errorMessage = - "ICqrsRegistrationService not registered. Ensure the CQRS runtime module has been installed before registering handlers."; + "ICqrsRegistrationService is not visible during the registration stage. Ensure the CQRS runtime module " + + "has been installed and that the registration service is pre-materialized as an instance binding before " + + "registering handlers."; _logger.Error(errorMessage); throw new InvalidOperationException(errorMessage); } @@ -625,18 +628,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) { if (_provider == null) { - // 如果容器未冻结,从服务集合中查找已注册的实例 - var serviceType = typeof(T); - var descriptor = GetServicesUnsafe.FirstOrDefault(s => - s.ServiceType == serviceType || serviceType.IsAssignableFrom(s.ServiceType)); - - if (descriptor?.ImplementationInstance is T instance) - { - return instance; - } - - // 在未冻结状态下无法调用工厂方法或创建实例,返回null - return null; + return CollectRegisteredImplementationInstances(typeof(T)).OfType().FirstOrDefault(); } var result = _provider!.GetService(); @@ -659,18 +651,14 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 服务实例或null public object? Get(Type type) { + ArgumentNullException.ThrowIfNull(type); ThrowIfDisposed(); EnterReadLockOrThrowDisposed(); try { if (_provider == null) { - // 如果容器未冻结,从服务集合中查找已注册的实例 - var descriptor = - GetServicesUnsafe.FirstOrDefault(s => - s.ServiceType == type || type.IsAssignableFrom(s.ServiceType)); - - return descriptor?.ImplementationInstance; + return CollectRegisteredImplementationInstances(type).FirstOrDefault(); } var result = _provider!.GetService(type); diff --git a/GFramework.Core/Rule/ContextAwareBase.cs b/GFramework.Core/Rule/ContextAwareBase.cs index 868583d9..970f5992 100644 --- a/GFramework.Core/Rule/ContextAwareBase.cs +++ b/GFramework.Core/Rule/ContextAwareBase.cs @@ -46,7 +46,7 @@ public abstract class ContextAwareBase : IContextAware /// /// 当前架构上下文对象。 /// - /// 当 为空时,该实现会直接回退到 。 + /// 当 为空时,该实现会直接回退到 返回的当前活动上下文。 /// 该回退过程不执行额外同步,也不支持替换 provider;如需这些能力,请改用生成的 ContextAware 实现。 /// IArchitectureContext IContextAware.GetContext() diff --git a/ai-plan/public/README.md b/ai-plan/public/README.md index 4ee6c551..d264cbd9 100644 --- a/ai-plan/public/README.md +++ b/ai-plan/public/README.md @@ -42,6 +42,10 @@ help the current worktree land on the right recovery documents without scanning - Purpose: track `PR #330` disposal-contract fixes for `MicrosoftDiContainer`, related benchmark cleanup hardening, and review follow-up. - Tracking: `ai-plan/public/microsoft-di-container-disposal/todos/microsoft-di-container-disposal-tracking.md` - Trace: `ai-plan/public/microsoft-di-container-disposal/traces/microsoft-di-container-disposal-trace.md` +- `single-context-priority` + - Purpose: converge `GameContext` toward a single active architecture context model and tighten related `MicrosoftDiContainer` pre-freeze lookup semantics. + - Tracking: `ai-plan/public/single-context-priority/todos/single-context-priority-tracking.md` + - Trace: `ai-plan/public/single-context-priority/traces/single-context-priority-trace.md` ## Worktree To Active Topic Map @@ -64,3 +68,5 @@ help the current worktree land on the right recovery documents without scanning - Priority 1: `documentation-full-coverage-governance` - Branch: `fix/microsoft-di-container-disposal` - Priority 1: `microsoft-di-container-disposal` +- Branch: `refactor/single-context-priority` + - Priority 1: `single-context-priority` 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 new file mode 100644 index 00000000..623a6c4e --- /dev/null +++ b/ai-plan/public/single-context-priority/todos/single-context-priority-tracking.md @@ -0,0 +1,53 @@ +# Single Context Priority 跟踪 + +## 目标 + +围绕 `GameContext` 与 `MicrosoftDiContainer` 收敛当前运行时语义: + +- 把 `GameContext` 从弱约束的多上下文字典收敛为“单活动上下文 + 兼容别名查找”模型 +- 保留按架构类型查找的兼容入口,同时禁止在同一全局上下文表中并存多个不同的架构上下文实例 +- 统一 `MicrosoftDiContainer` 预冻结阶段的单实例读取路径,减少 `RegisterPlurality` / CQRS 基础设施别名注册下的查询歧义 + +## 当前恢复点 + +- 恢复点编号:`SINGLE-CONTEXT-PRIORITY-RP-001` +- 当前阶段:`Phase 2` +- 当前结论: + - `GameContext` 已从“字典首个枚举值即默认上下文”收敛为“单活动上下文 + 类型别名兼容查找”;同一全局上下文表不再允许并存两个不同上下文实例 + - `MicrosoftDiContainer` 的预冻结 `Get()` / `Get(Type)` 已改为复用实例可见性收集逻辑,和 `GetAll*` 的实例暴露规则保持一致 + - `IIocContainer` XML 文档已明确预冻结查询与 `Contains()` 的契约边界,避免把注册阶段查询误读为完整 DI 激活语义 + - 当前分支从 `main` 创建,已完成 `git pull --ff-only origin main` + +## 当前活跃事实 + +- 当前分支:`refactor/single-context-priority` +- 当前预期改动面: + - `GFramework.Core/Architectures/GameContext.cs` + - `GFramework.Core/Rule/ContextAwareBase.cs` + - `GFramework.Core/Ioc/MicrosoftDiContainer.cs` + - `GFramework.Core.Abstractions/Ioc/IIocContainer.cs` + - 相关 `GFramework.Core.Tests` 与必要文档页 + +## 当前风险 + +- `GameContext` 是公开静态入口,任何“允许多个不同上下文并存”的现有测试都需要按单活动上下文语义重写 +- `Contains()` 在预冻结阶段目前更接近“是否存在注册”,不等同于“是否能立即解析实例”;本轮若不改其行为,需要在文档和测试中明确这一点 +- `ResolveCqrsRegistrationService()` 仍要求注册阶段对 `ICqrsRegistrationService` 可见的是实例绑定;若后续改成工厂或实现类型注册,需要额外设计注册阶段激活 helper + +## 最近权威验证 + +- `git pull --ff-only origin main` + - 结果:通过,当前主分支已同步 +- `rg -n "GetFirstArchitectureContext|GameContext|RegisterPlurality|ResolveCqrsRegistrationService|GetAll\\(" ...` + - 结果:已确认 `GameContext` 的默认回退集中在 `ContextAwareBase` / `GameContextProvider`,且 `MicrosoftDiContainer` 的预冻结查询实现存在分叉 +- `python3 scripts/license-header.py --check` + - 结果:通过 +- `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` + +## 下一推荐步骤 + +1. 若后续继续推进,可评估 `Architecture` 销毁阶段是否要显式解绑 `GameContext` 当前活动上下文 +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 new file mode 100644 index 00000000..6ae13dcd --- /dev/null +++ b/ai-plan/public/single-context-priority/traces/single-context-priority-trace.md @@ -0,0 +1,32 @@ +# Single Context Priority 追踪 + +## 2026-05-07 + +### 阶段:启动并收敛实现方向(SINGLE-CONTEXT-PRIORITY-RP-001) + +- 依据用户补充的运行时心智模型,确认当前 `Architecture` 更接近框架实例,`IArchitectureContext` 更接近功能入口,而不是需要并存运行的独立宿主 +- 启动阶段已按仓库规则读取 `AGENTS.md`、`.ai/environment/tools.ai.yaml`、`ai-plan/public/README.md`,并在 `main` 上执行 `git pull --ff-only origin main` 后创建 `refactor/single-context-priority` +- 本轮实现决策: + - `GameContext` 维持兼容 API,但内部收敛为单活动上下文模型;允许多个类型键指向同一上下文实例,不允许并存多个不同上下文实例 + - `MicrosoftDiContainer` 先做低风险修复:统一预冻结 `Get()` / `Get(Type)` 的实例可见性规则,并把 CQRS 注册服务解析改为复用同一条实例收集路径 + - 若 `Contains()` 的预冻结语义仍保持“是否已有注册”,则通过 XML 文档和测试显式记录,而不是隐含为“可立即解析” +- 本轮委托记录: + - explorer `Noether`:梳理 `GameContext` 单活动上下文收敛的兼容风险、测试缺口和必须保留的 API 面 + - explorer `Boole`:梳理 `MicrosoftDiContainer` 预冻结查询、`Contains()`、CQRS 注册依赖点的语义分叉 + +### 阶段:实现与验证完成(SINGLE-CONTEXT-PRIORITY-RP-001) + +- 实现摘要: + - `GameContext` 新增单活动上下文约束,`GetFirstArchitectureContext()` 改为显式返回当前活动上下文,不再依赖并发字典枚举顺序 + - `GameContext.GetByType(Type)`、`Get()`、`TryGet()` 增加对当前活动上下文的兼容匹配,保留按架构类型别名回查能力 + - `MicrosoftDiContainer.Get()` / `Get(Type)` 的预冻结实例查询改为复用 `CollectRegisteredImplementationInstances(...)` + - `ResolveCqrsRegistrationService()` 改为复用同一条注册阶段实例可见性路径,并把失败信息收敛为更明确的契约提示 + - `IIocContainer` XML 文档与 `docs/zh-CN/core/context.md`、`docs/zh-CN/core/rule.md`、`docs/zh-CN/source-generators/context-aware-generator.md` 已同步到“当前活动上下文 / 预冻结查询”语义 +- 测试与验证: + - `python3 scripts/license-header.py --check` 通过 + - `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` + +### 当前下一步 + +1. 若后续要进一步彻底移除全局回退,可单独评估 `Architecture` 销毁解绑与 `GameContext` 公开别名字典的收口策略 diff --git a/docs/zh-CN/core/context.md b/docs/zh-CN/core/context.md index 0aa2f104..40cf1fe5 100644 --- a/docs/zh-CN/core/context.md +++ b/docs/zh-CN/core/context.md @@ -142,7 +142,7 @@ var playerId = await this.SendAsync( `GameContext` 仍然存在,但已经退到兼容和回退路径。 -`ContextAwareBase` 在实例未显式注入上下文时,会回退到 `GameContext.GetFirstArchitectureContext()`。这能保证部分旧代码继续工作,但它不是新代码的首选接法。 +`ContextAwareBase` 在实例未显式注入上下文时,会回退到 `GameContext.GetFirstArchitectureContext()`。这个入口现在表示“当前活动上下文”,不再依赖全局注册表里的任意首项。这能保证部分旧代码继续工作,但它不是新代码的首选接法。 新代码更推荐: diff --git a/docs/zh-CN/core/rule.md b/docs/zh-CN/core/rule.md index 8b36cf0d..eefe8b4d 100644 --- a/docs/zh-CN/core/rule.md +++ b/docs/zh-CN/core/rule.md @@ -90,7 +90,7 @@ public abstract class ContextAwareBase : IContextAware - `Context` 属性 - 存储架构上下文的引用 - `OnContextReady()` 钩子 - 在上下文设置完成后调用,用于初始化逻辑 -- 自动回退机制 - 如果上下文未被显式设置,会自动从 `GameContext.GetFirstArchitectureContext()` 获取 +- 自动回退机制 - 如果上下文未被显式设置,会自动从 `GameContext.GetFirstArchitectureContext()` 获取当前活动上下文 ## 扩展方法:框架能力的来源 @@ -310,7 +310,7 @@ public class SaveManager : ContextAwareBase ### 回退机制 -如果组件的上下文未被显式设置,`GetContext()` 会自动尝试从 `GameContext.GetFirstArchitectureContext()` 获取。这提供了一个安全的回退机制。 +如果组件的上下文未被显式设置,`GetContext()` 会自动尝试从 `GameContext.GetFirstArchitectureContext()` 获取当前活动上下文。这提供了一个安全的回退机制。 ```csharp IArchitectureContext IContextAware.GetContext() diff --git a/docs/zh-CN/source-generators/context-aware-generator.md b/docs/zh-CN/source-generators/context-aware-generator.md index 5d230d3a..26e845f5 100644 --- a/docs/zh-CN/source-generators/context-aware-generator.md +++ b/docs/zh-CN/source-generators/context-aware-generator.md @@ -100,7 +100,7 @@ public partial class PlayerController : IController - `ContextAwareBase` - 只维护简单的实例级缓存 - 不维护共享 provider - - 默认直接回退到 `GameContext.GetFirstArchitectureContext()` + - 默认直接回退到 `GameContext.GetFirstArchitectureContext()` 返回的当前活动上下文 因此,这两条路径不是“只是写法不同”,而是共享 provider 策略和实例缓存边界都不同。 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 2/3] =?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)); } } From ee8b6a4debbfc6c30dcfae71146ca58ed92ae5e0 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 7 May 2026 10:43:07 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(core):=20=E4=BF=AE=E5=A4=8D=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87=E9=94=80=E6=AF=81=E8=A7=A3=E7=BB=91=E4=B8=8E?= =?UTF-8?q?=E5=B9=B6=E5=8F=91=E4=B8=80=E8=87=B4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 GameContext 的别名字典与当前活动上下文同步边界,避免解绑与读取路径出现状态漂移 - 修复 Architecture.Destroy() 缺少全局解绑的问题,并补充相关生命周期 XML 文档 - 更新回归测试、CQRS 注册断言与 single-context-priority 跟踪记录 --- .../ArchitectureLifecycleBehaviorTests.cs | 20 +++ .../Ioc/MicrosoftDiContainerTests.cs | 5 + GFramework.Core/Architectures/Architecture.cs | 19 ++- GFramework.Core/Architectures/GameContext.cs | 124 ++++++++++++------ .../Architectures/GameContextProvider.cs | 1 + GFramework.Core/Rule/ContextAwareBase.cs | 2 + .../todos/single-context-priority-tracking.md | 15 ++- .../traces/single-context-priority-trace.md | 22 ++++ 8 files changed, 166 insertions(+), 42 deletions(-) diff --git a/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs index e5eb0e1f..359a92bf 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs @@ -237,6 +237,26 @@ public class ArchitectureLifecycleBehaviorTests Assert.Throws(() => probe.GetContext()); } + /// + /// 验证同步兼容销毁入口同样会解除全局 GameContext 绑定。 + /// + [Test] + public async Task Destroy_Should_Unbind_Context_From_GameContext() + { + var architecture = new PhaseTrackingArchitecture(); + + await architecture.InitializeAsync(); + + Assert.That(GameContext.GetByType(architecture.GetType()), Is.SameAs(architecture.Context)); + +#pragma warning disable CS0618 + architecture.Destroy(); +#pragma warning restore CS0618 + + Assert.Throws(() => GameContext.GetByType(architecture.GetType())); + Assert.Throws(() => GameContext.GetFirstArchitectureContext()); + } + /// /// 验证启用 AllowLateRegistration 时,生命周期层会立即初始化后注册的组件,而不是继续沿用初始化期的拒绝策略。 /// 由于公共架构 API 在 Ready 之后会先触发容器限制,此回归测试直接覆盖生命周期协作者的对齐逻辑。 diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index b1c5e336..38832cdd 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -494,6 +494,11 @@ public class MicrosoftDiContainerTests { Assert.DoesNotThrow(() => _container.RegisterCqrsHandlersFromAssemblies([typeof(DeterministicOrderNotification).Assembly])); + + Assert.That( + _container.GetServicesUnsafe.Any(static descriptor => + descriptor.ServiceType == typeof(INotificationHandler)), + Is.True); } /// diff --git a/GFramework.Core/Architectures/Architecture.cs b/GFramework.Core/Architectures/Architecture.cs index 7fa66b1d..20883e7b 100644 --- a/GFramework.Core/Architectures/Architecture.cs +++ b/GFramework.Core/Architectures/Architecture.cs @@ -359,6 +359,11 @@ public abstract class Architecture : IArchitecture /// /// 异步销毁架构及所有组件 /// + /// + /// 无论 _lifecycle.DestroyAsync() 是否抛出异常,该方法都会在 中调用 + /// (()),移除当前架构类型在全局上下文表中的绑定。 + /// 这样可以阻止新的惰性上下文回退命中已销毁实例;但已经缓存上下文的对象不会被自动重置。 + /// public virtual async ValueTask DestroyAsync() { try @@ -376,10 +381,22 @@ public abstract class Architecture : IArchitecture /// /// 销毁架构并清理所有组件资源(同步方法,保留用于向后兼容) /// + /// + /// 该同步兼容入口会与 保持相同的全局解绑语义;即使销毁过程抛出异常, + /// 也会在 中调用 (())。 + /// [Obsolete("建议使用 DestroyAsync() 以支持异步清理")] public virtual void Destroy() { - _lifecycle.Destroy(); + try + { + _lifecycle.Destroy(); + } + finally + { + // 同步销毁同样需要解除全局回退入口,避免兼容调用路径保留过期上下文。 + GameContext.Unbind(GetType()); + } } #endregion diff --git a/GFramework.Core/Architectures/GameContext.cs b/GFramework.Core/Architectures/GameContext.cs index a25a07ca..580b8661 100644 --- a/GFramework.Core/Architectures/GameContext.cs +++ b/GFramework.Core/Architectures/GameContext.cs @@ -12,6 +12,12 @@ namespace GFramework.Core.Architectures; /// public static class GameContext { + // ConcurrentDictionary 负责向外暴露安全的实时视图;该锁负责维护“别名字典 + 当前活动上下文”之间的组合不变式。 +#if NET9_0_OR_GREATER + private static readonly Lock SyncRoot = new(); +#else + private static readonly object SyncRoot = new(); +#endif private static readonly ConcurrentDictionary ArchitectureDictionary = new(); private static IArchitectureContext? _currentArchitectureContext; @@ -39,15 +45,19 @@ public static class GameContext ArgumentNullException.ThrowIfNull(architectureType); ArgumentNullException.ThrowIfNull(context); - var currentContext = Interlocked.CompareExchange(ref _currentArchitectureContext, context, comparand: null); - if (currentContext != null && !ReferenceEquals(currentContext, context)) - throw new InvalidOperationException( - $"GameContext already tracks active context '{currentContext.GetType().Name}'. " + - $"Cannot bind a different context '{context.GetType().Name}'."); + lock (SyncRoot) + { + if (_currentArchitectureContext != null && !ReferenceEquals(_currentArchitectureContext, context)) + throw new InvalidOperationException( + $"GameContext already tracks active context '{_currentArchitectureContext.GetType().Name}'. " + + $"Cannot bind a different context '{context.GetType().Name}'."); - if (!ArchitectureDictionary.TryAdd(architectureType, context)) - throw new InvalidOperationException( - $"Architecture context for '{architectureType.Name}' already exists"); + if (!ArchitectureDictionary.TryAdd(architectureType, context)) + throw new InvalidOperationException( + $"Architecture context for '{architectureType.Name}' already exists"); + + _currentArchitectureContext ??= context; + } } /// @@ -58,8 +68,11 @@ public static class GameContext /// 当当前没有活动上下文时抛出。 public static IArchitectureContext GetFirstArchitectureContext() { - if (_currentArchitectureContext is { } context) - return context; + lock (SyncRoot) + { + if (_currentArchitectureContext is { } context) + return context; + } throw new InvalidOperationException("No active architecture context is currently bound."); } @@ -76,11 +89,14 @@ public static class GameContext { ArgumentNullException.ThrowIfNull(type); - if (ArchitectureDictionary.TryGetValue(type, out var context)) - return context; + lock (SyncRoot) + { + if (ArchitectureDictionary.TryGetValue(type, out var context)) + return context; - if (_currentArchitectureContext != null && type.IsInstanceOfType(_currentArchitectureContext)) - return _currentArchitectureContext; + if (_currentArchitectureContext != null && type.IsInstanceOfType(_currentArchitectureContext)) + return _currentArchitectureContext; + } throw new InvalidOperationException( $"Architecture context for '{type.Name}' not found"); @@ -88,25 +104,30 @@ public static class GameContext /// - /// 获取指定类型的架构上下文实例 + /// 获取指定类型的架构上下文实例。 + /// 该方法会优先复用当前活动上下文,再回退到显式注册的类型别名。 /// /// 架构上下文类型,必须实现IArchitectureContext接口 /// 指定类型的架构上下文实例 /// 当指定类型的架构上下文不存在时抛出 public static T Get() where T : class, IArchitectureContext { - if (_currentArchitectureContext is T currentContext) - return currentContext; + lock (SyncRoot) + { + if (_currentArchitectureContext is T currentContext) + return currentContext; - if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx)) - return (T)ctx; + if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx)) + return (T)ctx; + } throw new InvalidOperationException( $"Architecture context '{typeof(T).Name}' not found"); } /// - /// 尝试获取指定类型的架构上下文实例 + /// 尝试获取指定类型的架构上下文实例。 + /// 该方法会优先检查当前活动上下文是否兼容目标类型,再回退到显式注册的类型别名。 /// /// 架构上下文类型,必须实现IArchitectureContext接口 /// 输出参数,如果找到则返回对应的架构上下文实例,否则返回null @@ -114,16 +135,19 @@ public static class GameContext public static bool TryGet(out T? context) where T : class, IArchitectureContext { - if (_currentArchitectureContext is T currentContext) + lock (SyncRoot) { - context = currentContext; - return true; - } + if (_currentArchitectureContext is T currentContext) + { + context = currentContext; + return true; + } - if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx)) - { - context = (T)ctx; - return true; + if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx)) + { + context = (T)ctx; + return true; + } } context = null; @@ -131,30 +155,54 @@ public static class GameContext } /// - /// 移除指定类型的架构上下文绑定 + /// 移除指定类型的架构上下文绑定。 + /// 当最后一个指向当前活动上下文的别名被移除时,也会同步清空当前活动上下文指针。 /// /// 要移除的架构类型 + /// public static void Unbind(Type architectureType) { ArgumentNullException.ThrowIfNull(architectureType); - if (!ArchitectureDictionary.TryRemove(architectureType, out var removedContext)) - return; + lock (SyncRoot) + { + if (!ArchitectureDictionary.TryRemove(architectureType, out var removedContext)) + return; - if (_currentArchitectureContext == null || !ReferenceEquals(_currentArchitectureContext, removedContext)) - return; + if (_currentArchitectureContext == null || !ReferenceEquals(_currentArchitectureContext, removedContext)) + return; - if (!ArchitectureDictionary.Values.Any(current => ReferenceEquals(current, removedContext))) - Interlocked.CompareExchange(ref _currentArchitectureContext, null, removedContext); + if (!HasAliasForContext(removedContext)) + _currentArchitectureContext = null; + } } /// - /// 清空所有架构上下文绑定 + /// 清空所有架构上下文绑定,并重置当前活动上下文。 /// public static void Clear() { - ArchitectureDictionary.Clear(); - Interlocked.Exchange(ref _currentArchitectureContext, null); + lock (SyncRoot) + { + ArchitectureDictionary.Clear(); + _currentArchitectureContext = null; + } + } + + /// + /// 判断当前是否仍存在指向同一上下文实例的其他类型别名。 + /// + /// 被移除绑定原本指向的上下文实例。 + /// 如果还有其他别名指向同一实例则返回 ;否则返回 + private static bool HasAliasForContext(IArchitectureContext context) + { + foreach (var current in ArchitectureDictionary.Values) + { + if (ReferenceEquals(current, context)) + return true; + } + + return false; } } diff --git a/GFramework.Core/Architectures/GameContextProvider.cs b/GFramework.Core/Architectures/GameContextProvider.cs index 5725d00b..32a530da 100644 --- a/GFramework.Core/Architectures/GameContextProvider.cs +++ b/GFramework.Core/Architectures/GameContextProvider.cs @@ -15,6 +15,7 @@ public sealed class GameContextProvider : IArchitectureContextProvider /// 获取当前的架构上下文。 /// /// 架构上下文实例 + /// 当前没有已绑定的活动架构上下文时抛出。 public IArchitectureContext GetContext() { return GameContext.GetFirstArchitectureContext(); diff --git a/GFramework.Core/Rule/ContextAwareBase.cs b/GFramework.Core/Rule/ContextAwareBase.cs index 970f5992..85931faa 100644 --- a/GFramework.Core/Rule/ContextAwareBase.cs +++ b/GFramework.Core/Rule/ContextAwareBase.cs @@ -48,6 +48,8 @@ public abstract class ContextAwareBase : IContextAware /// /// 当 为空时,该实现会直接回退到 返回的当前活动上下文。 /// 该回退过程不执行额外同步,也不支持替换 provider;如需这些能力,请改用生成的 ContextAware 实现。 + /// 一旦回退结果被写入 ,后续即使关联架构解除 绑定, + /// 该实例仍会保留原引用,调用方需要自行约束其生命周期或改用支持 provider 协调的生成实现。 /// IArchitectureContext IContextAware.GetContext() { 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 b25cc74f..11296e83 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 @@ -10,13 +10,14 @@ ## 当前恢复点 -- 恢复点编号:`SINGLE-CONTEXT-PRIORITY-RP-001` -- 当前阶段:`Phase 2` +- 恢复点编号:`SINGLE-CONTEXT-PRIORITY-RP-003` +- 当前阶段:`Phase 3` - 当前结论: - `GameContext` 已从“字典首个枚举值即默认上下文”收敛为“单活动上下文 + 类型别名兼容查找”;同一全局上下文表不再允许并存两个不同上下文实例 - `MicrosoftDiContainer` 的预冻结 `Get()` / `Get(Type)` 已改为复用实例可见性收集逻辑,和 `GetAll*` 的实例暴露规则保持一致 - `IIocContainer` XML 文档已明确预冻结查询与 `Contains()` 的契约边界,避免把注册阶段查询误读为完整 DI 激活语义 - `Architecture.DestroyAsync()` 现会在生命周期销毁完成后显式解除 `GameContext` 绑定,防止已销毁架构继续充当默认上下文回退入口 + - 当前 PR review 跟进中,`Architecture.Destroy()` 已补齐相同解绑语义,`GameContext` 也改为通过统一临界区维护别名字典与当前活动上下文的一致性 - 当前分支从 `main` 创建,已完成 `git pull --ff-only origin main` ## 当前活跃事实 @@ -35,6 +36,7 @@ - `Contains()` 在预冻结阶段目前更接近“是否存在注册”,不等同于“是否能立即解析实例”;本轮若不改其行为,需要在文档和测试中明确这一点 - `ResolveCqrsRegistrationService()` 仍要求注册阶段对 `ICqrsRegistrationService` 可见的是实例绑定;若后续改成工厂或实现类型注册,需要额外设计注册阶段激活 helper - 现有解绑逻辑通过 `Architecture.GetType()` 移除初始化期绑定;若后续引入更多显式上下文别名,需同步评估是否要在销毁时额外移除这些别名 +- `GameContext` 当前通过粗粒度锁保护组合状态一致性;若该入口后续进入高频并发热点,需要基于真实性能数据再评估更细粒度方案 ## 最近权威验证 @@ -52,8 +54,15 @@ - 结果:通过,`32/32` passed - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` - 结果:再次通过,`0 warning / 0 error` +- `python3 scripts/license-header.py --check` + - 结果:再次通过,所有受支持文件头合法 +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~GameContextTests|FullyQualifiedName~ContextProviderTests|FullyQualifiedName~ContextAwareTests|FullyQualifiedName~ArchitectureLifecycleBehaviorTests|FullyQualifiedName~MicrosoftDiContainerTests"` + - 结果:通过,`87/87` passed +- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` + - 结果:再次通过,`0 warning / 0 error` ## 下一推荐步骤 1. 若后续继续推进,可评估是否要把 `GameContext.ArchitectureReadOnlyDictionary` 标记为兼容层,并收口其公开使用面 -2. 若 CQRS runtime seam 计划改成工厂式注册,再单独补“注册阶段激活 helper”的设计与测试 +2. 若 `GameContext` 的组合锁成为瓶颈,再基于压测结果讨论更细粒度的原子状态模型 +3. 若 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 44273c0b..ec2f32d1 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 @@ -42,3 +42,25 @@ ### 当前下一步 1. 若后续要进一步彻底移除全局回退,可单独评估 `GameContext` 公开别名字典的收口策略与生成器默认 provider 的进一步简化空间 + +### 阶段:PR review 跟进修复(SINGLE-CONTEXT-PRIORITY-RP-003) + +- Review 来源: + - PR `#332`(`refactor/single-context-priority`)上的 CodeRabbit unresolved outside-diff / nitpick comments + - PR `#332` 上 Greptile 的两个 latest-head open threads +- 本轮确认并接受的结论: + - `Architecture.Destroy()` 与 `DestroyAsync()` 存在语义漂移,前者缺少 `GameContext.Unbind(GetType())` 清理,仍然会泄漏过期全局回退入口 + - `GameContext` 仅靠 `ConcurrentDictionary` + `_currentArchitectureContext` 的分离更新无法保证组合状态一致;`Bind` / `Unbind` / `Clear` 与读取路径需要统一同步边界 + - `GameContextProvider.GetContext()`、`ContextAwareBase.GetContext()`、`Architecture.DestroyAsync()` 等处的 XML 文档仍未完全反映最新生命周期语义 + - `RegisterCqrsHandlersFromAssemblies_Should_Resolve_Registration_Service_When_Registered_As_Instance` 需要从“仅不抛异常”加强为“确实注册了 handler” +- 本轮实现摘要: + - 为 `GameContext` 新增统一临界区,保证别名字典与当前活动上下文指针的状态切换一致 + - 为 `Architecture.Destroy()` 增加 `finally` 解绑,和异步销毁路径对齐 + - 补充 `GameContextProvider`、`ContextAwareBase`、`Architecture`、`GameContext` 的 XML 文档说明 + - 新增同步 `Destroy()` 解绑回归测试,并加强 CQRS 注册断言 +- 验证与环境备注: + - `python3 scripts/license-header.py --check` 通过 + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~GameContextTests|FullyQualifiedName~ContextProviderTests|FullyQualifiedName~ContextAwareTests|FullyQualifiedName~ArchitectureLifecycleBehaviorTests|FullyQualifiedName~MicrosoftDiContainerTests"` 通过,`87/87` passed + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` 通过,`0 warning / 0 error` + - 中途尝试把 `dotnet test` 与 `dotnet build` 并行执行时再次触发 `GFramework.Core.pdb` 占用重试;按仓库规则改为串行直跑后结果稳定通过 + - `System.Threading.Lock` 在 `net8.0` 目标下不可用,因此最终采用 `#if NET9_0_OR_GREATER` 条件锁声明,同时保留 `net8.0` 的 `object` 锁兼容实现