Compartilhar via


Filtros de consulta globais

Os filtros de consulta globais permitem anexar um filtro a um tipo de entidade e ter esse filtro aplicado sempre que uma consulta nesse tipo de entidade é executada; pense neles como um operador LINQ Where adicional que é adicionado sempre que o tipo de entidade é consultado. Esses filtros são úteis em uma variedade de casos.

Dica

Você pode exibir o exemplo deste artigo no GitHub.

Exemplo básico – exclusão reversível

Em alguns cenários, em vez de excluir uma linha do banco de dados, é preferível definir um IsDeleted sinalizador para marcar a linha como excluída; esse padrão é chamado de exclusão reversível. A exclusão reversível permite que as linhas sejam restauradas, se necessário, ou que se preserve uma trilha de auditoria em que ainda seja possível acessar as linhas excluídas. Os filtros de consulta globais podem ser usados para filtrar linhas excluídas por padrão, ao mesmo tempo em que permitem acessá-las em locais específicos desabilitando o filtro para uma consulta específica.

Para habilitar a exclusão reversível, vamos adicionar uma propriedade IsDeleted ao nosso tipo de Blog:

public class Blog
{
    public int Id { get; set; }
    public bool IsDeleted { get; set; }

    public string Name { get; set; }
}

Agora configuramos um filtro de consulta global usando a HasQueryFilter API em OnModelCreating:

modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);

Agora podemos consultar nossas Blog entidades como de costume; o filtro configurado garantirá que todas as consultas , por padrão, filtrarão todas as instâncias em que IsDeleted for verdadeira.

Observe que, neste ponto, você deve definir IsDeleted manualmente para excluir uma entidade. Para uma solução mais completa, você pode substituir o método SaveChangesAsync do tipo de contexto para adicionar a lógica que percorre todas as entidades que o usuário excluiu e, em vez disso, alterá-las para o estado de modificado, definindo a propriedade IsDeleted como true.

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
    ChangeTracker.DetectChanges();

    foreach (var item in ChangeTracker.Entries<Blog>().Where(e => e.State == EntityState.Deleted))
    {
        item.State = EntityState.Modified;
        item.CurrentValues["IsDeleted"] = true;
    }

    return await base.SaveChangesAsync(cancellationToken);
}

Isso permite que você use APIs de EF que excluam uma instância de entidade como de costume e que elas sejam excluídas suavemente.

Usando dados de contexto – multilocação

Outro cenário principal para filtros de consulta globais é multilocação, em que seu aplicativo armazena dados pertencentes a diferentes usuários na mesma tabela. Nesses casos, geralmente há uma coluna de ID de locatário que associa a linha a um locatário específico e filtros de consulta globais podem ser usados para filtrar automaticamente as linhas do locatário atual. Isso fornece um forte isolamento de locatário para suas consultas por padrão, removendo a necessidade de pensar em filtragem para o locatário em cada consulta.

Ao contrário da exclusão reversível, a multilocação requer conhecer a ID do locatário atual; esse valor geralmente é determinado, por exemplo, quando o usuário se autentica na Web. Para fins de EF, a ID do locatário deve estar disponível na instância de contexto, para que o filtro de consulta global possa se referir a ela e usá-la ao consultar. Vamos aceitar um tenantId parâmetro no construtor do nosso tipo de contexto e referenciá-lo em nosso filtro:

public class MultitenancyContext(string tenantId) : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == tenantId);
    }
}

Isso obriga qualquer pessoa a construir um contexto a especificar o ID de inquilino associado e garante que apenas as entidades Blog com esse ID sejam retornadas de consultas por padrão.

Observação

Este exemplo mostrou apenas conceitos básicos de multi-tenancy necessários para demonstrar filtros de consulta globais. Para obter mais informações sobre multilocação e EF, consulte multilocação em aplicativos EF Core.

Usando vários filtros de consulta

A chamada HasQueryFilter com um filtro simples substitui qualquer filtro anterior, portanto, vários filtros não podem ser definidos no mesmo tipo de entidade dessa maneira:

modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);
// The following overwrites the previous query filter:
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == tenantId);

Observação

Esse recurso está sendo introduzido no EF Core 10.0 (em versão prévia).

Para definir vários filtros de consulta no mesmo tipo de entidade, eles devem ser nomeados:

modelBuilder.Entity<Blog>()
    .HasQueryFilter("SoftDeletionFilter", b => !b.IsDeleted)
    .HasQueryFilter("TenantFilter", b => b.TenantId == tenantId);

Isso permite que você gerencie cada filtro separadamente, incluindo desabilitar seletivamente um, mas não o outro.

Desabilitando filtros

Os filtros podem estar desabilitados para consultas LINQ individuais usando o IgnoreQueryFilters operador:

var allBlogs = await context.Blogs.IgnoreQueryFilters().ToListAsync();

Se vários filtros nomeados estiverem configurados, isso desabilita todos eles. Para desabilitar seletivamente filtros específicos (a partir do EF 10), passe a lista de nomes de filtro a serem desabilitados:

var allBlogs = await context.Blogs.IgnoreQueryFilters(["SoftDeletionFilter"]).ToListAsync();

Filtros de consulta e navegações obrigatórias

Cuidado

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

As navegações necessárias no EF implicam em que a entidade relacionada está sempre presente. Como as junções internas podem ser usadas para buscar entidades relacionadas, se uma entidade relacionada obrigatória for filtrada pelo filtro de consulta, a entidade pai também poderá ser filtrada. Isso pode resultar na recuperação inesperada de menos elementos do que o esperado.

Para ilustrar o problema, podemos usar Blog e Post entidades e configurá-las da seguinte maneira:

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() { Title = "Fish care 101" },
            new() { Title = "Caring for tropical fish" },
            new() { Title = "Types of ornamental fish" }
        ]
    });

db.Blogs.Add(
    new Blog
    {
        Url = "http://sample.com/blogs/cats",
        Posts =
        [
            new() { Title = "Cat care 101" },
            new() { Title = "Caring for tropical cats" },
            new() { Title = "Types of ornamental cats" }
        ]
    });

O problema pode ser observado ao executar as duas consultas a seguir:

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 todas as seis Post instâncias, mas a segunda consulta retorna apenas 3. Essa incompatibilidade ocorre porque o Include 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 todas as linhas Post cujas linhas relacionadas Blog foram removidas por um filtro de consulta. Esse problema pode ser resolvido configurando a navegação como navegação opcional em vez de necessária, fazendo com que o EF gere um LEFT JOIN em vez de um INNER JOIN:

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

Uma abordagem alternativa é especificar filtros consistentes em ambos os tipos de entidade Blog e Post; uma vez que os filtros correspondentes são aplicados a ambas as entidades Blog e Post, as linhas Post que podem acabar em um estado inesperado são removidas, fazendo com que ambas as consultas retornem 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"));

Filtros de consulta e IEntityTypeConfiguration

Se o filtro de consulta precisar acessar uma ID de inquilino ou informações contextuais semelhantes, IEntityTypeConfiguration<TEntity> poderá apresentar uma complicação adicional, pois, ao contrário de OnModelCreating, não há uma instância do seu tipo de contexto prontamente disponível para ser referenciada no filtro de consulta. Como solução alternativa, adicione um contexto fictício ao seu tipo de configuração e faça referência à seguinte:

private sealed class CustomerEntityConfiguration : IEntityTypeConfiguration<Customer>
{
    private readonly SomeDbContext _context = null!;

    public void Configure(EntityTypeBuilder<Customer> builder)
    {
        builder.HasQueryFilter(d => d.TenantId == _context.TenantId);
    }
}

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.
  • 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.