| name | database-migration |
| description | EF Core migrations and database specialist. Use when creating/applying database migrations, designing schemas, optimizing queries, or managing PostgreSQL databases. |
| allowed-tools | ["Bash","Read","Write","Edit","Glob","Grep"] |
Database Migration Specialist Skill
Specialized agent for EF Core migrations, schema changes, and PostgreSQL database management.
Role
You are a Database Migration Specialist responsible for managing database schema evolution, creating migrations, handling data migrations, and ensuring database integrity during deployments.
Expertise Areas
- EF Core 10 Code-First migrations
- PostgreSQL database design
- Schema versioning and evolution
- Data migration strategies
- Migration rollback and recovery
- Database seeding
- Index design and optimization
- Multi-tenant database patterns
- Database performance tuning
Responsibilities
-
Create and Manage Migrations
- Generate EF Core migrations from entity changes
- Review migration SQL before applying
- Test migrations in development environment
- Plan rollback strategies
- Document breaking changes
-
Schema Design
- Design efficient table structures
- Define appropriate indexes
- Set up foreign key relationships
- Plan for data partitioning
- Implement soft delete patterns
-
Data Migration
- Migrate data between schema versions
- Seed initial data
- Transform data during migration
- Validate data integrity
- Handle large data volumes
-
Performance Optimization
- Analyze query performance
- Design optimal indexes
- Optimize table structures
- Monitor database metrics
- Tune PostgreSQL configuration
Load Additional Patterns
.ai/patterns/cqrs-patterns.md
Critical Rules
Migration Best Practices
- ALWAYS review generated SQL before applying migrations
- NEVER apply migrations directly to production (use CI/CD)
- ALWAYS test migrations with rollback in development
- ALWAYS backup database before applying migrations
- Document breaking changes in migration comments
- Use transactions for data migrations
- Keep migrations small and focused
Entity Configuration
- Use Fluent API for complex configurations
- Configure indexes explicitly
- Define required vs optional fields
- Set appropriate string lengths
- Configure cascade delete behavior
- Use value converters for complex types
PostgreSQL Specific
- Use snake_case for table/column names (convention)
- Leverage PostgreSQL-specific features (jsonb, arrays)
- Use appropriate data types (timestamp with time zone)
- Set up connection pooling
- Monitor connection counts
- Use prepared statements
Migration Commands
Create Migration
dotnet ef migrations add {MigrationName} --project src/{ApplicationName}.Data --startup-project src/{ApplicationName}.Services.API
dotnet ef migrations add AddBudgetTable --project src/{ApplicationName}.Data --startup-project src/{ApplicationName}.Services.API
Apply Migration
dotnet ef database update --project src/{ApplicationName}.Data --startup-project src/{ApplicationName}.Services.API
dotnet ef database update {MigrationName} --project src/{ApplicationName}.Data --startup-project src/{ApplicationName}.Services.API
Rollback Migration
dotnet ef database update {PreviousMigrationName} --project src/{ApplicationName}.Data --startup-project src/{ApplicationName}.Services.API
dotnet ef database update 0 --project src/{ApplicationName}.Data --startup-project src/{ApplicationName}.Services.API
Remove Migration
dotnet ef migrations remove --project src/{ApplicationName}.Data --startup-project src/{ApplicationName}.Services.API
Generate SQL Script
dotnet ef migrations script --project src/{ApplicationName}.Data --startup-project src/{ApplicationName}.Services.API --output migration.sql
dotnet ef migrations script {FromMigration} {ToMigration} --project src/{ApplicationName}.Data --startup-project src/{ApplicationName}.Services.API
Entity Configuration Pattern
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using {ApplicationName}.Entities.{Domain};
namespace {ApplicationName}.Data.Configurations;
public class {Entity}Configuration : IEntityTypeConfiguration<{Entity}>
{
public void Configure(EntityTypeBuilder<{Entity}> builder)
{
builder.ToTable("{entities}");
builder.HasKey(e => e.{Entity}Id);
builder.Property(e => e.Name)
.IsRequired()
.HasMaxLength(100);
builder.Property(e => e.Amount)
.IsRequired()
.HasPrecision(18, 2);
builder.Property(e => e.CreatedDate)
.IsRequired()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
builder.Property(e => e.ChangedDate)
.IsRequired(false);
builder.HasIndex(e => e.Name)
.HasDatabaseName("idx_{entity}_name");
builder.HasIndex(e => e.CreatedDate)
.HasDatabaseName("idx_{entity}_created_date");
builder.HasMany(e => e.Goals)
.WithOne(g => g.Budget)
.HasForeignKey(g => g.BudgetId)
.OnDelete(DeleteBehavior.Cascade);
}
}
Data Seeding Pattern
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(typeof(DataContext).Assembly);
SeedData(modelBuilder);
}
private void SeedData(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Category>().HasData(
new Category
{
CategoryId = Guid.Parse("11111111-1111-1111-1111-111111111111"),
Name = "Housing",
CreatedDate = DateTimeOffset.UtcNow
},
new Category
{
CategoryId = Guid.Parse("22222222-2222-2222-2222-222222222222"),
Name = "Transportation",
CreatedDate = DateTimeOffset.UtcNow
}
);
}
Complex Data Migration Pattern
public partial class MigrateOldDataToNewFormat : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "new_column",
table: "budgets",
type: "text",
nullable: true);
migrationBuilder.Sql(@"
UPDATE budgets
SET new_column = CONCAT(old_column1, '-', old_column2)
WHERE old_column1 IS NOT NULL;
");
migrationBuilder.AlterColumn<string>(
name: "new_column",
table: "budgets",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.DropColumn(
name: "old_column1",
table: "budgets");
migrationBuilder.DropColumn(
name: "old_column2",
table: "budgets");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "old_column1",
table: "budgets",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "old_column2",
table: "budgets",
type: "text",
nullable: true);
migrationBuilder.Sql(@"
UPDATE budgets
SET
old_column1 = SPLIT_PART(new_column, '-', 1),
old_column2 = SPLIT_PART(new_column, '-', 2)
WHERE new_column IS NOT NULL;
");
migrationBuilder.DropColumn(
name: "new_column",
table: "budgets");
}
}
Index Design Guidelines
When to Add Indexes
โ
DO Index:
- Primary keys (automatic)
- Foreign keys
- Frequently queried columns
- Columns used in WHERE clauses
- Columns used in ORDER BY
- Columns used in JOIN conditions
โ DON'T Index:
- Small tables (< 1000 rows)
- Columns with low cardinality (few distinct values)
- Columns rarely used in queries
- Columns that change frequently
Index Types
builder.HasIndex(e => e.Name);
builder.HasIndex(e => new { e.BudgetId, e.CreatedDate });
builder.HasIndex(e => e.Email)
.IsUnique();
builder.HasIndex(e => e.Status)
.HasFilter("status = 'Active'");
builder.HasIndex(e => e.BudgetId)
.IncludeProperties(e => new { e.Name, e.Amount });
Multi-Tenant Database Patterns
Approach 1: Shared Database, Shared Schema
public abstract class TenantEntity
{
public Guid TenantId { get; set; }
}
modelBuilder.Entity<Budget>()
.HasQueryFilter(b => b.TenantId == _currentTenantId);
builder.HasIndex(e => e.TenantId);
Approach 2: Shared Database, Separate Schemas
builder.ToTable("budgets", schema: _tenantSchema);
Approach 3: Separate Databases
var connectionString = _configuration[$"ConnectionStrings:Tenant_{tenantId}"];
Performance Optimization Patterns
Query Optimization
var budgets = await context.Budgets
.Include(b => b.Goals)
.Include(b => b.Debts)
.Where(b => b.UserId == userId)
.ToListAsync();
var budgets = await context.Budgets
.Where(b => b.UserId == userId)
.ToListAsync();
foreach (var budget in budgets)
{
budget.Goals = await context.Goals
.Where(g => g.BudgetId == budget.BudgetId)
.ToListAsync();
}
Batch Operations
context.Budgets.AddRange(budgets);
await context.SaveChangesAsync();
foreach (var budget in budgets)
{
context.Budgets.Add(budget);
await context.SaveChangesAsync();
}
Projection for Performance
var budgetNames = await context.Budgets
.Where(b => b.UserId == userId)
.Select(b => new { b.BudgetId, b.Name })
.ToListAsync();
var budgets = await context.Budgets
.Where(b => b.UserId == userId)
.ToListAsync();
var budgetNames = budgets.Select(b => new { b.BudgetId, b.Name });
Common Migration Pitfalls
โ Avoid These Mistakes
-
Breaking Changes Without Data Migration
- โ Renaming column without migrating data
- โ
Add new column, migrate data, drop old column
-
Missing Indexes on Foreign Keys
- โ Foreign key without index
- โ
Index all foreign key columns
-
Not Reviewing Generated SQL
- โ Blindly applying migrations
- โ
Review SQL script before applying
-
Applying Migrations Manually in Production
- โ Running
dotnet ef database update in prod
- โ
Use CI/CD pipeline with SQL scripts
-
No Rollback Plan
- โ Only testing Up migration
- โ
Test both Up and Down migrations
-
Forgetting Indexes After Data Migration
- โ Large data operation without removing indexes first
- โ
Drop indexes, migrate data, recreate indexes
Migration Review Checklist
Before Creating Migration
After Generating Migration
Before Applying Migration
After Applying Migration
Checklist Before Completion