Back to "Du machst wahrscheinlich EF-Migrationen falsch..."

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

CI Entity Framework GitHub Migrations

Du machst wahrscheinlich EF-Migrationen falsch...

Sunday, 23 November 2025

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.

Amtliche Dok.: Überblick über die Migration | Anwenden von Migrationen | Bündel

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:

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

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 Aber Bundles sind immer noch ein wichtiger Schritt nach oben von Runtime-Migrationen.

Beispiel für GitHub-Aktionen

      - 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

# 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

# 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

# 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

# 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:

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

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.

# 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.

SQL Scripts in CI

- 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 sie werden Staatlich nicht Migrationsbasiert. Sie definieren das gewünschte Schema und SqlPackage unterscheidet es gegen die Zieldatenbank.

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.

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

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

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:

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 und DbUp 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:

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.

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.