//JorgenHoc
← Todos los artículos
EF CorePor Jorge CalderónActualizado 11 min read

Migraciones de EF Core: El Recorrido Definitivo

Un recorrido completo por las migraciones de EF Core: agregar, entender, aplicar, revertir, consolidar y desplegar migraciones de forma segura en bases de datos de producción.

#entity-framework#dotnet#database

Las migraciones de EF Core son el sistema de control de versiones para el esquema de tu base de datos. Cada cambio en tu modelo de entidades queda capturado como un archivo de migración con marca de tiempo y reversible. Este recorrido cubre todo: desde la primera migración hasta las estrategias de despliegue en producción.

La entidad Products y las cuatro migraciones reales de abajo — incluida la del backfill editada a mano — están en samples/ef-core-migrations-walkthrough. Las generó dotnet ef, no se escribieron a mano, así que puedes inspeccionar y ejecutar los archivos exactos (ver Míralo funcionar).

Requisitos previos

Instala las herramientas de EF Core de forma global y agrega el paquete de diseño a tu proyecto:

dotnet tool install --global dotnet-ef
dotnet tool update --global dotnet-ef  # Actualizar si ya está instalado
 
# Agregar al proyecto
dotnet add package Microsoft.EntityFrameworkCore.Design

Verifica que funciona:

dotnet ef --version
# Entity Framework Core .NET Command-line Tools 10.x.x
💡

Mantén la versión de la herramienta dotnet-ef alineada con la de tus paquetes Microsoft.EntityFrameworkCore.*. Una herramienta varias versiones mayores por detrás de tus paquetes (una de EF Core 8 contra proyectos de EF Core 10, por ejemplo) puede fallar al cargar el ensamblado de diseño. dotnet tool update --global dotnet-ef --version 10.* la fija.

Cómo agregar tu primera migración

Después de configurar tu DbContext y las entidades, crea la migración inicial:

dotnet ef migrations add InitialCreate

EF Core inspecciona tu DbContext, lo compara con el estado actual de la base de datos (vacía, en el caso de la primera migración) y genera los archivos de migración.

Aparecen tres archivos en una carpeta Migrations/:

Migrations/
  20250120143000_InitialCreate.cs        ← La migración en sí
  20250120143000_InitialCreate.Designer.cs  ← Metadatos de instantánea (no editar)
  AppDbContextModelSnapshot.cs           ← Instantánea del modelo actual (no editar)

Cómo entender el archivo de migración

// Migrations/20250120143000_InitialCreate.cs
public partial class InitialCreate : Migration
{
    /// <summary>
    /// Se ejecuta al aplicar la migración (dotnet ef database update)
    /// </summary>
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Products",
            columns: table => new
            {
                Id = table.Column<int>(type: "int", nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
                Price = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
                CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Products", x => x.Id);
            });
    }
 
    /// <summary>
    /// Se ejecuta al revertir la migración (dotnet ef database update <previous>)
    /// </summary>
    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(name: "Products");
    }
}

El archivo ModelSnapshot es crítico: EF Core lo usa para determinar qué cambió entre migraciones. Nunca lo elimines.

Cómo aplicar migraciones

# Aplicar todas las migraciones pendientes
dotnet ef database update
 
# Aplicar hasta una migración específica
dotnet ef database update AddProductDescription
 
# Aplicar solo la migración inicial
dotnet ef database update InitialCreate

EF Core mantiene una tabla __EFMigrationsHistory en tu base de datos que registra qué migraciones se han aplicado:

SELECT * FROM __EFMigrationsHistory;
-- MigrationId                              | ProductVersion
-- 20250120143000_InitialCreate             | 8.0.0
-- 20250124091500_AddProductDescription     | 8.0.0

Cómo agregar migraciones posteriores

Después de modificar una entidad, agrega una nueva migración:

