Your team of 8 developers is working on different features. Developer A adds a new table, Developer B modifies an existing column, Developer C renames a property. All three create migrations on the same day.
When you try to merge everything together, EF Core explodes with migration conflicts, database schema mismatches, and mysterious errors about missing tables. Sound familiar?
Large teams need structured approaches to EF Core migrations, or you’ll spend more time fixing database issues than building features.
The Migration Conflict Problem
EF Core migrations are sequential by design. Each migration has a timestamp-based filename and builds on the previous one:
20250923_081234_AddCustomerTable.cs
20250923_091456_AddOrderTable.cs // Depends on Customer table
20250923_095612_AddCustomerEmail.cs // Also modifies Customer table
When multiple developers create migrations simultaneously, you get parallel branches that can’t merge cleanly:
Feature Branch A: AddCustomerTable -> AddCustomerEmail
Feature Branch B: AddCustomerTable -> AddOrderTable
Feature Branch C: AddCustomerTable -> ModifyCustomerName
Merging these creates a broken migration history where later migrations reference schema changes that don’t exist in the merged timeline.
Team Workflow Strategy
1. Feature Branch Guidelines
Each developer works in feature branches with clear migration rules:
# Developer starts new feature
git checkout -b feature/customer-management
dotnet ef migrations add AddCustomerTable
# Work on feature, test locally
git commit -am "Add customer management feature"
Key rules:
- One logical change per migration
- Descriptive migration names
- Test migrations locally before pushing
- Include both Up and Down methods
2. Migration Review Process
Before merging, migrations must be reviewed:
// Good migration - clear, focused, reversible
public partial class AddCustomerTable : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Customers",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(maxLength: 100, nullable: false),
Email = table.Column<string>(maxLength: 200, nullable: false),
CreatedAt = table.Column<DateTime>(nullable: false, defaultValueSql: "GETUTCDATE()")
},
constraints: table =>
{
table.PrimaryKey("PK_Customers", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Customers_Email",
table: "Customers",
column: "Email",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(name: "Customers");
}
}
Review checklist:
- Does the migration name describe the change?
- Are both Up and Down methods implemented?
- Will this work on a production-sized database?
- Are there any breaking changes?
- Does it handle existing data properly?
3. Merge Conflict Resolution
When migration conflicts occur during merge, follow this process:
# 1. Identify conflicting migrations
git status
# Shows multiple migrations with overlapping timestamps
# 2. Remove conflicting migrations (keep changes in model)
dotnet ef migrations remove
dotnet ef migrations remove
# 3. Create a new migration that combines all changes
dotnet ef migrations add CombinedFeatureChanges
# 4. Verify the new migration looks correct
# Check both Up and Down methods
# 5. Test locally
dotnet ef database drop
dotnet ef database update
# 6. Commit the resolved migration
git add .
git commit -m "Resolve migration conflicts - combined customer and order features"
Database Synchronization Strategies
Local Development Databases
Each developer maintains their own local database:
// appsettings.Development.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyApp_Dev_{Environment.UserName};Trusted_Connection=true;MultipleActiveResultSets=true"
}
}
This prevents developers from stepping on each other’s schema changes during development.
Shared Development Environment
For integration testing, maintain a shared dev database that’s reset regularly:
# Daily script to reset shared dev database
#!/bin/bash
dotnet ef database drop --context ProductionDbContext --force
dotnet ef database update --context ProductionDbContext
dotnet run --seed-data
Environment-Specific Migration Scripts
Generate SQL scripts for different environments:
# Generate script for production deployment
dotnet ef migrations script --output migrations-v2.3.sql --idempotent
# Generate script for specific migration range
dotnet ef migrations script AddCustomerTable AddOrderTable --output partial-update.sql
The --idempotent
flag ensures scripts can be run multiple times safely.
Production Migration Practices
Migration Bundles
For automated deployments, use migration bundles:
# Create a migration bundle
dotnet ef migrations bundle --configuration Release --output migrations.exe
# Deploy in production
./migrations.exe --connection "Server=prod;Database=MyApp;..."
Migration bundles are self-contained executables that include all pending migrations.
Backup Strategy
Always backup before production migrations:
-- Pre-migration backup
BACKUP DATABASE MyApp
TO DISK = 'C:\Backups\MyApp_PreMigration_20250923.bak'
WITH FORMAT, COMPRESSION;
-- Run migration
-- Verify results
-- Optional: Cleanup old backups after successful migration
Large Table Migrations
For tables with millions of rows, consider these strategies:
// Bad: Will lock table for hours
public partial class AddCustomerIndex : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_Customers_Email",
table: "Customers",
column: "Email");
}
}
// Better: Use SQL for online index creation
public partial class AddCustomerIndexOnline : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"
CREATE INDEX IX_Customers_Email
ON Customers (Email)
WITH (ONLINE = ON, MAXDOP = 4);
");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex("IX_Customers_Email", "Customers");
}
}
Rollback Planning
Every production migration needs a rollback plan:
// Migration with data-safe rollback
public partial class AddCustomerStatus : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// Add column with default value
migrationBuilder.AddColumn<string>(
name: "Status",
table: "Customers",
maxLength: 20,
nullable: false,
defaultValue: "Active");
// Populate existing data
migrationBuilder.Sql(@"
UPDATE Customers
SET Status = CASE
WHEN DeletedAt IS NOT NULL THEN 'Deleted'
ELSE 'Active'
END
");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// Safe to drop - data preserved elsewhere
migrationBuilder.DropColumn(name: "Status", table: "Customers");
}
}
Advanced Team Patterns
Migration Coordinator Role
Designate one team member as the migration coordinator:
- Reviews all database-related PRs
- Resolves migration conflicts
- Manages production deployments
- Maintains migration documentation
Feature Flags for Schema Changes
Decouple schema changes from feature releases:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
// New column deployed but not used yet
public string Status { get; set; }
}
public class CustomerService
{
public async Task<Customer> GetCustomerAsync(int id)
{
var customer = await context.Customers.FindAsync(id);
// Feature flag controls usage of new column
if (featureFlags.IsEnabled("CustomerStatus"))
{
// Use Status column
return customer;
}
else
{
// Ignore Status column for now
customer.Status = null;
return customer;
}
}
}
This allows schema deployment separate from feature activation.
Automated Migration Testing
Include migration testing in CI/CD:
# Azure DevOps pipeline
- task: DotNetCoreCLI@2
displayName: 'Test Migrations'
inputs:
command: 'custom'
custom: 'ef'
arguments: 'database update --connection "$(TestConnectionString)"'
- task: DotNetCoreCLI@2
displayName: 'Verify Schema'
inputs:
command: 'test'
projects: '**/DatabaseIntegrationTests.csproj'
Common Pitfalls and Solutions
Editing Existing Migrations
Problem: Developer edits a migration that’s already been applied elsewhere.
Solution: Never edit migrations that have been committed. Create new migrations for changes.
Missing Dependencies
Problem: Migration assumes schema changes that don’t exist in target environment.
Solution: Always test migrations against a clean database that matches production.
Data Loss During Rollback
Problem: Down migration drops columns without preserving data.
Solution: Plan rollbacks carefully. Consider backup tables for destructive changes.
Pro Tip: I’ve managed EF Core migrations for teams of 20+ developers. The key is establishing clear workflows and sticking to them religiously. One developer going rogue with migrations can break the entire team’s productivity for hours.
Monitoring and Maintenance
Track migration health across environments:
public class MigrationHealthCheck : IHealthCheck
{
private readonly DbContext context;
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var pendingMigrations = await this.context.Database.GetPendingMigrationsAsync();
if (pendingMigrations.Any())
{
return HealthCheckResult.Degraded($"Pending migrations: {string.Join(", ", pendingMigrations)}");
}
return HealthCheckResult.Healthy("All migrations applied");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Migration check failed", ex);
}
}
}
Key Takeaway
Managing EF Core migrations in large teams requires discipline and clear processes. Establish migration workflows early, stick to them consistently, and invest in proper tooling and automation.
The upfront investment in migration management pays huge dividends in reduced merge conflicts, faster deployments, and fewer production issues. Your team’s productivity depends on getting this right.