Skip to content

Latest commit

 

History

History
137 lines (101 loc) · 6.83 KB

File metadata and controls

137 lines (101 loc) · 6.83 KB

EntityFramework Migrations

EntityFramework Migrations are supported via the buildTemplate parameter on SqlInstance. When using migrations, pass the TemplateFromConnection overload of buildTemplate so that the template database schema is created by applying migrations rather than EnsureCreatedAsync.

var sqlInstance = new SqlInstance<MyDbContext>(
    buildTemplate: async (connection, options) =>
    {

        options.ReplaceService<IMigrationsSqlGenerator, MigrationsGenerator>();

        await using var data = new MyDbContext(options.Options);
        await data.Database.MigrateAsync();

    },
    constructInstance: builder => new(builder.Options));

snippet source | anchor

The above performs the following actions:

  • Creates a SqlInstance using the connection-based buildTemplate delegate.
  • Optionally replaces IMigrationsSqlGenerator with a custom implementation.
  • Constructs a DbContext from the provided DbContextOptionsBuilder.
  • Applies all pending migrations via Database.MigrateAsync().

The template database is built once and then cloned for each test, so migrations only run a single time regardless of how many tests execute.

EnsureCreated vs Migrate

By default (when no buildTemplate is provided), SqlInstance uses Database.EnsureCreatedAsync() to create the schema. This is simpler but does not use migration history. If the project uses EF migrations, pass a buildTemplate that calls Database.MigrateAsync() instead. Do not mix both — EnsureCreated and Migrate are mutually exclusive in EF Core.

Pending changes detection

When a buildTemplate delegate is provided via the context-based overload (TemplateFromContext), SqlInstance automatically calls ThrowIfPendingChanges() before building the template. This compares the current DbContext model against the latest migration snapshot and throws an InvalidOperationException listing the differences if any model changes have not been captured in a migration. This ensures the template database always matches the migration history and prevents silent schema drift during tests.

Custom Migrations Operations

Optionally use Custom Migrations Operations by replacing IMigrationsSqlGenerator on the options builder before applying migrations.

options.ReplaceService<IMigrationsSqlGenerator, MigrationsGenerator>();

snippet source | anchor

This is useful for scenarios such as custom SQL generation, adding seed data via migration operations, or integrating with database-specific features not covered by the default SQL generator.

Apply the migration

Perform a Runtime apply of migrations.

await using var data = new MyDbContext(options.Options);
await data.Database.MigrateAsync();

snippet source | anchor

This constructs a DbContext using the options builder (which is pre-configured to connect to the template database) and then applies all pending migrations. After this point the template database has the full schema and is ready to be cloned for individual tests.

Testing a specific migration

To test a single migration in isolation, build the template up to the migration before the one under test using IMigrator.MigrateAsync("targetMigration"). Each test then receives a clone at that point and can apply the next migration independently.

Build the template to a known migration

static SqlInstance<MyDbContext> sqlInstance = new(
    buildTemplate: async (connection, options) =>
    {
        await using var data = new MyDbContext(options.Options);
        var migrator = data.GetInfrastructure()
            .GetRequiredService<IMigrator>();
        // apply up to and including a target migration
        await migrator.MigrateAsync("AddOrders");
    },
    constructInstance: builder => new(builder.Options));

snippet source | anchor

This uses IMigrator (resolved from the EF Core service provider) instead of Database.MigrateAsync() so a target migration name can be specified. The template is frozen at that point and cloned for every test.

Apply and verify the next migration

[Test]
public async Task TestNextMigration()
{
    await using var database = await sqlInstance.Build();

    // apply the next migration under test
    var migrator = database.Context
        .GetInfrastructure()
        .GetRequiredService<IMigrator>();
    await migrator.MigrateAsync("AddOrderStatus");

    // verify the migration applied the expected schema change
    await using var command = database.Connection.CreateCommand();
    command.CommandText = """
        SELECT COUNT(*)
        FROM INFORMATION_SCHEMA.COLUMNS
        WHERE TABLE_NAME = 'Orders'
          AND COLUMN_NAME = 'Status'
        """;
    var result = (int) (await command.ExecuteScalarAsync())!;
    That(result, Is.EqualTo(1));
}

snippet source | anchor

Each test gets its own database clone at the earlier migration state. It then applies the target migration and verifies the expected schema change. Because every test starts from the same cloned template, migrations under test are fully isolated from each other.