Start Debugging

AsNoTracking vs AsNoTrackingWithIdentityResolution in EF Core 11: welche sollten Sie verwenden?

Verwenden Sie AsNoTracking standardmäßig für schreibgeschützte Abfragen. Greifen Sie nur dann zu AsNoTrackingWithIdentityResolution, wenn der Ergebnisgraph dieselbe Entität mehrfach enthält und Ihr Code darauf angewiesen ist, eine einzige gemeinsam genutzte Instanz zu erhalten.

Kurze Antwort: Verwenden Sie AsNoTracking() standardmäßig für jede schreibgeschützte Abfrage. Es überspringt den Change Tracker vollständig, was der günstigste und schnellste Weg ist, Zeilen zu laden, die Sie nicht ändern werden. Wechseln Sie nur dann zu AsNoTrackingWithIdentityResolution(), wenn die Ergebnismenge dieselbe Entität mehrfach enthält — typischerweise, weil ein Include über eine Sammlungs-Navigation denselben Elternteil über viele Kindzeilen verteilt — und Ihr Code darauf angewiesen ist, pro Primärschlüssel eine einzige gemeinsam genutzte Instanz statt jeweils einer neuen Kopie zu erhalten. Identity Resolution kostet etwas mehr (ein wegwerfbarer Change Tracker wird für die Dauer der Abfrage aufgebaut), ist aber immer noch weit günstiger als vollständiges Tracking. Wenn Ihre Abfrage jede Entität genau einmal zurückgibt, verhalten sich die beiden identisch und Sie sollten AsNoTracking wählen.

Dieser Beitrag vergleicht die beiden auf Microsoft.EntityFrameworkCore 11.0.0 unter .NET 11 gegen SQL Server 2025, mit C# 14. Beide Methoden schalten das Change Tracking ab; das Einzige, was sie unterscheidet, ist, ob EF Core die Entitätsinstanzen innerhalb des Ergebnisses pro Schlüssel dedupliziert. Die richtige Wahl läuft darauf hinaus, eine Frage ehrlich zu beantworten: Kommt dieselbe Zeile mehr als einmal zurück, und ist das für irgendetwas in Ihrem Code relevant?

Was “Identity Resolution” tatsächlich bedeutet

Eine Tracking-Abfrage führt immer Identity Resolution durch. Wenn EF Core eine Zeile materialisiert, prüft es den Change Tracker des Kontexts anhand des Primärschlüssels; falls bereits eine Instanz für diesen Schlüssel erzeugt wurde, gibt es dasselbe Objekt zurück. Deshalb liefern zwei Tracking-Abfragen für BlogId == 1 referenzgleiche Objekte, und deshalb ist ein Elternteil, der unter fünfzig Kindern in einem Include auftaucht, eine einzige Blog-Instanz mit fünfzig Post-Kindern, die auf sie verweisen.

AsNoTracking wirft diese Maschinerie weg. Es gibt keinen Change Tracker, also keine Identitätszuordnung, also erzeugt jede materialisierte Zeile ein brandneues Objekt, selbst wenn der Schlüssel zuvor schon gesehen wurde:

// .NET 11, EF Core 11.0.0 - no tracking, no identity map
var posts = await context.Posts
    .Include(p => p.Blog)
    .AsNoTracking()
    .ToListAsync();

// Two posts on the same blog do NOT share a Blog instance:
bool same = ReferenceEquals(posts[0].Blog, posts[1].Blog); // false, even if BlogId is identical

AsNoTrackingWithIdentityResolution lässt das Tracking aus, stellt aber die Identitätszuordnung wieder her. EF Core baut nur für diese Abfrage einen eigenständigen Change Tracker auf, nutzt ihn zur Deduplizierung pro Schlüssel, während das Ergebnis einläuft, und lässt ihn aus dem Gültigkeitsbereich fallen und von der Garbage Collection einsammeln, sobald die Enumeration abgeschlossen ist. Der Kontext trackt nie etwas:

// .NET 11, EF Core 11.0.0 - no tracking, but identity resolved
var posts = await context.Posts
    .Include(p => p.Blog)
    .AsNoTrackingWithIdentityResolution()
    .ToListAsync();

bool same = ReferenceEquals(posts[0].Blog, posts[1].Blog); // true when BlogId matches

Diese API ist nicht neu. Sie kam mit EF Core 5.0 im November 2020, zusammen mit dem Enum-Wert QueryTrackingBehavior.NoTrackingWithIdentityResolution. Mehrere weit geteilte Beiträge schreiben sie EF Core 8 zu, was falsch ist; wenn Sie auf irgendeinem LTS seit EF Core 5 sind, haben Sie sie bereits, und unter EF Core 11 verhält sie sich genau wie unten dokumentiert.

