10 nouvelles fonctionnalités dans EF 9

Entity Framework Core s’est amélioré chaque année, apportant des outils pour simplifier l’accès et la gestion des données dans les applications .NET. Consultez cet article avec 10 des nouvelles fonctionnalités principales de EF 9 pour ASP.NET Core.
Le noyau du cadre de l’entité – EF Core – est le modèle relationnel principal (ORM) maintenu par Microsoft. Il a été continuellement amélioré pour répondre aux demandes des développeurs et aux besoins des applications modernes. Certains des principaux aspects comprennent les performances, la productivité des développeurs, l’évolutivité, la flexibilité de cartographie et autres.
Dans cet article, nous examinerons les 10 nouvelles fonctionnalités principales d’EF 9, qui est arrivée avec la sortie de .NET 9 en novembre 2024.
Exemple de code
Vous pouvez accéder au projet avec tout l’exemple de code couvert dans le post dans ce référentiel GitHub: EF 9 Code source d’information.
1. Nouvelles requêtes Linq
Linq (Language Integrated Query) est une fonction .NET qui vous permet de remettre en question différentes sources de données directement dans le code C #, en utilisant une syntaxe déclarative intégrée dans la langue. Dans la version 9 d’EF Core, nous pouvons mettre en évidence les améliorations suivantes, concernant les méthodes de requête LINQ.
1.1 Support de groupe pour les types complexes
Jusqu’à ef 8, le GroupBy
La méthode d’extension n’était disponible que pour les propriétés. Vous pouvez maintenant regrouper des types complexes, tels que les classes et les enregistrements.
Notez l’exemple ci-dessous:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
public string Region { get; set; }
public required Address CustomerAddress { get; set; }
}
[ComplexType]
public record class Address(string Street, int Number, string City, string Country, string PostCode);
public async Task ComplexTypesGroupByAsync()
{
var groupedAddresses = await _dbContext.Customers
.GroupBy(b => b.CustomerAddress)
.Select(g => new { g.Key, Count = g.Count() })
.ToListAsync();
}
Ici, nous avons la classe client qui utilise l’enregistrement d’adresse qui est déclaré comme un type complexe, donc dans le ComplexTypesGroupByAsync()
Méthode, les adresses sont regroupées par le Address
objet (type complexe). Dans ce cas, la clé est toutes les propriétés de l’objet, et la quantité est le nombre d’enregistrements présents dans ce groupe.
La sortie en SQL dans SQLServer est la suivante:
SELECT
[c].[CustomerAddress_City],
[c].[CustomerAddress_Country],
[c].[CustomerAddress_Number],
[c].[CustomerAddress_PostCode],
[c].[CustomerAddress_Street],
COUNT(*) AS [Count]
FROM [Customers] AS [c]
GROUP BY
[c].[CustomerAddress_City],
[c].[CustomerAddress_Country],
[c].[CustomerAddress_Number],
[c].[CustomerAddress_PostCode],
[c].[CustomerAddress_Street]
1.2 Executeupdate Prise en charge des types complexes
Une autre méthode maintenant disponible pour les types complexes est ExecuteUpdate
. Cette méthode est utilisée pour effectuer des mises à jour en vrac directement sur la base de données, sans charger les entités en mémoire, réalisant ainsi de meilleures performances par rapport à l’approche traditionnelle du chargement, de la modification et de la sauvegarde des entités.
Remarque le code ci-dessous:
public async Task ExecuteUpdateAddress(Address newAddress)
{
await _dbContext.Customers
.Where(e => e.Region == "USA")
.ExecuteUpdateAsync(s => s.SetProperty(b => b.CustomerAddress, newAddress));
}
Ici, nous disons à EF de mettre à jour toutes les adresses client dans la région des États-Unis avec les valeurs reçues par la nouvelle adresse. Dans ce cas, la traduction SQL est la suivante:
UPDATE [c] SET
[c].[CustomerAddress_City] = @__complex_type_newAddress_0_City,
[c].[CustomerAddress_Country] = @__complex_type_newAddress_0_Country,
[c].[CustomerAddress_Number] = @__complex_type_newAddress_0_Number,
[c].[CustomerAddress_PostCode] = @__complex_type_newAddress_0_PostCode,
[c].[CustomerAddress_Street] = @__complex_type_newAddress_0_Street
FROM [Customers] AS [c]
WHERE [c].[Region] = N'USA'
Auparavant, il était nécessaire de répertorier manuellement les différentes propriétés du type complexe lors de l’appel du ExecuteUpdate
Méthode, mais maintenant ce n’est plus nécessaire.
2. Amélioration des requêtes aller-retour
EF 9 a reçu une amélioration pour enregistrer les aller-retour de la base de données.
Considérez la requête LINQ suivante:
public async Task<IEnumerable<CustomerWithCount>> GetUSCustomers()
{
var usCustomers = _dbContext.Customers.Where(c => c.Region.Contains("USA"));
return await usCustomers
.Where(c => c.Id > 1)
.Select(c => new CustomerWithCount{ Customer = c, TotalCount = usCustomers.Count() })
.Skip(2).Take(10)
.ToArrayAsync();
}
Dans EF 8, deux requêtes aller-retour sont exécutées, qui sont traduites par SQL Server comme suit:
– round-trip 1
SELECT COUNT(*)
FROM [Customers] AS [c]
WHERE [c].[Region] LIKE N'%USA%'
– round-trip 2
SELECT
[c].[Id],
[c].[Name],
[c].[Phone],
[c].[Region],
[c].[CustomerAddress_City],
[c].[CustomerAddress_Country],
[c].[CustomerAddress_Number],
[c].[CustomerAddress_PostCode],
[c].[CustomerAddress_Street]
FROM [Customers] AS [c]
WHERE [c].[Region] LIKE N'%USA%' AND [c].[Id] > 1
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
Dans EF 9, le IQueryable
sur usCustomers
est incliné, donc une seule base de données aller-retour est faite:
SELECT
[c].[Id],
[c].[Name],
[c].[Phone],
[c].[Region],
[c].[CustomerAddress_City],
[c].[CustomerAddress_Country],
[c].[CustomerAddress_Number],
[c].[CustomerAddress_PostCode],
[c].[CustomerAddress_Street],
(SELECT COUNT(*) FROM [Customers] AS [c0]
WHERE [c0].[Region] LIKE N'%USA%') AS [TotalCount]
FROM [Customers] AS [c]
WHERE [c].[Region] LIKE N'%USA%' AND [c].[Id] > 1
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
Cette approche EF 9 économise des ressources car elle ne fait qu’un seul aller-retour dans la base de données, au lieu de deux comme dans EF 8.
3. Collections primitives en lecture seule
La prise en charge de la cartographie du tableau et des listes mutables de types primitives a été introduite dans EF 8, mais cette fonction a été élargie dans EF 9 pour inclure également des collections / listes en lecture seule.
Cette fonctionnalité aide à gérer les scénarios où les collections de types primitives telles que String, INT ou Bool sont empêchées d’être modifiées, ce qui nécessitait auparavant des solutions de contournement.
Notez l’exemple ci-dessous:
public class PartyEvent
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public ReadOnlyCollection<int> EventDays { get; set; }
public DateTime CreatedDate { get; set; }
}
Le EventDays
la propriété est de type ReadOnlyCollection<int>
Dans ce cas, il ne peut pas être modifié, cependant, il peut être utilisé normalement comme indiqué dans le code ci-dessous:
public async Task ProcessReadOnlyPrimitiveCollections()
{
var today = DateTime.Today;
var pastEvents = await _dbContext.PartyEvents
.Where(e => e.CreatedDate < today)
.Select(p => new
{
Id = Guid.NewGuid(),
Name = p.Name,
Count = p.EventDays.Count(d => p.EventDays.Contains(d)),
TotalCount = p.EventDays.Count
})
.ToListAsync();
}
Ici nous utilisons les valeurs du EventDays
Liste, mais sans les modifier. Si nous essayons de modifier la liste, nous recevrons l’erreur suivante de l’interprète Visual Studio:
var initialList = new List<int> { 1, 2, 3 };
var readWriteCollection = new List<int>(initialList);
var readOnlyCollection = new ReadOnlyCollection<int>(initialList);
readWriteCollection.Add(4);
readOnlyCollection.Add(4);
Notez que les collections du ReadOnlyCollection
type n’a pas le Add()
Méthode d’extension et autres, précisément pour qu’ils ne soient pas modifiés.
4. Requêtes plus efficaces
Jusqu’à la version 8, EF Core a parfois produit SQL avec des éléments qui n’étaient pas vraiment nécessaires. EF 9 supprime la plupart de ces éléments, produisant plus de code SQL plus compact et, dans certains cas, plus efficace.
4.1 Suppression de Jjoin inutile
Notez l’exemple ci-dessous:
public class Post
{
public Guid Id { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public List<Author> Authors { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime UpdatedDate { get; set; }
}
public class Author
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
public class TopAuthor : Author
{
public int TotalPosts { get; set; }
}
Si nous exécutons la requête suivante pour obtenir tous les messages avec au moins un auteur:
var postsWithAuthors = await _dbContext.Posts.Where(p => p.Authors.Any()).ToListAsync();
EF 8 générerait le SQL suivant:
SELECT
[p].[Id],
[p].[Body],
[p].[CreatedDate],
[p].[Title],
[p].[UpdatedDate]
FROM [Posts] AS [p]
WHERE EXISTS (
SELECT 1
FROM [Author] AS [a]
LEFT JOIN [TopAuthor] AS [t] ON [a].[Id] = [t].[Id]
WHERE [p].[Id] = [a].[PostId])
Notez qu’il y a un LEFT JOIN
avec TopAuthor
qui pour la requête ci-dessus devient inutile.
EF 9 se traduit par un SQL poli sans le jointure:
SELECT
[p].[Id],
[p].[Body],
[p].[CreatedDate],
[p].[Title],
[p].[UpdatedDate]
FROM [Posts] AS [p]
WHERE EXISTS (
SELECT 1
FROM [Author] AS [a]
WHERE [p].[Id] = [a].[PostId])
4.2 Suppression de la colonne inutile
Semblable à l’exemple précédent, la requête ci-dessous est désormais optimisée dans EF 9.
int recentPostQuantity = await _dbContext.Posts
.Where(p => p.CreatedDate >= DateTime.UtcNow)
.Take(6)
.CountAsync();
Dans EF 8, le SQL généré est le suivant:
SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) [p].[Id]
FROM [Posts] AS [p]
WHERE [p].[CreatedDate] >= GETUTCDATE()
) AS [p0]
Notez que le [p].[Id]
La colonne est utilisée dans la requête, même si elle n’est pas nécessaire, car la sélection supérieure ne compte que des enregistrements.
Dans EF 9, cela a été amélioré, la projection est maintenant vide. Cela peut ne pas sembler beaucoup, mais selon le scénario, cela peut simplifier considérablement le SQL.
SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) 1 AS empty
FROM [Posts] AS [p]
WHERE [p].[CreatedDate] >= GETUTCDATE()
) AS [p0]
5. Amélioration des requêtes qui utilisent! = 0
Dans EF 8, la requête LINQ suivante utilise le SQL COUNT
fonction:
var postsWithAuthors = await _dbContext.Posts
.Where(b => b.Authors.Count > 0)
.ToListAsync();
SELECT
[p].[Id],
[p].[Body],
[p].[CreatedDate],
[p].[Title],
[p].[UpdatedDate]
FROM [Posts] AS [p]
WHERE (
SELECT COUNT(*)
FROM [Authors] AS [a]
WHERE [a].[PostId] = [p].[Id]
) > 0
EF 9 génère une traduction SQL plus efficace en utilisant EXISTS
:
SELECT
[p].[Id],
[p].[Body],
[p].[CreatedDate],
[p].[Title],
[p].[UpdatedDate]
FROM [Posts] AS [p]
WHERE EXISTS (
SELECT 1
FROM [Author] AS [a]
WHERE [p].[Id] = [a].[PostId])
6. Opérateurs de commande et d’ordre
EF 9 a simplifié la conversion du Order
et OrderDescending
Trier les opérations. Ils travaillent de la même manière que le traditionnel OrderBy
/ /OrderByDescending
mais au lieu d’utiliser un argument passé comme paramètre, ils appliquent la commande par défaut. Dans le cas des entités, la commande est effectuée sur la base des valeurs de clé primaire. Et pour d’autres types, la commande est effectuée en fonction de leurs valeurs.
Notez l’exemple ci-dessous:
var orderedPostsWithAuthors = await _dbContext.Posts
.Order()
.Select(x => new
{
x.Title,
OrderedAuthors = x.Authors.OrderDescending().ToList(),
OrderedAuthorName = x.Authors.Select(xx => xx.Name).Order().ToList()
})
.ToListAsync();
Ici nous utilisons le nouveau Order
et OrderDescending
Opérateurs, qui produiront le SQL suivant:
SELECT
[p].[Title],
[p].[Id],
[a].[Id],
[a].[Email],
[a].[Name],
[a].[PostId],
[a0].[Name],
[a0].[Id]
FROM [Posts] AS [p]
LEFT JOIN [Author] AS [a] ON [p].[Id] = [a].[PostId]
LEFT JOIN [Author] AS [a0] ON [p].[Id] = [a0].[PostId]
ORDER BY [p].[Id], [a].[Id] DESC, [a0].[Name]
La même commande peut être effectuée comme suit OrderBy
et OrderByDescending
:
var orderedByPostsWithAuthors = await _dbContext.Posts
.OrderBy(x => x.Title)
.Select(x => new
{
x.Title,
OrderedAuthors = x.Authors.OrderByDescending(a => a.Name).ToList(),
OrderedAuthorName = x.Authors.Select(a => a.Name).OrderBy(n => n).ToList()
})
.ToListAsync();
Un point important est que le Order
et OrderDescending
Les méthodes ne sont compatibles qu’avec les collections d’entités, de types complexes ou de scalaires. Pour des projections plus complexes telles que les collections de types anonymes contenant plusieurs propriétés, par exemple, ces méthodes ne fonctionneront pas.
7. L’opérateur de négation logique (!)
Requêtes qui utilisent l’opérateur de négation logique !
avoir une traduction plus efficace dans EF 9. Notez l’exemple ci-dessous:
var newProducts = await _dbContext.Products
.Select(b => !(b.Id > 10 ? false : true))
.ToListAsync();
Le résultat dans EF 8 est traduit en blocs de cas imbriqués:
SELECT CASE
WHEN CASE
WHEN [p].[Id] > 10 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 [Products] AS [p]
Dans EF 9, la nidification a été supprimée:
SELECT CASE
WHEN [p].[Id] > 10 THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
FROM [Products] AS [p]
8. Hiérarchyide dans SQL Server
SQL Server HierarchyId
a été ajouté dans EF 8 et a été amélioré dans EF 9. Une nouvelle méthode a été ajoutée pour faciliter la création de nouveaux nœuds enfants dans une structure d’arbre.
Notez l’exemple ci-dessous.
public class Person
{
public Person(HierarchyId pathFromPatriarch, string name)
{
PathFromPatriarch = pathFromPatriarch;
Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
public HierarchyId PathFromPatriarch { get; set; }
}
public async Task SeedPerson()
{
await _dbContext.Persons.AddAsync(new Person(HierarchyId.Parse("/4/1/3/1/"), "John"));
await _dbContext.SaveChangesAsync();
}
var john = await _dbContext.Persons.SingleAsync(p => p.Name == "John");
Ici, nous filtrons une personne dont le nom est John. Nous pouvons l’utiliser HierarchyId
propriété pour créer des nœuds enfants simplement, sans aucune manipulation explicite de caractère:
var child1 = new Person(HierarchyId.Parse(john.PathFromPatriarch, 1), "Doe");
var child2 = new Person(HierarchyId.Parse(john.PathFromPatriarch, 2), "Smith");
Dans ce cas, John a une hiérarchyide de /4/1/3/1/
Child1 obtiendra le hiérarchyide /4/1/3/1/1/1/1/
Et Child2 obtiendra le hiérarchyide /4/1/3/1/2/
.
Une autre possibilité est de créer un nœud entre ces deux enfants, auquel cas un sous-niveau supplémentaire peut être utilisé:
var child1b = new Person(HierarchyId.Parse(john.PathFromPatriarch, 1, 5), "Johnson");
Cela créera un nœud avec une hiérarchyide de /4/1/3/1/1.5/
avec 1.5
ce qui le met entre Child1 et Child2.
9. Nouvelles méthodes de propagation Useseding et UseaSyncseeding
EF 9 a ajouté de nouvelles méthodes pour remplir la base de données avec les données initiales, à savoir UseSeeding
et UseAsyncSeeding
.
Ces méthodes visent à unifier tout le code de propagation des données en un seul endroit. De plus, les deux méthodes sont protégées par le mécanisme de verrouillage de la migration pour éviter les problèmes de concurrence.
Vous trouverez ci-dessous un exemple de la façon d’utiliser les nouvelles méthodes. Notez que la vérification de l’existence des enregistrements se fait manuellement via une question simple:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSeeding((context, _) =>
{
var testPerson = context.Set<Person>().FirstOrDefault(p => p.Name == "Test_Person");
if (testPerson == null)
{
context.Set<Person>().Add(new Person(HierarchyId.Parse("/1/"), "Test_Person"));
context.SaveChanges();
}
})
.UseAsyncSeeding(async (context, _, cancellationToken) =>
{
var testPerson = await context.Set<Person>().FirstOrDefaultAsync(p => p.Name == "Test_Person", cancellationToken);
if (testPerson == null)
{
context.Set<Person>().Add(new Person(HierarchyId.Parse("/1/"), "Test_Person"));
await context.SaveChangesAsync(cancellationToken);
}
});
10. Azure Cosmos DB: Des requêtes plus efficaces avec des clés de partition et des identifiants de document
Lorsqu’un document est stocké dans une base de données Azure Cosmos DB, il se voit attribuer un ID unique. Une autre caractéristique disponible dans Azure Cosmos DB est que chaque document peut contenir une «clé de partition» qui détermine le partitionnement logique des données, fournissant une mise à l’échelle efficace.
Dans EF 9, le fournisseur a reçu des améliorations pour identifier les comparaisons de clés de partition dans les requêtes LINQ et les extraire pour s’assurer que les requêtes ne sont envoyées qu’à la partition pertinente, améliorant considérablement les performances. En effet, la requête n’utilise pas toutes les données disponibles, mais seulement ce qui fait partie de la partition liée à la requête.
Notez la requête ci-dessous:
var books = await _dbContext.Books
.Where(b => b.PartitionKey == "someValue" && b.Title.StartsWith("R"))
.ToListAsync();
Dans cette requête, le fournisseur de DB Cosmos reconnaît automatiquement la comparaison sur PartitionKey
. Dans les journaux, nous pouvons voir ce qui suit:
Executed ReadNext (132.5782 ms, 2.6 RU) ActivityId='1abfdd11-e8b9-45dc-a788-74e11094a119',
Container='mytest',
Partition='["someValue"]',
Parameters=[]
SELECT VALUE c
FROM root c
WHERE STARTSWITH(c["Name"], "R")
Dans les versions précédentes, la comparaison a été laissée dans la clause où PartitionKey
cela a fait ignorer la requête de la partition pertinente et exécuter la requête sur toutes les partitions entraînant une augmentation des coûts et une réduction des performances.
Maintenant dans EF 9, cette comparaison a été obsolète et est utilisée pour exécuter la requête uniquement sur la partition pertinente.
Un autre point important est que si la requête a un identifiant pour le document et n’inclut aucune autre opération de requête, le fournisseur peut appliquer une optimisation supplémentaire. Notez la requête ci-dessous, en passant le livre ID:
var bookById = await _dbContext.Books
.Where(b => b.PartitionKey == "someValue" && b.Id == 1)
.SingleAsync();
Si nous regardons les journaux, nous voyons ce qui suit:
Executed ReadItem (82 ms, 1 RU) ActivityId='304a1a63-8337-42a6-a553-8213598ef662',
Container='mytest',
Id='1',
Partition='["someValue"]'
Notez qu’aucune requête SQL n’a été envoyée. Au lieu de cela, le fournisseur effectue une lecture ponctuelle (API ReadItem), qui récupère directement le document à l’aide de la clé de partition et de l’ID.
Il s’agit du type de lecture le plus efficace et le plus rentable possible pour effectuer sur Azure Cosmos DB.
Conclusion
EF 9 a apporté de nombreuses améliorations à ASP.NET Core, dont beaucoup se concentrent sur l’amélioration des performances des requêtes de base de données, tandis que d’autres apportent plus d’options pour gérer les tâches quotidiennes, telles que les migrations de base de données et de table, et les commandes d’enregistrement, entre autres.
Que vous débutiez ou que vous utilisiez déjà EF Core, je recommande d’essayer ces nouvelles fonctionnalités et d’évaluer comment ils peuvent ajouter de la valeur à votre projet.
Dans cet article, nous avons couvert 10 des nouvelles fonctionnalités de EF 9. Pour un aperçu plus complet, consultez la liste complète des modifications et des améliorations de la documentation officielle: Quoi de neuf dans EF 9.
Source link