C# - Entity Framework handling Migrate() when deploying old versions of code

Some code bases use the .Migrate() or .MigrateAsync() to upgrade their databases to the newest version. Using .Migrate() with no additional parameters upgrades the database to the newest migration and the migrations are normally created using the following:

dotnet ef migrations add <name of migration>

This will create a file with an Up() and Down() method that can be applied to upgrade or downgrade your database. With a simple table called SomeEntities with just an Id column it could look like the following:

public partial class first : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "SomeEntities",
            columns: table => new
            {   
                Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_SomeEntities", x => x.Id);
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "SomeEntities");
    }
}

In the above a table called SomeEntities is either created or dropped. You can choose to upgrade / downgrade to a specific migration by calling the Migrate() method and provide it with a specific migration:

//You do not need the full name, for example "20221024204148_first".
await migrator.MigrateAsync("First"); 

The above will migrate the database to the migration named "First". Whether we are behind or ahead of this migration, it will simply iterate through all the Ups or Downs it needs until it reaches that migration.

The question behind this post is: what if we checkout an older version of the code and try to migrate that version? If we have the following migrations:

  • First
  • Second
  • Third

If I have applied all three migrations. But then decide to checkout a version of the code base that only contains the first migration and run .Migrate(), what happens then? The code will not contain the definition for the Up() or Down() methods for migration second and third, but the database will have all three migrations recorded in the EFMigrationsHistory table.

If we peek inside the code for Migrate() for Entity Framework 6 we see the following:

if (string.IsNullOrEmpty(targetMigration))
{
    migrationsToApply = unappliedMigrations
        .OrderBy(m => m.Key)
        .Select(p => _migrationsAssembly.CreateMigration(p.Value, _activeProvider))
        .ToList();
    migrationsToRevert = Array.Empty<Migration>();
    actualTargetMigration = null;
}
else if (targetMigration == Migration.InitialDatabase)
{
    migrationsToApply = Array.Empty<Migration>();
    migrationsToRevert = appliedMigrations
        .OrderByDescending(m => m.Key)
        .Select(p => _migrationsAssembly.CreateMigration(p.Value, _activeProvider))
        .ToList();
    actualTargetMigration = null;
}
else
{
    targetMigration = _migrationsAssembly.GetMigrationId(targetMigration);
    migrationsToApply = unappliedMigrations
        .Where(m => string.Compare(m.Key, targetMigration, StringComparison.OrdinalIgnoreCase) <= 0)
        .OrderBy(m => m.Key)
        .Select(p => _migrationsAssembly.CreateMigration(p.Value, _activeProvider))
        .ToList();
    migrationsToRevert = appliedMigrations
        .Where(m => string.Compare(m.Key, targetMigration, StringComparison.OrdinalIgnoreCase) > 0)
        .OrderByDescending(m => m.Key)
        .Select(p => _migrationsAssembly.CreateMigration(p.Value, _activeProvider))
        .ToList();
    actualTargetMigration = appliedMigrations
        .Where(m => string.Compare(m.Key, targetMigration, StringComparison.OrdinalIgnoreCase) == 0)
        .Select(p => _migrationsAssembly.CreateMigration(p.Value, _activeProvider))
        .SingleOrDefault();
}

We can see that if we provide no targetMigration the list of migrationsToRevert will be empty, hence nothing will be rolled back. It will try to roll forward to the newest version that it knows of (first), which would be the current - as it does not know the second and third - so it does nothing.

If we supply the current migration in the code base with "First" It will have nothing to revert or apply as we have already applied that migration. It seems not to care that it has applied two additional ones in the Database.

If we try to roll forward to a non-existing migration we will see the following error: System.AggregateException: 'One or more errors occurred. (The migration 'Third' was not found.)'.

So this seems safe from what I can tell. But please try it in another environment than production. I did the above testing on a local MSSQL.

If you are wondering, Migrate() can revert to old versions and you might end up with a data loss if that happens, but it needs the newer migrations to do so. You need to be careful if you supply an old migration for your Migrate() call - as it will try to revert all newer changes if it has the migration files.

That is all

I hope you found this helpful, feel free to leave a comment down below!