Skip to main content

Finished reading? Get articles like this every Tuesday

Multiple DbContext in EF Core 10 - Scenarios, Setup & Migrations

When and how to use multiple DbContext in EF Core 10. Multi-database setup, schema separation, migrations, transactions, and modular monolith patterns.

dotnet webapi-course

efcore dbcontext multiple-databases ef-core-10 dotnet-10 migrations schema-separation dependency-injection modular-monolith postgresql web-api minimal-api transactions bounded-context read-replica connection-string csharp dotnet-webapi-zero-to-hero-course

14 min read
3.1K views

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:

ScenarioWhat It Looks LikeExample
Multiple databasesEach context connects to a different databaseMovie API + Analytics DB
Bounded contexts (DDD)Same database, different schemas, different entity setsCatalog schema + Ordering schema
Modular monolithEach module owns its own context and schemaUsers module + Billing module
Read replicaDedicated read-only context pointing to a replicaReporting queries on a read replica
Multi-tenantEach tenant gets its own context/databaseSaaS platform with per-tenant isolation
Mixed providersOne context for PostgreSQL, another for Redis/CosmosRelational + 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 which DbContextOptions belongs to which context. If you use the non-generic DbContextOptions, both contexts would receive the same options - which means the same connection string and configuration. The generic version DbContextOptions<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 operations
app.MapGet("/api/movies", async (MovieDbContext db, CancellationToken ct) =>
{
var movies = await db.Movies.AsNoTracking().ToListAsync(ct);
return Results.Ok(movies);
});
// Uses AnalyticsDbContext - event logging
app.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 endpoint
app.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:

Terminal window
dotnet ef migrations add InitialMovies --context MovieDbContext --output-dir Migrations/MovieDb
dotnet ef migrations add InitialAnalytics --context AnalyticsDbContext --output-dir Migrations/AnalyticsDb

To apply migrations:

Terminal window
dotnet ef database update --context MovieDbContext
dotnet ef database update --context AnalyticsDbContext

Migration 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.cs

CI/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 database
public class MovieDbContext(DbContextOptions<MovieDbContext> options) : DbContext(options)
{
public DbSet<Movie> Movies => Set<Movie>();
}
// Read-only context - read replica
public 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 DbConnection and DbTransaction explicitly 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 UseTransactionAsync for 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 AddDbContext instead.

Common Mistakes and How to Fix Them

Mistake 1: Using Non-Generic DbContextOptions

// WRONG - both contexts get the same options
public 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 options
public class MovieDbContext(DbContextOptions<MovieDbContext> options) : DbContext(options) { }
public class AnalyticsDbContext(DbContextOptions<AnalyticsDbContext> options) : DbContext(options) { }

Mistake 2: Forgetting —context in Migration Commands

Terminal window
# ERROR: "More than one DbContext was found"
dotnet ef migrations add Initial

Fix: Always specify the context:

Terminal window
dotnet ef migrations add Initial --context MovieDbContext --output-dir Migrations/MovieDb

Mistake 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 types
var 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?

QuestionIf YesIf No
Do the entities live in different databases?Split - no choice, EF Core requires separate contextsKeep single
Are the entities owned by different teams/modules?Split - independent deployability and ownershipProbably keep single
Do you need a read replica?Split - separate read/write contextsKeep single
Do the entities frequently join with each other?Keep single - cross-context joins aren’t possibleConsider splitting
Is your DbContext > 100 entities with slow startup?Split - reduces model compilation timeKeep single
Is this a modular monolith with bounded contexts?Split - one context per moduleKeep 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) - not DbContextOptions - to avoid DI resolution issues with multiple contexts.
  • Manage migrations separately with --context and --output-dir flags. Configure separate MigrationsHistoryTable when 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 :)

Grab the Source Code

Get the full implementation. Enter your email for instant access, or skip to GitHub.

Skip, go to GitHub directly

Want to reach 7,100+ .NET developers? See sponsorship options.

What's your Feedback?

Do let me know your thoughts around this article.

Weekly .NET tips, free

Free weekly newsletter

Stay ahead in .NET

Tutorials Architecture DevOps AI

Once-weekly email. Best insights. No fluff.

Join 7,100+ developers · Delivered every Tuesday

We value your privacy

We use cookies to improve your browsing experience, analyze site traffic, and personalize content. By clicking "Accept All", you consent to our use of cookies. Read our Privacy Policy