There's many ways to deploy pending Entity Framework 6 (EF6) migrations, especially for multi-tenanted production scenarios. In this post, I'll demonstrate a strategy to efficiently apply pending migrations using a .NET 6 console app.
Looking for Entity Framework Core?
Update: after creating this post, I was inspired to try this strategy using EF Core. That post can be found here.
Update 2: Updated code and post content for .NET 6
The Code
The code for the example in this post can be found here. Feel free to dive right in!
Multi-Tenanted Databases
For the purposes of this post, multi-tenanted refers to each tenant having its own database or connection string that's compatible with a single DbContext.
Applying EF6 Migrations
There's several ways to apply pending EF6 migrations, and these are not limited to:
- PowerShell modules, e.g.
Enable-Migrations
,Add-Migration
,Update-Database
- Automatically applying migrations at app start up, e.g.
Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyDbContext, MyDataProject.Configuration>())
- Using ef6.exe (formerly migrate.exe)
With these options, you'll have to figure out a strategy to run pending migrations to each database.
A Fourth Option: Using DbMigrator in a Console App
A fourth option is to programatically use EF6's DbMigrator
object in a console app. I've decided for this example to use a small console app that targets .NET 6.
Here's the app's Main
method which outlines what I want this app to accomplish.
ConfigureLogging();
Log.Information("Starting parallel execution of tenant database migrations...");
List<MigratorTenantInfo> tenants = GetConfiguredTenants();
ExitCode exitCode = ExecuteMigrations(tenants);
Log.Information("Parallel execution of tenant database migrations is complete.");
return (int)exitCode;
I want to 1) configure some logging, so I know what's going on while the console app is running, 2) get information about the tenants, e.g. name, connection string, 3) execute the migrations against the tenants, 4) finally, return an exit code to signal to the runner that the process was a success or failure.
Getting the Configured Tenants
To gather information about each tenants, I have an appsettings.json file like this:
{
"MigratorTenantInfo": [
{
"Name": "Default",
"ConnectionString": "Server=(LocalDb)\\MSSQLLocalDB;Database=DefaultContext;Trusted_Connection=True;"
},
{
"Name": "ExtremeGolf",
"ConnectionString": "Server=(LocalDb)\\MSSQLLocalDB;Database=Ef6DbContextExtremeGolf;Trusted_Connection=True;"
},
{
"Name": "BirdiesRUs",
"ConnectionString": "Server=(LocalDb)\\MSSQLLocalDB;Database=Ef6ContextBirdiesRUs;Trusted_Connection=True;"
},
...
I can load this information into a list of strongly typed MigratorTenantInfo
objects using Microsoft.Extensions.Configuration
.
internal sealed class MigratorTenantInfo
{
public string Name { get; set; }
public string ConnectionString { get; set; }
}
List<MigratorTenantInfo> GetConfiguredTenants()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appSettings.json", optional: false);
IConfiguration config = builder.Build();
return config.GetSection(nameof(MigratorTenantInfo)).Get<List<MigratorTenantInfo>>();
}
Executing the Migrations
Now the fun part, I want to execute the migrations for each tenant. Being that there could be a decent number of tenant databases and the fact I'm fairly impatient, I want this to be fast. I'm going to use TPL, specifically Parallel.ForEach
, to execute each set of tenant migrations in parallel.
ExitCode ExecuteMigrations(List<MigratorTenantInfo> tenants)
{
ExitCode exitCode = ExitCode.Success;
try
{
Parallel.ForEach(tenants, tenant =>
{
MigrateTenantDatabase(tenant);
});
}
catch
{
exitCode = ExitCode.MigrationFailed;
}
return exitCode;
}
If I encounter an error during any of the migrations, such as a scenario where my code first model disagrees with the current state of migrations in my databases, I'll return an exit code that indicates failure. The console app runner, which could be my local OS or my deployment pipeline, will know something went wrong without making a mess of my console output with an unhandled exception.
void MigrateTenantDatabase(MigratorTenantInfo t)
{
var dbMigrator = new MigratorLoggingDecorator(
new DbMigrator(new Configuration
{
TargetDatabase = new DbConnectionInfo(t.ConnectionString, "System.Data.SqlClient")
}),
new EfLogger(t)
);
try
{
dbMigrator.Update();
}
catch (Exception e)
{
Log.Error(e, $"Error executing migrations for {t.Name}");
throw;
}
}
I've created a dbMigrator
object, ran the .Update()
method, and log any exceptions if they occur while propagating the exception to the caller.
There's a couple of additional things I did that were important to me. I wrapped the new DbMigrator(...)
, where I passed in the tenant information, in a MigratorLoggingDecorator
object. If you've used EF's PowerShell commands or used the ef6.exe, you'll recognize that EF6 logs information to the console such as the name of the migrations applied, or error information if an exception is encountered.
This logging doesn't happen by default when using DbMigrator
, so I passed in my own implementation of an abstract MigrationsLogger
, EfLogger
, as the second argument of the MigratorLoggingDecorator
.
internal sealed class EfLogger : MigrationsLogger
{
private readonly MigratorTenantInfo _tenantInfo;
public EfLogger(MigratorTenantInfo tenantInfo) => _tenantInfo = tenantInfo;
public override void Info(string message) => Log.Information($"{_tenantInfo.Name}: {message}");
public override void Verbose(string message) { /* no op */ }
public override void Warning(string message) => Log.Warning($"{_tenantInfo.Name}: {message}");
}
All I've done here is implemented the two methods, Info
, and Warning
with my corresponding logger methods. I could've implemented Verbose
, but it turned out to be more verbose than I wanted, so I left it no-op. I also passed in the tenant information as a constructor argument so my logger methods can know which tenant is being logged.
Conclusion
And that's it! Each of my tenant's migrations have run. Here's what the output looks like when no migrations are pending:
[14:50:54 INF] Starting parallel execution of tenant database migrations...
[14:50:56 INF] AblatrossWasHere: No pending explicit migrations.
[14:50:56 INF] ExtremeGolf: No pending explicit migrations.
[14:50:56 INF] Hole19: No pending explicit migrations.
[14:50:56 INF] BirdiesRUs: No pending explicit migrations.
[14:50:56 INF] AugustaWho: No pending explicit migrations.
[14:50:56 INF] HeadcoverCentral: No pending explicit migrations.
[14:50:56 INF] Default: No pending explicit migrations.
[14:50:56 INF] HeadcoverCentral: Running Seed method.
[14:50:56 INF] AblatrossWasHere: Running Seed method.
[14:50:56 INF] AugustaWho: Running Seed method.
[14:50:56 INF] Default: Running Seed method.
[14:50:56 INF] ExtremeGolf: Running Seed method.
[14:50:56 INF] BirdiesRUs: Running Seed method.
[14:50:56 INF] Hole19: Running Seed method.
[14:51:01 INF] Parallel execution of tenant database migrations is complete.
Thank you for reading. I hope this is valuable for folks that are still using EF6.
Happy Coding!