MVAESTECH
Guide .NET · 08 · Applications

Publié le · par Mathieu Vaes

Données & EF Core

La base est presque toujours le goulot d'étranglement. Savoir ce que l'ORM envoie réellement au serveur est non négociable.

Chapitres du guide
En clair

Entity Framework traduit votre code C# en requêtes SQL. C'est pratique, mais il peut générer des requêtes inefficaces si on ne regarde pas ce qu'il envoie, par exemple 101 requêtes au lieu d'une. La règle : ne demander à la base que ce dont on a besoin, et vérifier le SQL produit.

Choisir l'outil d'accès Junior

OutilForcesUsage
EF CoreSuivi des modifications, LINQ, migrations, concurrence optimisteCôté écriture (commandes, agrégats)
DapperSQL écrit à la main, mapping minimal, très rapideLectures complexes, rapports, procédures stockées
ADO.NET (SqlClient, Npgsql)Contrôle total, bulk copyImports massifs, cas extrêmes

Combiner EF Core (écriture) et Dapper (lecture) dans la même appli est un choix CQRS courant et sain.

EF Core performant Confirmé

// ❌ N+1 : une requête pour les commandes, puis une par commande pour les lignes
var orders = await db.Orders.ToListAsync(ct);
foreach (var o in orders) total += o.Lines.Sum(l => l.Qty);   // lazy loading

// ✅ Projection : une seule requête, seulement les colonnes utiles, sans suivi
var page = await db.Orders
    .AsNoTracking()
    .Where(o => o.CustomerId == customerId && o.CreatedAt < cursor)
    .OrderByDescending(o => o.CreatedAt)
    .Take(50)
    .Select(o => new OrderSummary(o.Id, o.CreatedAt, o.Lines.Sum(l => l.Qty * l.UnitPrice)))
    .ToListAsync(ct);

// ✅ Mise à jour en masse sans charger les entités (EF Core 7+)
await db.Orders.Where(o => o.Status == OrderStatus.Draft && o.CreatedAt < limit)
               .ExecuteUpdateAsync(s => s.SetProperty(o => o.Status, OrderStatus.Expired), ct);
TechniqueGain
AsNoTracking() en lecturePas de snapshot en mémoire, moins de CPU
Projection Select vers DTOColonnes utiles seulement, pas de Include
AsSplitQuery()Évite l'explosion cartésienne avec plusieurs Include de collections
AddDbContextPoolRéutilise les instances de contexte sur les API à fort trafic
ExecuteUpdate / ExecuteDeleteOpérations ensemblistes en un seul ordre SQL
Journal SQL en dev (LogTo, niveau Information sur Microsoft.EntityFrameworkCore.Database.Command)Voir le SQL généré, toujours

Database First et procédures stockées

dotnet ef dbcontext scaffold "Name=ConnectionStrings:Shop" Microsoft.EntityFrameworkCore.SqlServer \
  --context ShopDbContext --output-dir Entities --context-dir . --no-onconfiguring --force
// Interpolation = paramètres SQL : protégé contre l'injection
var rows = await db.Database
    .SqlQuery<CustomerBalance>($"EXEC dbo.usp_GetBalance @CustomerId = {customerId}")
    .ToListAsync(ct);

// ❌ FromSqlRaw avec concaténation de chaîne = injection SQL
// db.Orders.FromSqlRaw("SELECT * FROM Orders WHERE Ref = '" + input + "'")

Après un re-scaffold, la configuration personnalisée se perd si elle est dans les fichiers générés : placez-la dans des classes partial ou des IEntityTypeConfiguration<T> séparées.

Transactions et concurrence Senior

Niveau d'isolationEmpêcheRemarque
Read CommittedLectures salesDéfaut SQL Server et PostgreSQL
Read Committed Snapshot (RCSI)Lectures sales, sans bloquer les écrivainsFortement recommandé sur SQL Server (défaut sur Azure SQL, pas on-premise)
Repeatable ReadLectures non répétablesPlus de verrous
SerializableLectures fantômesRisque élevé d'interblocages
public sealed class Order
{
    [Timestamp] public byte[] RowVersion { get; set; } = [];   // SQL Server rowversion
}
// SaveChanges lève DbUpdateConcurrencyException si la ligne a changé entre lecture et écriture
// → répondre 409 ou 412 au client

Migrations en production Confirmé

  • Générez un script idempotent relu en revue : dotnet ef migrations script --idempotent, ou un migration bundle exécuté par la CI.
  • N'appelez pas Database.Migrate() au démarrage quand plusieurs instances démarrent ensemble : courses entre réplicas, et l'appli a besoin de droits DDL.
  • Changements de schéma en deux temps (expand / contract) : ajouter la colonne, déployer le code compatible, migrer les données, puis supprimer l'ancienne colonne.

Cache Confirmé

CachePortéePoint clé
IMemoryCacheInstanceToujours fixer expiration et SizeLimit
IDistributedCache (Redis, Memorystore)ClusterSérialisation, latence réseau
HybridCache (.NET 9+)L1 mémoire + L2 distribuéProtection contre le stampede : un seul calcul pour N appelants simultanés
Output cachingRéponses HTTPInvalidation par tags
Piège
  • Pool de connexions saturé : 100 connexions par défaut par chaîne de connexion et par processus avec SqlClient. Multipliez par le nombre d'instances et comparez à la limite du serveur (Cloud SQL notamment).
  • ToList() trop tôt : filtrer après matérialisation charge toute la table en mémoire.
  • Évaluation côté client : une méthode C# non traduisible dans un Where lève une exception (EF Core 3+), ne la contournez pas par un AsEnumerable() en amont du filtre.
  • Dates : stocker en UTC (DateTimeOffset ou datetime2 + convention UTC), convertir à l'affichage.

À retenir

  • Lire le SQL généré, projeter, AsNoTracking en lecture.
  • Concurrence optimiste par rowversion, RCSI activé sur SQL Server.
  • Migrations par script ou bundle en CI, jamais au démarrage multi-instance.

Un projet .NET à cadrer ou à reprendre ?

API REST documentées et testées, applications ASP.NET MVC (y compris VB.NET), migration de services anciens vers une architecture claire : parlons de votre code.