Add a migration
Change a Module's schema with dotnet-ef, from its own migrations folder, and apply it before the Host starts.
What you build
An EF Core migration in the Module's own migrations folder, for the Module's own schema. Each Module keeps its migrations apart, with its own history table in its schema. The DbContext stays internal and needs no design-time factory: dotnet-ef builds the Host as its Program does and finds the DbContext in its container.
The Host applies no migration when it starts
Apply migrations yourself, before the Host runs, as part of your deployment. In tests, HostFixture applies every Module's migrations before it starts the Host.
When to use it
Whenever a Module's model changes: a new aggregate or property, opting an aggregate into audit, soft delete or a tenant, or a new unique index. Also once for each table the Kit keeps outside the model, such as the Receipt table or the idempotency key table.
Steps
Restore the tools
The solution's tool manifest holds dotnet-ef. From the solution's folder:
dotnet tool restoreAdd the migration
From the folder that holds src/, name the Module's project, the Host as the startup project, the DbContext and the output folder. These are the Proving Sample's paths:
dotnet ef migrations add Initial \
--project src/Modules/Catalog/Catalog \
--startup-project src/ModularMonolith.Host \
--context CatalogDbContext \
--output-dir Infrastructure/Data/MigrationsFill a migration for a table outside the model
The Receipt table, the Snapshot version table and the idempotency key table are not in the Module's model, so their migration is empty and the model snapshot stays as it was. Fill its Up and Down with the Kit's call and the Module's schema:
// The Receipts of Catalog's Notification Handlers. The table is not in the model, so it leaves the snapshot unchanged.
[DbContext(typeof(CatalogDbContext))]
[Migration("20261005180000_Receipts")]
internal sealed class Receipts : Migration
{
protected override void Up(MigrationBuilder migrationBuilder) => migrationBuilder.CreateReceiptTable("catalog");
protected override void Down(MigrationBuilder migrationBuilder) => migrationBuilder.DropReceiptTable("catalog");
}Apply it
In a solution generated from a template, apply each Module's migrations to the database the Host's connection string names. A missing database is created before the first migration:
dotnet ef database update --project src/Modules/Catalog/Catalog --startup-project src/Acme.Shop.Host --context CatalogDbContextThe modular monolith also migrates Billing and Inventory the same way, and Ledger, a Stream Module, needs none.
What the Kit guarantees
| Guarantee | Stated in |
|---|---|
| How a Module's migrations are added | Migrations |
| Which Module needs the Receipt and Snapshot version tables | Receipts |
| The idempotency key table | Migrations |
| Running and migrating a generated Host | Running the generated Host |
Failure messages
Next steps
Make a Module tenant-scoped
Require a tenant for every command and query of a Module, and read and save its aggregates only within the tenant of the Operation.
Test a Feature
Run a Feature's Operations against the hosted application on a real PostgreSQL, through its Module's Gateway or over HTTP, and assert its Result, its rows and its deliveries.