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.HasData
nel 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
READPAST
per 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
HasData
per 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
IDesignTimeDbContextFactory
per 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
__DataPatches
o 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.