Was ist ein Compiled Model in EF Core 11, und wann lohnt sich die Aktivierung?
Ein Compiled Model ist generierter C#-Code, der Ihr EF Core Modell ohne Ausführung von OnModelCreating neu aufbaut. Gemessen mit EF Core 11 RC1: Das Laden des Modells sinkt bei 500 Entitäten von 714 ms auf 215 ms, aber der MSBuild-Weg (der EFOptimizeContext ersetzt hat) ist langsamer, veraltete Modelle schlagen unbemerkt fehl und Query Filter verhindern die Generierung.
Kurze Antwort: Ein Compiled Model ist C#-Quellcode, erzeugt von dotnet ef dbcontext optimize (oder vom MSBuild-Paket Microsoft.EntityFrameworkCore.Tasks), der Ihr EF Core Modell direkt aufbaut, statt bei der ersten Verwendung von DbContext die Konventionen und OnModelCreating auszuführen. Die Aktivierung lohnt sich, wenn die erste Abfrage in einem frischen Prozess echtes Geld oder Latenz kostet (Serverless, automatisch skalierte Container, CLI-Tools, Desktop-Apps) und Ihr Modell etwa 100 Entitätstypen oder mehr hat, oder wenn Sie mit Native AOT veröffentlichen, wo es Pflicht ist. In EF Core 11 gibt es die Eigenschaft EFOptimizeContext nicht mehr: Sie aktivieren das Feature stattdessen mit EFScaffoldModelStage. Für eine Web-API mit 20 Tabellen, die einmal pro Woche startet, lohnt sich der Wartungsaufwand nicht.
Jede Zahl und jede Fehlermeldung unten stammt aus Läufen mit Microsoft.EntityFrameworkCore.Sqlite 11.0.0-rc.1.26425.128 und dotnet-ef 11.0.0-rc.1.26425.128 auf dem .NET 11 RC1 SDK (11.0.100-rc.1.26425.128), Release-Builds, auf einem MacBook mit Apple Silicon. Ein Gegencheck verwendete EF Core 10.0.12 mit SDK 10.0.302.
Was EF Core bei der ersten Verwendung von DbContext tut
Das Erzeugen eines DbContext ist billig. Der teure Teil passiert, sobald zum ersten Mal etwas auf DbContext.Model zugreift: eine Abfrage, Add, SaveChanges oder Ihr eigenes Lesen von Model. Dann führt EF Core seine Konventions-Pipeline über jeden erreichbaren Entitätstyp aus, entdeckt Eigenschaften und Beziehungen per Reflection, wendet Ihre OnModelCreating-Konfiguration an, validiert das Ergebnis und wandelt das veränderliche Modell schließlich in ein schreibgeschütztes RuntimeModel um. Das Ergebnis wird pro Kontexttyp (genauer: pro Model Cache Key) für die Lebensdauer des Prozesses zwischengespeichert, Sie zahlen also einmal pro Prozess.
Ein Compiled Model überspringt die Pipeline. Statt das Modell zu entdecken, instanziiert EF Core generierte Klassen, die RuntimeModel.AddEntityType, AddProperty, AddKey, AddForeignKey und so weiter mit den bereits aufgelösten Antworten aufrufen. Hier ein Ausschnitt dessen, was dotnet ef dbcontext optimize in meinem Testprojekt für eine Entität erzeugt hat:
// <auto-generated /> by dotnet-ef 11.0.0-rc.1.26425.128
var runtimeEntityType = model.AddEntityType(
"Bench.Entity1",
typeof(Entity1),
baseEntityType,
propertyCount: 8,
navigationCount: 1,
foreignKeyCount: 1,
unnamedIndexCount: 2,
keyCount: 1);
var name = runtimeEntityType.AddProperty(
"Name",
typeof(string),
propertyInfo: typeof(Entity1).GetProperty("Name", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(Entity1).GetField("<Name>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
maxLength: 200);
Das maxLength: 200 stammt aus HasMaxLength(200) in OnModelCreating. Diese Konfiguration ist jetzt fest im generierten Code verankert, was der ganze Sinn der Sache ist und zugleich die Quelle aller späteren Stolperfallen in diesem Beitrag.
Der Befehl schreibt pro Entitätstyp eine Datei <Entity>EntityType.cs sowie drei weitere: <Context>Model.cs (eine RuntimeModel-Unterklasse mit statischer Instance), <Context>ModelBuilder.cs und <Context>AssemblyAttributes.cs. Die letzte enthält die Zeile, durch die EF Core das Modell ohne jede Codeänderung auf Ihrer Seite findet:
[assembly: DbContextModel(typeof(BenchContext), typeof(BenchContextModel), ProviderName = "Microsoft.EntityFrameworkCore.Sqlite")]
Seit EF Core 9 wird ein Compiled Model in derselben Assembly wie der Kontext über dieses Attribut automatisch gefunden. optionsBuilder.UseModel(BenchContextModel.Instance) brauchen Sie nur, wenn das Compiled Model in einer anderen Assembly liegt oder wenn Sie zur Laufzeit zwischen mehreren Compiled Models wählen wollen.
Messung: Was es Ihnen bringt
Die Microsoft-Learn-Seite zu Compiled Models sagt, sie helfen “applications with large models”, also “hundreds to thousands of entity types”. Das ist zu vage für eine Entscheidung, also habe ich gemessen. Ein Python-Skript hat Modelle mit 10, 100 und 500 Entitätstypen erzeugt. Jede Entität hat acht skalare Eigenschaften, einen nullbaren Fremdschlüssel auf die vorherige Entität, einen eindeutigen Index und einen HasMaxLength-Aufruf, damit die Konventionen echte Arbeit haben. Das Programm misst den ersten Modellzugriff und die erste Abfrage in einem frischen Prozess:
// .NET 11, C# 14, EF Core 11.0.0-rc.1.26425.128, Microsoft.EntityFrameworkCore.Sqlite
using System.Diagnostics;
using Bench;
using Microsoft.EntityFrameworkCore;
var sw = Stopwatch.StartNew();
using var ctx = new BenchContext();
var model = ctx.Model; // first touch: build or load the model
var modelMs = sw.Elapsed.TotalMilliseconds;
var n = ctx.Set<Entity1>().Where(e => e.IsActive).Count(); // first query
var firstQueryMs = sw.Elapsed.TotalMilliseconds;
Console.WriteLine($"{model.GetType().Name},{model.GetEntityTypes().Count()}," +
$"model={modelMs:F0}ms,firstQuery={firstQueryMs:F0}ms," +
$"OnModelCreating={BenchContext.ModelCreatingCalls}");
BenchContext.ModelCreatingCalls ist ein statischer Zähler, der in OnModelCreating hochgezählt wird, sodass jeder Lauf beweist, welchen Weg er genommen hat. Die Datenbank war eine vorab erstellte SQLite-Datei. Mediane aus sieben Kaltstarts pro Zeile:
| Entitätstypen | Modell laden, zur Laufzeit gebaut | Modell laden, kompiliert | Erste Abfrage, Laufzeit | Erste Abfrage, kompiliert | Assembly-Größe, Laufzeit / kompiliert |
|---|---|---|---|---|---|
| 10 | 185 ms | 65 ms | 277 ms | 171 ms | 23 KB / 43 KB |
| 100 | 280 ms | 92 ms | 383 ms | 213 ms | 158 KB / 342 KB |
| 500 | 714 ms | 215 ms | 868 ms | 401 ms | 888 KB / 1,8 MB |
Zwei Dinge fallen auf. Erstens ist die Ersparnis schon bei 10 Entitätstypen real, rund 110 ms, weil ein guter Teil der Laufzeitkosten im JIT-Kompilieren der Konventions-Pipeline selbst steckt und nicht nur in ihrer Ausführung pro Entität. Zweitens skaliert sie: Bei 500 Entitätstypen spart das Compiled Model bei jedem Kaltstart eine halbe Sekunde. OnModelCreating wurde in jedem kompilierten Lauf null Mal und in jedem Laufzeitlauf genau einmal aufgerufen.
Ob 110 ms eine Rolle spielen, ist eine Produktfrage. Bei einer ASP.NET Core App hinter einem Load Balancer, die sich aufwärmt, bevor sie Datenverkehr annimmt, spielt es keine Rolle. Bei AWS Lambda oder einem Azure-Functions-Verbrauchsplan, wo der Kaltstart für Benutzer sichtbar ist, bei einem dotnet tool, das zwei Sekunden läuft, und bei einer Desktop- oder MAUI-App, deren erster Bildschirm auf eine Abfrage wartet, schon.
Wohin EFOptimizeContext in EF Core 11 verschwunden ist
EF Core 9 lieferte im Paket Microsoft.EntityFrameworkCore.Tasks eine MSBuild-Integration mit, die das Compiled Model beim Build oder Publish neu generiert, sodass es nicht vom Code abdriften kann. In EF Core 9 und 10 schalteten Sie sie mit EFOptimizeContext=true ein und wählten dann die Phase mit EFScaffoldModelStage und EFPrecompileQueriesStage.
EF Core 11 hat EFOptimizeContext entfernt (dotnet/efcore#35079). Die beiden Stage-Eigenschaften funktionieren nun eigenständig, und das Setzen der alten Eigenschaft lässt den Build fehlschlagen. Ist PublishAot auf true gesetzt, ist die Generierung beim Publish standardmäßig aktiv. Ohne AOT sieht das Äquivalent zum alten Opt-in in EF Core 11 so aus:
<!-- .NET 11, EF Core 11.0.0-rc.1.26425.128 -->
<PropertyGroup>
<EFScaffoldModelStage>build</EFScaffoldModelStage>
<EFPrecompileQueriesStage>none</EFPrecompileQueriesStage>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="11.0.0-rc.1.26425.128" PrivateAssets="all" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tasks" Version="11.0.0-rc.1.26425.128" PrivateAssets="all" />
</ItemGroup>
Die generierten Dateien landen in obj/<Configuration>/<TFM>/ als *.g.cs und werden zur Kompilierung hinzugefügt, sodass nichts in die Versionskontrolle gelangt. Gültige Stage-Werte sind build, publish und alles andere (üblicherweise none), um die Funktion zu deaktivieren. Die MSBuild-Tasks-Referenz listet DbContextName, EFTargetNamespace, EFOutputDir und EFNullable für feinere Steuerung auf. Falls Sie ein Projekt mit der alten Eigenschaft aktualisiert haben und der Build explodiert ist, behandelt der Beitrag zu PublishAot plus EFOptimizeContext diesen Fehler und den vorangegangenen Bug mit erschöpftem Speicher.
Lohnt sich also der MSBuild-Weg? Für Native AOT ja: Sie brauchen ohnehin ein Compiled Model und vorkompilierte Abfragen, und deren Neugenerierung beim Publish ist der sicherste Weg, sie synchron zu halten. Für eine normale JIT-App sagen meine Messungen nein, aus zwei Gründen.
Der MSBuild-Weg erzeugt das langsamere, AOT-geprägte Modell
Die MSBuild-Dokumentation vermerkt, dass die Integration “will always generate additional code in the compiled model that’s required for NativeAOT”. In der Praxis bedeutet das eine zusätzliche Datei <Entity>UnsafeAccessors.g.cs pro Entitätstyp: 203 generierte Dateien für mein Modell mit 100 Entitäten statt 103. Dieser Code ist beim Start nicht gratis. Dasselbe Modell mit 100 Entitäten, dieselbe Maschine:
| Quelle des Compiled Models | Modell laden | Erste Abfrage |
|---|---|---|
dotnet ef dbcontext optimize | 92 ms | 213 ms |
dotnet ef dbcontext optimize --nativeaot | 231 ms | 394 ms |
EFScaffoldModelStage=build | 235 ms | 393 ms |
| Kein Compiled Model | 280 ms | 383 ms |
Das per MSBuild generierte Modell lädt nur etwa 45 ms schneller als gar kein Compiled Model, und die erste Abfrage war nicht schneller. Die Ausgabe der einfachen CLI lädt 2,5-mal schneller. Wenn Sie nicht mit AOT veröffentlichen, ist der NativeAOT-Code reiner Overhead.
Ein sauberer Build schlägt fehl, und der Wiederholungsversuch überspringt die Generierung unbemerkt
Der zweite Grund ist ein Problem mit der Build-Reihenfolge, auf das ich sowohl mit 11.0.0-rc.1 als auch mit 10.0.12 gestoßen bin. Das Tasks-Paket hängt die Generierung in TargetsTriggeredByCompilation ein, das direkt nach CoreCompile läuft, aber der Task OptimizeDbContext lädt die Assembly aus bin/, das erst später im Build befüllt wird. Bei einem frischen Checkout ohne bin-Ordner schlägt der erste Build fehl:
Optimizing DbContext...
Microsoft.EntityFrameworkCore.Tasks.targets(105,5): error : File '.../bin/Debug/net11.0/Bench.dll' not found.
Build FAILED.
Ein erneutes dotnet build meldet dann Build succeeded, weil der Kompilierungsschritt aktuell ist und die Generierung übersprungen wird. Die entstandene App läuft ohne Compiled Model: Mein Test gab RuntimeModel und OnModelCreating=1 aus. Erst nachdem ich eine Quelldatei angefasst hatte, gab der nächste Build Optimizing DbContext... aus und erzeugte BenchContextModel. In CI, wo jeder Build sauber startet, ist das bei jedem Lauf ein roter Build. Ein zugehöriges Issue in dotnet/efcore habe ich zum Zeitpunkt des Schreibens nicht gefunden, prüfen Sie also die Release Notes, wenn 11.0 GA erscheint.
Der CLI-Weg: einmal generieren, einchecken
Für eine JIT-App ist das bessere Setup das alte: die CLI ausführen, die Ausgabe committen, bei Modelländerungen neu generieren.
# .NET 11 SDK, dotnet-ef 11.0.0-rc.1.26425.128
dotnet tool install --global dotnet-ef --version 11.0.0-rc.1.26425.128
dotnet ef dbcontext optimize --output-dir CompiledModels --namespace MyApp.CompiledModels
Bei einem bereits gebauten Projekt dauerte die Generierung für meine drei Modellgrößen zwischen 1,3 und 2,3 Sekunden. Zwei wissenswerte Details: Das Projekt muss zuvor wiederhergestellt sein (sonst erhalten Sie Unable to retrieve project metadata), und wenn der Kontext in einem anderen Startprojekt konfiguriert ist, übergeben Sie --startup-project oder fügen eine IDesignTimeDbContextFactory<T> hinzu.
Das Risiko des CLI-Wegs ist Drift, und Drift ist schlimmer, als die Dokumentation vermuten lässt. Der Warm-up-Beitrag vom April besagte, dass EF Core ein veraltetes Compiled Model erkennt und eine Exception wirft. Ein Test mit EF Core 11 RC1 zeigt etwas anderes. Ich habe ein Compiled Model generiert, dann einer Entität eine Eigenschaft Sku hinzugefügt und eine Spalte mit HasColumnName("DisplayName") umbenannt, ohne neu zu generieren:
// .NET 11, EF Core 11.0.0-rc.1.26425.128, compiled model generated BEFORE these changes
Console.WriteLine(ctx.Set<Entity2>().Select(e => e.Name).ToQueryString());
// With the stale compiled model: SELECT "t"."Name" FROM "T2" AS "t"
// Without any compiled model: SELECT "t"."DisplayName" FROM "T2" AS "t"
Keine Exception, keine Warnung: Das veraltete Modell erzeugte SQL für den alten Spaltennamen. Die neue Eigenschaft Sku schlug zwar fehl, aber nur, wenn eine Abfrage sie verwendete, und zwar mit dem generischen Fehler “The LINQ expression could not be translated … Translation of member ‘Sku’ on entity type ‘Entity0’ failed”, der Sie nicht einmal in die Nähe der eigentlichen Ursache führt.
Die Lösung ist, Drift zu einem CI-Fehler zu machen. Der Generator ist deterministisch bis auf eine Zeile, die GUID modelId in <Context>ModelBuilder.cs, die sich bei jedem Lauf ändert. Git kann diese Zeile ignorieren:
# .NET 11 SDK, dotnet-ef 11.0.0-rc.1.26425.128, git 2.30+
dotnet ef dbcontext optimize --output-dir CompiledModels --namespace MyApp.CompiledModels
git diff --exit-code -I 'modelId:' -- CompiledModels
Hat jemand das Modell geändert, ohne neu zu generieren, ist der Diff nicht leer und der Job schlägt fehl. Es ist dieselbe Idee wie die Prüfung auf ausstehende Migrationen, und sie passt in denselben CI-Schritt.
Was ein Compiled Model nicht kann
Die Learn-Seite hat eine Liste von Einschränkungen, die teilweise veraltet ist. Verifiziert mit EF Core 11 RC1:
- Globale Query Filter verhindern die Generierung vollständig. Ein einziges
HasQueryFilterirgendwo im Modell, unddotnet ef dbcontext optimizebricht mitThe entity type 'Entity1' has a query filter configured. Compiled model can't be generated, because query filters are not supported.ab. Das Tracking-Issue dotnet/efcore#24897 ist im Backlog weiterhin offen. Wenn Sie Soft Delete oder Mandantenfähigkeit über Query Filter umsetzen, sind Compiled Models heute ausgeschlossen. - Das Modell muss von Hand neu generiert werden (CLI-Weg), sobald sich eine Entität, ein Konfigurationsaufruf oder eine Konvention ändert. Siehe den Abschnitt zur Drift oben.
- Eigene
IModelCacheKeyFactory-Implementierungen werden nicht unterstützt. Wenn Ihr Modell pro Mandant oder pro Schema variiert, generieren Sie pro Variante ein Compiled Model in getrennte Ordner und Namespaces und wählen mitUseModel(...)anhand des Laufzeitzustands eines aus, wie es die Learn-Seite zeigt. - Value Converter, die auf private Methoden verweisen, können nicht generiert werden; machen Sie die Methoden
internaloderpublic. EnsureCreatedund Migrationen führen weiterhinOnModelCreatingaus. Der Aufruf vonDatabase.EnsureCreated()bei vorhandenem Compiled Model hat meinenOnModelCreating-Zähler auf 1 erhöht, weil Schemaoperationen das vollständige Design-Time-Modell brauchen und nicht das reduzierte Laufzeitmodell. Ein Compiled Model beschleunigt Abfragen undSaveChanges, nicht Ihre Schemaerstellung beim Start. Wenn eine Testsuite bei jeder FixtureEnsureCreatedaufruft, erwarten Sie dadurch keine Beschleunigung.- Lazy-Loading- und Change-Tracking-Proxys stehen noch in der Learn-Liste, aber das Tracking-Issue dotnet/efcore#24902 wurde für EF Core 7.0 geschlossen. Ich habe Proxys für diesen Beitrag nicht erneut getestet, betrachten Sie diesen Punkt also als veraltet und nicht als verifizierte Unterstützung.
- Partielle
Customize()-Methoden im generierten Modell funktionieren mit dem CLI-Weg, aber nicht mit dem MSBuild-Weg, weil das Projekt kompilieren muss, bevor das Modell existiert.
Eine Entscheidungsregel, die trägt
Aus den Messungen und den Einschränkungen ergibt sich:
- Veröffentlichung mit Native AOT? Dann brauchen Sie es. Setzen Sie
PublishAot, referenzieren SieMicrosoft.EntityFrameworkCore.Tasksund lassen Sie das Publish das Modell und die vorkompilierten Abfragen generieren. Ohne wirft die erste Abfrage “Model building is not supported when publishing with NativeAOT”, wie in der MAUI-iOS-Variante dieses Fehlers beschrieben. - Kaltstarts sind für Benutzer sichtbar (Serverless, Scale-to-Zero-Container, CLI-Tools, Desktop-Apps) und Sie haben keine Query Filter? Nutzen Sie den CLI-Weg, committen Sie die Ausgabe und fügen Sie die Drift-Prüfung zu CI hinzu. Rechnen Sie mit etwa 100 ms Ersparnis bei kleinen Modellen und 500 ms bei 500 Entitätstypen. Es ergänzt sich gut mit den anderen Tricks aus Kaltstartzeit einer .NET 11 AWS Lambda reduzieren.
- Langlaufender Server, der sich vor dem Datenverkehr aufwärmt? Lassen Sie es. Ein Start-Warm-up, das
DbContext.Modelberührt, liefert dasselbe für den Benutzer sichtbare Ergebnis ohne generierten Code, den Sie warten müssten. - Sie nutzen
EFOptimizeContextaus EF Core 9 oder 10 für eine JIT-App? Erwägen Sie beim Upgrade auf EF Core 11, die MSBuild-Integration zu löschen, statt sie aufEFScaffoldModelStage=buildzu übertragen. Sie erhalten ein schnelleres Modell von der CLI und vermeiden den Fehler beim sauberen Build.
Das Modell ist nur die Hälfte der Kosten der ersten Abfrage. Jede neue LINQ-Form wird ebenfalls einmal pro Prozess übersetzt und kompiliert; wenn einige wenige heiße Abfragen dominieren, greifen Compiled Queries für heiße Pfade in EF Core diese zweite Hälfte an.
Verwandte Beiträge
- Wie Sie das Modell von EF Core vor der ersten Abfrage aufwärmen, die wartungsfreie Alternative für langlaufende Server.
- Fix: PublishAot plus EFOptimizeContext erschöpft den Speicher bei einem EF Core Build, zum Build-Bug in EF Core 10 und zum Entfernungsfehler in EF Core 11.
- Wie Sie Compiled Queries mit EF Core für heiße Pfade verwenden, das abfrageseitige Gegenstück zu einem Compiled Model.
- Was ist eine Shadow Property in EF Core 11, hilfreich, wenn Sie eine generierte Datei
EntityType.cslesen und sich fragen, woher eine zusätzliche Eigenschaft kommt.
Quellen
- Compiled models, EF Core Dokumentation, einschließlich der Liste der Einschränkungen.
- EF Core MSBuild tasks, EF Core Dokumentation, zu
EFScaffoldModelStage,EFPrecompileQueriesStageund dem Hinweis zum NativeAOT-Code. - EF Core 11 breaking changes:
EFOptimizeContextMSBuild property has been removed. - What’s new in EF Core 9: auto-compiled models, zur automatischen Erkennung und zur ursprünglichen MSBuild-Integration.
dotnet ef dbcontext optimize, CLI-Referenz zu--output-dir,--namespace,--nativeaotund--precompile-queries.- dotnet/efcore#24897 (Query Filter in Compiled Models, offen) und dotnet/efcore#35079 (Entfernung von
EFOptimizeContext).
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.