Zum Inhalt springen
← Alle Beiträge
· 9 Min. Lesezeit· Von

.NET 8 auf .NET 10 migrieren: Breaking Changes, EF Core und Docker

Von net8.0 auf net10.0: TFM, EF-Core-Migrationen, das neue Ubuntu-Basis-Image und Breaking Changes in ASP.NET Core 10 — ein durchgehendes Beispiel.

.NET.NET 10EF CoreDockerASP.NET CoreMigrationGitHub ActionsDevOps

Dieser Artikel setzt voraus, dass Sie die Entscheidung getroffen haben: .NET 10 LTS. Wenn Sie noch im „Warum jetzt"-Stadium sind, lesen Sie zuerst den Überblick .NET 8 und .NET 9 laufen im November 2026 aus. Hier geht es um das Wie: net8.0 → net10.0, an einer konkreten Solution — Minimal API mit EF Core 10 und SQL Server, ein BackgroundService, Multi-Stage-Dockerfile, Traefik als Reverse Proxy, GitHub Actions als CI. Die Schrittfolge richtet sich nach der Reihenfolge, in der Fehler auftreten.

Das Beispiel: eine Solution, zwei Projekte

Die Solution enthält zwei Projekte, die denselben Upgrade-Pfad durchlaufen:

  • Api — Minimal API mit EF Core 10, SQL Server als Datenbankbackend, hinter Traefik
  • Worker — BackgroundService mit PeriodicTimer, liest dieselbe Datenbank
+-------------------+     +--------------------+
|  Browser / Client |     |  Worker-Container  |
+--------+----------+     +--------+-----------+
         |                         |
    +----+------+            +-----+------+
    |  Traefik  |            |  SQL Server|
    |   :443    |            |   :1433    |
    +----+------+            +-----+------+
         |                         ^
  +------+---------+               |
  |  Api-Container  +---EF Core ---+
  |  :8080 intern  |
  +----------------+

TFM und SDK: der Wechsel, der alles in Bewegung setzt

Die einzig zwingende Änderung ist der Tausch des Target Framework Monikers. Bei einer Multi-Projekt-Solution empfiehlt sich die zentrale Steuerung über Directory.Build.props im Solution-Root:

<Project>
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

Mit dem TFM-Wechsel springt die Sprachversion automatisch von C# 12 auf C# 14. Das ist in den meisten Codebasen unsichtbar — außer wenn ein Property-Accessor einen Bezeichner namens field enthält. field ist in C# 14 das Schlüsselwort für das synthetisierte Backing Field; eine lokale Variable dieses Namens erzeugt jetzt:

error CS9272: 'field' is a keyword within a property accessor.
Rename the variable or use the identifier '@field' instead.

Umbenennen oder @field löst das Problem in einer Zeile.

NETSDK1045 — wenn das SDK nicht mitzieht

Wer den TFM-Wechsel vornimmt, ohne das SDK zu aktualisieren, sieht unmittelbar:

NETSDK1045: The current .NET SDK does not support 'net10.0' as a target.
Either target 'net8.0' or lower, or use a version of the .NET SDK that supports 'net10.0'.

Die häufigste Ursache: global.json pinnt noch auf eine 8.x-Version. dotnet --list-sdks zeigt, was installiert ist. Nach der SDK-Installation:

{
  "sdk": {
    "version": "10.0.100",
    "rollForward": "latestFeature"
  }
}

rollForward: latestFeature erlaubt neuere Feature-Bänder (10.0.200 usw.) ohne die Versionspinnung aufzugeben.

Direkt 8 → 10, nicht über 9 — und warum nicht auf .NET 11 warten

.NET 8 und .NET 9 laufen am 10. November 2026 am selben Tag aus. Ein Zwischenstop bei .NET 9 bedeutet eine zweite Migration innerhalb von Wochen, ohne jeglichen zusätzlichen Support-Zeitraum. .NET 11 abzuwarten ist keine stabilere Wahl: Es ist als STS nur zwei Jahre unterstützt und erscheint erst am 10.11.2026 — also genau am EOL-Tag von .NET 8, und mit kürzerer Laufzeit als .NET 10 LTS (bis November 2028). Der dritte Grund ist EF Core: „EF10 will not run on earlier .NET versions" — EF Core 10 setzt zwingend .NET 10 voraus. Wer EF Core 9 auf net8.0 nutzt, macht das Upgrade vollständig jetzt, mit .NET 10.

