Start Debugging

Migrar del seeding con HasData a UseAsyncSeeding en EF Core 11

Guia paso a paso para mover los datos de seed de HasData a UseSeeding y UseAsyncSeeding en EF Core 11, incluida la trampa de la migracion DeleteData que borra tus filas existentes si la pasas por alto.

Mover los datos de seed de HasData a UseSeeding/UseAsyncSeeding en EF Core 11 es un trabajo de medio dia para una aplicacion tipica, y lo unico que te va a morder no es la API nueva: es que borrar una llamada a HasData hace que el siguiente dotnet ef migrations add emita una instruccion DeleteData que elimina esas mismas filas de cada base de datos contra la que se ejecute la migracion. Si borras el HasData y agregas el UseSeeding en el mismo despliegue sin pensar en el orden, puedes borrar datos de referencia en produccion. Esta guia recorre la migracion en el orden que evita eso, usando .NET 11, EF Core 11 (Microsoft.EntityFrameworkCore 11.0) y C# 14. Reserva una tarde para una aplicacion mediana, la mayor parte gastada en auditar que seeds deberian haberse movido hace anios.

Vale la pena? Para cualquier cosa dinamica, con clave generada, calculada o condicional: si, y era necesario desde hace tiempo. Para una tabla de referencia genuinamente estatica de tres filas, dejala en HasData. El objetivo aqui no es “borrar todos los HasData”, es “mover los seeds que nunca pertenecieron al modelo”.

Por que dejar HasData

HasData incrusta tus datos de seed en el snapshot del modelo. Ese unico hecho de diseno es la raiz de cada razon para dejarlo:

El equipo de EF renombro HasData a “model-managed data” precisamente para desalentar su uso como herramienta general de seeding. UseSeeding y UseAsyncSeeding, introducidos en EF Core 9 y vigentes en EF Core 11, son codigo de aplicacion corriente que se ejecuta contra un DbContext vivo, sin ninguno de esos limites. Si todavia estas decidiendo que seeds mover, la matriz de decision HasData vs UseSeeding traza la linea fila por fila.

Que se rompe

AreaCambioSeveridad
Bases de datos existentesBorrar una llamada HasData genera DeleteData, que elimina las filas del seed en la migracionalta
IdempotenciaHasData se difundia automaticamente; UseSeeding se ejecuta cada vez y necesita una comprobacion de existencia escrita a manoalta
Dos rutas de codigoEl tooling llama al UseSeeding sincrono, el arranque async llama a UseAsyncSeeding; implementa solo uno y el otro no hace nada en silencioalta
Control de versionesLos valores del seed salen del snapshot de la migracion y viven en el codigo de arranque; ya no quedan capturados en el historial de migracionesmedia
Integridad referencialLos datos de los que dependen claves foraneas de otras tablas ahora aterrizan en un callback de arranque, no dentro de la transaccion de la migracionmedia
Configuracion de pruebasLas pruebas que dependian de que HasData se ejecutara via EnsureCreated siguen funcionando, pero solo si se implementan ambos overloadsbaja

La fila superior es la que termina en un informe de incidente. Todo lo demas es mecanico.

Lista previa al despegue

Antes de tocar una linea de codigo de seeding:

Pasos de migracion

El orden importa. Hacer esto en la secuencia equivocada es exactamente como borras datos de referencia de produccion.

1. Escribe primero los callbacks UseSeeding y UseAsyncSeeding

Agrega la nueva ruta de seeding antes de borrar nada. Ambos overloads, logica identica, comprobacion de existencia en la parte superior de cada uno. Factoriza el cuerpo en metodos compartidos para que las versiones sincrona y async no se desvien:

// Program.cs -- .NET 11, ASP.NET Core 11, EF Core 11 (Microsoft.EntityFrameworkCore 11.0), C# 14
builder.Services.AddDbContext<AppDbContext>(options =>
    options
        .UseSqlServer(builder.Configuration.GetConnectionString("Default"))
        .UseSeeding((context, _) => SeedStatuses(context))
        .UseAsyncSeeding((context, _, ct) => SeedStatusesAsync(context, ct)));

static void SeedStatuses(DbContext context)
{
    string[] required = ["Pending", "Shipped", "Delivered"];
    var existing = context.Set<OrderStatus>()
        .Where(s => required.Contains(s.Name))
        .Select(s => s.Name)
        .ToHashSet();

    var missing = required
        .Where(name => !existing.Contains(name))
        .Select(name => new OrderStatus { Name = name })
        .ToList();

    if (missing.Count > 0)
    {
        context.Set<OrderStatus>().AddRange(missing);
        context.SaveChanges();
    }
}

static async Task SeedStatusesAsync(DbContext context, CancellationToken ct)
{
    string[] required = ["Pending", "Shipped", "Delivered"];
    var existing = await context.Set<OrderStatus>()
        .Where(s => required.Contains(s.Name))
        .Select(s => s.Name)
        .ToListAsync(ct);

    var missing = required
        .Where(name => !existing.Contains(name))
        .Select(name => new OrderStatus { Name = name })
        .ToList();

    if (missing.Count > 0)
    {
        context.Set<OrderStatus>().AddRange(missing);
        await context.SaveChangesAsync(ct);
    }
}

