Most EF Core tutorials show you one DbContext, one database, one set of migrations. That works fine for small projects. But the moment your application grows - a separate analytics database, a modular monolith with bounded contexts, a read replica for reporting - you need multiple DbContexts in the same application. And that’s where things get interesting.
The good news: EF Core handles multiple DbContexts natively. The not-so-good news: there are subtle pitfalls around dependency injection, migrations, transactions, and schema isolation that will waste your time if you don’t know about them upfront. I’ve hit most of them myself - from the cryptic “More than one DbContext was found” migration error to silently losing data because two contexts shared a connection without a shared transaction.
In this article, I’ll walk you through building a real ASP.NET Core Web API that uses two separate DbContexts - one for the Movie API (the domain database) and one for an Analytics service (a separate database). I’ll cover every scenario: same database with different schemas, different databases entirely, migrations management, cross-context transactions, and the read replica pattern. By the end, you’ll know exactly when to split your DbContext and how to do it without breaking things.
Let’s get into it.
When Should You Use Multiple DbContexts?
Multiple DbContext in EF Core (Entity Framework Core) means defining more than one class that inherits from DbContext, each responsible for a specific set of entities, a specific database, or a specific bounded context within your application. Instead of one monolithic context that maps every table in every database, you split your data access into focused, purpose-built contexts.
Here are the real-world scenarios where this makes sense:
| Scenario | What It Looks Like | Example |
|---|---|---|
| Multiple databases | Each context connects to a different database | Movie API + Analytics DB |
| Bounded contexts (DDD) | Same database, different schemas, different entity sets | Catalog schema + Ordering schema |
| Modular monolith | Each module owns its own context and schema | Users module + Billing module |
| Read replica | Dedicated read-only context pointing to a replica | Reporting queries on a read replica |
| Multi-tenant | Each tenant gets its own context/database | SaaS platform with per-tenant isolation |
| Mixed providers | One context for PostgreSQL, another for Redis/Cosmos | Relational + document store |
When NOT to Split
Don’t create multiple DbContexts just because your application has many entities. A single DbContext with 50 entities is perfectly fine - EF Core handles large models well. Split only when you have a genuine architectural reason: separate databases, module isolation, or bounded context boundaries.
Rule of thumb: If two entity groups will never need cross-entity joins and have different lifecycles (deployed separately, owned by different teams, or stored in different databases), they belong in separate DbContexts. If they frequently join and share transactions, keep them in one context.
Setting Up Multiple DbContexts - Step by Step
Let’s build this with a concrete example. Let’s extend the Movie API to include a separate Analytics database that tracks API usage events. The Movie database stores the domain data; the Analytics database stores event logs.
Step 1: Define the Entities
Our Movie entities already exist from previous lessons. Let’s add the Analytics entity:
namespace MultiDbDemo.Api.Entities;
public class Movie{ public Guid Id { get; set; } public string Title { get; set; } = string.Empty; public string Genre { get; set; } = string.Empty; public decimal Rating { get; set; } public int ReleaseYear { get; set; }}namespace MultiDbDemo.Api.Entities;
public class ApiEvent{ public Guid Id { get; set; } public string Endpoint { get; set; } = string.Empty; public string Method { get; set; } = string.Empty; public int StatusCode { get; set; } public long DurationMs { get; set; } public DateTime OccurredAt { get; set; } = DateTime.UtcNow;}Step 2: Create Separate DbContext Classes
Each context manages its own set of entities and its own database connection. The critical detail: use DbContextOptions<TContext> - not plain DbContextOptions - in the constructor. This is how EF Core’s dependency injection resolves the correct options for each context when multiple contexts are registered.
using Microsoft.EntityFrameworkCore;using MultiDbDemo.Api.Entities;
namespace MultiDbDemo.Api.Data;
public class MovieDbContext(DbContextOptions<MovieDbContext> options) : DbContext(options){ public DbSet<Movie> Movies => Set<Movie>();
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Movie>(entity => { entity.HasKey(m => m.Id); entity.Property(m => m.Title).HasMaxLength(300).IsRequired(); entity.Property(m => m.Genre).HasMaxLength(100).IsRequired(); entity.Property(m => m.Rating).HasColumnType("decimal(3,1)"); }); }}using Microsoft.EntityFrameworkCore;using MultiDbDemo.Api.Entities;
namespace MultiDbDemo.Api.Data;
public class AnalyticsDbContext(DbContextOptions<AnalyticsDbContext> options) : DbContext(options){ public DbSet<ApiEvent> ApiEvents => Set<ApiEvent>();
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<ApiEvent>(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.Endpoint).HasMaxLength(500).IsRequired(); entity.Property(e => e.Method).HasMaxLength(10).IsRequired(); }); }}Why
DbContextOptions<TContext>matters: When you register multiple DbContexts in DI, EF Core needs to know whichDbContextOptionsbelongs to which context. If you use the non-genericDbContextOptions, both contexts would receive the same options - which means the same connection string and configuration. The generic versionDbContextOptions<MovieDbContext>ensures each context gets its own options. This is documented by Microsoft as a requirement for multiple DbContext registration.
Step 3: Register Both Contexts in DI
Each context gets its own AddDbContext call with its own connection string:
using Microsoft.EntityFrameworkCore;using MultiDbDemo.Api.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
builder.Services.AddDbContext<MovieDbContext>(options => options.UseNpgsql(builder.Configuration.GetConnectionString("MovieDb")));
builder.Services.AddDbContext<AnalyticsDbContext>(options => options.UseNpgsql(builder.Configuration.GetConnectionString("AnalyticsDb")));
var app = builder.Build();And the connection strings in appsettings.json:
{ "ConnectionStrings": { "MovieDb": "Host=localhost;Port=5432;Database=movies;Username=postgres;Password=postgres", "AnalyticsDb": "Host=localhost;Port=5433;Database=analytics;Username=postgres;Password=postgres" }}Notice the different ports - we’re running two PostgreSQL instances in Docker. In production, these could be entirely separate database servers.
Step 4: Use Each Context in Endpoints
Each endpoint injects only the context it needs:
// Uses MovieDbContext - domain operationsapp.MapGet("/api/movies", async (MovieDbContext db, CancellationToken ct) =>{ var movies = await db.Movies.AsNoTracking().ToListAsync(ct); return Results.Ok(movies);});
// Uses AnalyticsDbContext - event loggingapp.MapGet("/api/analytics/events", async (AnalyticsDbContext analytics, CancellationToken ct) =>{ var events = await analytics.ApiEvents .AsNoTracking() .OrderByDescending(e => e.OccurredAt) .Take(100) .ToListAsync(ct); return Results.Ok(events);});
// Uses both contexts in one endpointapp.MapGet("/api/movies/{id:guid}", async (Guid id, MovieDbContext db, AnalyticsDbContext analytics, CancellationToken ct) =>{ var movie = await db.Movies.FindAsync([id], ct); if (movie is null) return Results.NotFound();
analytics.ApiEvents.Add(new ApiEvent { Id = Guid.CreateVersion7(), Endpoint = $"/api/movies/{id}", Method = "GET", StatusCode = 200, DurationMs = 0 }); await analytics.SaveChangesAsync(ct);
return Results.Ok(movie);});Get the point? Each context is independent - it has its own connection, its own change tracker, and its own SaveChanges() scope.
Managing Migrations with Multiple DbContexts
This is where most developers get stuck. When you have multiple DbContexts, the dotnet ef CLI doesn’t know which one to use. Run dotnet ef migrations add without specifying a context and you’ll get:
More than one DbContext was found. Specify which one to use.Use the '-Context' option for the .NET Core CLI or the '-DbContext' option for the Package Manager Console.Running Migration Commands
Always specify the --context flag and use separate output directories to keep migrations organized. This is covered in the EF Core migrations with multiple providers documentation:
dotnet ef migrations add InitialMovies --context MovieDbContext --output-dir Migrations/MovieDbdotnet ef migrations add InitialAnalytics --context AnalyticsDbContext --output-dir Migrations/AnalyticsDbTo apply migrations:
dotnet ef database update --context MovieDbContextdotnet ef database update --context AnalyticsDbContextMigration History Tables
By default, EF Core stores migration history in a table called __EFMigrationsHistory. When both contexts target the same database (e.g., different schemas), they’ll collide on this table. Fix it by configuring separate history tables:
builder.Services.AddDbContext<MovieDbContext>(options => options.UseNpgsql(connectionString, npgsql => npgsql.MigrationsHistoryTable("__MovieMigrations", "movies")));
builder.Services.AddDbContext<AnalyticsDbContext>(options => options.UseNpgsql(connectionString, npgsql => npgsql.MigrationsHistoryTable("__AnalyticsMigrations", "analytics")));Project Structure for Migrations
Keep your migration files organized:
MultiDbDemo.Api/├── Data/│ ├── MovieDbContext.cs│ └── AnalyticsDbContext.cs├── Migrations/│ ├── MovieDb/│ │ ├── 20260212_InitialMovies.cs│ │ └── MovieDbContextModelSnapshot.cs│ └── AnalyticsDb/│ ├── 20260212_InitialAnalytics.cs│ └── AnalyticsDbContextModelSnapshot.csCI/CD tip: In your deployment pipeline, run migrations for each context separately. If one fails, the other database remains untouched. This is one of the key benefits of separate contexts - independent deployability.
Schema Separation - Same Database, Different Schemas
Sometimes you don’t need separate databases - you just need logical separation within a single database. This is common in modular monolith architectures where each module owns a set of tables under its own schema.
public class CatalogDbContext(DbContextOptions<CatalogDbContext> options) : DbContext(options){ public DbSet<Movie> Movies => Set<Movie>();
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasDefaultSchema("catalog");
modelBuilder.Entity<Movie>(entity => { entity.HasKey(m => m.Id); entity.Property(m => m.Title).HasMaxLength(300).IsRequired(); }); }}
public class OrderingDbContext(DbContextOptions<OrderingDbContext> options) : DbContext(options){ public DbSet<Rental> Rentals => Set<Rental>();
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasDefaultSchema("ordering");
modelBuilder.Entity<Rental>(entity => { entity.HasKey(r => r.Id); entity.Property(r => r.MovieTitle).HasMaxLength(300).IsRequired(); }); }}Both contexts connect to the same database but operate on different schemas (catalog.Movies vs ordering.Rentals). This gives you:
- Logical isolation - each module can only see its own tables
- Independent migrations - adding a column to Catalog doesn’t touch Ordering
- Single database - simpler infrastructure, no cross-database complexity
Remember to configure separate migration history tables when using schemas - the default __EFMigrationsHistory won’t inherit the schema.
The Read Replica Pattern
A practical use of multiple DbContexts is separating read and write operations at the database level. You create a write context pointing to the primary database and a read context pointing to a read replica, configured with NoTracking by default for better performance.
// Write context - primary databasepublic class MovieDbContext(DbContextOptions<MovieDbContext> options) : DbContext(options){ public DbSet<Movie> Movies => Set<Movie>();}
// Read-only context - read replicapublic class MovieReadDbContext(DbContextOptions<MovieReadDbContext> options) : DbContext(options){ public DbSet<Movie> Movies => Set<Movie>();}Register with different connection strings and configure the read context for no-tracking:
builder.Services.AddDbContext<MovieDbContext>(options => options.UseNpgsql(builder.Configuration.GetConnectionString("MovieDb")));
builder.Services.AddDbContext<MovieReadDbContext>(options => options.UseNpgsql(builder.Configuration.GetConnectionString("MovieDbReplica")) .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));Now your write endpoints inject MovieDbContext and your read endpoints inject MovieReadDbContext. The read replica handles all the query load while the primary handles writes. This is a significant scaling pattern for read-heavy APIs.
Important: Only run migrations against the write context (primary database). The read replica receives changes through database replication, not through EF Core migrations.
Cross-Context Transactions
Here’s a common question: can you wrap operations on two different DbContexts in a single transaction? Yes - but only if they share the same database and the same database connection.
app.MapPost("/api/movies", async (CreateMovieRequest request, MovieDbContext movieDb, AnalyticsDbContext analyticsDb, CancellationToken ct) =>{ // Both contexts must use the same underlying database for this to work var connection = movieDb.Database.GetDbConnection(); await connection.OpenAsync(ct);
await using var transaction = await connection.BeginTransactionAsync(ct);
try { // Use the shared connection and transaction in the analytics context analyticsDb.Database.SetDbConnection(connection); await analyticsDb.Database.UseTransactionAsync(transaction, ct);
var movie = new Movie { Id = Guid.CreateVersion7(), Title = request.Title, Genre = request.Genre, Rating = request.Rating, ReleaseYear = request.ReleaseYear }; movieDb.Movies.Add(movie); await movieDb.SaveChangesAsync(ct);
analyticsDb.ApiEvents.Add(new ApiEvent { Id = Guid.CreateVersion7(), Endpoint = "/api/movies", Method = "POST", StatusCode = 201, DurationMs = 0 }); await analyticsDb.SaveChangesAsync(ct);
await transaction.CommitAsync(ct); return Results.Created($"/api/movies/{movie.Id}", movie); } catch { await transaction.RollbackAsync(ct); throw; }});Constraints:
- Both contexts must target the same database (same connection string). Cross-database transactions require distributed transactions, which most cloud databases don’t support.
- You share the
DbConnectionandDbTransactionexplicitly between contexts. - If you’re using different databases, you’ll need eventual consistency patterns (outbox pattern, message queues) instead of shared transactions.
In .NET 10, EF Core 10 continues to support
UseTransactionAsyncfor cross-context transaction coordination. For scenarios involving different databases, consider the Outbox Pattern or a message broker like RabbitMQ.
DbContext Pooling with Multiple Contexts
For high-throughput APIs, AddDbContextPool reuses DbContext instances instead of creating new ones per request, reducing allocation overhead. You can pool multiple contexts:
builder.Services.AddDbContextPool<MovieDbContext>(options => options.UseNpgsql(builder.Configuration.GetConnectionString("MovieDb")));
builder.Services.AddDbContextPool<AnalyticsDbContext>(options => options.UseNpgsql(builder.Configuration.GetConnectionString("AnalyticsDb")));Each context gets its own pool. The default pool size is 1024 instances per context type. Pooling works transparently - the context is reset (change tracker cleared, configuration preserved) when returned to the pool. See Microsoft’s advanced performance topics for DbContext pooling benchmarks and configuration details.
Caveat: Don’t store per-request state in DbContext fields when using pooling. The same instance will be reused across requests. If your context has custom state in its constructor or fields, use
AddDbContextinstead.
Common Mistakes and How to Fix Them
Mistake 1: Using Non-Generic DbContextOptions
// WRONG - both contexts get the same optionspublic class MovieDbContext(DbContextOptions options) : DbContext(options) { }public class AnalyticsDbContext(DbContextOptions options) : DbContext(options) { }DI can’t distinguish between the two. Both contexts receive the last registered DbContextOptions, meaning one of them silently connects to the wrong database.
Fix: Always use DbContextOptions<TContext>:
// CORRECT - each context gets its own typed optionspublic class MovieDbContext(DbContextOptions<MovieDbContext> options) : DbContext(options) { }public class AnalyticsDbContext(DbContextOptions<AnalyticsDbContext> options) : DbContext(options) { }Mistake 2: Forgetting —context in Migration Commands
# ERROR: "More than one DbContext was found"dotnet ef migrations add InitialFix: Always specify the context:
dotnet ef migrations add Initial --context MovieDbContext --output-dir Migrations/MovieDbMistake 3: Shared Migration History Table Conflicts
When two contexts target the same database without separate history tables, migrations from one context can appear to conflict with the other.
Fix: Configure separate history tables as shown in the Migration History Tables section.
Mistake 4: Attempting Cross-Context Joins
// This will NOT compile - different DbContext typesvar query = from m in movieDb.Movies join e in analyticsDb.ApiEvents on m.Id equals e.MovieId select new { m.Title, e.StatusCode };EF Core cannot join across contexts. Each context produces its own SQL query against its own database.
Fix: Query each context separately and join in memory:
var movies = await movieDb.Movies.AsNoTracking().ToListAsync(ct);var events = await analyticsDb.ApiEvents.AsNoTracking().ToListAsync(ct);
var combined = from m in movies join e in events on m.Title equals e.Endpoint select new { m.Title, e.StatusCode };For large datasets, this is inefficient. If you frequently need cross-entity queries, those entities probably belong in the same context.
Decision Guide - Should You Split Your DbContext?
| Question | If Yes | If No |
|---|---|---|
| Do the entities live in different databases? | Split - no choice, EF Core requires separate contexts | Keep single |
| Are the entities owned by different teams/modules? | Split - independent deployability and ownership | Probably keep single |
| Do you need a read replica? | Split - separate read/write contexts | Keep single |
| Do the entities frequently join with each other? | Keep single - cross-context joins aren’t possible | Consider splitting |
| Is your DbContext > 100 entities with slow startup? | Split - reduces model compilation time | Keep single |
| Is this a modular monolith with bounded contexts? | Split - one context per module | Keep single |
Key Takeaways
- Multiple DbContexts are EF Core’s answer to multi-database, bounded context, and read replica scenarios. Each context is independent - its own connection, change tracker, and migrations.
- Always use
DbContextOptions<TContext>(generic) - notDbContextOptions- to avoid DI resolution issues with multiple contexts. - Manage migrations separately with
--contextand--output-dirflags. Configure separateMigrationsHistoryTablewhen contexts share a database. - Cross-context transactions require a shared database connection. For different databases, use eventual consistency patterns.
- The read replica pattern - a write context on the primary, a read context on the replica with
NoTracking- is a powerful scaling technique for read-heavy APIs.
When should I use multiple DbContext in EF Core?
Use multiple DbContexts when your application connects to multiple databases, implements a modular monolith with bounded contexts, uses read replicas, or needs schema-level isolation between modules. If all your entities live in one database and frequently join with each other, a single DbContext is usually the better choice.
Can multiple DbContexts share the same database?
Yes. Multiple DbContexts can point to the same database with different schemas using HasDefaultSchema in OnModelCreating. This is common in modular monolith architectures. Configure separate migration history tables to avoid conflicts.
How do I run migrations with multiple DbContexts?
Specify the context with the --context flag: dotnet ef migrations add InitialCreate --context MovieDbContext --output-dir Migrations/MovieDb. Without this flag, EF Core throws a 'More than one DbContext was found' error.
Can I join data across multiple DbContexts?
No. EF Core cannot create SQL joins across different DbContext instances because each context manages its own database connection and query pipeline. You need to query each context separately and join the results in memory at the application layer.
How do transactions work across multiple DbContexts?
Transactions across multiple DbContexts only work when both contexts use the same database. You share a DbConnection and call UseTransactionAsync on the second context. For different databases, distributed transactions are required, which most cloud databases dont support - use eventual consistency patterns instead.
What is the difference between DbContextOptions and DbContextOptions<TContext>?
DbContextOptions<TContext> is the generic version that ties options to a specific DbContext type. When multiple DbContexts are registered in DI, the generic version ensures each context receives its own configuration (connection string, provider, etc.). Using the non-generic DbContextOptions causes DI to inject the same options into all contexts.
Should I use multiple DbContexts in a modular monolith?
Yes. In a modular monolith, each module should own its own DbContext with its own schema (using HasDefaultSchema). This enforces module boundaries at the database level - modules cannot accidentally query each others tables. Each module also gets independent migrations.
How do I configure separate schemas for each DbContext?
Override OnModelCreating in each DbContext and call modelBuilder.HasDefaultSchema(schemaName). For example, HasDefaultSchema("catalog") puts all entities in the catalog schema. Also configure separate migration history tables using MigrationsHistoryTable to prevent conflicts.
Troubleshooting Common Issues
“More than one DbContext was found. Specify which one to use.”
You forgot the --context flag in your migration command. Always specify: dotnet ef migrations add Name --context YourDbContext.
“Unable to resolve service for type DbContextOptions while attempting to activate YourDbContext.”
Your DbContext constructor uses DbContextOptions (non-generic) instead of DbContextOptions<YourDbContext>. Change the constructor parameter to the generic version.
“The migration history table conflicts between two contexts on the same database.”
Configure separate migration history tables with MigrationsHistoryTable in each context’s UseNpgsql (or UseSqlServer) call.
“SaveChanges on one context rolls back when the other fails, but I don’t have a shared transaction.”
Without a shared transaction, each context’s SaveChanges is independent. If you need atomicity across contexts, you must share the DbConnection and use UseTransactionAsync. Otherwise, implement compensation logic or the outbox pattern.
“My read replica context is running migrations and failing.” Only run migrations against the write context (primary database). Read replicas receive schema changes through database replication. Remove the read context from your migration scripts.
Summary
I covered why and when to use multiple DbContexts in EF Core 10, how to set them up with separate DI registrations, manage migrations independently, configure schema separation for modular monoliths, implement the read replica pattern, and handle cross-context transactions. The key principle: each DbContext should have a clear boundary and purpose - whether that’s a separate database, a module boundary, or a read/write split.
The complete source code - including both DbContexts, Docker Compose for two PostgreSQL instances, and separate migration folders - is available on GitHub.
If you found this helpful, share it with your team - especially if you’re planning a modular monolith or multi-database architecture. And if there’s a topic you’d like covered next, drop a comment.
Happy Coding :)



What's your Feedback?
Do let me know your thoughts around this article.