fix(core): 修复容器释放与基准资源泄漏

- 修复 MicrosoftDiContainer 的 IDisposable 释放逻辑、根 ServiceProvider 清理与释放后访问保护
- 更新 CQRS benchmarks 的容器 cleanup,并补齐 RequestStartupBenchmarks 的冷启动容器释放路径
- 补充 Core 容器生命周期回归测试并归档 issue 327 的 ai-plan topic
This commit is contained in:
gewuyou 2026-05-06 19:08:48 +08:00
parent 588800bb7b
commit 0ec8aa076b
13 changed files with 241 additions and 8 deletions

View File

@ -10,7 +10,7 @@ namespace GFramework.Core.Abstractions.Ioc;
/// <summary>
/// 依赖注入容器接口,定义了服务注册、解析和管理的基本操作
/// </summary>
public interface IIocContainer : IContextAware
public interface IIocContainer : IContextAware, IDisposable
{
#region Register Methods

View File

@ -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();
}
}
[Test]
public void Dispose_Should_Dispose_Resolved_Singleton_And_Block_Further_Use()
{
// Arrange
var container = new MicrosoftDiContainer();
container.RegisterSingleton<DisposableTestService, DisposableTestService>();
container.Freeze();
var service = container.GetRequired<DisposableTestService>();
// Act
container.Dispose();
// Assert
Assert.That(service.IsDisposed, Is.True);
Assert.Throws<ObjectDisposedException>(() => container.Get<DisposableTestService>());
Assert.Throws<ObjectDisposedException>(() => container.CreateScope());
}
[Test]
public void Dispose_Should_Be_Idempotent()
{
var container = new MicrosoftDiContainer();
Assert.DoesNotThrow(container.Dispose);
Assert.DoesNotThrow(container.Dispose);
}
}

View File

@ -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));
}
/// <summary>
/// 测试容器释放后会阻止后续注册与解析,避免 benchmark 或短生命周期宿主继续使用已回收状态。
/// </summary>
[Test]
public void Dispose_Should_Block_Subsequent_Registration_And_Query_Operations()
{
_container.Dispose();
Assert.Throws<ObjectDisposedException>(() => _container.Register(new TestService()));
Assert.Throws<ObjectDisposedException>(() => _container.Contains<TestService>());
Assert.Throws<ObjectDisposedException>(() => _container.GetAll<TestService>());
}
}

View File

