If you've ever worked in a solution where your DbContext doesn't live in your startup project, every single EF Core command ends up looking like this:

dotnet ef migrations add InitialCreate --project Playlist.DataAccess --startup-project PlaylistApi dotnet ef database update --project Playlist.DataAccess --startup-project PlaylistApi dotnet ef migrations list --project Playlist.DataAccess --startup-project PlaylistApi 

The next logical step is to encode them in a shell script that wraps the dotnet ef command, or in your CI scripts. EF CORE 11 finally fixes this. The dotnet ef command-line tool now loads default option values from a .config/dotnet-ef.json configuration file. You write your project layout down once, and every dotnet ef invocation picks it up automatically.

The scenario: a DbContext shared across APIs

A common architecture is to keep the EF Core model in its own class library so that multiple APIs can share the same DbContext. In this sample solution, we have two independent areas, each in its own subfolder:

  • Playlists/PlaylistApi an ASP.NET Core Web API. This is the startup project.
  • Playlists/Playlist.DataAccess a plain SDK class library that contains PlaylistDbContext and the entity models. This is the project that contains the DbContext.
  • RecipeApi a second, unrelated API with its own RecipeDbContext.

The solution https://github.com/mikhailnefedov/EfCoreConfigFiles looks like this:

EfCoreConfigFiles/ ├─ Playlists/ │ ├─ .config/ │ │ └─ dotnet-ef.json <-- defaults for the Playlist area │ ├─ PlaylistApi/ <-- startup project (Web API) │ └─ Playlist.DataAccess/ <-- DbContext lives here ├─ RecipeApi/ │ ├─ .config/ │ │ └─ dotnet-ef.json <-- defaults for the Recipe area │ └─ RecipeApi.csproj └─ EfCoreConfigFiles.slnx 

PlaylistApi simply references Playlist.DataAccess and registers the context:

// Playlists/PlaylistApi/Program.cs using Microsoft.EntityFrameworkCore; using Playlist.DataAccess.Data; var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext<PlaylistDbContext>(options => options.UseSqlite(builder.Configuration.GetConnectionString("Playlist"))); var app = builder.Build(); // ... 

And Playlist.DataAccess owns the context itself:

// Playlists/Playlist.DataAccess/Data/PlaylistDbContext.cs using Microsoft.EntityFrameworkCore; namespace Playlist.DataAccess.Data; public class PlaylistDbContext(DbContextOptions<PlaylistDbContext> options) : DbContext(options) { public DbSet<Models.Playlist> Playlists => Set<Models.Playlist>(); public DbSet<Track> Tracks => Set<Track>(); } 

Because the context is not in the startup project, every tooling command needs to know both where the context lives (--project) and how to build the app (--startup-project).

Scoped .config/dotnet-ef.json

Create a .config directory and drop a dotnet-ef.json inside it. Here it sits next to the Playlist projects in Playlists/:

{ "project": "Playlist.DataAccess", "startupProject": "PlaylistApi", "context": "Playlist.DataAccess.Data.PlaylistDbContext" } 

The context value is optional, but it is especially useful when a project contains multiple DbContext types: it tells the tool which one to use without passing --context on every command.

That's it. Now the commands shrink down to:

dotnet ef migrations add InitialCreate dotnet ef database update 

Other properties, such as framework and verbose, can also be configured. You can find more information in the EF Core 11 What's New documentation.

How discovery works

A few details worth knowing:

  • Walking up the tree. When you run dotnet ef, the tool searches for a .config/dotnet-ef.json by walking up the directory tree from the current working directory. The first file it finds is used. Run the command from Playlists/PlaylistApi, for example, and it walks up to Playlists/ and finds the config there.
  • Paths are relative to the .config folder's parent. The project and startupProject values are resolved relative to the directory containing .config, not relative to your current working directory. In our Playlist case, that is the Playlists/ folder, which is why "Playlist.DataAccess" and "PlaylistApi" resolve correctly even when you invoke the tool from Playlists/PlaylistApi.
  • Command-line options always win. Anything you pass explicitly on the command line overrides the config file. This means you can set sane defaults in the file and override them for one-off commands:
 # Uses Playlist.DataAccess from the config, but points the tool at a different output folder dotnet ef migrations add AddRating --output-dir Migrations/Rating 
  • Scoped per directory. Because discovery is folder-based, each area of the solution can carry its own defaults. Playlists/.config/dotnet-ef.json and RecipeApi/.config/dotnet-ef.json are completely independent. Run a command from inside either area and the nearest config wins.

Summary

The new .config/dotnet-ef.json support is a small quality-of-life feature with a big payoff for any solution that separates its data layer from its startup project. Write your --project and --startup-project values once, let the walking-up discovery find them from anywhere in the tree, and override on the command line only when you actually need to.