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. Pour plus d’informations, consultez Plan for Entity Framework Core 9.

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 une préversion de .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.

Translation LINQ et SQL

L’équipe travaille sur certaines modifications importantes de l’architecture du pipeline de requête dans EF Core 9 dans le cadre de nos améliorations continues apportées au mappage JSON et aux bases de données de documents. Cela signifie que nous devons faire appel à des personnes comme vous pour exécuter votre code sur ces nouveaux éléments internes. (Si vous lisez un document « What’s New » à ce stade de la publication, cela signifie que vous êtes une partie vraiment engagée de la communauté ; merci !) Nous avons plus de 120 000 tests, mais ce n’est pas suffisant ! Nous avons besoin de vous pour exécuter du code réel sur nos bits, afin de trouver les problèmes et d’obtenir une version solide !

Élagage des colonnes passées à la clause WITH d’OPENJSON

Conseil

Le code affiché ici provient de JsonColumnsSample.cs.

EF9 supprime les colonnes inutiles quand vous appelez OPENJSON WITH. Par exemple, prenez une requête qui obtient un nombre à partir d’une collection JSON en utilisant un prédicat :

var postsUpdatedOn = await context.Posts
    .Where(p => p.Metadata!.Updates.Count(e => e.UpdatedOn >= date) == 1)
    .ToListAsync();

Dans EF8, cette requête génère le code SQL suivant quand vous utilisez le fournisseur de base de données Azure SQL :

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 (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Metadata], '$.Updates') WITH (
        [PostedFrom] nvarchar(45) '$.PostedFrom',
        [UpdatedBy] nvarchar(max) '$.UpdatedBy',
        [UpdatedOn] date '$.UpdatedOn',
        [Commits] nvarchar(max) '$.Commits' AS JSON
    ) AS [u]
    WHERE [u].[UpdatedOn] >= @__date_0) = 1

Notez que les colonnes UpdatedBy et Commits ne sont pas nécessaire dans cette requête. À compter d’EF9, ces colonnes sont élaguées :

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 (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Metadata], '$.Updates') WITH (
        [PostedFrom] nvarchar(45) '$.PostedFrom',
        [UpdatedOn] date '$.UpdatedOn'
    ) AS [u]
    WHERE [u].[UpdatedOn] >= @__date_0) = 1

Dans certains scénarios, cela entraîne la suppression complète de la clause WITH. Par exemple :

var tagsWithCount = await context.Tags.Where(p => p.Text.Length == 1).ToListAsync();

Dans EF8, cette requête se traduit par le code SQL suivant :

SELECT [t].[Id], [t].[Text]
FROM [Tags] AS [t]
WHERE (
    SELECT COUNT(*)
    FROM OPENJSON([t].[Text]) WITH ([value] nvarchar(max) '$') AS [t0]) = 1

Dans EF9, cela a été amélioré de la façon suivante :

SELECT [t].[Id], [t].[Text]
FROM [Tags] AS [t]
WHERE (
    SELECT COUNT(*)
    FROM OPENJSON([t].[Text]) AS [t0]) = 1

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 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 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 :

info: 2/5/2024 15:43:13.789 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      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îne la pollution du cache de requêtes avec un grand nombre de requêtes qui diffèrent uniquement par les valeurs de paramètre. 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 :

info: 2/5/2024 15:43:13.812 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      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

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 » :

info: 2/5/2024 15:43:13.803 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      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

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 inlined, ce qui entraîne un seul aller-retour :

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

ToHashSetAsync<T> nouvelles méthodes

Conseil

Le code présenté ici est tiré de QuerySample.cs.

Les Enumerable.ToHashSet méthodes ont existé depuis .NET Core 2.0. Dans EF9, les méthodes asynchrones équivalentes ont été ajoutées. Par exemple :

var set1 = await context.Posts
    .Where(p => p.Tags.Count > 3)
    .ToHashSetAsync();

var set2 = await context.Posts
    .Where(p => p.Tags.Count > 3)
    .ToHashSetAsync(ReferenceEqualityComparer.Instance);

Cette amélioration a été apportée par @wertzui. Merci !

ExecuteUpdate et ExecuteDelete

Autoriser le passage d’instances de type complexe à ExecuteUpdate

Conseil

Le code affiché ici provient de ExecuteUpdateSample.cs.

L’API ExecuteUpdate a été introduite dans EF7 pour effectuer des mises à jour immédiates et directes de la base de données sans suivi ni SaveChanges. Par exemple :

await context.Stores
    .Where(e => e.Region == "Germany")
    .ExecuteUpdateAsync(s => s.SetProperty(b => b.Region, "Deutschland"));

L’exécution de ce code exécute la requête suivante pour mettre à jour Region avec la valeur « Deutschland » :

UPDATE [s]
SET [s].[Region] = N'Deutschland'
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'

Dans EF8, ExecuteUpdate peut également être utilisé pour mettre à jour les valeurs des 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 == "Deutschland")
    .ExecuteUpdateAsync(
        s => s.SetProperty(b => b.StoreAddress.Line1, newAddress.Line1)
            .SetProperty(b => b.StoreAddress.Line2, newAddress.Line2)
            .SetProperty(b => b.StoreAddress.City, newAddress.City)
            .SetProperty(b => b.StoreAddress.Country, newAddress.Country)
            .SetProperty(b => b.StoreAddress.PostCode, newAddress.PostCode));

L’exécution de ce code entraîne l’exécution de la requête suivante :

