Lire en anglais

Partager via


Nouveautés de EF Core 9

EF Core 9 (EF9) est la prochaine version après EF Core 8 et est prévue pour la publication en novembre 2024.

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 quotidiennes.

Conseil

Vous pouvez exécuter et déboguer dans les exemples en en téléchargeant l’exemple de code à partir de 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 exemples sont configurés pour utiliser les builds quotidiennes EF9, qui ont généralement plusieurs semaines supplémentaires de travail terminées par rapport à la dernière préversion. Nous encourageons vivement l’utilisation des builds quotidiennes lors du test de nouvelles fonctionnalités afin de ne pas effectuer vos tests sur les bits 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 importantes du fournisseur ont été réécrites pour fournir de nouvelles fonctionnalités, permettre de nouvelles formes de requêtes, et mieux aligner le fournisseur avec les meilleures pratiques de Azure Cosmos DB Les principales améliorations de haut niveau sont énumérées ci-dessous ; pour une liste complète, voir ce problème épique.

Avertissement

Dans le cadre des améliorations apportées au fournisseur, un certain nombre de ruptures à fort impact ont dû être effectuées ; si vous mettez à jour une application existante, veuillez lire attentivement la section relative aux ruptures.

Amélioration des requêtes avec les clés de partition et les ID de documents

Chaque document stocké dans la 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 Partitionnement et mise à l’échelle horizontale dans Azure Cosmos DB.

