Rulegeneral
Database Migrations Rule
Rules for creating database migrations
Database Migrations
Guidelines for creating database migrations.
Implementation
-
Create migrations manually rather than using Entity Framework tooling:
- Place migrations in
/[scs-name]/Core/Database/Migrations - Name migration files with 14-digit timestamp prefix:
YYYYMMDDHHmmss_MigrationName.cs - Only implement the
Upmethod—don't createDownmigration
- Place migrations in
-
Follow this strict column ordering in table creation statements:
TenantId(if applicable)Id(always required)- Foreign keys (if applicable)
CreatedAtandModifiedAtas non-nullabledatetimeoffset- All other properties in the same order as they appear in the C# Aggregate class
-
Use appropriate SQL Server data types:
- Use
varchar(32)for strongly typed IDs (ULID is 26 chars + underscore + max 5-char prefix = exactly 32) - Intelligently deduce varchar vs nvarchar based on property type, validators, enum values, etc.
- Use
datetimeoffset(default),datetime2(timezone agnostic), ordate—neverdatetime - Default to
varchar(10)orvarchar(20)for enum values
- Use
-
Create appropriate constraints and indexes:
- Primary keys:
PK_TableName - Foreign keys:
FK_ChildTable_ParentTable_ColumnName - Indexes:
IX_TableName_ColumnName
- Primary keys:
-
Migrate existing data:
- Use
migrationBuilder.Sql("UPDATE [table] SET [column] = [value] WHERE [condition]")with care
- Use
-
Use standard SQL Server naming conventions:
- Table names should be plural (e.g.,
Users, notUser) - Constraint and index names should follow the patterns above
- Table names should be plural (e.g.,
Examples
Example 1 - Simple table
[DbContext(typeof(AccountManagementDbContext))]
[Migration("20250507141500_AddUserPreferences")] // ✅ DO: Use 14-digit timestamp
public sealed class AddUserPreferences : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
"UserPreferences",
table => new
{
TenantId = table.Column<long>("bigint", nullable: false), // ✅ DO: Add TenantId as first column
Id = table.Column<string>("varchar(32)", nullable: false), // ✅ DO: Make Id varchar(32) by default
UserId = table.Column<string>("varchar(32)", nullable: false), // ✅ DO: Add Foreginkey before CreatedAt/ModifiedAt
CreatedAt = table.Column<DateTimeOffset>("datetimeoffset", nullable: false),
ModifiedAt = table.Column<DateTimeOffset>("datetimeoffset", nullable: true),
Language = table.Column<string>("varchar(10)", nullable: false) // ✅ DO: Use varchar when colum has known values
},
constraints: table =>
{
table.PrimaryKey("PK_UserPreferences", x => x.Id);
table.ForeignKey("FK_UserPreferences_Users_UserId", x => x.UserId, "Users", "Id");
}
);
migrationBuilder.CreateIndex("IX_UserPreferences_TenantId", "UserPreferences", "TenantId");
migrationBuilder.CreateIndex("IX_UserPreferences_UserId", "UserPreferences", "UserId");
}
}
// ❌ DON'T: Forget to add the attribute [DbContext(typeof(XxxDbContext))] for the self-contained system
[Migration("20250507_AddUserPrefs")] // ❌ Missing proper 14-digit timestamp
public class AddUserPrefsMigration : Migration // ❌ Not sealed, incorrect naming, suffixed with Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// Create UserPreferences table // ❌ DON'T: Add comments
migrationBuilder.CreateTable(
"UserPreference", // ❌ DON'T: use singular name for table
table => new
{
Id = table.Column<string>("varchar(30)", nullable: false), // ❌ DON'T: Use varchar(30) for ULID
Theme = table.Column<string>("varchar(20)", nullable: false), // ❌ DON'T: Add properties before CreatedAt/ModifiedAt
TenantId = table.Column<long>("bigint", nullable: false), // ❌ TenantId should be first
CreatedAt = table.Column<DateTimeOffset>("datetimeoffset", nullable: false),
ModifiedAt = table.Column<DateTimeOffset>("datetime", nullable: true), // ❌ DON'T: Use datetime
UserId = table.Column<string>("varchar(32)", nullable: false), // ❌ Foreign key after CreatedAt/ModifiedAt
Language = table.Column<string>("varchar(10)", nullable: false), // ❌ Trailing comma
},
constraints: table =>
{
table.PrimaryKey("PrimaryKey_UserPreference", i => i.Id); // ❌ Incorrect PK naming, variable should be x not i
table.ForeignKey("ForeignKey_UserPreference_User", x => x.UserId, "Users", "Id"); // ❌ Incorrect FK naming
}
);
}
protected override void Down(MigrationBuilder migrationBuilder) // ❌ DON'T: Create a down method
{
migrationBuilder.DropTable("UserPreference");
}
}
Example 2 - Determining column sizes from validators
public sealed class UpdateUserValidator : AbstractValidator<UpdateUserCommand>
{
public UpdateUserValidator()
{
RuleFor(x => x.TimeZone).NotEmpty().MaximumLength(50); // ✅ DO: Use column sizes based on command validators
}
}
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>("TimeZone", "Users", "varchar(50)", nullable: false, defaultValue: "UTC"); // ✅ DO: Match column size to validator
// ✅ DO: Consider running complex logic here to update existing records
}