Skip to content

Latest commit

 

History

History
621 lines (469 loc) · 21.2 KB

File metadata and controls

621 lines (469 loc) · 21.2 KB

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[3.0.1] - 2026-02-27

Fixed

EF Core migrations now apply independently of application migrations (#4)

Previously, EF Core database migrations were only checked and applied when there were pending application migrations. This caused issues when switching between Git branches where the registered app migration version was higher than the target version in the current codebase.

Scenario that was broken:

  • Branch A: App version 1.0.9 applied, all EF migrations applied
  • Branch B: App version 1.0.7, but has a new EF Core migration
  • When switching to Branch B, the new EF Core migration would not apply because the registered version (1.0.9) was higher than the target (1.0.7)

Fix: EF Core migrations are now decoupled from application migrations. They are always checked and applied when a DbContext is configured, regardless of the application migration version comparison.


[3.0.0] - 2026-01-23

Breaking Changes

Package split: AreaProg.Migrations + AreaProg.AspNetCore.Migrations

The library has been split into two packages for cleaner separation of concerns:

Package Purpose Dependencies
AreaProg.Migrations Core library EF Core, Microsoft.Extensions.Hosting.Abstractions
AreaProg.AspNetCore.Migrations ASP.NET Core extensions AreaProg.Migrations, Microsoft.AspNetCore.Http.Abstractions

For console apps, worker services, or any non-ASP.NET Core application:

dotnet add package AreaProg.Migrations

For ASP.NET Core applications:

dotnet add package AreaProg.AspNetCore.Migrations

The ASP.NET Core package automatically includes the core package as a dependency.

Namespace changes

All core types have moved from AreaProg.AspNetCore.Migrations.* to AreaProg.Migrations.*:

Type Old Namespace (v2.x) New Namespace (v3.x)
BaseMigration AreaProg.AspNetCore.Migrations.Abstractions AreaProg.Migrations.Abstractions
BaseMigrationEngine AreaProg.AspNetCore.Migrations.Abstractions AreaProg.Migrations.Abstractions
EfCoreMigrationEngine AreaProg.AspNetCore.Migrations.Abstractions AreaProg.Migrations.Abstractions
SqlServerMigrationEngine AreaProg.AspNetCore.Migrations.Abstractions AreaProg.Migrations.Abstractions
DefaultEfCoreMigrationEngine AreaProg.AspNetCore.Migrations.Engines AreaProg.Migrations.Engines
DefaultSqlServerMigrationEngine AreaProg.AspNetCore.Migrations.Engines AreaProg.Migrations.Engines
AppliedMigration AreaProg.AspNetCore.Migrations.Models AreaProg.Migrations.Models
UseMigrationsOptions AreaProg.AspNetCore.Migrations.Models AreaProg.Migrations.Models
ApplicationMigrationsOptions AreaProg.AspNetCore.Migrations.Extensions AreaProg.Migrations.Extensions
AddApplicationMigrations() AreaProg.AspNetCore.Migrations.Extensions AreaProg.Migrations.Extensions
IApplicationMigrationEngine AreaProg.AspNetCore.Migrations.Interfaces AreaProg.Migrations.Interfaces

The ASP.NET Core extension stays in its original namespace:

  • UseMigrations() / UseMigrationsAsync()AreaProg.AspNetCore.Migrations.Extensions

Added

IHost extension methods for console apps and worker services

New extension methods on IHost allow running migrations in non-ASP.NET Core applications:

var host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddDbContext<MyDbContext>(options => options.UseSqlite("..."));
        services.AddApplicationMigrations<MyMigrationEngine, MyDbContext>();
    })
    .Build();

await host.RunMigrationsAsync();
await host.RunAsync();

Available methods (in AreaProg.Migrations.Extensions):

  • host.RunMigrations() - Synchronous execution
  • host.RunMigrations(Action<UseMigrationsOptions>) - Synchronous with options
  • host.RunMigrationsAsync() - Asynchronous execution
  • host.RunMigrationsAsync(Action<UseMigrationsOptions>) - Asynchronous with options

EnforceLatestMigration option

Control whether the current version migration is re-executed on startup:

await app.UseMigrationsAsync(opts =>
{
    opts.EnforceLatestMigration = env.IsDevelopment(); // Enable re-execution in development
});
EnforceLatestMigration Behavior
true Re-executes current version (development-friendly)
false (default) Skips current version (production-recommended)

Migration Guide from v2.x to v3.x

  1. Update package reference:

    <!-- For ASP.NET Core apps (most common) -->
    <PackageReference Include="AreaProg.AspNetCore.Migrations" Version="3.0.0" />
    
    <!-- For console apps / worker services -->
    <PackageReference Include="AreaProg.Migrations" Version="3.0.0" />
  2. Update using statements:

    // Before (v2.x)
    using AreaProg.AspNetCore.Migrations.Abstractions;
    using AreaProg.AspNetCore.Migrations.Extensions;
    using AreaProg.AspNetCore.Migrations.Models;
    
    // After (v3.x)
    using AreaProg.Migrations.Abstractions;
    using AreaProg.Migrations.Extensions;
    using AreaProg.Migrations.Models;
    using AreaProg.AspNetCore.Migrations.Extensions; // Only for UseMigrations()
  3. For ASP.NET Core apps, you need both namespaces:

    • AreaProg.Migrations.Extensions for AddApplicationMigrations()
    • AreaProg.AspNetCore.Migrations.Extensions for UseMigrations()
  4. For console apps, you only need:

    • AreaProg.Migrations.Extensions for both AddApplicationMigrations() and RunMigrations()

[2.1.0] - 2026-01-17

Added

RunEFCoreMigrationAsync virtual method on BaseMigrationEngine

A new virtual method that allows customizing how Entity Framework Core migrations are executed. Override this method in your migration engine to implement custom behavior such as:

  • Custom command timeout values
  • Per-migration logging
  • Custom execution strategies
  • Progress reporting

Default behavior:

public virtual async Task RunEFCoreMigrationAsync(DbContext? dbContext)
{
    if (dbContext is null) return;

    var pendingMigrations = await dbContext.Database.GetPendingMigrationsAsync();

    if (pendingMigrations.Any())
    {
        var strategy = dbContext.Database.CreateExecutionStrategy();

        await strategy.ExecuteAsync(async () =>
        {
            dbContext.Database.SetCommandTimeout(TimeSpan.FromMinutes(15));

            await dbContext.Database.MigrateAsync();
        });
    }
}

Custom implementation example:

public class MyMigrationEngine : EfCoreMigrationEngine
{
    private readonly ILogger<MyMigrationEngine> _logger;

    public MyMigrationEngine(
        ApplicationMigrationsOptions options,
        IServiceProvider serviceProvider,
        ILogger<MyMigrationEngine> logger)
        : base(serviceProvider, options.DbContext)
    {
        _logger = logger;
    }

    public override async Task RunEFCoreMigrationAsync(DbContext? dbContext)
    {
        if (dbContext is null) return;

        var pendingMigrations = await dbContext.Database.GetPendingMigrationsAsync();

        foreach (var migration in pendingMigrations)
        {
            _logger.LogInformation("Applying EF Core migration: {Migration}", migration);
        }

        // Custom timeout
        dbContext.Database.SetCommandTimeout(TimeSpan.FromMinutes(5));

        await dbContext.Database.MigrateAsync();
    }
}

[2.0.0] - 2026-01-16

Breaking Changes

Namespace reorganization

Classes have been moved to more appropriate namespaces:

Class Old Namespace New Namespace
BaseMigration AreaProg.AspNetCore.Migrations.Models AreaProg.AspNetCore.Migrations.Abstractions
BaseMigrationEngine AreaProg.AspNetCore.Migrations.Models AreaProg.AspNetCore.Migrations.Abstractions
EfCoreMigrationEngine AreaProg.AspNetCore.Migrations.Models AreaProg.AspNetCore.Migrations.Abstractions
SqlServerMigrationEngine AreaProg.AspNetCore.Migrations.Models AreaProg.AspNetCore.Migrations.Abstractions
DefaultEfCoreMigrationEngine AreaProg.AspNetCore.Migrations.Models AreaProg.AspNetCore.Migrations.Engines
DefaultSqlServerMigrationEngine AreaProg.AspNetCore.Migrations.Models AreaProg.AspNetCore.Migrations.Engines

