EF Core 11 Part I: Migrations Get Friendlier for Teams

November 05, 2026EF Core 11 Part I: Migrations Get Friendlier for Teams
Barret Blake

Barret Blake, Architect

This is part 1 of a 2-part post covering all the new stuff in Entity Framework Core 11

EF Core 11 ships alongside .NET 11 in November, and a lot of the attention is going to the big modeling and querying improvements. Sadukie has been digging into those in her post on querying deeply nested game state, so go check that out if you’re working with complex types and JSON columns.

But there’s another set of changes in EF Core 11 that I think will have a bigger impact on most teams’ day-to-day work: migrations. None of them are flashy. Most of them fix little papercuts that we’ve all just learned to live with. Taken together, they make working with migrations on a team noticeably smoother.

This is the first of a two-part series on EF Core 11. In this post, we’ll look at:

  • Keeping a relationship in your model while leaving the foreign key constraint out of the database
  • How the model snapshot now catches diverged migrations at merge time
  • Creating and applying a migration in a single command
  • A configuration file for dotnet ef, so you can stop typing --project and --startup-project
  • Running commands against every DbContext at once
  • A handful of smaller command-line improvements

Relationships Without Foreign Key Constraints

Normally, when you configure a relationship in EF Core, migrations create a matching foreign key constraint in the database. That’s what you want most of the time. The database enforces referential integrity, and you can’t end up with an order line that points to an order that doesn’t exist.

But there are scenarios where that constraint gets in the way:

  • Legacy databases that never had constraints in the first place, where adding them now would fail on existing data or break other applications that write to the same tables.
  • Data synchronization and imports, where records arrive from another system in an order you don’t control. If order lines can show up before their orders, a foreign key constraint will reject them.

Before EF Core 11, your options here weren’t great. You could leave the relationship out of your model entirely and lose navigation properties, or you could hand-edit every generated migration to remove the constraint and hope nobody forgot the next time.

EF Core 11 adds the ExcludeForeignKeyFromMigrations() syntax to address this:

public class OrderLineConfiguration : IEntityTypeConfiguration<OrderLine>
{
    public void Configure(EntityTypeBuilder<OrderLine> builder)
    {
        builder.HasOne(line => line.Order)
            .WithMany(order => order.Lines)
            .HasForeignKey(line => line.OrderId)
            .ExcludeForeignKeyFromMigrations();
    }
}

The relationship is still a full citizen in your EF model. Navigation properties, Include, change tracking, and queries all work exactly the same. The only difference is that migrations won’t create the foreign key constraint in the database. EF still creates an index on the OrderId column, so your joins don’t suffer.

A word of caution: this is a deliberate trade-off. Once the constraint is gone, the database won’t stop orphaned rows from being written, and it won’t cascade deletes for you. If you go this route, you’re taking responsibility for integrity somewhere else, whether that’s in your domain logic, your import process, or a cleanup job. Use it where the constraint is genuinely the problem, not as a default.

Catching Diverged Migrations at Merge Time

Here’s a scenario most teams have probably run into. Two developers each branch off main. Each adds a migration on their branch. Both branches pass their builds and their tests. Both get merged.

Now main has two migrations that were each created against a model that didn’t include the other’s changes. Git didn’t complain, because the two migrations are separate files. Sometimes everything works. Sometimes the model snapshot ends up subtly wrong, and the next person to run dotnet ef migrations add gets a migration full of changes that have nothing to do with what they did. Figuring out what went wrong after the fact is no fun.

EF Core 11 makes this problem visible. The model snapshot now records the ID of the latest migration. If two branches both add a migration, they both change that same value in the snapshot, and source control flags it as a merge conflict. Instead of finding out weeks later, you find out at merge time.

When you hit that conflict, don’t just pick one side and move on. The fix is to rebuild your migration on top of the other one:

  1. Before merging, remove your own migration. If you’ve already applied it to your local database, roll back to the previous migration first, or use --force to revert and remove it in one step:

    dotnet ef migrations remove --force
    
  2. Merge the other branch’s changes into yours. With your migration gone, the snapshot comes over cleanly.

  3. Re-create your migration, so it’s generated against a model that includes both sets of changes:

    dotnet ef migrations add AddCustomerLoyaltyTier
    
  4. Review the new migration, then apply it and run your tests.

It’s a little extra work, but it’s work that happens at the right time, while the change is fresh in your mind, instead of as a mystery for someone else to untangle later.

This pairs nicely with the has-pending-model-changes check that Scott covered in Maintaining Clean Database Migration States. Between the two, you can catch both forgotten migrations and diverged ones before they ever reach your stable branch.

Is it an ideal approach? It’s probably not what you’re hoping for to resolve conflicting migrations. But it’s a step in the right direction.

Create and Apply in One Step

The usual migration workflow is two commands: add the migration, then update the database.

dotnet ef migrations add AddCustomerLoyaltyTier
dotnet ef database update

