This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
For complete documentation, see the README.md.
AreaProg.Migrations is a .NET class library split into two NuGet packages (AreaProg.Migrations for console/worker apps and AreaProg.AspNetCore.Migrations for ASP.NET Core) that provides application-level migration infrastructure. It complements Entity Framework Core database migrations by enabling versioned application migrations with lifecycle hooks.
# Build the solution
dotnet build
# Build for a specific framework
dotnet build -f net8.0
# Create NuGet package (auto-generated on build)
dotnet packThe project multi-targets: net6.0, net8.0, net9.0, net10.0.
The library uses an abstract base class pattern for extensibility:
-
BaseMigration (
Abstractions/BaseMigration.cs) - Abstract class that individual migrations inherit from. Requires implementingVersionproperty andUpAsync()method. -
BaseMigrationEngine (
Abstractions/BaseMigrationEngine.cs) - Abstract orchestration layer that tracks applied versions. Subclasses implement version storage (e.g., database, file system). -
ApplicationMigrationEngine (
Services/ApplicationMigrationEngine.cs) - Generic concrete implementation that:- Discovers
BaseMigrationimplementations via reflection from assembly containing typeT - Executes migrations sequentially based on version ordering
- Wraps migrations in EF Core transactions when DbContext is configured
- Provides lifecycle hooks:
RunBeforeAsync(),RunBeforeDatabaseMigrationAsync(),RunAfterDatabaseMigrationAsync(),RunAfterAsync()
- Discovers
services.AddApplicationMigrations<MyMigrationEngine>(options =>
{
options.DbContext = typeof(MyDbContext); // Optional: enables transactional migrations
});ASP.NET Core - Use extension methods on IApplicationBuilder:
var app = builder.Build();
app.UseMigrations(); // or await app.UseMigrationsAsync();
app.Run();Console Apps / Worker Services - Use extension methods on IHost:
var host = Host.CreateDefaultBuilder(args).Build();
host.RunMigrations(); // or await host.RunMigrationsAsync();
await host.RunAsync();IApplicationMigrationEngine exposes:
Run()- Executes pending migrations synchronouslyRunAsync()- Executes pending migrations asynchronouslyHasRun- Indicates if migrations have executed
- Migrations are discovered at runtime via reflection scanning the assembly containing the engine type
- Version comparison uses
System.Versionordering - migrations with higher versions run later - EF Core database migrations run automatically via
context.Database.Migrate()when DbContext is configured
The migration matching the current registered version is re-executed on each application startup. This is intentional to facilitate development:
- You can iterate on a migration without manually rolling back the database version
- No need to delete version records or reset state between debugging sessions
To handle re-execution, use one of these strategies:
-
Use
FirstTimeproperty - Guards operations that should only run once:public override async Task UpAsync() { if (FirstTime) { // Runs only on first installation (e.g., seed data, notifications) } // Idempotent operations run every time }
-
Design idempotent migrations - Methods that are safe to re-execute (upserts, "create if not exists", etc.)
The FirstTime flag is true when the migration version has never been registered, false on re-executions. During debugging, you can bypass the FirstTime check by moving the execution pointer.
When changing column types (e.g., enum to string), you may need to capture existing data before the schema change and transform it afterward. Use PrepareMigrationAsync in individual migrations:
public class Migration_1_2_0 : BaseMigration
{
private readonly MyDbContext _db;
public Migration_1_2_0(MyDbContext db) => _db = db;
public override Version Version => new(1, 2, 0);
// Called BEFORE EF Core migrations - capture data while old schema exists
public override async Task PrepareMigrationAsync(IDictionary<string, object> cache)
{
var oldValues = await _db.Database
.SqlQueryRaw<OldStatusRecord>("SELECT Id, Status FROM Orders")
.ToListAsync();
cache["OrderStatuses"] = oldValues;
}
// Called AFTER EF Core migrations - transform data with new schema
public override async Task UpAsync()
{
if (Cache.TryGetValue("OrderStatuses", out var data))
{
var oldStatuses = (List<OldStatusRecord>)data;
foreach (var record in oldStatuses)
{
var newStatus = record.Status switch
{
0 => "pending",
1 => "complete",
_ => "unknown"
};
// Transform the data after schema change
}
}
}
}Important:
PrepareMigrationAsyncis only called when there are pending EF Core migrations, avoiding performance impact on regular application startups.- Each migration has its own isolated cache - no key collisions between migrations.