// Agregar una propiedad Description al Product
public class Product
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public decimal Price { get; set; }
    public string? Description { get; set; }  // Nueva propiedad
    public DateTime CreatedAt { get; set; }
}
dotnet ef migrations add AddProductDescription
dotnet ef database update

La migración generada contiene únicamente el delta:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AddColumn<string>(
        name: "Description",
        table: "Products",
        type: "nvarchar(max)",
        nullable: true);
}
 
protected override void Down(MigrationBuilder migrationBuilder)
{
    migrationBuilder.DropColumn(
        name: "Description",
        table: "Products");
}

Cómo revertir migraciones

Revertir a una migración específica

# Revertir hasta después de InitialCreate (deshace AddProductDescription)
dotnet ef database update InitialCreate
 
# Revertir todas las migraciones (base de datos vacía — tablas eliminadas, pero __EFMigrationsHistory permanece)
dotnet ef database update 0
⚠️

Revertir elimina datos. dotnet ef database update 0 eliminará todas tus tablas. Haz esto solo en desarrollo o con un respaldo confirmado.

Eliminar la última migración (antes de aplicarla)

Si todavía no has aplicado una migración, puedes eliminarla completamente:

dotnet ef migrations remove

Esto borra el archivo de migración y revierte el ModelSnapshot. Solo puedes eliminar la migración más reciente, y únicamente si no se ha aplicado a ninguna base de datos.

💡

Si accidentalmente ejecutas dotnet ef database update sobre una migración con errores, primero debes revertir la base de datos antes de eliminar el archivo de migración.

Cómo personalizar migraciones

Las migraciones generadas por EF Core son un punto de partida: puedes editarlas para agregar inicialización de datos, columnas calculadas u otras operaciones que EF Core no puede detectar automáticamente:

protected override void Up(MigrationBuilder migrationBuilder)
{
    // EF Core generó esto
    migrationBuilder.AddColumn<string>(
        name: "Slug",
        table: "Products",
        nullable: true);
 
    // Tú agregaste esto — rellena Slug con los datos existentes de Name
    migrationBuilder.Sql(@"
        UPDATE Products
        SET Slug = LOWER(REPLACE(Name, ' ', '-'))
        WHERE Slug IS NULL
    ");
 
    // Luego hazlo no nulo después de rellenarlo
    migrationBuilder.AlterColumn<string>(
        name: "Slug",
        table: "Products",
        nullable: false,
        oldClrType: typeof(string),
        oldNullable: true);
}
⚠️

Editar una migración que ya se aplicó a cualquier base de datos (desarrollo, staging, producción) es peligroso. El ModelSnapshot no coincidirá con tu archivo de migración. Edita únicamente migraciones que no hayan sido aplicadas.

💡

Esta edición exacta es la migración AddProductSlug del sample. EF generó primero un único AddColumn no-nullable con defaultValue: "" — que compila, pero deja cada fila existente con un slug vacío. Se editó a mano a la forma de tres pasos de arriba, y el sample siembra filas antes de aplicarla, así que puedes ver cómo Mechanical Keyboard se convierte en mechanical-keyboard. Un detalle que conviene saber: mantén el tipo de columna de tu migración editada igual al del modelo (nvarchar(max) aquí), o dotnet ef migrations has-pending-model-changes reportará una diferencia fantasma contra el snapshot.

Cómo consolidar migraciones

Con el tiempo, se acumulan cientos de migraciones. Consolidarlas las combina en una sola para tener un historial más limpio. Esto se hace típicamente en un límite de versión mayor.

Método 1: Empezar de cero (destructivo — solo para desarrollo)

# 1. Eliminar todos los archivos de migración
# 2. Eliminar la base de datos
dotnet ef database drop --force
 
# 3. Agregar una única migración inicial nueva
dotnet ef migrations add InitialCreate
 
# 4. Recrear la base de datos
dotnet ef database update

Método 2: Consolidar sin perder datos (seguro para producción)

# 1. Anotar el nombre de la migración actual
dotnet ef migrations list
 
# 2. Agregar una nueva migración de consolidación que parta desde cero
#    (EF Core no generará nada — el modelo coincide con la BD)
dotnet ef migrations add Squash_v2 --no-build
 
# 3. Reemplazar manualmente el Up() y el Down() de la nueva migración
#    con el SQL completo de creación/destrucción del esquema
 
# 4. Actualizar __EFMigrationsHistory en producción para eliminar las entradas antiguas
#    y agregar únicamente la nueva entrada de migración consolidada

Un enfoque más práctico usando la salida de Script-Migration:

# Generar un script SQL del esquema actual completo
dotnet ef migrations script 0 AddProductDescription --output schema_v2.sql

Cómo generar scripts SQL

Para despliegues en producción, genera un script SQL en lugar de ejecutar dotnet ef database update directamente:

# Script completo desde cero hasta la versión más reciente
dotnet ef migrations script --output migration.sql
 
# Script desde una migración específica hasta la más reciente (idempotente)
dotnet ef migrations script InitialCreate --idempotent --output migration.sql
 
# Script entre dos migraciones específicas
dotnet ef migrations script InitialCreate AddProductDescription --output delta.sql

El flag --idempotent genera un script que verifica __EFMigrationsHistory antes de ejecutar cada migración, por lo que es seguro ejecutarlo múltiples veces:

IF NOT EXISTS(SELECT * FROM [__EFMigrationsHistory] WHERE [MigrationId] = N'20250124091500_AddProductDescription')
BEGIN
    ALTER TABLE [Products] ADD [Description] nvarchar(max) NULL;
END;
GO

Estrategia de despliegue en producción

Opción 1: Migración automática al arrancar

Aplica las migraciones pendientes cuando la aplicación inicia:

// Program.cs
var app = builder.Build();
 
using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    await db.Database.MigrateAsync();
}
 