EF Core 11 adds an --add option to database update that does both:

dotnet ef database update AddCustomerLoyaltyTier --add

This scaffolds the migration, compiles it on the fly using Roslyn, and applies it to the database. The migration files are still written to disk, so they end up in source control just like always. Any option you’d pass to migrations add works here too:

dotnet ef database update AddCustomerLoyaltyTier --add --output-dir Data/Migrations --namespace MyApp.Infrastructure.Data.Migrations

In Visual Studio’s Package Manager Console, it’s the -Add parameter:

Update-Database -Migration AddCustomerLoyaltyTier -Add

The runtime compilation is the interesting part. It means this works in situations where rebuilding the project isn’t an option, such as a containerized app or an Aspire-orchestrated environment.

Treat this as a local development convenience. It’s great for the tight loop of “change the model, update my local database, keep going.” It is not a replacement for reviewing the generated migration before you commit it, and it’s definitely not something to point at a shared or production database.

A Configuration File for dotnet ef

If you work in a Clean Architecture solution, or any similar design with a separation of concerns, you know this pain. Your DbContext lives in the Infrastructure project, or some other project, but the configuration and connection strings live in the Web project. So every single dotnet ef command looks something like this:

dotnet ef migrations add AddCustomerLoyaltyTier --project src/MyApp.Infrastructure --startup-project src/MyApp.Web

Every time. And every new developer on the team has to learn the incantation.

EF Core 11 lets you put those defaults in a .config/dotnet-ef.json file:

{
  "project": "src/MyApp.Infrastructure",
  "startupProject": "src/MyApp.Web",
  "context": "AppDbContext"
}

When you run dotnet ef, it walks up the directory tree from wherever you are until it finds a .config/dotnet-ef.json file, and uses the first one it finds. Paths in the file are relative to the folder that contains .config, so if you put it at the root of your repo, it works from anywhere in the solution. And now that same command is just:

dotnet ef migrations add AddCustomerLoyaltyTier

Anything you pass on the command line still wins over the config file, so you can override a default when you need to. The file supports other options too, including framework, configuration, runtime, and verbose, but I’d keep the committed version limited to the settings that are true for everyone on the team.

If your repo already has a .config folder for dotnet-tools.json, this file sits right next to it. Commit it, and the whole team benefits.

Targeting Every DbContext at Once

Plenty of solutions have more than one DbContext in the same project. Maybe there’s an AppDbContext for the main domain and an AuditDbContext for audit logging that writes to a separate database. Until now, you had to run each command once per context.

In EF Core 11, several commands accept a wildcard for --context:

dotnet ef migrations list --context "*"
dotnet ef migrations script --context "*" --idempotent --output migrations.sql
dotnet ef database update --context "*"
dotnet ef database drop --context "*" --force

Here’s what each one does with the wildcard:

  • migrations list lists the migrations for every context.
  • migrations script generates a script for every context and concatenates them.
  • database update applies migrations for every context. This covers migration bundles as well.
  • database drop drops the database for every context.

Put the asterisk in quotes so your shell doesn’t try to expand it into a list of file names.

One thing to keep in mind: the wildcard covers the contexts in the project the command is pointed at. If you’ve built a modular monolith where each module has its own DbContext in its own project, you’ll still run commands per module project.

The Smaller Stuff

A few more improvements round out the release.

--connection for migrations remove and database drop. You can now pass a connection string directly, instead of relying on whatever your DbContext is configured to use:

dotnet ef database drop --connection "Server=localhost;Database=MyApp_Test;Trusted_Connection=True;TrustServerCertificate=True" --force

--offline for migrations remove. Normally, migrations remove connects to the database to check whether the migration has been applied. With --offline, it skips that check entirely. That’s handy when the database isn’t reachable, or when you know for certain the migration was never applied:

dotnet ef migrations remove --offline

You can’t combine --offline with --force, since --force needs the database connection to revert the migration.

-NoBuild in the Package Manager Console. Add-Migration and Update-Database now support -NoBuild. This one exists for a very specific frustration: a project that builds fine but produces warnings, such as NuGet vulnerability warnings, that the Package Manager Console treats as errors and refuses to continue past:

Add-Migration AddCustomerLoyaltyTier -NoBuild
Update-Database -NoBuild

Only use it when your build is already up to date. Running against a stale build can give you confusing results.

Wrapping Up

None of these features are major innovations in and of themselves. But migrations are something every team touches constantly. And every one of these changes removes a little bit of friction: the merge conflict that tells you something is wrong before it becomes a mystery, the config file that saves every new developer from memorizing a long command, and the foreign key option that ends the hand-editing of generated migrations.

If I had to pick one thing to do the day you upgrade, it’s adding a .config/dotnet-ef.json file to your repo. It takes two minutes, and everyone on your team will thank you for it.

In Part II, we’ll look at what EF Core 11 does at query time: the performance improvements you get just by upgrading, and a long list of new SQL Server features, including vector and hybrid search.

Resources