Quando evolviamo uno schema database con Entity Framework Core, spesso non basta aggiungere una colonna o creare una tabella: serve migrare i dati esistenti (backfill), normalizzare valori storici, riempire tabelle di lookup, creare utenti amministrativi, riallineare flag, ecc. In questo articolo vediamo strategie e pattern per eseguire data patch durante le migrazioni, mantenendo il deploy ripetibile, idempotente e sicuro.
Perché eseguire le data patch dentro le migrations?
- Atomicità: schema e dati evolvono insieme, nello stesso change-set e nella stessa transazione.
- Tracciabilità: il backfill è versionato nel controllo di versione e nel journal delle migrazioni.
- Ripetibilità: ambienti diversi (local, stage, prod) eseguono lo stesso script nella stessa fase.
Alternativa: job una tantum o script manuali. Pro: flessibilità. Contro: rischio di drift, scarsa auditabilità.
Opzioni in EF Core per modificare i dati
migrationBuilder.Sql(...)dentroUp/Down
Esegui SQL raw idempotente per aggiornare dati, creare indici o eseguire operazioni non coperte dai metadati EF.InsertData/UpdateData/DeleteData
Utile per lookup piccoli e stabili; genera SQL portabile, ma meno flessibile per patch complesse.HasDatanel model + migrazioni
Va bene per seed di base, ma evita di usarlo per dati dinamici/grandi backfill: tende a “cristallizzare” valori nel ModelSnapshot.
Regola pratica: patch complesse e backfill → SQL idempotente in
Up(); lookup statici →InsertData.
Pattern essenziali per Data Patch robuste
1) Idempotenza
Le patch devono poter essere rieseguite senza effetti collaterali:
IF NOT EXISTS (SELECT 1 FROM dbo.__DataPatches WHERE PatchId = '2025-09-backfill-isactive')
BEGIN
-- Esempio di backfill
UPDATE c
SET IsActive = CASE WHEN c.DeactivatedAt IS NULL THEN 1 ELSE 0 END
FROM dbo.Customers c;
INSERT INTO dbo.__DataPatches(PatchId, AppliedAtUtc)
VALUES ('2025-09-backfill-isactive', SYSUTCDATETIME());
END
Crea una tabella di controllo, ad es. dbo.__DataPatches(PatchId PK, AppliedAtUtc), per segnare le patch applicate.
2) Transazioni
EF Core avvolge le operazioni della migrazione in una transazione (se il provider lo supporta). Mantieni operazioni coerenti nello stesso Up().
3) Safe rollout & rollback
- In
Up()aggiungi prima la colonna nullable o con default, poi backfill, poiNOT NULL(se necessario). - In
Down()rimuovi con ordine inverso. - Evita lock prolungati: usa backfill a batch su tabelle grandi.
4) Feature flag e rollout progressivo
Se il codice applicativo dipende dal backfill, considera un flag applicativo per attivare la nuova logica solo a patch completata.
Esempio completo: aggiunta colonna + backfill + indice
Supponiamo di voler introdurre IsActive su Customers, derivato da DeactivatedAt.
using Microsoft.EntityFrameworkCore.Migrations;
public partial class AddIsActiveAndBackfill : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// 1) Aggiungi la colonna con default iniziale (non bloccare con NOT NULL subito)
migrationBuilder.AddColumn<bool>(
name: "IsActive",
table: "Customers",
type: "bit",
nullable: true); // temporaneamente nullable
// 2) Esegui il backfill in modo idempotente
migrationBuilder.Sql(@"
IF OBJECT_ID('dbo.__DataPatches','U') IS NULL
BEGIN
CREATE TABLE dbo.__DataPatches(
PatchId NVARCHAR(200) NOT NULL PRIMARY KEY,
AppliedAtUtc DATETIME2(3) NOT NULL
);
END
IF NOT EXISTS (SELECT 1 FROM dbo.__DataPatches WHERE PatchId = '2025-09-customers-isactive-backfill')
BEGIN
UPDATE c
SET IsActive = CASE WHEN c.DeactivatedAt IS NULL THEN 1 ELSE 0 END
FROM dbo.Customers c;
INSERT INTO dbo.__DataPatches(PatchId, AppliedAtUtc)
VALUES ('2025-09-customers-isactive-backfill', SYSUTCDATETIME());
END
");
// 3) Imposta NOT NULL solo dopo il backfill
migrationBuilder.AlterColumn<bool>(
name: "IsActive",
table: "Customers",
type: "bit",
nullable: false,
oldClrType: typeof(bool),
oldType: "bit",
oldNullable: true);
// 4) Crea l'indice (idempotente)
migrationBuilder.Sql(@"
IF NOT EXISTS (
SELECT 1 FROM sys.indexes
WHERE name = 'IX_Customers_IsActive' AND object_id = OBJECT_ID('dbo.Customers')
)
CREATE INDEX IX_Customers_IsActive ON dbo.Customers(IsActive);
");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// Drop indice se esiste
migrationBuilder.Sql(@"
IF EXISTS (
SELECT 1 FROM sys.indexes
WHERE name = 'IX_Customers_IsActive' AND object_id = OBJECT_ID('dbo.Customers')
)
DROP INDEX IX_Customers_IsActive ON dbo.Customers;
");
// Rimuovi la colonna
migrationBuilder.DropColumn(
name: "IsActive",
table: "Customers");
}
}
Note pratiche
- Per tabelle grandi, usa backfill a lotti:
DECLARE @BatchSize INT = 5000; WHILE 1=1 BEGIN ;WITH cte AS ( SELECT TOP (@BatchSize) Id FROM dbo.Customers WITH (READPAST) WHERE IsActive IS NULL ORDER BY Id ) UPDATE c SET IsActive = CASE WHEN c.DeactivatedAt IS NULL THEN 1 ELSE 0 END FROM dbo.Customers c INNER JOIN cte ON c.Id = cte.Id; IF @@ROWCOUNT = 0 BREAK; END - Usa hint come
READPASTper ridurre contese; evita transazioni eccessivamente lunghe.
Data patch per tabelle di lookup (senza SQL raw)
Per lookup piccoli e stabili, InsertData è comodo:
migrationBuilder.InsertData(
table: "OrderStatus",
columns: new[] { "Code", "Name" },
values: new object[,]
{
{ "PENDING", "Pending" },
{ "PAID", "Paid" },
{ "CANCELLED", "Cancelled" }
});
Evita
HasDataper entità dinamiche: può generare update indesiderati se i valori cambiano nel tempo.
Orchestrazione: comandi e CI/CD
- Local:
dotnet ef migrations add AddIsActiveAndBackfill dotnet ef database update - CI/CD:
- Step “build & test”
- Step “apply migrations” (con backup prima in stage/prod)
- Step “smoke test” applicativo
- Considera migrazioni idempotenti eseguibili più volte in ambienti containerizzati.
Strategia Zero-Downtime (SQL Server)
- Compatibilità avanti: rilascia prima lo schema aggiuntivo (colonne nuove) senza rimuovere quelle vecchie.
- Backfill asincrono a batch.
- Switch logico lato app (feature flag).
- Cleanup colonne e indici obsoleti in una migrazione successiva, quando il traffico è basso.
Testing delle migrazioni
- Integration test che creano un DB temporaneo, applicano tutte le migrazioni, e validano dati chiave.
- Usa
IDesignTimeDbContextFactoryper generare le migrazioni in modo consistente. - Verifica
Down()solo se supporti realmente rollback; in molti team si preferisce solo up con migrazioni additive.
Checklist veloce per data patch sicure
- La patch è idempotente (usa
__DataPatcheso controlliIF NOT EXISTS)? - L’ordine: Add column → Backfill → NOT NULL → Indici è rispettato?
- Per tabelle grandi, hai previsto un backfill a batch?
- Hai testato su un dump realistico?
- È presente un piano di rollback/mitigazione (backup + migrazione correttiva)?
- La pipeline applica le migrazioni in modo automatico e tracciato?
Conclusione
Integrare i data patch dentro le EF Core Migrations ti consente di consegnare cambi schema coerenti, ripetibili e auditable. Con idempotenza, batching e ordine delle operazioni corretto, puoi evolvere il database senza downtime, riducendo rischi e sorprese in produzione.
FAQ
Posso usare HasData per backfill complessi?
Meglio di no. HasData è ideale per seed statici e piccoli. Per patch complesse, preferisci SQL idempotente in Up().
Come evito lock pesanti durante il backfill?
Esegui update a lotti, usa READPAST, esegui in orari di bassa attività, e crea indici dopo il backfill quando possibile.
È sicuro rendere NOT NULL subito?
Solo dopo aver backfillato tutti i record. Inizialmente lascia la colonna nullable o con default sensato.
Dove salvo lo stato delle patch?
In una tabella di controllo (es. dbo.__DataPatches) con una chiave PatchId univoca.
Come testo le migrazioni in CI?
Alza un DB effimero (SQL Server in container), applica tutte le migrazioni, poi esegui test di integrità e query di convalida.
