ef data patch import

Migrazioni in EF Core: come progettare e applicare Data Patch in modo sicuro

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

  1. migrationBuilder.Sql(...) dentro Up/Down
    Esegui SQL raw idempotente per aggiornare dati, creare indici o eseguire operazioni non coperte dai metadati EF.
  2. InsertData/UpdateData/DeleteData
    Utile per lookup piccoli e stabili; genera SQL portabile, ma meno flessibile per patch complesse.
  3. 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 staticiInsertData.


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, poi NOT 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)

  1. Compatibilità avanti: rilascia prima lo schema aggiuntivo (colonne nuove) senza rimuovere quelle vecchie.
  2. Backfill asincrono a batch.
  3. Switch logico lato app (feature flag).
  4. 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 controlli IF 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.


Pubblicato

in

da

Tag: