Skip to main content

Command Palette

Search for a command to run...

Entity Framework Core - A real world migration analogy

Published
5 min read

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 AppDbContext be 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 update command 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: References Infrastructure (to register services).

  • MyApp.Infrastructure: References Core. It does not reference Migrations.

  • MyApp.Migrations: References Infrastructure. It needs access to AppDbContext to 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":

  1. The Trigger: We run the command against the Migrations Project.

  2. The Config: It looks at the API Project to read the Connection String (from appsettings.json).

  3. The Logic: It loads the AppDbContext from Infrastructure.

Command Scenario (Local Dev):

dotnet ef database update 
    --project src/MyApp.Migrations 
    --startup-project src/MyApp.Api

Step-by-Step Execution:

  1. EF Core starts up MyApp.Api (it actually runs Program.cs temporarily to read configuration).

  2. It constructs AppDbContext.

  3. It checks the configuration and sees x.MigrationsAssembly("MyApp.Migrations").

  4. It loads the MyApp.Migrations assembly.

  5. It reads AppDbContextModelSnapshot.cs from that assembly to understand the "expected" state.

  6. It queries the actual database __EFMigrationsHistory table.

  7. 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):

  1. Build Stage: Copies all project folders (Api, Infra, Migrations, Core) into the container so dotnet restore and dotnet build can link them together.

  2. Publish Stage: Compiles the MyApp.Api project. Because MyApp.Api references Infra, and our startup config references Migrations, the compiler pulls the DLLs for all of them into the /publish folder.

  3. Final Stage: We copy only the contents of the /publish folder 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:

EF Core can compile our migrations (from our separate project) into a single, standalone Linux executable file.

  1. 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 efbundle
    

    This creates a binary file named efbundle.

  2. Include it in Docker: Copy this efbundle file into our Docker image.

  3. 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

  1. Development: We maintain the 4-project structure.

  2. Migration Creation: We run dotnet ef migrations add locally. The files land in MyApp.Migrations.

  3. 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.

  4. Deployment: We run the Migration Bundle (generated from the Migrations project) to update the DB, then start the API container.

More from this blog

E

EF Core

31 posts