Entity Framework Core : Optimiser les performances

EF Core est puissant mais peut devenir lent si mal utilisé. Voici les optimisations que j'applique systématiquement en production.

Le problème N+1

Le piège classique qui tue les performances :

Problème :
// 1 requête pour les commandes + N requêtes pour les clients
var orders = await _context.Orders.ToListAsync();
foreach (var order in orders)
{
    Console.WriteLine(order.Customer.Name); // Requête à chaque itération !
}

Solution : Include (Eager Loading)

// 1 seule requête avec JOIN
var orders = await _context.Orders
    .Include(o => o.Customer)
    .ToListAsync();

Solution : Projection

// Encore mieux : ne récupère que ce dont on a besoin
var orderDtos = await _context.Orders
    .Select(o => new OrderDto
    {
        Id = o.Id,
        CustomerName = o.Customer.Name,
        Total = o.Total
    })
    .ToListAsync();

Désactiver le tracking

Par défaut, EF Core "track" toutes les entités pour détecter les changements. Coûteux en lecture seule :

// Pour une requête spécifique
var products = await _context.Products
    .AsNoTracking()
    .ToListAsync();

// Pour tout le DbContext (lecture seule)
_context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

Split Queries

Pour les Include multiples, évitez les jointures cartésiennes :

// Peut exploser en taille si beaucoup de données
var orders = await _context.Orders
    .Include(o => o.Items)
    .Include(o => o.Payments)
    .ToListAsync();

// Mieux : requêtes séparées
var orders = await _context.Orders
    .Include(o => o.Items)
    .Include(o => o.Payments)
    .AsSplitQuery()
    .ToListAsync();

Projections vs Entités complètes

Ne chargez que les colonnes nécessaires :

// Mauvais : charge toutes les colonnes
var customers = await _context.Customers.ToListAsync();

// Bon : charge uniquement Id et Name
var customerNames = await _context.Customers
    .Select(c => new { c.Id, c.Name })
    .ToListAsync();

Compiled Queries

Pour les requêtes fréquentes, pré-compilez :

private static readonly Func<AppDbContext, int, Task<Product?>> _getProductById =
    EF.CompileAsyncQuery((AppDbContext ctx, int id) =>
        ctx.Products.FirstOrDefault(p => p.Id == id));

// Utilisation
var product = await _getProductById(_context, productId);

Raw SQL quand nécessaire

Parfois, le SQL brut est plus efficace :

// Pour des requêtes complexes
var products = await _context.Products
    .FromSqlRaw(@"
        SELECT p.* FROM Products p
        INNER JOIN Categories c ON p.CategoryId = c.Id
        WHERE c.IsActive = 1 AND p.Price > {0}", minPrice)
    .ToListAsync();

// Pour les opérations bulk
await _context.Database.ExecuteSqlRawAsync(
    "UPDATE Products SET Price = Price * 1.1 WHERE CategoryId = {0}", categoryId);

Indexation

Configurez vos index dans OnModelCreating :

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>()
        .HasIndex(p => p.Sku)
        .IsUnique();

    modelBuilder.Entity<Order>()
        .HasIndex(o => new { o.CustomerId, o.CreatedAt });
}

Outils de diagnostic

Conseil : Activez les warnings EF Core pendant le développement pour détecter les problèmes tôt :
optionsBuilder.ConfigureWarnings(w =>
    w.Throw(RelationalEventId.MultipleCollectionIncludeWarning));

Conclusion

EF Core peut être très performant si utilisé correctement. Les trois règles d'or : éviter N+1, projeter au lieu de charger, désactiver le tracking en lecture.

Besoin d'un audit performance de votre application .NET ? Contactez-moi.

Davy Abderrahman

Davy Abderrahman

Expert .NET et Entity Framework. Optimisation de bases de données en production.

En savoir plus