Entity Framework, LINQ: não é possível usar Any com StartsWith?
Eu tenho uma consulta simples do Entity Framework LINQ. O objetivo é encontrar todas as operadoras que começam com A, B ou C:
var letters = new List<string>() { "A", "B", "C" }; // Dynamic, can be many
var results = db.Carriers.AsNoTracking()
.Where(c => letters.Any(val => c.Name.StartsWith(val)))
.ToList();
eu recebo
System.InvalidOperationException: 'The LINQ expression' DbSet .Where (c => __letters_0 .Any (val => val == "" || c.Name! = Null && val! = Null && c.Name.StartsWith (val) )) 'não pôde ser traduzido. Reescreva a consulta em um formato que possa ser traduzido ou alterne para a avaliação do cliente explicitamente inserindo uma chamada para AsEnumerable (), AsAsyncEnumerable (), ToList () ou ToListAsync (). Vejohttps://go.microsoft.com/fwlink/?linkid=2101038 Para maiores informações.'
Não tem como fazer isso?
Respostas
No Entity Framework 6, essa consulta funcionaria bem. EF6 oferece suporte à tradução de SQL StartsWithe ao uso de sequências locais ( letters) em uma expressão de consulta.
EF core 3 (a versão em questão conforme a mensagem de exceção indica) também oferece suporte à tradução de SQL StartsWith. O problema aqui (que a outra resposta perde completamente) é que a sequência local é usada de uma maneira não suportada. Uma consulta como ...
var results = db.Carriers.AsNoTracking()
.Where(c => letters.Contains(c.Name))
.ToList();
... seria suportado porque letterspode simplesmente ser traduzido em uma INcláusula. Mas é claro que essa é uma pergunta totalmente diferente.
O uso letters.Anyrequer EF para se converter lettersem "algo" que pode ser associado no SQL. EF6 faz isso criando um conjunto de resultados na consulta SQL:
WHERE EXISTS (SELECT
1 AS [C1]
FROM (SELECT
N'A' AS [C1]
FROM ( SELECT 1 AS X ) AS [SingleRowTable1]
UNION ALL
SELECT
N'B' AS [C1]
FROM ( SELECT 1 AS X ) AS [SingleRowTable2]
UNION ALL
SELECT
N'C' AS [C1]
FROM ( SELECT 1 AS X ) AS [SingleRowTable3]) AS [UnionAll2]
WHERE ( CAST(CHARINDEX([UnionAll2].[C1], [Extent1].[Name]) AS int)) = 1
O que funciona, mas não é escalonável. O EF core 3 não o suporta e não há solução fácil, como sugerido na outra resposta.
Uma possível solução é construir um predicado com predicados ||(Or), por exemplo:
var pred = letters.Aggregate(PredicateBuilder.False<Carrier>(),
(p,x) => p = p.Or(c => c.Name.StartsWith(x)));
var results = db.Carriers.AsNoTracking()
.Where(pred)
.ToList();
Onde PredicateBuilderestá um construtor de predicado como o Linqkit ou este . Mas esse método também não é escalonável. EF cria um parâmetro para cada entrada em letters, então você pode atingir o limite de 2100 parâmetros no Sql Server.
O Entity Framework não permite o uso de predicados personalizados. Essa é a razão pela qual sua consulta está falhando com uma mensagem:
... não pôde ser traduzido. Reescreva a consulta em um formato que possa ser traduzido ...
Então, isso provavelmente significa que sua versão EF não é compatível StartsWithou há alguma combinação, como StartsWithe Anyque não pode ser traduzida.
Uma solução alternativa poderia ser:
- Primeiro, você pode filtrar sua consulta da maneira mais próxima, usando o Containsmétodo.
var results = students.Where(s => letters.Any(l => s.name.Contains(l)));
Aqui está um exemplo semelhante que escrevi para o .NET fiddle.
- Em seguida, caso você ainda queira usar,
StartsWithmas seu EF não suporta, você pode converter sua consulta para a coleção IN-MEMORY:
var results = filter_by_contains_query
.ToList() // convert filtered result to in-memory collection
.Where(i => letters.Any(l => i.StudentName.StartsWith(l)));
onde filter_by_contains_queryestá uma consulta da 1ª parte.
Mais:
- Sobre os métodos suportados pela EF, verifique aqui .
- Discussões em torno do
StartsWith,EndsWitheContainsaqui