DEV Community

Mikhail Nefedov
Mikhail Nefedov

Posted on AI-assisted

Simplifying life with Configuration Files in EF Core 11

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
// ...
Enter fullscreen mode Exit fullscreen mode

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>();
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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.

Top comments (1)

Collapse
 
mehdimohseni82 profile image
Mehdi Mohseni •

It pairs well with local tools, since .config is also where dotnet new tool-manifest puts dotnet-tools.json. Install dotnet-ef as a local tool and the tool version and its defaults sit in the same folder, so a fresh clone only needs dotnet tool restore. It's worth checking what happens when the manifest is in a different .config higher up, though.