GFramework/GFramework.Core.Tests/Ioc/IocContainerLifetimeTests.cs
gewuyou f0a2978882 fix(core): 修复容器并发释放重复销毁锁
- 修复 MicrosoftDiContainer 在并发 Dispose 场景下可能重复执行底层读写锁销毁的问题

- 补充 IocContainerLifetimeTests 回归用例以覆盖并发释放时的单次锁销毁约束

- 更新 microsoft-di-container-disposal 追踪文档记录剩余 PR review 处理结果
2026-05-06 20:39:38 +08:00

334 lines
10 KiB
C#

// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Ioc;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 测试 IoC 容器生命周期功能
/// </summary>
[TestFixture]
public class IocContainerLifetimeTests
{
private interface ITestService
{
Guid Id { get; }
}
private class TestService : ITestService
{
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()
{
// Arrange
var container = new MicrosoftDiContainer();
container.RegisterSingleton<ITestService, TestService>();
container.Freeze();
// Act
var instance1 = container.Get<ITestService>();
var instance2 = container.Get<ITestService>();
// Assert
Assert.That(instance1, Is.Not.Null);
Assert.That(instance2, Is.Not.Null);
Assert.That(instance1!.Id, Is.EqualTo(instance2!.Id));
}
[Test]
public void RegisterTransient_Should_Return_Different_Instances()
{
// Arrange
var container = new MicrosoftDiContainer();
container.RegisterTransient<ITestService, TestService>();
container.Freeze();
// Act
var instance1 = container.Get<ITestService>();
var instance2 = container.Get<ITestService>();
// Assert
Assert.That(instance1, Is.Not.Null);
Assert.That(instance2, Is.Not.Null);
Assert.That(instance1!.Id, Is.Not.EqualTo(instance2!.Id));
}
[Test]
public void RegisterScoped_Should_Return_Same_Instance_Within_Scope()
{
// Arrange
var container = new MicrosoftDiContainer();
container.RegisterScoped<ITestService, TestService>();
container.Freeze();
// Act
using var scope = container.CreateScope();
var instance1 = scope.ServiceProvider.GetService<ITestService>();
var instance2 = scope.ServiceProvider.GetService<ITestService>();
// Assert
Assert.That(instance1, Is.Not.Null);
Assert.That(instance2, Is.Not.Null);
Assert.That(instance1!.Id, Is.EqualTo(instance2!.Id));
}
[Test]
public void RegisterScoped_Should_Return_Different_Instances_Across_Scopes()
{
// Arrange
var container = new MicrosoftDiContainer();
container.RegisterScoped<ITestService, TestService>();
container.Freeze();
// Act
ITestService? instance1;
ITestService? instance2;
using (var scope1 = container.CreateScope())
{
instance1 = scope1.ServiceProvider.GetService<ITestService>();
}
using (var scope2 = container.CreateScope())
{
instance2 = scope2.ServiceProvider.GetService<ITestService>();
}
// Assert
Assert.That(instance1, Is.Not.Null);
Assert.That(instance2, Is.Not.Null);
Assert.That(instance1!.Id, Is.Not.EqualTo(instance2!.Id));
}
[Test]
public void CreateScope_Should_Throw_When_Container_Not_Frozen()
{
// Arrange
var container = new MicrosoftDiContainer();
container.RegisterScoped<ITestService, TestService>();
// Act & Assert
Assert.Throws<InvalidOperationException>(() => container.CreateScope());
}
[Test]
public void RegisterTransient_Should_Throw_When_Container_Is_Frozen()
{
// Arrange
var container = new MicrosoftDiContainer();
container.Freeze();
// Act & Assert
Assert.Throws<InvalidOperationException>(() =>
container.RegisterTransient<ITestService, TestService>());
}
[Test]
public void RegisterScoped_Should_Throw_When_Container_Is_Frozen()
{
// Arrange
var container = new MicrosoftDiContainer();
container.Freeze();
// Act & Assert
Assert.Throws<InvalidOperationException>(() =>
container.RegisterScoped<ITestService, TestService>());
}
[Test]
public void Mixed_Lifetimes_Should_Work_Together()
{
// Arrange
var container = new MicrosoftDiContainer();
container.RegisterSingleton<ITestService, TestService>();
container.RegisterTransient<ITestService, TestService>();
container.RegisterScoped<ITestService, TestService>();
container.Freeze();
// Act
var singletonInstances = container.GetAll<ITestService>().ToList();
// Assert
Assert.That(singletonInstances.Count, Is.EqualTo(3));
}
[Test]
public void Scoped_Service_Should_Be_Disposed_When_Scope_Disposed()
{
// Arrange
var container = new MicrosoftDiContainer();
container.RegisterScoped<ITestService, TestService>();
container.Freeze();
ITestService? instance;
using (var scope = container.CreateScope())
{
instance = scope.ServiceProvider.GetService<ITestService>();
Assert.That(instance, Is.Not.Null);
}
// Act & Assert - 作用域已释放,实例应该被清理
// 注意:这里只是验证作用域可以正常释放,无法直接验证实例是否被 Dispose
Assert.Pass("Scope disposed successfully");
}
[Test]
public void Multiple_Scopes_Can_Be_Created_Concurrently()
{
// Arrange
var container = new MicrosoftDiContainer();
container.RegisterScoped<ITestService, TestService>();
container.Freeze();
// Act
var scope1 = container.CreateScope();
var scope2 = container.CreateScope();
var scope3 = container.CreateScope();
var instance1 = scope1.ServiceProvider.GetService<ITestService>();
var instance2 = scope2.ServiceProvider.GetService<ITestService>();
var instance3 = scope3.ServiceProvider.GetService<ITestService>();
// Assert
Assert.That(instance1, Is.Not.Null);
Assert.That(instance2, Is.Not.Null);
Assert.That(instance3, Is.Not.Null);
Assert.That(instance1!.Id, Is.Not.EqualTo(instance2!.Id));
Assert.That(instance2!.Id, Is.Not.EqualTo(instance3!.Id));
Assert.That(instance1!.Id, Is.Not.EqualTo(instance3!.Id));
// Cleanup
scope1.Dispose();
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);
}
[Test]
public void Dispose_Should_Be_Idempotent_When_Called_Concurrently()
{
var container = new MicrosoftDiContainer();
var containerLock = GetContainerLock(container);
var releasedGate = false;
containerLock.EnterWriteLock();
try
{
var firstDisposeTask = Task.Run(container.Dispose);
Thread.Sleep(50);
var secondDisposeTask = Task.Run(container.Dispose);
Thread.Sleep(50);
containerLock.ExitWriteLock();
releasedGate = true;
Assert.That(async () => await Task.WhenAll(firstDisposeTask, secondDisposeTask).ConfigureAwait(false), Throws.Nothing);
}
finally
{
if (!releasedGate)
{
containerLock.ExitWriteLock();
}
}
}
[Test]
public void Dispose_Should_Only_Attempt_Lock_Disposal_Once_When_Called_Concurrently()
{
var container = new MicrosoftDiContainer();
var containerLock = GetContainerLock(container);
var releasedGate = false;
containerLock.EnterWriteLock();
try
{
var firstDisposeTask = Task.Run(container.Dispose);
Thread.Sleep(50);
var secondDisposeTask = Task.Run(container.Dispose);
Thread.Sleep(50);
containerLock.ExitWriteLock();
releasedGate = true;
Assert.That(async () => await Task.WhenAll(firstDisposeTask, secondDisposeTask).ConfigureAwait(false), Throws.Nothing);
Assert.That(GetLockDisposalStarted(container), Is.EqualTo(1));
}
finally
{
if (!releasedGate)
{
containerLock.ExitWriteLock();
}
}
}
/// <summary>
/// 通过反射获取容器内部锁,用于构造可重复的并发释放竞态回归。
/// </summary>
private static ReaderWriterLockSlim GetContainerLock(MicrosoftDiContainer container)
{
ArgumentNullException.ThrowIfNull(container);
var lockField = typeof(MicrosoftDiContainer).GetField("_lock", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.That(lockField, Is.Not.Null);
return (ReaderWriterLockSlim)lockField!.GetValue(container)!;
}
/// <summary>
/// 读取锁销毁启动标记,验证并发释放路径不会重复执行底层锁销毁。
/// </summary>
private static int GetLockDisposalStarted(MicrosoftDiContainer container)
{
ArgumentNullException.ThrowIfNull(container);
var flagField = typeof(MicrosoftDiContainer).GetField("_lockDisposalStarted", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.That(flagField, Is.Not.Null);
return (int)flagField!.GetValue(container)!;
}
}