Entity Framework, LINQ: ¿no se puede usar Any con StartsWith?
Tengo una consulta LINQ de Entity Framework simple. El objetivo es encontrar todos los transportistas que comiencen con A, B o 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();
yo obtengo
System.InvalidOperationException: 'La expresión LINQ' DbSet .Where (c => __letters_0 .Any (val => val == "" || c.Name! = Null && val! = Null && c.Name.StartsWith (val) )) 'no se pudo traducir. Reescriba la consulta en un formulario que pueda traducirse o cambie a la evaluación del cliente explícitamente insertando una llamada a AsEnumerable (), AsAsyncEnumerable (), ToList () o ToListAsync (). Verhttps://go.microsoft.com/fwlink/?linkid=2101038 para más información.'
¿No hay forma de hacer esto?
Respuestas
En Entity Framework 6, esta consulta funcionaría bien. EF6 admite la traducción de SQL StartsWithy admite el uso de secuencias locales ( letters) en una expresión de consulta.
EF core 3 (la versión en la pregunta como indica el mensaje de excepción) también admite la traducción SQL de StartsWith. El problema aquí (que la otra respuesta pierde por completo) es que la secuencia local se usa de una manera que no es compatible. Una consulta como ...
var results = db.Carriers.AsNoTracking()
.Where(c => letters.Contains(c.Name))
.ToList();
... sería compatible porque letterssimplemente se puede traducir en una INcláusula. Pero, por supuesto, esa es una consulta completamente diferente.
El uso letters.Anyrequiere que EF se convierta lettersen "algo" que se pueda unir en SQL. EF6 hace esto construyendo un conjunto de resultados en la 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
Lo cual funciona, pero no es escalable en absoluto. EF core 3 no lo admite y no hay una solución fácil como se sugiere en la otra respuesta.
Una posible solución alternativa es construir un predicado con predicados ||(O), por ejemplo:
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();
¿Dónde PredicateBuilderestá un constructor de predicados como Linqkit o este ? Pero este método tampoco es escalable. EF crea un parámetro para cada entrada letters, por lo que puede alcanzar el umbral de 2100 parámetros en Sql Server.
Entity Framework no permite el uso de predicados personalizados. Esa es la razón por la que su consulta falla con un mensaje:
... no se pudo traducir. O reescriba la consulta en una forma que se pueda traducir ...
Por lo tanto, lo más probable es que esto signifique que su versión de EF no es compatible StartsWitho que hay alguna combinación, como StartsWithy Anyque no se puede traducir.
Una solución alternativa podría ser:
- Primero, puede filtrar su consulta con la forma más cercana, utilizando el Containsmétodo.
var results = students.Where(s => letters.Any(l => s.name.Contains(l)));
Aquí hay un ejemplo similar que escribí para .NET fiddle.
- A continuación, en caso de que aún quiera usar
StartsWithpero su EF no lo admita, puede convertir su consulta a la colección 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)));
donde filter_by_contains_queryes una consulta de la primera parte.
Más:
- Acerca de los métodos compatibles con EF, consulte aquí .
- Debates sobre
StartsWith,EndsWithyContainsaquí