app.Run();

Ideal para: Equipos pequeños, despliegues blue/green donde solo corre una versión a la vez.

Riesgoso cuando: Varias instancias arrancan simultáneamente (condición de carrera en las migraciones). Mitiga esto con un bloqueo distribuido o ejecutando las migraciones como un paso previo al despliegue.

Opción 2: Migración como trabajo independiente

Ejecuta las migraciones como un trabajo puntual antes de desplegar la nueva versión de la aplicación:

# En tu pipeline de CI/CD, antes de desplegar la app:
dotnet ef database update --connection "$PRODUCTION_CONNECTION_STRING"
 
# O usando un trabajo de migración dedicado en Docker:
dotnet run --project MyApp.Migrations
// MyApp.Migrations/Program.cs (proyecto de migración dedicado)
var host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((context, services) =>
    {
        services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(context.Configuration.GetConnectionString("DefaultConnection")));
    })
    .Build();
 
using var scope = host.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
 
Console.WriteLine("Aplicando migraciones...");
await db.Database.MigrateAsync();
Console.WriteLine("Migraciones completadas.");

Opción 3: Scripts SQL en CI/CD

El enfoque más seguro para entornos regulados:

# Ejemplo con GitHub Actions
- name: Generar script de migración
  run: dotnet ef migrations script --idempotent --output migration.sql
 
- name: Aplicar script de migración
  run: |
    sqlcmd -S ${{ secrets.DB_SERVER }} \
           -d ${{ secrets.DB_NAME }} \
           -U ${{ secrets.DB_USER }} \
           -P ${{ secrets.DB_PASS }} \
           -i migration.sql

Buenas prácticas con migraciones

💡

Mantén las migraciones pequeñas y enfocadas. Una migración que agrega una columna y rellena datos existentes son dos operaciones: considera dividirlas para hacer los rollbacks más seguros.

💡

Usa nombres de migración descriptivos que expliquen qué cambió: AddUserEmailIndex, RenameProductCodeToSku, CreateOrdersTable. La marca de tiempo ya es única; el nombre es para las personas.

⚠️

