# 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. - Published: 2026-04-15 - Updated: 2026-04-15 - Author: SharpSkill - Tags: dotnet, entity framework core, csharp, performance, ef core 10 - Reading time: 12 min --- 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 erfordert .NET 10** > > 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` 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. ```csharp // ProductRepository.cs public async Task> 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: ```csharp // Program.cs builder.Services.AddDbContext(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. ```csharp // OrderRepository.cs public async Task 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. > **Wann Single Queries sinnvoll bleiben** > > 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. ```csharp // PromotionService.cs 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: ```csharp // CleanupService.cs public async Task PurgeExpiredSessionsAsync(CancellationToken ct) { await _context.Sessions .Where(s => s.ExpiresAt < DateTime.UtcNow) .ExecuteDeleteAsync(ct); } ``` ## 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. ```csharp // ReportService.cs public async Task> 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. ```csharp // AppDbContext.cs protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .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: ```csharp // InvoiceRepository.cs public async Task> 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. ```csharp // Program.cs builder.Services.AddDbContext(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. > **Retry-Logik und Transaktionen** > > 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. ```csharp // AppDbContext.cs - OnModelCreating modelBuilder.Entity(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: ```csharp // Program.cs (development only) builder.Services.AddDbContext(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. ```csharp // CompiledQueries.cs public static class UserQueries { // compiled once, reused on every call public static readonly Func> 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. ```csharp // 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: ```csharp var orders = await _context.Orders .Where(o => EF.Parameter(orderIds).Contains(o.Id)) // force JSON mode .ToListAsync(ct); ``` ## Fazit - `AsNoTracking()` und `Select`-Projektionen sollten bei jeder schreibgeschützten Abfrage eingesetzt werden, um den Overhead des Change Trackers zu eliminieren - `AsSplitQuery()` empfiehlt sich beim Laden mehrerer Collection-Navigationen, um kartesische Explosionen zu vermeiden - Getrackte Lade-Ändern-Speichern-Muster sollten bei Massenoperationen durch `ExecuteUpdateAsync` und `ExecuteDeleteAsync` ersetzt werden - Der `LeftJoin`-Operator aus EF Core 10 ersetzt die umständliche `GroupJoin`/`SelectMany`/`DefaultIfEmpty`-Kette - Benannte Query-Filter ermöglichen die unabhängige Verwaltung von Soft-Delete und Multi-Tenancy - `EnableRetryOnFailure` sollte 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](/technologies/dotnet/interview-questions/ef-core-advanced) behandelt diese Muster im Interview-Kontext, und der [Clean-Architecture-Leitfaden für .NET](/blog/dotnet/clean-architecture-dotnet-practical-guide) zeigt, wie Repository- und Service-Schichten strukturiert werden, die diese Abfragen kapseln. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/de/blog/dotnet/ef-core-performance-best-practices