# Probablemente estás haciendo mal las migraciones de EF...

Correr `MigrateAsync()` ¿Estás dando derechos de propietario de tu base de datos de aplicaciones y esperando que nada salga mal? Hay una mejor manera: los paquetes de migración de EF te permiten ejecutar migraciones como un paso de CI controlado, manteniendo segura tu aplicación de producción. Pero esto es lo siguiente: a veces la manera "equivocada" está realmente bien. Vamos a explorar cuándo usar cada enfoque.

<datetime class="hidden">2025-11-23T18:39</datetime>

<!--category--  Entity Framework, Migrations, GitHub, CI -->
**Documentos oficiales:** [Panorama general de las migraciones](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/) | [Aplicación de las migraciones](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying) | [Paquetes](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying?tabs=dotnet-core-cli#bundles)

[TOC]

# La manera "equivocada" (que yo uso)

Este blog utiliza `MigrateAsync()` En el inicio - el enfoque que estoy a punto de decirte que no uses. He aquí por qué eso está bien para mí, y por qué probablemente no es para ti.

En mi `Program.cs` Tengo lo siguiente:

```csharp
    using (var scope = app.Services.CreateScope())
    {
        var blogContext = scope.ServiceProvider.GetRequiredService<IMostlylucidDBContext>();
        await blogContext.Database.MigrateAsync();
    }
```

[`MigrateAsync()`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.relationaldatabasefacadeextensions.migrateasync) aplica las migraciones pendientes y crea la base de datos si es necesario. Simple - pero problemático:

1. **Dependencia de inicio** ¿La migración falla? Tu aplicación no arranca.
2. **Violación de la seguridad** - Tu app necesita `db_owner` Acabas de darle a tu aplicación de tiempo de ejecución las llaves para soltar las mesas.

Por qué me salgo con la mía: datos públicos, una sola red Docker, proyecto personal. **Probablemente no puedas.**

## Cuando las migraciones en tiempo de ejecución están bien

- **Desarrollo local** - La iteración rápida late ceremonia
- **Proyectos personales** - Radio de explosión bajo, sin datos sensibles
- **Entornos de dev compuestos por Docker** - Gana la conveniencia.
- **Prototipado** - El esquema está cambiando constantemente de todos modos.

## Cuando no lo son

- **Múltiples instancias de aplicación** - Las condiciones de carrera en abundancia
- **Datos sensibles** - IIP, financiera, regulada = se requiere una separación adecuada
- **Producción con usuarios reales** - Migración fallida = interrupción del servicio

# El camino correcto: paquetes de EF

Un paquete de EF es un ejecutable autónomo que contiene sus migraciones compiladas. `dotnet ef database update` empaquetado en una unidad independiente `.exe`.

**¿Por qué ganan los paquetes?**

- **Sin dependencias de tiempo de ejecución** - El objetivo no necesita SDK o EF CLI
- **Separación adecuada** - La aplicación nunca necesita `db_owner`; sólo el corredor CI lo hace, sólo durante el despliegue
- **Visibilidad de la CI** - Los fallos se muestran en los registros de tuberías, no enterrados en el inicio de la aplicación
- **Seguridad de la marcha atrás** El despliegue se detiene antes de que el código incorrecto se despliegue.
- **Idempotente** - Rastrea lo que se aplica, ejecuta sólo lo que se necesita

> **Nota:** Para la seguridad de la calidad de producción, utilizar [Identidad administrada](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) Pero los paquetes siguen siendo un gran paso adelante de las migraciones en tiempo de ejecución.

## Ejemplo de acciones de GitHub

```yaml
      - name: Install EF Core tools
        run: dotnet tool install --global dotnet-ef

      - name: Add EF tools to PATH
        run: echo "$HOME/.dotnet/tools" >> $GITHUB_PATH

      - name: Generate EF migration bundle
        run: |
          dotnet ef migrations bundle \
            --project ${{ env.WEB_PROJECT }} \
            --output efbundle.exe \
            --configuration ${{ env.BUILD_CONFIGURATION }} \
            --runtime ${{ env.RUNTIME_IDENTIFIER }} \
            --context AdminDbContext \
        env:
          AdminSite__ConnectionString: ${{ secrets.PROD_SQL_CONNECTIONSTRING }}

      - name: Run EF migration bundle
        run: |
          ./efbundle.exe
        env:
          AdminSite__ConnectionString: ${{ secrets.PROD_SQL_CONNECTIONSTRING }}
```

El paquete lee cadenas de conexión de variables de entorno y aplica migraciones pendientes. ¿Ya se ha aplicado? Sólo sale con éxito.

# Paquetes locales

¿Quieres probar antes de empujar? Construye paquetes localmente.

**Casos de uso:** Prueba antes de CI, entrega DBA (exe autónomo, no necesita SDK), despliegues de puesta en escena, depuración con `--verbose`.

## Crear un paquete

```bash
# Install EF CLI (once)
dotnet tool install --global dotnet-ef

# Basic bundle
dotnet ef migrations bundle \
    --project Mostlylucid.DbContext \
    --startup-project Mostlylucid \
    --output efbundle.exe

# Self-contained (includes runtime - portable to machines without .NET)
dotnet ef migrations bundle \
    --project Mostlylucid.DbContext \
    --startup-project Mostlylucid \
    --output efbundle.exe \
    --self-contained

# Cross-platform (e.g., build on Windows, deploy to Linux)
dotnet ef migrations bundle \
    --project Mostlylucid.DbContext \
    --startup-project Mostlylucid \
    --output efbundle \
    --runtime linux-x64
```

## Ejecutar su paquete

```bash
# Using default connection string from appsettings.json
./efbundle.exe

# Override with a specific connection string
./efbundle.exe --connection "Host=localhost;Database=mostlylucid;Username=postgres;Password=secret"

# Using an environment variable (matches your config key)
$env:ConnectionStrings__DefaultConnection="Host=localhost;..." # PowerShell
export ConnectionStrings__DefaultConnection="Host=localhost;..." # Bash
./efbundle.exe
```

## Opciones de paquetes útiles

```bash
# See what migrations would be applied without running them
./efbundle.exe --dry-run

# Verbose output for debugging
./efbundle.exe --verbose

# Apply migrations up to a specific migration (useful for testing)
./efbundle.exe --target-migration "20231115_AddUserTable"

# Combine options
./efbundle.exe --verbose --dry-run
```

## Flujo de trabajo local de pruebas

```bash
# 1. Create migration
dotnet ef migrations add AddNewFeature \
    --project Mostlylucid.DbContext \
    --startup-project Mostlylucid

# 2. Build bundle
dotnet ef migrations bundle \
    --project Mostlylucid.DbContext \
    --startup-project Mostlylucid \
    --output efbundle.exe

# 3. Dry run first
./efbundle.exe --dry-run --verbose

# 4. Run for real
./efbundle.exe --verbose

# 5. Broken? Remove and retry
dotnet ef migrations remove \
    --project Mostlylucid.DbContext \
    --startup-project Mostlylucid
```

Captura errores de sintaxis, violaciones de restricciones, problemas FK - todos *antes* CI o producción.

## Rendimiento de construcción

**La generación de paquetes es lenta** - 30+ segundos en grandes proyectos. No generar en cada construcción.

- Generar manualmente cuando se prueba localmente
- Generar en IC sólo durante el despliegue, no todas las relaciones públicas
- Paquetes de caché si las migraciones no han cambiado

Si realmente quieres auto-generación, agrega un objetivo de MSBuild:

```xml
<Target Name="BuildMigrationBundle">
  <Exec Command="dotnet ef migrations bundle --output $(OutputPath)efbundle.exe --force" />
</Target>
```

Entonces: `dotnet build -t:BuildMigrationBundle`

# Enfoque híbrido

Lo mejor de ambos mundos: comodidad local, seguridad en la producción.

```csharp
if (builder.Environment.IsDevelopment())
{
    using var scope = app.Services.CreateScope();
    var context = scope.ServiceProvider.GetRequiredService<IMostlylucidDBContext>();
    await context.Database.MigrateAsync();
}
// Production: CI pipeline runs the bundle
```

# Alternativas a los paquetes

## Scripts SQL

Generar SQL plano en lugar de un ejecutable. Ideal para la revisión de DBA y los procesos de gestión de cambios existentes.

```bash
# All migrations
dotnet ef migrations script --output migrations.sql

# Idempotent (safe to run multiple times) - USE THIS
dotnet ef migrations script --idempotent --output migrations.sql

# Range of migrations
dotnet ef migrations script FromMigration ToMigration --output migrations.sql
```

**Pros:** Visibilidad completa, cualquier cliente SQL puede ejecutarlo, control de versiones amigable, flujos de trabajo de aprobación DBA.

**Contras:** Sin seguimiento automático (utilizar `--idempotent`), ejecución manual, deriva potencial si los scripts se modifican.

Ver [Documentos oficiales en scripts SQL](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying?tabs=dotnet-core-cli#sql-scripts).

### Scripts SQL en CI

```yaml
- name: Generate and apply migrations
  run: |
    dotnet ef migrations script --idempotent --output migrations.sql
    # SQL Server
    sqlcmd -S ${{ secrets.DB_SERVER }} -d ${{ secrets.DB_NAME }} -i migrations.sql
    # Or PostgreSQL
    PGPASSWORD=${{ secrets.DB_PASSWORD }} psql -h ${{ secrets.DB_HOST }} -f migrations.sql
```

## CADPAC (sólo servidor SQL)

[CADPACs](https://learn.microsoft.com/en-us/sql/relational-databases/data-tier-applications/data-tier-applications) son *con sede en el Estado* no *sobre la base de la migración*. Usted define el esquema deseado, y SqlPackage lo diferencia contra la base de datos de destino.

```bash
SqlPackage.exe /Action:Publish /SourceFile:MyDatabase.dacpac /TargetConnectionString:"..."
```

**Pros:** Schema como código, generación de auto-diff, maneja todo (tablas, vistas, SPs, índices), herramientas empresariales.

**Contras:** SQL Server solamente, esquema en dos lugares (modelos EF + proyecto SQL), el motor diff hace opciones cuestionables, los renombres de columna se ven como drop+add.

Ver [SqlPackage docs](https://learn.microsoft.com/en-us/sql/tools/sqlpackage/sqlpackage).

## Cuadro comparativo

Aproximación  Mejor para  Requiere .NET  Auto-pistas Aplicadas  DBA Friendly  Cross-plataform DB
|----------|----------|---------------|---------------------|--------------|-------------------|
| `MigrateAsync()` # Dev/pequeños proyectos # # Sí (tiempo de ejecución) # # Sí # No # # Sí #
EF Bundles  CI/CD tuberías  No (self-contained)  Sí  Algo  Sí
SQL Scripts  ambientes controlados por DBA  No  Con `--idempotent` # Sí # # Sí # # Sí # # Sí # Sí # # Sí # Sí # # Sí # Sí # # Sí # # Sí # # Sí # Sí #  # Sí # Sí # Sí # Sí # Sí # Sí #
CADPAC  SQL Empresa de servidores  No  Sí (basado en el estado)  Sí  No

# Consejos

## El archivo de diseñador Gotcha

¿Las migraciones trabajan localmente pero no en CI? **Compruebe que ha comprometido ambos archivos:**

- `20231115_AddUserTable.cs` - Código de migración
- `20231115_AddUserTable.Designer.cs` - La instantánea del modelo

Falta el archivo Designer = fallo silencioso.

## Múltiples DbContextos

```bash
dotnet ef migrations bundle --context BlogDbContext --output blog-migrations.exe
dotnet ef migrations bundle --context IdentityDbContext --output identity-migrations.exe
```

## Prioridad de cadena de conexión

1. `--connection` argumento
2. Variable ambiental
3. `appsettings.json`

Usar variables de entorno en IC.

## IDesignTimeDbContextFactory

Las herramientas de EF necesitan instanciar su DbContext. Si su DbContext está en un proyecto separado o tiene un inicio complejo, implemente [`IDesignTimeDbContextFactory<T>`](https://learn.microsoft.com/en-us/ef/core/cli/dbcontext-creation?tabs=dotnet-core-cli#from-a-design-time-factory):

```csharp
public class AdminDbContextFactory : IDesignTimeDbContextFactory<AdminDbContext>
{
    public AdminDbContext CreateDbContext(string[] args)
    {
        var config = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", optional: true)
            .AddEnvironmentVariables()
            .AddUserSecrets<AdminDbContextFactory>()
            .Build();

        var connectionString = config["AdminSite:ConnectionString"]
            ?? throw new InvalidOperationException("Missing connection string");

        var optionsBuilder = new DbContextOptionsBuilder<AdminDbContext>();
        optionsBuilder.UseSqlServer(connectionString, sql => sql.CommandTimeout(120));

        return new AdminDbContext(optionsBuilder.Options);
    }
}
```

Utilice cuando: DbContext en un proyecto separado, inicio complejo, necesita secretos de usuario para el tiempo de diseño.

# ¿Qué hay de...?

Preguntas comunes y retroceso que he recibido.

## "¿Por qué no simplemente correr `dotnet ef database update` en CI?"

Cubierto arriba, pero la versión corta: los paquetes son artefactos portátiles. Su paso de implementación no necesita EF CLI, código fuente o resolución de tiempo de diseño. El mismo paquete se ejecuta en pruebas, estadificación y prod - deriva cero.

## "¿No es una exageración para una aplicación pequeña?"

Si estás solo, los datos son públicos, y el radio de explosión es bajo. `MigrateAsync()` Pero en el momento en que se agrega un segundo desarrollador, datos sensibles, o múltiples entornos, los paquetes pagan por sí mismos.

## "¿Qué hay de los retrocesos?"

EF no hace retrocesos automáticos. Opciones:

- Generar a `Down()` migración y ejecutarlo (pero tienes que haberlo escrito)
- Restaurar desde la copia de seguridad
- Escriba una migración manual para deshacer los cambios

Para sistemas críticos: probar primero las migraciones contra un clon de base de datos.

## "¿Puedo ejecutar migraciones en un contenedor de Kubernetes?"

Sí. Bundle + init contenedor es un patrón sólido:

```yaml
initContainers:
  - name: migrate
    image: myapp:latest
    command: ["./efbundle.exe"]
    env:
      - name: ConnectionStrings__Default
        valueFrom:
          secretKeyRef:
            name: db-secrets
            key: connection-string
```

El contenedor de aplicaciones espera a que se complete el init.

## "¿Qué hay de FluentMigrator / DbUp / otras herramientas?"

Funcionan muy bien. Los paquetes de EF son la solución nativa de EF, pero [FluentMigrator](https://fluentmigrator.github.io/) y [DbUp](https://dbup.readthedocs.io/) diferencia clave: son herramientas específicas para la migración, mientras que los paquetes de EF provienen de su modelo de EF existente.

## "Mi DBA quiere revisar todo SQL antes de que se ejecute"

Uso `--idempotent` scripts:

```bash
dotnet ef migrations script --idempotent --output migrations.sql
```

El DBA revisa y aprueba.

- Ejecute el script manualmente, o
- Una vez aprobado, ejecute el paquete (que hace lo mismo)

## "¿Cómo puedo manejar las migraciones con cero tiempo de inactividad?"

Esa es una pregunta de estrategia de despliegue, no una pregunta de migraciones. Generalmente:

1. Hacer las migraciones compatibles hacia atrás (añadir columnas anulables, no renombrar)
2. Implementar código nuevo que maneja tanto el esquema antiguo como el nuevo
3. Ejecutar migración
4. Implementar código que solo utilice el nuevo esquema
5. Limpiar (derivar columnas viejas en una migración posterior)

Los paquetes no resuelven esto, solo hacen el paso 3 más predecible.