Multi-Tenanted Entity Framework 6 Migration Deployment

by Chad
Published April 10, 2021
Last updated March 19, 2023

Waves Waves

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:

  1. PowerShell modules, e.g. Enable-Migrations, Add-Migration, Update-Database
  2. Automatically applying migrations at app start up, e.g. Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyDbContext, MyDataProject.Configuration>())
  3. 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!

Read Next

Multi-Tenanted Entity Framework Core Migration Deployment image

April 11, 2021 by Chad

Multi-Tenanted Entity Framework Core Migration Deployment

There's many ways to deploy pending Entity Framework Core (EF Core) migrations, especially for multi-tenanted scenarios. In this post, I'll demonstrate a strategy to efficiently apply pending EF Core 6 migrations using a .NET 6 console app.

Read Article
Entity Framework 6 vs Entity Framework Core 3: Comparing Performance image

December 02, 2019 by Chad

Entity Framework 6 vs Entity Framework Core 3: Comparing Performance

Entity Framework (EF) Core was a complete rewrite from the tried and tested EF6. One of the most touted benefits EF Core has over EF6 is improved performance. Using real benchmarks, I will use worked examples to demonstrate whether Entity Framework 6 or Entity Framework Core performs the best.

Read Article
Entity Framework Performance: 3 Things You Must Consider image

July 20, 2019 by Chad

Entity Framework Performance: 3 Things You Must Consider

I hear it all the time: Entity Framework is slow, Entity Framework can't handle this kind of volume, We need to rip out Entity Framework for regular SQL. In some cases this is necessary, but let me demonstrate a few easy ways to make sure you're eeking the most performance out of your Entity Framework queries.

Read Article