# Je doet waarschijnlijk EF Migrations verkeerd...

Uitvoeren `MigrateAsync()` bij het opstarten? U geeft uw app database eigenaar rechten en hopen dat er niets mis gaat. Er is een betere manier - EF migratie bundels kunt u migraties uitvoeren als een gecontroleerde CI stap, het houden van uw productie app veilig. Maar hier is het ding: soms is de "verkeerde" manier is eigenlijk prima. Laten we verkennen wanneer om elke aanpak te gebruiken.

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

<!--category--  Entity Framework, Migrations, GitHub, CI -->
**Officiële documenten:** [Overzicht migraties](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/) | [Migratie toepassen](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying) | [Bundelsunit synonyms for matching user input](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying?tabs=dotnet-core-cli#bundles)

[TOC]

# De "verkeerde" manier (dat ik gebruik)

Deze blog gebruikt `MigrateAsync()` bij het opstarten - de aanpak die ik je ga vertellen om niet te gebruiken. Hier is waarom dat oké is voor mij, en waarom het waarschijnlijk niet voor jou is.

In mijn `Program.cs` bestand Ik heb het volgende:

```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) van toepassing in afwachting van migraties en maakt de database indien nodig. Simpel - maar problematisch:

1. **Opstartafhankelijkheid** Je app start niet.
2. **Beveiligingsovertreding** - Uw app heeft nodig `db_owner` Je gaf je runtime app de sleutels om tafels te laten vallen.

Waarom ik ermee weg kom: publieke data, een enkel Docker netwerk, persoonlijk project. **Dat kun je waarschijnlijk niet.**

## Wanneer Runtime migraties zijn prima

- **Lokale dev** - Snelle iteratie is beter dan ceremonie
- **Persoonlijke projecten** - Lage straal, geen gevoelige gegevens.
- **Docker-compose dev omgevingen** - Gemak wint
- **Prototypering** - Schema verandert toch constant.

## Als ze dat niet zijn.

- **Meerdere app-instances** - Raceomstandigheden in overvloed
- **Gevoelige gegevens** - PII, financieel, gereguleerd = juiste scheiding vereist
- **Productie met echte gebruikers** - Mislukte migratie = uitval

# De juiste manier: EF-bundels

Een EF-bundel is een zelfstandig uitvoerbaar bestand dat uw gecompileerde migraties bevat. `dotnet ef database update` verpakt in een standalone `.exe`.

**Waarom bundels winnen:**

- **Geen runtime afhankelijkheden** - Doel heeft geen SDK of EF CLI nodig
- **Juiste scheiding** - App heeft nooit nodig `db_owner`; alleen CI runner doet, alleen tijdens implementatie
- **Zichtbaarheid van CI** - Mislukkingen tonen in pijplijn logs, niet begraven in app opstarten
- **Veiligheid van de terugrol** - Migratie mislukt? Implementatie stopt voordat slechte code inzet
- **Idempotent** - Tracks wat wordt toegepast, draait alleen wat nodig is

> **Opmerking:** Voor de beveiliging van productiekwaliteit, gebruik [Beheerde identiteit](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) Maar bundels zijn nog steeds een belangrijke stap hoger dan runtime migraties.

## Voorbeeld van GitHub-acties

```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 }}
```

De bundel leest verbindingsstrings van omgevingsvariabelen en past in afwachting van migraties toe. Reeds toegepast? Het sluit gewoon succesvol af.

# Lokale bundels

Geen CI? Wilt u testen voordat u duwt? Bouw bundels lokaal.

**Gebruiks gevallen:** Test vóór CI, DBA-handoff (zelfingesloten exe, geen SDK nodig), ensceneringen, debuggen met `--verbose`.

## Een bundel aanmaken

```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
```

## Running Your Bundle

```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
```

## Nuttige bundelopties

```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
```

## Lokale testworkflow

```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
```

Vangst syntaxis fouten, beperkingen schendingen, FK problemen - alle *voor* CI of productie.

## Bouwprestaties

**Bundelgeneratie is traag** - 30+ seconden op grote projecten. Niet genereren op elk gebouw.

- Handmatig genereren bij lokaal testen
- Genereren in CI alleen tijdens implementatie, niet elke PR
- Cache bundels als migraties niet zijn veranderd

Als je echt auto-generatie wilt, voeg dan een MSBuild target toe:

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

Dan: `dotnet build -t:BuildMigrationBundle`

# Hybride aanpak

Beste van beide werelden: gemak lokaal, veiligheid in de productie.

```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
```

# Alternatieven voor bundels

## SQL-scripts

Genereer gewone SQL in plaats van een uitvoerbaar programma. Geweldig voor DBA-evaluatie en bestaande veranderingsbeheerprocessen.

```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
```

**Voordelen:** Volledige zichtbaarheid, elke SQL client kan uitvoeren, versie controle vriendelijk, DBA goedkeuring workflows.

**Nadelen:** Geen auto-tracking (gebruik `--idempotent`), handmatige uitvoering, potentiële drift als scripts worden gewijzigd.

Zie [officiële documenten over SQL-scripts](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying?tabs=dotnet-core-cli#sql-scripts).

### SQL Scripts in 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
```