Pakete, NuGet-Audit und eine neue stille Fehlerquelle

dotnet list package --outdated

Alle Microsoft.AspNetCore.*, Microsoft.EntityFrameworkCore.*, Microsoft.Extensions.* und System.Net.Http.Json auf 10.0.x heben. Die EF-Core-Tools müssen denselben Major-Version-Stand haben:

dotnet tool update --global dotnet-ef

Ab net10.0 als TFM schaltet NuGet den Audit-Modus auf all um — transitive Pakete werden auf Schwachstellen geprüft. Bei TreatWarningsAsErrors=true bricht das sofort den Build. Die sauberere Lösung gegenüber einem Downgrade auf NuGetAuditMode=direct ist die gezielte Ausnahme, kombiniert mit dotnet nuget why <Paketname> zur Analyse der Abhängigkeitskette:

<PropertyGroup>
  <WarningsNotAsErrors>
    NU1901;NU1902;NU1903;NU1904;$(WarningsNotAsErrors)
  </WarningsNotAsErrors>
</PropertyGroup>

System.Linq.Async herausnehmen

System.Linq.AsyncEnumerable ist ab .NET 10 Teil der Standardbibliothek. Wer das Community-Paket System.Linq.Async bisher direkt referenziert hat, bekommt beim Kompilieren Mehrdeutigkeitsfehler auf LINQ-Methoden für IAsyncEnumerable<T>. Die Paket-Referenz aus der csproj entfernen; bei transitiver Nutzung durch ein anderes Paket <ExcludeAssets>compile</ExcludeAssets> setzen.

EF Core 10: Migrationen sauber halten

Was PendingModelChangesWarning beim Upgrade bedeutet

Das Paket-Upgrade ändert den internen Model-Snapshot von EF Core. Ab EF Core 9 wirft MigrateAsync() eine Exception, sobald das Laufzeitmodell von der letzten gespeicherten Migration abweicht:

The model for context 'AppDbContext' has pending changes. Add a new migration
before updating the database. This exception can be suppressed or logged by
passing event ID 'RelationalEventId.PendingModelChangesWarning' to the
'ConfigureWarnings' method in 'DbContext.OnConfiguring' or 'AddDbContext'.

Fix: nach dem Paket-Upgrade eine neue (ggf. inhaltlich leere) Migration erzeugen, bevor Sie deployen. Als CI-Gate eignet sich dotnet ef migrations has-pending-model-changes — gibt er mit Exit-Code 1 zurück, fehlt eine Migration und der Build bricht korrekt ab.

Migrations-Bundle statt Migrate() beim Container-Start

Migrate() beim App-Start ist möglich, hat aber laut EF-Core-Dokumentation dokumentierte Vorbehalte: DDL-Rechte für die Anwendung, kein SQL-Review, kein Rollback. Das Migrations-Bundle als einmaliger Compose-Job ist die sauberere Wahl:

dotnet ef migrations bundle --self-contained -r linux-x64 \
  --project src/Api --startup-project src/Api \
  --output ./docker/efbundle

Im Compose-Stack läuft das Bundle als kurzlebiger Service; die API-Container warten via condition: service_completed_successfully:

services:
  db-migrate:
    image: my-app:${VERSION}
    entrypoint: /app/efbundle
    environment:
      ASPNETCORE_ENVIRONMENT: Production
      ConnectionStrings__Default: "${DB_CONNECTION}"
    depends_on:
      db:
        condition: service_healthy
    restart: "no"

  api:
    image: my-app:${VERSION}
    depends_on:
      db-migrate:
        condition: service_completed_successfully
    stop_grace_period: 35s

Der seit EF Core 9 eingebaute Migrations-Lock verhindert parallele Ausführung durch mehrere Replicas. Wie depends_on-Bedingungen und Healthchecks im Stack korrekt verdrahtet werden, zeigt Docker HEALTHCHECK und restart_policy.

Das Dockerfile nach dem Basiswechsel

Die Änderung am Dockerfile besteht aus zwei FROM-Zeilen. Was sich dahinter ändert, ist die Basis-Distribution — und das hat messbare Konsequenzen.

Zwei Nächte Docker: Debian raus, Ubuntu rein — Zahlen vom VPS