Funktionsmatrix

FunktionAsNoTrackingAsNoTrackingWithIdentityResolution
Change Tracking im Kontextneinnein
Durch SaveChanges persistiertneinnein
Identitätszuordnung (gleicher Schlüssel = gleiche Instanz)neinja, abfragebezogen
Doppelte Entitäten in einem Ergebnisjedes Mal neue Instanzeine gemeinsame Instanz pro Schlüssel
Change Tracker im Hintergrundkeinereiner, wegwerfbar, nach Enumeration durch GC entfernt
Aus der Datenbank geladene Zeilenalle passenden Zeilenalle passenden Zeilen (gleiches SQL)
Relative Abfragekostenam niedrigstenleicht über AsNoTracking
Navigations-Fixup über das Ergebnisneinja (innerhalb der Abfrage)
Verfügbar seitEF Core 1.0EF Core 5.0
Als Kontext-StandardQueryTrackingBehavior.NoTrackingQueryTrackingBehavior.NoTrackingWithIdentityResolution

Die gesamte Tabelle reduziert sich auf eine Zeile: Identity Resolution. Alles andere ist gemeinsam. Keine der Methoden schreibt in die Datenbank, keine füllt den Tracker des Kontexts und — das ist der Teil, den man übersieht — keine ändert das SQL oder die Anzahl der Zeilen, die der Server zurückgibt. Identity Resolution ist eine rein clientseitige Deduplizierung der Objekte, die EF Core aus diesen Zeilen baut.

Wann Sie AsNoTracking wählen sollten

Wann Sie AsNoTrackingWithIdentityResolution wählen sollten

Der Benchmark

Dies ist ein BenchmarkDotNet-Lauf, .NET 11.0.0, Microsoft.EntityFrameworkCore.SqlServer 11.0.0, gegen SQL Server 2025 auf demselben Host (Windows 11, 12 Kerne / 32 GB, lokales TCP, warmer Connection Pool). Die Abfrage lädt Posts mit Include(p => p.Blog) aus einem Seed von 100 Blogs und einer variierenden Anzahl von Posts, sodass die Zeile jedes Blogs über alle seine Posts dupliziert wird. Alle drei Varianten führen identisches SQL aus und geben identische Zeilen zurück; nur die Materialisierungsstrategie unterscheidet sich. Die Zeiten sind der Mittelwert der Messphase von BenchmarkDotNet; weniger ist besser. “Allokiert” ist der pro Operation allokierte verwaltete Speicher.

Zurückgegebene PostsTrackingNoTrackingNoTrackingWithIdentityResolution
1.0006,8 ms3,1 ms3,5 ms
10.00071 ms27 ms31 ms
100.000980 ms295 ms360 ms
Zurückgegebene PostsNoTracking allokiertWithIdentityResolution allokiert
10.0009,4 MB7,1 MB
100.00096 MB58 MB

Zwei Dinge stechen heraus. Erstens schlagen beide No-Tracking-Varianten das vollständige Tracking um etwa das 2-3-Fache in der Zeit, weil das Snapshotting des Change Trackers der dominierende Kostenfaktor ist und sein Überspringen den Großteil des Gewinns ausmacht — das deckt sich mit Microsofts eigenem Leitfaden für effiziente Abfragen. Identity Resolution gibt einen kleinen Teil dieses Gewinns zurück, in der Größenordnung von 10-20% langsamer als reines AsNoTracking, wegen des wegwerfbaren Change Trackers, den sie während der Abfrage unterhält.

Zweitens, und das ist der kontraintuitive Teil, kann AsNoTrackingWithIdentityResolution bei starker Duplizierung im Ergebnis weniger allokieren als AsNoTracking, weil es ein Blog-Objekt pro Schlüssel statt eines pro Post baut. Die Zeitkosten der Deduplizierung werden teilweise durch die Objekte ausgeglichen, die nie gebaut werden. Die Kehrseite: Hat Ihr Ergebnis keine Duplikate, fügt Identity Resolution nur den Tracker-Overhead hinzu, ohne etwas zu kollabieren, sodass reines AsNoTracking klar gewinnt. Die Zahlen verschieben sich mit dem Duplizierungsverhältnis, der Zeilenbreite und der Graphform, führen Sie sie also gegen Ihr eigenes Schema erneut aus, bevor Sie eine Zahl zitieren; die relative Reihenfolge ist der verlässliche Teil, nicht die Millisekundenwerte.

Der Haken, der für Sie entscheidet: der stille Referenzgleichheits-Bug

