In this article, we will discuss the different approaches to migrating a production database when using Entity Framework (EF) Core code-first migrations.
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:
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.
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.