.NET 8 nutzte Debian 12 als Standard-Basis. .NET 10 nutzt Ubuntu 24.04 (Noble Numbat) — Debian-Images werden für .NET 10 nicht mehr ausgeliefert. Gemessen am 24.09.2026 auf einem arm64-VPS (Docker Engine 29.8.0, kein lokaler Cache) per docker images nach docker pull:

Image Basis-OS Größe entpackt Pull-Dauer Runtime
aspnet:8.0 Debian 12 249 MB 7,4 s 8.0.31
aspnet:10.0 Ubuntu 24.04 262 MB 7,7 s 10.0.12
aspnet:10.0-noble-chiseled Ubuntu 24.04 (kein Shell) 131 MB 3,6 s 10.0.12

Der direkte Wechsel von aspnet:8.0 auf aspnet:10.0 macht das Runtime-Image 5 % größer (249 → 262 MB) — der Distro-Wechsel von Debian nach Ubuntu erklärt das. Erst aspnet:10.0-noble-chiseled halbiert die Größe auf 131 MB bei identischer Runtime-Version (10.0.12 auf beiden). Wie das Multi-Stage-Dockerfile aufgebaut ist, beschreibt Multi-Stage-Dockerfile für .NET und Node.js.

aspnet:10.0-noble-chiseled: halb so groß, aber ohne Shell

Chiseled-Images laufen per Default als Non-Root-User (UID 1654), enthalten keinen Paketmanager und keine Shell. Die konkreten Einschränkungen:

  • HEALTHCHECK CMD curl ... und CMD sh -c ... funktionieren nicht; die Prüfung muss über den Reverse Proxy (Traefik-Healthcheck) oder einen im Image vorhandenen Prüfer erfolgen.
  • Standardmäßig keine ICU-Bibliotheken: kultursensitive String-Operationen schlagen fehl. Abhilfe: aspnet:10.0-noble-chiseled-extra (enthält ICU und tzdata) oder DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1.

Für Apps hinter Traefik ohne Shell-basierte Runtime-Checks ist chiseled die empfehlenswerte Wahl. Unabhängig vom Image-Typ gilt: Der Generic Host empfängt SIGTERM korrekt (HostOptions.ShutdownTimeout Default: 30 Sekunden). Da Docker laut Docker-Dokumentation nach docker stop per Default nach 10 Sekunden per SIGKILL beendet, gehört stop_grace_period: 35s in die Compose-Konfiguration.

ASP.NET Core 10: drei Breaking Changes aus dem laufenden Betrieb

Forwarded Headers und der Log-Eintrag „Unknown proxy"

Seit ASP.NET Core 8.0.17 verwirft die Forwarded-Headers-Middleware X-Forwarded-*-Header von nicht konfigurierten Proxys. Ohne eingetragene Proxy-IP findet sich im Log Unknown proxy: 10.0.0.100:54321; HTTPS-Redirects und Auth-Prüfungen laufen dann ins Leere. In .NET 10 sind IPNetwork und KnownNetworks als ASPDEPR005 obsolet; die neuen Typen sind System.Net.IPNetwork und KnownIPNetworks:

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders =
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    options.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
    // Traefik-Docker-Subnetz als IPNetwork:
    options.KnownIPNetworks.Add(
        new System.Net.IPNetwork(IPAddress.Parse("172.16.0.0"), 12));
});
app.UseForwardedHeaders(); // vor UseHsts()

Cookie-Auth liefert jetzt 401 statt 302

[ApiController]-Endpunkte und Minimal APIs mit JSON-Response liefern bei fehlender Authentifizierung ab .NET 10 401/403 statt Redirect auf die Login-URL. Für eine reine Web-API ist das das korrekte Verhalten; SPAs bemerken die Änderung in der Regel nicht. Wer für einzelne Endpunkte das Redirect-Verhalten behalten möchte:

app.MapGet("/dashboard", () => Results.Ok())
   .RequireAuthorization()
   .AllowCookieRedirect();

WithOpenApi ist deprecated

Das neue Web-API-Template nutzt AddOpenApi()/MapOpenApi() und liefert das Dokument unter /openapi/v1.json (ab .NET 10 im Format OpenAPI 3.1). WithOpenApi() ist als ASPDEPR002 deprecated. Wer Operationen anpassen möchte:

app.MapGet("/products", GetProducts)
   .AddOpenApiOperationTransformer((op, ctx, ct) => {
       op.Summary = "Alle Produkte";
       return Task.CompletedTask;
   });

