Start Debugging

Filtros de consulta nomeados vs um único filtro de consulta global no EF Core 11: qual usar?

Ambos geram o mesmo SQL. Recorra aos filtros nomeados apenas quando precisar desativar um predicado de forma independente; um único filtro combinado é mais simples para uma única preocupação no EF Core 11.

Se você tem uma única preocupação de filtragem em uma entidade, use um único HasQueryFilter(e => ...) sem nome. Se você tem duas ou mais preocupações que precisam ser levantadas de forma independente no momento da consulta, por exemplo um filtro de exclusão lógica e um filtro de tenant onde uma tela de administração precisa ver as linhas excluídas mas nunca as linhas de outro tenant, use filtros nomeados. Essa é toda a decisão. O SQL gerado é idêntico nos dois casos; a única coisa que muda é se IgnoreQueryFilters consegue desligar metade do seu predicado. Os filtros de consulta nomeados chegaram no EF Core 10 e são o mecanismo padrão para múltiplos filtros no EF Core 11 (Microsoft.EntityFrameworkCore 11.0, .NET 11, C# 14).

Tudo o que segue é sobre esse único eixo: a granularidade da desativação. Como ambas as abordagens se traduzem na mesma cláusula WHERE, aqui não há nenhuma história de desempenho nem de ecossistema. A comparação é puramente sobre quanto controle você precisa para desligar filtros.

As duas formas lado a lado

Um filtro de consulta global é um predicado que o EF Core injeta em cada consulta LINQ de um tipo de entidade. Antes do EF Core 10 você tinha exatamente um por entidade, então uma segunda preocupação significava &&:

// EF Core 9 style -- one unnamed filter, two concerns welded with &&
// .NET 11, EF Core 11 (Microsoft.EntityFrameworkCore 11.0), C# 14
modelBuilder.Entity<Invoice>()
    .HasQueryFilter(i => !i.IsDeleted && i.TenantId == _tenantId);

A sobrecarga nomeada recebe primeiro uma chave do tipo string, e as chamadas repetidas se compõem em vez de se sobrescrever:

// EF Core 11 -- two named filters on the same entity
modelBuilder.Entity<Invoice>()
    .HasQueryFilter("SoftDeletionFilter", i => !i.IsDeleted)
    .HasQueryFilter("TenantFilter",       i => i.TenantId == _tenantId);

Execute uma consulta simples contra qualquer uma das duas configurações e você obtém as mesmas linhas e o mesmo SQL:

-- Identical for both the && filter and the two named filters
SELECT ... FROM Invoices WHERE NOT IsDeleted AND TenantId = @__tenantId

Essa é a coisa importante a internalizar antes de escolher: filtros nomeados não são “filtragem mais poderosa”. Eles filtram exatamente igual. O que você compra com a cerimônia extra é um handle nomeado que você pode pegar mais tarde.

Matriz de recursos

RecursoFiltro global únicoFiltros nomeados
Filtros por entidade (EF Core 11)um predicado sem nomevários, cada um com um nome
Combinar múltiplas preocupações&& em uma expressãouma chamada HasQueryFilter por preocupação
SQL geradoWHERE unido com ANDWHERE idêntico unido com AND
IgnoreQueryFilters() (sem argumentos)descarta o filtro inteirodescarta todos os filtros nomeados
IgnoreQueryFilters(["Name"])não se aplicadescarta apenas aquele predicado
Misturar com e sem nome em uma entidaden/alança ao construir o modelo; escolha um estilo
Disponível desdeEF Core 2.0EF Core 10
Impacto no cache de planos de consultaparametrizado, um planoparametrizado, um plano
Cerimôniaa menorconstantes de nome de filtro, helpers

A única linha que muda um resultado é a linha IgnoreQueryFilters(["Name"]). Qualquer outra diferença é cosmética ou uma restrição que você resolve uma vez.

Quando usar um único filtro global

Recorra ao filtro simples sem nome quando o endereçamento extra não trouxer nada:

A vantagem do filtro único é que ele não tem strings mágicas. Não há nenhum typo IgnoreQueryFilters(["SoftDeletonFilter"]) esperando para desativar nada em silêncio, porque não há nenhum nome para digitar errado.

Quando usar filtros nomeados

Nomeie seus filtros no momento em que duas preocupações precisarem de ciclos de vida diferentes em uma única consulta:

// EF Core 11 -- filter name hidden behind a readable call site
public static class InvoiceFilters
{
    public const string SoftDelete = nameof(SoftDelete);
    public const string Tenant     = nameof(Tenant);
}

public static IQueryable<Invoice> IncludeDeleted(this IQueryable<Invoice> q)
    => q.IgnoreQueryFilters([InvoiceFilters.SoftDelete]); // tenant filter stays on

// Reads like English, cannot leak tenants
var report = await context.Invoices.IncludeDeleted().ToListAsync();

Por que não há seção de benchmark

Um artigo de comparação normalmente deve números a você. Este não deve, e vale a pena dizer isso com clareza para que você não vá procurar uma diferença que não existe. Ambas as abordagens se reduzem à mesma árvore de predicados, então o EF Core emite a mesma cláusula WHERE e o planejador de consultas vê o mesmo SQL. O valor do tenant se torna um parâmetro de consulta nos dois casos, o que significa que nenhuma das abordagens fragmenta o cache de planos como uma constante embutida faria. Não há custo por consulta ao nomear um filtro; o nome existe apenas nos metadados do modelo, não na consulta traduzida. Se você esperava que os filtros nomeados também fossem uma alavanca de desempenho, eles não são. Escolha apenas pelo eixo de granularidade da desativação. Se você está medindo a contagem de consultas enquanto adiciona qualquer filtro, o que realmente mexe o ponteiro são os joins, tratado em como detectar consultas N+1 no EF Core 11.

Os detalhes que decidem por você

Três restrições podem forçar a decisão independentemente de qual você preferiria por razões de estilo.

Você não pode misturar filtros com e sem nome em uma entidade. Assim que qualquer filtro sobre Invoice tiver um nome, todos precisam ter. Adicionar um HasQueryFilter(i => ...) sem nome a uma entidade que já tem um filtro nomeado lança no momento de construir o modelo. Então a escolha é por entidade, não por filtro: decida o estilo uma vez, e se você algum dia esperar uma segunda preocupação comutada de forma independente, comece nomeado.

IgnoreQueryFilters() sem parâmetros ainda destrói tudo. Nomear seus filtros não torna a antiga chamada sem parâmetros segura. context.Invoices.IgnoreQueryFilters().ToListAsync() descarta também o filtro de tenant. Em qualquer tabela que carregue uma coluna de tenant, trate a sobrecarga sem argumentos como um code smell e sempre passe a lista explícita de nomes que você pretende desativar. Esse único hábito é a diferença entre um interruptor de exclusão lógica e uma leitura acidental entre tenants.

Nomes de filtro como strings mágicas falham em silêncio. IgnoreQueryFilters(["SoftDeletonFilter"]) com um typo não desativa nada e não lança nenhum erro, porque o EF não tem como saber que você quis dizer um filtro real. Se você for de nomes, fixe os nomes em campos const (como no trecho acima) para que um typo seja um erro de compilação, não um vazamento em produção. Um único filtro sem nome não tem essa armadilha, o que é um ponto genuíno a seu favor quando você tem apenas uma preocupação.

Um detalhe se aplica igualmente a ambos e não é um critério de desempate: um filtro no lado obrigatório de uma navegação transforma Include em um INNER JOIN, então um pai filtrado descarta em silêncio seus filhos. Esse comportamento é idêntico quer o predicado seja nomeado ou combinado com &&. A correção, um LEFT JOIN via uma navegação opcional ou um filtro correspondente em ambas as pontas, é a mesma nos dois mundos e é explicada passo a passo no guia de filtros de consulta nomeados.

Migrando de um filtro combinado para filtros nomeados

Se você já distribui um filtro !IsDeleted && TenantId == x e agora precisa expor “mostrar excluídos” sem descartar a restrição por tenant, a migração é mecânica. Divida a única expressão em duas chamadas nomeadas:

// Before -- one filter, cannot disable half of it
modelBuilder.Entity<Invoice>()
    .HasQueryFilter(i => !i.IsDeleted && i.TenantId == _tenantId);

// After -- two named filters, soft delete now independently ignorable
modelBuilder.Entity<Invoice>()
    .HasQueryFilter(InvoiceFilters.SoftDelete, i => !i.IsDeleted)
    .HasQueryFilter(InvoiceFilters.Tenant,     i => i.TenantId == _tenantId);

Nada do comportamento de consulta padrão muda; as mesmas linhas voltam e o mesmo SQL é gerado. A única capacidade nova é que IgnoreQueryFilters([InvoiceFilters.SoftDelete]) agora existe como um bypass direcionado. Como você não pode misturar estilos, faça essa divisão para a entidade inteira em uma única edição, e audite cada chamada IgnoreQueryFilters() existente sobre essa entidade: qualquer chamada sem parâmetros que costumava significar “visão de administração com excluídos” já estava descartando o filtro de tenant, e dividir o filtro é o momento de tornar essas chamadas explícitas.

A decisão

Por padrão, um único filtro de consulta global sem nome. É a opção de menor cerimônia, não tem strings mágicas, e para o caso comum de uma única preocupação por entidade é estritamente a escolha certa. Suba para filtros nomeados no instante em que uma segunda preocupação precisar ser comutada de forma independente da primeira, o que na prática quase sempre significa “exclusão lógica ao lado de um limite de tenant ou de propriedade”. A regra prática: se você consegue imaginar uma consulta que quer um predicado desligado e o outro ligado, nomeie-os agora; caso contrário combine com && e siga em frente. O SQL não vai notar a diferença, mas o seu eu futuro depurando por que um relatório de administração vazou as faturas de outro tenant com certeza vai.

Relacionados

Fontes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Voltar