Entity Framework Core: Prestatieoptimalisatie en Best Practices in 2026

Optimaliseer EF Core 10-prestaties met AsNoTracking, split queries, bulkbewerkingen, de nieuwe LeftJoin-operator en benoemde queryfilters. Praktische gids met C#-voorbeelden voor .NET 10-applicaties in productie.

Entity Framework Core prestatieoptimalisatie met databasequeries en .NET 10

Entity Framework Core 10, uitgebracht met .NET 10 LTS in november 2025, introduceert LeftJoin-operatoren, vectorzoekfuncties, benoemde queryfilters en aanzienlijke verbeteringen in SQL-vertaling. Deze gids behandelt de EF Core best practices die directe impact hebben op querysnelheid, geheugenallocatie en schaalbaarheid in productieomgevingen.

EF Core 10 vereist .NET 10

EF Core 10 is een Long-Term Support-release met ondersteuning tot november 2028. Het vereist de .NET 10 SDK en runtime. Applicaties die nog op .NET 8 draaien, dienen EF Core 8 (LTS) te gebruiken totdat de migratie is afgerond.

Query Tracking: wanneer uitschakelen en waarom

Elke aanroep naar DbSet<T> koppelt standaard de opgehaalde entiteiten aan de change tracker. De tracker bewaart een snapshot van de oorspronkelijke eigendomswaarden, berekent verschillen bij SaveChanges en lost identiteitsconflicten op bij navigatie-eigenschappen. Die overhead is overbodig wanneer de gegevens rechtstreeks naar een API-response of een read-only viewmodel stromen.

ProductRepository.cscsharp
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 elimineert de per-entiteit overhead van snapshot-creatie en identiteitsresolutie. In combinatie met Select worden uitsluitend de benodigde kolommen over het netwerk verstuurd. Bij een tabel met 50.000 rijen reduceert dit patroon de geheugenallocaties doorgaans met 40-60% ten opzichte van een volledig getrackte entiteitsquery.

Voor contexten die nooit gegevens wijzigen, kan het standaardgedrag bij registratie worden aangepast:

Program.cscsharp
builder.Services.AddDbContext<CatalogContext>(options =>
    options.UseSqlServer(connectionString)
           .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));

Split Queries ter voorkoming van cartesische explosie

Het laden van een entiteit met meerdere collectie-navigaties via Include genereert een enkel SQL-statement met JOINs. Wanneer twee of meer collecties tegelijkertijd worden geladen, groeit de resultset als een cartesisch product van de collecties, waardoor de bovenliggende rijgegevens worden gedupliceerd over elke combinatie.

OrderRepository.cscsharp
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 splitst het laden op in afzonderlijke SQL-statements per navigatie. De afweging: meerdere databaseroundtrips in plaats van één enkele, maar elke resultset blijft compact en vermijdt het duplicatieprobleem. EF Core 10 verhelpt daarnaast een langdurige inconsistentie in de volgorde van split queries, waardoor de volgorde in subqueries overeenkomt met de primaire query.

Wanneer enkele queries de voorkeur verdienen

Enkele queries blijven geschikter bij het laden van één enkele collectie-navigatie of wanneer de roundtrip-latentie hoog is (cross-regio databaseverbindingen). Het is raadzaam om beide modi te benchmarken voor het specifieke toegangspatroon alvorens een keuze te maken.

Bulkbewerkingen met ExecuteUpdate en ExecuteDelete

Traditionele EF-workflows laden entiteiten, passen eigenschappen aan en roepen vervolgens SaveChanges aan. Bij bulkbewerkingen die duizenden rijen raken, levert dit duizenden getrackte instanties en individuele UPDATE-statements op. EF Core 7 introduceerde ExecuteUpdateAsync en ExecuteDeleteAsync om de bewerking naar een enkel SQL-commando te verplaatsen. EF Core 10 vereenvoudigt de API verder door een gewone lambda te accepteren in plaats van een expression tree.

PromotionService.cscsharp
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
}

