使用LINQ过滤集合

10

假设我们有一个Person对象的集合。

class Person 
{
     public string PersonName {get;set;}
     public string PersonAddress {get;set;}    
}

在代码的某个地方定义了一个集合

List<Person> pesonsList = new List<Person>();
我们需要一个过滤器来过滤集合并将结果返回给最终用户。假设我们有一组Filter类型的对象。
class Filter 
{
    public string FieldName {get;set;}
    public string FilterString {get;set;}
}

而在代码的某个地方,我们有

List<Filter> userFilters = new List<Filter>(); 

我们需要根据userFilters集合中定义的过滤器来过滤personsList集合的内容。其中,Filter.FieldName == "PersonName" || Filter.FieldName == "PersonAddress"。我该如何使用LINQ以一种简洁的方式完成?类似于switch / case或者扩展方法(可以从FieldName确定要查找的Person属性)等解决方案已经存在。还有别的什么技巧吗?


这是否使用内存中的LINQ或LinqToSql? - JustLoren
这是内存中的LINQ。我需要使用其他集合中定义的过滤器查询在集合中定义的一组对象。没有任何数据库交互。 - Tigran
4个回答

10
你可以使用Expression类构建一个Lambda表达式来创建一个合适的谓词。
public static Expression<Func<TInput, bool>> CreateFilterExpression<TInput>(
                                                   IEnumerable<Filter> filters)
{
    ParameterExpression param = Expression.Parameter(typeof(TInput), "");
    Expression lambdaBody = null;
    if (filters != null)
    {
        foreach (Filter filter in filters)
        {
            Expression compareExpression = Expression.Equal(
                    Expression.Property(param, filter.FieldName),
                    Expression.Constant(filter.FilterString));
            if (lambdaBody == null)
                lambdaBody = compareExpression;
            else
                lambdaBody = Expression.Or(lambdaBody, compareExpression);
        }
    }
    if (lambdaBody == null)
        return Expression.Lambda<Func<TInput, bool>>(Expression.Constant(false));
    else
        return Expression.Lambda<Func<TInput, bool>>(lambdaBody, param);
}

使用这个帮助方法,你可以在任何IQueryable<T>类上创建扩展方法,因此它适用于每个LINQ后端:

public static IQueryable<T> Where<T>(this IQueryable<T> source, 
                                          IEnumerable<Filter> filters)
{
    return Queryable.Where(source, CreateFilterExpression<T>(filters));
}

你可以这样调用它:

var query = context.Persons.Where(userFilters);

如果你想要支持IEnumerable<T>集合,你需要使用这个额外的扩展方法:

public static IEnumerable<T> Where<T>(this IEnumerable<T> source, 
                                           IEnumerable<Filter> filters)
{
    return Enumerable.Where(source, CreateFilterExpression<T>(filters).Compile());
}

请注意,这仅适用于字符串属性。如果您想要过滤字段,则需要将 Expression.Property 更改为 Expression.Field(或 MakeMemberAccess),如果您需���支持其他类型的属性而不仅仅是字符串属性,则必须向 CreateFilterExpression 方法中的 Expression.Constant 部分提供更多类型信息。


3
您可以通过反射来实现:
IQueryable<Person> filteredPersons = personsList.AsQueryable();
Type personType = typeof(Person);
foreach(Filter filter in userFilters) {
    filteredPersons = filteredPersons.Where(p => (string)personType.InvokeMember(filter.FieldName, BindingFlags.GetProperty, null, p, null) == filter.FilterString);
}

(未编译,但这应该沿着正确的轨道)

太棒了!老实说,我不知道是否会在我的真实代码中应用这种技术,但它确实非常好。我喜欢通用的东西,即使出于性能原因可能不是那么好。不幸的是,我无法为你的答案投票(我的声望低于15),但答案绝对很棒。 - Tigran
请注意,这在大多数LINQ后端上不起作用;我非常怀疑LINK到SQL能够将反射调用转换为SQL。 - Ruben
是的,我同意。顺便说一下,在我的情况下,我不需要任何数据库交互。 - Tigran

2
你能不能只做这个?
personList.Where(x => x.PersonName == "YourNameHere").ToList() ?

过滤器包括FieldName属性,因此我不知道用户是想针对PersonName还是PersonAddress进行过滤,或者可能是Person类的其他可能属性。 - Tigran

0
我会在Filter类中添加一个方法来检查过滤器是否满足要求:
class Filter 
{
    public string FieldName {get;set;}
    public string FilterString {get;set;}

    public bool IsSatisfied(object o)
    { return o.GetType().GetProperty(FieldName).GetValue(o, null) as string == FilterString;
}

然后您可以像这样使用它:

var filtered_list = personsList.Where(p => userFilters.Any(f => f.IsSatisfied(p)));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接