Wolfix.Server uses Entity Framework Core for database management, with each module maintaining its own migrations. This guide covers creating, applying, and managing migrations in a modular monolith.
Understanding Module-Specific Migrations
Each module has its own DbContext and migration history:
Admin.Infrastructure/Migrations/
Catalog.Infrastructure/Migrations/
Customer.Infrastructure/Migrations/
Identity.Infrastructure/Migrations/
Media.Infrastructure/Migrations/
Order.Infrastructure/Migrations/
Seller.Infrastructure/Migrations/
Multiple DbContext instances can share the same physical database while maintaining independent migration histories.
Creating Migrations
Create Migration for a Module
When you modify entities in a module, create a migration:
# From solution root
dotnet ef migrations add < MigrationNam e > \
--project < Modul e > .Infrastructure \
--startup-project Wolfix.API \
--context < Modul e > DbContext
Example: Add ProductRating to Catalog
Modify the Entity
Catalog.Domain/ProductAggregate/Product.cs
public sealed class Product : BaseEntity
{
// Existing properties...
// New property
public int ReviewCount { get ; private set ; }
}
Create Migration
dotnet ef migrations add AddReviewCountToProduct \
--project Catalog.Infrastructure \
--startup-project Wolfix.API \
--context CatalogDbContext
This generates: Catalog.Infrastructure/Migrations/
20260305120000_AddReviewCountToProduct.cs
20260305120000_AddReviewCountToProduct.Designer.cs
CatalogDbContextModelSnapshot.cs
Review Generated Migration
Catalog.Infrastructure/Migrations/20260305120000_AddReviewCountToProduct.cs
public partial class AddReviewCountToProduct : Migration
{
protected override void Up ( MigrationBuilder migrationBuilder )
{
migrationBuilder . AddColumn < int >(
name : "ReviewCount" ,
schema : "catalog" ,
table : "catalog_products" ,
type : "integer" ,
nullable : false ,
defaultValue : 0 );
}
protected override void Down ( MigrationBuilder migrationBuilder )
{
migrationBuilder . DropColumn (
name : "ReviewCount" ,
schema : "catalog" ,
table : "catalog_products" );
}
}
Apply Migration
dotnet ef database update \
--project Catalog.Infrastructure \
--startup-project Wolfix.API \
--context CatalogDbContext
Initial Migrations
Each module has an initial migration that creates its schema:
Admin.Infrastructure/Migrations/20250920141248_Initial.cs
public partial class Initial : Migration
{
protected override void Up ( MigrationBuilder migrationBuilder )
{
migrationBuilder . EnsureSchema (
name : "admin" );
migrationBuilder . CreateTable (
name : "Admins" ,
schema : "admin" ,
columns : table => new
{
Id = table . Column < Guid >( type : "uuid" , nullable : false ),
AccountId = table . Column < Guid >( type : "uuid" , nullable : false ),
FirstName = table . Column < string >( type : "text" , nullable : false ),
LastName = table . Column < string >( type : "text" , nullable : false ),
MiddleName = table . Column < string >( type : "text" , nullable : false ),
PhoneNumber = table . Column < string >( type : "text" , nullable : false )
},
constraints : table =>
{
table . PrimaryKey ( "PK_Admins" , x => x . Id );
});
}
protected override void Down ( MigrationBuilder migrationBuilder )
{
migrationBuilder . DropTable (
name : "Admins" ,
schema : "admin" );
}
}
Each module uses a schema prefix (e.g., admin, catalog) to avoid table name conflicts.
Applying Migrations
Development Environment
Apply all module migrations:
#!/bin/bash
# apply-migrations.sh
MODULES = ( "Admin" "Catalog" "Customer" "Identity" "Media" "Order" "Seller" )
for MODULE in "${ MODULES [ @ ]}" ; do
echo "Applying migrations for $MODULE ..."
dotnet ef database update \
--project " $MODULE .Infrastructure" \
--startup-project Wolfix.API \
--context "${ MODULE }DbContext"
done
echo "All migrations applied successfully!"
chmod +x apply-migrations.sh
./apply-migrations.sh
Production Environment
Never apply migrations directly in production during application startup. Use a separate deployment step.
Option 1: Manual Application
# Connect to production container
docker exec -it < api-containe r > /bin/bash
# Apply migrations
dotnet ef database update \
--project Admin.Infrastructure \
--startup-project Wolfix.API
Option 2: Migration Scripts
Generate SQL scripts for DBA review:
# Generate SQL script
dotnet ef migrations script \
--project Catalog.Infrastructure \
--startup-project Wolfix.API \
--output catalog-migration.sql \
--idempotent
The --idempotent flag makes the script safe to run multiple times.
Option 3: CI/CD Pipeline
# GitHub Actions example
- name : Apply Database Migrations
run : |
dotnet tool install --global dotnet-ef
MODULES=("Admin" "Catalog" "Customer" "Identity" "Media" "Order" "Seller")
for MODULE in "${MODULES[@]}"; do
dotnet ef database update \
--project "$MODULE.Infrastructure" \
--startup-project Wolfix.API \
--context "${MODULE}DbContext" \
--connection "${{ secrets.DB_CONNECTION_STRING }}"
done
Migration Patterns
Adding a Column
public partial class AddEmailVerifiedColumn : Migration
{
protected override void Up ( MigrationBuilder migrationBuilder )
{
migrationBuilder . AddColumn < bool >(
name : "EmailVerified" ,
schema : "identity" ,
table : "AspNetUsers" ,
type : "boolean" ,
nullable : false ,
defaultValue : false ); // Provide default for existing rows
}
protected override void Down ( MigrationBuilder migrationBuilder )
{
migrationBuilder . DropColumn (
name : "EmailVerified" ,
schema : "identity" ,
table : "AspNetUsers" );
}
}
Renaming a Column
public partial class RenameProductTitleColumn : Migration
{
protected override void Up ( MigrationBuilder migrationBuilder )
{
migrationBuilder . RenameColumn (
name : "Title" ,
schema : "catalog" ,
table : "catalog_products" ,
newName : "Name" );
}
protected override void Down ( MigrationBuilder migrationBuilder )
{
migrationBuilder . RenameColumn (
name : "Name" ,
schema : "catalog" ,
table : "catalog_products" ,
newName : "Title" );
}
}
Adding an Index
public partial class AddIndexOnProductTitle : Migration
{
protected override void Up ( MigrationBuilder migrationBuilder )
{
migrationBuilder . CreateIndex (
name : "IX_Products_Title" ,
schema : "catalog" ,
table : "catalog_products" ,
column : "Title" );
}
protected override void Down ( MigrationBuilder migrationBuilder )
{
migrationBuilder . DropIndex (
name : "IX_Products_Title" ,
schema : "catalog" ,
table : "catalog_products" );
}
}
Data Migration
For complex data transformations:
public partial class MigrateProductPrices : Migration
{
protected override void Up ( MigrationBuilder migrationBuilder )
{
// Add new column
migrationBuilder . AddColumn < decimal >(
name : "FinalPrice" ,
schema : "catalog" ,
table : "catalog_products" ,
type : "decimal(18,2)" ,
nullable : false ,
defaultValue : 0m );
// Migrate data using raw SQL
migrationBuilder . Sql ( @"
UPDATE catalog.catalog_products
SET FinalPrice = CASE
WHEN Discount IS NOT NULL THEN Price * (100 - DiscountPercent) / 100
ELSE Price
END
" );
}
protected override void Down ( MigrationBuilder migrationBuilder )
{
migrationBuilder . DropColumn (
name : "FinalPrice" ,
schema : "catalog" ,
table : "catalog_products" );
}
}
Managing Migrations
List Migrations
dotnet ef migrations list \
--project Catalog.Infrastructure \
--startup-project Wolfix.API
Output:
20250920141248_Initial (Applied)
20250921104723_AddReviewCount (Applied)
20260305120000_AddFinalPrice (Pending)
Remove Last Migration
# Only if NOT applied to database
dotnet ef migrations remove \
--project Catalog.Infrastructure \
--startup-project Wolfix.API
Cannot remove a migration that has been applied to the database. Create a new migration to revert changes instead.
Revert to Specific Migration
# Revert to specific migration
dotnet ef database update < MigrationNam e > \
--project Catalog.Infrastructure \
--startup-project Wolfix.API
# Example: Revert to Initial
dotnet ef database update Initial \
--project Catalog.Infrastructure \
--startup-project Wolfix.API
Drop All Migrations
# Drop database
dotnet ef database drop \
--project Catalog.Infrastructure \
--startup-project Wolfix.API \
--force
# Recreate with all migrations
dotnet ef database update \
--project Catalog.Infrastructure \
--startup-project Wolfix.API
Multi-Module Coordination
Cross-Module Dependencies
When one module references another’s data:
Order.Domain/OrderAggregate/Order.cs
public sealed class Order : BaseEntity
{
// Reference by ID, not entity
public Guid CustomerId { get ; private set ; }
public Guid ProductId { get ; private set ; }
// No navigation properties to other modules
}
Modules reference each other by ID only, never by navigation properties. This maintains module independence.
Coordinated Schema Changes
If you need to change a shared concept across modules:
Create Migrations in Each Module
dotnet ef migrations add ChangeCustomerIdFormat \
--project Customer.Infrastructure \
--startup-project Wolfix.API
dotnet ef migrations add ChangeCustomerIdFormat \
--project Order.Infrastructure \
--startup-project Wolfix.API
Apply in Correct Order
# Apply to source module first
dotnet ef database update \
--project Customer.Infrastructure \
--startup-project Wolfix.API
# Then apply to dependent modules
dotnet ef database update \
--project Order.Infrastructure \
--startup-project Wolfix.API
Test Thoroughly
Run integration tests to ensure modules still communicate correctly.
Best Practices
Always Review Generated Migrations
EF Core doesn’t always generate optimal SQL. Review and customize: protected override void Up ( MigrationBuilder migrationBuilder )
{
// EF Core generated this
migrationBuilder . DropColumn ( name : "OldColumn" );
migrationBuilder . AddColumn < string >( name : "NewColumn" );
// Better: Rename instead of drop/add to preserve data
migrationBuilder . RenameColumn (
name : "OldColumn" ,
newName : "NewColumn" );
}
Use Descriptive Migration Names
# ✅ Good
dotnet ef migrations add AddEmailVerificationToUsers
dotnet ef migrations add CreateProductReviewsTable
# ❌ Bad
dotnet ef migrations add Update
dotnet ef migrations add Changes
Always Provide Default Values
When adding non-nullable columns: migrationBuilder . AddColumn < bool >(
name : "IsActive" ,
type : "boolean" ,
nullable : false ,
defaultValue : true ); // Always provide default
Test Down Migrations
Ensure rollback works: # Apply migration
dotnet ef database update
# Test rollback
dotnet ef database update < PreviousMigratio n >
# Re-apply
dotnet ef database update
Keep Migrations Small
Create separate migrations for distinct changes: # Instead of one large migration
dotnet ef migrations add AddMultipleProductFields
# Create multiple focused migrations
dotnet ef migrations add AddProductRating
dotnet ef migrations add AddProductTags
dotnet ef migrations add AddProductInventory
Troubleshooting
Migration Already Applied
Problem: “The migration ‘X’ has already been applied to the database.”
Solution:
# Remove from migrations table
dotnet ef migrations remove \
--project Catalog.Infrastructure \
--startup-project Wolfix.API
# Or update to skip that migration
dotnet ef database update < NextMigratio n >
Pending Model Changes
Problem: “Your target project doesn’t match your migrations assembly.”
Solution:
# Create migration for pending changes
dotnet ef migrations add PendingChanges \
--project Catalog.Infrastructure \
--startup-project Wolfix.API
Connection String Not Found
Problem: “A connection string was not found in configuration.”
Solution:
# Specify connection string explicitly
dotnet ef database update \
--project Catalog.Infrastructure \
--startup-project Wolfix.API \
--connection "Host=localhost;Database=wolfix;Username=postgres;Password=pass"
Multiple DbContexts
Problem: “More than one DbContext was found.”
Solution:
# Always specify the context
dotnet ef migrations add MyMigration \
--project Catalog.Infrastructure \
--startup-project Wolfix.API \
--context CatalogDbContext
MongoDB (Support Module)
The Support module uses MongoDB, which doesn’t require migrations:
Support.Infrastructure.MongoDB/Extensions/MongoDbExtensions.cs
public static async Task AddSupportMongoDbIndexes ( this IServiceProvider services )
{
var database = services . GetRequiredService < IMongoDatabase >();
var collection = database . GetCollection < SupportRequest >( "SupportRequests" );
// Create indexes
await collection . Indexes . CreateOneAsync (
new CreateIndexModel < SupportRequest >(
Builders < SupportRequest >. IndexKeys . Ascending ( x => x . CustomerId )
)
);
await collection . Indexes . CreateOneAsync (
new CreateIndexModel < SupportRequest >(
Builders < SupportRequest >. IndexKeys . Ascending ( x => x . Status )
)
);
}
Indexes are created automatically on application startup.
Next Steps
Deployment Deploy with migrations in CI/CD
Aspire Orchestration Manage migrations with .NET Aspire