Entity Framework Core: Performance-Optimierung und Best Practices 2026
EF Core 10 Performance-Optimierung mit AsNoTracking, Split Queries, Massenoperationen, dem neuen LeftJoin-Operator und benannten Query-Filtern. Praxisleitfaden fuer .NET-10-Anwendungen in Produktion.

Entity Framework Core 10, veröffentlicht mit .NET 10 LTS im November 2025, bringt LeftJoin-Operatoren, Vektorsuche, benannte Query-Filter und erhebliche Verbesserungen der SQL-Übersetzung mit. Dieser Leitfaden behandelt die EF-Core-Best-Practices, die sich direkt auf Abfragegeschwindigkeit, Speicherverbrauch und Skalierbarkeit in Produktionsumgebungen auswirken.
EF Core 10 ist ein Long-Term-Support-Release mit Unterstützung bis November 2028. Die Ausführung erfordert das .NET 10 SDK und die zugehörige Runtime. Anwendungen, die noch auf .NET 8 laufen, sollten bis zum Abschluss der Migration EF Core 8 (LTS) verwenden.
Query Tracking: Wann und warum es deaktiviert werden sollte
Jeder Aufruf von DbSet<T> hängt zurückgegebene Entitäten standardmäßig an den Change Tracker an. Der Tracker hält einen Snapshot der ursprünglichen Eigenschaftswerte vor, berechnet Diffs bei SaveChanges und löst Identitätskonflikte über Navigationen hinweg auf. Dieser Overhead ist überflüssig, wenn die Daten direkt in eine API-Antwort oder ein schreibgeschütztes View-Model fließen.
public async Task<List<ProductDto>> GetActiveByCategoryAsync(
int categoryId, CancellationToken ct)
{
return await _context.Products
.AsNoTracking() // skip change tracker entirely
.Where(p => p.CategoryId == categoryId && p.IsActive)
.OrderBy(p => p.Name)
.Select(p => new ProductDto // project to DTO at the database level
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.ToListAsync(ct);
}AsNoTracking eliminiert den Aufwand für Snapshot-Erstellung und Identitätsauflösung pro Entität. In Kombination mit Select werden nur die benötigten Spalten über die Leitung gesendet. Bei einer Tabelle mit 50.000 Zeilen reduziert dieses Muster die Speicherallokationen typischerweise um 40–60 % gegenüber einer getrackten Vollentitäten-Abfrage.
Für Kontexte, die Daten niemals modifizieren, lässt sich das Standardverhalten bei der Registrierung festlegen:
builder.Services.AddDbContext<CatalogContext>(options =>
options.UseSqlServer(connectionString)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));Split Queries gegen kartesische Explosion
Das Laden einer Entität mit mehreren Collection-Navigationen über Include erzeugt eine einzelne SQL-Anweisung mit JOINs. Wenn zwei oder mehr Collections gleichzeitig geladen werden, wächst die Ergebnismenge als kartesisches Produkt der Collections, wobei die Daten der übergeordneten Zeile über jede Kombination hinweg dupliziert werden.
public async Task<Order?> GetWithDetailsAsync(int orderId, CancellationToken ct)
{
return await _context.Orders
.AsSplitQuery() // one SQL per Include
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.Include(o => o.Payments)
.Include(o => o.ShippingEvents)
.FirstOrDefaultAsync(o => o.Id == orderId, ct);
}AsSplitQuery teilt den Ladevorgang in separate SQL-Anweisungen pro Navigation auf. Der Trade-off: mehrere Roundtrips statt eines einzigen, doch jede Ergebnismenge bleibt klein und vermeidet das Problem der Zeilenduplikation. EF Core 10 behebt zudem eine langjährig bestehende Inkonsistenz bei der Sortierung in Split Queries, sodass die Sortierung der Unterabfrage mit der Hauptabfrage übereinstimmt.
Single Queries sind weiterhin vorzuziehen, wenn nur eine einzige Collection-Navigation geladen wird oder wenn die Roundtrip-Latenz hoch ist (beispielsweise bei regionsübergreifenden Datenbankaufrufen). Beide Modi sollten für das jeweilige Zugriffsmuster per Benchmark verglichen werden, bevor eine Entscheidung getroffen wird.
Massenoperationen mit ExecuteUpdate und ExecuteDelete
Traditionelle EF-Workflows laden Entitäten, modifizieren Eigenschaften und rufen anschließend SaveChanges auf. Bei Massenoperationen, die tausende Zeilen betreffen, entstehen tausende getrackte Instanzen und individuelle UPDATE-Anweisungen. EF Core 7 führte ExecuteUpdateAsync und ExecuteDeleteAsync ein, um die gesamte Operation in einen einzelnen SQL-Befehl zu verpacken. EF Core 10 vereinfacht die API weiter, indem statt eines Ausdrucksbaums ein einfacher Lambda-Ausdruck akzeptiert wird.
public async Task ApplySeasonalDiscountAsync(
int categoryId, decimal discountPercent, CancellationToken ct)
{
var affected = await _context.Products
.Where(p => p.CategoryId == categoryId && p.IsActive)
.ExecuteUpdateAsync(s =>
{
s.SetProperty(p => p.Price, p => p.Price * (1 - discountPercent / 100));
s.SetProperty(p => p.LastModified, DateTime.UtcNow);
}, ct);
// affected = number of rows updated
}Dies wird in eine einzelne UPDATE ... SET ... WHERE-Anweisung übersetzt. Keine Entitäten werden in den Speicher geladen. Bei einem Update von 10.000 Zeilen sinkt die Ausführungszeit von Sekunden (getrackter Ansatz) auf Millisekunden.
Dasselbe Muster gilt für Löschvorgänge:
public async Task PurgeExpiredSessionsAsync(CancellationToken ct)
{
await _context.Sessions
.Where(s => s.ExpiresAt < DateTime.UtcNow)
.ExecuteDeleteAsync(ct);
}Bereit für deine .NET-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Der LeftJoin-Operator in EF Core 10
Vor EF Core 10 erforderte ein LEFT JOIN die Kombination von GroupJoin, SelectMany und DefaultIfEmpty in einem spezifischen Muster, das die meisten Entwickler jedes Mal nachschlagen mussten. EF Core 10 führt LeftJoin als erstklassigen LINQ-Operator ein.
public async Task<List<EmployeeReportDto>> GetEmployeeDepartmentReportAsync(
CancellationToken ct)
{
return await _context.Employees
.LeftJoin(
_context.Departments,
employee => employee.DepartmentId,
department => department.Id,
(employee, department) => new EmployeeReportDto
{
FullName = employee.FirstName + " " + employee.LastName,
Department = department.Name ?? "Unassigned",
HiredAt = employee.HiredAt
})
.OrderBy(r => r.FullName)
.ToListAsync(ct);
}Das generierte SQL verwendet eine standardmäßige LEFT JOIN-Klausel. Der RightJoin-Operator steht ebenfalls zur Verfügung. Beide Operatoren ersetzen die bisher erforderliche, umständliche Drei-Methoden-Kette.
Benannte Query-Filter für mehrstufige Filterung
Globale Query-Filter waren seit EF Core 2.0 auf einen einzigen Filter pro Entitätstyp beschränkt. Anwendungen, die sowohl Soft-Delete als auch Multi-Tenancy implementieren, mussten Bedingungen in einem einzigen Ausdruck kombinieren und konnten Filter nicht selektiv deaktivieren. EF Core 10 führt benannte Query-Filter ein.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Invoice>()
.HasQueryFilter("SoftDelete", i => !i.IsDeleted)
.HasQueryFilter("Tenant", i => i.TenantId == _tenantId);
}Admin-Endpunkte können den Soft-Delete-Filter deaktivieren und gleichzeitig die Mandantenisolation aufrechterhalten:
public async Task<List<Invoice>> GetAllIncludingDeletedAsync(CancellationToken ct)
{
return await _context.Invoices
.IgnoreQueryFilters(["SoftDelete"]) // tenant filter still applied
.ToListAsync(ct);
}Damit entfällt die Notwendigkeit für benutzerdefinierte IQueryable-Erweiterungsmethoden, die Filterbedingungen manuell anhängten.
Verbindungsresilienz und Pooling-Konfiguration
Transiente Datenbankfehler – Netzwerkstörungen, Azure-SQL-Failover, Erschöpfung des Verbindungspools – verursachen Exceptions, die häufig Request-Pipelines zum Absturz bringen. EF Core bietet eine integrierte Retry-Logik, deren Standardwerte jedoch explizit konfiguriert werden müssen.
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString, sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(10),
errorNumbersToAdd: null); // retry on all transient errors
sqlOptions.CommandTimeout(30); // 30-second command timeout
}));Auch die Pooling-Parameter im Connection String spielen eine wesentliche Rolle:
Server=db.example.com;Database=AppDb;Min Pool Size=5;Max Pool Size=100;Connection Timeout=15;Min Pool Size hält warme Verbindungen für Lastspitzen bereit. Max Pool Size begrenzt die Gesamtzahl offener Verbindungen, um eine Überlastung der Datenbank zu verhindern. Der Standardwert von 100 eignet sich für die meisten Webanwendungen, doch Hochdurchsatz-Dienste erfordern möglicherweise eine Anpassung basierend auf dem tatsächlichen gleichzeitigen Abfragevolumen.
Automatische Retries funktionieren nicht innerhalb benutzerdefinierter Transaktionen. Die gesamte Transaktion sollte in eine manuelle Retry-Strategie mittels context.Database.CreateExecutionStrategy().ExecuteAsync(...) eingebettet werden, um transiente Fehler über mehrere Operationen hinweg abzufangen.
Indexierungsstrategie und Abfrageanalyse
EF-Core-Migrationen können Indizes deklarativ definieren, doch die Wahl der zu indizierenden Spalten erfordert ein Verständnis der tatsächlichen Abfragemuster.
modelBuilder.Entity<Order>(entity =>
{
// composite index for frequent query pattern
entity.HasIndex(o => new { o.CustomerId, o.Status, o.CreatedAt })
.HasDatabaseName("IX_Order_Customer_Status_Date");
// filtered index for active orders only
entity.HasIndex(o => o.Status)
.HasFilter("[Status] <> 'Cancelled'")
.HasDatabaseName("IX_Order_ActiveStatus");
});Gefilterte Indizes reduzieren die Indexgröße, indem Zeilen ausgeschlossen werden, die Abfragen ohnehin nie ansteuern. Bei einer Tabelle, in der 80 % der Bestellungen storniert sind, kann ein gefilterter Index auf aktive Status bis zu 5-mal kleiner und schneller zu durchsuchen sein.
Zur Analyse des generierten SQL lässt sich in der Entwicklungsumgebung das Logging aktivieren:
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString)
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging());Das protokollierte SQL kann in SQL Server Management Studio mit SET STATISTICS IO ON ausgeführt werden, um logische Lesevorgänge zu prüfen, oder unter PostgreSQL mit EXPLAIN ANALYZE. Fehlende Index-Vorschläge aus dem Abfrageplan zeigen häufig die Optimierungen mit dem größten Wirkungsgrad auf.
Kompilierte Abfragen für stark frequentierte Pfade
LINQ-Ausdrucksbäume werden bei jeder Ausführung geparst und in SQL übersetzt. Bei Abfragen, die tausende Male pro Minute ausgeführt werden (z. B. Authentifizierungsprüfungen, Sitzungsvalidierung), summieren sich diese Übersetzungskosten. Kompilierte Abfragen cachen die Übersetzung beim Anwendungsstart.
public static class UserQueries
{
// compiled once, reused on every call
public static readonly Func<AppDbContext, string, CancellationToken, Task<UserSession?>>
GetActiveSession = EF.CompileAsyncQuery(
(AppDbContext ctx, string token, CancellationToken ct) =>
ctx.UserSessions
.AsNoTracking()
.FirstOrDefault(s => s.Token == token && s.ExpiresAt > DateTime.UtcNow));
}
// Usage in middleware
var session = await UserQueries.GetActiveSession(dbContext, bearerToken, ct);Kompilierte Abfragen überspringen die Phase des Ausdrucksbaum-Parsings vollständig. Der Leistungsunterschied ist allerdings nur auf hochfrequenten Pfaden messbar (1.000+ Aufrufe pro Minute). Für Standard-CRUD-Endpunkte übernimmt der integrierte Query-Cache von EF Core bereits die Wiederverwendung der Übersetzung.
Parametrisierte Collection-Übersetzung in EF Core 10
Abfragen, die nach einer Liste von IDs filtern, gehören zu den am häufigsten verwendeten Mustern im Datenzugriff. EF Core 10 ändert die Standard-Übersetzungsstrategie für parametrisierte Collections. Anstatt die Liste als JSON-Array zu kodieren (EF Core 8-9) oder Konstanten inline zu setzen (EF Core 7 und früher), übersetzt EF Core 10 jeden Wert als separaten SQL-Parameter.
// Before (EF Core 8-9): JSON array parameter
// @__ids_0='[1,2,3]'
// SELECT ... WHERE Id IN (SELECT value FROM OPENJSON(@__ids_0))
// EF Core 10: individual parameters with padding
// SELECT ... WHERE Id IN (@ids1, @ids2, @ids3)
var orderIds = new[] { 101, 205, 389 };
var orders = await _context.Orders
.Where(o => orderIds.Contains(o.Id))
.ToListAsync(ct);Der neue Ansatz liefert dem Datenbankabfrageplaner Kardinalitätsinformationen und vermeidet gleichzeitig durch Parameter-Padding ein Aufblähen des Plan-Caches. Für Fälle, in denen der JSON-Ansatz performanter ist (sehr große Collections), lässt sich das Verhalten pro Abfrage überschreiben:
var orders = await _context.Orders
.Where(o => EF.Parameter(orderIds).Contains(o.Id)) // force JSON mode
.ToListAsync(ct);Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
Fazit
AsNoTracking()undSelect-Projektionen sollten bei jeder schreibgeschützten Abfrage eingesetzt werden, um den Overhead des Change Trackers zu eliminierenAsSplitQuery()empfiehlt sich beim Laden mehrerer Collection-Navigationen, um kartesische Explosionen zu vermeiden- Getrackte Lade-Ändern-Speichern-Muster sollten bei Massenoperationen durch
ExecuteUpdateAsyncundExecuteDeleteAsyncersetzt werden - Der
LeftJoin-Operator aus EF Core 10 ersetzt die umständlicheGroupJoin/SelectMany/DefaultIfEmpty-Kette - Benannte Query-Filter ermöglichen die unabhängige Verwaltung von Soft-Delete und Multi-Tenancy
EnableRetryOnFailuresollte konfiguriert und die Poolgrößen für Produktionsresilienz angepasst werden- Zusammengesetzte und gefilterte Indizes sollten auf tatsächlichen Abfragemustern basieren, nicht auf Vermutungen
- Kompilierte Abfragen sind genuinen Hot Paths mit mehr als 1.000 Ausführungen pro Minute vorbehalten
- Die parametrisierte Collection-Übersetzung von EF Core 10 sollte standardmäßig verwendet und nur dann überschrieben werden, wenn Benchmarks dies rechtfertigen
Weitergehende Informationen: Das EF Core Advanced-Modul behandelt diese Muster im Interview-Kontext, und der Clean-Architecture-Leitfaden für .NET zeigt, wie Repository- und Service-Schichten strukturiert werden, die diese Abfragen kapseln.
Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
Tags
Teilen
Verwandte Artikel

C# und .NET Interview-Fragen: Vollstaendiger Leitfaden 2026
Die 17 haeufigsten C#- und .NET-Interviewfragen. LINQ, async/await, Dependency Injection, Entity Framework Core und Best Practices mit ausfuehrlichen Antworten.

.NET 8: Eine API mit ASP.NET Core erstellen
Vollstaendiger Leitfaden zur Erstellung einer professionellen REST-API mit .NET 8 und ASP.NET Core. Controller, Entity Framework Core, Validierung und Best Practices erklaert.

Rust: Grundlagen fuer erfahrene Entwickler in 2026
Rust schnell erlernen mit vorhandenem Wissen. Ownership, Borrowing, Lifetimes und wesentliche Patterns erklaert fuer Entwickler aus C++, Java oder Python.