tags:

views:

316

answers:

4

Let's say we have a collection of Person objects

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

And somewhere in the code defined collection

List<Person> pesonsList = new List<Person>();

We need to have a filter that need to filter the collection and return the result to the end user. Let's say we have a collection of Filter type objects

class Filter 
{
    public string FieldName {get;set;}
    public string FilterString {get;set;}
}

And somewhere in the code we have

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

So we need to filter the content of the personsList collection by filters defined in the userFilters collection. Where the Filter.FieldName == "PersonName" || Filter.FieldName == "PersonAddress". How can I do that with LINQ in a cool way ? The solutions like switch/case, or may be, I thought, extension method on personsList that determines from the FiledName the property of the Person to look into, are known. Something else ? Something tricky:) Thank you.

+2  A: 

You can do it via reflection:

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);
}

(not compiled, but this should be along the right track)

kevingessner
Cool! I don't know, honestly, if I will apply this technique on my real code, by the way it's very nice. I love generic stuff, even if it's not so good for performance reasons. Unfortunately I'm not able to vote for your answer (my reputation is under 15), but answer is definitely great.
Tigran
Note that this won't work on most LINQ backends; I very much doubt LINK to SQL will be able to translate reflection calls into SQL.
Ruben
Yes, I agree. By the way, in my case, I do not need any DB interaction.
Tigran
+7  A: 

You can build a lambda expression to create a proper predicate using the Expression class.

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);
}

with this helper method, you can create an extension method on any IQueryable<T> class, so this should work for every LINQ backend:

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

which you can call like

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

If you want to support IEnumerable<T> collections as well, you'll need to use this extra extension method:

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

Note that this only works for string properties. If you want to filter on fields, you'll need to change Expression.Property into Expression.Field (or MakeMemberAccess), and if you need to support other types than string properties, you'll have to provide more type information to the Expression.Constant part of the CreateFilterExpression method.

Ruben
Wow ! Cool stuff Ruben. Thank you a lot. Definitely +1.
Tigran
A: 

I would add a method to the Filter class to check if the filter is satisfied:

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;
}

You can then use it like this:

var filtered_list = personsList.Where(p => userFilters.Any(f => f.IsSatisfied(p)));
Bojan Resnik
A: 

Can't you just do personList.Where(x => x.PersonName == "YourNameHere").ToList() ?

the filter consists of FieldName property, so I don't know if the user wants to filter against PersonName or PersonAddress, or may be some other possible properties of Person class.
Tigran