In this article, we will discuss the different approaches to migrating a production database when using Entity Framework (EF) Core code-first migrations.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

Entity Framework Core Project Setup

Let’s start by creating a template Web API project and installing the necessary Entity Framework Core NuGet packages:

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Design

In this article, we use Microsoft SQL Server as the database, but the principles work well with any other database supported by Entity Framework Core. Just install the appropriate NuGet package instead of Microsoft.EntityFrameworkCore.SqlServer.

Create the Entity Framework Core Domain Model

Let’s create a WeatherForecast class:

public class WeatherForecast(DateOnly date, int temperatureC, string? summary)
{
    public Guid Id { get; private set; }
    public DateOnly Date { get; private set; } = date;
    public int TemperatureC { get; private set; } = temperatureC;
    public string? Summary { get; private set; } = summary;
    
    public int TemperatureF => 32 + (int) (TemperatureC / 0.5556);

    private WeatherForecast() : this(default, default, default)
    {
    }
}

Here, we define a copy of the WeatherForecast record, with some additional changes to make it Entity Framework Core compatible. First, we add an Id property so that it has a primary key. Next, we add private setters for every property, so Entity Framework will be able to set the values through reflection.

Lastly, besides the C# 12 styled primary constructor, we add a private parameterless one to make instantiation easier for Entity Framework. With that done, we can remove the WeatherForecast record and update the references to use our new domain class.

Create First Entity Framework Migration

Now, with everything set up, let’s navigate to the project’s root folder, and then create the initial migration:

dotnet ef migrations add InitialMigration

A new Migrations folder is created, which contains the migration class and the DbContextModelSnapshot. These classes are necessary for Entity Framework Core to be able to generate the correct SQL script. That said, let’s explore the possible options to apply this migration to a production database.

Manual Entity Framework Migrations

Manual migration means that we have to log in directly to the production SQL Server and apply a migration script, either by hand or with the help of CLI tools. This approach requires the least configuration and setup but is hard to automate. Let’s see what our options are for carrying out this kind of migration.

Generate the Entity Framework Migration SQL

To generate a simple migration SQL that will migrate a blank database to the latest version, we can use the dotnet ef CLI tool:

dotnet ef migrations script [From] [To]

The From and To parameters are optional. We can specify the name of the migration we wish to use as a starting point and another migration as the endpoint that we intend to update to. This allows us to generate partial (incremental) migrations scripts, or even rollback scripts if To is an earlier migration than From.

The value of From should always be the latest migration that is currently applied to the database. However, at the time of script generation, the latest applied migration might be unknown to us. Fortunately, the dotnet ef tool has an -idempotent flag, which will check what the latest applied migration is, and generate the script from that migration up to the newest one.

If we run the command and the SQL script generated in the console looks and behaves correctly, we simply have to copy and execute it on the SQL Server, with a query console, or by using SQL Server Management Studio for example.

When using script generation as our migration strategy, we can inspect and modify the SQL script before applying it, preventing faulty migrations. In production scenarios, this is a huge benefit. However, we must have access to the production database in order to apply it, which is a security risk. Moreover, because humans apply these scripts, in a complex scenario with multiple databases and migrations, this is prone to user error. Also, applying migrations has to be in sync with releasing the code, which can be challenging.

Now let’s see how we can eliminate the need for manual SQL script running.

Using EF Core Command-line Tools

The dotnet ef CLI tool is very versatile and it can also be used to apply Entity Framework migrations directly to the database:

dotnet ef database update

This command mirrors the idempotent script migration. It checks the last applied migration, generates the script up to the newest migration, and applies it to the database. Now we won’t have to run the script manually, but the drawback is that we also cannot check whether or not the script is correct. The other drawback of this approach is that the source code and dotnet tools have to be available on the production machine. It is a huge security risk and a bad practice to expose the source code on the server.

So now let’s see how to apply migrations without exposing the source code.

Using Entity Framework Migration Bundles

Entity Framework Migration bundles are executable binary files that contain everything necessary to carry out migrations. Even the dotnet runtime is unnecessary because the executable includes it if we build it using the --self-contained option. Let’s generate a migration bundle:

dotnet ef migrations bundle [--self-contained] [-r]

The two main options we can use are the self-contained flag, which we discussed earlier, and the -r flag, which specifies the target runtime. If the machine is running on the same operating system (eg: windows-x64) as the production server, the flag can be omitted. The output on Windows will be a file named efbundle.exe.

