Skip to main content
SEMastery
Data Accessintermediate

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.

11 min readUpdated January 26, 2026

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:

IdeaSQL ServerSQLitePostgreSQL
Text columnnvarchar(max)TEXTtext
Auto-number keyIDENTITY(1,1)AUTOINCREMENTserial / identity
BooleanbitINTEGER (0/1)boolean
Date and timedatetime2TEXTtimestamp

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.

One C# model produces a different migration set for each database provider.

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

Core (DbContext + Model)
SqlServerMigrations
SqliteMigrations
PostgresMigrations

Steps

1

Core

Has DbContext and entities

2

SqlServerMigrations

Refs Core + SqlServer provider

3

SqliteMigrations

Refs Core + Sqlite provider

4

PostgresMigrations

Refs Core + Npgsql provider

One core project holds the model. Each provider gets a thin migrations project.

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.

How the Provider setting decides which path the configuration takes.

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 \
  -- Postgres

Let's read these flags slowly, because they confuse a lot of people:

FlagWhat it means
--projectWhere to write the migration files
--startup-projectWhere the app config and DI live
--Everything after this goes to your app as arguments
SqlServer / SqliteThe 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

Run dotnet ef
Factory reads arg
Pick provider
Write to project

Steps

1

Run dotnet ef

With --project and -- arg

2

Factory reads arg

args[0] = provider name

3

Pick provider

UseSqlServer / UseSqlite / UseNpgsql

4

Write to project

Migration saved in its own assembly

Each command targets one project and passes one provider name.

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 \
  -- Sqlite

In 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.

Lifecycle from a model change to three updated databases.

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 -- Postgres

Now ./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:

StrategyGood forTrade-off
One context + provider argumentSame model on every providerNeeds a factory and the -- argument trick
One context type per providerWhen providers need different model tweaksMore 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 DbContext and 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 update per 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

Related Posts