Custom migrations
Implement ILocalMigration to run arbitrary C# code alongside generated DDL migrations.
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.
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.
.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.