To apply the migration we either have to copy the appsettings.json file to the same directory as the efbundle.exe file, or use the --connection option to pass a connection string to the database:

./efbundle.exe --connection 'Server=.;Database=Weather;Trusted_Connection=True;Encrypt=False'

Furthermore, we can use a CICD pipeline to automate this method.

The common feature in all the migration options so far is that they allow us to apply migrations independently from the application’s deployment. This could cause issues if either the application’s deployment or the database migration fails. Only the migration bundle method is an exception from this when combined with a CICD pipeline. Next, let’s explore how to apply migrations at runtime.

Automated Entity Framework Migrations at Runtime

To make Entity Framework migrations execution more convenient, we can trigger them at runtime from the application code. The DbContext class exposes a Database property, which has a Migrate() function. However, we have to be careful when migrating the database at runtime.

In production, we might have multiple instances of the application running. If some start at the same time, there is a chance that they will both start the migration and cause a conflict, or even worse a deadlock in the database. Moreover, it can lead to unexpected results if some instances are serving users while other instances migrate the database.

Let’s solve these issues by creating two Startup classes, one for the migration and another for running the application. We determine which Startup class to use based on command line arguments. This way, in a CICD pipeline we can start the application with the migration Startup class, then proceed to start the application only if the migration is successful. When instances restart, they will use the regular Startup class, hence not triggering a migration.

Create Two Startup Classes to Support EF Migrations

To simplify the Startup class selection, we will implement a factory pattern. To start, let’s create an IStartup interface:

public interface IStartup
{
    Task StartAsync(string[] args);
}

Our Startup classes will implement our new interface. This is necessary for the factory pattern, so the factory can return a common interface and in the Program class we can call the StartAsync() method on either Startup class.

To learn more about the factory pattern, please feel free to check out our article How to Use Factory Pattern With Dependency Injection in .NET.

Now, let’s create the factory itself:

public class StartupFactory
{
    public IStartup GetStartup(IEnumerable<string> args)
    {
        return args.Contains("--migrate") ? new MigrationStartup() : new WebApiStartup();
    }
}

We simply check if the --migrate option is specified in the command line arguments, and return the appropriate Startup class.

Next, let’s create our Startup classes. We’ll start by moving the code from our Program class into a new class called WebApiStartup:

public class WebApiStartup : IStartup
{
    public async Task StartAsync(string[] args)
    {
        //Code omitted for brevity
        
        await app.RunAsync();
    }
}

Here, we use our regular Startup class that runs the application. The only change necessary here is to use the app.RunAsync() method instead of its synchronous version.

Now, let’s create the MigrationStartup class:

public class MigrationStartup : IStartup
{
    public async Task StartAsync(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        
        builder.Services.AddDbContext<WeatherDbContext>((sp, o) =>
        {
            o.UseSqlServer(builder.Configuration.GetConnectionString("SqlServer"));
        });

        var app = builder.Build();

        using var scope = app.Services.CreateScope();
        await scope.ServiceProvider.GetRequiredService<WeatherDbContext>().Database.MigrateAsync();
    }
}

Here, we register only the DbContext into the DI container, since other services are not necessary for running Entity Framework migrations. After building the app, we create a scope before requesting the WeatherDbContext. Since WeatherDbContext is registered as a Scoped service by default, we must have a scope to request it. The DI’s root scope won’t be able to resolve it for us.

Then we call the MigrateAsync() method of the Database property. Similarly to migration bundles and the idempotent script, this will also check what is the latest migration and only apply the newer ones.

Finally, let’s get the appropriate Startup class in the Program class:

var startup = new StartupFactory().GetStartup(args);

await startup.StartAsync(args);

Now let’s take a look at how to start a migration and then the application.

Run Entity Framework Migrations at Startup

To apply migrations, we add the --migrate flag when starting the application:

./EfCoreCodeFirstMigrationsInProd --migrate

The most important benefit is the ease of CICD integration. But, it’s also convenient to do in the developer environment. It has all the benefits of migration bundles, but it doesn’t require an additional executable, and it works seamlessly in all environments.

Conclusion

We explored several ways to perform Entity Framework Core migrations in production, each with its advantages and disadvantages. It’s important to consider which of those benefits are the most important to us in a given situation, and which drawbacks we can tolerate.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!