diff --git a/TUnit.Core/Attributes/TestData/ClassDataSources.cs b/TUnit.Core/Attributes/TestData/ClassDataSources.cs
index a933e860b4..efad3790cd 100644
--- a/TUnit.Core/Attributes/TestData/ClassDataSources.cs
+++ b/TUnit.Core/Attributes/TestData/ClassDataSources.cs
@@ -73,24 +73,21 @@ private string GetKey(int index, SharedType[] sharedTypes, string[] keys)
private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, DataGeneratorMetadata dataGeneratorMetadata)
{
- return CreateWithNestedDependencies(type, dataGeneratorMetadata, recursionDepth: 0);
+ return Create(type, dataGeneratorMetadata, recursionDepth: 0);
}
private const int MaxRecursionDepth = 10;
- [UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' requirements",
- Justification = "PropertyType from PropertyInjectionMetadata has the required DynamicallyAccessedMembers annotations")]
- private static object CreateWithNestedDependencies([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, DataGeneratorMetadata dataGeneratorMetadata, int recursionDepth)
+ private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, DataGeneratorMetadata dataGeneratorMetadata, int recursionDepth)
{
if (recursionDepth >= MaxRecursionDepth)
{
throw new InvalidOperationException($"Maximum recursion depth ({MaxRecursionDepth}) exceeded when creating nested ClassDataSource dependencies. This may indicate a circular dependency.");
}
- object instance;
try
{
- instance = Activator.CreateInstance(type)!;
+ return Activator.CreateInstance(type)!;
}
catch (TargetInvocationException targetInvocationException)
{
@@ -101,21 +98,5 @@ private static object CreateWithNestedDependencies([DynamicallyAccessedMembers(D
throw;
}
-
- // Populate nested ClassDataSource properties recursively
- var propertySource = PropertySourceRegistry.GetSource(type);
- if (propertySource?.ShouldInitialize == true)
- {
- var propertyMetadata = propertySource.GetPropertyMetadata();
- foreach (var metadata in propertyMetadata)
- {
- // Recursively create the property value using CreateWithNestedDependencies
- // This will handle nested ClassDataSource properties
- var propertyValue = CreateWithNestedDependencies(metadata.PropertyType, dataGeneratorMetadata, recursionDepth + 1);
- metadata.SetProperty(instance, propertyValue);
- }
- }
-
- return instance;
}
}
diff --git a/TUnit.TestProject/Bugs/3803/TestRabbitContainer.cs b/TUnit.TestProject/Bugs/3803/TestRabbitContainer.cs
new file mode 100644
index 0000000000..58c1a14622
--- /dev/null
+++ b/TUnit.TestProject/Bugs/3803/TestRabbitContainer.cs
@@ -0,0 +1,51 @@
+using TUnit.Core.Interfaces;
+
+namespace TUnit.TestProject.Bugs._3803;
+
+///
+/// Simulates a RabbitMQ test container.
+/// This class should be instantiated only once per test session when marked as SharedType.PerTestSession.
+///
+public class TestRabbitContainer : IAsyncInitializer, IAsyncDisposable
+{
+ private static int _instanceCount = 0;
+ private static int _initializeCount = 0;
+ private static int _disposeCount = 0;
+
+ public static int InstanceCount => _instanceCount;
+ public static int InitializeCount => _initializeCount;
+ public static int DisposeCount => _disposeCount;
+
+ public int InstanceId { get; }
+ public bool IsInitialized { get; private set; }
+ public bool IsDisposed { get; private set; }
+
+ public TestRabbitContainer()
+ {
+ InstanceId = Interlocked.Increment(ref _instanceCount);
+ Console.WriteLine($"[TestRabbitContainer] Constructor called - Instance #{InstanceId}");
+ }
+
+ public Task InitializeAsync()
+ {
+ Interlocked.Increment(ref _initializeCount);
+ IsInitialized = true;
+ Console.WriteLine($"[TestRabbitContainer] InitializeAsync called - Instance #{InstanceId}");
+ return Task.CompletedTask;
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ Interlocked.Increment(ref _disposeCount);
+ IsDisposed = true;
+ Console.WriteLine($"[TestRabbitContainer] DisposeAsync called - Instance #{InstanceId}");
+ return default;
+ }
+
+ public static void ResetCounters()
+ {
+ _instanceCount = 0;
+ _initializeCount = 0;
+ _disposeCount = 0;
+ }
+}
diff --git a/TUnit.TestProject/Bugs/3803/TestSqlContainer.cs b/TUnit.TestProject/Bugs/3803/TestSqlContainer.cs
new file mode 100644
index 0000000000..57c6f317db
--- /dev/null
+++ b/TUnit.TestProject/Bugs/3803/TestSqlContainer.cs
@@ -0,0 +1,51 @@
+using TUnit.Core.Interfaces;
+
+namespace TUnit.TestProject.Bugs._3803;
+
+///
+/// Simulates a SQL test container.
+/// This class should be instantiated only once per test session when marked as SharedType.PerTestSession.
+///
+public class TestSqlContainer : IAsyncInitializer, IAsyncDisposable
+{
+ private static int _instanceCount = 0;
+ private static int _initializeCount = 0;
+ private static int _disposeCount = 0;
+
+ public static int InstanceCount => _instanceCount;
+ public static int InitializeCount => _initializeCount;
+ public static int DisposeCount => _disposeCount;
+
+ public int InstanceId { get; }
+ public bool IsInitialized { get; private set; }
+ public bool IsDisposed { get; private set; }
+
+ public TestSqlContainer()
+ {
+ InstanceId = Interlocked.Increment(ref _instanceCount);
+ Console.WriteLine($"[TestSqlContainer] Constructor called - Instance #{InstanceId}");
+ }
+
+ public Task InitializeAsync()
+ {
+ Interlocked.Increment(ref _initializeCount);
+ IsInitialized = true;
+ Console.WriteLine($"[TestSqlContainer] InitializeAsync called - Instance #{InstanceId}");
+ return Task.CompletedTask;
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ Interlocked.Increment(ref _disposeCount);
+ IsDisposed = true;
+ Console.WriteLine($"[TestSqlContainer] DisposeAsync called - Instance #{InstanceId}");
+ return default;
+ }
+
+ public static void ResetCounters()
+ {
+ _instanceCount = 0;
+ _initializeCount = 0;
+ _disposeCount = 0;
+ }
+}
diff --git a/TUnit.TestProject/Bugs/3803/Tests.cs b/TUnit.TestProject/Bugs/3803/Tests.cs
new file mode 100644
index 0000000000..556c173d52
--- /dev/null
+++ b/TUnit.TestProject/Bugs/3803/Tests.cs
@@ -0,0 +1,135 @@
+using TUnit.TestProject.Attributes;
+
+namespace TUnit.TestProject.Bugs._3803;
+
+///
+/// Bug #3803: Nested dependencies with SharedType.PerTestSession are instantiated multiple times
+///
+/// Expected behavior:
+/// - TestRabbitContainer should be instantiated ONCE per test session (InstanceCount == 1)
+/// - TestSqlContainer should be instantiated ONCE per test session (InstanceCount == 1)
+/// - All WebApplicationFactory instances should receive the SAME container instances
+///
+/// Actual behavior (BUG):
+/// - Containers are instantiated multiple times (once per test or once per factory)
+/// - Each WebApplicationFactory receives DIFFERENT container instances
+///
+
+[NotInParallel]
+[EngineTest(ExpectedResult.Pass)]
+public class Bug3803_TestClass1
+{
+ [ClassDataSource(Shared = SharedType.PerTestSession)]
+ public required WebApplicationFactory Factory { get; init; }
+
+ [Test]
+ public async Task Test1_VerifyContainersAreShared()
+ {
+ Console.WriteLine($"[Bug3803_TestClass1.Test1] Factory Instance: #{Factory.InstanceId}");
+ Console.WriteLine($"[Bug3803_TestClass1.Test1] RabbitContainer Instance: #{Factory.RabbitContainer.InstanceId}");
+ Console.WriteLine($"[Bug3803_TestClass1.Test1] SqlContainer Instance: #{Factory.SqlContainer.InstanceId}");
+
+ // Verify containers are initialized
+ await Assert.That(Factory.RabbitContainer.IsInitialized).IsTrue();
+ await Assert.That(Factory.SqlContainer.IsInitialized).IsTrue();
+
+ // BUG VERIFICATION: These should ALWAYS be 1 if SharedType.PerTestSession works correctly
+ await Assert.That(TestRabbitContainer.InstanceCount).IsEqualTo(1);
+ await Assert.That(TestSqlContainer.InstanceCount).IsEqualTo(1);
+
+ // All instances should have ID = 1 (first and only instance)
+ await Assert.That(Factory.RabbitContainer.InstanceId).IsEqualTo(1);
+ await Assert.That(Factory.SqlContainer.InstanceId).IsEqualTo(1);
+ }
+
+ [Test]
+ public async Task Test2_VerifyContainersAreStillShared()
+ {
+ Console.WriteLine($"[Bug3803_TestClass1.Test2] Factory Instance: #{Factory.InstanceId}");
+ Console.WriteLine($"[Bug3803_TestClass1.Test2] RabbitContainer Instance: #{Factory.RabbitContainer.InstanceId}");
+ Console.WriteLine($"[Bug3803_TestClass1.Test2] SqlContainer Instance: #{Factory.SqlContainer.InstanceId}");
+
+ // Same assertions as Test1 - containers should still be the same instances
+ await Assert.That(TestRabbitContainer.InstanceCount).IsEqualTo(1);
+ await Assert.That(TestSqlContainer.InstanceCount).IsEqualTo(1);
+ await Assert.That(Factory.RabbitContainer.InstanceId).IsEqualTo(1);
+ await Assert.That(Factory.SqlContainer.InstanceId).IsEqualTo(1);
+ }
+
+ [Test]
+ public async Task Test3_VerifyInitializationCalledOnce()
+ {
+ Console.WriteLine($"[Bug3803_TestClass1.Test3] RabbitContainer InitializeCount: {TestRabbitContainer.InitializeCount}");
+ Console.WriteLine($"[Bug3803_TestClass1.Test3] SqlContainer InitializeCount: {TestSqlContainer.InitializeCount}");
+
+ // Initialize should be called only once per container
+ await Assert.That(TestRabbitContainer.InitializeCount).IsEqualTo(1);
+ await Assert.That(TestSqlContainer.InitializeCount).IsEqualTo(1);
+ }
+}
+
+[NotInParallel]
+[EngineTest(ExpectedResult.Pass)]
+public class Bug3803_TestClass2
+{
+ [ClassDataSource(Shared = SharedType.PerTestSession)]
+ public required WebApplicationFactory Factory { get; init; }
+
+ [Test]
+ public async Task Test1_DifferentClassShouldGetSameContainers()
+ {
+ Console.WriteLine($"[Bug3803_TestClass2.Test1] Factory Instance: #{Factory.InstanceId}");
+ Console.WriteLine($"[Bug3803_TestClass2.Test1] RabbitContainer Instance: #{Factory.RabbitContainer.InstanceId}");
+ Console.WriteLine($"[Bug3803_TestClass2.Test1] SqlContainer Instance: #{Factory.SqlContainer.InstanceId}");
+
+ // Even in a different test class, we should get the SAME container instances
+ await Assert.That(TestRabbitContainer.InstanceCount).IsEqualTo(1);
+ await Assert.That(TestSqlContainer.InstanceCount).IsEqualTo(1);
+
+ // Should be the same instance (ID = 1)
+ await Assert.That(Factory.RabbitContainer.InstanceId).IsEqualTo(1);
+ await Assert.That(Factory.SqlContainer.InstanceId).IsEqualTo(1);
+ }
+
+ [Test]
+ public async Task Test2_VerifyContainersAreInitialized()
+ {
+ await Assert.That(Factory.RabbitContainer.IsInitialized).IsTrue();
+ await Assert.That(Factory.SqlContainer.IsInitialized).IsTrue();
+ }
+}
+
+[NotInParallel]
+[EngineTest(ExpectedResult.Pass)]
+public class Bug3803_TestClass3
+{
+ [ClassDataSource(Shared = SharedType.PerTestSession)]
+ public required WebApplicationFactory Factory { get; init; }
+
+ [Test]
+ public async Task Test1_ThirdClassAlsoGetsSameContainers()
+ {
+ Console.WriteLine($"[Bug3803_TestClass3.Test1] Factory Instance: #{Factory.InstanceId}");
+ Console.WriteLine($"[Bug3803_TestClass3.Test1] RabbitContainer Instance: #{Factory.RabbitContainer.InstanceId}");
+ Console.WriteLine($"[Bug3803_TestClass3.Test1] SqlContainer Instance: #{Factory.SqlContainer.InstanceId}");
+
+ // Still the same instances
+ await Assert.That(TestRabbitContainer.InstanceCount).IsEqualTo(1);
+ await Assert.That(TestSqlContainer.InstanceCount).IsEqualTo(1);
+ await Assert.That(Factory.RabbitContainer.InstanceId).IsEqualTo(1);
+ await Assert.That(Factory.SqlContainer.InstanceId).IsEqualTo(1);
+ }
+
+ [Test]
+ public async Task Test2_FinalVerification()
+ {
+ Console.WriteLine($"[Bug3803_TestClass3.Test2] Final verification");
+ Console.WriteLine($" Total RabbitContainer instances: {TestRabbitContainer.InstanceCount}");
+ Console.WriteLine($" Total SqlContainer instances: {TestSqlContainer.InstanceCount}");
+ Console.WriteLine($" Total WebApplicationFactory instances: {WebApplicationFactory.InstanceCount}");
+
+ // Final assertion: containers should have been instantiated exactly once
+ await Assert.That(TestRabbitContainer.InstanceCount).IsEqualTo(1);
+ await Assert.That(TestSqlContainer.InstanceCount).IsEqualTo(1);
+ }
+}
diff --git a/TUnit.TestProject/Bugs/3803/WebApplicationFactory.cs b/TUnit.TestProject/Bugs/3803/WebApplicationFactory.cs
new file mode 100644
index 0000000000..3172f4d792
--- /dev/null
+++ b/TUnit.TestProject/Bugs/3803/WebApplicationFactory.cs
@@ -0,0 +1,61 @@
+using TUnit.Core.Interfaces;
+
+namespace TUnit.TestProject.Bugs._3803;
+
+///
+/// Simulates a WebApplicationFactory that depends on test containers.
+/// The containers should be shared (SharedType.PerTestSession), meaning:
+/// - Each container should be instantiated only ONCE per test session
+/// - All instances of WebApplicationFactory should receive the SAME container instances
+///
+public class WebApplicationFactory : IAsyncInitializer, IAsyncDisposable
+{
+ private static int _instanceCount = 0;
+ private static int _initializeCount = 0;
+ private static int _disposeCount = 0;
+
+ public static int InstanceCount => _instanceCount;
+ public static int InitializeCount => _initializeCount;
+ public static int DisposeCount => _disposeCount;
+
+ public int InstanceId { get; }
+ public bool IsInitialized { get; private set; }
+ public bool IsDisposed { get; private set; }
+
+ [ClassDataSource(Shared = SharedType.PerTestSession)]
+ public required TestRabbitContainer RabbitContainer { get; init; }
+
+ [ClassDataSource(Shared = SharedType.PerTestSession)]
+ public required TestSqlContainer SqlContainer { get; init; }
+
+ public WebApplicationFactory()
+ {
+ InstanceId = Interlocked.Increment(ref _instanceCount);
+ Console.WriteLine($"[WebApplicationFactory] Constructor called - Instance #{InstanceId}");
+ }
+
+ public Task InitializeAsync()
+ {
+ Interlocked.Increment(ref _initializeCount);
+ IsInitialized = true;
+ Console.WriteLine($"[WebApplicationFactory] InitializeAsync called - Instance #{InstanceId}");
+ Console.WriteLine($" -> RabbitContainer Instance: #{RabbitContainer.InstanceId}");
+ Console.WriteLine($" -> SqlContainer Instance: #{SqlContainer.InstanceId}");
+ return Task.CompletedTask;
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ Interlocked.Increment(ref _disposeCount);
+ IsDisposed = true;
+ Console.WriteLine($"[WebApplicationFactory] DisposeAsync called - Instance #{InstanceId}");
+ return default;
+ }
+
+ public static void ResetCounters()
+ {
+ _instanceCount = 0;
+ _initializeCount = 0;
+ _disposeCount = 0;
+ }
+}