Appliquer un OU sur une collection dans une requête LINQ (clause dynamique)

July 29, 2011 at 3:19 PMNathanael
Lorsqu'on manipule du LINQ (Linq To SQL, Linq To Entities, Linq To Objects, bref tout ce qui a un provider IQueryable<T>), on peut être confronté à la problématique de construire dynamiquement des filtres. Boucler sur des Where comme après produit un AND des clauses:
var filterList1 = new List{"e","o"};
var f = Employees.AsQueryable();
foreach (var element in filterList1)
{
	var e = element;
	f = f.Where(emp => emp.LastName.Contains(e));
}
-- Region Parameters
DECLARE @p0 NVarChar(1000) = '%o%'
DECLARE @p1 NVarChar(1000) = '%e%'
-- EndRegion
SELECT [t0].[LastName]
FROM [Employees] AS [t0]
WHERE ([t0].[LastName] LIKE @p0) AND ([t0].[LastName] LIKE @p1)
GO
Or il est également courant de vouloir faire une boucle et appliquer un OR à la place du AND (je veux trouver les employés qui ont un "ca" OU un "da" dans leur nom ou ceux nés dans les décénnies 1930 OU 1950).
Il est possible d'effectuer tout ca en manipulant des ExpressionTrees: il faut construire un arbre d'expression qui sera évalué par le provider Linq afin de le traduire (en SQL par exemple). Tant que l'on reste avec des Expressions le provider est en mesure de le traduire (si le contenu de l'expression est traductible).
Si on décompose d'ailleurs ce qui est fait lorsqu'on enchaine plusieurs clauses Where(), on s'apercoit qu'à la fin, elles sont toutes passées dans un AND pour donner une grosse clause Where. Nous allons donc reprendre le même principe avec un OR:
public Expression<Func<T1,bool>> BuildOr<T1, T2>(Expression<Func<T1, T2, bool>> selector, IEnumerable<T2> args)
{
	// Initialisation: faux OU a = a
	var q = (Expression<Func<T1,bool>>)(u=>false);
	
	foreach (var element in args)
	{
		// Invocation du selecteur en précisant que le premier paramètre du selecteur est le premier paramètre de la fonction finale
		// et que le second paramètre du selecteur est l'item courant dans la boucle
		var t1 = Expression.Invoke(selector, q.Parameters[0], Expression.Constant(element));
		
		// OU avec les précédents
		var t2 = Expression.OrElse(q.Body, t1);
		q = Expression.Lambda<Func<T1,bool>>(t2, q.Parameters[0]);
	}
	
	return q;
}
Ce qui produit le code suivant les requêtes SQL juste après:
Employees.Where(BuildOr((Employees e, String ch) => e.LastName.Contains(ch), filterList3)).Select (e => e.LastName).Dump();
Employees.Where(BuildOr((Employees e, int year) => e.BirthDate.HasValue && year <= e.BirthDate.Value.Year && e.BirthDate.Value.Year < year + 10, filterList2)).Select (e => e.LastName).Dump();
-- Region Parameters
DECLARE @p0 NVarChar(1000) = '%da%'
DECLARE @p1 NVarChar(1000) = '%ca%'
-- EndRegion
SELECT [t0].[LastName]
FROM [Employees] AS [t0]
WHERE ([t0].[LastName] LIKE @p0) OR ([t0].[LastName] LIKE @p1)
GO

-- Region Parameters
DECLARE @p0 Int = 1930
DECLARE @p1 Int = 1930
DECLARE @p2 Int = 10
DECLARE @p3 Int = 1950
DECLARE @p4 Int = 1950
DECLARE @p5 Int = 10
-- EndRegion
SELECT [t0].[LastName]
FROM [Employees] AS [t0]
WHERE (([t0].[BirthDate] IS NOT NULL) AND (@p0 <= DATEPART(Year, [t0].[BirthDate])) AND (DATEPART(Year, [t0].[BirthDate]) < (@p1 + @p2))) OR (([t0].[BirthDate] IS NOT NULL) AND (@p3 <= DATEPART(Year, [t0].[BirthDate])) AND (DATEPART(Year, [t0].[BirthDate]) < (@p4 + @p5)))

 

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading