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 策略和实例缓存边界都不同。