Quoi de neuf dans EF Core 9 : les fonctionnalités que vous devez connaître
Entity Framework Core 9 a été livré avec .NET 9 en novembre 2024, et après avoir passé beaucoup de temps à travailler avec lui sur plusieurs projets, je peux dire que c’est l’une des versions les plus significatives depuis un certain temps. Non pas parce qu’il réinvente la roue, mais parce qu’il peaufine les domaines dans lesquels EF Core a historiquement causé le plus de frictions : la traduction des requêtes, les performances et l’utilisation de modèles de données modernes.
Dans cet article, je passerai en revue les fonctionnalités qui ont eu le plus grand impact sur mon travail quotidien. Si vous utilisez toujours EF Core 8 (ou même 7), cela devrait vous donner une idée claire de ce qui vous attend de l’autre côté de la mise à niveau.
EF Core 9 dans l’écosystème .NET 9
EF Core 9 cible .NET 8 et .NET 9, ce qui signifie que vous n’avez pas nécessairement besoin de mettre à niveau l’intégralité de votre application vers .NET 9 pour profiter de la plupart de ces fonctionnalités. Cela dit, certaines améliorations de l’AOT et des performances sont étroitement liées aux modifications du runtime .NET 9, vous en tirerez donc le meilleur parti en allant jusqu’au bout.
La version suit la cadence impaire/pair établie par Microsoft : les versions impaires (comme .NET 9) bénéficient d’un support à terme standard (STS) avec 18 mois de support, tandis que les versions paires (comme .NET 8) bénéficient d’un support à long terme (LTS). Gardez cela à l’esprit lors de la planification de votre calendrier de mise à niveau.
Améliorations de la traduction LINQ
C’est là que la plupart des développeurs ressentiront immédiatement la différence. EF Core 9 a fait des progrès significatifs dans la traduction d’expressions LINQ en SQL qui ont réellement du sens.
Meilleures traductions GroupBy
Si vous avez déjà écrit une requête GroupBy dans EF Core et que vous vous êtes retrouvé avec des avertissements d’évaluation côté client ou du SQL bizarre, vous connaissez la douleur. EF Core 9 gère un ensemble beaucoup plus large de scénarios GroupBy directement dans SQL.
var salesByCategory = await context.Products
.GroupBy(p => p.Category.Name)
.Select(g => new
{
Category = g.Key,
TotalRevenue = g.Sum(p => p.Price * p.UnitsSold),
AveragePrice = g.Average(p => p.Price),
ProductCount = g.Count()
})
.OrderByDescending(x => x.TotalRevenue)
.ToListAsync();
Dans les versions précédentes, les requêtes impliquant des agrégations sur les propriétés de navigation à l’intérieur de GroupBy revenaient parfois à l’évaluation du client. EF Core 9 traduit cela proprement en une seule requête SQL avec GROUP BY, SUM, AVG et COUNT.
Projections et sous-requêtes complexes
Les sous-requêtes imbriquées et les projections complexes ont également bénéficié d’une sérieuse mise à niveau. Considérez quelque chose comme ceci :
var orderSummaries = await context.Customers
.Select(c => new CustomerSummaryDto
{
Name = c.FullName,
TotalOrders = c.Orders.Count(),
MostRecentOrder = c.Orders
.OrderByDescending(o => o.OrderDate)
.Select(o => new OrderBriefDto
{
Id = o.Id,
Date = o.OrderDate,
Total = o.LineItems.Sum(li => li.Quantity * li.UnitPrice)
})
.FirstOrDefault(),
TopCategory = c.Orders
.SelectMany(o => o.LineItems)
.GroupBy(li => li.Product.Category.Name)
.OrderByDescending(g => g.Count())
.Select(g => g.Key)
.FirstOrDefault()
})
.ToListAsync();
EF Core 9 peut désormais traduire l’intégralité de cette expression en SQL sans déclencher une évaluation côté client. La requête générée utilise des sous-requêtes corrélées et des jointures latérales, le cas échéant, et le plan SQL est considérablement plus efficace que ce que produisaient les versions antérieures.
Collections primitives paramétrées
L’une des améliorations remarquables de LINQ est la possibilité de transmettre des collections de valeurs primitives directement dans les requêtes :
var statusFilter = new List<string> { "Active", "Pending", "Review" };
var filteredOrders = await context.Orders
.Where(o => statusFilter.Contains(o.Status))
.ToListAsync();
Dans EF Core 8, cela a été traduit à l’aide de clauses IN avec des valeurs intégrées, ce qui signifiait que le cache du plan de requête ne pouvait pas être réutilisé lorsque la liste était modifiée. EF Core 9 paramétre correctement ces collections, en les envoyant sous forme de paramètre structuré. C’est un gros problème pour la mise en cache du plan de requête sur SQL Server et PostgreSQL.## Opérations groupées – ExecuteUpdate et ExecuteDelete
ExecuteUpdate et ExecuteDelete ont été introduits dans EF Core 7, mais EF Core 9 étend ce que vous pouvez en faire de manière significative.
Expressions de mise à jour plus complexes
Vous pouvez désormais utiliser des expressions plus complexes dans ExecuteUpdate, y compris des références à d’autres tables via les propriétés de navigation :
await context.Products
.Where(p => p.Category.IsDiscontinued)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.IsAvailable, false)
.SetProperty(p => p.DiscontinuedDate, DateTimeOffset.UtcNow)
.SetProperty(p => p.Price, p => p.Price * 0.5m));
Cela génère une seule instruction UPDATE avec un JOIN dans la table des catégories — pas besoin de charger des entités en mémoire, pas de surcharge de suivi des modifications.
Suppressions groupées conditionnelles avec sous-requêtes
Les suppressions groupées avec des filtres de sous-requête sont désormais entièrement prises en charge :
await context.AuditLogs
.Where(log => log.CreatedAt < DateTime.UtcNow.AddYears(-2))
.Where(log => !context.ProtectedRecords
.Any(pr => pr.AuditLogId == log.Id))
.ExecuteDeleteAsync();
Cela se traduit par un DELETE avec une sous-requête NOT EXISTS, exactement ce que vous écririez à la main. Aucune entité chargée, aucun aller-retour.
Améliorations des colonnes JSON
Les colonnes JSON sont l’une des fonctionnalités les plus intéressantes des récentes versions d’EF Core, et EF Core 9 les pousse encore plus loin.
Interrogation dans JSON
Vous pouvez désormais filtrer et projeter des données à partir de colonnes JSON avec une meilleure prise en charge de la traduction :
public class Order
{
public int Id { get; set; }
public string CustomerName { get; set; }
public ShippingAddress Address { get; set; } // Stored as JSON
public List<OrderNote> Notes { get; set; } // Stored as JSON
}
public class ShippingAddress
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
public string Country { get; set; }
}
public class OrderNote
{
public DateTime CreatedAt { get; set; }
public string Text { get; set; }
public string Author { get; set; }
}
Configuration dans votre DbContext :
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(builder =>
{
builder.OwnsOne(o => o.Address, ab =>
{
ab.ToJson();
});
builder.OwnsMany(o => o.Notes, nb =>
{
nb.ToJson();
});
});
}
Vous pouvez désormais interroger directement dans JSON :
var newYorkOrders = await context.Orders
.Where(o => o.Address.State == "NY")
.OrderByDescending(o => o.Notes.Count)
.Select(o => new
{
o.CustomerName,
o.Address.City,
LatestNote = o.Notes
.OrderByDescending(n => n.CreatedAt)
.Select(n => n.Text)
.FirstOrDefault()
})
.ToListAsync();
EF Core 9 génère des appels JSON_VALUE et JSON_QUERY appropriés sur SQL Server (ou équivalent sur d’autres fournisseurs), et la traduction couvre une gamme beaucoup plus large d’opérations LINQ sur les éléments JSON qu’auparavant.
Mise à jour des propriétés JSON
L’un des points de friction dans EF Core 8 était que la mise à jour d’une seule propriété dans une colonne JSON entraînerait la réécriture de l’intégralité du document JSON. EF Core 9 améliore cela avec un suivi des modifications plus granulaire pour les types mappés JSON, générant des mises à jour plus ciblées lorsque cela est possible.
var order = await context.Orders.FindAsync(orderId);
order.Address.ZipCode = "10001";
await context.SaveChangesAsync();
Sur les fournisseurs pris en charge, cela peut générer une modification JSON plus ciblée plutôt que de réécrire l’intégralité du blob.
Types complexes — Objets de valeur sans identité
Les types complexes sont l’une des fonctionnalités attendues par les praticiens du Domain-Driven Design. Contrairement aux types détenus, les types complexes n’ont pas d’identité : ce sont de purs objets de valeur.
[ComplexType]
public record Money(decimal Amount, string Currency);
[ComplexType]
public record DateRange(DateTime Start, DateTime End);
public class Project
{
public int Id { get; set; }
public string Name { get; set; }
public Money Budget { get; set; }
public DateRange Timeline { get; set; }
}
Ceux-ci sont stockés sous forme de colonnes aplaties dans la table parent — Budget_Amount, Budget_Currency, Timeline_Start, Timeline_End — sans nécessiter de table séparée ni aucun type de clé.
La principale différence avec les types détenus : les types complexes sont comparés par valeur et non par référence. Deux instances de Money avec les mêmes Amount et Currency sont considérées comme égales, quelle que soit l’entité à laquelle elles appartiennent.
var expensiveProjects = await context.Projects
.Where(p => p.Budget.Amount > 100_000m && p.Budget.Currency == "USD")
.OrderByDescending(p => p.Budget.Amount)
.ToListAsync();
Cela se traduit directement par un filtrage sur les colonnes aplaties : propre, efficace et exactement ce à quoi vous vous attendez.
Prise en charge de HierarchyId pour SQL Server
Si vous avez déjà travaillé avec des données hiérarchiques dans SQL Server (organigrammes, arborescences de catégories, systèmes de fichiers), vous savez que HierarchyId est le type intégré pour cela. EF Core 9 lui apporte un support de première classe.
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public string Title { get; set; }
public HierarchyId PathFromCeo { get; set; }
}
Vous pouvez désormais interroger directement les relations hiérarchiques :
var managerId = HierarchyId.Parse("/1/3/");
// All direct and indirect reports
var allReports = await context.Employees
.Where(e => e.PathFromCeo.IsDescendantOf(managerId))
.Where(e => e.PathFromCeo != managerId) // exclude the manager
.OrderBy(e => e.PathFromCeo)
.ToListAsync();
// Direct reports only (one level down)
var directReports = await context.Employees
.Where(e => e.PathFromCeo.GetAncestor(1) == managerId)
.ToListAsync();
// Get an employee's depth in the hierarchy
var employeesWithDepth = await context.Employees
.Select(e => new
{
e.Name,
e.Title,
Level = e.PathFromCeo.GetLevel()
})
.OrderBy(e => e.Level)
.ToListAsync();
```Tous ces éléments se traduisent par les méthodes `HierarchyId` natives de SQL Server. Si vous avez implémenté des structures arborescentes avec des clés étrangères auto-référencées et des CTE récursives, il s'agit d'une approche beaucoup plus propre.
## Modèles compilés et prise en charge AOT
Les développeurs soucieux des performances apprécieront l'investissement continu dans les modèles compilés et la prise en charge de la compilation anticipée (AOT).
### Modèles compilés
Les modèles compilés prégénérent les métadonnées du modèle qu’EF Core génère normalement au démarrage. Pour les grands modèles (pensez à des centaines d’entités), cela peut réduire considérablement le temps de démarrage à froid.
```bash
dotnet ef dbcontext optimize --output-dir CompiledModels --namespace MyApp.CompiledModels
Puis câblez-le :
builder.Services.AddDbContext<AppDbContext>(options =>
options
.UseSqlServer(connectionString)
.UseModel(MyApp.CompiledModels.AppDbContextModel.Instance));
Dans EF Core 9, les modèles compilés sont plus complets : ils prennent en charge davantage de fonctionnalités de mappage et génèrent une sortie plus petite. Pour un modèle comportant environ 400 entités, le temps de démarrage peut passer de quelques secondes à quasi-instantané.
Progression de la compilation AOT
La prise en charge native complète de l’AOT pour EF Core est toujours en cours, mais EF Core 9 fait des progrès significatifs. De nombreux chemins de code nécessitant beaucoup de réflexion ont été refactorisés pour être faciles à découper, et les modèles compilés sont un élément clé de l’histoire d’AOT. Si vous ciblez des scénarios comme Azure Functions ou des microservices où le démarrage à froid est important, ces améliorations sont directement pertinentes.
Mises à jour du fournisseur Cosmos DB
Le fournisseur Azure Cosmos DB continue de mûrir avec EF Core 9. Quelques améliorations notables :
Gestion des clés de partition
Le fournisseur prend désormais en charge les clés de partition hiérarchiques et gère les filtres de clés de partition de manière plus intelligente :
public class TenantDocument
{
public string Id { get; set; }
public string TenantId { get; set; } // Partition key
public string Region { get; set; } // Sub-partition key
public string Content { get; set; }
}
// This query now correctly uses the partition key for routing
var docs = await context.TenantDocuments
.Where(d => d.TenantId == "tenant-42" && d.Region == "us-east")
.Where(d => d.Content.Contains("important"))
.ToListAsync();
Traduction améliorée de LINQ vers NoSQL
Un plus grand nombre d’opérations LINQ se traduisent désormais dans le dialecte SQL de Cosmos DB, notamment une meilleure prise en charge de Contains, Any, des opérations sur tableaux imbriqués et des fonctions mathématiques. Les requêtes qui revenaient auparavant à l’évaluation du client sont désormais traitées côté serveur.
Prise en charge de la recherche de vecteurs
EF Core 9 introduit une prise en charge anticipée de la recherche de similarité vectorielle avec Cosmos DB, ce qui est utile si vous créez des applications qui s’intègrent aux intégrations ou à la recherche basée sur l’IA :
var results = await context.Documents
.OrderBy(d => EF.Functions.VectorDistance(d.Embedding, queryVector))
.Take(10)
.ToListAsync();
## Améliorations de la migration
Les migrations ont bénéficié d’améliorations en matière de qualité de vie qui rendent leur utilisation moins pénible dans des environnements d’équipe.
Tables temporelles dans les migrations
Les migrations gèrent désormais la configuration des tables temporelles de manière plus gracieuse, avec une prise en charge appropriée des colonnes de période et de la dénomination des tables d’historique :
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>()
.ToTable("Employees", b => b.IsTemporal(t =>
{
t.HasPeriodStart("ValidFrom");
t.HasPeriodEnd("ValidTo");
t.UseHistoryTable("EmployeeHistory");
}));
}
Scripts idempotents
La commande Script-Migration (et son équivalent CLI) produit par défaut de meilleurs scripts idempotents, avec une gestion améliorée des cas extrêmes autour des modifications de schéma qui dépendent des données existantes dans certains états.
Offres groupées de migration
Les bundles de migration, qui regroupent vos migrations dans un exécutable autonome pour le déploiement, sont plus fiables dans EF Core 9 avec un meilleur rapport d’erreurs et une logique de nouvelle tentative pour les échecs transitoires.
dotnet ef migrations bundle --self-contained -r linux-x64
Cela produit un binaire que vous pouvez exécuter dans votre pipeline CI/CD sans avoir besoin du SDK .NET installé sur votre cible de déploiement.
## Benchmarks de performancesVoici quelques repères approximatifs issus de mes propres tests. Il s’agit d’un projet comprenant environ 200 entités, exécuté sur SQL Server 2022, mesuré avec BenchmarkDotNet. Vos chiffres varieront, mais les améliorations relatives devraient être similaires.
| Scénario | EF Core 8 | EF Core 9 | Amélioration |
|---|---|---|---|
| Construction du modèle (démarrage à froid) | 1 850 ms | 320 ms | ~5,8x plus rapide (compilé) |
| Requête simple (entité unique par PK) | 0,42 ms | 0,38 ms | ~10 % plus rapide |
| Requête complexe (jointures + agrégation) | 3,1 ms | 2,4 ms | ~23 % plus rapide |
| Mise à jour groupée (10 000 lignes) | 145 ms | 118 ms | ~19 % plus rapide |
| Requête de colonne JSON | 2,8 ms | 1,9 ms | ~32% plus rapide |
| SaveChanges (100 entités) | 48 ms | 41 ms | ~15 % plus rapide |
L’amélioration du modèle compilé est la plus spectaculaire, mais les améliorations constantes à tous les niveaux s’additionnent, en particulier dans les scénarios à haut débit où vous exécutez des milliers de requêtes par seconde.
Mise à niveau depuis EF Core 8
Si vous utilisez EF Core 8, le chemin de mise à niveau est relativement fluide. Voici une liste de contrôle :
1. Mettez à jour vos forfaits :
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />
2. Vérifiez les modifications majeures. La liste d’EF Core 9 est relativement courte par rapport à certaines versions précédentes. Les plus notables :
- Certaines API précédemment obsolètes ont été supprimées
- Modifications dans la façon dont certaines requêtes
GroupBysont traduites (elles passent désormais côté serveur, ce qui change le comportement si vous comptiez sur l’évaluation du client) - Modifications mineures dans la sortie de l’échafaudage de migration
3. Regénérez les modèles compilés si vous les utilisez. Le format a changé, donc les anciens modèles compilés ne fonctionneront pas avec EF Core 9.
4. Exécutez votre suite de tests. Portez une attention particulière aux requêtes qui ont été précédemment évaluées sur le client : elles peuvent désormais être évaluées sur le serveur, ce qui est généralement meilleur mais peut faire apparaître des différences de données.
5. Vérifiez vos requêtes Cosmos DB si vous utilisez ce fournisseur. Les traductions améliorées signifient que certaines requêtes s’exécuteront différemment (généralement plus rapidement), mais il vaut la peine de vérifier que les résultats sont identiques.
Une mise à niveau minimale pour un projet typique ressemble à ceci :
dotnet add package Microsoft.EntityFrameworkCore --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.0
dotnet build
dotnet test
Si tout se compile et que les tests réussissent, vous êtes probablement en bonne forme. Si vous rencontrez des problèmes, la documentation sur les modifications avec rupture d’EF Core 9 contient des conseils de migration détaillés pour chaque modification.
Conclusion
EF Core 9 n’est pas une version révolutionnaire, c’est une version évolutive, et c’est exactement ce qu’elle devait être. Les améliorations de LINQ justifient à elles seules la mise à niveau pour la plupart des projets, et des fonctionnalités telles que les améliorations des colonnes JSON, les types complexes et la prise en charge de HierarchyId ouvrent des modèles qui étaient auparavant gênants ou impossibles.
Si je devais retenir les trois fonctionnalités qui ont eu le plus d’impact sur mes projets :
- Collections primitives paramétrées — parce que l’efficacité du cache du plan de requête est importante à grande échelle
- Améliorations des colonnes JSON — parce que le modèle de document relationnel hybride est incroyablement utile
- Modèles compilés — car le temps de démarrage affecte directement la productivité des développeurs et la vitesse de déploiementL’équipe EF Core est sur une trajectoire solide depuis EF Core 5, et la version 9 poursuit cette tendance. Si vous utilisez déjà EF Core 8, la mise à niveau est à faible risque et très rémunératrice. Si vous utilisez quelque chose de plus ancien, il n’y a jamais eu de meilleur moment pour faire le saut.
Bon codage – et bonnes requêtes.