Skip to content

Add EF Core Code First sample with per-test schema isolation#4840

Merged
thomhurst merged 4 commits intomainfrom
feature/ef-core-sample
Feb 17, 2026
Merged

Add EF Core Code First sample with per-test schema isolation#4840
thomhurst merged 4 commits intomainfrom
feature/ef-core-sample

Conversation

@thomhurst
Copy link
Owner

Summary

  • Adds an official EF Core Code First example to TUnit.Example.Asp.Net.TestProject showing how to integrate EF Core with WebApplicationFactory, TestContainers PostgreSQL, and per-test schema isolation
  • Each test gets a unique PostgreSQL schema via GetIsolatedName("schema"), with tables created by EnsureCreatedAsync() and cleaned up via DROP SCHEMA CASCADE
  • Includes custom IModelCacheKeyFactory to handle EF Core model caching across multiple schemas
  • Updates documentation in aspnet.md, complex-test-infrastructure.md, and cookbook.md

Closes #4835

New files

File Purpose
TUnit.Example.Asp.Net/EfCore/TodoDbContext.cs DbContext with dynamic HasDefaultSchema()
TUnit.Example.Asp.Net/EfCore/SchemaModelCacheKeyFactory.cs Per-schema model cache keys
TUnit.Example.Asp.Net/EfCore/EfCoreDatabaseOptions.cs Schema config options
TUnit.Example.Asp.Net.TestProject/EfCore/EfCoreWebApplicationFactory.cs Factory wiring EF Core to shared PostgreSQL container
TUnit.Example.Asp.Net.TestProject/EfCore/EfCoreTodoTestBase.cs Base class with schema-per-test isolation
TUnit.Example.Asp.Net.TestProject/EfCore/EfCoreTodoApiTests.cs 7 sample tests (CRUD, DbContext verification, parallel isolation)

Test plan

  • Verify both projects build successfully (dotnet build)
  • Run the EF Core tests with Docker available: dotnet run --project TUnit.Example.Asp.Net.TestProject -- --treenode-filter "/*/*/EfCoreTodoApiTests/*"
  • Verify parallel isolation test (ParallelTests_AreIsolated with [Repeat(3)]) passes without data leaks
  • Review documentation renders correctly in docs site

🤖 Generated with Claude Code

@claude
Copy link
Contributor

claude bot commented Feb 17, 2026

Code Review

Resource Leak Issue

File: TUnit.Example.Asp.Net.TestProject/EfCore/EfCoreTodoTestBase.cs (lines 95-101)

Issue: The CreateDbContext() method creates an IServiceScope but never disposes it, causing a resource leak.

protected TodoDbContext CreateDbContext()
{
    var scope = Factory.Services.CreateScope();  // ⚠️ scope is never disposed
    var dbContext = scope.ServiceProvider.GetRequiredService<TodoDbContext>();
    dbContext.SchemaName = SchemaName;
    return dbContext;  // only DbContext returned; scope leaks
}

Problem: Callers use await using var dbContext = CreateDbContext(); which disposes the DbContext, but disposing a DbContext does not dispose the scope that created it. The .NET DI model works in one direction: disposing a scope disposes its services, but disposing a service does NOT dispose the scope.

Impact:

  • Each call leaks an IServiceScope and its associated scoped services
  • Called in 3 places: CreateTodo_CanBeVerifiedViaDbContext, ParallelTests_AreIsolated, and SeedTodosAsync
  • Particularly problematic in sample code that users will copy into their own test suites

Suggested Fix: Return both the scope and context so callers can dispose both:

protected (TodoDbContext DbContext, IServiceScope Scope) CreateDbContext()
{
    var scope = Factory.Services.CreateScope();
    var dbContext = scope.ServiceProvider.GetRequiredService<TodoDbContext>();
    dbContext.SchemaName = SchemaName;
    return (dbContext, scope);
}

// Usage in tests:
await using var (dbContext, scope) = CreateDbContext();
// ... use dbContext ...
// Both will be disposed automatically

Alternatively, you could make the scope a field and dispose it in the cleanup method, though the tuple approach is cleaner for this pattern.

Why this matters: This is especially important in sample/example code because developers will replicate this pattern. A resource leak in production test suites could accumulate across thousands of tests.


Reference: Microsoft EF Core DbContext Lifetime Documentation


Otherwise, the PR looks great! The per-test schema isolation pattern is well-designed and the documentation is thorough. This is a valuable addition to the TUnit examples.

Adds an official example showing how to use EF Core Code First with
WebApplicationFactory, TestContainers PostgreSQL, and per-test schema
isolation. Each test gets a unique PostgreSQL schema via GetIsolatedName,
with tables created by EF Core's EnsureCreatedAsync and cleaned up via
DROP SCHEMA CASCADE.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move schema reading into DbContext constructor via IConfiguration,
eliminating EfCoreDatabaseOptions and per-endpoint schema assignment.
Fix IServiceScope leak in CreateDbContext by returning AsyncServiceScope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Without this parameter, DI-resolved DbContext always uses "public" schema,
making ConfigureTestConfiguration ineffective for schema isolation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Official sample of working with EF Code First DbContext

1 participant