Migration: Update your using statements:

// Before (v1.x)
using AreaProg.AspNetCore.Migrations.Models;

// After (v2.x)
using AreaProg.AspNetCore.Migrations.Abstractions; // For BaseMigration, BaseMigrationEngine, EfCoreMigrationEngine, SqlServerMigrationEngine
using AreaProg.AspNetCore.Migrations.Engines;      // For DefaultEfCoreMigrationEngine, DefaultSqlServerMigrationEngine

ShouldRun property replaced by ShouldRunAsync() method

The synchronous ShouldRun property on BaseMigrationEngine has been replaced with an asynchronous ShouldRunAsync() method to support distributed locking scenarios.

Before (v1.x):

public class MyMigrationEngine : BaseMigrationEngine
{
    public override bool ShouldRun => true;
}

After (v2.x):

public class MyMigrationEngine : BaseMigrationEngine
{
    public override Task<bool> ShouldRunAsync() => Task.FromResult(true);
}

RunBeforeDatabaseMigrationAsync no longer receives a cache parameter

The cache parameter has been removed from RunBeforeDatabaseMigrationAsync. Data capture before schema changes should now be done in individual migrations using PrepareMigrationAsync.

Before (v1.x):

public override Task RunBeforeDatabaseMigrationAsync(IDictionary<string, object> cache)
{
    // Capture data here
    cache["key"] = value;
    return Task.CompletedTask;
}

After (v2.x):

// In your migration engine - for global setup/logging only
public override Task RunBeforeDatabaseMigrationAsync()
{
    // Global setup, logging, validation
    return Task.CompletedTask;
}

// In your migration - for data capture
public override async Task PrepareMigrationAsync(IDictionary<string, object> cache)
{
    // Capture data specific to this migration
    cache["key"] = await CaptureDataAsync();
}

Each migration now has its own isolated cache

Previously, all migrations shared a single cache dictionary. Now each migration has its own isolated cache instance, preventing key collisions between migrations.

AddApplicationMigrations with setup action is no longer public

The AddApplicationMigrations<TEngine>(Action<ApplicationMigrationsOptions>) overload has been made internal. Use the generic overloads instead:

Before (v1.x):

services.AddApplicationMigrations<MyEngine>(options =>
{
    options.DbContext = typeof(MyDbContext);
});

After (v2.x):

// With DbContext integration
services.AddApplicationMigrations<MyEngine, MyDbContext>();

// Without DbContext (no EF Core integration)
services.AddApplicationMigrations<MyEngine>();

Added

AppliedMigration entity

A built-in entity for tracking applied migrations. Add it to your DbContext to use the new EfCoreMigrationEngine:

public class AppDbContext : DbContext
{
    public DbSet<AppliedMigration> AppliedMigrations { get; set; }
}

EfCoreMigrationEngine abstract class

A ready-to-use migration engine that stores version history using Entity Framework Core. Eliminates boilerplate code for GetAppliedVersionsAsync() and RegisterVersionAsync().

Usage:

public class AppMigrationEngine : EfCoreMigrationEngine
{
    public AppMigrationEngine(
        ApplicationMigrationsOptions<AppMigrationEngine> options,
        IServiceProvider serviceProvider)
        : base(serviceProvider, options.DbContext) { }

    // Optionally override lifecycle hooks
}

Features:

  • Automatic version tracking via AppliedMigration entity
  • Graceful handling when database/table doesn't exist yet (uses CanConnect + try/catch)
  • Deduplication of version registration

SqlServerMigrationEngine abstract class

Extends EfCoreMigrationEngine with SQL Server distributed locking using sp_getapplock. Ensures only one application instance executes migrations at a time in multi-instance deployments.

Usage:

public class AppMigrationEngine : SqlServerMigrationEngine
{
    public AppMigrationEngine(
        ApplicationMigrationsOptions<AppMigrationEngine> options,
        IServiceProvider serviceProvider)
        : base(serviceProvider, options.DbContext) { }
}

How it works:

  • Acquires an exclusive application lock named AppMigrations before running migrations
  • Lock is released automatically when the connection/transaction ends
  • Other instances wait or skip based on lock timeout configuration

Configurable properties:

Property Default Description
LockResourceName "AppMigrations" Name of the SQL Server application lock resource. Override to use different lock scopes for different engines.
LockTimeoutMs 0 (no wait) Lock acquisition timeout in milliseconds. 0 = skip if locked, positive value = wait, -1 = infinite wait (not recommended).
public class AppMigrationEngine : SqlServerMigrationEngine
{
    public AppMigrationEngine(
        ApplicationMigrationsOptions<AppMigrationEngine> options,
        IServiceProvider serviceProvider)
        : base(serviceProvider, options.DbContext) { }

    protected override string LockResourceName => "MyApp_Migrations";
    protected override int LockTimeoutMs => 5000; // Wait up to 5 seconds
}

DefaultEfCoreMigrationEngine and DefaultSqlServerMigrationEngine classes

Ready-to-use concrete implementations for projects that don't need custom lifecycle hooks:

// For any database supported by EF Core
builder.Services.AddApplicationMigrations<DefaultEfCoreMigrationEngine, MyDbContext>();

// For SQL Server with distributed locking
builder.Services.AddApplicationMigrations<DefaultSqlServerMigrationEngine, MyDbContext>();

No custom engine class required - just register and go.

New AddApplicationMigrations<TEngine, TDbContext>() overload

A simpler registration API that takes the DbContext as a generic parameter:

// Before
builder.Services.AddApplicationMigrations<MyEngine>(options =>
{
    options.DbContext = typeof(MyDbContext);
});

// After
builder.Services.AddApplicationMigrations<MyEngine, MyDbContext>();

MigrationsAssembly property on ApplicationMigrationsOptions

A new property to explicitly specify which assembly to scan for migration classes. This is particularly useful when using built-in engines (DefaultEfCoreMigrationEngine, DefaultSqlServerMigrationEngine) since these are in the NuGet package assembly, not your application.

Default behavior:

  • AddApplicationMigrations<TEngine, TDbContext>() → scans the TDbContext assembly (recommended for built-in engines)
  • AddApplicationMigrations<TEngine>() → scans the TEngine assembly

Important: When using built-in engines like DefaultSqlServerMigrationEngine, always use the <TEngine, TDbContext> overload so migrations are discovered from your application's assembly (via the DbContext).

PrepareMigrationAsync method on BaseMigration

A new per-migration hook for capturing data before EF Core schema changes. Unlike the global RunBeforeDatabaseMigrationAsync on the engine, this method is specific to each migration version.

Usage:

public class Migration_1_2_0 : BaseMigration
{
    public override Version Version => new(1, 2, 0);

    public override async Task PrepareMigrationAsync(IDictionary<string, object> cache)
    {
        // Capture data BEFORE schema change (called before EF Core migrations)
        var oldStatuses = await _db.Database
            .SqlQueryRaw<OldStatus>("SELECT Id, Status FROM Orders")
            .ToListAsync();
        cache["OrderStatuses"] = oldStatuses;
    }

    public override async Task UpAsync()
    {
        // Transform data AFTER schema change
        if (Cache.TryGetValue("OrderStatuses", out var data))
        {
            var oldStatuses = (List<OldStatus>)data;
            // ... transform data
        }
    }
}

Execution order:

1. ShouldRunAsync()
2. RunBeforeAsync()                              ← Engine (global)
3. RunBeforeDatabaseMigrationAsync()             ← Engine (global)
4. For each pending migration:
   └─ PrepareMigrationAsync(cache)               ← Migration (per-version, isolated cache)
5. EF Core MigrateAsync()
6. RunAfterDatabaseMigrationAsync()              ← Engine (global)
7. For each pending migration:
   └─ UpAsync()                                  ← Migration (per-version)
8. RunAfterAsync()                               ← Engine (global)

