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.
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.
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.MigrationsFor ASP.NET Core applications:
dotnet add package AreaProg.AspNetCore.MigrationsThe ASP.NET Core package automatically includes the core package as a dependency.
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
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 executionhost.RunMigrations(Action<UseMigrationsOptions>)- Synchronous with optionshost.RunMigrationsAsync()- Asynchronous executionhost.RunMigrationsAsync(Action<UseMigrationsOptions>)- Asynchronous with options
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) |
-
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" />
-
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()
-
For ASP.NET Core apps, you need both namespaces:
AreaProg.Migrations.ExtensionsforAddApplicationMigrations()AreaProg.AspNetCore.Migrations.ExtensionsforUseMigrations()
-
For console apps, you only need:
AreaProg.Migrations.Extensionsfor bothAddApplicationMigrations()andRunMigrations()
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();
}
}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, DefaultSqlServerMigrationEngineThe 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);
}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();
}Previously, all migrations shared a single cache dictionary. Now each migration has its own isolated cache instance, preventing key collisions between migrations.
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>();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; }
}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
AppliedMigrationentity - Graceful handling when database/table doesn't exist yet (uses
CanConnect+ try/catch) - Deduplication of version registration
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
AppMigrationsbefore 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
}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.
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>();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 theTDbContextassembly (recommended for built-in engines)AddApplicationMigrations<TEngine>()→ scans theTEngineassembly
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).
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)
-
Update
AddApplicationMigrationsregistration:// Before services.AddApplicationMigrations<MyEngine>(options => { options.DbContext = typeof(MyDbContext); }); // After - with DbContext services.AddApplicationMigrations<MyEngine, MyDbContext>(); // After - without DbContext services.AddApplicationMigrations<MyEngine>();
-
Update
ShouldRuntoShouldRunAsync():// Before public override bool ShouldRun => _config.GetValue<bool>("Migrations:Enabled"); // After public override Task<bool> ShouldRunAsync() => Task.FromResult(_config.GetValue<bool>("Migrations:Enabled"));
-
Update
RunBeforeDatabaseMigrationAsyncsignature:If you were capturing data in
RunBeforeDatabaseMigrationAsync, move that logic toPrepareMigrationAsyncin 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(); }
-
(Optional) Simplify your engine using
EfCoreMigrationEngine:If you were manually implementing
GetAppliedVersionsAsync()andRegisterVersionAsync()with a database table, you can now inherit fromEfCoreMigrationEngineinstead:// 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) { } }
-
(Optional) Add distributed locking for SQL Server:
Replace
EfCoreMigrationEnginewithSqlServerMigrationEngine:public class AppMigrationEngine : SqlServerMigrationEngine { public AppMigrationEngine( ApplicationMigrationsOptions<AppMigrationEngine> options, IServiceProvider serviceProvider) : base(serviceProvider, options.DbContext) { } }
-
Add
AppliedMigrationto your DbContext:If using
EfCoreMigrationEngineorSqlServerMigrationEngine: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
- Renamed
GetAppliedVersionAsynctoGetAppliedVersionsAsyncfor clarity
BaseMigrationabstract class for defining application migrationsBaseMigrationEngineabstract class for implementing version trackingApplicationMigrationEngine<T>for automatic migration discovery and execution- Lifecycle hooks:
RunBeforeAsync,RunAfterAsync,RunBeforeDatabaseMigrationAsync,RunAfterDatabaseMigrationAsync FirstTimeproperty to distinguish initial execution from re-runsCachedictionary for passing data between hooks and migrations- EF Core transaction support when
DbContextis configured UseMigrations()andUseMigrationsAsync()extension methods forIApplicationBuilder