Compartilhar via


Filtros de consulta globais

Filtros de consulta globais 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 booliana normalmente passada para o operador de consulta LINQ Where . O EF Core aplica esses filtros automaticamente a quaisquer consultas LINQ que envolvam esses tipos de entidade. O EF Core também os aplica aos Tipos de Entidade, referenciados indiretamente por meio do uso da propriedade Include ou de navegação. Alguns aplicativos comuns desse recurso são:

  • Exclusão reversível – um tipo de entidade define uma IsDeleted propriedade.
  • Multi-tenancy – Um Tipo de Entidade define uma TenantId propriedade.

Exemplo

O exemplo a seguir mostra como usar filtros de consulta globais para implementar comportamentos de consulta de várias locações e exclusão reversível em um modelo de blog simples.

Dica

Você pode exibir o exemplo deste artigo no GitHub.

Observação

O multi-tenant é usado aqui como um exemplo simples. Há também um artigo com diretrizes 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 do campo _tenantId na entidade Blog. Esse campo será usado para associar cada instância de Blog a um locatário específico. Também é definida uma IsDeleted propriedade do tipo de entidade Post. Essa propriedade é usada para controlar se uma instância de postagem foi "excluída suavemente". Ou seja, a instância é marcada como excluída sem remover fisicamente os dados subjacentes.

Em seguida, configure os filtros de consulta em 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 automaticamente aplicadas a quaisquer consultas LINQ que envolvam esses tipos.

Dica

Observe o uso de um campo de nível de instância 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 – somente o último será aplicado. No entanto, você pode definir um único filtro com várias condições usando o operador lógico AND (&& em C#).

Uso de navegações

Você também pode usar navegações para definir filtros globais de consulta. O uso de navegações nos filtros 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 aplicará os filtros de consulta definidos em entidades referenciadas.

Para ilustrar isso, configure filtros OnModelCreating de consulta 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 Blog entidades:

var filteredBlogs = await db.Blogs.ToListAsync();

Essa consulta produz o seguinte SQL, que aplica filtros de consulta definidos para ambas as Blog entidades: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 globais de filtro de consulta, portanto, você deve ter cuidado ao defini-los. Se os ciclos forem especificados incorretamente, podem levar a loops infinitos durante a tradução de consultas.

Acessando a entidade com filtro de consulta usando a navegação obrigató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 uma entidade relacionada necessária for filtrada pelo filtro de consulta, a entidade pai não estará no resultado. Portanto, você pode obter menos elementos do que o esperado no resultado.

Para ilustrar o problema, podemos usar as entidades Blog e Post especificadas acima, assim como 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 inicializado 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 = await db.Posts.ToListAsync();
var allPostsWithBlogsIncluded = await db.Posts.Include(p => p.Blog).ToListAsync();

Com a configuração acima, a primeira consulta retorna todos os 6 Posts, no entanto, a segunda consulta retorna apenas 3. Essa incompatibilidade ocorre porque Include o método na segunda consulta carrega as entidades relacionadas Blog . 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 Post cujos Blog relacionados foram removidos por um filtro de consulta global.

Ele pode ser resolvido usando a navegação opcional em vez de necessá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 ambas as entidades Blog e Post. Dessa forma, os filtros correspondentes são aplicados a ambos Blog e Post. Posts que podem terminar num 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"));

Desabilitando filtros

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

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

Limitações

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

  • Os filtros só podem ser definidos para o tipo de entidade raiz de uma hierarquia de herança.