Fijate en que esta version ya no fija Id a mano. La base de datos lo asigna. Ese es todo el punto: si antes asignabas claves a mano en HasData, las referencias de clave foranea existentes pueden apuntar a esos valores especificos, que es lo que hace delicado el paso 2.

Verifica: ejecuta la aplicacion contra una base de datos local vacia con las llamadas HasData todavia presentes. Deberia arrancar limpiamente sin filas duplicadas. La comprobacion de existencia deberia hacer que un segundo arranque no haga nada (una consulta de lectura, cero escrituras).

2. Decide como preservar las filas existentes antes de borrar HasData

Este es el paso que todos se saltan y lamentan. Cuando borras una llamada HasData y ejecutas dotnet ef migrations add, EF genera un DeleteData en el metodo Up para cada fila previamente sembrada:

// .NET 11, EF Core 11 -- what removing HasData generates
migrationBuilder.DeleteData(
    table: "OrderStatuses",
    keyColumn: "Id",
    keyValues: new object[] { 1, 2, 3 });

En una base de datos existente, ese DELETE se ejecuta. Si otras tablas tienen claves foraneas apuntando a esas filas, el borrado o bien falla con un error FOREIGN KEY constraint failed o, peor, se propaga en cascada. Tienes tres opciones seguras:

Para cualquier cosa con claves foraneas entrantes, la opcion B es casi siempre lo que quieres. Las filas no necesitan moverse; solo la propiedad de ellas.

Verifica: despues de generar la migracion de eliminacion, lee el archivo generado antes de aplicarlo. Confirma que las llamadas DeleteData apuntan exactamente a las filas que esperas, y que has elegido y aplicado A, B o C para cada tabla.

3. Quita las llamadas HasData de OnModelCreating

Ahora borra el bloque HasData de los seeds que estas moviendo:

// Before -- .NET 11, EF Core 11
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<OrderStatus>().HasData(
        new OrderStatus { Id = 1, Name = "Pending" },
        new OrderStatus { Id = 2, Name = "Shipped" },
        new OrderStatus { Id = 3, Name = "Delivered" });
}

// After -- the HasData block is gone; seeding lives in UseSeeding now
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // OrderStatus seeding moved to UseSeeding in Program.cs
}

Verifica: el proyecto sigue compilando, y dotnet ef migrations has-pending-model-changes reporta un cambio pendiente (el seed eliminado) que estas a punto de capturar.

4. Genera y revisa la migracion de eliminacion

# .NET 11, EF Core 11
dotnet ef migrations add RemoveOrderStatusSeed

Abre la migracion generada y aplica tu decision del paso 2. Si elegiste la opcion B, borra el DeleteData de Up y el InsertData correspondiente de Down. Deja intactos los cambios del snapshot del modelo; esos son correctos y necesarios.

Verifica: ejecuta dotnet ef migrations script contra tu base de datos de staging y lee el SQL. Confirma que no sobrevive ningun DELETE FROM OrderStatuses inesperado si tu intencion era preservar las filas.

5. Aplica contra staging y confirma que los datos sobreviven

# .NET 11, EF Core 11
dotnet ef database update

Luego arranca la aplicacion para que UseAsyncSeeding se ejecute.

Verifica: consulta la tabla. Las filas de referencia estan presentes exactamente una vez. Arranca la aplicacion una segunda vez y confirma que no aparecieron duplicados, lo cual prueba que la comprobacion de existencia funciona. Si las claves foraneas referencian las filas, confirma que esas relaciones estan intactas.

Prueba de humo posterior a la migracion

Ejecuta esta lista de verificacion contra staging antes de enviar:

Plan de reversion

Esta migracion es reversible, pero lee la letra pequenia. Si elegiste la opcion B (editaste a mano quitando el DeleteData), el Down a nivel de esquema no hace nada para los datos, asi que revertir la migracion deja las filas en su lugar y simplemente restaura el snapshot del modelo. Volver a agregar el bloque HasData y generar una migracion nueva te devuelve al mundo antiguo, aunque EF querra hacer InsertData de cualquier fila que falte.

Si elegiste la opcion A (dejaste que ocurriera el borrado y resiembra), la reversion es mas arriesgada: las filas ahora tienen claves generadas por la base de datos, asi que volver a un modelo HasData que espera claves fijas no coincidira. En ese caso, no intentes una reversion en el sitio de datos en vivo. Restaura desde el respaldo que tomaste en el paso previo al despegue. Por eso el respaldo previo al despegue no es opcional.

Trampas que golpeamos

Si tus entidades son records, nada de esto cambia: los records funcionan correctamente con EF Core 11 bajo tanto HasData como UseSeeding. Y si estas ajustando la capa de datos mientras estas aqui, la misma disciplina de “que se ejecuta y cuando” aparece en los interceptores de EF Core 11 para auditoria.

Relacionado

Fuentes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Volver