Filtres de requête globale
Les filtres de requête globaux sont des prédicats de requête LINQ appliqués aux types d’entité dans le modèle de métadonnées (généralement dans OnModelCreating
). Un prédicat de requête est une expression booléenne qui est généralement passée à l'opérateur de requête LINQ Where
. EF Core applique ces filtres automatiquement à toutes les requêtes LINQ impliquant ces types d’entités. EF Core les applique également aux types d’entités, référencés indirectement par le biais de l’utilisation de la propriété d’inclusion ou de navigation. Voici deux applications courantes de cette fonctionnalité :
- Suppression réversible : un type d’entité définit une propriété
IsDeleted
. - Architecture multilocataire : un type d’entité définit une propriété
TenantId
.
Exemple
L’exemple suivant montre comment utiliser les filtres de requête globale pour implémenter des comportements de suppression réversible et d’architecture multilocataire dans un modèle de création de blogs simple.
Conseil
Vous pouvez afficher cet exemple sur GitHub.
Remarque
Le service d’architecture multi-locataire est utilisé dans ce contexte à titre de simple exemple. Il existe également un article avec des conseils détaillés pour multilocataire dans les applications EF Core.
Tout d’abord définissez les entités :
public class Blog
{
#pragma warning disable IDE0051, CS0169 // Remove unused private members
private string _tenantId;
#pragma warning restore IDE0051, CS0169 // Remove unused private members
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public bool IsDeleted { get; set; }
public Blog Blog { get; set; }
}
Notez la déclaration d’un champ _tenantId
sur l’entité Blog
. Ce champ doit servir à associer chaque instance de blog à un client spécifique. Une propriété IsDeleted
est également définie sur le type d’entité Post
. Cette propriété est utilisée pour déterminer si une instance post a été « supprimée de façon réversible ». Autrement dit, l’instance est marquée comme supprimée sans que les données sous-jacentes soient réellement supprimées.
Ensuite, configurez les filtres de requête dans OnModelCreating
à l’aide de l’API HasQueryFilter
.
modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "_tenantId") == _tenantId);
modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);
Les expressions de prédicat passées aux appels HasQueryFilter
seront désormais automatiquement appliquées à toutes les requêtes LINQ pour ces types.
Conseil
Notez l’utilisation d’un champ de niveau d’instance DbContext : _tenantId
permet de définir le client en cours. Les filtres au niveau du modèle utilisent la valeur de l’instance de contexte correcte (c’est-à-dire celle qui exécute la requête).
Remarque
Il n’est actuellement pas possible de définir plusieurs filtres de requête sur la même entité ; seul le dernier sera appliqué. Toutefois, vous pouvez définir un filtre unique avec plusieurs conditions à l’aide de l’opérateur de AND
logique (&&
en C#).
Utilisation des navigations
Vous pouvez également utiliser des navigations dans la définition de filtres de requête globaux. L’utilisation de navigations dans le filtre de requête entraîne l’application de filtres de requête de manière récursive. Lorsque EF Core développe les navigations utilisées dans les filtres de requête, il applique également des filtres de requête définis sur les entités référencées.
Pour illustrer cela, configurez les filtres de requête dans OnModelCreating
de la manière suivante :
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog);
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Posts.Count > 0);
modelBuilder.Entity<Post>().HasQueryFilter(p => p.Title.Contains("fish"));
Ensuite, interrogez toutes les entités Blog
:
var filteredBlogs = db.Blogs.ToList();
Cette requête produit le code SQL suivant, qui applique les filtres de requête définis pour les entités Blog
et Post
:
SELECT [b].[BlogId], [b].[Name], [b].[Url]
FROM [Blogs] AS [b]
WHERE (
SELECT COUNT(*)
FROM [Posts] AS [p]
WHERE ([p].[Title] LIKE N'%fish%') AND ([b].[BlogId] = [p].[BlogId])) > 0
Remarque
Actuellement EF Core ne détecte pas les cycles dans les définitions de filtre de requête globale. vous devez donc être prudent lorsque vous les définissez. S’ils sont spécifiés de manière incorrecte, les cycles peuvent entraîner des boucles infinies lors de la traduction de la requête.
Accès à une entité avec un filtre de requête à l’aide de la navigation requise
Avertissement
L’utilisation de la navigation requise pour accéder à l’entité qui a un filtre de requête global défini peut entraîner des résultats inattendus.
La navigation obligatoire s’attend à ce que l’entité associée soit toujours présente. Si l’entité connexe nécessaire est filtrée par le filtre de requête, l’entité parente n’est pas dans le résultat. Vous risquez de recevoir moins d’éléments que prévu dans le résultat.
Pour illustrer le problème, nous pouvons utiliser les entités Blog
et Post
spécifiées ci-dessus et la méthode OnModelCreating
suivante :
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
Le modèle peut être amorcé avec les données suivantes :
db.Blogs.Add(
new Blog
{
Url = "http://sample.com/blogs/fish",
Posts = new List<Post>
{
new Post { Title = "Fish care 101" },
new Post { Title = "Caring for tropical fish" },
new Post { Title = "Types of ornamental fish" }
}
});
db.Blogs.Add(
new Blog
{
Url = "http://sample.com/blogs/cats",
Posts = new List<Post>
{
new Post { Title = "Cat care 101" },
new Post { Title = "Caring for tropical cats" },
new Post { Title = "Types of ornamental cats" }
}
});
Le problème peut être observé lors de l’exécution de deux requêtes :
var allPosts = db.Posts.ToList();
var allPostsWithBlogsIncluded = db.Posts.Include(p => p.Blog).ToList();
Avec le programme d’installation ci-dessus, la première requête retourne tous les 6 Post
, mais la deuxième requête retourne uniquement 3. Cette incompatibilité se produit, car Include
méthode dans la deuxième requête charge les entités Blog
associées. Étant donné que la navigation entre Blog
et Post
est requise, EF Core utilise INNER JOIN
lors de la construction de la requête :
SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[IsDeleted], [p].[Title], [t].[BlogId], [t].[Name], [t].[Url]
FROM [Posts] AS [p]
INNER JOIN (
SELECT [b].[BlogId], [b].[Name], [b].[Url]
FROM [Blogs] AS [b]
WHERE [b].[Url] LIKE N'%fish%'
) AS [t] ON [p].[BlogId] = [t].[BlogId]
L’utilisation du INNER JOIN
filtre tous les Post
dont les Blog
associées ont été supprimées par un filtre de requête global.
Vous pouvez le résoudre à l’aide de la navigation facultative au lieu de obligatoire.
De cette façon, la première requête reste la même qu’avant, mais la deuxième requête génère LEFT JOIN
et retourne 6 résultats.
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(false);
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
Une autre approche consiste à spécifier des filtres cohérents sur les entités Blog
et Post
.
De cette façon, les filtres de correspondance sont appliqués à Blog
et Post
. Post
qui pourraient finir par un état inattendu sont supprimés et les deux requêtes retournent 3 résultats.
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
modelBuilder.Entity<Post>().HasQueryFilter(p => p.Blog.Url.Contains("fish"));
Désactivation des filtres
Les filtres peuvent être désactivés pour des requêtes LINQ individuelles à l’aide de l’opérateur IgnoreQueryFilters.
blogs = db.Blogs
.Include(b => b.Posts)
.IgnoreQueryFilters()
.ToList();
Limites
Les filtres de requête globale présentent les limitations suivantes :
- Les filtres ne peuvent être définis que pour le type d’entité racine d’une hiérarchie d’héritage.