在 Linq 中动态构建 Where 子句






4.87/5 (65投票s)
在 Linq 中动态构建 where 子句
引言
想象一下这样一个场景:我们有一组对象,并且希望允许用户通过筛选对象的组合属性来过滤该集合。为了使场景更具体,假设我们的对象声明如下
public class Person
{
public string Name { get ; set ; }
public string Surname { get ; set ; }
public int Age { get ; set ; }
public string City { get ; set ; }
public double Salary { get ; set ; }
public bool IsHomeOwner { get ; set ; }
}
现在假设我们有一组 Persons
,如下所示(这只是为了说明目的,在实际生活中,您将从数据库中获得更大的集合)
List <Person> persons = new List <Person>
{
new Person { Name = "Flamur" , Surname = "Dauti" , Age = 39,
City = "Prishtine" , IsHomeOwner = true , Salary = 12000.0 },
new Person { Name = "Blerta" , Surname = "Frasheri" , Age = 25,
City = "Mitrovice" , IsHomeOwner = false , Salary = 9000.0 },
new Person { Name = "Berat" , Surname = "Dajti" , Age = 45,
City = "Peje" , IsHomeOwner = true , Salary = 10000.0 },
new Person { Name = "Laura" , Surname = "Morina" , Age = 23,
City = "Mitrovice" , IsHomeOwner = true , Salary = 25000.0 },
new Person { Name = "Olti" , Surname = "Kodra" , Age = 19,
City = "Prishtine" , IsHomeOwner = false , Salary = 8000.0 },
new Person { Name = "Xhenis" , Surname = "Berisha" , Age = 26,
City = "Gjakove" , IsHomeOwner = false , Salary = 7000.0 },
new Person { Name = "Fatos" , Surname = "Gashi" , Age = 32,
City = "Peje" , IsHomeOwner = true , Salary = 6000.0 },
};
假设我们希望允许用户在 UI 表单上筛选集合中的任何属性或任何属性的组合。一种方法是为每个属性和每个属性组合创建一个函数,例如
public IList<Person> FilterByName(string name)
{
return persons.Where(p => p.Name == name).ToList();
}
public IList<Person> FilterBySurname(string surname)
{
return persons.Where(p => p.Surname == surname).ToList();
}
public IList<Person> FilterByNameAndSurname(string name, string surname)
{
return persons.Where(p => p.Name == name && p.Surname == surname).ToList();
}
...
正如您所看到的,这变成了一项非常繁琐的工作,因为要涵盖所有可能组合的函数数量相当大。
过滤集合的另一种方法,更加方便和整洁,是动态构建表达式树并将其传递给 where
子句进行过滤。将构建表达式树的函数签名如下所示
public Func <Person , bool > Build(IList <Filter > filters)
其中 Filter
类声明如下
public class Filter
{
public string Property { get ; set ; }
public object Value { get ; set ; }
}
它用于包含我们想要在集合上过滤的属性名称和值。我不会详细介绍如何构建表达式树,因为网络上有大量关于它的信息,因此用于构建表达式树的类如下所示
public class PersonExpressionBuilder
{
public static Func<Person, bool> Build(IList<Filter2> filters)
{
ParameterExpression param = Expression.Parameter(typeof(Person), "t" );
Expression exp = null ;
if (filters.Count == 1)
exp = GetExpression(param, filters[0]);
else if (filters.Count == 2)
exp = GetExpression(param, filters[0], filters[1]);
else
{
while (filters.Count > 0)
{
var f1 = filters[0];
var f2 = filters[1];
if (exp == null )
exp = GetExpression(param, filters[0], filters[1]);
else
exp = Expression.AndAlso(exp, GetExpression(param, filters[0], filters[1]));
filters.Remove(f1);
filters.Remove(f2);
if (filters.Count == 1)
{
exp = Expression.AndAlso(exp, GetExpression(param, filters[0]));
filters.RemoveAt(0);
}
}
}
return Expression.Lambda<Func<Person, bool>>(exp, param).Compile();
}
private static Expression GetExpression(ParameterExpression param, Filter2 filter)
{
MemberExpression member = Expression.Property(param, filter.PropertyName);
ConstantExpression constant = Expression.Constant(filter.Value);
return Expression.Equal(member, constant);
}
private static BinaryExpression GetExpression
(ParameterExpression param, Filter2 filter1, Filter2 filter2)
{
Expression bin1 = GetExpression(param, filter1);
Expression bin2 = GetExpression(param, filter2);
return Expression.AndAlso(bin1, bin2);
}
}
为了测试我们的表达式构建器,我们将像这样在方法中使用它
List<Filter> filter = new List<Filter>
{
new Filter { PropertyName = "City" , Value = "Mitrovice" },
new Filter { PropertyName = "IsHomeOwner" , Value = false }
};
var deleg = PersonExpressionBuilder .Build(filter);
var filteredCollection = persons.Where(deleg).ToList();
目前,表达式构建器构建的表达式树仅检查属性值是否等于提供的value,但我们可以进一步扩展此方法,使其通用,以便它也可以在其他地方使用其他类型,并扩展它以检查其他比较方式。因此,我们的通用表达式构建器类如下所示
public static class ExpressionBuilder
{
private static MethodInfo containsMethod = typeof(string).GetMethod("Contains" );
private static MethodInfo startsWithMethod =
typeof(string).GetMethod("StartsWith", new Type [] {typeof(string)});
private static MethodInfo endsWithMethod =
typeof(string).GetMethod("EndsWith", new Type [] { typeof(string)});
public static Expression<Func<T,
bool >> GetExpression<T>(IList<Filter> filters)
{
if (filters.Count == 0)
return null ;
ParameterExpression param = Expression.Parameter(typeof (T), "t" );
Expression exp = null ;
if (filters.Count == 1)
exp = GetExpression<T>(param, filters[0]);
else if (filters.Count == 2)
exp = GetExpression<T>(param, filters[0], filters[1]);
else
{
while (filters.Count > 0)
{
var f1 = filters[0];
var f2 = filters[1];
if (exp == null )
exp = GetExpression<T>(param, filters[0], filters[1]);
else
exp = Expression.AndAlso(exp, GetExpression<T>(param, filters[0], filters[1]));
filters.Remove(f1);
filters.Remove(f2);
if (filters.Count == 1)
{
exp = Expression .AndAlso(exp, GetExpression<T>(param, filters[0]));
filters.RemoveAt(0);
}
}
}
return Expression.Lambda<Func<T, bool>>(exp, param);
}
private static Expression GetExpression<T>(ParameterExpression param, Filter filter)
{
MemberExpression member = Expression.Property(param, filter.PropertyName);
ConstantExpression constant = Expression.Constant(filter.Value);
switch (filter.Operation)
{
case Op.Equals:
return Expression.Equal(member, constant);
case Op.GreaterThan:
return Expression.GreaterThan(member, constant);
case Op.GreaterThanOrEqual:
return Expression.GreaterThanOrEqual(member, constant);
case Op.LessThan:
return Expression.LessThan(member, constant);
case Op.LessThanOrEqual:
return Expression.LessThanOrEqual(member, constant);
case Op.Contains:
return Expression.Call(member, containsMethod, constant);
case Op.StartsWith:
return Expression.Call(member, startsWithMethod, constant);
case Op.EndsWith:
return Expression.Call(member, endsWithMethod, constant);
}
return null ;
}
private static BinaryExpression GetExpression<T>
(ParameterExpression param, Filter filter1, Filter filter2)
{
Expression bin1 = GetExpression<T>(param, filter1);
Expression bin2 = GetExpression<T>(param, filter2);
return Expression.AndAlso(bin1, bin2);
}
}
并且 filter
类已扩展为接受比较操作
public class Filter
{
public string PropertyName { get ; set ; }
public Op Operation { get ; set ; }
public object Value { get ; set ; }
}
并且操作声明为枚举
public enum Op
{
Equals,
GreaterThan,
LessThan,
GreaterThanOrEqual,
LessThanOrEqual,
Contains,
StartsWith,
EndsWith
}
然后,新的通用表达式构建器将如下使用
List<Filter> filter = new List<Filter>()
{
new Filter { PropertyName = "City" ,
Operation = Op .Equals, Value = "Mitrovice" },
new Filter { PropertyName = "Name" ,
Operation = Op .StartsWith, Value = "L" },
new Filter { PropertyName = "Salary" ,
Operation = Op .GreaterThan, Value = 9000.0 }
};
var deleg = ExpressionBuilder.GetExpression<Person>(filter).Compile();
var filteredCollection = persons.Where(deleg).ToList();
ExpressionBuilder
类可以扩展为其他 Linq 操作。它也可以轻松地用于远程执行 Linq 语句(通过使 Filter
类可序列化,它可以传递到 WCF 服务等)。
更新
关于实现其他功能有很多问题。在添加功能时,最重要的是记住表达式生成 C# 可执行代码。因此,例如,如果您的属性是 Nullable<int>,那么您需要记住 Nullable<int> 是不同于 int 的类型,更重要的是,它并不实现 int 具有的所有属性/方法,或者如果属性是字符串,则操作 <、> >= 可能会返回意外的结果。