Run code after all EF Core migrations are applied
B

2

8

EF Core 5 has various events, but they relate to DbContext. There are no events related to migrations (Migration).

I want to run custom code after all migrations are applied - whether triggered by code (context.Database.Migrate()) or the CLI (dotnet ef database update).

The workaround is to add an "empty" migration, and place my code in its Up method. But I would need to do that every time I add a migration.

Assuming there's no event or hook I can use (is there?), how can I run custom code after all migrations are applied?

Brownstone answered 11/12, 2021 at 4:53 Comment(0)
B
3

Here's a workaround.

[DbContext(typeof(MyContext))]
[Migration("99999999999999_Last1")]
public class Last1 : Migration {
  protected override void Up(MigrationBuilder migrationBuilder) {
    Task.Run(() => callPostMigrationCodeThatIsIdempotent()).GetAwaiter().GetResult();
    migrationBuilder.DeleteData(HistoryRepository.DefaultTableName, nameof(HistoryRow.MigrationId), "string", "99999999999999_Last2", null);
  }
}

[DbContext(typeof(MyContext))]
[Migration("99999999999999_Last2")]
public class Last2 : Migration {
  protected override void Up(MigrationBuilder migrationBuilder) {
    Task.Run(() => callPostMigrationCodeThatIsIdempotent()).GetAwaiter().GetResult();
    migrationBuilder.DeleteData(HistoryRepository.DefaultTableName, nameof(HistoryRow.MigrationId), "string", "99999999999999_Last1", null);
  }
}

There are two "last" migrations:

  • their ids are chosen so they run last ("99999999999999")
  • both call the custom code, which must be idempotent
  • each deletes the other from the history table
  • they use Task.Run for async over sync

During each run one or the other runs the custom code, and deletes the other from the history table. On the next run the opposite would occur.

The same double setup can be used for a pre-migration hook.

There's an open backlog item on the repo.

Brownstone answered 15/12, 2021 at 13:13 Comment(0)
C
0

This can be achieved by extending the standard EFCore migrator implementation:

public class CustomMigrator : Migrator
{
    public CustomMigrator (
        IMigrationsAssembly migrationsAssembly, 
        IHistoryRepository historyRepository, 
        IDatabaseCreator databaseCreator, 
        IMigrationsSqlGenerator migrationsSqlGenerator, 
        IRawSqlCommandBuilder rawSqlCommandBuilder, 
        IMigrationCommandExecutor migrationCommandExecutor, 
        IRelationalConnection connection, 
        ISqlGenerationHelper sqlGenerationHelper, 
        ICurrentDbContext currentContext, 
        IModelRuntimeInitializer modelRuntimeInitializer, 
        IDiagnosticsLogger<DbLoggerCategory.Migrations> logger, 
        IRelationalCommandDiagnosticsLogger commandLogger, 
        IDatabaseProvider databaseProvider) : base(migrationsAssembly, historyRepository, databaseCreator, migrationsSqlGenerator, rawSqlCommandBuilder, migrationCommandExecutor, connection, sqlGenerationHelper, currentContext, modelRuntimeInitializer, logger, commandLogger, databaseProvider)
    {
        _connection = connection;
    }

    IRelationalConnection _connection;

    public override void Migrate(string? targetMigration = null)
    {
        base.Migrate(targetMigration);
        if (!_connection.DbConnection.State.HasFlag(ConnectionState.Open))
        {
            _connection.Open();
        }
        using var command = _connection.DbConnection.CreateCommand();
        command.CommandText = "create or replace view v_aaa as select 1 as aaa";
        command.ExecuteNonQuery();
    }

    public override async Task MigrateAsync(string? targetMigration = null, CancellationToken cancellationToken = default)
    {
        await base.MigrateAsync(targetMigration, cancellationToken);
        if (!_connection.DbConnection.State.HasFlag(ConnectionState.Open))
        {
            await _connection.OpenAsync(cancellationToken);
        }
        using var command = _connection.DbConnection.CreateCommand();
        command.CommandText = "create or replace view v_aaa as select 1 as aaa";
        await command.ExecuteNonQueryAsync();
    }
}

The service should be substituted as follows:

builder.Services.AddDbContext<TDbContext>(options =>
{
    options.UseNpgsql(builder.Configuration.GetConnectionString(connectionStringName));
    options.ReplaceService<IMigrator, CustomMigrator>();
});
Cudlip answered 28/9 at 20:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.