En clairEntity 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
| Outil | Forces | Usage |
| EF Core | Suivi des modifications, LINQ, migrations, concurrence optimiste | Côté écriture (commandes, agrégats) |
| Dapper | SQL écrit à la main, mapping minimal, très rapide | Lectures complexes, rapports, procédures stockées |
ADO.NET (SqlClient, Npgsql) | Contrôle total, bulk copy | Imports 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é
var orders = await db.Orders.ToListAsync(ct);
foreach (var o in orders) total += o.Lines.Sum(l => l.Qty);
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);
await db.Orders.Where(o => o.Status == OrderStatus.Draft && o.CreatedAt < limit)
.ExecuteUpdateAsync(s => s.SetProperty(o => o.Status, OrderStatus.Expired), ct);
| Technique | Gain |
AsNoTracking() en lecture | Pas de snapshot en mémoire, moins de CPU |
Projection Select vers DTO | Colonnes utiles seulement, pas de Include |
AsSplitQuery() | Évite l'explosion cartésienne avec plusieurs Include de collections |
AddDbContextPool | Réutilise les instances de contexte sur les API à fort trafic |
ExecuteUpdate / ExecuteDelete | Opé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
var rows = await db.Database
.SqlQuery<CustomerBalance>($"EXEC dbo.usp_GetBalance @CustomerId = {customerId}")
.ToListAsync(ct);
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'isolation | Empêche | Remarque |
| Read Committed | Lectures sales | Défaut SQL Server et PostgreSQL |
| Read Committed Snapshot (RCSI) | Lectures sales, sans bloquer les écrivains | Fortement recommandé sur SQL Server (défaut sur Azure SQL, pas on-premise) |
| Repeatable Read | Lectures non répétables | Plus de verrous |
| Serializable | Lectures fantômes | Risque élevé d'interblocages |
public sealed class Order
{
[Timestamp] public byte[] RowVersion { get; set; } = [];
}
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é
| Cache | Portée | Point clé |
IMemoryCache | Instance | Toujours fixer expiration et SizeLimit |
IDistributedCache (Redis, Memorystore) | Cluster | Sé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 caching | Réponses HTTP | Invalidation 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.