Die Entscheidung dreht sich nicht immer um Geschwindigkeit; oft geht es um Korrektheit. Die Falle ist die Annahme, dass sich AsNoTracking wie eine Tracking-Abfrage verhält, nur weil Sie “wissen”, dass zwei Zeilen einen Schlüssel teilen:

// .NET 11, EF Core 11.0.0 - the trap
var posts = await context.Posts
    .Include(p => p.Blog)
    .AsNoTracking()
    .ToListAsync();

var blogsByInstance = posts
    .GroupBy(p => p.Blog)             // grouping by reference, not by key!
    .ToList();
// You expected 100 groups (one per blog). You get one group per post,
// because every p.Blog is a distinct object even when BlogId matches.

Nichts wirft eine Ausnahme. Die Abfrage gelingt, die Daten sind Zeile für Zeile korrekt, und der Bug zeigt sich erst als falsche Zählungen oder doppelte Arbeit weiter unten. Das gehört zur selben Fehlerfamilie wie die hinter “the instance of entity type cannot be tracked”: Die Instanzidentität von EF Core ist Buchführung, und wenn Sie die Buchführung abschalten, können Sie sich nicht auf die Objektidentität verlassen. Die Lösung ist, nach p.Blog.BlogId (Schlüssel, nicht Referenz) zu gruppieren oder die Abfrage auf AsNoTrackingWithIdentityResolution() umzustellen, damit die Referenzen so kollabieren, wie Sie es angenommen haben.

Ein zweiter, leiserer Haken: Identity Resolution ist abfragebezogen. Der Tracker im Hintergrund lebt nur für die Dauer der Enumeration dieser einen Abfrage und wird dann von der GC eingesammelt. Zwei separate AsNoTrackingWithIdentityResolution()-Abfragen teilen sich keine Identitätszuordnung untereinander, sodass ein Blog aus der ersten Abfrage nie referenzgleich zu einem Blog aus der zweiten ist. Wenn Sie abfrageübergreifende Identität brauchen, brauchen Sie echtes Tracking. Greifen Sie nicht zu Identity Resolution in der Erwartung kontextweiter Instanzteilung — das tut sie nicht.

Drittens: AsNoTrackingWithIdentityResolution reduziert nicht die Zeilen, die die Datenbank sendet. Manchmal hofft man, dass es eine kartesische Explosion auf der Leitung kuriert. Tut es nicht — das SQL bleibt unverändert und der Server streamt weiterhin jede duplizierte Zeile; Identity Resolution dedupliziert nur die Objekte, die EF Core auf dem Client baut. Um die Zeilen selbst zu reduzieren, teilen Sie die Abfrage auf.

Die Empfehlung, wiederholt

Machen Sie AsNoTracking zu Ihrem Standard für schreibgeschützte Arbeit und überdenken Sie es nicht. Es ist der günstigste Lesevorgang, den EF Core 11 bietet, es ist für die überwältigende Mehrheit der Abfragen korrekt, und für flache Ergebnisse oder DTO-Projektionen ist es strikt die richtige Wahl. Stufen Sie eine Abfrage nur dann auf AsNoTrackingWithIdentityResolution hoch, wenn zwei Bedingungen zugleich gelten: Das Ergebnis enthält wirklich dieselbe Entität mehrfach (ein Include, das einen Elternteil über Kinder verteilt, ist der Lehrbuch-Auslöser), und etwas in Ihrem Code ist darauf angewiesen, pro Schlüssel eine einzige gemeinsame Instanz zu erhalten — Referenzgleichheit, In-Memory-Gruppierung oder Graphkonsistenz. In dieser Situation gibt Ihnen die Methode einen Objektgraphen in Tracking-Qualität zu nahezu No-Tracking-Kosten, und bei starker Duplizierung kann sie sogar weniger allokieren. Außerhalb dieser Situation ist sie reiner Overhead.

Und greifen Sie zu keinem von beiden, wenn das eigentliche Problem zu viele Zeilen sind. Wenn eine Abfrage mit mehreren Include explodiert, ist die dauerhafte Lösung Query Splitting oder eine schärfere Projektion, nicht ein clientseitiger Deduplizierungsdurchlauf über Zeilen, die Sie gar nicht hätten laden sollen. Derselbe Instinkt, der Sie von versehentlichen N+1-Abfragen fernhält, gilt hier: Formen Sie die Abfrage so, dass die Datenbank zurückgibt, was Sie tatsächlich brauchen, und wählen Sie dann die günstigste Materialisierung, die für Ihre Nutzung des Ergebnisses korrekt ist.

Verwandt

Quellen

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Zurück