/DB

Custom migrations

Implement ILocalMigration to run arbitrary C# code alongside generated DDL migrations.

updated 5 Jun 20263 min readv0.3.2View as Markdown

When to use custom migrations

The generated DDL migration covers structural changes: CREATE TABLE, ALTER TABLE, ADD COLUMN, and so on. Custom migrations let you run SQL that goes beyond DDL, such as:

  • Seeding reference data after a new table is created (put the INSERT in UpSql)
  • Backfilling a new column with computed values (put the UPDATE in UpSql)
  • Running any SQL the DDL generator cannot express automatically

Custom migrations and generated migrations share the same ILocalMigration contract and the same PreviousId chain, so they apply in one ordered sequence.

ILocalMigration interface

Implement ILocalMigration (namespace Socigy.OpenSource.DB.Migrations) in your DB project:

public interface ILocalMigration
{
    string Id { get; }
    string? PreviousId { get; }
    string UpSql { get; }
    string DownSql { get; }
}
Member Description
Id Unique string identifier for this migration (must be globally unique)
PreviousId Id of the migration that directly precedes this one in the chain, or null for the first migration
UpSql SQL to apply on upgrade. May be "" when only data changes are needed
DownSql SQL to apply on rollback. Always provide a value (use "" when irreversible)

Example: seed data after creating a table

public class SeedRolesMigration : ILocalMigration
{
    public string Id => "seed-roles-2026-05-01";
    public string? PreviousId => "202605011200_Initial_Migration_abc123";

    // Roles table already exists from the generated migration; only seed data here
    public string UpSql => """
        INSERT INTO "roles" ("id", "value", "description") VALUES
            (1, 'Reader', NULL), (2, 'Writer', NULL), (4, 'Moderator', NULL), (8, 'Admin', NULL)
        ON CONFLICT ("id") DO UPDATE SET "value" = EXCLUDED."value";
        """;

    public string DownSql => "DELETE FROM \"roles\" WHERE \"id\" IN (1, 2, 4, 8);";
}

Example: backfill a new column

public class BackfillDisplayNameMigration : ILocalMigration
{
    public string Id => "backfill-display-name-2026-05-15";
    public string? PreviousId => "seed-roles-2026-05-01";

    // The ADD COLUMN is done in a generated migration; this only backfills the data
    public string UpSql => "UPDATE \"users\" SET \"display_name\" = \"username\" WHERE \"display_name\" IS NULL;";

    public string DownSql => "UPDATE \"users\" SET \"display_name\" = NULL;";
}

Ordering custom migrations

The migration manager uses the PreviousId chain to determine execution order. Set PreviousId to the Id of the last generated migration (found in Socigy/structure.json as the top-level "id" field) or the Id of the custom migration that should run immediately before it.

WARNING
If two migrations (generated or custom) share the same Id, only one is applied and the other is silently skipped. Use descriptive, globally unique Id values that include a date or timestamp.

Discovery

The source generator collects every ILocalMigration implementation at build time and bakes the ordered list into the generated MigrationManager. You do not register them anywhere. Implement the interface in your DB project, rebuild, and the migration is included in the generated manager automatically.

Combining with generated migrations

Generated migrations (.g.cs files) also implement ILocalMigration. The manager sees all of them together and applies them in PreviousId order regardless of whether they are hand-written or generated.

TIP
To anchor a custom migration to the latest generated migration, open the most recent .g.cs file in Socigy/Migrations/ and use its id as your PreviousId. Each generated class exposes the value as public const string _Id near the top.