Generic method to filter a list object

Viewed 22289

I am trying to create a generic method that takes three parameters. 1) List collection 2) String PropertyName 3) String FilterString

The idea is we pass a collection of Objects, the name of the property of the object and a Filter Criteria and it returns back a list of Objects where the property contains the FilterString.

Also, the PropertyName is optional so if its not supplied I would like to return all objects that contain the FilterString in any property.

Any pointers on this would be really helpful.

I am trying to have a method signature like this: public static List FilterList(List collection, String FilterString, String Property = "")

This way I can call this method from anywhere and pass it any List and it would return me a filtered list.

4 Answers

You could do what you want using LINQ, as such,

var collection = ...
var filteredCollection = 
    collection.Where(item => item.Property == "something").ToList();

Otherwise, you could try Reflection,

public List<T> Filter<T>(
    List<T> collection, 
    string property, 
    string filterValue)
{
    var filteredCollection = new List<T>();
    foreach (var item in collection)
    {
         // To check multiple properties use,
         // item.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)

         var propertyInfo = 
             item.GetType()
                 .GetProperty(property, BindingFlags.Public | BindingFlags.Instance);
         if (propertyInfo == null)
             throw new NotSupportedException("property given does not exists");             

         var propertyValue = propertyInfo.GetValue(item, null);
         if (propertyValue == filterValue)
             filteredCollection.Add(item);       
    }

    return filteredCollection;
}

The problem with this solution is that changes to the name of the property or misspellings result in a runtime error, rather than a compilation error as would be using an actual property expression where the name is hard-typed.

Also, do note that based on the binding flags, this will work only on public, non-static properties. You can modify such behavior by passing different flags.

You can use a combination of reflection and dynamic expressions for this. I've put together a sample that might look a bit long at the first look. However, it matches your requirements and addresses these by

  • Using reflection to find the properties that are of type string and match the property name - if provided.
  • Creating an expression that calls string.Contains on all properties that have been identified. If several properties have been identified, the calls to string.Contains are combined by Or-expressions. This filter expression is compiled and handed to the Where extension method as a parameter. The provided list is filtered using the expression.

Follow this link to run the sample.

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;
using System.Linq.Expressions;

public class Test
{
    public static IEnumerable<T> SelectItems<T>(IEnumerable<T> items, string propName, string value)
    {
        IEnumerable<PropertyInfo> props;
        if (!string.IsNullOrEmpty(propName))
            props = new PropertyInfo[] { typeof(T).GetProperty(propName) };
        else
            props = typeof(T).GetProperties();
        props = props.Where(x => x != null && x.PropertyType == typeof(string));
        Expression lastExpr = null;
        ParameterExpression paramExpr = Expression.Parameter(typeof(T), "x");
        ConstantExpression valueExpr = Expression.Constant(value);
        foreach(var prop in props)
        {
            var propExpr = GetPropertyExpression(prop, paramExpr, valueExpr);
            if (lastExpr == null)
                lastExpr = propExpr;
            else
                lastExpr = Expression.MakeBinary(ExpressionType.Or, lastExpr, propExpr);
        }
        if (lastExpr == null)
            return new T[] {};
        var filterExpr = Expression.Lambda(lastExpr, paramExpr);
        return items.Where<T>((Func<T, bool>) filterExpr.Compile());
    }

    private static Expression GetPropertyExpression(PropertyInfo prop, ParameterExpression paramExpr, ConstantExpression valueExpr)
    {
        var memberAcc = Expression.MakeMemberAccess(paramExpr, prop);
        var containsMember = typeof(string).GetMethod("Contains");
        return Expression.Call(memberAcc, containsMember, valueExpr);
    }

    class TestClass
    {
        public string SomeProp { get; set; }
        public string SomeOtherProp { get; set; }
    }

    public static void Main()
    {
        var data = new TestClass[] {
            new TestClass() { SomeProp = "AAA", SomeOtherProp = "BBB" }, 
            new TestClass() { SomeProp = "BBB", SomeOtherProp = "CCC" }, 
            new TestClass() { SomeProp = "CCC", SomeOtherProp = "AAA" }, 
        };
        var result = SelectItems(data, "", "A");
        foreach(var item in result)
            Console.WriteLine(item.SomeProp);
    }
}

In contrast to a completely reflection based approach, this one assembles the filter expression only once and compiles it, so that I'd expect a (small) performance improvement.

You should use Dynamic LINQ, for example, given SomeClass:

public class SomeClass 
{
    public int SomeField { get; set; }
}
List<SomeClass> list = new List<SomeClass>() { new SomeClass() { SomeField = 2 } };

and then:

var temp = list.AsQueryable().Where("SomeField == 1").Select("it");
var result= temp .Cast<SomeClass>().ToList();

So your function would be even simpler, with property name and filter merged into one parameter:

public List<T> Filter<T>(List<T> list, string filter)
{
    var temp = list.AsQueryable().Where(filter).Select("it");
    return temp.Cast<T>().ToList();
}

and you can provide different filters, like for example "SomeField > 4 && SomeField < 10" etc.

Related