diff --git a/Directory.Packages.props b/Directory.Packages.props
index a0edaab80d..a775d41b51 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -66,6 +66,7 @@
+
diff --git a/TUnit.Example.Asp.Net.TestProject/EfCore/EfCoreTodoApiTests.cs b/TUnit.Example.Asp.Net.TestProject/EfCore/EfCoreTodoApiTests.cs
new file mode 100644
index 0000000000..3af357d9c8
--- /dev/null
+++ b/TUnit.Example.Asp.Net.TestProject/EfCore/EfCoreTodoApiTests.cs
@@ -0,0 +1,113 @@
+using System.Net;
+using System.Net.Http.Json;
+using Microsoft.EntityFrameworkCore;
+using TUnit.Example.Asp.Net.Models;
+
+namespace TUnit.Example.Asp.Net.TestProject.EfCore;
+
+///
+/// Integration tests for the EF Core Todo API demonstrating per-test schema isolation.
+/// Each test gets its own schema within the shared PostgreSQL container.
+/// Compare with which uses raw SQL with per-test table names.
+///
+public class EfCoreTodoApiTests : EfCoreTodoTestBase
+{
+ [Test]
+ public async Task GetTodos_WhenEmpty_ReturnsEmptyList()
+ {
+ var client = Factory.CreateClient();
+
+ var response = await client.GetAsync("/ef/todos");
+
+ await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
+ var todos = await response.Content.ReadFromJsonAsync>();
+ await Assert.That(todos).IsNotNull();
+ await Assert.That(todos!.Count).IsEqualTo(0);
+ }
+
+ [Test]
+ public async Task CreateTodo_ReturnsCreatedTodo()
+ {
+ var client = Factory.CreateClient();
+
+ var response = await client.PostAsJsonAsync("/ef/todos", new { Title = "EF Core Todo" });
+
+ await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Created);
+ var todo = await response.Content.ReadFromJsonAsync();
+ await Assert.That(todo).IsNotNull();
+ await Assert.That(todo!.Title).IsEqualTo("EF Core Todo");
+ await Assert.That(todo.IsComplete).IsFalse();
+ }
+
+ [Test]
+ public async Task CreateTodo_CanBeVerifiedViaDbContext()
+ {
+ var client = Factory.CreateClient();
+
+ await client.PostAsJsonAsync("/ef/todos", new { Title = "Verify via EF" });
+
+ // Verify directly via EF Core DbContext
+ await using var scope = CreateDbScope(out var dbContext);
+ var todo = await dbContext.Todos.SingleAsync();
+ await Assert.That(todo.Title).IsEqualTo("Verify via EF");
+ }
+
+ [Test]
+ public async Task GetTodos_ReturnsSeededData()
+ {
+ // Seed via EF Core directly
+ await SeedTodosAsync("Seeded Item 1", "Seeded Item 2");
+
+ // Verify via API
+ var client = Factory.CreateClient();
+ var todos = await client.GetFromJsonAsync>("/ef/todos");
+ await Assert.That(todos!.Count).IsEqualTo(2);
+ }
+
+ [Test]
+ public async Task UpdateTodo_ChangesValues()
+ {
+ var client = Factory.CreateClient();
+
+ // Create
+ var createResponse = await client.PostAsJsonAsync("/ef/todos", new { Title = "Update Me" });
+ var created = await createResponse.Content.ReadFromJsonAsync();
+
+ // Update
+ var updateResponse = await client.PutAsJsonAsync(
+ $"/ef/todos/{created!.Id}",
+ new { Title = "Updated", IsComplete = true });
+
+ await Assert.That(updateResponse.StatusCode).IsEqualTo(HttpStatusCode.OK);
+ var updated = await updateResponse.Content.ReadFromJsonAsync();
+ await Assert.That(updated!.Title).IsEqualTo("Updated");
+ await Assert.That(updated.IsComplete).IsTrue();
+ }
+
+ [Test]
+ public async Task DeleteTodo_RemovesTodo()
+ {
+ var client = Factory.CreateClient();
+
+ // Create
+ var createResponse = await client.PostAsJsonAsync("/ef/todos", new { Title = "Delete Me" });
+ var created = await createResponse.Content.ReadFromJsonAsync();
+
+ // Delete
+ var deleteResponse = await client.DeleteAsync($"/ef/todos/{created!.Id}");
+ await Assert.That(deleteResponse.StatusCode).IsEqualTo(HttpStatusCode.NoContent);
+
+ // Verify gone
+ var getResponse = await client.GetAsync($"/ef/todos/{created.Id}");
+ await Assert.That(getResponse.StatusCode).IsEqualTo(HttpStatusCode.NotFound);
+ }
+
+ [Test, Repeat(3)]
+ public async Task ParallelTests_AreIsolated()
+ {
+ // Each repeat gets its own schema - no data leaks between parallel tests
+ await using var scope = CreateDbScope(out var dbContext);
+ var count = await dbContext.Todos.CountAsync();
+ await Assert.That(count).IsEqualTo(0);
+ }
+}
diff --git a/TUnit.Example.Asp.Net.TestProject/EfCore/EfCoreTodoTestBase.cs b/TUnit.Example.Asp.Net.TestProject/EfCore/EfCoreTodoTestBase.cs
new file mode 100644
index 0000000000..1fa1402df4
--- /dev/null
+++ b/TUnit.Example.Asp.Net.TestProject/EfCore/EfCoreTodoTestBase.cs
@@ -0,0 +1,93 @@
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Npgsql;
+using TUnit.AspNetCore;
+using TUnit.Example.Asp.Net.EfCore;
+using TUnit.Example.Asp.Net.Models;
+
+namespace TUnit.Example.Asp.Net.TestProject.EfCore;
+
+///
+/// Base class for EF Core integration tests with per-test schema isolation.
+/// Each test gets a unique PostgreSQL schema, with tables created via
+/// EF Core's EnsureCreatedAsync.
+///
+[SuppressMessage("Usage", "TUnit0043:Property must use `required` keyword")]
+public abstract class EfCoreTodoTestBase : WebApplicationTest
+{
+ [ClassDataSource(Shared = SharedType.PerTestSession)]
+ public InMemoryPostgreSqlDatabase PostgreSql { get; init; } = null!;
+
+ protected string SchemaName { get; private set; } = null!;
+
+ protected override async Task SetupAsync()
+ {
+ SchemaName = GetIsolatedName("schema");
+
+ // Create the schema via raw SQL
+ await using var connection = new NpgsqlConnection(PostgreSql.Container.GetConnectionString());
+ await connection.OpenAsync();
+ await using var cmd = connection.CreateCommand();
+ cmd.CommandText = $"CREATE SCHEMA IF NOT EXISTS \"{SchemaName}\"";
+ await cmd.ExecuteNonQueryAsync();
+
+ // Use EF Core to create tables in the new schema
+ var options = new DbContextOptionsBuilder()
+ .UseNpgsql(PostgreSql.Container.GetConnectionString())
+ .ReplaceService()
+ .Options;
+
+ await using var dbContext = new TodoDbContext(options) { SchemaName = SchemaName };
+ await dbContext.Database.EnsureCreatedAsync();
+ }
+
+ protected override void ConfigureTestConfiguration(IConfigurationBuilder config)
+ {
+ config.AddInMemoryCollection(new Dictionary
+ {
+ { "Database:Schema", SchemaName }
+ });
+ }
+
+ [After(HookType.Test)]
+ public async Task CleanupSchema()
+ {
+ if (string.IsNullOrEmpty(SchemaName))
+ {
+ return;
+ }
+
+ await using var connection = new NpgsqlConnection(PostgreSql.Container.GetConnectionString());
+ await connection.OpenAsync();
+ await using var cmd = connection.CreateCommand();
+ cmd.CommandText = $"DROP SCHEMA IF EXISTS \"{SchemaName}\" CASCADE";
+ await cmd.ExecuteNonQueryAsync();
+ }
+
+ ///
+ /// Creates a scoped TodoDbContext for direct database access in tests.
+ /// The context reads Database:Schema from the per-test configuration automatically.
+ ///
+ protected AsyncServiceScope CreateDbScope(out TodoDbContext dbContext)
+ {
+ var scope = Factory.Services.CreateAsyncScope();
+ dbContext = scope.ServiceProvider.GetRequiredService();
+ return scope;
+ }
+
+ ///
+ /// Seeds the database with todos directly via EF Core (bypassing the API).
+ ///
+ protected async Task SeedTodosAsync(params string[] titles)
+ {
+ await using var scope = CreateDbScope(out var dbContext);
+ foreach (var title in titles)
+ {
+ dbContext.Todos.Add(new Todo { Title = title });
+ }
+ await dbContext.SaveChangesAsync();
+ }
+}
diff --git a/TUnit.Example.Asp.Net.TestProject/EfCore/EfCoreWebApplicationFactory.cs b/TUnit.Example.Asp.Net.TestProject/EfCore/EfCoreWebApplicationFactory.cs
new file mode 100644
index 0000000000..ec1cd7851c
--- /dev/null
+++ b/TUnit.Example.Asp.Net.TestProject/EfCore/EfCoreWebApplicationFactory.cs
@@ -0,0 +1,45 @@
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using TUnit.AspNetCore;
+using TUnit.Example.Asp.Net.EfCore;
+
+namespace TUnit.Example.Asp.Net.TestProject.EfCore;
+
+///
+/// WebApplicationFactory configured with EF Core services.
+/// Reuses the shared PostgreSQL container and adds DbContext registration.
+///
+[SuppressMessage("Usage", "TUnit0043:Property must use `required` keyword")]
+public class EfCoreWebApplicationFactory : TestWebApplicationFactory
+{
+ [ClassDataSource(Shared = SharedType.PerTestSession)]
+ public InMemoryPostgreSqlDatabase PostgreSql { get; init; } = null!;
+
+ protected override void ConfigureWebHost(IWebHostBuilder builder)
+ {
+ builder.ConfigureServices(services =>
+ {
+ // Remove the app's default DbContext registration (which has an empty connection string)
+ services.RemoveAll>();
+
+ // Re-register with the container's real connection string
+ services.AddDbContext(options =>
+ options.UseNpgsql(PostgreSql.Container.GetConnectionString())
+ .ReplaceService());
+ });
+ }
+
+ protected override void ConfigureStartupConfiguration(IConfigurationBuilder configurationBuilder)
+ {
+ configurationBuilder.AddInMemoryCollection(new Dictionary
+ {
+ { "SomeKey", "SomeValue" },
+ { "Database:ConnectionString", PostgreSql.Container.GetConnectionString() }
+ });
+ }
+}
diff --git a/TUnit.Example.Asp.Net.TestProject/TUnit.Example.Asp.Net.TestProject.csproj b/TUnit.Example.Asp.Net.TestProject/TUnit.Example.Asp.Net.TestProject.csproj
index 5d5406cef4..b01619b3de 100644
--- a/TUnit.Example.Asp.Net.TestProject/TUnit.Example.Asp.Net.TestProject.csproj
+++ b/TUnit.Example.Asp.Net.TestProject/TUnit.Example.Asp.Net.TestProject.csproj
@@ -18,6 +18,7 @@
+
diff --git a/TUnit.Example.Asp.Net/EfCore/SchemaModelCacheKeyFactory.cs b/TUnit.Example.Asp.Net/EfCore/SchemaModelCacheKeyFactory.cs
new file mode 100644
index 0000000000..b0ffc38da8
--- /dev/null
+++ b/TUnit.Example.Asp.Net/EfCore/SchemaModelCacheKeyFactory.cs
@@ -0,0 +1,19 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+
+namespace TUnit.Example.Asp.Net.EfCore;
+
+///
+/// Custom model cache key factory that includes the schema name in the cache key.
+/// This ensures EF Core creates a separate model for each schema, enabling
+/// per-test schema isolation without model conflicts.
+///
+public class SchemaModelCacheKeyFactory : IModelCacheKeyFactory
+{
+ public object Create(DbContext context, bool designTime)
+ {
+ return context is TodoDbContext todoContext
+ ? (context.GetType(), todoContext.SchemaName, designTime)
+ : (object)(context.GetType(), designTime);
+ }
+}
diff --git a/TUnit.Example.Asp.Net/EfCore/TodoDbContext.cs b/TUnit.Example.Asp.Net/EfCore/TodoDbContext.cs
new file mode 100644
index 0000000000..45cc9d240b
--- /dev/null
+++ b/TUnit.Example.Asp.Net/EfCore/TodoDbContext.cs
@@ -0,0 +1,32 @@
+using Microsoft.EntityFrameworkCore;
+using TUnit.Example.Asp.Net.Models;
+
+namespace TUnit.Example.Asp.Net.EfCore;
+
+public class TodoDbContext : DbContext
+{
+ public string SchemaName { get; set; }
+
+ public DbSet Todos => Set();
+
+ public TodoDbContext(DbContextOptions options, IConfiguration? config = null)
+ : base(options)
+ {
+ SchemaName = config?["Database:Schema"] ?? "public";
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.HasDefaultSchema(SchemaName);
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(t => t.Id);
+ entity.Property(t => t.Id).HasColumnName("id");
+ entity.Property(t => t.Title).IsRequired().HasMaxLength(200).HasColumnName("title");
+ entity.Property(t => t.IsComplete).HasDefaultValue(false).HasColumnName("is_complete");
+ entity.Property(t => t.CreatedAt).HasDefaultValueSql("NOW()").HasColumnName("created_at");
+ entity.ToTable("todos");
+ });
+ }
+}
diff --git a/TUnit.Example.Asp.Net/Program.cs b/TUnit.Example.Asp.Net/Program.cs
index 1ba58798ea..65f9317ca1 100644
--- a/TUnit.Example.Asp.Net/Program.cs
+++ b/TUnit.Example.Asp.Net/Program.cs
@@ -1,4 +1,7 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
using TUnit.Example.Asp.Net.Configuration;
+using TUnit.Example.Asp.Net.EfCore;
using TUnit.Example.Asp.Net.Models;
using TUnit.Example.Asp.Net.Repositories;
using TUnit.Example.Asp.Net.Services;
@@ -17,6 +20,12 @@
builder.Services.AddScoped();
builder.Services.AddScoped();
+// EF Core DbContext - reads connection string and schema from configuration.
+// The DbContext constructor reads Database:Schema from IConfiguration directly.
+builder.Services.AddDbContext(options =>
+ options.UseNpgsql(builder.Configuration["Database:ConnectionString"] ?? "")
+ .ReplaceService());
+
var app = builder.Build();
var logger = app.Services.GetRequiredService().CreateLogger("Endpoints");
@@ -34,7 +43,7 @@
return "Hello, World!";
});
-// Todo CRUD endpoints
+// Todo CRUD endpoints (raw SQL)
app.MapGet("/todos", async (ITodoRepository repo) =>
await repo.GetAllAsync());
@@ -76,6 +85,52 @@ await cache.DeleteAsync(key)
? Results.NoContent()
: Results.NotFound());
+// EF Core Todo endpoints (Code First approach alongside raw SQL above)
+// TodoDbContext reads Database:Schema from IConfiguration in its constructor,
+// so no manual schema wiring is needed in each endpoint.
+app.MapGet("/ef/todos", async (TodoDbContext db) =>
+ await db.Todos.OrderByDescending(t => t.CreatedAt).ToListAsync());
+
+app.MapGet("/ef/todos/{id:int}", async (int id, TodoDbContext db) =>
+ await db.Todos.FindAsync(id) is { } todo
+ ? Results.Ok(todo)
+ : Results.NotFound());
+
+app.MapPost("/ef/todos", async (CreateTodoRequest request, TodoDbContext db) =>
+{
+ var todo = new Todo { Title = request.Title };
+ db.Todos.Add(todo);
+ await db.SaveChangesAsync();
+ return Results.Created($"/ef/todos/{todo.Id}", todo);
+});
+
+app.MapPut("/ef/todos/{id:int}", async (int id, UpdateTodoRequest request, TodoDbContext db) =>
+{
+ var todo = await db.Todos.FindAsync(id);
+ if (todo is null)
+ {
+ return Results.NotFound();
+ }
+
+ todo.Title = request.Title;
+ todo.IsComplete = request.IsComplete;
+ await db.SaveChangesAsync();
+ return Results.Ok(todo);
+});
+
+app.MapDelete("/ef/todos/{id:int}", async (int id, TodoDbContext db) =>
+{
+ var todo = await db.Todos.FindAsync(id);
+ if (todo is null)
+ {
+ return Results.NotFound();
+ }
+
+ db.Todos.Remove(todo);
+ await db.SaveChangesAsync();
+ return Results.NoContent();
+});
+
app.Run();
public partial class Program;
diff --git a/TUnit.Example.Asp.Net/TUnit.Example.Asp.Net.csproj b/TUnit.Example.Asp.Net/TUnit.Example.Asp.Net.csproj
index adf9850290..6894170add 100644
--- a/TUnit.Example.Asp.Net/TUnit.Example.Asp.Net.csproj
+++ b/TUnit.Example.Asp.Net/TUnit.Example.Asp.Net.csproj
@@ -11,6 +11,7 @@
+
diff --git a/docs/docs/examples/aspnet.md b/docs/docs/examples/aspnet.md
index 8045904eb2..bbe642641a 100644
--- a/docs/docs/examples/aspnet.md
+++ b/docs/docs/examples/aspnet.md
@@ -333,6 +333,106 @@ public abstract class TodoTestBase : TestsBase
}
```
+### Per-Test Schema Isolation with EF Core
+
+For EF Core Code First applications, use per-test PostgreSQL schemas instead of per-test table names. This works with EF Core's model conventions and provides complete isolation:
+
+```csharp
+// 1. DbContext with dynamic schema support
+public class TodoDbContext : DbContext
+{
+ public string SchemaName { get; set; }
+ public DbSet Todos => Set();
+
+ // IConfiguration is optional: resolved via DI in the app, absent when
+ // constructing standalone (e.g. in SetupAsync for EnsureCreatedAsync).
+ public TodoDbContext(DbContextOptions options, IConfiguration? config = null)
+ : base(options)
+ {
+ SchemaName = config?["Database:Schema"] ?? "public";
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.HasDefaultSchema(SchemaName);
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(t => t.Id);
+ entity.Property(t => t.Title).IsRequired().HasMaxLength(200);
+ });
+ }
+}
+
+// 2. Model cache key factory (required for multiple schemas)
+public class SchemaModelCacheKeyFactory : IModelCacheKeyFactory
+{
+ public object Create(DbContext context, bool designTime)
+ {
+ return context is TodoDbContext todoContext
+ ? (context.GetType(), todoContext.SchemaName, designTime)
+ : (object)(context.GetType(), designTime);
+ }
+}
+
+// 3. Test base class with schema-per-test isolation
+public abstract class EfCoreTodoTestBase : WebApplicationTest
+{
+ [ClassDataSource(Shared = SharedType.PerTestSession)]
+ public InMemoryDatabase Database { get; init; } = null!;
+
+ protected string SchemaName { get; private set; } = null!;
+
+ protected override async Task SetupAsync()
+ {
+ SchemaName = GetIsolatedName("schema");
+
+ // Create schema via raw SQL
+ await using var connection = new NpgsqlConnection(
+ Database.Container.GetConnectionString());
+ await connection.OpenAsync();
+ await using var cmd = connection.CreateCommand();
+ cmd.CommandText = $"CREATE SCHEMA IF NOT EXISTS \"{SchemaName}\"";
+ await cmd.ExecuteNonQueryAsync();
+
+ // Create tables via EF Core
+ var options = new DbContextOptionsBuilder()
+ .UseNpgsql(Database.Container.GetConnectionString())
+ .ReplaceService()
+ .Options;
+
+ await using var dbContext = new TodoDbContext(options) { SchemaName = SchemaName };
+ await dbContext.Database.EnsureCreatedAsync();
+ }
+
+ protected override void ConfigureTestConfiguration(IConfigurationBuilder config)
+ {
+ config.AddInMemoryCollection(new Dictionary
+ {
+ { "Database:Schema", SchemaName }
+ });
+ }
+
+ [After(HookType.Test)]
+ public async Task CleanupSchema()
+ {
+ await using var connection = new NpgsqlConnection(
+ Database.Container.GetConnectionString());
+ await connection.OpenAsync();
+ await using var cmd = connection.CreateCommand();
+ cmd.CommandText = $"DROP SCHEMA IF EXISTS \"{SchemaName}\" CASCADE";
+ await cmd.ExecuteNonQueryAsync();
+ }
+}
+```
+
+**Key differences from raw SQL approach:**
+- Uses `EnsureCreatedAsync()` instead of manual `CREATE TABLE` statements
+- Isolation is at the **schema level** rather than the table name level
+- `IModelCacheKeyFactory` ensures EF Core caches a separate model per schema
+- Cleanup uses `DROP SCHEMA ... CASCADE` to remove all tables at once
+
+See the full working example in `TUnit.Example.Asp.Net.TestProject/EfCore/`.
+
## HTTP Exchange Capture
Capture and inspect HTTP requests/responses for assertions:
diff --git a/docs/docs/examples/complex-test-infrastructure.md b/docs/docs/examples/complex-test-infrastructure.md
index 75af949e7e..151b5a7a35 100644
--- a/docs/docs/examples/complex-test-infrastructure.md
+++ b/docs/docs/examples/complex-test-infrastructure.md
@@ -200,6 +200,91 @@ public class InMemoryPostgreSqlDatabase : IAsyncInitializer, IAsyncDisposable
}
```
+### EF Core Code First with Per-Test Schema Isolation
+
+For EF Core Code First applications, use per-test PostgreSQL schemas instead of per-test table names. This avoids fighting EF Core's table naming conventions:
+
+```csharp
+// DbContext with dynamic schema support — reads schema from IConfiguration when
+// resolved via DI, falls back to "public" when constructed standalone.
+public class TodoDbContext : DbContext
+{
+ public string SchemaName { get; set; }
+ public DbSet Todos => Set();
+
+ public TodoDbContext(DbContextOptions options, IConfiguration? config = null)
+ : base(options)
+ {
+ SchemaName = config?["Database:Schema"] ?? "public";
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.HasDefaultSchema(SchemaName);
+ // ... entity configuration
+ }
+}
+
+// IModelCacheKeyFactory ensures different schemas get different model caches
+public class SchemaModelCacheKeyFactory : IModelCacheKeyFactory
+{
+ public object Create(DbContext context, bool designTime)
+ => context is TodoDbContext tc
+ ? (context.GetType(), tc.SchemaName, designTime)
+ : (object)(context.GetType(), designTime);
+}
+
+// Test base: creates schema + tables in SetupAsync, drops in cleanup
+public abstract class EfCoreTodoTestBase
+ : WebApplicationTest
+{
+ [ClassDataSource(Shared = SharedType.PerTestSession)]
+ public InMemoryPostgreSqlDatabase PostgreSql { get; init; } = null!;
+
+ protected string SchemaName { get; private set; } = null!;
+
+ protected override async Task SetupAsync()
+ {
+ SchemaName = GetIsolatedName("schema"); // e.g. "Test_42_schema"
+
+ // Create schema, then let EF Core create tables
+ await using var conn = new NpgsqlConnection(
+ PostgreSql.Container.GetConnectionString());
+ await conn.OpenAsync();
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = $"CREATE SCHEMA IF NOT EXISTS \"{SchemaName}\"";
+ await cmd.ExecuteNonQueryAsync();
+
+ var options = new DbContextOptionsBuilder()
+ .UseNpgsql(PostgreSql.Container.GetConnectionString())
+ .ReplaceService()
+ .Options;
+ await using var db = new TodoDbContext(options) { SchemaName = SchemaName };
+ await db.Database.EnsureCreatedAsync();
+ }
+
+ protected override void ConfigureTestConfiguration(IConfigurationBuilder config)
+ {
+ config.AddInMemoryCollection(new Dictionary
+ {
+ { "Database:Schema", SchemaName }
+ });
+ }
+
+ [After(HookType.Test)]
+ public async Task CleanupSchema()
+ {
+ await using var conn = new NpgsqlConnection(
+ PostgreSql.Container.GetConnectionString());
+ await conn.OpenAsync();
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = $"DROP SCHEMA IF EXISTS \"{SchemaName}\" CASCADE";
+ await cmd.ExecuteNonQueryAsync();
+ }
+}
+```
+
+See the full working example in `TUnit.Example.Asp.Net.TestProject/EfCore/`.
## Comparison with Other Frameworks
diff --git a/docs/docs/guides/cookbook.md b/docs/docs/guides/cookbook.md
index 6721c8df26..6310fc0e93 100644
--- a/docs/docs/guides/cookbook.md
+++ b/docs/docs/guides/cookbook.md
@@ -621,6 +621,17 @@ public class OrderRepositoryIntegrationTests
}
```
+### Testing with EF Core + TestContainers + WebApplicationFactory
+
+For a complete integration testing setup with EF Core Code First, TestContainers, and per-test schema isolation, see the full working example in `TUnit.Example.Asp.Net.TestProject/EfCore/`. This demonstrates:
+
+- **Per-test schema isolation**: Each test gets its own PostgreSQL schema via `GetIsolatedName("schema")`
+- **EF Core table creation**: Uses `EnsureCreatedAsync()` to create tables from the model
+- **Dynamic schema support**: Custom `IModelCacheKeyFactory` ensures correct model caching per schema
+- **Clean teardown**: `DROP SCHEMA ... CASCADE` removes all tables after each test
+
+See the [ASP.NET Core Integration Testing](/docs/examples/aspnet#per-test-schema-isolation-with-ef-core) docs for the full pattern.
+
### Testing with Test Containers (Docker)
```csharp