Start Debugging

Correção: "An exception was thrown while attempting to evaluate a LINQ query parameter expression" no EF Core 11

O EF Core lança isso quando um trecho da sua consulta avaliado no cliente falha enquanto o EF o avalia. Leia InnerException, ative EnableSensitiveDataLogging e mova a checagem de null para fora da lambda.

Isto não é uma falha de tradução. O EF Core 11 lança An exception was thrown while attempting to evaluate a LINQ query parameter expression quando ele já decidiu que uma subárvore da sua consulta é avaliável no cliente (um “parâmetro de consulta”) e o seu próprio código falhou enquanto o EF a avaliava. Nove em cada dez vezes o erro real é um NullReferenceException sobre um objeto capturado, e ele está em InnerException. Chame EnableSensitiveDataLogging() no seu DbContextOptionsBuilder para que o EF imprima a expressão exata que o engasgou, e depois tire a checagem de null da lambda e leve-a para a composição da consulta. Tudo abaixo foi verificado contra o Microsoft.EntityFrameworkCore 10.0.11 no .NET 10; o ponto onde a exceção é lançada é idêntico caractere por caractere nas versões prévias do EF Core 11, então o comportamento se mantém inalterado.

O erro em contexto

Existem duas variantes desta mensagem, e qual delas você recebe depende inteiramente de o log de dados sensíveis estar ligado ou não. Sem ele:

