Entity Framework & SQL Server: Der Preis der Abstraktion
Entity Framework macht den Zugriff auf die Datenbank einfacher. Die Folgen jedes Zugriffs bleiben: SQL Server muss die erzeugten Anweisungen übersetzen, einen Plan wählen, Daten lesen, sperren und übertragen, für jeden Aufruf und für alle Benutzer gleichzeitig. Deshalb garantiert gutes C# noch keinen guten SQL-Server-Workload.
Diese Seite verfolgt den Weg vom LINQ-Ausdruck bis zur Last auf dem Server. Sie zeigt, an welcher Stelle Sie was messen, welche verbreiteten Annahmen nicht stimmen und wie Sie mit einer lesenden SQL-Abfrage sehen, was Ihre Anwendung tatsächlich ausführt. Es geht nicht gegen Entity Framework: Viele EF-Anwendungen laufen gut, und handgeschriebenes SQL ist nicht automatisch besser. Es geht darum, beide Seiten zusammen zu betrachten.
Wie groß der Unterschied sein kann, zeigt ein Versuch aus unserem Lab: Dieselbe LINQ-Zeile mit Contains() kostete SQL Server für dieselbe Liste unter EF Core 7 rund 0,9 Sekunden CPU, unter EF Core 8 rund 2 Sekunden und unter EF Core 10 je nach Reihenfolge der Aufrufe das eine oder das andere. Der C#-Code war in allen Fällen gleich.
Vom C#-Code zur Last auf dem Server
Zwischen einer Zeile LINQ und der Last auf dem Server liegen mehrere Stationen. An jeder fällt eine Entscheidung, und jede hat ihre eigene Messstelle.
| Station | Was hier entschieden wird | Wo Sie es sehen |
|---|---|---|
| C# und LINQ | Welche Daten die Anwendung anfordert: ganze Entitäten oder nur benötigte Spalten, ein Objektgraph oder einzelne Zeilen, eine Abfrage oder eine je Datensatz | Quelltext, Code-Review |
| EF Core und Provider | Wie daraus SQL wird: Joins oder mehrere Anweisungen, Werte als Konstanten oder Parameter, welche Datentypen die Parameter haben. Das hängt von der EF-Version und von Optionen ab | ToQueryString(), Logging mit LogTo() |
| SQL-Text und Parameter | Ob SQL Server einen vorhandenen Plan wiederverwenden kann oder neu kompilieren muss | Query Store, Plan Cache, Abfrage 1 |
| Optimizer | Welcher Plan gewählt wird, auf Basis von Statistiken, Schätzungen und dem Compatibility Level der Datenbank | Ausführungsplan |
| Ausführung | CPU, gelesene Seiten, Speicherzusage, Sperren und Wartezeiten | Query Store, tatsächlicher Ausführungsplan |
| Übertragung und Materialisierung | Wie viele Zeilen und Bytes über das Netz gehen, wie viele Roundtrips nötig sind, was EF in Objekte umwandelt und verfolgt | Anwendungsmessung, SqlClient-Statistik |
| Workload | Wie oft das alles passiert, gleichzeitig mit welchen anderen Zugriffen | Query Store über Zeitfenster, Monitoring |
Zwei Folgen daraus sind im Alltag wichtig. Erstens: Eine Optimierung kann auf einer Station wirken und auf einer anderen nichts ändern. Weniger Arbeit in EF bedeutet nicht weniger Arbeit in SQL Server und umgekehrt. Zweitens: Derselbe C#-Code kann nach einem Update von EF Core, nach einem Wechsel des Compatibility Levels oder mit anderen Daten anderes SQL, andere Pläne und eine andere Last erzeugen.
Die Vertiefungen
Include, Projektion und Split Query
„Die Seite lädt langsam, die SQL-Anweisung ist schnell.“
Ausführlich, mit LabZum Artikel →02Ein Contains(), vier EF-Core-Versionen
„Seit dem Update ist die Suche langsamer.“
Ausführlich, mit LabZum Artikel →Welche Beobachtung passt zu Ihnen?
| Ihre Beobachtung | Erster Blick | Vertiefung |
|---|---|---|
| Eine Seite lädt langsam, die einzelne SQL-Anweisung dazu wirkt schnell | Anzahl Roundtrips und übertragene Zeilen je Seitenaufruf messen | Include, Projektion und Split Query |
| Nach einem EF-Core-Update ist eine Abfrage langsamer, der Code ist unverändert | Erzeugtes SQL vor und nach dem Update vergleichen | Ein Contains(), vier EF-Core-Versionen |
| Der Plan Cache oder Query Store enthält viele fast gleiche Anweisungen | Abfrage 1: Spalte texte | Ein Contains(), vier EF-Core-Versionen |
| Query Store zeigt eine teure Anweisung, aber niemand weiß, woher im Code sie kommt | Abfragen, Spalte kennzeichnung der zweiten | Herkunft finden |
| Mal schnell, mal langsam, je nach Parameter | Pläne im Query Store vergleichen | Ratgeber: Mal schnell, mal langsam |
| Timeouts beim Speichern | Blocking-Kette prüfen | Ratgeber: Blocking und Timeouts |
Abfrage: Was schickt die Anwendung an SQL Server?
Die Abfrage fasst im Query Store Anweisungen gleicher Form zusammen und zeigt, wie viele unterschiedliche Texte und Pläne dahinterstehen, wie oft sie liefen und was sie gekostet haben. Führen Sie sie in der Datenbank der Anwendung aus. Sie ist nur lesend; ab SQL Server 2022 ist Query Store für neue Datenbanken standardmäßig eingeschaltet. Ohne Query Store liefert die zweite Abfrage darunter eine ähnliche Sicht aus dem Plan Cache, allerdings nur für das, was seit dem letzten Neustart noch im Cache liegt.
Abfrage: Anweisungen im Query Store
/* PSG · Entity Framework & SQL Server · Abfrage 1
Was schickt die Anwendung wirklich an SQL Server? (Query Store)
Geprüft im PSG-Lab am 03.11.2026 auf SQL Server 2017 CU31, 2019 CU32, 2022 CU27 und 2025 CU9
(Testdatenbank mit aktivem Query Store; Anweisungen im EF-Core-Format, siehe Labbericht).
- Nur lesend. Ändert nichts.
- In der Anwendungsdatenbank ausführen; Query Store muss dort aktiv sein
(Standard für neue Datenbanken ab SQL Server 2022).
- Rechte: VIEW DATABASE STATE (ab SQL Server 2022 genügt VIEW DATABASE PERFORMANCE STATE).
Ohne das Recht endet die Abfrage mit Meldung 262 (danach 297).
- Fasst Anweisungen mit gleicher Form (query_hash) zusammen. Viele Texte bei gleicher Form
sind ein Hinweis auf Werte im SQL-Text, z. B. IN-Listen mit Konstanten.
- Die Spalte „merkmal“ nennt Verdachtsmerkmale, keinen Nachweis, dass EF die Anweisung erzeugt hat.
- Query Store speichert Summen je Plan und Intervall, keine Einzelaufrufe und keine Parameterwerte.
- Query Store speichert den Anweisungstext ohne vorangestellte Kommentare. Eine mit TagWith gesetzte
Marke steht deshalb nicht im Text dieser Abfrage; sie steht im Plan-Cache-Text (Abfrage 2).
*/
DECLARE @stunden int = 24; -- Betrachtungszeitraum
WITH laufzeit AS (
SELECT p.query_id,
SUM(rs.count_executions) AS ausfuehrungen,
SUM(rs.count_executions * rs.avg_cpu_time) / 1000.0 AS cpu_ms,
SUM(rs.count_executions * rs.avg_duration) / 1000.0 AS dauer_ms,
SUM(rs.count_executions * rs.avg_logical_io_reads) AS logische_reads
FROM sys.query_store_runtime_stats AS rs
JOIN sys.query_store_runtime_stats_interval AS i
ON i.runtime_stats_interval_id = rs.runtime_stats_interval_id
JOIN sys.query_store_plan AS p
ON p.plan_id = rs.plan_id
WHERE i.end_time >= DATEADD(HOUR, -@stunden, SYSDATETIMEOFFSET())
GROUP BY p.query_id
), je_anweisung AS (
SELECT q.query_hash,
q.query_id,
qt.query_sql_text,
l.ausfuehrungen, l.cpu_ms, l.dauer_ms, l.logische_reads,
(SELECT COUNT(*) FROM sys.query_store_plan AS p2 WHERE p2.query_id = q.query_id) AS plaene
FROM sys.query_store_query AS q
JOIN sys.query_store_query_text AS qt ON qt.query_text_id = q.query_text_id
JOIN laufzeit AS l ON l.query_id = q.query_id
WHERE q.object_id = 0 -- nur Anweisungen außerhalb von Prozeduren, Funktionen und Triggern
AND qt.query_sql_text NOT LIKE N'%sys.query_store[_]%' -- ohne Abfragen auf Query-Store-Sichten (auch diese hier)
)
SELECT TOP (25)
CONVERT(varchar(20), a.query_hash, 1) AS query_hash,
COUNT(*) AS texte,
SUM(a.plaene) AS plaene,
SUM(a.ausfuehrungen) AS ausfuehrungen,
CAST(SUM(a.cpu_ms) AS decimal(18, 1)) AS cpu_ms_gesamt,
CAST(SUM(a.dauer_ms) AS decimal(18, 1)) AS dauer_ms_gesamt,
CAST(SUM(a.logische_reads) AS bigint) AS logische_reads_gesamt,
CAST(SUM(a.cpu_ms) / NULLIF(SUM(a.ausfuehrungen), 0) AS decimal(18, 3)) AS cpu_ms_je_ausfuehrung,
CASE
WHEN MAX(CASE WHEN a.query_sql_text LIKE '%OPENJSON(@%' THEN 1 ELSE 0 END) = 1
THEN 'Liste als JSON-Parameter'
WHEN COUNT(*) > 1
AND MAX(CASE WHEN a.query_sql_text LIKE '% IN ([0-9-]%'
OR a.query_sql_text LIKE '% IN (N''%'
OR a.query_sql_text LIKE '% IN (''%' THEN 1 ELSE 0 END) = 1
THEN 'IN-Liste, viele Texte'
WHEN MAX(CASE WHEN a.query_sql_text LIKE '% IN (@%' THEN 1 ELSE 0 END) = 1
THEN 'IN-Liste mit Parametern'
ELSE ''
END AS merkmal,
LEFT(MIN(a.query_sql_text), 300) AS beispieltext
FROM je_anweisung AS a
GROUP BY a.query_hash
ORDER BY SUM(a.cpu_ms) DESC
OPTION (MAXDOP 1);Abfrage: Anweisungen im Plan Cache
Ohne Query Store, und um die TagWith-Kennzeichnung zu sehen:
/* PSG · Entity Framework & SQL Server · Abfrage 2
Ersatz ohne Query Store: dieselbe Sicht aus dem Plan Cache.
Geprüft im PSG-Lab am 03.11.2026 auf SQL Server 2017 CU31, 2019 CU32, 2022 CU27 und 2025 CU9.
- Nur lesend. Rechte: VIEW SERVER STATE (ab SQL Server 2022 VIEW SERVER PERFORMANCE STATE).
Ohne das Recht endet die Abfrage mit Meldung 300 (danach 297).
- Zeigt nur, was seit dem letzten Neustart oder Cache-Leeren noch im Cache liegt.
- Filtert auf die aktuelle Datenbank über das Planattribut dbid.
- Der Cache-Text enthält vorangestellte Kommentare. Die Spalte „kennzeichnung“ zeigt die erste
Kommentarzeile direkt vor der Anweisung, bei EF Core also die mit TagWith gesetzte Marke.
*/
WITH cache AS (
SELECT qs.query_hash,
qs.sql_handle,
qs.plan_handle,
qs.execution_count,
qs.total_worker_time / 1000.0 AS cpu_ms,
qs.total_elapsed_time / 1000.0 AS dauer_ms,
qs.total_logical_reads AS logische_reads
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY (SELECT CONVERT(int, pa.value) AS dbid
FROM sys.dm_exec_plan_attributes(qs.plan_handle) AS pa
WHERE pa.attribute = 'dbid') AS db
WHERE db.dbid = DB_ID()
)
SELECT TOP (25)
CONVERT(varchar(20), c.query_hash, 1) AS query_hash,
COUNT(DISTINCT c.sql_handle) AS texte,
COUNT(DISTINCT c.plan_handle) AS plaene,
SUM(c.execution_count) AS ausfuehrungen,
CAST(SUM(c.cpu_ms) AS decimal(18, 1)) AS cpu_ms_gesamt,
CAST(SUM(c.dauer_ms) AS decimal(18, 1)) AS dauer_ms_gesamt,
SUM(c.logische_reads) AS logische_reads_gesamt,
MAX(CASE WHEN k.pos > 0
THEN REPLACE(LEFT(SUBSTRING(st.text, k.pos + 3, 120),
ISNULL(NULLIF(CHARINDEX(CHAR(10), SUBSTRING(st.text, k.pos + 3, 120)), 0) - 1, 120)),
CHAR(13), '')
END) AS kennzeichnung,
LEFT(MIN(st.text), 300) AS beispieltext
FROM cache AS c
CROSS APPLY sys.dm_exec_sql_text(c.sql_handle) AS st
CROSS APPLY (SELECT CASE WHEN st.text LIKE N'-- %' THEN 1
WHEN st.text LIKE N'(@%' THEN ISNULL(NULLIF(CHARINDEX(N')-- ', st.text), 0) + 1, 0)
ELSE 0 END AS pos) AS k
GROUP BY c.query_hash
ORDER BY SUM(c.cpu_ms) DESC
OPTION (MAXDOP 1);So lesen Sie das Ergebnis:
textegrößer als 1 bei derselben Form heißt: Die Anwendung schickt Werte im SQL-Text statt als Parameter. Typisch sind IN-Listen mit Konstanten. Jede Variante belegt eigenen Platz im Plan Cache und muss beim ersten Aufruf kompiliert werden.plaenegrößer als 1 bei einem Text heißt: SQL Server hat für dieselbe Anweisung verschiedene Pläne gewählt, etwa nach einer Statistikaktualisierung oder für andere Parameterwerte. Ob das ein Problem ist, zeigt der Vergleich der Pläne.merkmalnennt Hinweise auf typische Muster, zum Beispiel eine Liste als JSON-Parameter. Das ist ein Verdacht, kein Beweis, dass Entity Framework die Anweisung erzeugt hat.kennzeichnung(nur in der zweiten Abfrage, aus dem Plan Cache) zeigt den Kommentar, den die Anwendung mitTagWith()an die Anweisung gehängt hat. Ist die Spalte leer, fehlt die Spur zurück in den Code. Query Store speichert diesen Kommentar nicht mit, siehe unten.- Parameterdeklaration vorn im Text, etwa
(@ids4 int,@ids3 int,…)SELECT: So speichert Query Store parametrisierte Anweisungen. Eine lange Liste einzelner Parameter ist bei EF Core 10 typisch fürContains()auf einer Liste. cpu_ms_gesamtsortiert. Eine billige Anweisung, die hunderttausendmal läuft, kann mehr kosten als die eine langsame, über die sich alle beschweren.
Beispielausgabe aus einem nachgestellten Lab-Lauf (SQL Server 2025, synthetische Daten, Kennzeichnungen frei gewählt, Spalten gekürzt). Die vier Zeilen zeigen die drei Übersetzungen von Contains(): eine Liste als JSON-Parameter (EF Core 8 und 9), Parameterlisten (EF Core 10) und Konstanten (EF Core 7):
Anweisungen im Query Store
query_hash texte plaene ausf. cpu_ms_gesamt merkmal beispieltext
0xC272B4BDD75093D9 1 1 12 1.2 Liste als JSON-Parameter (@__ids_0 nvarchar(4000))SELECT [p].[Id], ...
0x4B531E19E1CDD5CE 2 2 7 .8 IN-Liste, viele Texte (@ids4 int,@ids3 int,@ids2 int,@ids1 int)SELECT ...
0x87C447D1AF3AC850 1 1 3 .4 IN-Liste mit Parametern (@ids8 int,@ids7 int, ... ,@ids1 int)SELECT ...
0x1948732EF86250B4 3 3 3 .2 IN-Liste, viele Texte SELECT [p].[Id], ... WHERE [p].[Id] IN (1, 2, 3)
Anweisungen im Plan Cache
query_hash texte plaene ausf. kennzeichnung beispieltext
0xC272B4BDD75093D9 1 1 12 Katalog-Suche (@__ids_0 nvarchar(4000))-- Katalog-Suche
0x87C447D1AF3AC850 1 1 3 Katalog-Suche-v10 (@ids1 int, @ids2 int, ...)-- Katalog-Suche-v10Gelesen: Die letzte Form im Query Store hat drei Texte und drei Pläne für eine einzige Abfrage, die Werte stehen im Text. Die zweite Zeile zeigt eine Eigenheit: Eine Konstantenliste und eine Parameterliste gleicher Länge haben dieselbe Form (query_hash), die Spalte merkmal kann sie dann nicht trennen. Die Kennzeichnung „Katalog-Suche“ steht nur im Plan Cache.
Vom Query Store zurück zum Code
Query Store sagt Ihnen, welche Anweisung teuer ist. Er sagt Ihnen nicht, aus welcher Methode sie kommt. Paging, Joins über Navigationen oder die Form der Parameter sind Hinweise, aber keine sichere Rekonstruktion des LINQ-Ausdrucks.
Die zuverlässige Spur legt die Anwendung selbst: Mit TagWith("Bestellübersicht: offene Aufträge") schreibt EF Core einen Kommentar vor die Anweisung. Seit EF Core 6 schreibt TagWithCallSite() Datei und Zeile des Aufrufs dazu. Aber Vorsicht, wo Sie danach suchen: Query Store speichert den Anweisungstext ohne vorangestellte Kommentare. Im Lab fehlte die Marke dort auf allen Versionen von SQL Server 2017 bis 2025. Sie steht im Text des Plan Cache, in Extended Events und in den Logs der Anwendung. Deshalb zeigt die zweite Abfrage oben die Marke in der Spalte kennzeichnung, die erste nicht. Wer die Marke auch im Query Store wiederfinden will, braucht einen Abgleich über die Form der Anweisung (query_hash) mit dem Plan Cache, solange die Anweisung dort noch liegt. Halten Sie Tags fest und sprechend. Schreiben Sie keine Benutzer-IDs, Zeitstempel oder andere wechselnde Werte hinein: Jede Variante ergibt einen eigenen Anweisungstext und damit einen eigenen Eintrag im Plan Cache.
Query Store speichert Summen je Plan und Zeitintervall. Er kennt keine einzelnen Aufrufe, keine Parameterwerte und keine Antwortzeit eines bestimmten Benutzers. Für diese Fragen brauchen Sie die Messung in der Anwendung. Beide zusammen ergeben das Bild: die Anwendung zeigt, welcher Vorgang langsam ist, Query Store zeigt, was er auf dem Server kostet.
Fünf verbreitete Annahmen, die nicht stimmen
„AsNoTracking() macht die SQL-Abfrage billiger.“ Change Tracking findet in EF statt, nicht in SQL Server. Bei gleichem SQL und gleichen Parametern bleibt der Plan derselbe. Im Lab war das erzeugte SQL mit und ohne AsNoTracking() zeichengleich. Die Antwortzeit kann sich trotzdem ändern, weil EF anders arbeitet, und zwar in beide Richtungen: In unserem Versuch mit vielen doppelten Zeilen war AsNoTracking() sogar 7 bis 8 % langsamer als Tracking (Details). Kosten in EF und Kosten in SQL Server sind zwei verschiedene Messgrößen.
„Compiled Queries sparen SQL Server die Kompilierung.“ EF Core übersetzt eine LINQ-Abfrage ohnehin nur einmal und merkt sich das Ergebnis in einem eigenen Cache. Eine Compiled Query spart EF lediglich das Nachschlagen in diesem Cache. SQL Server hat seinen eigenen Plan Cache; ob er einen Plan wiederverwendet, entscheidet der SQL-Text, nicht EF.
„Include() ist immer besser als Lazy Loading.“ Include vermeidet viele kleine Abfragen. Mit zwei Collections in einer Abfrage kann es aber ein Vielfaches der Zeilen übertragen, die Sie eigentlich brauchen: im Lab 16.257 Zeilen und 15,9 MB für eine Seite mit 50 Kunden, statt 2.485 Zeilen und 0,6 MB als Split Query. Eine Projektion auf wenige Spalten ändert daran wenig, sie macht die Zeilen nur schmaler. Wann Include, Projektion oder Split Query passt, zeigt die erste Vertiefung.
„Ein Update von EF Core ändert nichts an der Datenbank, solange der Code gleich bleibt.“ EF Core übersetzt dieselben Ausdrücke von Version zu Version unterschiedlich. Ein gut dokumentiertes Beispiel ist Contains() auf einer Liste: Konstanten, ein JSON-Parameter oder mehrere einzelne Parameter, je nach Version. Schon kleine Änderungen zählen: EF Core 10 benennt Parameter kürzer (@city statt @__city_0). Für SQL Server ist das ein neuer Text, nach dem Update werden die betroffenen Anweisungen also neu kompiliert. Was das für Plan Cache und Schätzungen bedeutet, zeigt die zweite Vertiefung.
„EnableRetryOnFailure() macht das Speichern sicher.“ Die Wiederholung hilft bei kurzen Verbindungsfehlern. Bricht die Verbindung aber genau beim Commit ab, weiß die Anwendung nicht, ob die Änderung gespeichert wurde. Eine Wiederholung kann dann doppelt buchen, wenn der Vorgang nicht idempotent ist. Microsoft beschreibt das Problem und vier Wege damit umzugehen ausdrücklich (siehe Quellen). Wer eigene Transaktionen öffnet, muss sie zudem über CreateExecutionStrategy() ausführen, sonst lehnt EF die Wiederholung ab.
EF Core und EF6
Entity Framework 6 und Entity Framework Core sind zwei getrennte Produktlinien. EF Core ist kein Nachfolger mit gleichem Verhalten, sondern eine Neuentwicklung. Die Aussagen auf dieser Seite gelten für EF Core, wenn nicht anders angegeben. EF6 (aktuell 6.5) wird von Microsoft weiter unterstützt, aber nicht mehr aktiv weiterentwickelt; seit Version 6.3 läuft es auch unter .NET Core und .NET. Viele Bestandsanwendungen nutzen es. Die Grundsätze dieser Seite gelten dort ebenso, das erzeugte SQL und die Optionen unterscheiden sich aber.
Ein konkreter Anlass: Der Support für EF Core 8 und EF Core 9 endet am 10. November 2026, zusammen mit .NET 8 und .NET 9. Der nächste Schritt ist EF Core 10 (Support bis November 2028). Viele Teams stellen deshalb gerade um. Das ist der richtige Moment, das erzeugte SQL vor und nach dem Update zu vergleichen, bevor es die Produktion tut. Wie, zeigt die zweite Vertiefung.
Was dauerhaft hilft
- Das erzeugte SQL ansehen.
ToQueryString()zeigt das SQL einer Abfrage ohne sie auszuführen,LogTo()protokolliert alle Anweisungen mit Dauer. Ein Blick in der Entwicklung erspart viele Überraschungen im Betrieb. - Nur laden, was gebraucht wird. Eine Projektion auf die benötigten Spalten spart Übertragung, Speicher und oft Lesearbeit. Das ist kein Trick, sondern eine Architekturentscheidung: Welche Daten braucht dieser Vorgang wirklich?
- Abfragen kennzeichnen. Feste Tags an den wichtigen Abfragen machen den Weg vom Plan Cache, aus Extended Events und aus den Logs zurück in den Code kurz.
- Query Store einschalten und nutzen. Er bewahrt Pläne und Laufzeiten über Neustarts hinweg auf und ist die Grundlage für Vergleiche vor und nach einem Release.
- Upgrades mit Last prüfen. Vor einem Update von EF Core oder einem Wechsel des Compatibility Levels die wichtigsten Vorgänge mit realistischen Daten ausführen und SQL, Pläne und Kosten vergleichen. Die Achsen getrennt ändern: erst EF, dann den Compatibility Level.
- Datentypen im Modell festlegen. Länge, Unicode und Genauigkeit der Spalten bestimmen die Parametertypen. Passen sie nicht zur Spalte, kann SQL Server einen Index nicht wie erwartet nutzen.
- Das passende Werkzeug wählen. Mengenänderungen mit
ExecuteUpdate()oderExecuteDelete(), eine gezielte SQL-Anweisung oder eine gespeicherte Prozedur, wo LINQ an Grenzen stößt. Entity Framework schließt das nicht aus. - Den Blick weiten. Ist der Server insgesamt langsam, führt der SQL Server Performance Ratgeber durch die passenden Diagnosewege.
Vertiefungen: Include, Projektion und Split Query · Ein Contains(), vier EF-Core-Versionen
Quellen
- Microsoft: EF Core und EF6 im Vergleich
- Microsoft: Releases und Supportzeiträume von EF Core, .NET-Supportrichtlinie
- Microsoft: Efficient Querying
- Microsoft: Tracking und No-Tracking-Abfragen
- Microsoft: Compiled Queries
- Microsoft: Query Tags
- Microsoft: Connection Resiliency, Commit-Fehler und Idempotenz
- Microsoft: EF Core 10: vereinfachte Parameternamen
- Microsoft: Query Store einschalten