Entity Framework Core - A real world migration analogy
Questions:
What is the best project structure for a Dockerized application targeting Linux with Entity Framework Core migrations in a separate project? Should the application code and
AppDbContextbe in one project, and should the migrations project be part of the same solution?Where does the EF Core model snapshot class go when migrations are in a separate project? From which project is the
database updatecommand run, and how does EF Core figure out the startup project, migrations assembly, and DbContext?When creating a Docker image, is the image built just for the application project or for the whole solution? If database changes are deployed using EF Core migrations instead of SQL scripts, how does this work when the application and migrations are in separate projects?
Real world analogy and deployment strategy
Moving to Docker and Linux with a split architecture is exactly what many enterprise applications do.
Here is the breakdown of how to structure this, where the files live, and how deployment works.
1. The Project Structure
Yes, all projects live inside a single Solution (.sln).
When building a Docker image, we generally need the whole solution available during the build phase so the compiler can resolve dependencies between projects.
Here is the recommended folder structure for our scenario:
/MySolution.sln
│
├── /src
│ ├── /MyApp.Api (Startup Project / Docker Entry Point)
│ │ ├── Dockerfile
│ │ ├── Program.cs
│ │ └── appsettings.json
│ │
│ ├── /MyApp.Core (Entities / Domain Models)
│ │ └── User.cs
│ │
│ ├── /MyApp.Infrastructure (DbContext / Repository Logic)
│ │ └── AppDbContext.cs
│ │
│ └── /MyApp.Migrations (The "Dump" Project)
│ ├── 20240101_Initial.cs
│ └── AppDbContextModelSnapshot.cs <-- Snapshot lives here!
MyApp.Api: ReferencesInfrastructure(to register services).MyApp.Infrastructure: ReferencesCore. It does not referenceMigrations.MyApp.Migrations: ReferencesInfrastructure. It needs access toAppDbContextto generate the migration code.
2. The Mechanics: Snapshot & Updates
Where is the Snapshot?
When we use .MigrationsAssembly("MyApp.Migrations"), EF Core puts the AppDbContextModelSnapshot.cs file inside the MyApp.Migrations project, alongside our migration files.
How is the update executed?
When we run a command (locally or in CI/CD), EF Core performs a "Triangular Handshake":
The Trigger: We run the command against the Migrations Project.
The Config: It looks at the API Project to read the Connection String (from
appsettings.json).The Logic: It loads the
AppDbContextfrom Infrastructure.
Command Scenario (Local Dev):
dotnet ef database update
--project src/MyApp.Migrations
--startup-project src/MyApp.Api
Step-by-Step Execution:
EF Core starts up
MyApp.Api(it actually runsProgram.cstemporarily to read configuration).It constructs
AppDbContext.It checks the configuration and sees
x.MigrationsAssembly("MyApp.Migrations").It loads the
MyApp.Migrationsassembly.It reads
AppDbContextModelSnapshot.csfrom that assembly to understand the "expected" state.It queries the actual database
__EFMigrationsHistorytable.It applies the difference.
3. Docker & Deployment Strategy
This is where the confusion usually lies.
Q: "Is it creating image only for the code base or the whole solution?"
A: The final image only contains the compiled binaries.
However, the Docker Build Context (the environment where the image is built) needs the whole solution initially.
How the Dockerfile works (Multi-Stage Build):
Build Stage: Copies all project folders (
Api,Infra,Migrations,Core) into the container sodotnet restoreanddotnet buildcan link them together.Publish Stage: Compiles the
MyApp.Apiproject. BecauseMyApp.ApireferencesInfra, and our startup config referencesMigrations, the compiler pulls the DLLs for all of them into the/publishfolder.Final Stage: We copy only the contents of the
/publishfolder into a lightweight Linux image.
Q: "How to deploy database changes using migrations?"
Since we split the projects, we cannot easily run dotnet ef inside the final production container (because the production container shouldn't have the heavy .NET SDK installed, only the Runtime).
We have two robust options for Linux/Docker:
Option A: The "Migration Bundle" (Recommended)
EF Core can compile our migrations (from our separate project) into a single, standalone Linux executable file.
Generate the Bundle (during CI/CD build):
dotnet ef migrations bundle --project src/MyApp.Migrations --startup-project src/MyApp.Api --self-contained -r linux-x64 --output efbundleThis creates a binary file named
efbundle.Include it in Docker: Copy this
efbundlefile into our Docker image.Run it on Container Start: In our Docker entrypoint (or an Init Container in Kubernetes), run:
./efbundle --connection "Server=MyDb;..."This applies pending migrations immediately before the app starts.
Option B: Database.Migrate() in C
This is simpler but less flexible. In our Program.cs (in MyApp.Api), we add code to run on startup.
// Program.cs
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// This works because the MyApp.Migrations.dll is present
// in the bin folder of the API after compilation.
db.Database.Migrate();
}
Pros: Very easy. No extra script needed.
Cons: If we launch 5 replicas of our API at once, they might all try to upgrade the database simultaneously (Race Condition).
Summary of the Workflow
Development: We maintain the 4-project structure.
Migration Creation: We run
dotnet ef migrations addlocally. The files land inMyApp.Migrations.Docker Build: We use a multi-stage Dockerfile that copies all folders, builds the API, and creates a lightweight image containing the API and the DLLs of the other projects.
Deployment: We run the Migration Bundle (generated from the
Migrationsproject) to update the DB, then start the API container.

