In a layered solution, the DbContext and entities live in a data project — but by default EF Core insists migrations live in the same assembly as the context, and the moment your startup project and data project differ, dotnet ef starts throwing its famously unhelpful errors. This post sets up migrations in a separate assembly the clean way on .NET 10 and EF Core: MigrationsAssembly, a design-time factory, and the exact commands — all verified against a real two-project solution.

The solution shape
DotnetUtilities.sln
├── src/Shop.Data class library: entities, ShopDbContext, Migrations/
└── src/Shop.Api ASP.NET Core API — references Shop.Data
Why bother: migrations are schema history, and schema belongs to the data layer. Worker services, admin CLIs, and the API can all reference Shop.Data and share one migration history; the API project stays free of EF design-time baggage.
The context and the one crucial line
Shop.Data holds the model:
public class ShopDbContext(DbContextOptions<ShopDbContext> options) : DbContext(options)
{
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder) =>
modelBuilder.Entity<Product>(b =>
{
b.Property(p => p.Name).IsRequired().HasMaxLength(120);
b.Property(p => p.Price).HasPrecision(10, 2);
});
}
The API registers it, telling EF where migrations live:
builder.Services.AddDbContext<ShopDbContext>(o =>
o.UseSqlite(builder.Configuration.GetConnectionString("Default") ?? "Data Source=shop.db",
sqlite => sqlite.MigrationsAssembly("Shop.Data")));
MigrationsAssembly("Shop.Data") is the line this whole article exists for. Without it, EF looks for migrations in the assembly that contains the context — which is already Shop.Data here, but the explicit declaration keeps runtime Migrate() and design-time tooling agreed, and it's mandatory the moment your context and migrations ever separate. (Provider packages differ: SQL Server takes the same lambda via UseSqlServer(..., sql => sql.MigrationsAssembly("Shop.Data")).)
The design-time factory
dotnet ef must construct your context without running your app. Give the data project a self-sufficient recipe:
public class ShopDbContextFactory : IDesignTimeDbContextFactory<ShopDbContext>
{
public ShopDbContext CreateDbContext(string[] args) =>
new(new DbContextOptionsBuilder<ShopDbContext>()
.UseSqlite("Data Source=shop.db",
sqlite => sqlite.MigrationsAssembly(typeof(ShopDbContext).Assembly.FullName))
.Options);
}
With the factory in place, migration commands run from the data project directly — no --startup-project juggling, no booting the API to add a column. The connection string here is design-time only (migrations are generated from the model, not the database), so a local placeholder is fine.
The commands, verified
cd src/Shop.Data
dotnet ef migrations add InitialCreate
Done. To undo this action, use 'ef migrations remove'
Migrations/
├── 20260726054828_InitialCreate.cs
├── 20260726054828_InitialCreate.Designer.cs
└── ShopDbContextModelSnapshot.cs
Migrations landed in Shop.Data/Migrations — the data project owns its history. (dotnet tool install -g dotnet-ef first if the CLI is missing, and the project needs Microsoft.EntityFrameworkCore.Design.)
Applying them, the API calls Database.Migrate() at startup for the demo:
using (var scope = app.Services.CreateScope())
scope.ServiceProvider.GetRequiredService<ShopDbContext>().Database.Migrate();
Verified: the API boots, creates shop.db from the migration, and GET /products returns [] — schema exists, table empty, exactly right. For production, prefer applying migrations from the pipeline (idempotent script via dotnet ef migrations script --idempotent) over migrate-on-start — the trade-offs are covered in the CI/CD article.
The errors this setup prevents
Recognize these? They're all the same missing piece:
- "Your target project doesn't match your migrations assembly" → the
MigrationsAssembly(...)lambda is missing or names the wrong project. - "Unable to create a 'DbContext' of type" / "Unable to resolve service for type DbContextOptions" → no design-time factory (or you're forcing
--startup-projectto boot an app whose DI can't be constructed at design time). - Migrations generated into the API project → commands were run from the wrong directory before the factory existed; delete them, regenerate from
Shop.Data.
Working habits
- One migration per change, named for intent —
AddProductPrecision, notUpdate3. The migration list becomes readable schema history. - The snapshot file is code — merge conflicts in
ShopDbContextModelSnapshot.csmean two branches added migrations; resolve by removing and regenerating the later one, never by hand-merging the snapshot. - Review generated migrations like any code — EF is right about column adds and wrong often enough about renames (it sees drop+add; you correct it to
RenameColumnto keep the data).
The complete two-project solution is in the companion repository, ready for dotnet ef migrations add experiments — and pairs with the DataAnnotations migrations tutorial that covers the migration workflow itself.
Comments (0)
No comments yet — be the first to share your thoughts.