Migration Guide from v1.x to v2.x

  1. Update AddApplicationMigrations registration:

    // Before
    services.AddApplicationMigrations<MyEngine>(options =>
    {
        options.DbContext = typeof(MyDbContext);
    });
    
    // After - with DbContext
    services.AddApplicationMigrations<MyEngine, MyDbContext>();
    
    // After - without DbContext
    services.AddApplicationMigrations<MyEngine>();
  2. Update ShouldRun to ShouldRunAsync():

    // Before
    public override bool ShouldRun => _config.GetValue<bool>("Migrations:Enabled");
    
    // After
    public override Task<bool> ShouldRunAsync()
        => Task.FromResult(_config.GetValue<bool>("Migrations:Enabled"));
  3. Update RunBeforeDatabaseMigrationAsync signature:

    If you were capturing data in RunBeforeDatabaseMigrationAsync, move that logic to PrepareMigrationAsync in individual migrations:

    // Before (v1.x) - in your engine
    public override async Task RunBeforeDatabaseMigrationAsync(IDictionary<string, object> cache)
    {
        cache["data"] = await CaptureDataAsync();
    }
    
    // After (v2.x) - in your engine (for global setup only)
    public override Task RunBeforeDatabaseMigrationAsync()
    {
        _logger.LogInformation("Starting schema migrations...");
        return Task.CompletedTask;
    }
    
    // After (v2.x) - in your migration (for data capture)
    public override async Task PrepareMigrationAsync(IDictionary<string, object> cache)
    {
        cache["data"] = await CaptureDataAsync();
    }
  4. (Optional) Simplify your engine using EfCoreMigrationEngine:

    If you were manually implementing GetAppliedVersionsAsync() and RegisterVersionAsync() with a database table, you can now inherit from EfCoreMigrationEngine instead:

    // Before (v1.x) - ~80 lines of boilerplate
    public class AppMigrationEngine : BaseMigrationEngine
    {
        private readonly AppDbContext _dbContext;
    
        public override async Task<Version[]> GetAppliedVersionsAsync()
        {
            // Manual table existence check
            // Manual query
            // Manual version parsing
        }
    
        public override async Task RegisterVersionAsync(Version version)
        {
            // Manual deduplication
            // Manual insert
        }
    }
    
    // After (v2.x) - ~5 lines
    public class AppMigrationEngine : EfCoreMigrationEngine
    {
        public AppMigrationEngine(
            ApplicationMigrationsOptions<AppMigrationEngine> options,
            IServiceProvider serviceProvider)
            : base(serviceProvider, options.DbContext) { }
    }
  5. (Optional) Add distributed locking for SQL Server:

    Replace EfCoreMigrationEngine with SqlServerMigrationEngine:

    public class AppMigrationEngine : SqlServerMigrationEngine
    {
        public AppMigrationEngine(
            ApplicationMigrationsOptions<AppMigrationEngine> options,
            IServiceProvider serviceProvider)
            : base(serviceProvider, options.DbContext) { }
    }
  6. Add AppliedMigration to your DbContext:

    If using EfCoreMigrationEngine or SqlServerMigrationEngine:

    using AreaProg.AspNetCore.Migrations.Models;
    
    public class AppDbContext : DbContext
    {
        public DbSet<AppliedMigration> AppliedMigrations { get; set; }
    }

    Then create an EF Core migration:

    dotnet ef migrations add AddAppliedMigrations

[1.1.0] - Previous Release

Changed

  • Renamed GetAppliedVersionAsync to GetAppliedVersionsAsync for clarity

[1.0.0] - Initial Release

Added

  • BaseMigration abstract class for defining application migrations
  • BaseMigrationEngine abstract class for implementing version tracking
  • ApplicationMigrationEngine<T> for automatic migration discovery and execution
  • Lifecycle hooks: RunBeforeAsync, RunAfterAsync, RunBeforeDatabaseMigrationAsync, RunAfterDatabaseMigrationAsync
  • FirstTime property to distinguish initial execution from re-runs
  • Cache dictionary for passing data between hooks and migrations
  • EF Core transaction support when DbContext is configured
  • UseMigrations() and UseMigrationsAsync() extension methods for IApplicationBuilder