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.
One recipe, three different kitchens
Imagine your grandmother has a famous recipe for biryani. The recipe is the same in her head, but the kitchen changes. At home she cooks on a gas stove. At your uncle's place she uses an induction cooktop. At the village house she cooks on a wood fire. The dish she wants is identical, but the steps to cook it are slightly different in each kitchen. The wood fire needs more time. The induction needs different settings.
EF Core migrations work the same way. Your C# classes (your Product, your Order, your Customer) are the recipe. But the database is the kitchen. SQL Server, SQLite, and PostgreSQL are three different kitchens. Each one needs slightly different instructions to build the same tables. So you cannot use one single set of migration files for all of them. You need one set of cooking steps per kitchen.
This article shows you how to create and organize those separate steps in a clean, simple way.
Why one set of migrations is not enough
A migration is a small file that tells the database how to change its shape. EF Core writes the SQL inside that file for one specific provider. The trouble is that every database engine has its own way of saying things.
Look at how the same idea is written differently:
| Idea | SQL Server | SQLite | PostgreSQL |
|---|---|---|---|
| Text column | nvarchar(max) | TEXT | text |
| Auto-number key | IDENTITY(1,1) | AUTOINCREMENT | serial / identity |
| Boolean | bit | INTEGER (0/1) | boolean |
| Date and time | datetime2 | TEXT | timestamp |
Because the generated code is different, a SQL Server migration simply will not run correctly on SQLite. So we keep them apart. The good news is that EF Core has built-in support for this, and once you set it up, the day-to-day work is easy.
The plan: separate projects per provider
There are two common ways to keep migrations apart. You can keep them in separate folders inside one project, or you can keep them in separate projects. The folder way is fine for tiny demos, but it gets messy fast. The cleaner, recommended way is to give each provider its own small class library project that holds only its migrations.
Here is the shape we are aiming for:
Project layout for multi-provider migrations
Steps
Core
Has DbContext and entities
SqlServerMigrations
Refs Core + SqlServer provider
SqliteMigrations
Refs Core + Sqlite provider
PostgresMigrations
Refs Core + Npgsql provider
The big idea: your DbContext and your entity classes live in one shared place. Each migrations project references that shared place, adds its own provider package, and stores only its own migration files. When you change the model, you add one migration into each provider project. The model stays in one spot; only the generated SQL steps multiply.
Step 1: Build the shared model
Let's build a small online shop. First, the entity and the DbContext. This lives in the core project. Notice it does not pick a provider. It only describes the data.
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public DateTime CreatedOn { get; set; }
}
public class ShopDbContext : DbContext
{
public ShopDbContext(DbContextOptions<ShopDbContext> options)
: base(options)
{
}
public DbSet<Product> Products => Set<Product>();
}The DbContext takes its options from the outside. That is the key trick. Because it does not hard-code SQL Server or SQLite, we can hand it whichever provider we want at runtime or at migration time.
Step 2: Teach the app which provider to use
Now we need a way to say "use SQL Server" or "use SQLite". We read a simple setting and switch on it. This code can live in your Program.cs for the web app, and we will reuse the same idea in the design-time factory.
var provider = builder.Configuration["Provider"] ?? "SqlServer";
builder.Services.AddDbContext<ShopDbContext>(options =>
{
switch (provider)
{
case "Sqlite":
options.UseSqlite(
builder.Configuration.GetConnectionString("Sqlite"),
b => b.MigrationsAssembly("SqliteMigrations"));
break;
case "Postgres":
options.UseNpgsql(
builder.Configuration.GetConnectionString("Postgres"),
b => b.MigrationsAssembly("PostgresMigrations"));
break;
default:
options.UseSqlServer(
builder.Configuration.GetConnectionString("SqlServer"),
b => b.MigrationsAssembly("SqlServerMigrations"));
break;
}
});The most important part here is MigrationsAssembly. It tells EF Core, "for this provider, the migration files live in that project." Without it, EF Core would look for the migrations next to the DbContext, and it would not find them. This one line is what wires each provider to its own project.
Step 3: Add a design-time factory
When you run dotnet ef, the tools need to build your DbContext without actually starting your whole app. If your app uses dependency injection (and most do), the cleanest helper is a class that implements IDesignTimeDbContextFactory<ShopDbContext>. The EF tools look for this class first and use it to create the context.
We read the provider from the command-line arguments, just like before.
public class ShopDbContextFactory : IDesignTimeDbContextFactory<ShopDbContext>
{
public ShopDbContext CreateDbContext(string[] args)
{
// EF passes everything after "--" into args
var provider = args.Length > 0 ? args[0] : "SqlServer";
var optionsBuilder = new DbContextOptionsBuilder<ShopDbContext>();
switch (provider)
{
case "Sqlite":
optionsBuilder.UseSqlite("Data Source=shop.db",
b => b.MigrationsAssembly("SqliteMigrations"));
break;
case "Postgres":
optionsBuilder.UseNpgsql("Host=localhost;Database=shop;Username=postgres;Password=pass",
b => b.MigrationsAssembly("PostgresMigrations"));
break;
default:
optionsBuilder.UseSqlServer("Server=.;Database=Shop;Trusted_Connection=True;TrustServerCertificate=True",
b => b.MigrationsAssembly("SqlServerMigrations"));
break;
}
return new ShopDbContext(optionsBuilder.Options);
}
}For migration commands you do not need a real, reachable database. EF only needs to know the provider so it can generate the right SQL shape. So a placeholder connection string is fine here. You will use real connection strings later, when you actually apply the migrations.
Step 4: Create a migration for each provider
Now the fun part. We add the same logical migration once per provider, sending each into its own project. The pieces after the -- are arguments handed to your factory.
# Make sure the EF tool is installed
dotnet tool install --global dotnet-ef
# SQL Server migration, stored in the SqlServerMigrations project
dotnet ef migrations add InitialCreate \
--project SqlServerMigrations \
--startup-project Shop.Web \
-- SqlServer
# SQLite migration, stored in the SqliteMigrations project
dotnet ef migrations add InitialCreate \
--project SqliteMigrations \
--startup-project Shop.Web \
-- Sqlite
# PostgreSQL migration, stored in the PostgresMigrations project
dotnet ef migrations add InitialCreate \
--project PostgresMigrations \
--startup-project Shop.Web \
-- PostgresLet's read these flags slowly, because they confuse a lot of people:
| Flag | What it means |
|---|---|
--project | Where to write the migration files |
--startup-project | Where the app config and DI live |
-- | Everything after this goes to your app as arguments |
SqlServer / Sqlite | The provider name your factory reads |
After these run, each migrations project has its own InitialCreate file. Same table, three different SQL flavors. Open them up and you will see the difference with your own eyes: the SQL Server one uses nvarchar, the SQLite one uses TEXT, the Postgres one uses text.
The add-migration flow per provider
Steps
Run dotnet ef
With --project and -- arg
Factory reads arg
args[0] = provider name
Pick provider
UseSqlServer / UseSqlite / UseNpgsql
Write to project
Migration saved in its own assembly
Step 5: Apply the migrations
Creating a migration only writes the file. To actually change a real database, you run database update. Again, you pick the provider per run.
# Apply to SQL Server
dotnet ef database update \
--project SqlServerMigrations \
--startup-project Shop.Web \
-- SqlServer
# Apply to SQLite
dotnet ef database update \
--project SqliteMigrations \
--startup-project Shop.Web \
-- SqliteIn real projects you usually do not run these by hand on production. Instead you generate a SQL script or a migration bundle in your deployment pipeline, and run that. But for learning and local work, database update is perfect.
Keeping the three sets in sync
The one rule you must never forget: when you change the model, add a migration to every provider project. If you add a Description property to Product and only create a SQL Server migration, your SQLite and Postgres databases will fall behind. Sooner or later that mismatch causes a confusing error.
A tiny habit that saves hours: write a small script that runs all three migrations add commands together with the same name. Then you cannot forget one. Here is the idea:
# add-all.sh — add the same migration to all providers
NAME=$1
dotnet ef migrations add $NAME --project SqlServerMigrations --startup-project Shop.Web -- SqlServer
dotnet ef migrations add $NAME --project SqliteMigrations --startup-project Shop.Web -- Sqlite
dotnet ef migrations add $NAME --project PostgresMigrations --startup-project Shop.Web -- PostgresNow ./add-all.sh AddProductDescription updates all three at once. One command, three matching migrations, no drift.
Common questions and gotchas
Why a separate project and not just folders? You can use the --output-dir flag to keep providers in different folders inside one project. But then that one project must reference every provider package, and your project file gets crowded. Separate projects keep each provider's dependencies clean and clearly separated. It also means a small mobile or desktop app can reference just the one provider it needs.
Do I need the Design package everywhere? You need Microsoft.EntityFrameworkCore.Design in the startup project (or the project where the tools run). Each migrations project needs its own provider package, for example Microsoft.EntityFrameworkCore.Sqlite in the SQLite project.
What if I use one DbContext but the tooling complains? Make sure MigrationsAssembly points to the correct project name, and that the migrations project references the core project. Most "no migrations found" errors come from a missing or wrong MigrationsAssembly value.
Should I use multiple DbContext types instead? Some teams create one DbContext subclass per provider. That also works and the EF docs describe it. But for a single model shared across providers, the "one context + provider argument" approach shown here is usually simpler to maintain.
Here is a quick comparison of the two main strategies:
| Strategy | Good for | Trade-off |
|---|---|---|
| One context + provider argument | Same model on every provider | Needs a factory and the -- argument trick |
| One context type per provider | When providers need different model tweaks | More classes to keep in step |
A quick word on licensing
If you read community articles about multi-provider setups, you may see tools like MediatR and MassTransit mentioned in the same projects. Just so you know, both of those are now under a commercial license for many uses. They are not required for EF Core migrations at all, so you can build everything in this guide without them. EF Core itself stays free and open source.
Quick recap
- Your C# model is one recipe, but each database is a different kitchen, so each provider needs its own set of migrations.
- Keep your
DbContextand entities in one shared core project that does not pick a provider. - Give each provider its own small migrations project, and reference the core project from each.
- Use
MigrationsAssembly("...")to point each provider at its own migrations project. - Add a design-time factory (
IDesignTimeDbContextFactory) that reads a provider name from the command-line arguments. - Create migrations with
dotnet ef migrations add Name --project XProject --startup-project App -- ProviderName. - Apply them with
dotnet ef database updateper provider, or use SQL scripts and bundles in production. - Every time the model changes, add a matching migration to all providers. A small script keeps them in sync.
References and further reading
- Migrations with Multiple Providers — EF Core (Microsoft Learn)
- Using a Separate Migrations Project — EF Core (Microsoft Learn)
- Entity Framework Core and Multiple Database Providers — The .NET Tools Blog (JetBrains)
- EF Core Multiple Providers Example — khalidabuhakmeh (GitHub)
Related Posts
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.
5 EF Core Features You Need to Know (Beginner Friendly)
Learn 5 must-know EF Core features with simple examples: change tracking, AsNoTracking, eager loading, bulk ExecuteUpdate, and query filters.
A Clever Way to Implement Pessimistic Locking in EF Core
Learn pessimistic locking in EF Core using UPDLOCK and FOR UPDATE with a simple analogy, diagrams, and a clean reusable helper. Stop race conditions on shared rows.
Scheduling Jobs with Quartz.NET and Database Persistence using EF Core Migrations
Learn Quartz.NET job scheduling in .NET 10 with database persistence. Set up AdoJobStore, create QRTZ tables via EF Core migrations, and survive restarts.
Multi-Tenant Applications With EF Core: A Beginner's Guide
Learn multi-tenancy in EF Core the simple way. Isolate tenant data with global query filters, ITenantService, and named filters in .NET 10, with diagrams and code.