Nunca renombres ni elimines archivos de migración que se hayan aplicado en producción. EF Core usa el nombre del archivo para compararlo con __EFMigrationsHistory. Renombrarlo provoca errores de "migración no encontrada".

Cómo verificar el estado de las migraciones

# Listar todas las migraciones y su estado de aplicación
dotnet ef migrations list
 
# Salida:
# 20250120143000_InitialCreate (Applied)
# 20250124091500_AddProductDescription (Applied)
# 20250127110000_AddIndexOnProductName (Pending)

Verificación mediante código:

var pendingMigrations = await db.Database.GetPendingMigrationsAsync();
var appliedMigrations = await db.Database.GetAppliedMigrationsAsync();
 
if (pendingMigrations.Any())
{
    _logger.LogWarning("Migraciones pendientes: {Migrations}",
        string.Join(", ", pendingMigrations));
}

Míralo funcionar

samples/ef-core-migrations-walkthrough es este tutorial en archivos ejecutables. La carpeta Migrations/ tiene cuatro migraciones reales en el orden de arriba — InitialCreate, AddProductDescription, AddIndexOnProductName y la editada a mano AddProductSlug — cada una generada por dotnet ef migrations add. Abre InitialCreate y verás el mismo CreateTable(...) de antes, producido por la herramienta en vez de transcrito.

Ejecutarlo cuenta la historia del backfill de principio a fin: en una base de datos nueva migra al esquema previo a Slug, inserta dos filas y luego aplica AddProductSlug para que el UPDATE corra sobre datos que ya existen:

Migrating to AddIndexOnProductName (pre-Slug schema)...
Seeded 2 rows, then applying AddProductSlug (backfills Slug)...
Products (Slug backfilled from Name by the AddProductSlug migration):
  Mechanical Keyboard    -> slug "mechanical-keyboard"
  USB C Cable            -> slug "usb-c-cable"
Salida de consola del sample de migraciones: las cuatro migraciones InitialCreate, AddProductDescription, AddIndexOnProductName y AddProductSlug listadas como pendientes, luego el programa migrando al esquema previo a Slug, sembrando dos filas y aplicando AddProductSlug; la tabla __EFMigrationsHistory lista las cuatro migraciones aplicadas, y los dos productos muestran su Slug rellenado a partir de Name — Mechanical Keyboard a mechanical-keyboard y USB C Cable a usb-c-cable.
La ejecución del sample en una base de datos nueva: cuatro migraciones reales aplicadas en orden, y la editada a mano AddProductSlug rellenando Slug desde Name en filas que existían antes que la columna.
💡

El sample apunta a su propia base de datos (JorgenHocSamples_Migrations) precisamente porque los comandos de este artículo — database update 0, database drop --force — son destructivos. Apúntalos a una base de datos desechable, nunca a una compartida. dotnet ef migrations has-pending-model-changes devuelve "No changes" para el sample, que es como confirmas que una migración editada a mano sigue coincidiendo con el snapshot del modelo.

Lectura Relacionada

Las migraciones son una pieza del panorama más amplio de EF Core — la guía completa de EF Core cubre cómo encajan a su alrededor el modelo, el contexto y el pipeline de consultas.

Dos características del esquema interactúan con las migraciones de formas que conviene conocer antes de toparte con ellas: los filtros de consulta globales, que cambian lo que devuelven tus consultas sin cambiar el esquema, y las consultas SQL directas, que necesitarás cuando una migración requiera una transformación de datos que la API a nivel de modelo no puede expresar.

Lecturas adicionales

Sobre el autor

Jorge Calderón

Ingeniero de software con más de una década construyendo y operando aplicaciones .NET en producción — capas de datos con EF Core, servicios intensivos en async y despliegues en Azure y contenedores. Cada benchmark y proyecto de ejemplo de estas guías está publicado en un repositorio público de GitHub para que puedas reproducirlo.

Perfil de GitHubLinkedIn ↗Benchmarks y código de ejemplo

Artículos relacionados