Filtros de consulta global

Os filtros de consulta global são predicados de consulta LINQ aplicados a Tipos de Entidade no modelo de metadados (geralmente em OnModelCreating). Um predicado de consulta é uma expressão booleana normalmente passada para o operador de consultaWhere da LINQ. O EF Core aplica esses filtros automaticamente a quaisquer consultas LINQ que envolvam esses tipos de entidade. O EF Core também os aplica a Tipos de Entidade, referenciados indiretamente por meio do uso de Incluir ou propriedade de navegação. Alguns aplicativos comuns desse recurso são:

  • Exclusão suave - Um Tipo de Entidade define uma propriedade IsDeleted.
  • Multilocação - Um Tipo de Entidade define uma propriedade TenantId.

Exemplo

O exemplo a seguir mostra como usar Filtros de Consulta Global para implementar comportamentos de consulta de multilocação e exclusão flexível em um modelo de blog simples.

Dica

Veja o exemplo deste artigo no GitHub.

Observação

A multilocação é usada aqui como um exemplo simples. Há também um artigo com orientações abrangentes para multilocação em aplicativos EF Core.

Primeiro, defina as entidades:

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

Observe a declaração de um campo _tenantId na entidade Blog. Esse campo será usado para associar cada instância do Blog a um locatário específico. Também é definida uma propriedade IsDeleted no tipo de entidade Post. Essa propriedade é usada para controlar se uma instância de postagem foi "excluída de maneira reversível". Ou seja, a instância está marcada como excluída sem remover fisicamente os dados subjacentes.

Em seguida, configure os filtros de consulta no OnModelCreating usando a API HasQueryFilter.

modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "_tenantId") == _tenantId);
modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);

As expressões de predicado passadas para as chamadas HasQueryFilter agora serão aplicadas automaticamente a quaisquer consultas LINQ para esses tipos.

Dica

Observe o uso de um campo de nível de instância de DbContext: _tenantId usado para definir o locatário atual. Os filtros de nível de modelo usarão o valor da instância de contexto correta (ou seja, a instância que está executando a consulta).

Observação

No momento, não é possível definir vários filtros de consulta na mesma entidade - apenas o último será aplicado. No entanto, você pode definir um único filtro com várias condições usando o operador de AND lógico (&& em C#).

Uso de navegações

Você também pode usar navegações na definição de filtros de consulta global. O uso de navegações no filtro de consulta fará com que os filtros de consulta sejam aplicados recursivamente. Quando o EF Core expande as navegações usadas em filtros de consulta, ele também aplica filtros de consulta definidos em entidades referenciadas.

Para ilustrar isso, configure filtros de consulta em OnModelCreating da seguinte maneira:

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

Em seguida, consulte todas as entidades Blog:

var filteredBlogs = db.Blogs.ToList();

Essa consulta produz o seguinte SQL, que aplica filtros de consulta definidos para entidades Blog e 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

Observação

Atualmente, o EF Core não detecta ciclos em definições de filtro de consulta global, portanto, você deve ter cuidado ao defini-las. Se especificado incorretamente, os ciclos podem levar a loops infinitos durante a conversão da consulta.

Acessando entidade com filtro de consulta usando a navegação necessária

Cuidado

Usar a navegação necessária para acessar a entidade que tem o filtro de consulta global definido pode levar a resultados inesperados.

A navegação necessária espera que a entidade relacionada esteja sempre presente. Se a entidade relacionada necessária for filtrada pelo filtro de consulta, a entidade pai também não estará no resultado. Assim, você pode obter menos elementos do que o esperado no resultado.

Para ilustrar o problema, podemos usar as entidades Blog e Post especificadas acima e o seguinte método OnModelCreating:

modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));

O modelo pode ser semeado com os seguintes dados:

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

O problema pode ser observado ao executar duas consultas:

var allPosts = db.Posts.ToList();
var allPostsWithBlogsIncluded = db.Posts.Include(p => p.Blog).ToList();

Com a configuração acima, a primeira consulta retorna todos os 6 Posts, no entanto, a segunda consulta retorna apenas 3. Essa incompatibilidade acontece porque o método Include na segunda consulta carrega as entidades Blog relacionadas. Como a navegação entre Blog e Post é necessária, o EF Core usa INNER JOIN ao construir a consulta:

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]

O uso do INNER JOIN filtra todos os Posts cujos Blogs relacionados foram removidos por um filtro de consulta global.

Ele pode ser resolvido usando navegação opcional em vez de obrigatória. Dessa forma, a primeira consulta permanece a mesma de antes, no entanto, a segunda consulta agora gerará LEFT JOIN e retornará 6 resultados.

modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(false);
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));

A abordagem alternativa é especificar filtros consistentes em entidades Blog e Post. Dessa forma, os filtros correspondentes são aplicados ao Blog e ao Post. Post que podem acabar em estado inesperado são removidos e ambas as consultas retornam 3 resultados.

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

Como desabilitar filtros

Os filtros podem ser desabilitados para consultas LINQ individuais usando o operador IgnoreQueryFilters.

blogs = db.Blogs
    .Include(b => b.Posts)
    .IgnoreQueryFilters()
    .ToList();

Limitações

Os filtros de consulta global têm as seguintes limitações:

  • Os filtros podem ser definidos somente para o Tipo de Entidade raiz de uma hierarquia de herança.