## DACAC (alleen SQL-server)

[DACCAC's](https://learn.microsoft.com/en-us/sql/relational-databases/data-tier-applications/data-tier-applications) zijn *state-based* niet *migratie-gebaseerd*. U definieert het gewenste schema, en SqlPackage diffeert het met de doeldatabase.

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

**Voordelen:** Schema als code, auto-diff generatie, behandelt alles (tabellen, weergaven, SP's, indexen), enterprise tooling.

**Nadelen:** SQL Server alleen, schema op twee plaatsen (EF-modellen + SQL project), diff engine maakt twijfelachtige keuzes, kolomhernoemingen lijken op drop+add.

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

## Vergelijkingstabel

Aanpak Best Voor .NET vereist Auto-tracks Toegepast DBA Vriendelijk Cross-platform DB
|----------|----------|---------------|---------------------|--------------|-------------------|
| `MigrateAsync()` Dev/kleine projecten Ja (runtime) Ja
EF-bundels - CI/CD-pijpleidingen - Nee (zelfvoorzienend) - Ja -- Ja -- Ja -- Ja
SQL Scripts DBA-gecontroleerde omgevingen `--idempotent` Ja Ja Ja
DACAC SQL Server enterprise Nee Ja (state-based) Ja Nee

# Tips

## Het Designer-bestand Hebbes

Migratie werkt lokaal, maar niet in CI? **Controleer of je beide bestanden hebt vastgelegd:**

- `20231115_AddUserTable.cs` - De migratiecode.
- `20231115_AddUserTable.Designer.cs` - De model snapshot

Ontbreken van het Designer bestand = stil falen.

## Meerdere DbContexts

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

## Verbindingstekenreeksprioriteit

1. `--connection` argument
2. Omgevingsvariabele
3. `appsettings.json`

Gebruik omgevingsvariabelen in CI.

## IDesignTimeDbContextFactory

EF-tools moeten uw DbContext instantiëren. Als uw DbContext in een apart project zit of complexe opstart heeft, implementeren [`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);
    }
}
```

Gebruik wanneer: DbContext in apart project, complexe opstart, User Secrets nodig hebben voor ontwerp-tijd.

# Hoe zit het met...?

Veelgestelde vragen en terugval die ik heb ontvangen.

## "Waarom niet gewoon rennen `dotnet ef database update` in CI?"

Overdekt boven, maar de korte versie: bundels zijn draagbare artefacten. Uw implementatie stap hoeft niet EF CLI, broncode, of ontwerp-tijd resolutie. Dezelfde bundel draait in test, enscenering, en prod - nul drift.

## "Is dit niet overkill voor een kleine app?"

Als je alleen bent, is de data openbaar en is de straal laag... `MigrateAsync()` Maar zodra je een tweede ontwikkelaar, gevoelige gegevens of meerdere omgevingen toevoegt, betalen bundels voor zichzelf.

## "Hoe zit het met terugdraaien?"

EF doet geen automatische terugrol. Opties:

- Genereren a `Down()` migratie en voer het uit (maar je moet het hebben geschreven)
- Herstellen van back-up
- Schrijf een handmatige migratie om wijzigingen ongedaan te maken

Voor kritieke systemen: eerst de migratie testen op een databasekloon.

## "Kan ik migraties uitvoeren in een Kubernetes init container?"

Ja. Bundel + init container is een solide patroon:

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

App container wacht tot init klaar is.

## "Hoe zit het met FluentMigrator / DbUp / andere tools?"

Ze werken geweldig. EF bundels zijn de EF-native oplossing, maar [FluentMigrator](https://fluentmigrator.github.io/) en [DbUp](https://dbup.readthedocs.io/) belangrijk verschil: dat zijn migratie-specifieke tools, terwijl EF-bundels afkomstig zijn van uw bestaande EF-model.

## "Mijn DBA wil alle SQL bekijken voordat het draait"

Gebruik `--idempotent` scripts:

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

DBA beoordelingen en keurt het goed.

- Het script handmatig uitvoeren, of
- Eenmaal goedgekeurd, voer de bundel (die hetzelfde doet)

## "Hoe kan ik omgaan met migraties met nul stilstand?"

Dat is een inzet strategie vraag, geen migratie vraag.

1. Maak migraties achterstevoren compatibel (voeg kolommen nullable toe, hernoem niet)
2. Implementeer nieuwe code die zowel oude als nieuwe schema's behandelt
3. Migratie uitvoeren
4. Implementeer code die alleen nieuwe schema's gebruikt
5. Opruimen (oude kolommen in een latere migratie laten vallen)

Bundels lossen dit niet op - ze maken stap 3 gewoon voorspelbaarer.