UPDATE [s]
SET [s].[StoreAddress_PostCode] = @__newAddress_PostCode_4,
    [s].[StoreAddress_Country] = @__newAddress_Country_3,
    [s].[StoreAddress_City] = @__newAddress_City_2,
    [s].[StoreAddress_Line2] = NULL,
    [s].[StoreAddress_Line1] = @__newAddress_Line1_0
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Deutschland'

Dans EF9, la même mise à jour peut être effectuée en passant l’instance elle-même du type complexe. Autrement dit, chaque membre n’a pas besoin d’ê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));

L’exécution de ce code entraîne la même exécution de requête que dans l’exemple précédent :

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'

Plusieurs mises à jour des propriétés de type complexe et des propriétés simples peuvent être combinées dans un seul appel à ExecuteUpdate. Par exemple :

await context.Customers
    .Where(e => e.Name == name)
    .ExecuteUpdateAsync(
        s => s.SetProperty(
                b => b.CustomerInfo.WorkAddress, new Address("Gressenhall Workhouse", null, "Beetley", "Norfolk", "NR20 4DR"))
            .SetProperty(b => b.CustomerInfo.HomeAddress, new Address("Gressenhall Farm", null, "Beetley", "Norfolk", "NR20 4DR"))
            .SetProperty(b => b.CustomerInfo.Tag, "Tog"));

L’exécution de ce code entraîne la même exécution de requête que dans l’exemple précédent :

UPDATE [c]
SET [c].[CustomerInfo_Tag] = N'Tog',
    [c].[CustomerInfo_HomeAddress_City] = N'Beetley',
    [c].[CustomerInfo_HomeAddress_Country] = N'Norfolk',
    [c].[CustomerInfo_HomeAddress_Line1] = N'Gressenhall Farm',
    [c].[CustomerInfo_HomeAddress_Line2] = NULL,
    [c].[CustomerInfo_HomeAddress_PostCode] = N'NR20 4DR',
    [c].[CustomerInfo_WorkAddress_City] = N'Beetley',
    [c].[CustomerInfo_WorkAddress_Country] = N'Norfolk',
    [c].[CustomerInfo_WorkAddress_Line1] = N'Gressenhall Workhouse',
    [c].[CustomerInfo_WorkAddress_Line2] = NULL,
    [c].[CustomerInfo_WorkAddress_PostCode] = N'NR20 4DR'
FROM [Customers] AS [c]
WHERE [c].[Name] = @__name_0

Migrations

Amélioration des migrations de table temporelle

La migration créée pendant la conversion d’une table existante en table temporelle a été réduite de taille pour EF9. Par exemple, dans EF8, la conversion d’une seule table existante en table temporelle entraîne la migration suivante :

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AlterTable(
        name: "Blogs")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<string>(
        name: "SiteUri",
        table: "Blogs",
        type: "nvarchar(max)",
        nullable: false,
        oldClrType: typeof(string),
        oldType: "nvarchar(max)")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<string>(
        name: "Name",
        table: "Blogs",
        type: "nvarchar(max)",
        nullable: false,
        oldClrType: typeof(string),
        oldType: "nvarchar(max)")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<int>(
        name: "Id",
        table: "Blogs",
        type: "int",
        nullable: false,
        oldClrType: typeof(int),
        oldType: "int")
        .Annotation("SqlServer:Identity", "1, 1")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart")
        .OldAnnotation("SqlServer:Identity", "1, 1");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodEnd",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodStart",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
}

Dans EF9, la même opération entraîne désormais une migration beaucoup plus petite :

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AlterTable(
        name: "Blogs")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodEnd",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:TemporalIsPeriodEndColumn", true);

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodStart",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:TemporalIsPeriodStartColumn", true);
}

Génération de modèles

Spécifier la mise en cache pour les séquences

Conseil

Le code présenté ici est tiré de ModelBuildingSample.cs.

EF9 permet de définir les options de mise en cache des séquences de base de données pour n’importe quel fournisseur de base de données relationnelle qui prend en charge cela. Par exemple, UseCache peut être utilisé pour activer explicitement la mise en cache et définir la taille du cache :

modelBuilder.HasSequence<int>("MyCachedSequence")
    .HasMin(10).HasMax(255000)
    .IsCyclic()
    .StartsAt(11).IncrementsBy(2)
    .UseCache(3);

Cela entraîne la définition de séquence suivante lors de l’utilisation de SQL Server :

CREATE SEQUENCE [MyCachedSequence] AS int START WITH 11 INCREMENT BY 2 MINVALUE 10 MAXVALUE 255000 CYCLE CACHE 3;

De même, UseNoCache désactive explicitement la mise en cache :

modelBuilder.HasSequence<int>("MyUncachedSequence")
    .HasMin(10).HasMax(255000)
    .IsCyclic()
    .StartsAt(11).IncrementsBy(2)
    .UseNoCache();
CREATE SEQUENCE [MyUncachedSequence] AS int START WITH 11 INCREMENT BY 2 MINVALUE 10 MAXVALUE 255000 CYCLE NO CACHE;

Lorsque ni UseNoCache, ni UseCache n’est appelé, la mise en cache n’est pas spécifiée et la base de données utilise la valeur par défaut. Il peut s’agir d’une valeur par défaut différente pour différentes bases de données.

Cette amélioration a été apportée par @bikbov. Merci !

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);

Remarque

Il existe actuellement un bogue dans la préversion 2 où les facteurs de remplissage ne sont pas inclus lorsque la table est créée pour la première fois. Ceci est suivi par le problème #33269

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/, en 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.