diff --git a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs index 76a560f5..03525b55 100644 --- a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs +++ b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs @@ -10,7 +10,7 @@ namespace GFramework.Core.Abstractions.Ioc; /// /// 依赖注入容器接口,定义了服务注册、解析和管理的基本操作 /// -public interface IIocContainer : IContextAware +public interface IIocContainer : IContextAware, IDisposable { #region Register Methods diff --git a/GFramework.Core.Tests/Ioc/IocContainerLifetimeTests.cs b/GFramework.Core.Tests/Ioc/IocContainerLifetimeTests.cs index d0dcdd08..a3325ff6 100644 --- a/GFramework.Core.Tests/Ioc/IocContainerLifetimeTests.cs +++ b/GFramework.Core.Tests/Ioc/IocContainerLifetimeTests.cs @@ -22,6 +22,18 @@ public class IocContainerLifetimeTests public Guid Id { get; } = Guid.NewGuid(); } + private sealed class DisposableTestService : ITestService, IDisposable + { + public Guid Id { get; } = Guid.NewGuid(); + + public bool IsDisposed { get; private set; } + + public void Dispose() + { + IsDisposed = true; + } + } + [Test] public void RegisterSingleton_Should_Return_Same_Instance() { @@ -207,4 +219,31 @@ public class IocContainerLifetimeTests scope2.Dispose(); scope3.Dispose(); } -} \ No newline at end of file + + [Test] + public void Dispose_Should_Dispose_Resolved_Singleton_And_Block_Further_Use() + { + // Arrange + var container = new MicrosoftDiContainer(); + container.RegisterSingleton(); + container.Freeze(); + var service = container.GetRequired(); + + // Act + container.Dispose(); + + // Assert + Assert.That(service.IsDisposed, Is.True); + Assert.Throws(() => container.Get()); + Assert.Throws(() => container.CreateScope()); + } + + [Test] + public void Dispose_Should_Be_Idempotent() + { + var container = new MicrosoftDiContainer(); + + Assert.DoesNotThrow(container.Dispose); + Assert.DoesNotThrow(container.Dispose); + } +} diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index 820cfa66..bc16b6d2 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -760,4 +760,17 @@ public class MicrosoftDiContainerTests Assert.That(((IPrioritizedService)services[0]).Priority, Is.EqualTo(10)); Assert.That(((IPrioritizedService)services[1]).Priority, Is.EqualTo(30)); } + + /// + /// 测试容器释放后会阻止后续注册与解析,避免 benchmark 或短生命周期宿主继续使用已回收状态。 + /// + [Test] + public void Dispose_Should_Block_Subsequent_Registration_And_Query_Operations() + { + _container.Dispose(); + + Assert.Throws(() => _container.Register(new TestService())); + Assert.Throws(() => _container.Contains()); + Assert.Throws(() => _container.GetAll()); + } } diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 5a6d728c..4c5fe90b 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -22,6 +22,19 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) { #region Helper Methods + /// + /// 检查容器是否已释放,避免访问已经失效的服务提供者与同步原语。 + /// + /// 当容器已释放时抛出。 + private void ThrowIfDisposed() + { + if (!_disposed) return; + + const string objectName = nameof(MicrosoftDiContainer); + _logger.Warn("Attempted to use a disposed MicrosoftDiContainer."); + throw new ObjectDisposedException(objectName); + } + /// /// 检查容器是否已冻结,如果已冻结则抛出异常 /// 用于保护注册操作的安全性 @@ -57,6 +70,11 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// private volatile bool _frozen; + /// + /// 容器释放状态标志,true 表示容器已释放,不允许继续访问。 + /// + private volatile bool _disposed; + /// /// 读写锁,确保多线程环境下的线程安全操作 /// @@ -85,6 +103,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 当容器已冻结或类型已被注册时抛出 public void RegisterSingleton(T instance) { + ThrowIfDisposed(); var type = typeof(T); _lock.EnterWriteLock(); try @@ -119,6 +138,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) where TImpl : class, TService where TService : class { + ThrowIfDisposed(); _lock.EnterWriteLock(); try { @@ -142,6 +162,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) where TImpl : class, TService where TService : class { + ThrowIfDisposed(); _lock.EnterWriteLock(); try { @@ -165,6 +186,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) where TImpl : class, TService where TService : class { + ThrowIfDisposed(); _lock.EnterWriteLock(); try { @@ -187,6 +209,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 当容器已冻结时抛出 public void RegisterPlurality(object instance) { + ThrowIfDisposed(); var concreteType = instance.GetType(); var interfaces = concreteType.GetInterfaces(); @@ -219,6 +242,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// public void RegisterPlurality() where T : class { + ThrowIfDisposed(); _lock.EnterWriteLock(); try { @@ -262,6 +286,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 当容器已冻结时抛出 public void Register(T instance) { + ThrowIfDisposed(); _lock.EnterWriteLock(); try { @@ -284,6 +309,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 当容器已冻结时抛出 public void Register(Type type, object instance) { + ThrowIfDisposed(); _lock.EnterWriteLock(); try { @@ -307,6 +333,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public void RegisterFactory( Func factory) where TService : class { + ThrowIfDisposed(); _lock.EnterWriteLock(); try { @@ -328,6 +355,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 行为类型,必须是引用类型 public void RegisterCqrsPipelineBehavior() where TBehavior : class { + ThrowIfDisposed(); _lock.EnterWriteLock(); try { @@ -392,6 +420,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) { ArgumentNullException.ThrowIfNull(assemblies); + ThrowIfDisposed(); var assemblyArray = assemblies.ToArray(); foreach (var assembly in assemblyArray) { @@ -419,6 +448,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 服务配置委托 public void ExecuteServicesHook(Action? configurator = null) { + ThrowIfDisposed(); _lock.EnterWriteLock(); try { @@ -464,6 +494,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 服务实例或null public T? Get() where T : class { + ThrowIfDisposed(); _lock.EnterReadLock(); try { @@ -503,6 +534,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 服务实例或null public object? Get(Type type) { + ThrowIfDisposed(); _lock.EnterReadLock(); try { @@ -593,6 +625,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 只读的服务实例列表 public IReadOnlyList GetAll() where T : class { + ThrowIfDisposed(); _lock.EnterReadLock(); try { @@ -620,6 +653,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public IReadOnlyList GetAll(Type type) { ArgumentNullException.ThrowIfNull(type); + ThrowIfDisposed(); _lock.EnterReadLock(); try @@ -750,6 +784,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 排序后的只读服务实例列表 public IReadOnlyList GetAllSorted(Comparison comparison) where T : class { + ThrowIfDisposed(); var list = GetAll().ToList(); list.Sort(comparison); return list; @@ -816,6 +851,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// true表示包含该类型实例,false表示不包含 public bool Contains() where T : class { + ThrowIfDisposed(); _lock.EnterReadLock(); try { @@ -838,6 +874,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// true表示包含该实例,false表示不包含 public bool ContainsInstance(object instance) { + ThrowIfDisposed(); _lock.EnterReadLock(); try { @@ -855,6 +892,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// public void Clear() { + ThrowIfDisposed(); _lock.EnterWriteLock(); try { @@ -867,7 +905,9 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) GetServicesUnsafe.Clear(); _registeredInstances.Clear(); + (_provider as IDisposable)?.Dispose(); _provider = null; + _frozen = false; _logger.Info("Container cleared"); } finally @@ -882,6 +922,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// public void Freeze() { + ThrowIfDisposed(); _lock.EnterWriteLock(); try { @@ -917,6 +958,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 当容器未冻结时抛出 public IServiceScope CreateScope() { + ThrowIfDisposed(); _lock.EnterReadLock(); try { @@ -938,5 +980,44 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) } } + /// + /// 释放容器持有的服务提供者、注册状态和同步原语。 + /// + /// + /// 冻结后的根 会拥有 DI 创建的单例与作用域根缓存,因此 benchmark、 + /// 测试宿主或短生命周期架构在结束时需要显式释放容器,避免这些对象与内部 + /// 一起滞留。 + /// 释放是幂等的;首次释放后所有后续访问都会抛出 。 + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _lock.EnterWriteLock(); + try + { + if (_disposed) + { + return; + } + + _disposed = true; + (_provider as IDisposable)?.Dispose(); + _provider = null; + GetServicesUnsafe.Clear(); + _registeredInstances.Clear(); + _frozen = false; + _logger.Info("IOC Container disposed"); + } + finally + { + _lock.ExitWriteLock(); + _lock.Dispose(); + } + } + #endregion } diff --git a/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs index 42ee16b9..46929154 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs @@ -83,6 +83,7 @@ public class NotificationBenchmarks [GlobalCleanup] public void Cleanup() { + _container.Dispose(); _serviceProvider.Dispose(); } diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs index 731f2a23..6b4f060b 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs @@ -85,6 +85,7 @@ public class RequestBenchmarks [GlobalCleanup] public void Cleanup() { + _container.Dispose(); _serviceProvider.Dispose(); } diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs index a943e095..72cb423e 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs @@ -103,6 +103,8 @@ public class RequestInvokerBenchmarks [GlobalCleanup] public void Cleanup() { + _reflectionContainer.Dispose(); + _generatedContainer.Dispose(); _serviceProvider.Dispose(); BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); } diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs index ece2b977..69ea20c0 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs @@ -101,6 +101,7 @@ public class RequestPipelineBenchmarks [GlobalCleanup] public void Cleanup() { + _container.Dispose(); _serviceProvider.Dispose(); BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); } diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs index 494e6296..81fa96b6 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs @@ -29,6 +29,7 @@ public class RequestStartupBenchmarks private static readonly ILogger RuntimeLogger = CreateLogger(nameof(RequestStartupBenchmarks)); private static readonly BenchmarkRequest Request = new(Guid.NewGuid()); + private MicrosoftDiContainer _container = null!; private ServiceProvider _serviceProvider = null!; private IMediator _mediatr = null!; private ICqrsRuntime _runtime = null!; @@ -62,7 +63,8 @@ public class RequestStartupBenchmarks _serviceProvider = CreateMediatRServiceProvider(); _mediatr = _serviceProvider.GetRequiredService(); - _runtime = CreateGFrameworkRuntime(); + _container = CreateGFrameworkContainer(); + _runtime = CreateGFrameworkRuntime(_container); } /// @@ -84,6 +86,7 @@ public class RequestStartupBenchmarks [GlobalCleanup] public void Cleanup() { + _container.Dispose(); _serviceProvider.Dispose(); } @@ -124,10 +127,11 @@ public class RequestStartupBenchmarks /// [Benchmark] [BenchmarkCategory("ColdStart")] - public ValueTask ColdStart_GFrameworkCqrs() + public async ValueTask ColdStart_GFrameworkCqrs() { - var runtime = CreateGFrameworkRuntime(); - return runtime.SendAsync(BenchmarkContext.Instance, Request, CancellationToken.None); + using var container = CreateGFrameworkContainer(); + var runtime = CreateGFrameworkRuntime(container); + return await runtime.SendAsync(BenchmarkContext.Instance, Request, CancellationToken.None).ConfigureAwait(false); } /// @@ -137,12 +141,21 @@ public class RequestStartupBenchmarks /// 该 benchmark 故意保持与 MediatR 对照组同样的“单 handler 最小宿主”模型, /// 因此这里继续使用单点手工注册,而不引入依赖完整 CQRS 注册协调器的程序集扫描路径。 /// - private static ICqrsRuntime CreateGFrameworkRuntime() + private static MicrosoftDiContainer CreateGFrameworkContainer() { - var container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static currentContainer => + return BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static currentContainer => { currentContainer.RegisterTransient, BenchmarkRequestHandler>(); }); + } + + /// + /// 基于已冻结的 benchmark 容器构建最小 GFramework.CQRS runtime。 + /// + /// 当前 benchmark 拥有并负责释放的容器。 + private static ICqrsRuntime CreateGFrameworkRuntime(MicrosoftDiContainer container) + { + ArgumentNullException.ThrowIfNull(container); return GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, RuntimeLogger); } diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs index d6ce01ba..7dd4db28 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs @@ -103,6 +103,8 @@ public class StreamInvokerBenchmarks [GlobalCleanup] public void Cleanup() { + _reflectionContainer.Dispose(); + _generatedContainer.Dispose(); _serviceProvider.Dispose(); BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); } diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs index 1c22d5f3..9cdadcfe 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs @@ -86,6 +86,7 @@ public class StreamingBenchmarks [GlobalCleanup] public void Cleanup() { + _container.Dispose(); _serviceProvider.Dispose(); } diff --git a/ai-plan/public/archive/microsoft-di-container-disposal/todos/microsoft-di-container-disposal-tracking.md b/ai-plan/public/archive/microsoft-di-container-disposal/todos/microsoft-di-container-disposal-tracking.md new file mode 100644 index 00000000..4cef31b4 --- /dev/null +++ b/ai-plan/public/archive/microsoft-di-container-disposal/todos/microsoft-di-container-disposal-tracking.md @@ -0,0 +1,48 @@ +# MicrosoftDiContainer Disposal Tracking + +## Goal + +Fix issue `#327` by making `MicrosoftDiContainer` explicitly disposable, ensuring frozen service providers and lock +state are released deterministically, and updating CQRS benchmarks so every owned container is disposed in cleanup or +cold-start paths. + +## Current Recovery Point + +- Recovery point: `MDC-DISPOSE-RP-001` +- Phase: completed and ready to archive +- Focus: + - keep the final validated implementation and archive handoff concise + +## Active Risks + +- No active implementation blockers remain after validation. + +## Completed In This Stage + +- Extended `IIocContainer` to inherit `IDisposable` so callers holding the abstraction can release container-owned + resources explicitly. +- Implemented `MicrosoftDiContainer.Dispose()` with idempotent root-provider release, lock cleanup, state clearing, and + `ObjectDisposedException` guards for post-disposal access. +- Updated `Clear()` to dispose the currently built root provider before resetting container state. +- Added Core regression tests that verify resolved DI-owned singletons are disposed and that disposed containers reject + further registration, lookup, and scope creation. +- Fixed CQRS benchmark cleanup so every benchmark-owned `MicrosoftDiContainer` is disposed, including the temporary + cold-start container path in `RequestStartupBenchmarks`. + +## Validation Target + +- `python3 scripts/license-header.py --check` +- `dotnet test GFramework.Core.Tests -c Release --filter "FullyQualifiedName~MicrosoftDiContainer|FullyQualifiedName~IocContainerLifetimeTests"` +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` +- `dotnet build GFramework.sln -c Release` + +## Latest Validation Result + +- `python3 scripts/license-header.py --check` passed on 2026-05-06. +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainer|FullyQualifiedName~IocContainerLifetimeTests"` passed on 2026-05-06 with `55` tests passed. +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` passed on 2026-05-06 with `0 warnings` and `0 errors`. +- `dotnet build GFramework.sln -c Release` passed on 2026-05-06 with `0 warnings` and `0 errors`. + +## Next Recommended Resume Step + +Archive this topic under `ai-plan/public/archive/` and push the fix branch for review. diff --git a/ai-plan/public/archive/microsoft-di-container-disposal/traces/microsoft-di-container-disposal-trace.md b/ai-plan/public/archive/microsoft-di-container-disposal/traces/microsoft-di-container-disposal-trace.md new file mode 100644 index 00000000..80ffce6d --- /dev/null +++ b/ai-plan/public/archive/microsoft-di-container-disposal/traces/microsoft-di-container-disposal-trace.md @@ -0,0 +1,31 @@ +# MicrosoftDiContainer Disposal Trace + +## 2026-05-06 + +### MDC-DISPOSE-RP-001 Issue #327 disposal repair + +- Trigger: + - issue `#327` reports that `MicrosoftDiContainer` holds a `ReaderWriterLockSlim` and a frozen `IServiceProvider` + but never releases either resource explicitly + - CQRS benchmark types keep `MicrosoftDiContainer` fields alive across runs and currently only dispose the MediatR + `ServiceProvider` side + - `RequestStartupBenchmarks` also creates temporary GFramework runtimes whose backing containers are never surfaced + for cleanup +- Decisions: + - treat the fix as a container lifetime contract update, not only a benchmark workaround + - add the disposal contract at the `IIocContainer` abstraction so callers holding interface references can release the + container explicitly + - keep runtime ownership unchanged; benchmarks that create containers remain responsible for disposing them +- Implementation notes: + - `MicrosoftDiContainer` now releases its frozen root `IServiceProvider`, clears registration state, disposes the + internal `ReaderWriterLockSlim`, and rejects all later operations with `ObjectDisposedException` + - `RequestStartupBenchmarks` was rewritten so the steady-state runtime keeps an explicit container field and the + cold-start benchmark disposes its temporary container in the same measured invocation + - other benchmark classes that own `MicrosoftDiContainer` fields now dispose them during `GlobalCleanup` +- Validation milestone: + - `python3 scripts/license-header.py --check` passed + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainer|FullyQualifiedName~IocContainerLifetimeTests"` passed (`55/55`) + - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` passed with `0 warnings / 0 errors` + - `dotnet build GFramework.sln -c Release` passed with `0 warnings / 0 errors` +- Immediate next step: + - archive the topic and push the branch