Dit vertaalt zich naar een enkel UPDATE ... SET ... WHERE-statement. Er worden geen entiteiten in het geheugen geladen. Bij een update van 10.000 rijen daalt de uitvoeringstijd van seconden (getrackte aanpak) naar milliseconden.

Hetzelfde patroon geldt voor verwijderingen:

CleanupService.cscsharp
public async Task PurgeExpiredSessionsAsync(CancellationToken ct)
{
    await _context.Sessions
        .Where(s => s.ExpiresAt < DateTime.UtcNow)
        .ExecuteDeleteAsync(ct);
}

Klaar om je .NET gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

De LeftJoin-operator in EF Core 10

Vóór EF Core 10 vereiste het uitvoeren van een LEFT JOIN de combinatie van GroupJoin, SelectMany en DefaultIfEmpty in een specifiek patroon dat de meeste ontwikkelaars telkens opnieuw moesten opzoeken. EF Core 10 voegt LeftJoin toe als eersteklas LINQ-operator.

ReportService.cscsharp
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);
}

De gegenereerde SQL maakt gebruik van een standaard LEFT JOIN-clausule. De RightJoin-operator is eveneens beschikbaar. Beide operatoren elimineren de omslachtige drie-methode-keten die voorheen noodzakelijk was.

Benoemde queryfilters voor gescheiden filterconcerns

Globale queryfilters waren sinds EF Core 2.0 beperkt tot één enkel filter per entiteitstype. Applicaties die zowel soft-delete als multi-tenancy implementeerden, moesten voorwaarden samenvoegen tot één enkele expressie en konden deze niet selectief uitschakelen. EF Core 10 introduceert benoemde queryfilters.

AppDbContext.cscsharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Invoice>()
        .HasQueryFilter("SoftDelete", i => !i.IsDeleted)
        .HasQueryFilter("Tenant", i => i.TenantId == _tenantId);
}

Beheerders-endpoints kunnen het soft-delete-filter uitschakelen terwijl de tenantisolatie actief blijft:

InvoiceRepository.cscsharp
public async Task<List<Invoice>> GetAllIncludingDeletedAsync(CancellationToken ct)
{
    return await _context.Invoices
        .IgnoreQueryFilters(["SoftDelete"])  // tenant filter still applied
        .ToListAsync(ct);
}

Dit maakt aangepaste IQueryable-extensiemethoden overbodig die handmatig filtercondities toevoegden.

Verbindingsweerbaarheid en poolingconfiguratie

Tijdelijke databasefouten (netwerkonderbrekingen, Azure SQL-failovers, uitputting van de verbindingspool) veroorzaken exceptions die veelal de request-pipeline laten crashen. EF Core biedt ingebouwde retry-logica, maar de standaardinstellingen vereisen expliciete configuratie.

Program.cscsharp
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
    }));

Poolingparameters in de verbindingsstring zijn evenzeer van belang:

text
Server=db.example.com;Database=AppDb;Min Pool Size=5;Max Pool Size=100;Connection Timeout=15;

Min Pool Size houdt warme verbindingen gereed voor pieken in het verkeer. Max Pool Size begrenst het totale aantal open verbindingen om overbelasting van de database te voorkomen. De standaardwaarde van 100 volstaat voor de meeste webapplicaties, maar diensten met hoge doorvoer vereisen mogelijk afstemming op basis van het daadwerkelijke gelijktijdige queryvolume.

Retry-logica en transacties

Automatische retries werken niet binnen door de gebruiker geopende transacties. Omhul de gehele transactie in een handmatige retrystrategie via context.Database.CreateExecutionStrategy().ExecuteAsync(...) om tijdelijke fouten over meerdere bewerkingen heen af te handelen.

Indexeringsstrategie en queryanalyse

EF Core-migraties kunnen indexes declaratief definiëren, maar de keuze welke kolommen te indexeren vereist inzicht in querypatronen.

AppDbContext.cs - OnModelCreatingcsharp
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");
});

