Entity Framework, LINQ: impossible d'utiliser Any avec StartsWith?
J'ai une simple requête LINQ Entity Framework. Le but est de trouver tous les transporteurs qui commencent par 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();
Je reçois
System.InvalidOperationException: 'L'expression LINQ' DbSet .Where (c => __letters_0 .Any (val => val == "" || c.Name! = Null && val! = Null && c.Name.StartsWith (val) )) 'n'a pas pu être traduit. Réécrivez la requête sous une forme qui peut être traduite ou basculez explicitement vers l'évaluation du client en insérant un appel à AsEnumerable (), AsAsyncEnumerable (), ToList () ou ToListAsync (). Voirhttps://go.microsoft.com/fwlink/?linkid=2101038 pour plus d'informations.'
N'y a-t-il aucun moyen de faire cela?
Réponses
Dans Entity Framework 6, cette requête fonctionnerait correctement. EF6 prend en charge la traduction SQL StartsWithet prend en charge l'utilisation de séquences locales ( letters) dans une expression de requête.
EF core 3 (la version dans la question comme l'indique le message d'exception) prend également en charge la traduction SQL de StartsWith. Le problème ici (que l'autre réponse manque complètement) est que la séquence locale est utilisée d'une manière qui n'est pas prise en charge. Une requête comme ...
var results = db.Carriers.AsNoTracking()
.Where(c => letters.Contains(c.Name))
.ToList();
... serait appuyée parce qu'elle letterspeut simplement être traduite en INclause. Mais bien sûr, c'est une requête entièrement différente.
L'utilisation letters.Anynécessite EF pour convertir lettersen "quelque chose" qui peut être joint dans SQL. EF6 le fait en créant un jeu de résultats dans la requête 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
Ce qui fonctionne, mais n'est pas du tout évolutif. EF core 3 ne le prend pas en charge et il n'y a pas de contournement facile comme suggéré dans l'autre réponse.
Une solution possible consiste à créer un prédicat avec des prédicats ||(Or), par exemple:
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();
Où PredicateBuilderest un constructeur de prédicat comme Linqkit ou celui-ci . Mais cette méthode n'est pas non plus évolutive. EF crée un paramètre pour chaque entrée dans letters, vous pouvez donc atteindre le seuil de 2100 paramètres dans Sql Server.
L'Entity Framework n'autorise pas l'utilisation de prédicats personnalisés. C'est la raison pour laquelle votre requête échoue avec un message:
... n'a pas pu être traduit. Soit réécrivez la requête sous une forme traduisible ...
Donc, cela signifie très probablement que votre version EF ne prend pas en charge StartsWithou qu'il existe une combinaison, comme StartsWithet Anyqui ne peut pas être traduite.
Une solution de contournement pourrait être:
- Tout d'abord, vous pouvez filtrer votre requête de la manière la plus proche, en utilisant la Containsméthode.
var results = students.Where(s => letters.Any(l => s.name.Contains(l)));
Voici un exemple similaire que j'ai écrit pour le violon .NET.
- Ensuite, si vous souhaitez toujours utiliser
StartsWithmais que votre EF ne prend pas en charge le fait que vous pouvez convertir votre requête en collection 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)));
où filter_by_contains_queryest une requête de la 1ère partie.
Plus:
- À propos des méthodes prises en charge par EF, vérifiez ici .
- Discussions autour du
StartsWith,EndsWithetContainsici