Swagger UI funktioniert weiterhin über Swashbuckle.AspNetCore.SwaggerUi mit Endpunkt /openapi/v1.json.

CI mit GitHub Actions

actions/setup-dotnet@v6 unterstützt dotnet-version: '10.0.x' und global-json-file. Für die Übergangsphase bietet sich eine kurzzeitige Matrix an:

strategy:
  matrix:
    dotnet: ['8.0.x', '10.0.x']
steps:
  - uses: actions/setup-dotnet@v6
    with:
      dotnet-version: ${{ matrix.dotnet }}
  - run: dotnet restore
  - run: dotnet build --configuration Release --no-restore
  - run: dotnet test --no-restore

MTP beim Upgrade nicht aktivieren

.NET 10 unterstützt neben VSTest auch Microsoft Testing Platform (MTP), aktivierbar über global.json. Die Umstellung gilt repoweit — kein einziges Testprojekt darf danach noch VSTest nutzen. Während die 8/10-Übergangsmatrix noch läuft, ist MTP nicht zu aktivieren: Testprojekte, die noch auf net8.0 zeigen, würden fehlschlagen. MTP ist eine separate Migration nach dem abgeschlossenen TFM-Wechsel. Wie GitHub Actions, GHCR-Push und der Build-Workflow strukturiert sind, beschreibt GitHub Actions CI für .NET und GHCR.

Fragen aus dem Migrations-Alltag

Muss ich gleichzeitig auf EF Core 10 wechseln? Nein. EF Core 9 läuft auf net10.0. EF Core 10 läuft jedoch nicht auf früheren .NET-Versionen. Die Empfehlung ist trotzdem, beides zusammen zu machen: EF Core 9 hat denselben EOL-Termin (10.11.2026), und EF Core 10 bringt benannte Query-Filter, vereinfachten ExecuteUpdateAsync-Syntax und LeftJoin/RightJoin als echte LINQ-Operatoren.

Was passiert, wenn ASPNETCORE_ENVIRONMENT vor dem Bundle-Build fehlt? Das Bundle wird mit Development-Defaults gebaut — falscher Connection String, falsche Feature-Flags. Die Umgebungsvariable muss sowohl beim dotnet ef migrations bundle-Befehl als auch beim späteren Ausführen des Bundles explizit gesetzt sein.

Das chiseled-Image wirft beim Start eine Globalisierungsausnahme. Chiseled-Images enthalten keine ICU-Bibliotheken. Kultursensitive Zeichenkettenvergleiche und bestimmte Kalender-Operationen schlagen fehl. Abhilfe: aspnet:10.0-noble-chiseled-extra verwenden (enthält ICU und tzdata) oder DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 setzen, sofern die App keine kulturabhängigen Vergleiche benötigt.

EF-Core-Tools bei Multi-Targeting: warum erscheint ein Fehler? Ab EF Core 10 verlangen die Tools bei <TargetFrameworks> (Plural, Multi-Targeting) die explizite Angabe des Ziel-Frameworks: dotnet ef migrations add ... --framework net10.0. Ohne den Flag erscheint: The project targets multiple frameworks. Use the --framework option to specify which target framework to use.

Native AOT — lohnt sich das im Rahmen dieses Upgrades? Nicht für eine EF-Core-lastige Web-API. EF Core stuft seine AOT-Unterstützung selbst als „highly experimental" ein; klassisches MVC wird von AOT nicht unterstützt. Native AOT ist ein separates Vorhaben nach dem abgeschlossenen TFM-Wechsel, das ein eigenes Template (dotnet new webapiaot) und einen AOT-kompatiblen Datenzugriffsweg erfordert.

Offizielle Quellen und nächste Schritte

Direkte Einstiegspunkte bei Microsoft: Upgrade auf eine neue .NET-Version · ASP.NET Core 9→10 Migration Guide · EF Core 10 Breaking Changes · Migrations-Bundles · Container-Images: Ubuntu als neue Basis.

Wenn Sie Unterstützung beim Upgrade, bei der Überprüfung von Breaking Changes oder bei der Anpassung Ihrer CI/CD-Pipeline benötigen, finden Sie weitere Informationen unter .NET-Entwicklung.

Hinweis: Die Beiträge dieses Blogs werden unter Einsatz von KI erstellt und vor der Veröffentlichung redaktionell geprüft. Die redaktionelle Verantwortung trägt Emre Yurtbay (siehe Impressum).

Projekt besprechen