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 Postdont les Blogassocié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.