Add EF Core 8 Migrations to a .NET 8 Class Library
Step-by-step guide to adding EF Core 8 migrations in a .NET 8 class library, covering DbContext setup, CLI commands, runtime application, and idempotent script generation for CI/CD.
08 Jul 2025, 01:06 UTC

Desired outcome
Create and maintain Entity Framework Core 8 migrations inside a net8.0 class library, then apply them safely at runtime or via idempotent SQL scripts in a deployment pipeline.
Prerequisites
- .NET 8 SDK installed (
dotnet --versionshows8.0.x). - A class library project targeting
net8.0(e.g.,MyApp.Data.csproj). - An executable startup project (Web API, console app, or worker) that references the class library and configures the
DbContext(e.g.,MyApp.Api.csproj). - NuGet packages in the class library:
Microsoft.EntityFrameworkCoreMicrosoft.EntityFrameworkCore.Design- Provider package:
Microsoft.EntityFrameworkCore.SqlServerorNpgsql.EntityFrameworkCore.PostgreSQL
Set up the DbContext in the class library
Define a DbContext-derived class with DbSet<T> properties for each entity. Keep the class library free of connection strings; accept DbContextOptions via constructor injection.
// MyApp.Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
namespace MyApp.Data;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Customer> Customers => Set<Customer>();
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Fluent API configuration, e.g.,
modelBuilder.Entity<Customer>().HasIndex(c => c.Email).IsUnique();
}
}
In the startup project's Program.cs, register the context with the provider and connection string:
// MyApp.Api/Program.cs
builder.Services.AddDbContext<AppDbContext>(options =
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
Note: Do not override OnConfiguring in the class library unless you need a design-time fallback; the host application supplies the real configuration.
Add the first migration
Run the EF Core CLI from the solution root (the folder containing the .sln file). The --project flag points to the class library; --startup-project points to the executable project that provides the configured DbContext.
dotnet ef migrations add InitialCreate \
--project MyApp.Data/MyApp.Data.csproj \
--startup-project MyApp.Api/MyApp.Api.csproj
This creates a Migrations folder in the class library with two files:
20240101000000_InitialCreate.cs– containsUp/Downmethods.AppDbContextModelSnapshot.cs– represents the current model state; must be committed to source control.
Apply migrations locally
Sync your development database:
dotnet ef database update \
--project MyApp.Data/MyApp.Data.csproj \
--startup-project MyApp.Api/MyApp.Api.csproj
To target a specific migration (roll forward or back):
dotnet ef database update --migration AddCustomerIndex \
--project MyApp.Data/MyApp.Data.csproj \
--startup-project MyApp.Api/MyApp.Api.csproj
Verification: Inspect the database schema (tables, indexes, foreign keys) to confirm they match the entity model.
Apply migrations at runtime
Call context.Database.Migrate() during application startup. This requires the database user to have DDL permissions (CREATE TABLE, ALTER TABLE, etc.).
// MyApp.Api/Program.cs
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.Migrate(); // blocks until complete
}
Risk: Running Migrate() concurrently on multiple instances can cause race conditions. Mitigations:
- Run migrations as a single deployment step (see next section).
- Use a distributed lock (e.g.,
sp_getapplockon SQL Server, advisory locks on PostgreSQL) if runtime migration is unavoidable.
Generate idempotent SQL scripts for CI/CD
Produce a single script that can be run repeatedly without error. Execute this script as a separate deployment step (e.g., in a pipeline task, Flyway, or DbUp) so the application never needs DDL permissions.
dotnet ef migrations script --idempotent \
--output migrate.sql \
--project MyApp.Data/MyApp.Data.csproj \
--startup-project MyApp.Api/MyApp.Api.csproj
The generated migrate.sql contains IF NOT EXISTS guards and a __EFMigrationsHistory table check. Review the script for unexpected DDL before committing.
Verification checklist
- List migrations:
dotnet ef migrations list --project MyApp.Data --startup-project MyApp.Api– confirms discovery and order. - Clean database test: Run
database updateagainst an empty database; verify tables, columns, indexes match the model. - Script review: Open
migrate.sqland look for spurious statements (e.g., unexpectedDROP COLUMN). - Incremental migration test: Add a property to an entity, run
dotnet ef migrations add AddNewColumn, and confirm the new migration contains only the expected column addition. - Staging deployment: Apply the idempotent script to a staging environment; validate schema consistency with a comparison tool (SQLPackage,
pg_dump --schema-onlydiff, etc.).
Common pitfalls and recovery
| Issue | Cause | Recovery |
|---|---|---|
| "No executable project found" | Startup project is a class library or missing DbContext registration. | Ensure --startup-project references an executable project that calls AddDbContext. |
| ModelSnapshot.cs deleted | Accidental removal from source control. | Recreate by deleting all migrations and running migrations add Baseline on a database that matches the current model, or restore from git history. |
| Concurrent migration failure | Multiple app instances call Migrate() simultaneously. | Switch to script-based deployment; add a distributed lock if runtime migration is required. |
| Provider-specific features missing | Annotations or fluent API not added in OnModelCreating. | Add provider-specific configuration (e.g., .ForSqlServerIsMemoryOptimized(), .HasColumnType("jsonb")). |
Limitations
- EF Core tooling only works for the target framework that includes the EF Core runtime. If the class library multi-targets (
net8.0;netstandard2.0), migrations only run fornet8.0. Prefer single TFMnet8.0for migration projects. - Runtime
Migrate()is not transactional across multiple contexts; each context migrates independently. - Idempotent scripts assume the
__EFMigrationsHistorytable exists; the first run creates it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.