Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
EF Core 9.0 (EF9) a été publié en novembre 2024 et est une version de support à court terme (STS). EF9 sera pris en charge jusqu’au 10 novembre 2026.
EF9 est disponible en tant que builds quotidiennes qui contiennent toutes les dernières fonctionnalités EF9 et ajustements d’API. Les exemples ici utilisent ces builds quotidiens.
Conseil
Vous pouvez exécuter et déboguer dans les exemples en téléchargeant le code d'exemple depuis GitHub. Chaque section ci-dessous renvoie au code source propre à cette section.
EF9 cible .NET 8 et peut donc être utilisé avec .NET 8 (LTS) ou .NET 9.
Conseil
Les nouveaux documents sont mis à jour pour chaque préversion. Tous les échantillons sont configurés pour utiliser les builds quotidiens EF9, qui ont généralement plusieurs semaines supplémentaires de travail terminées comparativement à la dernière préversion. Nous encourageons vivement l’utilisation des versions quotidiennes lors des tests de nouvelles fonctionnalités afin de ne pas effectuer vos tests sur des éléments obsolètes.
Azure Cosmos DB pour NoSQL
EF 9.0 apporte des améliorations substantielles au fournisseur EF Core pour Azure Cosmos DB ; des parties significatives du fournisseur ont été réécrites pour fournir de nouvelles fonctionnalités, autoriser de nouvelles formes de requêtes et mieux aligner le fournisseur avec les meilleures pratiques Azure Cosmos DB. Les principales améliorations sont répertoriées ci-dessous ; pour obtenir une liste complète, consultez cette épique.
Avertissement
Dans le cadre des améliorations apportées au fournisseur, un certain nombre de changements incompatibles d'impact important ont dû être effectués ; si vous mettez à niveau une application existante, lisez attentivement la section des modifications incompatibles.
Amélioration des requêtes avec les clés de partition et les ID de documents
Chaque document stocké dans une base de données Azure Cosmos DB a un ID de ressource unique. En outre, chaque document peut contenir une « clé de partition » qui détermine le partitionnement logique des données afin que la base de données puisse être mise à l’échelle efficacement. Pour plus d’informations sur le choix des clés de partition, consultez Partitioning et mise à l’échelle horizontale dans Azure Cosmos DB.
Dans EF 9.0, le fournisseur de Azure Cosmos DB est nettement meilleur pour identifier les comparaisons de clés de partition dans vos requêtes LINQ et les extraire pour vous assurer que vos requêtes ne sont envoyées qu’à la partition appropriée. Cela peut considérablement améliorer les performances de vos requêtes et réduire les frais de RU. Par exemple :
var sessions = await context.Sessions
.Where(b => b.PartitionKey == "someValue" && b.Username.StartsWith("x"))
.ToListAsync();
Dans cette requête, le fournisseur reconnaît automatiquement la comparaison sur PartitionKey. Si nous examinons les logs, nous verrons ce qui suit :
Executed ReadNext (189.8434 ms, 2.8 RU) ActivityId='8cd669ed-2ca5-4f2b-8923-338899071361', Container='test', Partition='["someValue"]', Parameters=[]
SELECT VALUE c
FROM root c
WHERE STARTSWITH(c["Username"], "x")
Notez que la clause WHERE ne contient pas PartitionKey : cette comparaison a été « levée » et est utilisée pour exécuter la requête uniquement sur la partition concernée. Dans les versions précédentes, la comparaison était laissée dans la clause WHERE dans de nombreuses situations, ce qui entraînait l'exécution de la requête sur toutes les partitions et se traduisait par une augmentation des coûts et une réduction des performances.
En outre, si votre requête fournit également une valeur pour la propriété ID du document et n'inclut aucune autre opération de requête, le fournisseur peut appliquer une optimisation supplémentaire :
var somePartitionKey = "someValue";
var someId = 8;
var sessions = await context.Sessions
.Where(b => b.PartitionKey == somePartitionKey && b.Id == someId)
.SingleAsync();
Les logs montrent ce qui suit pour cette requête :
Executed ReadItem (73 ms, 1 RU) ActivityId='13f0f8b8-d481-47f0-bf41-67f7deb008b2', Container='test', Id='8', Partition='["someValue"]'
Ici, aucune requête SQL n'est envoyée. Au lieu de cela, le fournisseur effectue une lecture de point extrêmement efficace ( API), qui récupère directement le document à partir de la clé de partition et de l'ID. Il s’agit du type de lecture le plus efficace et le plus économique que vous pouvez effectuer dans Azure Cosmos DB ; consultez la documentation Azure Cosmos DB pour plus d’informations sur les lectures de points.
Pour en savoir plus sur l’interrogation avec des clés de partition et des lectures de points, consultez la page de documentation sur l’interrogation.
Clés de partition hiérarchiques
Conseil
Le code présenté ici provient de HierarchicalPartitionKeysSample.cs.
Azure Cosmos DB initialement ne prenait en charge qu'une seule clé de partition, mais a depuis développé des fonctionnalités de partitionnement pour prendre également en charge la sous-partition via la spécification de jusqu'à trois niveaux de hiérarchie dans la clé de partition. EF Core 9 apporte une prise en charge complète des clés de partition hiérarchiques, ce qui vous permet de profiter des meilleures performances et des économies associées à cette fonctionnalité.
Les clés de partition sont spécifiées à l’aide de l’API de génération de modèles, généralement dans DbContext.OnModelCreating. Il doit y avoir une propriété mappée dans le type d’entité pour chaque niveau de la clé de partition. Par exemple, considérez un type d’entité UserSession :
public class UserSession
{
// Item ID
public Guid Id { get; set; }
// Partition Key
public string TenantId { get; set; } = null!;
public Guid UserId { get; set; }
public int SessionId { get; set; }
// Other members
public string Username { get; set; } = null!;
}
Le code suivant spécifie une clé de partition à trois niveaux à l’aide des propriétés et TenantId, UserId et SessionId :
modelBuilder
.Entity<UserSession>()
.HasPartitionKey(e => new { e.TenantId, e.UserId, e.SessionId });
Conseil
Cette définition de clé de partition suit l’exemple donné dans Choose vos clés de partition hiérarchiques à partir de la documentation Azure Cosmos DB.
Notez comment, à partir d’EF Core 9, les propriétés d’un type mappé peuvent être utilisées dans la clé de partition. Pour les types bool et numériques, comme la propriété int SessionId, la valeur est utilisée directement dans la clé de partition. D’autres types, comme la propriété Guid UserId, sont automatiquement convertis en chaînes.
Lors de l’interrogation, EF extrait automatiquement les valeurs de clé de partition des requêtes et les applique à l’API de requête Azure Cosmos DB pour garantir que les requêtes sont limitées de manière appropriée au plus petit nombre de partitions possibles. Par exemple, considérez la requête LINQ suivante qui fournit les trois valeurs de clé de partition dans la hiérarchie :
var tenantId = "Microsoft";
var sessionId = 7;
var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C");
var sessions = await context.Sessions
.Where(
e => e.TenantId == tenantId
&& e.UserId == userId
&& e.SessionId == sessionId
&& e.Username.Contains("a"))
.ToListAsync();
Lors de l’exécution de cette requête, EF Core extrait les valeurs des paramètres tenantId, userId et sessionId, puis les transmet à l’API de requête Azure Cosmos DB comme valeur de clé de partition. Par exemple, consultez les journaux d’activité de l’exécution de la requête ci-dessus :
info: 6/10/2024 19:06:00.017 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command)
Executing SQL query for container 'UserSessionContext' in partition '["Microsoft","00aa00aa-bb11-cc22-dd33-44ee44ee44ee",7.0]' [Parameters=[]]
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a"))
Notez que les comparaisons de clés de partition ont été supprimées de la clause WHERE et sont plutôt utilisées comme clé de partition pour une exécution efficace : ["Microsoft","00aa00aa-bb11-cc22-dd33-44ee44ee44ee",7.0].
Pour plus d’informations, consultez la documentation sur l’interrogation avec des clés de partition.
Des capacités d'interrogation LINQ considérablement améliorées
Dans EF 9.0, les fonctionnalités de traduction LINQ du fournisseur de Azure Cosmos DB ont été considérablement développées et le fournisseur peut désormais exécuter de plus en plus de types de requêtes. La liste complète des améliorations apportées aux requêtes est trop longue pour être énumérée, mais en voici les grandes lignes :
- Prise en charge complète des collections primitives d’EF, vous permettant de faire des requêtes LINQ sur des collections d’entiers ou de chaînes, par exemple. Consultez les nouveautés d’EF8 : collections primitives pour plus d’informations .
- Prise en charge des requêtes arbitraires sur des collections non primitives.
- De nombreux opérateurs LINQ supplémentaires sont désormais pris en charge : indexation dans les collections,
Length/Count,ElementAt,Contains, et bien d'autres. - Prise en charge des opérateurs d’agrégat tels que
CountetSum. - Traductions de fonctions supplémentaires (consultez la documentation sur les mappages de fonctions pour obtenir la liste complète des traductions prises en charge) :
- Traductions pour les membres des composants
DateTimeetDateTimeOffset(DateTime.Year,DateTimeOffset.Month...). -
IsDefined et CoalesceUndefined permettent désormais de traiter les valeurs
undefined. -
string.Contains,StartsWithetEndsWithprennent désormais en chargeStringComparison.OrdinalIgnoreCase.
- Traductions pour les membres des composants
Pour obtenir la liste complète des améliorations d’interrogation, consultez ce problème :
Modélisation améliorée alignée sur les normes Azure Cosmos DB et JSON
EF 9.0 convertit les documents Azure Cosmos DB de façon plus naturelle pour une base de données de documents JSON et facilite l’interopérabilité avec d'autres systèmes accédant à vos documents. Bien que cela implique des changements radicaux, il existe des API qui permettent de revenir au comportement antérieur à la version 9.0 dans tous les cas.
Propriétés id simplifiées sans discriminants
Tout d'abord, les versions précédentes d'EF inséraient la valeur du discriminant dans la propriété JSON id, ce qui produisait des documents tels que les suivants :
{
"id": "Blog|1099",
...
}
Cette modification a été effectuée afin de permettre à des documents de types différents (par exemple Blog et Post) et ayant la même valeur clé (1099) d'exister au sein de la même partition de conteneur. Depuis EF 9.0, la propriété id ne contient que la valeur de la clé :
{
"id": 1099,
...
}
Il s’agit d’un moyen plus naturel de mapper au json et facilite l’interaction des outils et systèmes externes avec des documents JSON générés par EF ; ces systèmes externes ne sont généralement pas conscients des valeurs de discrimination EF, qui sont par défaut dérivées de types .NET.
Notez qu'il s'agit d'un changement radical, puisque EF ne pourra plus interroger les documents existants avec l'ancien format id. Une API a été introduite pour revenir au comportement précédent, consultez la note de modification majeure et la documentation pour obtenir plus de détails.
La propriété Discriminator a été renommée
La propriété de discriminateur par défaut était auparavant nommée Discriminator. EF 9.0 modifie la valeur par défaut en $type :
{
"id": 1099,
"$type": "Blog",
...
}
Ceci suit la norme émergente pour le polymorphisme JSON, permettant une meilleure interopérabilité avec d'autres outils. Par exemple, system.Text.Json de .NET prend également en charge le polymorphisme, en utilisant $type comme nom de propriété de discrimination par défaut (docs).
Notez qu'il s'agit d'un changement radical, puisque EF ne pourra plus interroger les documents existants avec l'ancien nom de propriété du discriminateur. Consultez la note de changement de rupture pour savoir comment revenir au nom précédent.
Recherche de similarité vectorielle (preview)
Azure Cosmos DB prend désormais en charge en préversion la recherche de similarité vectorielle. La recherche vectorielle est une partie fondamentale de certains types d’applications, y compris l’IA, la recherche sémantique et d’autres. Azure Cosmos DB vous permet de stocker des vecteurs directement dans vos documents en même temps que le reste de vos données, ce qui signifie que vous pouvez effectuer toutes vos requêtes sur une base de données unique. Cela peut considérablement simplifier votre architecture et éliminer le besoin d’une solution de base de données vectorielle dédiée supplémentaire dans votre pile. Pour en savoir plus sur Azure Cosmos DB recherche vectorielle, consultez la documentation.
Une fois que votre conteneur Azure Cosmos DB est correctement configuré, l’utilisation de la recherche vectorielle via EF est une question simple d’ajout d’une propriété vectorielle et de sa configuration :
public class Blog
{
...
public float[] Vector { get; set; }
}
public class BloggingContext
{
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Embeddings)
.IsVector(DistanceFunction.Cosine, dimensions: 1536);
}
}
Une fois cela fait, utilisez la fonction VectorDistance dans les requêtes LINQ pour effectuer une recherche de similarité vectorielle :
var blogs = await context.Blogs
.OrderBy(s => EF.Functions.VectorDistance(s.Vector, vector))
.Take(5)
.ToListAsync();
Pour plus d’informations, consultez la documentation sur la recherche vectorielle.
Prise en charge de la pagination
Le fournisseur Azure Cosmos DB permet désormais de paginer via des résultats de requête via des jetons continuation, qui est beaucoup plus efficace et rentable que l’utilisation traditionnelle de Skip et Take :
var firstPage = await context.Posts
.OrderBy(p => p.Id)
.ToPageAsync(pageSize: 10, continuationToken: null);
var continuationToken = firstPage.ContinuationToken;
foreach (var post in page.Values)
{
// Display/send the posts to the user
}
Le nouvel opérateur ToPageAsync renvoie un CosmosPage<T>, qui expose un jeton de continuation qui peut être utilisé pour reprendre efficacement la requête à un moment ultérieur, en récupérant les 10 éléments suivants :
var nextPage = await context.Sessions.OrderBy(s => s.Id).ToPageAsync(10, continuationToken);
Pour plus d’informations, consultez la section de documentation sur la pagination.
FromSql pour des requêtes SQL plus sûres
Le fournisseur Azure Cosmos DB a autorisé l’interrogation SQL via FromSqlRaw. Cependant, cette API peut être sujette à des attaques par injection SQL lorsque des données fournies par l'utilisateur sont interpolées ou concaténées dans le code SQL. Dans EF 9.0, vous pouvez désormais utiliser la nouvelle méthode FromSql, qui intègre toujours les données paramétrées en tant que paramètre en dehors du SQL :
var maxAngle = 8;
_ = await context.Blogs
.FromSql($"SELECT VALUE c FROM root c WHERE c.Angle1 <= {maxAngle}")
.ToListAsync();
Pour plus d’informations, consultez la section de documentation sur la pagination.
Accès en fonction du rôle
Azure Cosmos DB pour NoSQL inclut un système de contrôle d’accès en fonction du rôle (RBAC) défini. Ceci est désormais pris en charge par EF9 pour toutes les opérations du plan de données. Toutefois, Azure Cosmos DB SDK ne prend pas en charge RBAC pour les opérations de plan de gestion dans Azure Cosmos DB. Utilisez Azure API Gestion au lieu de EnsureCreatedAsync avec RBAC.
Les E/S synchrones sont désormais bloquées par défaut
Azure Cosmos DB pour NoSQL ne prend pas en charge les API synchrones (bloquantes) du code d’application. Auparavant, EF masquait ce problème en bloquant pour vous les appels asynchrones. Toutefois, cela encourage l’utilisation synchrone des E/S, ce qui est une mauvaise pratique et peut entraîner des blocages. Par conséquent, à partir de EF 9, une exception est levée lorsqu'un accès synchrone est tenté. Par exemple :
Les E/S synchrones peuvent encore être utilisées pour l'instant en configurant le niveau d'avertissement de manière appropriée. Par exemple, dans OnConfiguring sur votre type DbContext :
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.ConfigureWarnings(b => b.Ignore(CosmosEventId.SyncNotSupported));
Notez cependant que nous prévoyons de supprimer complètement le support de la synchronisation dans EF 11, alors commencez à mettre à jour pour utiliser des méthodes asynchrones comme ToListAsync et SaveChangesAsync dès que possible !
Requêtes AOT et précompilées
Avertissement
NativeAOT et la précompilation des requêtes sont des fonctionnalités hautement expérimentales et ne sont pas encore adaptées à l’utilisation de production. La prise en charge décrite ci-dessous doit être considérée comme une infrastructure vers la fonctionnalité finale, qui sera publiée dans une version ultérieure. Nous vous encourageons à expérimenter avec le support actuel et à faire un rapport sur vos expériences, mais nous vous déconseillons de déployer des applications EF NativeAOT en production.
EF 9.0 apporte une prise en charge initiale et expérimentale pour .NET NativeAOT, ce qui permet la publication d’applications compilées à l’avance qui utilisent EF pour accéder aux bases de données. Pour prendre en charge les requêtes LINQ en mode NativeAOT, EF s’appuie sur la précompilation des requêtes : ce mécanisme identifie statiquement les requêtes EF LINQ et génère des intercepteurs C#, qui contiennent du code pour exécuter chaque requête spécifique. Cela peut réduire considérablement le temps de démarrage de votre application, car le traitement et la compilation de vos requêtes LINQ dans SQL ne se produisent plus chaque fois que votre application démarre. Au lieu de cela, l'intercepteur de chaque requête contient le SQL finalisé pour cette requête, ainsi que le code optimisé pour matérialiser les résultats de la base de données en tant qu'objets .NET.
Par exemple, étant donné un programme avec la requête Entity Framework (EF) suivante :
var blogs = await context.Blogs.Where(b => b.Name == "foo").ToListAsync();
EF génère un intercepteur C# dans votre projet, qui prend en charge l’exécution de la requête. Au lieu de traiter la requête et de la traduire en SQL chaque fois que le programme démarre, l’intercepteur y est intégré directement (pour SQL Server dans ce cas), ce qui permet à votre programme de démarrer beaucoup plus rapidement :
var relationalCommandTemplate = ((IRelationalCommandTemplate)(new RelationalCommand(materializerLiftableConstantContext.CommandBuilderDependencies, "SELECT [b].[Id], [b].[Name]\nFROM [Blogs] AS [b]\nWHERE [b].[Name] = N'foo'", new IRelationalParameter[] { })));
En outre, le même intercepteur contient du code pour matérialiser votre objet .NET à partir des résultats de la base de données :
var instance = new Blog();
UnsafeAccessor_Blog_Id_Set(instance) = dataReader.GetInt32(0);
UnsafeAccessor_Blog_Name_Set(instance) = dataReader.GetString(1);
Cela utilise une autre nouvelle fonctionnalité .NET : accesseurs non sécurisés, pour injecter des données de la base de données dans les champs privés de votre objet.
Si vous êtes intéressé par NativeAOT et que vous souhaitez expérimenter avec des fonctionnalités de pointe, faites un essai ! N’oubliez pas que la fonctionnalité doit être considérée comme instable et présente actuellement de nombreuses limitations ; nous prévoyons de le stabiliser et de le rendre plus adapté à l’utilisation de production dans EF 10.
Pour plus d’informations, consultez la page de documentation NativeAOT .
Traduction de LINQ et de SQL
Comme chaque version, EF9 inclut un grand nombre d'améliorations des capacités d'interrogation LINQ. De nouvelles requêtes peuvent être traduites, et de nombreuses traductions SQL pour les scénarios supportés ont été améliorées, à la fois pour de meilleures performances et une meilleure lisibilité.
Le nombre d'améliorations est trop important pour les énumérer toutes ici. Ci-dessous, certaines des améliorations les plus importantes sont mises en évidence ; consultez cet article pour une liste plus complète du travail effectué dans la version 9.0.
Nous aimerions appeler Andrea Canciani (@ranma42) pour ses nombreuses contributions de haute qualité à l’optimisation du sql généré par EF Core !
Types complexes : Support de GroupBy et ExecuteUpdate
Grouper par
Conseil
Le code présenté ici provient de ComplexTypesSample.cs.
EF9 prend en charge le regroupement par une instance de type complexe. Par exemple :
var groupedAddresses = await context.Stores
.GroupBy(b => b.StoreAddress)
.Select(g => new { g.Key, Count = g.Count() })
.ToListAsync();
EF traduit cela en regroupant chaque membre du type complexe, ce qui s'aligne avec la sémantique des types complexes en tant qu’objets de valeur. Par exemple, sur Azure SQL :
SELECT [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode], COUNT(*) AS [Count]
FROM [Stores] AS [s]
GROUP BY [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode]
ExécuterMiseÀJour
Conseil
Le code présenté ici provient de ExecuteUpdateSample.cs.
De même, dans EF9, ExecuteUpdateAsync a également été amélioré pour accepter les propriétés de type complexe. Toutefois, chaque membre du type complexe doit être spécifié explicitement. Par exemple :
var newAddress = new Address("Gressenhall Farm Shop", null, "Beetley", "Norfolk", "NR20 4DR");
await context.Stores
.Where(e => e.Region == "Germany")
.ExecuteUpdateAsync(s => s.SetProperty(b => b.StoreAddress, newAddress));
Cela génère du SQL qui met à jour chaque colonne mappée au type complexe :
UPDATE [s]
SET [s].[StoreAddress_City] = @__complex_type_newAddress_0_City,
[s].[StoreAddress_Country] = @__complex_type_newAddress_0_Country,
[s].[StoreAddress_Line1] = @__complex_type_newAddress_0_Line1,
[s].[StoreAddress_Line2] = NULL,
[s].[StoreAddress_PostCode] = @__complex_type_newAddress_0_PostCode
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'
Auparavant, vous deviez énumérer manuellement les différentes propriétés du type complexe dans votre appel ExecuteUpdateAsync.
Élaguer les éléments inutiles du SQL
Auparavant, EF produisait parfois du code SQL contenant des éléments qui n'étaient pas réellement nécessaires ; dans la plupart des cas, ces éléments étaient éventuellement nécessaires à un stade antérieur du traitement du code SQL et étaient laissés de côté. EF9 élague désormais la plupart de ces éléments, ce qui permet d'obtenir un code SQL plus compact et, dans certains cas, plus efficace.
Élagage des tables
Comme premier exemple, le code SQL généré par EF contenait parfois des JOINs vers des tables qui n'étaient pas réellement nécessaires dans la requête. Considérez le modèle suivant, qui utilise le mappage par table de l’héritage (table-per-type, TPT) :
public class Order
{
public int Id { get; set; }
...
public Customer Customer { get; set; }
}
public class DiscountedOrder : Order
{
public double Discount { get; set; }
}
public class Customer
{
public int Id { get; set; }
...
public List<Order> Orders { get; set; }
}
public class BlogContext : DbContext
{
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>().UseTptMappingStrategy();
}
}
Si nous exécutons la requête suivante pour obtenir tous les clients ayant au moins une commande :
var customers = await context.Customers.Where(o => o.Orders.Any()).ToListAsync();
EF8 a généré le code SQL suivant :
SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
SELECT 1
FROM [Orders] AS [o]
LEFT JOIN [DiscountedOrders] AS [d] ON [o].[Id] = [d].[Id]
WHERE [c].[Id] = [o].[CustomerId])
Notez que la requête contient une jointure avec la table DiscountedOrders même si aucune colonne n'y est référencée. EF9 génère un SQL élagué sans la jointure :
SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
SELECT 1
FROM [Orders] AS [o]
WHERE [c].[Id] = [o].[CustomerId])
Élagage par projection
De même, examinons la requête suivante :
var orders = await context.Orders
.Where(o => o.Amount > 10)
.Take(5)
.CountAsync();
Sur EF8, cette requête génère le SQL suivant :
SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) [o].[Id]
FROM [Orders] AS [o]
WHERE [o].[Amount] > 10
) AS [t]
Notez que la projection [o].[Id] n'est pas nécessaire dans la sous-requête, puisque l'expression SELECT extérieure se contente de compter les lignes. EF9 génère ce qui suit à la place :
SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) 1 AS empty
FROM [Orders] AS [o]
WHERE [o].[Amount] > 10
) AS [s]
... et la projection est vide. Cela peut ne pas sembler beaucoup, mais il peut considérablement simplifier le SQL dans certains cas ; vous êtes invité à faire défiler certaines des modifications SQL dans les tests pour voir l’effet.
Les traductions impliquant PLUS GRAND/PLUS PETIT
Conseil
Le code présenté ici provient de LeastGreatestSample.cs.
Plusieurs nouvelles traductions ont été introduites et utilisent les fonctions SQL GREATEST et LEAST.
Important
Les fonctions GREATEST et LEAST ont été introduites dans les bases de données SQL Server/Azure SQL dans la version 2022. Visual Studio 2022 installe SQL Server 2019 par défaut. Nous vous recommandons d’installer SQL Server Developer Edition 2022 pour essayer ces nouvelles traductions dans EF9.
Par exemple, les requêtes utilisant Math.Max ou Math.Min sont désormais traduites pour Azure SQL à l’aide de GREATEST et LEAST respectivement. Par exemple :
var walksUsingMin = await context.Walks
.Where(e => Math.Min(e.DaysVisited.Count, e.ClosestPub.Beers.Length) > 4)
.ToListAsync();
Cette requête est traduite en SQL suivant lors de l’utilisation d’EF9 exécutée sur SQL Server 2022 :
SELECT [w].[Id], [w].[ClosestPubId], [w].[DaysVisited], [w].[Name], [w].[Terrain]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]
WHERE LEAST((
SELECT COUNT(*)
FROM OPENJSON([w].[DaysVisited]) AS [d]), (
SELECT COUNT(*)
FROM OPENJSON([p].[Beers]) AS [b])) >
Math.Min et Math.Max peuvent également être utilisés sur les valeurs d’une collection primitive. Par exemple :
var pubsInlineMax = await context.Pubs
.SelectMany(e => e.Counts)
.Where(e => Math.Max(e, threshold) > top)
.ToListAsync();
Cette requête est traduite en SQL suivant lors de l’utilisation d’EF9 exécutée sur SQL Server 2022 :
SELECT [c].[value]
FROM [Pubs] AS [p]
CROSS APPLY OPENJSON([p].[Counts]) WITH ([value] int '$') AS [c]
WHERE GREATEST([c].[value], @__threshold_0) > @__top_1
Enfin, RelationalDbFunctionsExtensions.Least et RelationalDbFunctionsExtensions.Greatest peuvent être utilisées pour appeler directement la fonction Least ou Greatest dans SQL. Par exemple :
var leastCount = await context.Pubs
.Select(e => EF.Functions.Least(e.Counts.Length, e.DaysVisited.Count, e.Beers.Length))
.ToListAsync();
Cette requête est traduite en SQL suivant lors de l’utilisation d’EF9 exécutée sur SQL Server 2022 :
SELECT LEAST((
SELECT COUNT(*)
FROM OPENJSON([p].[Counts]) AS [c]), (
SELECT COUNT(*)
FROM OPENJSON([p].[DaysVisited]) AS [d]), (
SELECT COUNT(*)
FROM OPENJSON([p].[Beers]) AS [b]))
FROM [Pubs] AS [p]
Forcer ou empêcher la paramétrisation des requêtes
Conseil
Le code présenté ici provient de QuerySample.cs.
Sauf dans certains cas spéciaux, EF Core paramétrise les variables utilisées dans une requête LINQ, mais ajoute des constantes dans le code SQL généré. Prenons l’exemple de méthode de requête suivant :
async Task<List<Post>> GetPosts(int id)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && e.Id == id)
.ToListAsync();
Cela se traduit par le code SQL et les paramètres suivants lors de l’utilisation de Azure SQL :
Executed DbCommand (1ms) [Parameters=[@__id_0='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = @__id_0
Notez qu’EF a créé une constante dans sql pour « .NET Blog », car cette valeur ne passe pas de la requête à la requête. L’utilisation d’une constante permet au moteur de base de données d’examiner cette valeur pendant la création d’un plan de requête, ce qui peut produire une requête plus efficace.
En revanche, la valeur de id est paramétrisée, car la même requête peut être exécutée avec plusieurs valeurs différentes de id. La création d'une constante dans ce cas entraînerait la pollution du cache des requêtes par un grand nombre de requêtes qui ne diffèrent que par des valeurs id Cela est très mauvais pour les performances générales de la base de données.
En règle générale, ces valeurs par défaut ne doivent pas être changées. Toutefois, EF Core 8.0.2 introduit une méthode Constant qui force EF à utiliser une constante même si un paramètre est utilisé par défaut. Par exemple :
async Task<List<Post>> GetPostsForceConstant(int id)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && e.Id == EF.Constant(id))
.ToListAsync();
La traduction contient maintenant une constante pour la valeur id :
Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = 1
The EF. Méthode de paramètre
EF9 introduit la méthode Parameter pour faire l’inverse. Autrement dit, forcer EF à utiliser un paramètre même si la valeur est une constante dans le code. Par exemple :
async Task<List<Post>> GetPostsForceParameter(int id)
=> await context.Posts
.Where(e => e.Title == EF.Parameter(".NET Blog") && e.Id == id)
.ToListAsync();
La traduction inclut maintenant un paramètre pour la chaîne « .NET Blog » :
Executed DbCommand (1ms) [Parameters=[@__p_0='.NET Blog' (Size = 4000), @__id_1='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = @__p_0 AND [p].[Id] = @__id_1
Collections primitives paramétrées
EF8 a changé la façon dont certaines requêtes qui utilisent des collections primitives sont traduites. Lorsqu’une requête LINQ contient une collection primitive paramétrée, EF convertit son contenu en JSON et le transmet comme une seule valeur de paramètre à la requête :
async Task<List<Post>> GetPostsPrimitiveCollection(int[] ids)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && ids.Contains(e.Id))
.ToListAsync();
Cela entraîne la traduction suivante sur SQL Server :
Executed DbCommand (5ms) [Parameters=[@__ids_0='[1,2,3]' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (
SELECT [i].[value]
FROM OPENJSON(@__ids_0) WITH ([value] int '$') AS [i]
)
Cela permet d’avoir la même requête SQL pour différentes collections paramétrées (seule la valeur du paramètre change), mais dans certaines situations, cela peut entraîner des problèmes de performance car la base de données ne peut pas planifier la requête de manière optimale. La méthode Constant peut être utilisée pour revenir à la traduction précédente.
La requête suivante utilise Constant à cet effet :
async Task<List<Post>> GetPostsForceConstantCollection(int[] ids)
=> await context.Posts
.Where(
e => e.Title == ".NET Blog" && EF.Constant(ids).Contains(e.Id))
.ToListAsync();
Le SQL résultant est le suivant :
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (1, 2, 3)
De plus, EF9 introduit une TranslateParameterizedCollectionsToConstantsoption de contexte qui peut être utilisée pour empêcher la paramétrisation des collections primitives pour toutes les requêtes. Nous avons également ajouté une méthode complémentaire TranslateParameterizedCollectionsToParameters qui force explicitement la paramétrisation des collections primitives (c’est le comportement par défaut).
Conseil
La méthode Parameter remplace l’option de contexte. Si vous souhaitez empêcher la paramétrisation des collections primitives pour la plupart de vos requêtes (mais pas toutes), vous pouvez définir l’option de contexte TranslateParameterizedCollectionsToConstants et utiliser Parameter pour les requêtes ou les variables individuelles que vous souhaitez paramétrer.
Sous-requêtes inlinées non corrélées
Conseil
Le code présenté ici provient de QuerySample.cs.
Dans EF8, un IQueryable référencé dans une autre requête peut être exécuté en tant qu’aller-retour de base de données distinct. Prenons l'exemple de la requête LINQ suivante :
var dotnetPosts = context
.Posts
.Where(p => p.Title.Contains(".NET"));
var results = await dotnetPosts
.Where(p => p.Id > 2)
.Select(p => new { Post = p, TotalCount = dotnetPosts.Count() })
.Skip(2).Take(10)
.ToArrayAsync();
Dans EF8, la requête pour dotnetPosts est exécutée en un aller-retour, puis les résultats finaux sont exécutés en tant que deuxième requête. Par exemple, sur SQL Server :
SELECT COUNT(*)
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%'
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_1 ROWS FETCH NEXT @__p_2 ROWS ONLY
Dans EF9, le IQueryable dans le dotnetPosts est souligné, ce qui entraîne un seul aller-retour dans la base de données :
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata], (
SELECT COUNT(*)
FROM [Posts] AS [p0]
WHERE [p0].[Title] LIKE N'%.NET%')
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
Fonctions d’agrégation sur les sous-requêtes et les agrégats sur SQL Server
EF9 améliore la traduction de certaines requêtes complexes utilisant des fonctions d’agrégat composées sur des sous-requêtes ou d’autres fonctions d’agrégat. Voici un exemple d’une telle requête :
var latestPostsAverageRatingByLanguage = await context.Blogs
.Select(x => new
{
x.Language,
LatestPostRating = x.Posts.OrderByDescending(xx => xx.PublishedOn).FirstOrDefault()!.Rating
})
.GroupBy(x => x.Language)
.Select(x => x.Average(xx => xx.LatestPostRating))
.ToListAsync();
D’abord, Select calcule LatestPostRating pour chaque Post, ce qui nécessite une sous-requête lors de la traduction en SQL. Ensuite, ces résultats sont agrégés en utilisant l’opération Average. Le code SQL résultant se présente comme suit lors de l’exécution sur SQL Server :
SELECT AVG([s].[Rating])
FROM [Blogs] AS [b]
OUTER APPLY (
SELECT TOP(1) [p].[Rating]
FROM [Posts] AS [p]
WHERE [b].[Id] = [p].[BlogId]
ORDER BY [p].[PublishedOn] DESC
) AS [s]
GROUP BY [b].[Language]
Dans les versions précédentes, EF Core générerait un SQL invalide pour des requêtes similaires, essayant d’appliquer l’opération d’agrégat directement sur la sous-requête. Cela n’est pas autorisé sur SQL Server et entraîne une exception. Le même principe s’applique aux requêtes utilisant un agrégat sur un autre agrégat :
var topRatedPostsAverageRatingByLanguage = await context.Blogs.
Select(x => new
{
x.Language,
TopRating = x.Posts.Max(x => x.Rating)
})
.GroupBy(x => x.Language)
.Select(x => x.Average(xx => xx.TopRating))
.ToListAsync();
Remarque
Ce changement n’affecte pas Sqlite, qui prend en charge les agrégats sur des sous-requêtes (ou d’autres agrégats) et ne prend pas en charge LATERAL JOIN (APPLY). Voici le SQL pour la première requête exécutée sur Sqlite :
SELECT ef_avg((
SELECT "p"."Rating"
FROM "Posts" AS "p"
WHERE "b"."Id" = "p"."BlogId"
ORDER BY "p"."PublishedOn" DESC
LIMIT 1))
FROM "Blogs" AS "b"
GROUP BY "b"."Language"
Les requêtes utilisant Count != 0 sont optimisées
Conseil
Le code présenté ici provient de QuerySample.cs.
Dans EF8, la requête LINQ suivante a été traduite pour utiliser la fonction SQL COUNT :
var blogsWithPost = await context.Blogs
.Where(b => b.Posts.Count > 0)
.ToListAsync();
EF9 génère désormais une traduction plus efficace à l’aide de EXISTS :
SELECT "b"."Id", "b"."Name", "b"."SiteUri"
FROM "Blogs" AS "b"
WHERE EXISTS (
SELECT 1
FROM "Posts" AS "p"
WHERE "b"."Id" = "p"."BlogId")
Sémantique C# pour les opérations de comparaison sur les valeurs nullables
Dans la version EF8, les comparaisons entre des éléments nullables n'étaient pas effectuées correctement dans certains cas. En C#, si l'un des opérandes ou les deux sont nuls, le résultat d'une opération de comparaison est faux ; dans le cas contraire, les valeurs contenues dans les opérandes sont comparées. Dans EF8, nous traduisions les comparaisons en utilisant la sémantique null de la base de données. Cela produisait des résultats différents d'une requête similaire utilisant LINQ to Objects. De plus, nous obtenions des résultats différents lorsque la comparaison était effectuée dans le filtre ou dans la projection. Certaines requêtes produiraient également des résultats différents entre Sql Server et Sqlite/Postgres.
Par exemple, la requête :
var negatedNullableComparisonFilter = await context.Entities
.Where(x => !(x.NullableIntOne > x.NullableIntTwo))
.Select(x => new { x.NullableIntOne, x.NullableIntTwo }).ToListAsync();
générerait le SQL suivant :
SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE NOT ([e].[NullableIntOne] > [e].[NullableIntTwo])
qui filtre les entités dont la valeur de NullableIntOne ou NullableIntTwo est nulle.
Dans EF9, nous produisons :
SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE CASE
WHEN [e].[NullableIntOne] > [e].[NullableIntTwo] THEN CAST(0 AS bit)
ELSE CAST(1 AS bit)
END = CAST(1 AS bit)
Une comparaison similaire est effectuée dans une projection :
var negatedNullableComparisonProjection = await context.Entities.Select(x => new
{
x.NullableIntOne,
x.NullableIntTwo,
Operation = !(x.NullableIntOne > x.NullableIntTwo)
}).ToListAsync();
a donné le résultat SQL suivant :
SELECT [e].[NullableIntOne], [e].[NullableIntTwo], CASE
WHEN NOT ([e].[NullableIntOne] > [e].[NullableIntTwo]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [Operation]
FROM [Entities] AS [e]
qui renvoie false pour les entités dont NullableIntOne ou NullableIntTwo sont définies comme nulles (au lieu de true attendu en C#). L'exécution du même scénario sur Sqlite a généré :
SELECT "e"."NullableIntOne", "e"."NullableIntTwo", NOT ("e"."NullableIntOne" > "e"."NullableIntTwo") AS "Operation"
FROM "Entities" AS "e"
ce qui entraîne une exception Nullable object must have a value, car la traduction produit une valeur null dans les cas où NullableIntOne ou NullableIntTwo sont nuls.
EF9 gère désormais correctement ces scénarios, produisant des résultats cohérents avec LINQ to Objects et à travers différents fournisseurs.
Cette amélioration a été apportée par @ranma42. Merci !
Traduction des opérateurs LINQ Order et OrderDescending
EF9 permet la traduction des opérations de simplification d’ordre LINQ (Order et OrderDescending). Ces opérations fonctionnent de manière similaire à OrderBy/OrderByDescending mais ne nécessitent pas d’argument. Elles appliquent plutôt un ordre par défaut. Pour les entités, cela signifie un tri basé sur les valeurs des clés primaires et pour d’autres types, un tri basé sur les valeurs elles-mêmes.
Voici un exemple de requête qui tire parti des opérateurs d’ordre simplifiés :
var orderOperation = await context.Blogs
.Order()
.Select(x => new
{
x.Name,
OrderedPosts = x.Posts.OrderDescending().ToList(),
OrderedTitles = x.Posts.Select(xx => xx.Title).Order().ToList()
})
.ToListAsync();
Cette requête est équivalente à la suivante :
var orderByEquivalent = await context.Blogs
.OrderBy(x => x.Id)
.Select(x => new
{
x.Name,
OrderedPosts = x.Posts.OrderByDescending(xx => xx.Id).ToList(),
OrderedTitles = x.Posts.Select(xx => xx.Title).OrderBy(xx => xx).ToList()
})
.ToListAsync();
et produit le SQL suivant :
SELECT [b].[Name], [b].[Id], [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata], [p0].[Title], [p0].[Id]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Posts] AS [p0] ON [b].[Id] = [p0].[BlogId]
ORDER BY [b].[Id], [p].[Id] DESC, [p0].[Title]
Remarque
Les méthodes Order et OrderDescending ne sont prises en charge que pour les collections d’entités, de types complexes ou de scalaires - elles ne fonctionneront pas sur des projections plus complexes, comme des collections de types anonymes contenant plusieurs propriétés.
Cette amélioration a été apportée par l’alumnus EF Team @bricelam. Merci !
Amélioration de la traduction de l'opérateur de négation logique (!)
EF9 apporte de nombreuses optimisations autour de SQL CASE/WHEN, de la négation COALESCE et de diverses autres constructions ; la plupart ont été apportées par Andrea Canciani (@ranma42) - un grand merci à Andrea Canciani pour toutes ces contributions ! Ci-dessous, nous allons détailler quelques-unes de ces optimisations autour de la négation logique.
Examinons la requête suivante :
var negatedContainsSimplification = await context.Posts
.Where(p => !p.Content.Contains("Announcing"))
.Select(p => new { p.Content }).ToListAsync();
Dans EF8, nous produirions le code SQL suivant :
SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE NOT (instr("p"."Content", 'Announcing') > 0)
Dans EF9, nous « poussons » l'opération NOT dans la comparaison :
SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE instr("p"."Content", 'Announcing') <= 0
Un autre exemple, applicable à SQL Server, est une opération conditionnelle négative.
var caseSimplification = await context.Blogs
.Select(b => !(b.Id > 5 ? false : true))
.ToListAsync();
Dans EF8, cela se traduisait par des blocs CASE imbriqués :
SELECT CASE
WHEN CASE
WHEN [b].[Id] > 5 THEN CAST(0 AS bit)
ELSE CAST(1 AS bit)
END = CAST(0 AS bit) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]
Dans EF9, nous avons supprimé l'imbrication :
SELECT CASE
WHEN [b].[Id] > 5 THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]
Sur SQL Server, lors de la projection d'une propriété booléenne négée :
var negatedBoolProjection = await context.Posts.Select(x => new { x.Title, Active = !x.Archived }).ToListAsync();
EF8 génère un bloc CASE, car les comparaisons ne peuvent pas apparaître directement dans la projection dans les requêtes SQL Server :
SELECT [p].[Title], CASE
WHEN [p].[Archived] = CAST(0 AS bit) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [Active]
FROM [Posts] AS [p]
Dans EF9, cette traduction a été simplifiée et utilise désormais l'opérateur NON binaire (~) :
SELECT [p].[Title], ~[p].[Archived] AS [Active]
FROM [Posts] AS [p]
Meilleure prise en charge des Azure SQL et des Azure Synapse
EF9 permet une plus grande flexibilité lors de la spécification du type de SQL Server ciblé. Au lieu de configurer EF avec UseSqlServer, vous pouvez désormais spécifier UseAzureSql ou UseAzureSynapse.
Cela permet à EF de produire de meilleurs sql lors de l’utilisation de Azure SQL ou de Azure Synapse. EF peut tirer parti des fonctionnalités spécifiques de la base de données (par exemple, type dédicé pour JSON sur Azure SQL) ou contourner ses limitations (par exemple, ESCAPE clause n’est pas disponible lors de l’utilisation de LIKE sur Azure Synapse).
Autres améliorations des requêtes
- La prise en charge des requêtes de collections primitives introduite dans EF8 a été étendue pour prendre en charge tous les
ICollection<T>types. Notez que cela s’applique uniquement aux collections de paramètres et aux collections en ligne : les collections primitives qui font partie d’entités sont toujours limitées aux tableaux, aux listes et, dans EF9, également aux tableaux/lists en lecture seule. - Nouvelles
ToHashSetAsyncfonctions permettant de retourner les résultats d’une requête en tant queHashSet(#30033, contribuées par @wertzui). -
TimeOnly.FromDateTimeetFromTimeSpansont désormais traduits sur SQL Server (#33678). -
ToStringover enums est maintenant traduit (#33706, contribué par @Danevandy99). -
string.Joinse traduit désormais par CONCAT_WS dans un contexte non agrégé sur SQL Server (#28899). -
EF.Functions.PatIndexse traduit désormais par la fonction SQL ServerPATINDEX, qui retourne la position de départ de la première occurrence d’un modèle (#33702, @smnsht). -
SumetAveragefonctionnent maintenant avec les décimales sur SQLite (#33721, contribué par @ranma42). - Correctifs et optimisations sur
string.StartsWithetEndsWith(#31482). -
Convert.To*les méthodes peuvent désormais accepter l’argument de typeobject(#33891, contribué par @imangd). - L’opérationExclusive-Or (XOR) est désormais traduite sur SQL Server (#34071, contribuée par @ranma42).
- Optimisations relatives à la nullabilité pour les opérations
COLLATEetAT TIME ZONE(#34263, contribué par @ranma42). - Optimisations pour
DISTINCTpar rapport àIN,EXISTSet les opérations sur les ensembles (#34381, contribué par @ranma42).
Les éléments ci-dessus n’ont été que quelques-unes des améliorations de requête les plus importantes dans EF9 ; consultez ce numéro pour obtenir une liste plus complète.
Migrations
Protection contre les migrations concurrentes
EF9 introduit un mécanisme de verrouillage pour protéger contre plusieurs exécutions de migration se produisant simultanément, car cela pourrait laisser la base de données dans un état corrompu. Cela ne se produit pas lorsque les migrations sont déployées dans l’environnement de production à l’aide de méthodes recommandées, mais peuvent se produire si les migrations sont appliquées au moment de l’exécution à l’aide de la DbContext.Database.MigrateAsync() méthode. Nous vous recommandons d’appliquer des migrations au moment du déploiement, plutôt que dans le cadre du démarrage de l’application, mais cela peut entraîner des architectures d’application plus complexes (par exemple, qui utilisent des projets .NET Aspire).
Pour plus d’informations, consultez Verrouillage de la migration.
Remarque
Si vous utilisez la base de données Sqlite, consultez les problèmes potentiels associés à cette fonctionnalité.
Avertissement lorsque plusieurs opérations de migration ne peuvent pas être exécutées dans une transaction
La majorité des opérations effectuées lors des migrations sont protégées par une transaction. Cela garantit que si, pour une raison quelconque, une migration échoue, la base de données ne se retrouve pas dans un état corrompu. Toutefois, certaines opérations ne sont pas encapsulées dans une transaction (par exemple, operations sur SQL Server des tables à mémoire optimisée ou des opérations de modification de base de données telles que la modification du classement de base de données). Pour éviter de corrompre la base de données en cas d’échec de la migration, il est recommandé que ces opérations soient effectuées de manière isolée à l’aide d’une migration distincte. EF9 détecte désormais un scénario où une migration contient plusieurs opérations, dont l’une ne peut pas être incluse dans une transaction, et émet un avertissement.
Amélioration de l’amorçage des données
EF9 a introduit un moyen pratique d’effectuer l’amorçage des données, c’est-à-dire peupler la base de données avec des données initiales. DbContextOptionsBuilder contient désormais des méthodes UseSeeding et UseAsyncSeeding qui s’exécutent lors de l’initialisation de DbContext (dans le cadre de EnsureCreatedAsync).
Remarque
Si l’application a déjà été exécutée, la base de données peut déjà contenir les données d’exemple (qui auraient été ajoutées lors de la première initialisation du contexte). Ainsi, UseSeedingUseAsyncSeeding doit vérifier si les données existent avant d’essayer de peupler la base de données. Cela peut être réalisé en émettant une simple requête EF.
Voici un exemple de la façon dont ces méthodes peuvent être utilisées :
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFDataSeeding;Trusted_Connection=True;ConnectRetryCount=0")
.UseSeeding((context, _) =>
{
var testBlog = context.Set<Blog>().FirstOrDefault(b => b.Url == "http://test.com");
if (testBlog == null)
{
context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
context.SaveChanges();
}
})
.UseAsyncSeeding(async (context, _, cancellationToken) =>
{
var testBlog = await context.Set<Blog>().FirstOrDefaultAsync(b => b.Url == "http://test.com", cancellationToken);
if (testBlog == null)
{
context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
await context.SaveChangesAsync(cancellationToken);
}
});
Vous trouverez plus d’informations ici.
Autres améliorations de la migration
- Lors de la modification d’une table existante en table temporelle SQL Server, la taille du code de migration a été considérablement réduite.
Génération de modèles
Modèles compilés automatiquement
Conseil
Le code présenté ici provient de l’exemple NewInEFCore9.CompiledModels .
Les modèles compilés peuvent améliorer le temps de démarrage des applications avec des modèles volumineux, c’est-à-dire des centaines, voire des milliers de nombres de types d’entités. Dans les versions précédentes d’EF Core, un modèle compilé devait être généré manuellement à l’aide de la ligne de commande. Par exemple :
dotnet ef dbcontext optimize
Après avoir exécuté la commande, une ligne de ce type .UseModel(MyCompiledModels.BlogsContextModel.Instance) doit être ajoutée à OnConfiguring pour indiquer à EF Core d’utiliser le modèle compilé.
À compter d’EF9, cette ligne .UseModel n’est plus nécessaire lorsque le type DbContext de l’application se trouve dans le même projet/assembly que le modèle compilé. Au lieu de cela, le modèle compilé est détecté et utilisé automatiquement. Vous pouvez le constater en faisant enregistrer par EF chaque fois qu’il construit le modèle. L'exécution d'une application simple montre ensuite EF en train de construire le modèle au démarrage de l'application.
Starting application...
>> EF is building the model...
Model loaded with 2 entity types.
La sortie de l’exécution de dotnet ef dbcontext optimize sur le projet de modèle est la suivante :
PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model> dotnet ef dbcontext optimize
Build succeeded in 0.3s
Build succeeded in 0.3s
Build started...
Build succeeded.
>> EF is building the model...
>> EF is building the model...
Successfully generated a compiled model, it will be discovered automatically, but you can also call 'options.UseModel(BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model>
Remarquez que la sortie du fichier log indique que le modèle a été construit lors de l'exécution de la commande. Si nous réexécutons l’application, après la régénération, mais sans apporter de modifications de code, la sortie est :
Starting application...
Model loaded with 2 entity types.
Notez que le modèle n’a pas été généré lors du démarrage de l’application, car le modèle compilé a été détecté et utilisé automatiquement.
Intégration de MSBuild
Avec l’approche ci-dessus, le modèle compilé doit toujours être régénéré manuellement lorsque les types d’entités ou la configuration DbContext sont modifiés. Toutefois, EF9 est fourni avec un package de tâches MSBuild qui peut mettre à jour automatiquement le modèle compilé lorsque le projet de modèle est généré ! Pour commencer, installez le Microsoft. EntityFrameworkCore.Tasks package NuGet. Par exemple :
dotnet add package Microsoft.EntityFrameworkCore.Tasks --version 9.0.0
Conseil
Utilisez la version du package dans la commande ci-dessus qui correspond à la version d’EF Core que vous utilisez.
Activez ensuite l’intégration en définissant les propriétés EFOptimizeContext et EFScaffoldModelStage dans votre fichier .csproj. Par exemple :
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
<EFScaffoldModelStage>build</EFScaffoldModelStage>
</PropertyGroup>
Maintenant, si nous construisons le projet, nous pouvons voir le traçage lors de la construction indiquant que le modèle compilé est en cours de création.
Optimizing DbContext...
dotnet exec --depsfile D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.deps.json
--additionalprobingpath G:\packages
--additionalprobingpath "C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages"
--runtimeconfig D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.runtimeconfig.json G:\packages\microsoft.entityframeworkcore.tasks\9.0.0-preview.4.24205.3\tasks\net8.0\..\..\tools\netcoreapp2.0\ef.dll dbcontext optimize --output-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\obj\Release\net8.0\
--namespace NewInEfCore9
--suffix .g
--assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\bin\Release\net8.0\Model.dll
--project-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model
--root-namespace NewInEfCore9
--language C#
--nullable
--working-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App
--verbose
--no-color
--prefix-output
De plus, l’exécution de l’application montre que le modèle compilé a été détecté et, par conséquent, le modèle n’est pas généré à nouveau :
Starting application...
Model loaded with 2 entity types.
À présent, chaque fois que le modèle change, le modèle compilé sera automatiquement régénéré dès que le projet est généré.
Pour plus d’informations, consultez l’intégration de MSBuild.
Collections primitives en lecture seule
Conseil
Le code présenté ici provient de PrimitiveCollectionsSample.cs.
EF8 a introduit la prise en charge des tableaux de mappage et des listes mutables de types primitifs. Cela a été étendu dans EF9 pour inclure des collections/listes en lecture seule. Plus précisément, EF9 prend en charge les collections de type IReadOnlyList, IReadOnlyCollection ou ReadOnlyCollection. Par exemple, dans le code suivant, DaysVisited sera mappé par convention en tant que collection primitive de dates :
public class DogWalk
{
public int Id { get; set; }
public string Name { get; set; }
public ReadOnlyCollection<DateOnly> DaysVisited { get; set; }
}
La collection en lecture seule peut être prise en charge par une collection mutable et normale, si vous le souhaitez. Par exemple, dans le code suivant, DaysVisited peut être mappé en tant que collection primitive de dates, tout en autorisant le code de la classe à manipuler la liste sous-jacente.
public class Pub
{
public int Id { get; set; }
public string Name { get; set; }
public IReadOnlyCollection<string> Beers { get; set; }
private List<DateOnly> _daysVisited = new();
public IReadOnlyList<DateOnly> DaysVisited => _daysVisited;
}
Ces collections peuvent ensuite être utilisées dans les requêtes de la manière standard : Par exemple, cette requête LINQ :
var walksWithADrink = await context.Walks.Select(
w => new
{
WalkName = w.Name,
PubName = w.ClosestPub.Name,
Count = w.DaysVisited.Count(v => w.ClosestPub.DaysVisited.Contains(v)),
TotalCount = w.DaysVisited.Count
}).ToListAsync();
Se traduit par le code SQL suivant sur SQLite :
SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", (
SELECT COUNT(*)
FROM json_each("w"."DaysVisited") AS "d"
WHERE "d"."value" IN (
SELECT "d0"."value"
FROM json_each("p"."DaysVisited") AS "d0"
)) AS "Count", json_array_length("w"."DaysVisited") AS "TotalCount"
FROM "Walks" AS "w"
INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id"
Spécifier le facteur de remplissage pour les clés et les index
Conseil
Le code présenté ici provient de ModelBuildingSample.cs.
EF9 prend en charge la spécification du facteur de remplissage SQL Server lors de l’utilisation d’EF Core Migrations pour créer des clés et des index. À partir de la documentation SQL Server, « Lorsqu’un index est créé ou reconstruit, la valeur du facteur de remplissage détermine le pourcentage d’espace sur chaque page de niveau feuille à remplir avec des données, en réservant le reste sur chaque page comme espace libre pour la croissance future ».
Le facteur de remplissage peut être défini sur un seul ou un ensemble de clés principales et autres clés et index à l'aide de HasFillFactor. Par exemple :
modelBuilder.Entity<User>()
.HasKey(e => e.Id)
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasAlternateKey(e => new { e.Region, e.Ssn })
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasIndex(e => new { e.Name })
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasIndex(e => new { e.Region, e.Tag })
.HasFillFactor(80);
Lorsqu'il est appliqué aux tables existantes, cela modifie les tables au facteur de remplissage selon la contrainte.
ALTER TABLE [User] DROP CONSTRAINT [AK_User_Region_Ssn];
ALTER TABLE [User] DROP CONSTRAINT [PK_User];
DROP INDEX [IX_User_Name] ON [User];
DROP INDEX [IX_User_Region_Tag] ON [User];
ALTER TABLE [User] ADD CONSTRAINT [AK_User_Region_Ssn] UNIQUE ([Region], [Ssn]) WITH (FILLFACTOR = 80);
ALTER TABLE [User] ADD CONSTRAINT [PK_User] PRIMARY KEY ([Id]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Name] ON [User] ([Name]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Region_Tag] ON [User] ([Region], [Tag]) WITH (FILLFACTOR = 80);
Cette amélioration a été apportée par @deano-chasseur. Merci !
Rendre les conventions de création de modèles existantes plus extensibles
Conseil
Le code présenté ici provient de CustomConventionsSample.cs.
Les conventions de création de modèles publics pour les applications ont été introduites dans EF7. Dans EF9, nous avons facilité l’extension de certaines conventions existantes. Par exemple, le code permettant de mapper les propriétés par attribut dans EF7 est le suivant :
public class AttributeBasedPropertyDiscoveryConvention : PropertyDiscoveryConvention
{
public AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
: base(dependencies)
{
}
public override void ProcessEntityTypeAdded(
IConventionEntityTypeBuilder entityTypeBuilder,
IConventionContext<IConventionEntityTypeBuilder> context)
=> Process(entityTypeBuilder);
public override void ProcessEntityTypeBaseTypeChanged(
IConventionEntityTypeBuilder entityTypeBuilder,
IConventionEntityType? newBaseType,
IConventionEntityType? oldBaseType,
IConventionContext<IConventionEntityType> context)
{
if ((newBaseType == null
|| oldBaseType != null)
&& entityTypeBuilder.Metadata.BaseType == newBaseType)
{
Process(entityTypeBuilder);
}
}
private void Process(IConventionEntityTypeBuilder entityTypeBuilder)
{
foreach (var memberInfo in GetRuntimeMembers())
{
if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
{
entityTypeBuilder.Property(memberInfo);
}
else if (memberInfo is PropertyInfo propertyInfo
&& Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
{
entityTypeBuilder.Ignore(propertyInfo.Name);
}
}
IEnumerable<MemberInfo> GetRuntimeMembers()
{
var clrType = entityTypeBuilder.Metadata.ClrType;
foreach (var property in clrType.GetRuntimeProperties()
.Where(p => p.GetMethod != null && !p.GetMethod.IsStatic))
{
yield return property;
}
foreach (var property in clrType.GetRuntimeFields())
{
yield return property;
}
}
}
}
Dans EF9, cela peut être simplifié de la façon suivante :
public class AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
: PropertyDiscoveryConvention(dependencies)
{
protected override bool IsCandidatePrimitiveProperty(
MemberInfo memberInfo, IConventionTypeBase structuralType, out CoreTypeMapping? mapping)
{
if (base.IsCandidatePrimitiveProperty(memberInfo, structuralType, out mapping))
{
if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
{
return true;
}
structuralType.Builder.Ignore(memberInfo.Name);
}
mapping = null;
return false;
}
}
Mettre à jour ApplyConfigurationsFromAssembly pour appeler des constructeurs non publics
Dans les versions précédentes d’EF Core, la méthode ApplyConfigurationsFromAssembly instanciait seulement les types de configuration avec des constructeurs publics sans paramètre. Dans EF9, nous avons amélioré les messages d’erreur générés en cas d’échec, ainsi que l’instanciation par le constructeur non public. Cela est utile en cas de colocalisation de la configuration dans une classe imbriquée privée qui ne doit jamais être instanciée par le code d’application. Par exemple :
public class Country
{
public int Code { get; set; }
public required string Name { get; set; }
private class FooConfiguration : IEntityTypeConfiguration<Country>
{
private FooConfiguration()
{
}
public void Configure(EntityTypeBuilder<Country> builder)
{
builder.HasKey(e => e.Code);
}
}
}
Entre parenthèses, certaines personnes pensent que ce modèle est une abomination, car il couple le type d’entité à la configuration. D’autres personnes pensent qu’il est très utile, car il colocalise la configuration avec le type d’entité. Nous n’entrerons pas dans le débat. :-)
SQL Server HierarchyId
Conseil
Le code présenté ici provient de HierarchyIdSample.cs.
Sugar pour la génération de chemin HierarchyId
La prise en charge complète du type SQL Server HierarchyId a été ajoutée dans EF8. Dans EF9, une méthode de simplification syntaxique a été ajoutée pour faciliter la création de sous-nœuds dans la structure d’arborescence. Par exemple, les requêtes de code suivantes pour une entité existante avec une propriété HierarchyId :
var daisy = await context.Halflings.SingleAsync(e => e.Name == "Daisy");
Cette propriété HierarchyId peut ensuite être utilisée pour créer des nœuds enfants sans manipulation de chaîne explicite. Par exemple :
var child1 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1), "Toast");
var child2 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 2), "Wills");
Si daisy a un HierarchyId de /4/1/3/1/, alors child1 obtiendra le HierarchyId « /4/1/3/1/1/ » et child2 obtiendra le HierarchyId « /4/1/1/3/1/2/ ».
Pour créer un nœud entre ces deux enfants, un sous-niveau supplémentaire peut être utilisé. Par exemple :
var child1b = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1, 5), "Toast");
Cela crée un nœud avec un HierarchyId de /4/1/3/1/1.5/, le plaçant entre child1 et child2.
Cette amélioration a été apportée par @Rezakazemi890. Merci !
Outillage
Moins de régénérations
L’outildotnet ef en ligne de commande génère par défaut votre projet avant d’exécuter l’outil. Cela est dû au fait que ne pas régénérer avant d’exécuter l’outil est une source courante de confusion lorsque les choses ne fonctionnent pas. Les développeurs expérimentés peuvent utiliser l’option --no-build pour éviter cette build, ce qui peut être lent. Toutefois, même l’option --no-build peut entraîner la reconstruction du projet la prochaine fois qu’il est généré en dehors de l’outil EF.
Nous croyons qu’une contribution communautaire de @Suchiman a résolu cela. Toutefois, nous sommes également conscients que les ajustements autour des comportements MSBuild ont tendance à avoir des conséquences inattendues, donc nous demandons aux personnes comme vous d’essayer ceci et de signaler toute expérience négative rencontrée.