# Du machst wahrscheinlich EF-Migrationen falsch...

Laufen `MigrateAsync()` Beim Start? Sie geben Ihrem App-Datenbank-Eigentümer Rechte und hoffen, dass nichts schief geht. Es gibt einen besseren Weg - EF-Migrationspakete lassen Sie Migrationen als kontrollierten CI-Schritt laufen, halten Sie Ihre Produktions-App sicher. Aber hier ist die Sache: manchmal ist der "falsche" Weg ist eigentlich in Ordnung. Lassen Sie uns erkunden, wann jeder Ansatz zu verwenden.

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

<!--category--  Entity Framework, Migrations, GitHub, CI -->
**Amtliche Dok.:** [Überblick über die Migration](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/) | [Anwenden von Migrationen](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying) | [Bündel](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying?tabs=dotnet-core-cli#bundles)

[TOC]

# Der "falsche" Weg (der ich verwende)

Dieser Blog verwendet `MigrateAsync()` beim Start - der Ansatz, den ich Ihnen sagen werde, nicht zu verwenden. Hier ist, warum das okay für mich ist, und warum es wahrscheinlich nicht für Sie ist.

In meinem `Program.cs` Datei Ich habe die folgenden:

```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) wendet anstehende Migrationen an und erstellt bei Bedarf die Datenbank. Einfach - aber problematisch:

1. **Gründungsabhängigkeit** - Datenbank runter? Migration fehlgeschlagen? Ihre App wird nicht starten.
2. **Sicherheitsverletzung** - Ihre App braucht `db_owner` Sie haben Ihrer Laufzeit-App gerade die Schlüssel gegeben, um Tische fallen zu lassen.

Warum ich damit durchkomme: öffentliche Daten, einzelnes Docker-Netzwerk, persönliches Projekt. **Das kannst du wahrscheinlich nicht.**

## Wenn Runtime-Migrationen in Ordnung sind

- **Örtliche Entwicklung** - Schnelle Iteration schlägt Zeremonie
- **Persönliche Projekte** - Niedriger Explosionsradius, keine sensiblen Daten
- **Docker-kompose-dev-Umgebungen** - Convenience gewinnt
- **Prototyping** - Schema verändert sich sowieso ständig

## Wenn sie es nicht sind

- **Mehrere App-Instanzen** - Rennen Bedingungen galore
- **Empfindliche Daten** - PII, finanziell, reguliert = richtige Trennung erforderlich
- **Produktion mit realen Anwendern** - fehlgeschlagene Migration = Ausfall

# Der richtige Weg: EF-Bündel

Ein EF-Bundle ist ein in sich geschlossenes Executable, das Ihre kompilierten Migrationen enthält. `dotnet ef database update` verpackt in ein Standalone `.exe`.

**Warum Bundles gewinnen:**

- **Keine Laufzeitabhängigkeiten** - Ziel braucht kein SDK oder EF CLI
- **Richtige Trennung** - App braucht nie `db_owner`; nur CI-Läufer, nur während des Einsatzes
- **CI-Sichtbarkeit** - Fehler zeigen in Pipeline-Logs, nicht begraben in App-Start
- **Rückfahrsicherheit** - Migration fehlschlägt? Deployment stoppt vor schlechten Code-Deployments
- **Unerschöpflich** - Verfolgt, was angewendet wird, läuft nur, was benötigt wird

> **Anmerkung:** Für die Sicherheit in der Produktion, Verwendung [Verwaltete Identität](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) Aber Bundles sind immer noch ein wichtiger Schritt nach oben von Runtime-Migrationen.

## Beispiel für GitHub-Aktionen

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

Das Bundle liest Verbindungsstrings aus Umgebungsvariablen und wendet anstehende Migrationen an. Bereits angewendet? Es beendet einfach erfolgreich.

# Lokale Bündel

Keine CI? Möchten Sie testen, bevor Sie drücken? Bauen Sie Bündel lokal.

**Anwendungsfälle:** Test vor CI, DBA-Abgabe (inklusive Exe, kein SDK erforderlich), Staging-Deploits, Debugging mit `--verbose`.

## Ein Bündel erstellen

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

## Ihr Bündel laufen lassen

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

## Nützliche Bundle-Optionen

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

## Lokales Testen des Workflows

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

Catches Syntaxfehler, Einschränkungen, FK-Probleme - alle *Vorher* KI oder Produktion.

## Leistung aufbauen

**Bundle-Generation ist langsam** - 30+ Sekunden bei großen Projekten. Nicht auf jedem Build generieren.

- Beim lokalen Testen manuell generieren
- Generieren Sie in CI nur während des Einsatzes, nicht jede PR
- Cache-Bündel, wenn sich Migrationen nicht geändert haben

Wenn Sie wirklich Auto-Generation wollen, fügen Sie ein MSBuild-Ziel hinzu:

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

Dann: `dotnet build -t:BuildMigrationBundle`

# Hybridansatz

Das Beste aus beiden Welten: Komfort vor Ort, Sicherheit in der Produktion.

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

# Alternativen zu Bündeln

## SQL-Skripte

Generieren Sie einfache SQL anstelle einer ausführbaren Datei. Ideal für DBA-Review- und bestehende Change-Management-Prozesse.

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

**Vors.:** Volle Sichtbarkeit, jeder SQL-Client kann es ausführen, Versionskontrolle freundlich, DBA-Zulassung Workflows.

**Nachteile:** Keine Auto-Tracking (Verwendung) `--idempotent`), manuelle Ausführung, potenzielle Drift, wenn Skripte modifiziert werden.

Siehe [offizielle docs auf SQL-Skripten](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
```

## DACPAC (nur SQL-Server)

[DACPACs](https://learn.microsoft.com/en-us/sql/relational-databases/data-tier-applications/data-tier-applications) sie werden *Staatlich* nicht *Migrationsbasiert*. Sie definieren das gewünschte Schema und SqlPackage unterscheidet es gegen die Zieldatenbank.

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

**Vors.:** Schema als Code, Auto-Diff-Generierung, behandelt alles (Tabellen, Ansichten, SPs, Indizes), Enterprise Tooling.

**Nachteile:** Nur SQL Server, Schema an zwei Stellen (EF-Modelle + SQL-Projekt), diff-Engine macht fragwürdige Entscheidungen, Spaltenumbenennungen sehen aus wie drop+add.

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

## Vergleichstabelle

.NET .NET .NET .NET .NET .NET . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
|----------|----------|---------------|---------------------|--------------|-------------------|
| `MigrateAsync()` Dev/kleine Projekte Ja (Laufzeit) Ja Nein Ja
EF-Bundles-CI/CD-Pipelines-Nein (in sich geschlossen)
SQL-Skripte, DBA-gesteuerte Umgebungen, Nein `--idempotent` Ja, ja, ja, ja, ja, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein.
DACPAC-SQL-Server-Unternehmen Nein Ja (state-based)

# Tipps

## Die Designer-Datei Gotcha

Migrationen arbeiten lokal, aber nicht in CI? **Überprüfen Sie, ob Sie beide Dateien übergeben haben:**

- `20231115_AddUserTable.cs` - Der Migrationscode
- `20231115_AddUserTable.Designer.cs` - Der Modell-Snapshot

Fehlende Designer-Datei = stiller Fehler.

## Mehrere DbContexte

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

## Verbindung String Priorität

1. `--connection` Argument
2. Umweltvariable
3. `appsettings.json`

Verwenden Sie Umgebungsvariablen in CI.

## IDesignTimeDbContextFactory

EF-Tools müssen Ihren DbContext instantiieren. Wenn Ihr DbContext in einem separaten Projekt ist oder komplexes Starten hat, implementieren [`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);
    }
}
```

Verwenden Sie wann: DbContext in separatem Projekt, komplexes Starten, benötigen User Secrets für Design-Zeit.

# Was ist mit...?

Häufige Fragen und Pushback habe ich erhalten.

## "Warum nicht einfach rennen `dotnet ef database update` in CI?"

Covered oben, aber die kurze Version: Bundles sind tragbare Artefakte. Ihr Bereitstellungsschritt braucht nicht EF CLI, Quellcode oder Design-Zeitauflösung. Gleiche Bundle läuft in Test, Inszenierung und prod - Null Drift.

## "Ist das nicht Overkill für eine kleine App?"

Wenn du allein bist, sind die Daten öffentlich, und der Explosionsradius ist niedrig. `MigrateAsync()` Aber sobald Sie einen zweiten Entwickler, sensible Daten oder mehrere Umgebungen hinzufügen, zahlen Bundles für sich selbst.

## "Was ist mit Rollbacks?"

EF macht keine automatischen Rollbacks. Optionen:

- Erzeugen eines `Down()` Migration und laufen Sie es (aber Sie müssen es geschrieben haben)
- Wiederherstellen aus der Sicherung
- Schreiben Sie eine manuelle Migration auf Änderungen rückgängig machen

Für kritische Systeme: Test-Migrationen gegen einen Datenbank-Klon zuerst.

## "Kann ich Migrationen in einem Kubernetes-Init-Container ausführen?"

Ja. Bundle + init Behälter ist ein solides Muster:

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

App-Container wartet darauf, dass init abgeschlossen wird.

## "Was ist mit FluentMigrator / DbUp / anderen Tools?"

Sie funktionieren gut. EF-Bündel sind die EF-native Lösung, aber [FluentMigrator](https://fluentmigrator.github.io/) und [DbUp](https://dbup.readthedocs.io/) haben ihre Ventilatoren. Hauptunterschied: Das sind migrationsspezifische Werkzeuge, während EF-Bündel aus Ihrem bestehenden EF-Modell stammen.

## "Mein DBA will alle SQL überprüfen, bevor es läuft"

Verwendung `--idempotent` Skripte:

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

DBA prüft und genehmigt. Dann entweder:

- Führen Sie das Skript manuell aus oder
- Einmal genehmigt, führen Sie das Bundle (das das gleiche tut)

## "Wie behandle ich Migrationen mit null Ausfallzeiten?"

Das ist eine Einführungsstrategie-Frage, keine Migrationsfrage. Im Allgemeinen:

1. Migrationen rückwärtskompatibel machen (Spalten hinzufügen, nicht umbenennen)
2. Neuen Code bereitstellen, der sowohl altes als auch neues Schema behandelt
3. Migration ausführen
4. Code bereitstellen, der nur neues Schema verwendet
5. Aufräumen (Alte Spalten in einer späteren Migration ablegen)

Bundles lösen das nicht - sie machen einfach Schritt 3 vorhersehbarer.