@ -22,6 +22,19 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
{
#region Helper Methods
/// <summary>
/// 检查容器是否已释放,避免访问已经失效的服务提供者与同步原语。
/// </summary>
/// <exception cref="ObjectDisposedException">当容器已释放时抛出。</exception>
private void ThrowIfDisposed()
{
if (!_disposed) return;
const string objectName = nameof(MicrosoftDiContainer);
_logger.Warn("Attempted to use a disposed MicrosoftDiContainer.");
throw new ObjectDisposedException(objectName);
}
/// <summary>
/// 检查容器是否已冻结,如果已冻结则抛出异常
/// 用于保护注册操作的安全性
@ -57,6 +70,11 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// </summary>
private volatile bool _frozen;
/// <summary>
/// 容器释放状态标志true 表示容器已释放,不允许继续访问。
/// </summary>
private volatile bool _disposed;
/// <summary>
/// 读写锁,确保多线程环境下的线程安全操作
/// </summary>
@ -85,6 +103,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <exception cref="InvalidOperationException">当容器已冻结或类型已被注册时抛出</exception>
public void RegisterSingleton<T>(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)
/// <exception cref="InvalidOperationException">当容器已冻结时抛出</exception>
public void RegisterPlurality(object instance)
{
ThrowIfDisposed();
var concreteType = instance.GetType();
var interfaces = concreteType.GetInterfaces();
@ -219,6 +242,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// </summary>
public void RegisterPlurality<T>() where T : class
{
ThrowIfDisposed();
_lock.EnterWriteLock();
try
{
@ -262,6 +286,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <exception cref="InvalidOperationException">当容器已冻结时抛出</exception>
public void Register<T>(T instance)
{
ThrowIfDisposed();
_lock.EnterWriteLock();
try
{
@ -284,6 +309,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <exception cref="InvalidOperationException">当容器已冻结时抛出</exception>
public void Register(Type type, object instance)
{
ThrowIfDisposed();
_lock.EnterWriteLock();
try
{
@ -307,6 +333,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
public void RegisterFactory<TService>(
Func<IServiceProvider, TService> factory) where TService : class
{
ThrowIfDisposed();
_lock.EnterWriteLock();
try
{
@ -328,6 +355,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
public void RegisterCqrsPipelineBehavior<TBehavior>() where TBehavior : class
{
ThrowIfDisposed();
_lock.EnterWriteLock();
try
{
@ -392,6 +420,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
{
ArgumentNullException.ThrowIfNull(assemblies);
ThrowIfDisposed();
var assemblyArray = assemblies.ToArray();
foreach (var assembly in assemblyArray)
{
@ -419,6 +448,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <param name="configurator">服务配置委托</param>
public void ExecuteServicesHook(Action<IServiceCollection>? configurator = null)
{
ThrowIfDisposed();
_lock.EnterWriteLock();
try
{
@ -464,6 +494,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <returns>服务实例或null</returns>
public T? Get<T>() where T : class
{
ThrowIfDisposed();
_lock.EnterReadLock();
try
{
@ -503,6 +534,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <returns>服务实例或null</returns>
public object? Get(Type type)
{
ThrowIfDisposed();
_lock.EnterReadLock();
try
{
@ -593,6 +625,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <returns>只读的服务实例列表</returns>
public IReadOnlyList<T> GetAll<T>() where T : class
{
ThrowIfDisposed();
_lock.EnterReadLock();
try
{
@ -620,6 +653,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
public IReadOnlyList<object> GetAll(Type type)
{
ArgumentNullException.ThrowIfNull(type);
ThrowIfDisposed();
_lock.EnterReadLock();
try
@ -750,6 +784,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <returns>排序后的只读服务实例列表</returns>
public IReadOnlyList<T> GetAllSorted<T>(Comparison<T> comparison) where T : class
{
ThrowIfDisposed();
var list = GetAll<T>().ToList();
list.Sort(comparison);
return list;
@ -816,6 +851,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <returns>true表示包含该类型实例false表示不包含</returns>
public bool Contains<T>() where T : class
{
ThrowIfDisposed();
_lock.EnterReadLock();
try
{
@ -838,6 +874,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <returns>true表示包含该实例false表示不包含</returns>
public bool ContainsInstance(object instance)
{
ThrowIfDisposed();
_lock.EnterReadLock();
try
{
@ -855,6 +892,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// </summary>
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)
/// </summary>
public void Freeze()
{
ThrowIfDisposed();
_lock.EnterWriteLock();
try
{
@ -917,6 +958,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <exception cref="InvalidOperationException">当容器未冻结时抛出</exception>
public IServiceScope CreateScope()
{
ThrowIfDisposed();
_lock.EnterReadLock();
try
{
@ -938,5 +980,44 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
}
/// <summary>
/// 释放容器持有的服务提供者、注册状态和同步原语。
/// </summary>
/// <remarks>
/// 冻结后的根 <see cref="IServiceProvider" /> 会拥有 DI 创建的单例与作用域根缓存,因此 benchmark、
/// 测试宿主或短生命周期架构在结束时需要显式释放容器,避免这些对象与内部
/// <see cref="ReaderWriterLockSlim" /> 一起滞留。
/// 释放是幂等的;首次释放后所有后续访问都会抛出 <see cref="ObjectDisposedException" />。
/// </remarks>
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
}

View File

@ -83,6 +83,7 @@ public class NotificationBenchmarks
[GlobalCleanup]
public void Cleanup()
{
_container.Dispose();
_serviceProvider.Dispose();
}

View File

@ -85,6 +85,7 @@ public class RequestBenchmarks
[GlobalCleanup]
public void Cleanup()
{
_container.Dispose();
_serviceProvider.Dispose();
}

View File

@ -103,6 +103,8 @@ public class RequestInvokerBenchmarks
[GlobalCleanup]
public void Cleanup()
{
_reflectionContainer.Dispose();
_generatedContainer.Dispose();
_serviceProvider.Dispose();
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
}

View File

@ -101,6 +101,7 @@ public class RequestPipelineBenchmarks
[GlobalCleanup]
public void Cleanup()
{
_container.Dispose();
_serviceProvider.Dispose();
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
}

View File

@ -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<IMediator>();
_runtime = CreateGFrameworkRuntime();
_container = CreateGFrameworkContainer();
_runtime = CreateGFrameworkRuntime(_container);
}
/// <summary>
@ -84,6 +86,7 @@ public class RequestStartupBenchmarks
[GlobalCleanup]
public void Cleanup()
{
_container.Dispose();
_serviceProvider.Dispose();
}
@ -124,10 +127,11 @@ public class RequestStartupBenchmarks
/// </summary>
[Benchmark]
[BenchmarkCategory("ColdStart")]
public ValueTask<BenchmarkResponse> ColdStart_GFrameworkCqrs()
public async ValueTask<BenchmarkResponse> 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);
}
/// <summary>
@ -137,12 +141,21 @@ public class RequestStartupBenchmarks
/// 该 benchmark 故意保持与 MediatR 对照组同样的“单 handler 最小宿主”模型,
/// 因此这里继续使用单点手工注册,而不引入依赖完整 CQRS 注册协调器的程序集扫描路径。
/// </remarks>
private static ICqrsRuntime CreateGFrameworkRuntime()
private static MicrosoftDiContainer CreateGFrameworkContainer()
{
var container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static currentContainer =>
return BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static currentContainer =>
{
currentContainer.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>, BenchmarkRequestHandler>();
});
}
/// <summary>
/// 基于已冻结的 benchmark 容器构建最小 GFramework.CQRS runtime。
/// </summary>
/// <param name="container">当前 benchmark 拥有并负责释放的容器。</param>
private static ICqrsRuntime CreateGFrameworkRuntime(MicrosoftDiContainer container)
{
ArgumentNullException.ThrowIfNull(container);
return GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, RuntimeLogger);
}

View File

@ -103,6 +103,8 @@ public class StreamInvokerBenchmarks
[GlobalCleanup]
public void Cleanup()
{
_reflectionContainer.Dispose();
_generatedContainer.Dispose();
_serviceProvider.Dispose();
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
}

View File

@ -86,6 +86,7 @@ public class StreamingBenchmarks
[GlobalCleanup]
public void Cleanup()
{
_container.Dispose();
_serviceProvider.Dispose();
}

View File

@ -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.

View File

@ -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