Dans EF 9.0, le fournisseur Azure Cosmos DB identifie beaucoup mieux les comparaisons de clés de partition dans vos requêtes LINQ et les extrait afin de garantir que vos requêtes ne sont envoyées qu’à la partition concernée ; cela peut grandement améliorer les performances de vos requêtes et réduire les frais en unités de demande (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 journaux, 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 journaux 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 ponctuelle extrêmement efficace (API ReadItem), qui récupère directement le document en fonction de la clé de partition et de l'ID. Il s’agit du type de lecture le plus efficace et le plus rentable que vous puissiez effectuer dans Azure Cosmos DB ; consultez la documentation de Azure Cosmos DB pour plus d’informations sur les lectures ponctuelles.

Pour en savoir plus sur l'interrogation à l'aide de clés de partition et de lectures ponctuelles, 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 supportait à l’origine une clé de partition unique, mais a depuis élargi ses capacités de partitionnement pour également prendre en charge le sous-partitionnement en spécifiant 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 Choisir vos clés de partition hiérarchiques dans 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 des requêtes, EF extrait automatiquement les valeurs des clés de partition des requêtes et les applique à l’API de requêtes Azure Cosmos DB pour s’assurer que les requêtes sont limitées au plus petit nombre de partitions possible. 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, et les transmet à l’API de requête Azure Cosmos DB comme valeur de la 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","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a"))

Notez que les comparaisons de la clé de partition ont été supprimées de la clause WHERE et sont utilisées comme clé de partition pour une exécution efficace : ["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0].

Pour plus d'informations, consultez la documentation sur l'interrogation à l'aide de clés de partition.

Des capacités d'interrogation LINQ considérablement améliorées

Dans EF 9.0, les capacités de traduction LINQ du fournisseur Azure Cosmos DB ont été considérablement étendues, et le fournisseur peut maintenant exécuter beaucoup 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. Pour plus d'informations, reportez-vous à la section Nouveautés de EF8 : collections primitives.
  • 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 Count et Sum.
  • Traductions de fonctions supplémentaires (voir la documentation des mappages de fonctions pour la liste complète des traductions prises en charge) :
    • Traductions pour les membres des composants DateTime et DateTimeOffset (DateTime.Year, DateTimeOffset.Month...).
    • EF.Functions.IsDefined et EF.Functions.CoalesceUndefined permettent désormais de traiter les valeurs undefined.
    • string.Contains, StartsWith et EndsWith prennent désormais en charge StringComparison.OrdinalIgnoreCase.

Pour obtenir la liste complète des améliorations en matière d'interrogation, consultez ce problème :

Modélisation améliorée conforme aux normes Azure Cosmos DB et JSON

EF 9.0 s’aligne de manière plus naturelle sur les documents Azure Cosmos DB pour une base de données de documents basée sur JSON et aide à interopérer 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'une manière plus naturelle de mappage vers JSON, qui facilite l'interaction entre les outils et systèmes externes et les documents JSON générés par EF ; ces systèmes externes ne connaissent généralement pas les valeurs du discriminateur EF, qui sont par défaut dérivées des 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, voir la note de rupture et la documentation pour plus de détails.

La propriété Discriminator a été renommée $type

La propriété Discriminator 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é discriminante 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 pour savoir comment revenir au nommage précédent.

Recherche de similarité vectorielle (preview)

Azure Cosmos DB offre maintenant un support en avant-première pour 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 aux côtés de vos autres données, ce qui signifie que vous pouvez exécuter toutes vos requêtes contre une seule base de données. 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 la recherche par vecteurs dans Azure Cosmos DB, consultez la documentation.

Une fois que votre conteneur Azure Cosmos DB est correctement configuré, l’utilisation de la recherche vectorielle via EF est une simple question d’ajout d’une propriété vectorielle et de 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 EF.Functions.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 la pagination

Le fournisseur d’Azure Cosmos DB permet désormais de paginer les résultats des requêtes via les jetons de continuation, ce qui est beaucoup plus efficace et rentable que l’utilisation traditionnelle des jetons 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, 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 permis des requêtes 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 for NoSQL inclut un système de contrôle d’accès en fonction du rôle (RBAC) intégré. Ceci est désormais pris en charge par EF9 pour toutes les opérations du plan de données. Cependant, le SDK Azure Cosmos DB ne prend pas en charge RBAC pour les opérations du plan de gestion dans Azure Cosmos DB. Utilisez l’API de gestion Azure à la place de EnsureCreatedAsync avec RBAC.

Les E/S synchrones sont désormais bloquées par défaut

Azure Cosmos DB for NoSQL ne supporte pas les API synchrones (bloquantes) à partir du code de l'application. Auparavant, EF masquait ce problème en bloquant pour vous les appels asynchrones. Cependant, cela encourage l'utilisation d'E/S synchrones, ce qui est une mauvaise pratique, et peut provoquer 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 probablement publiée avec EF 10. Nous vous encourageons à expérimenter le support et le rapport actuels sur vos expériences, mais nous vous recommandons 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, en fonction d’un programme avec la requête 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.

Translation LINQ et 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. Vous trouverez ci-dessous quelques-unes des améliorations les plus importantes ; vous trouverez dans ce problème une liste plus complète des travaux réalisés dans la version 9.0.

Nous aimerions remercier Andrea Canciani (@ranma42) pour ses nombreuses et excellentes contributions à l'optimisation du langage SQL généré par EF Core !

Types complexes : Support de GroupBy et ExecuteUpdate

GroupBy

Conseil

Le code affiché 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 regroupement pour chaque membre du type complexe, qui s’aligne sur la sémantique des types complexes en tant qu’objets 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]

ExecuteUpdate

Conseil

Le code affiché ici provient de ExecuteUpdateSample.cs.

De même, dans EF9, ExecuteUpdate 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 ExecuteUpdate.

É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 de l'héritage par table par 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 n'a l'air de rien, mais cela peut simplifier considérablement le code SQL dans certains cas ; nous vous invitons à parcourir certaines des modifications du code SQL dans les tests pour en voir l'effet.

Traductions impliquant GREATEST/LEAST

Conseil

Le code présenté ici est tiré 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 la version 2022 des bases de données SQL Server/Azure SQL. Visual Studio 2022 installe SQL Server 2019 par défaut. Nous vous recommandons d’installer SQL Server Développeur Édition 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 par 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 par le code SQL suivant quand vous utilisez EF9 s’exécutant 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 par le code SQL suivant quand vous utilisez EF9 s’exécutant 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 par le code SQL suivant quand vous utilisez EF9 s’exécutant 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 est tiré 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 quand vous utilisez 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 le code SQL pour « .NET Blog », car cette valeur ne change pas d’une requête à l’autre. 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 EF.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

Méthode EF.Parameter

EF9 introduit la méthode EF.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 contient désormais 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 modifié la façon dont certaines requêtes utilisant 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 se traduira par 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 EF.Constant peut être utilisée pour revenir à la traduction précédente.

La requête suivante utilise EF.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 option de contexte TranslateParameterizedCollectionsToConstants 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 EF.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 EF.Parameter pour les requêtes ou les variables individuelles que vous souhaitez paramétrer.

Sous-requêtes inlined non liées

Conseil

Le code présenté ici est tiré 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 = dotnetPosts
    .Where(p => p.Id > 2)
    .Select(p => new { Post = p, TotalCount = dotnetPosts.Count() })
    .Skip(2).Take(10)
    .ToArray();

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égat sur des sous-requêtes et 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 SQL résultant est le suivant lorsqu’il est exécuté 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();

Notes

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 est tiré 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]

Notes

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 un ancien membre de l’équipe EF, @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, COALESCE, des négations et de divers autres concepts ; la plupart d’entre elles ont été apportées par Andrea Canciani (@ranma42). Un grand merci 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 celui d'une opération conditionnelle annulée.

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 négativée :

var negatedBoolProjection = await context.Posts.Select(x => new { x.Title, Active = !x.Archived }).ToListAsync();

EF8 générerait un bloc CASE car les comparaisons ne peuvent pas apparaître dans la projection directement 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 le complément bit à bit NON (~) :

