Using Multiple EF Core DbContext in a Single Application
Learn how to use multiple EF Core DbContext classes in one .NET app. See when to split, how to register, migrate, and coordinate them with simple examples.
Two diaries in one school bag
Imagine a student named Asha. She keeps two diaries in her school bag. One is her homework diary, where she writes assignments and due dates. The other is her pocket-money diary, where she tracks the rupees she saves and spends.
Both diaries live in the same bag. Both belong to Asha. But she never mixes them. Homework goes in one, money goes in the other. When she wants to add a note, she opens the right diary, writes, and closes it.
Why two diaries and not one big one? Because the topics are different. Homework and money have nothing to do with each other. Keeping them apart makes each diary clean, short, and easy to read.
In Entity Framework Core, a DbContext is like one diary. It is your window into a set of tables. Most small apps use just one DbContext for everything. But as an app grows, you sometimes want more than one DbContext in the same application, just like Asha's two diaries in one bag.
This article shows you when to do that, how to set it up, and the traps to avoid.
A quick reminder: what a DbContext is
A DbContext is the main class you use in EF Core to talk to a database. It does three jobs:
- It manages the database connection.
- It tracks changes to your objects so it knows what to save.
- It turns your LINQ queries into SQL.
A single DbContext groups together a set of DbSet properties. Each DbSet maps to a table. So one context is really just "a group of tables I work with together."
When we say "multiple DbContexts," we mean you write two or more DbContext classes and use them both inside the same running app.
When should you split into multiple DbContexts?
Do not split just because you can. Each extra context is more code to manage. Split only when you have a real reason. Here are the common ones.
| Reason to split | What it means | Example |
|---|---|---|
| Many databases | Each context points to a different physical database | One app reads from a CRM database and writes to an Orders database |
| Modular monolith | Each module owns its own tables and schema in one database | A Sales module and a Billing module live in the same app but stay separate |
| Read vs write split | One context for writing, one for a read replica | A write context on the main server, a read context with no change tracking on a copy |
| Third-party schema | One context for your tables, one for a vendor's tables | Your app plus an external Identity store you do not control |
The golden rule from the EF Core team is simple: one context per module or bounded context, not one per table. If you create a context for every single table, you will drown in code and gain nothing.
Deciding whether to split
Steps
Need
Do you have separate databases or modules?
Boundary
Is there a clear line between the data sets?
Decide
If yes to both, split. If not, keep one.
Setting up two DbContext classes
Let us build a tiny example. We have a SalesDbContext for orders and a BillingDbContext for invoices. Both are normal EF Core contexts. The only new idea is that we write two of them.
public class SalesDbContext : DbContext
{
public SalesDbContext(DbContextOptions<SalesDbContext> options)
: base(options) { }
public DbSet<Order> Orders => Set<Order>();
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Keep this module's tables in their own schema.
modelBuilder.HasDefaultSchema("sales");
}
}
public class BillingDbContext : DbContext
{
public BillingDbContext(DbContextOptions<BillingDbContext> options)
: base(options) { }
public DbSet<Invoice> Invoices => Set<Invoice>();
public DbSet<Payment> Payments => Set<Payment>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("billing");
}
}Notice the generic type. SalesDbContext takes DbContextOptions<SalesDbContext>, and BillingDbContext takes DbContextOptions<BillingDbContext>. This is very important. The generic parameter tells EF Core which context the options belong to. If you use the non-generic DbContextOptions, the dependency injection container gets confused when there is more than one context, and you will get a runtime error.
Registering both contexts with dependency injection
In Program.cs, you register each context with its own AddDbContext call. Each call names the context and gives it a connection string.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<SalesDbContext>(options =>
options.UseNpgsql(
builder.Configuration.GetConnectionString("SalesDb")));
builder.Services.AddDbContext<BillingDbContext>(options =>
options.UseNpgsql(
builder.Configuration.GetConnectionString("BillingDb")));
var app = builder.Build();The two contexts can point to the same database or to different databases. That choice is yours.
- If both connection strings point to the same database, the two contexts simply own different schemas (
salesandbilling) inside it. This is the modular monolith style. - If the connection strings point to different servers, you are working with two real databases. This is common when one is a legacy system.
Now any service can ask for either context through its constructor, and EF Core hands over the right one.
public class OrderService
{
private readonly SalesDbContext _sales;
private readonly BillingDbContext _billing;
public OrderService(SalesDbContext sales, BillingDbContext billing)
{
_sales = sales;
_billing = billing;
}
public async Task PlaceOrderAsync(Order order)
{
_sales.Orders.Add(order);
await _sales.SaveChangesAsync();
var invoice = new Invoice { OrderId = order.Id, Amount = order.Total };
_billing.Invoices.Add(invoice);
await _billing.SaveChangesAsync();
}
}The big limitation: no joins across contexts
This is the rule people forget most often, so read it twice.
You cannot write a single LINQ query that joins tables from two different DbContexts.
Why not? EF Core does not know whether the two contexts point to the same database. They might be on two different servers, or even two different database engines. So EF Core refuses to guess. It will not build a SQL JOIN across them.
This will not work:
// WRONG: you cannot join across two contexts in one query.
var report =
from o in _sales.Orders
join i in _billing.Invoices on o.Id equals i.OrderId
select new { o.Id, i.Amount };Instead, you load from each context separately and join the results in plain C# memory:
// RIGHT: load from each context, then combine in memory.
var orders = await _sales.Orders.ToListAsync();
var invoices = await _billing.Invoices.ToListAsync();
var report = orders.Join(
invoices,
o => o.Id,
i => i.OrderId,
(o, i) => new { o.Id, i.Amount });The lesson is clear: entities that need to be joined often should live in the same context. If two tables are always queried together, do not split them apart. This is exactly why we group by module, not by table.
Combining data across contexts
Steps
Load A
Query the first context to a list
Load B
Query the second context to a list
Join in C#
Use LINQ in memory to match them
Migrations with multiple contexts
Each DbContext has its own model, so each needs its own set of migrations. EF Core keeps them apart using a flag.
When you run a migrations command, add --context and name the context. You should also point each context's migrations to its own folder so the files do not mix.
# Create the first migration for each context separately.
dotnet ef migrations add InitSales --context SalesDbContext --output-dir Migrations/Sales
dotnet ef migrations add InitBilling --context BillingDbContext --output-dir Migrations/Billing
# Apply them, one context at a time.
dotnet ef database update --context SalesDbContext
dotnet ef database update --context BillingDbContextThere is one more detail when both contexts share a single database. EF Core records applied migrations in a table called __EFMigrationsHistory. If both contexts use the same history table, they will fight. The fix is to give each context its own history table, usually inside its own schema.
builder.Services.AddDbContext<SalesDbContext>(options =>
options.UseNpgsql(
builder.Configuration.GetConnectionString("AppDb"),
npgsql => npgsql.MigrationsHistoryTable(
"__EFMigrationsHistory", "sales")));
builder.Services.AddDbContext<BillingDbContext>(options =>
options.UseNpgsql(
builder.Configuration.GetConnectionString("AppDb"),
npgsql => npgsql.MigrationsHistoryTable(
"__EFMigrationsHistory", "billing")));Now the Sales module tracks its history in sales.__EFMigrationsHistory, and Billing uses billing.__EFMigrationsHistory. They never overwrite each other.
Transactions across two contexts
What if you must save to both contexts and want both saves to succeed together, or both to fail together? That is a transaction across contexts. It is possible, but how you do it depends on the setup.
If both contexts share the same database, the cleanest way is to share one database connection and one transaction. You open a connection, start a transaction, and tell both contexts to use it.
If the contexts use different databases, a single local transaction cannot cover both. You then need either a distributed transaction (heavier, and not supported on every platform) or a safer pattern like the outbox pattern, where you save a message in the first database and process it later.
| Situation | Recommended approach | Note |
|---|---|---|
| Same database, two contexts | Share one connection and transaction | Simple and reliable |
| Two different databases | Outbox pattern or eventual consistency | Avoids fragile distributed transactions |
| Cross-database, must be atomic | Distributed transaction | Heavy, limited platform support |
Here is the shared-database version. Both contexts ride the same transaction, so either everything commits or everything rolls back.
using var connection = new NpgsqlConnection(connectionString);
await connection.OpenAsync();
using var transaction = await connection.BeginTransactionAsync();
_sales.Database.SetDbConnection(connection);
await _sales.Database.UseTransactionAsync(transaction);
_billing.Database.SetDbConnection(connection);
await _billing.Database.UseTransactionAsync(transaction);
try
{
_sales.Orders.Add(order);
await _sales.SaveChangesAsync();
_billing.Invoices.Add(invoice);
await _billing.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}For most apps that span truly separate databases, reach for the outbox pattern instead. It is calmer and survives crashes better. Trying to force two databases into one atomic transaction is where many production headaches begin.
A note on read and write contexts
A popular use of multiple contexts is splitting reads from writes. You make a WriteDbContext that points to the main database, and a ReadDbContext that points to a read replica. The read context is configured with no change tracking, because reads do not need it. This makes queries lighter and faster.
builder.Services.AddDbContext<ReadDbContext>(options =>
options
.UseNpgsql(builder.Configuration.GetConnectionString("ReadReplica"))
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));This is a clean, beginner-friendly first step into CQRS (Command Query Responsibility Segregation). You do not have to learn the whole pattern. Just one read context and one write context already gives you a real speed win on read-heavy pages.
Common mistakes to avoid
Here are the slips that catch people most often.
- Using non-generic
DbContextOptions. Always useDbContextOptions<TContext>. The generic type is how DI tells the contexts apart. - Splitting by table. One context per table is too fine. Group by module or bounded context.
- Sharing a migration history table. Give each context its own history table or schema when they share a database.
- Trying to join across contexts in SQL. EF Core will not do it. Join in memory, or keep related tables together.
- Forcing distributed transactions everywhere. If your contexts use separate databases, prefer the outbox pattern over heavy distributed transactions.
Putting it all together
Think back to Asha and her two diaries. Two diaries, one bag. Each diary is clean because it holds only one kind of note. She never tries to read a money entry while flipping the homework diary.
Multiple DbContexts work the same way. Each context owns a clear group of tables. They live in the same app, register separately with DI, migrate separately, and stay out of each other's way. The moment two contexts start needing constant joins, that is your sign you split them in the wrong place, and you should merge those tables back into one context.
Used with care, multiple DbContexts keep large applications tidy and modules independent. Used carelessly, they add complexity for no gain. Now you know which side of that line to stand on.
Quick recap
- A
DbContextis one "diary" — a group of tables you work with together. - Use multiple contexts for separate databases, modules, or read/write splits, not for every table.
- Register each context with its own
AddDbContextcall and useDbContextOptions<TContext>. - You cannot join across two contexts in one query. Load each, then join in C# memory.
- Give each context its own migrations folder with
--contextand its own history table when sharing a database. - For cross-database saves, prefer the outbox pattern over distributed transactions.
- Group by bounded context, and merge tables back together if they always need joins.
References and further reading
- DbContext Configuration and Initialization — EF Core (Microsoft Learn)
- Managing Migrations for Multiple Projects and Contexts — EF Core (Microsoft Learn)
- Using Multiple EF Core DbContexts In a Single Application — Milan Jovanović
- Multiple DbContext in EF Core 10: Scenarios, Setup and Migrations — codewithmukesh
Related Posts
How to Manage EF Core DbContext Lifetime: A Beginner's Guide
Learn how to manage EF Core DbContext lifetime safely. Understand scoped, transient, singleton, pooling, and DbContextFactory with simple examples and diagrams.
How to Create Migrations for Multiple Databases in EF Core (.NET 10)
Learn how to create EF Core migrations for multiple databases like SQL Server, SQLite, and PostgreSQL using separate migration projects, with simple examples.
Querying and Performing Transactions Across Multiple Database Schemas in a Modular Monolith
Learn how to query data and run safe transactions across multiple database schemas in a .NET modular monolith using EF Core, the outbox pattern, and more.
EF Core DbContext Options Explained: A Beginner's Friendly Guide
Learn EF Core DbContext options in simple words: AddDbContext, the options builder, retry on failure, query splitting, logging, lifetimes and pooling, with diagrams and examples.
EF Core Migrations: A Detailed Beginner Guide for .NET
Learn EF Core migrations step by step. Add, apply, revert, and ship database changes safely with simple examples, diagrams, tables, and best practices for .NET 10.
Soft Delete with EF Core: Delete Data Without Losing It
Learn soft delete in EF Core the right way. Use an interceptor and global query filters to hide deleted rows automatically, with simple examples, diagrams, code, and best practices for .NET 10.