Gefilterde indexes verkleinen de indexgrootte door rijen uit te sluiten die queries nooit treffen. Bij een tabel waarin 80% van de orders is geannuleerd, kan een gefilterde index op actieve statussen tot 5x kleiner en sneller te doorzoeken zijn.

Om de gegenereerde SQL te analyseren, kan logging in de ontwikkelomgeving worden ingeschakeld:

Program.cs (development only)csharp
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString)
           .LogTo(Console.WriteLine, LogLevel.Information)
           .EnableSensitiveDataLogging());

Kopieer de gelogde SQL naar SQL Server Management Studio en voer deze uit met SET STATISTICS IO ON om logische reads te controleren, of gebruik EXPLAIN ANALYZE op PostgreSQL. Ontbrekende indexsuggesties uit het queryplan onthullen doorgaans de optimalisatiemogelijkheden met de grootste impact.

Gecompileerde queries voor veelgebruikte paden

LINQ-expression trees worden bij elke uitvoering opnieuw geparsed en vertaald naar SQL. Bij queries die duizenden keren per minuut worden uitgevoerd (bijvoorbeeld authenticatiecontroles of sessievalidatie), stapelen de vertaalkosten zich op. Gecompileerde queries cachen de vertaling bij het opstarten van de applicatie.

CompiledQueries.cscsharp
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);

Gecompileerde queries slaan de expression-tree-parsingfase volledig over. Het prestatieverschil is alleen meetbaar bij paden met hoge frequentie (1.000+ aanroepen per minuut). Voor standaard CRUD-endpoints zorgt de ingebouwde querycache van EF Core al voor hergebruik van vertalingen.

Geparametriseerde collectievertaling in EF Core 10

Queries die filteren op een lijst van ID's behoren tot de meest voorkomende patronen in datatoegang. EF Core 10 wijzigt de standaard vertaalstrategie voor geparametriseerde collecties. In plaats van de lijst als JSON-array te coderen (EF Core 8-9) of constanten inline te plaatsen (EF Core 7 en eerder), vertaalt EF Core 10 elke waarde als een afzonderlijke 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);

De nieuwe aanpak geeft de queryplanner van de database cardinaliteitsinformatie, terwijl opzwelling van de plancache wordt voorkomen door parameter-padding. Voor gevallen waarin de JSON-aanpak beter presteert (zeer grote collecties), kan het gedrag per query worden overschreven:

csharp
var orders = await _context.Orders
    .Where(o => EF.Parameter(orderIds).Contains(o.Id))  // force JSON mode
    .ToListAsync(ct);

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Conclusie

  • Gebruik AsNoTracking() en Select-projecties bij elke read-only query om de overhead van de change tracker te elimineren
  • Pas AsSplitQuery() toe bij het laden van meerdere collectie-navigaties om cartesische explosie te voorkomen
  • Vervang het patroon van laden-wijzigen-opslaan door ExecuteUpdateAsync en ExecuteDeleteAsync voor bulkbewerkingen
  • Maak gebruik van de LeftJoin-operator uit EF Core 10 ter vervanging van de omslachtige GroupJoin/SelectMany/DefaultIfEmpty-keten
  • Configureer benoemde queryfilters om soft-delete en multi-tenancy onafhankelijk van elkaar te beheren
  • Stel EnableRetryOnFailure in en stem verbindingspoolgroottes af voor productieweerbaarheid
  • Definieer samengestelde en gefilterde indexes op basis van daadwerkelijke querypatronen, niet op basis van aannames
  • Reserveer gecompileerde queries voor daadwerkelijk veelgebruikte paden met meer dan 1.000 uitvoeringen per minuut
  • Laat EF Core 10 de geparametriseerde collectievertaling standaard afhandelen en overschrijf alleen wanneer benchmarks dat rechtvaardigen

Verder lezen: de EF Core Advanced-module behandelt deze patronen in een sollicitatiecontext, en de Clean Architecture met .NET-gids laat zien hoe de repository- en servicelagen die deze queries omhullen, gestructureerd worden.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Tags

#dotnet
#entity framework core
#csharp
#performance
#ef core 10

Delen

Gerelateerde artikelen