System.InvalidOperationException: An exception was thrown while attempting to evaluate a LINQ query parameter expression. See the inner exception for more information. To show additional information call 'DbContextOptionsBuilder.EnableSensitiveDataLogging'.
 ---> System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Linq.Expressions.Interpreter.Instruction.NullCheck(Object o)
   at System.Linq.Expressions.Interpreter.FuncCallInstruction`2.Run(InterpretedFrame frame)
   at System.Linq.Expressions.Interpreter.Interpreter.Run(InterpretedFrame frame)
   at System.Linq.Expressions.Interpreter.LightLambda.Run(Object[] arguments)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.<Evaluate>g__EvaluateCore|74_0(...)
   --- End of inner exception stack trace ---
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.Evaluate(...)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.ProcessEvaluatableRoot(...)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.VisitBinary(BinaryExpression binary)

Com EnableSensitiveDataLogging() ligado, a mensagem muda para a variante bem mais útil, que nomeia a expressão:

System.InvalidOperationException: An exception was thrown while attempting to evaluate the LINQ query parameter expression 'value(Program+<>c__DisplayClass0_0).filter.MinRating'. See the inner exception for more information.
 ---> System.NullReferenceException: Object reference not set to an instance of an object.

Repare no artigo: a mensagem não sensível diz “a LINQ query parameter expression”, a sensível diz “the LINQ query parameter expression ’…’”. Se você pesquisou uma e chegou aqui com a outra, ainda está no lugar certo. Ambas vêm do mesmo par de strings de recurso, ExpressionParameterizationException e ExpressionParameterizationExceptionSensitive.

O <>c__DisplayClass0_0 nessa expressão é a classe de closure gerada pelo compilador que guarda suas variáveis locais capturadas. filter é a variável capturada, MinRating é o acesso a membro que explodiu. Essa string sozinha normalmente já basta para achar a linha.

Por que isso acontece

Antes de conseguir montar o SQL, o EF percorre sua árvore de expressão e a divide em dois tipos de nó: os que dependem da raiz da consulta (b.Rating, que vira uma coluna) e os que não dependem (filter.MinRating, que vira um parâmetro SQL). Essa segunda categoria é o que o EF chama de funcletização, e quem cuida dela é o ExpressionTreeFuncletizer. Para cada subárvore avaliável, o EF compila um Func<object> e o invoca:

// Microsoft.EntityFrameworkCore 11, ExpressionTreeFuncletizer.EvaluateCore
try
{
    return Lambda<Func<object>>(Convert(expression, typeof(object)))
        .Compile(preferInterpretation: true)
        .Invoke();
}
catch (Exception exception)
{
    throw new InvalidOperationException(
        _logger.ShouldLogSensitiveData()
            ? CoreStrings.ExpressionParameterizationExceptionSensitive(expression)
            : CoreStrings.ExpressionParameterizationException,
        exception);
}

Esse é todo o mecanismo. Qualquer exceção que o seu código lance dentro de uma expressão capturada acaba embrulhada neste InvalidOperationException e relançada. O EF não está reclamando da sua consulta, ele está informando que executar um pedaço dela falhou.

Isso importa para a depuração. A mensagem é genérica de propósito, porque o texto da expressão pode conter dados de usuário, e é por isso que a variante detalhada fica atrás do log de dados sensíveis. O erro específico está sempre em InnerException, e o stack trace da exceção interna aponta para System.Linq.Expressions.Interpreter em vez de apontar para o seu código, porque o EF compila com preferInterpretation: true. Não procure os seus próprios frames nessa pilha. Leia o tipo e a mensagem da exceção interna.

Compare com o erro irmão, The LINQ expression could not be translated, que dispara quando o EF não consegue converter uma construção em SQL de jeito nenhum. Outra etapa do pipeline, outra correção.

Reprodução mínima

Um DbSet<Blog>, um DTO de filtro anulável e um Where que o desreferencia:

// .NET 10, C# 14, Microsoft.EntityFrameworkCore.Sqlite 10.0.11
using Microsoft.EntityFrameworkCore;

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public int Rating { get; set; }
}

public class Filter { public int MinRating { get; set; } }

public class AppDb(DbContextOptions<AppDb> o) : DbContext(o)
{
    public DbSet<Blog> Blogs => Set<Blog>();
}
// .NET 10, C# 14, EF Core 10.0.11
Filter? filter = null;                                      // came back null from the request binder
var q = db.Blogs.Where(b => b.Rating >= filter!.MinRating); // no exception yet
var rows = q.ToList();                                      // throws here

Dois detalhes que vale a pena internalizar:

Estas são as exceções internas que medi para os gatilhos mais comuns, todas contra o EF Core 10.0.11 com o provedor SQLite:

O que você escreveuInnerException
b.Rating >= filter!.MinRating com filter nuloNullReferenceException
b.Rating >= config.MinRating onde o getter lançasua própria exceção, na íntegra
b.Rating == maybe!.Value com int? maybe = nullInvalidOperationException: Nullable object must have a value.
b.Rating == empty.First() sobre uma List<int> vaziaInvalidOperationException: Sequence contains no elements
b.Rating == int.Parse(raw) com raw = "not-a-number"FormatException
b.Rating == map["nope"] sobre um Dictionary<string, int>KeyNotFoundException
b.Rating >= Bad.Value onde o inicializador estático lançaTargetInvocationException embrulhando a real
b.Name == s!.Trim() com string? s = nullNullReferenceException

A penúltima linha pega as pessoas duas vezes: um inicializador de campo estático que falha deixa três níveis de aninhamento. O embrulho, depois TargetInvocationException, e depois a exceção com que você realmente se importa. Leia ex.InnerException.InnerException antes de concluir que a mensagem é inútil.

Correção, em detalhe

A correção tem sempre a mesma forma: garantir que a expressão capturada não possa lançar quando o EF a avaliar. Há quatro maneiras de fazer isso, em ordem de preferência.

1. Compor condicionalmente fora da lambda

Esta é a correção certa para o caso esmagadoramente comum do “filtro opcional”, e ainda produz um SQL melhor, porque o predicado some por completo quando o filtro não está presente:

// .NET 10, C# 14, EF Core 10.0.11
IQueryable<Blog> q = db.Blogs;

if (filter is not null)
{
    q = q.Where(b => b.Rating >= filter.MinRating);
}

var rows = await q.ToListAsync();

Verificado com filter nulo: nenhuma exceção, e nenhuma cláusula WHERE morta no SQL gerado.

2. Extrair o valor para uma variável local antes da consulta

Se o valor é genuinamente opcional mas o predicado não é, projete-o para uma variável local com um valor padrão definido. O EF então captura um int, que não tem como lançar:

// .NET 10, C# 14, EF Core 10.0.11
var min = filter?.MinRating ?? int.MinValue;
var rows = await db.Blogs.Where(b => b.Rating >= min).ToListAsync();

Esta é também a correção para int.Parse, Guid.Parse e buscas em dicionários. Faça o parse ou a busca antes da consulta, onde você consegue tratar a falha direito, em vez de dentro de uma lambda onde a falha chega embrulhada três camadas mais fundo.

3. Curto-circuito dentro da lambda

Se você precisa manter tudo em uma expressão só, uma guarda com &&, || ou um ternário funciona. O funcletizador trata de forma especial os operadores binários de curto-circuito e as ConditionalExpression, e não avalia avidamente o ramo morto:

// .NET 10, C# 14, EF Core 10.0.11
var rows = await db.Blogs
    .Where(b => filter == null || b.Rating >= filter.MinRating)
    .ToListAsync();

// the ternary form behaves identically
var rows2 = await db.Blogs
    .Where(b => filter == null ? true : b.Rating >= filter.MinRating)
    .ToListAsync();

As três variantes (filter != null && ..., filter == null || ... e o ternário) retornaram limpas na minha reprodução com filter nulo. Ainda assim, deixe esta em terceiro lugar, por dois motivos: ela manda para o banco de dados uma cláusula WHERE sempre verdadeira quando o filtro não está presente, e se apoia em um comportamento do funcletizador que já mudou entre versões maiores. A issue dotnet/efcore#34883 é exatamente esta forma, um condicional que mistura uma condição do cliente com uma do banco, e ela regrediu para um erro interno de unbound variable durante o ciclo do EF Core 9 antes de ser corrigida.

4. Consertar o que lança

Se o culpado é um getter de propriedade que lança porque um serviço ainda não foi inicializado (o clássico é um resolvedor de tenant lendo um escopo ambiente vazio), nada do acima ajuda. A consulta está certa; a sua raiz de composição está quebrada. Faça o getter devolver um valor, ou falhar antes com uma mensagem que diga algo útil.

Pegadinhas e variantes

Filtros de consulta não são embrulhados. Se a sua lambda de HasQueryFilter lê um campo do DbContext e essa leitura lança, você recebe a sua exceção crua, não esta. Montei um contexto com HasQueryFilter(b => b.TenantId == _tenant.Current) onde _tenant.Current lança, e db.Blogs.ToList() mostrou InvalidOperationException: no tenant in scope diretamente. O motivo está no funcletizador: expressões que tocam o contexto vão pelo caminho de acesso ao contexto, que devolve uma Lambda adiada em vez de invocá-la dentro daquele bloco try. Então, se você está depurando uma configuração multi-tenant e realmente vê o embrulho de parametrização, a captura culpada está em um Where comum, não no filtro. Chamar IgnoreQueryFilters() faz a consulta passar e é um jeito rápido de confirmar qual dos dois casos você tem.

Uma coleção nula dentro de Contains não lança. Ela silenciosamente não retorna nada. Esta é a variante mais perigosa da página, porque parece uma correção:

// .NET 10, C# 14, EF Core 10.0.11, SQLite provider
List<string>? names = null;
var rows = db.Blogs.Where(b => names!.Contains(b.Name)).ToList();
// rows.Count == 0, no exception
// SELECT "b"."Id", "b"."Name", "b"."Rating" FROM "Blogs" AS "b" WHERE 0

O EF traduz uma coleção parametrizada nula para um predicado sempre falso, exatamente como faz com uma vazia. Você não recebe um erro, recebe zero linhas, e o bug vai para produção. Se no seu domínio uma lista nula significa “sem filtro”, diga isso explicitamente com uma guarda names is null ||, ou componha condicionalmente como na correção 1.

EF.Constant não te salva. Embrulhar a captura como EF.Constant(filter!.MinRating) continua lançando. A desreferência acontece enquanto o argumento é avaliado, antes de o EF sequer ver o método marcador.

Um NullReferenceException cru em vez do embrulho significa que a falha foi no seu código, não no do EF. db.Blogs.Take(filter!.MinRating) lança um NullReferenceException simples, porque Take aceita um int: o compilador de C# avalia esse argumento no ponto da chamada e ele nunca chega a fazer parte de uma árvore de expressão. O mesmo vale para Skip e para qualquer coisa que você interpole em uma string antes de passar. Só lambdas recebem o embrulho.

Encadear não ajuda. Dividir em db.Blogs.Where(b => b.Id == 0).Where(b => b.Rating >= filter!.MinRating) continua lançando. A funcletização percorre a árvore composta inteira em tempo de compilação, e não operador por operador, então um filtro anterior não consegue dar curto-circuito em uma captura posterior.

Lança em toda execução, não só na primeira. O cache de consultas compiladas é indexado pelo formato da consulta, e a funcletização roda antes da busca no cache para extrair os valores dos parâmetros. Aqui não existe o “funcionou uma vez e depois começou a falhar”.

Relacionado

Fontes

Comments

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

< Voltar