diff --git a/TUnit.AspNetCore/WebApplicationTest.cs b/TUnit.AspNetCore/WebApplicationTest.cs
index 00a4793fb9..15b43fa317 100644
--- a/TUnit.AspNetCore/WebApplicationTest.cs
+++ b/TUnit.AspNetCore/WebApplicationTest.cs
@@ -10,17 +10,15 @@ namespace TUnit.AspNetCore;
public abstract class WebApplicationTest
{
- internal static int _idCounter;
-
///
/// Gets a unique identifier for this test instance.
- /// Useful for creating isolated resources (tables, topics, keys) per test.
+ /// Delegates to to ensure consistency
+ /// regardless of whether accessed via this property or the TestContext API.
///
- public int UniqueId { get; }
+ public int UniqueId => TestContext.Current!.Isolation.UniqueId;
internal WebApplicationTest()
{
- UniqueId = Interlocked.Increment(ref _idCounter);
}
///
@@ -36,7 +34,7 @@ internal WebApplicationTest()
/// var topicName = GetIsolatedName("orders"); // Returns "Test_42_orders"
///
///
- protected string GetIsolatedName(string baseName) => $"Test_{UniqueId}_{baseName}";
+ protected string GetIsolatedName(string baseName) => TestContext.Current!.Isolation.GetIsolatedName(baseName);
///
/// Creates an isolated prefix using the test's unique identifier.
@@ -51,7 +49,7 @@ internal WebApplicationTest()
/// var dotPrefix = GetIsolatedPrefix("."); // Returns "test.42."
///
///
- protected string GetIsolatedPrefix(string separator = "_") => $"test{separator}{UniqueId}{separator}";
+ protected string GetIsolatedPrefix(string separator = "_") => TestContext.Current!.Isolation.GetIsolatedPrefix(separator);
}
///
diff --git a/TUnit.Core/Interfaces/ITestIsolation.cs b/TUnit.Core/Interfaces/ITestIsolation.cs
new file mode 100644
index 0000000000..5ed764f038
--- /dev/null
+++ b/TUnit.Core/Interfaces/ITestIsolation.cs
@@ -0,0 +1,46 @@
+namespace TUnit.Core.Interfaces;
+
+///
+/// Provides helpers for creating isolated resource names per test instance.
+/// Useful for database tables, queue names, cache keys, and other resources
+/// that need to be unique across parallel test execution.
+/// Accessed via .
+///
+public interface ITestIsolation
+{
+ ///
+ /// Gets a unique identifier for this test instance.
+ /// This value is assigned atomically and is guaranteed to be unique across all test instances in the process.
+ ///
+ int UniqueId { get; }
+
+ ///
+ /// Creates an isolated name by combining a base name with the test's unique identifier.
+ /// Use for database tables, Redis keys, Kafka topics, etc.
+ ///
+ /// The base name for the resource.
+ /// A unique name in the format "Test_{UniqueId}_{baseName}".
+ ///
+ ///
+ /// // In a test with UniqueId = 42:
+ /// var tableName = TestContext.Current!.Isolation.GetIsolatedName("todos"); // Returns "Test_42_todos"
+ /// var topicName = TestContext.Current!.Isolation.GetIsolatedName("orders"); // Returns "Test_42_orders"
+ ///
+ ///
+ string GetIsolatedName(string baseName);
+
+ ///
+ /// Creates an isolated prefix using the test's unique identifier.
+ /// Use for key prefixes in Redis, Kafka topic prefixes, etc.
+ ///
+ /// The separator character. Defaults to "_".
+ /// A unique prefix in the format "test{separator}{UniqueId}{separator}".
+ ///
+ ///
+ /// // In a test with UniqueId = 42:
+ /// var prefix = TestContext.Current!.Isolation.GetIsolatedPrefix(); // Returns "test_42_"
+ /// var dotPrefix = TestContext.Current!.Isolation.GetIsolatedPrefix("."); // Returns "test.42."
+ ///
+ ///
+ string GetIsolatedPrefix(string separator = "_");
+}
diff --git a/TUnit.Core/TestContext.Isolation.cs b/TUnit.Core/TestContext.Isolation.cs
new file mode 100644
index 0000000000..06845df5d5
--- /dev/null
+++ b/TUnit.Core/TestContext.Isolation.cs
@@ -0,0 +1,19 @@
+using TUnit.Core.Interfaces;
+
+namespace TUnit.Core;
+
+public partial class TestContext
+{
+ internal static int _isolationIdCounter;
+
+ internal int IsolationUniqueId { get; }
+
+ ///
+ int ITestIsolation.UniqueId => IsolationUniqueId;
+
+ ///
+ string ITestIsolation.GetIsolatedName(string baseName) => $"Test_{IsolationUniqueId}_{baseName}";
+
+ ///
+ string ITestIsolation.GetIsolatedPrefix(string separator) => $"test{separator}{IsolationUniqueId}{separator}";
+}
diff --git a/TUnit.Core/TestContext.cs b/TUnit.Core/TestContext.cs
index e38cd07699..37641c0f38 100644
--- a/TUnit.Core/TestContext.cs
+++ b/TUnit.Core/TestContext.cs
@@ -15,7 +15,7 @@ namespace TUnit.Core;
///
[DebuggerDisplay("{TestDetails.ClassType.Name}.{GetDisplayName(),nq}")]
public partial class TestContext : Context,
- ITestExecution, ITestParallelization, ITestOutput, ITestMetadata, ITestDependencies, ITestStateBag, ITestEvents
+ ITestExecution, ITestParallelization, ITestOutput, ITestMetadata, ITestDependencies, ITestStateBag, ITestEvents, ITestIsolation
{
private static readonly ConcurrentDictionary _testContextsById = new();
private readonly TestBuilderContext _testBuilderContext;
@@ -30,6 +30,7 @@ public TestContext(string testName, IServiceProvider serviceProvider, ClassHookC
// Generate unique ID for this test instance
Id = Guid.NewGuid().ToString();
+ IsolationUniqueId = Interlocked.Increment(ref _isolationIdCounter);
_testContextsById[Id] = this;
}
@@ -44,6 +45,7 @@ public TestContext(string testName, IServiceProvider serviceProvider, ClassHookC
public ITestDependencies Dependencies => this;
public ITestStateBag StateBag => this;
public ITestEvents Events => this;
+ public ITestIsolation Isolation => this;
internal IServiceProvider Services => ServiceProvider;
diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
index 76bad876d9..2b14a5f4d8 100644
--- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
@@ -1327,7 +1327,7 @@ namespace
public TestConstructorAttribute() { }
}
[.DebuggerDisplay("{.Name}.{GetDisplayName(),nq}")]
- public class TestContext : .Context, ., ., ., ., ., ., .
+ public class TestContext : .Context, ., ., ., ., ., ., ., .
{
public TestContext(string testName, serviceProvider, .ClassHookContext classContext, .TestBuilderContext testBuilderContext, .CancellationToken cancellationToken) { }
public .ClassHookContext ClassContext { get; }
@@ -1335,6 +1335,7 @@ namespace
public . Events { get; }
public . Execution { get; }
public string Id { get; }
+ public . Isolation { get; }
public object Lock { get; }
public . Metadata { get; }
public . Output { get; }
@@ -2477,6 +2478,12 @@ namespace .Interfaces
string TestId { get; }
string TestName { get; }
}
+ public interface ITestIsolation
+ {
+ int UniqueId { get; }
+ string GetIsolatedName(string baseName);
+ string GetIsolatedPrefix(string separator = "_");
+ }
public interface ITestLocation
{
string TestFilePath { get; }
diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
index 72c3b943a5..d9c1b339dd 100644
--- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
@@ -1327,7 +1327,7 @@ namespace
public TestConstructorAttribute() { }
}
[.DebuggerDisplay("{.Name}.{GetDisplayName(),nq}")]
- public class TestContext : .Context, ., ., ., ., ., ., .
+ public class TestContext : .Context, ., ., ., ., ., ., ., .
{
public TestContext(string testName, serviceProvider, .ClassHookContext classContext, .TestBuilderContext testBuilderContext, .CancellationToken cancellationToken) { }
public .ClassHookContext ClassContext { get; }
@@ -1335,6 +1335,7 @@ namespace
public . Events { get; }
public . Execution { get; }
public string Id { get; }
+ public . Isolation { get; }
public object Lock { get; }
public . Metadata { get; }
public . Output { get; }
@@ -2477,6 +2478,12 @@ namespace .Interfaces
string TestId { get; }
string TestName { get; }
}
+ public interface ITestIsolation
+ {
+ int UniqueId { get; }
+ string GetIsolatedName(string baseName);
+ string GetIsolatedPrefix(string separator = "_");
+ }
public interface ITestLocation
{
string TestFilePath { get; }
diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
index 1caf4068ab..1a03a28673 100644
--- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
@@ -1327,7 +1327,7 @@ namespace
public TestConstructorAttribute() { }
}
[.DebuggerDisplay("{.Name}.{GetDisplayName(),nq}")]
- public class TestContext : .Context, ., ., ., ., ., ., .
+ public class TestContext : .Context, ., ., ., ., ., ., ., .
{
public TestContext(string testName, serviceProvider, .ClassHookContext classContext, .TestBuilderContext testBuilderContext, .CancellationToken cancellationToken) { }
public .ClassHookContext ClassContext { get; }
@@ -1335,6 +1335,7 @@ namespace
public . Events { get; }
public . Execution { get; }
public string Id { get; }
+ public . Isolation { get; }
public object Lock { get; }
public . Metadata { get; }
public . Output { get; }
@@ -2477,6 +2478,12 @@ namespace .Interfaces
string TestId { get; }
string TestName { get; }
}
+ public interface ITestIsolation
+ {
+ int UniqueId { get; }
+ string GetIsolatedName(string baseName);
+ string GetIsolatedPrefix(string separator = "_");
+ }
public interface ITestLocation
{
string TestFilePath { get; }
diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
index 319cb11fed..6d36c41ec7 100644
--- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
+++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
@@ -1281,7 +1281,7 @@ namespace
public TestConstructorAttribute() { }
}
[.DebuggerDisplay("{.Name}.{GetDisplayName(),nq}")]
- public class TestContext : .Context, ., ., ., ., ., ., .
+ public class TestContext : .Context, ., ., ., ., ., ., ., .
{
public TestContext(string testName, serviceProvider, .ClassHookContext classContext, .TestBuilderContext testBuilderContext, .CancellationToken cancellationToken) { }
public .ClassHookContext ClassContext { get; }
@@ -1289,6 +1289,7 @@ namespace
public . Events { get; }
public . Execution { get; }
public string Id { get; }
+ public . Isolation { get; }
public object Lock { get; }
public . Metadata { get; }
public . Output { get; }
@@ -2427,6 +2428,12 @@ namespace .Interfaces
string TestId { get; }
string TestName { get; }
}
+ public interface ITestIsolation
+ {
+ int UniqueId { get; }
+ string GetIsolatedName(string baseName);
+ string GetIsolatedPrefix(string separator = "_");
+ }
public interface ITestLocation
{
string TestFilePath { get; }
diff --git a/TUnit.UnitTests/TestIsolationTests.cs b/TUnit.UnitTests/TestIsolationTests.cs
new file mode 100644
index 0000000000..2f979ea3ac
--- /dev/null
+++ b/TUnit.UnitTests/TestIsolationTests.cs
@@ -0,0 +1,114 @@
+using TUnit.Assertions.Extensions;
+using TUnit.Core.Interfaces;
+
+namespace TUnit.UnitTests;
+
+public class TestIsolationTests
+{
+ [Test]
+ public async Task UniqueId_IsPositive()
+ {
+ var isolation = TestContext.Current!.Isolation;
+
+ await Assert.That(isolation.UniqueId).IsGreaterThan(0);
+ }
+
+ [Test]
+ public async Task UniqueId_IsDifferentAcrossTests_1()
+ {
+ // Store uniqueId in state bag so a parallel test can verify it's different
+ var isolation = TestContext.Current!.Isolation;
+ TestContext.Current.StateBag["isolation_test_id_1"] = isolation.UniqueId;
+
+ await Assert.That(isolation.UniqueId).IsGreaterThan(0);
+ }
+
+ [Test]
+ public async Task UniqueId_IsDifferentAcrossTests_2()
+ {
+ var isolation = TestContext.Current!.Isolation;
+ TestContext.Current.StateBag["isolation_test_id_2"] = isolation.UniqueId;
+
+ await Assert.That(isolation.UniqueId).IsGreaterThan(0);
+ }
+
+ [Test]
+ public async Task GetIsolatedName_ReturnsExpectedFormat()
+ {
+ var isolation = TestContext.Current!.Isolation;
+ var id = isolation.UniqueId;
+
+ var name = isolation.GetIsolatedName("foo");
+
+ await Assert.That(name).IsEqualTo($"Test_{id}_foo");
+ }
+
+ [Test]
+ public async Task GetIsolatedName_WithDifferentBaseNames_ReturnsDifferentNames()
+ {
+ var isolation = TestContext.Current!.Isolation;
+
+ var name1 = isolation.GetIsolatedName("todos");
+ var name2 = isolation.GetIsolatedName("orders");
+
+ await Assert.That(name1).IsNotEqualTo(name2);
+ await Assert.That(name1).Contains("todos");
+ await Assert.That(name2).Contains("orders");
+ }
+
+ [Test]
+ public async Task GetIsolatedPrefix_WithDefaultSeparator_ReturnsExpectedFormat()
+ {
+ var isolation = TestContext.Current!.Isolation;
+ var id = isolation.UniqueId;
+
+ var prefix = isolation.GetIsolatedPrefix();
+
+ await Assert.That(prefix).IsEqualTo($"test_{id}_");
+ }
+
+ [Test]
+ public async Task GetIsolatedPrefix_WithDotSeparator_ReturnsExpectedFormat()
+ {
+ var isolation = TestContext.Current!.Isolation;
+ var id = isolation.UniqueId;
+
+ var prefix = isolation.GetIsolatedPrefix(".");
+
+ await Assert.That(prefix).IsEqualTo($"test.{id}.");
+ }
+
+ [Test]
+ public async Task GetIsolatedPrefix_WithCustomSeparator_ReturnsExpectedFormat()
+ {
+ var isolation = TestContext.Current!.Isolation;
+ var id = isolation.UniqueId;
+
+ var prefix = isolation.GetIsolatedPrefix("-");
+
+ await Assert.That(prefix).IsEqualTo($"test-{id}-");
+ }
+
+ [Test]
+ public async Task Isolation_IsAccessibleViaInterface()
+ {
+ ITestIsolation isolation = TestContext.Current!.Isolation;
+
+ await Assert.That(isolation).IsNotNull();
+ await Assert.That(isolation.UniqueId).IsGreaterThan(0);
+ }
+
+ [Test]
+ public async Task Isolation_SameContextReturnsSameValues()
+ {
+ var context = TestContext.Current!;
+
+ var id1 = context.Isolation.UniqueId;
+ var id2 = context.Isolation.UniqueId;
+ var name1 = context.Isolation.GetIsolatedName("test");
+ var name2 = context.Isolation.GetIsolatedName("test");
+
+ await Assert.That(id1).IsEqualTo(id2);
+ await Assert.That(name1).IsEqualTo(name2);
+ }
+}
diff --git a/docs/docs/examples/aspnet.md b/docs/docs/examples/aspnet.md
index 96483e41b8..b06b42a6ce 100644
--- a/docs/docs/examples/aspnet.md
+++ b/docs/docs/examples/aspnet.md
@@ -241,6 +241,10 @@ protected override void ConfigureWebHostBuilder(IWebHostBuilder builder)
## Test Isolation Helpers
+:::tip Available on All Tests
+The isolation helpers (`UniqueId`, `GetIsolatedName`, `GetIsolatedPrefix`) are also available on `TestContext.Current!.Isolation` for any test — not just ASP.NET Core tests. Use `TestContext.Current!.Isolation.GetIsolatedName("resource")` when you don't inherit from `WebApplicationTest`. Both share the same counter, so IDs are unique across all test types.
+:::
+
### GetIsolatedName
Creates a unique name for resources like database tables:
diff --git a/docs/docs/test-lifecycle/test-context.md b/docs/docs/test-lifecycle/test-context.md
index d73b8b7681..825c0ef298 100644
--- a/docs/docs/test-lifecycle/test-context.md
+++ b/docs/docs/test-lifecycle/test-context.md
@@ -57,6 +57,29 @@ Artifacts are particularly useful for debugging test failures, especially in int
For complete information about working with test artifacts, including session-level artifacts, best practices, and common use cases, see the [Test Artifacts](./artifacts.md) guide.
+## Test Isolation
+
+The `TestContext` provides built-in helpers for creating isolated resource names, ensuring parallel tests don't interfere with each other. Access them via `TestContext.Current!.Isolation`:
+
+```csharp
+// Get a unique ID for this test instance
+var id = TestContext.Current!.Isolation.UniqueId; // e.g. 42
+
+// Create isolated resource names
+var tableName = TestContext.Current!.Isolation.GetIsolatedName("todos"); // "Test_42_todos"
+var topicName = TestContext.Current!.Isolation.GetIsolatedName("orders"); // "Test_42_orders"
+
+// Create isolated key prefixes
+var prefix = TestContext.Current!.Isolation.GetIsolatedPrefix(); // "test_42_"
+var dotPrefix = TestContext.Current!.Isolation.GetIsolatedPrefix("."); // "test.42."
+```
+
+These are useful for any test that needs unique resource names — database tables, message queue topics, cache keys, blob storage paths, etc. — without requiring a specific base class.
+
+:::tip ASP.NET Core Tests
+If you're using `TUnit.AspNetCore`, the `WebApplicationTest` base class provides the same helpers as `protected` methods (`GetIsolatedName`, `GetIsolatedPrefix`). Both share the same underlying counter, so IDs are unique across all test types.
+:::
+
## Dependency Injection
**Note**: `TestContext` does NOT provide direct access to dependency injection services. The internal service provider in `TestContext` is exclusively for TUnit framework services and is not meant for user-provided dependencies.