SELECT [p].[Title], ~[p].[Archived] AS [Active]
FROM [Posts] AS [p]

Meilleure prise en charge d’Azure SQL et d’Azure Synapse

EF9 permet plus de 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 un meilleur SQL lors de l’utilisation d’Azure SQL ou d’Azure Synapse. EF peut tirer parti des fonctionnalités spécifiques à la base de données (par exemple type dédié pour JSON sur Azure SQL), ou contourner ses limitations (par exemple ESCAPEla clause n’est pas disponible lors de l’utilisation de LIKE sur Azure Synapse).

Autres améliorations des requêtes

  • Le support d'interrogation des collections primitives introduit dans EF8 a été étendu à tous les types ICollection<T>. Notez que cela ne s'applique qu'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, aux tableaux/listes en lecture seule.
  • Nouvelles fonctions ToHashSetAsync pour renvoyer les résultats d'une requête sous forme de HashSet (#30033, contribution de @wertzui).
  • TimeOnly.FromDateTime et FromTimeSpan sont désormais traduits sur SQL Server (#33678).
  • ToString over enums est maintenant traduit (#33706, contribué par @Danevandy99).
  • string.Join se traduit désormais par CONCAT_WS dans un contexte non agrégé sur SQL Server (#28899).
  • EF.Functions.PatIndex se traduit maintenant par la fonction PATINDEX de SQL Server, qui renvoie la position de départ de la première occurrence d'un motif (#33702, @smnsht).
  • Sum et Average fonctionnent maintenant pour les décimales sur SQLite (#33721, contribué par @ranma42).
  • Corrections et optimisations pour string.StartsWith et EndsWith (#31482).
  • Les méthodes Convert.To* peuvent désormais accepter des arguments de type object (#33891, contribution de @imangd).
  • L’opération Exclusive-Or (XOR) est désormais traduite sur SQL Server (#34071, apporté par @ranma42).
  • Optimisations autour de la nullabilité pour les opérations COLLATE et AT TIME ZONE (#34263, apportées par @ranma42).
  • Optimisations pour DISTINCT sur IN, EXISTS et les opérations d’ensemble (#34381, apportées par @ranma42).

Les améliorations ci-dessus ne sont que quelques-unes des plus importantes apportées à EF9 en matière de requêtes; vous trouverez une liste plus complète dans ce problème.

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 en environnement de production à l’aide des méthodes recommandées, mais peut se produire si les migrations sont appliquées au moment de l’exécution à l’aide de la méthode DbContext.Database.Migrate(). 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 , lors de l’utilisation de projets .NET Aspire).

Notes

Si vous utilisez une base de données Sqlite, consultez la section problèmes potentiels lié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. Cependant, certaines opérations ne sont pas incluses dans une transaction (par exemple les opérations sur les tables optimisées pour la mémoire de SQL Server, ou les opérations de modification de la base de données telles que la modification de la collation de la 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).

Notes

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, UseSeeding UseAsyncSeeding 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);
            }
        });

Des informations supplémentaires sont disponibles 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 disposant d’un journal EF chaque fois qu’il génère le modèle. L’exécution d’une application simple montre ensuite EF qui génère 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> 

Notez que la sortie du journal indique que le modèle a été généré 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 package NuGet Microsoft.EntityFrameworkCore.Tasks. 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 et EFScaffoldModelStage les EFOptimizeContext propriétés dans votre .csproj fichier. Par exemple :

<PropertyGroup>
    <EFOptimizeContext>true</EFOptimizeContext>
    <EFScaffoldModelStage>build</EFScaffoldModelStage>
</PropertyGroup>

Maintenant, si nous générons le projet, nous pouvons voir la journalisation au moment de la génération indiquant que le modèle compilé est généré :

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 du mappage des tableaux et 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 sauvegardée par une collection normale et mutable 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 est tiré de ModelBuildingSample.cs.

EF9 prend en charge la spécification du facteur de remplissage SQL Server lors de l’utilisation des migrations EF Core 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 une croissance future ».

Le facteur de remplissage peut être défini sur un seul ou composite principal et des clés et des index de remplacement. 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);

En cas d’application aux tables existantes, cela modifie les tables en facteur de remplissage par 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.

Des conventions de création de modèles publiques 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, et avons rendu possible l’instanciation par les constructeurs non publics. 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 de première classe pour le type SQL Server HierarchyId a été ajoutée dans EF8. Dans EF9, une méthode de sugar a été ajoutée pour faciliter la création de nœuds enfants 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’dotnet efoutil de ligne de commande génère par défaut votre projet avant d’exécuter l’outil. Cela est dû au fait que la régénération avant l’exécution de l’outil est une source courante de confusion lorsque les choses ne fonctionnent pas normalement. 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 régénération 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.