.NET Coding

Construct Dynamic Filters with C#

Filtering data with C# is pretty straight-forward when using LINQ – you get results with one-liners as

var result=cars.Where(a=>a.carMake=="Fiat" && a.speed>100);

However, we know the filtering properties (carMake and speed) and the filtering conditions (equals with carMake and greater than with speed) in advance. The more interesting case, however, is when we don’t know the filtering properties, their number and the filtering criteria in advance but have to construct them in real time. Consider the case when you have data objects with many properties and allow the user on the front end to filter it according to his/her needs. In this case Dynamic Expression Construction comes to rescue.
LINQ extension methods work with predicates (or anonymous functions). Fortunately, C# provides very powerful api to build Expression trees, which can be compiled to predicates in runtime and passed to the LINQ extension methods afterwards.
Let’s see an example. Suppose that we have a list of cars and we want to filter them dynamically for whatever purpose – here is our Car object

 
public class Car
    {
        public string Model { get; set; }
        public string Make { get; set; }
        public string CountryOfOrigin { get; set; }
        public string CountryOfProduction { get; set; }

        public int MaxMilesPerHour { get; set; }
        public int NumberOfSeats { get; set; }
        public int MilesPerGallon { get; set; }
        public double WeightInKg { get; set; }

        public decimal BasePrice { get; set; }
        public decimal FullPrice { get; set; }
    }

With 10 properties, we can filter a collection of cars in many possible ways.
Next we need a class that would represent a filter – so we need a property name for comparison, value to be compared to and operation (equal, less than etc). Let’s call it ExpressionFilter

public class ExpressionFilter
    {
        public string PropertyName { get; set; }
        public object Value { get; set; }
        public Comparison Comparison{ get; set; }
    }

Comparison is nothing more than a enum with possible operations

public enum Comparison
    {
        Equal,
        LessThan,
        LessThanOrEqual,
        GreaterThan,
        GreaterThanOrEqual,
        NotEqual,
        Contains, //for strings
        StartsWith, //for strings
        EndsWith //for strings
    }

Next comes the real thing – we need a class which can take a list of ExpressionFilters, do its thing and return an Expression tree composed of these filters, which we can use as a parameter in any LINQ extension method after compiling it. Let’s have a class that constructs And expression trees. We will call it ConstructAndExpressionTree

 
public static Expression<Func<T, bool>> ConstructAndExpressionTree<T&gt(List<ExpressionFilter> filters)
        {
            if (filters.Count == 0)
                return null;

            ParameterExpression param = Expression.Parameter(typeof(T), "t");
            Expression exp = null;

            if (filters.Count == 1)
            {
                exp = ExpressionRetriever.GetExpression<T>(param, filters[0]);
            }
            else
            {
                exp = ExpressionRetriever.GetExpression<T>(param, filters[0]);
                for (int i = 1; i < filters.Count; i++)
                {
                    exp = Expression.And(exp, ExpressionRetriever.GetExpression<T>(param, filters[i]));
                }
            }

            return Expression.Lambda<Func<T, bool>>(exp, param);
        }

The class is generic so it can be used with any type. The code is pretty straight-forward -> we start with the ParameterExpression which is the t=> part of the predicate. Then we iterate through the filters and start building the actual expression called exp here. You probably notice that we use a helper method ExpressionRetriever.GetExpression to construct the separate expressions (like a.speed>100), which we stick with Or binary operator aferwards. Here is the code of ExpressionRetriever.GetExpression:

public static class ExpressionRetriever
    {
        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 GetExpression<T>(ParameterExpression param, ExpressionFilter filter)
        {
            MemberExpression member = Expression.Property(param, filter.PropertyName);
            ConstantExpression constant = Expression.Constant(filter.Value);
            switch (filter.Comparison)
            {
                case Comparison.Equal:
                    return Expression.Equal(member, constant);
                case Comparison.GreaterThan:
                    return Expression.GreaterThan(member, constant);
                case Comparison.GreaterThanOrEqual:
                    return Expression.GreaterThanOrEqual(member, constant);
                case Comparison.LessThan:
                    return Expression.LessThan(member, constant);
                case Comparison.LessThanOrEqual:
                    return Expression.LessThanOrEqual(member, constant);
                case Comparison.NotEqual:
                    return Expression.NotEqual(member, constant);
                case Comparison.Contains:
                    return Expression.Call(member, containsMethod, constant);
                case Comparison.StartsWith:
                    return Expression.Call(member, startsWithMethod, constant);
                case Comparison.EndsWith:
                    return Expression.Call(member, endsWithMethod, constant);
                default:
                    return null;
            }
        }
    }

Expression class provides methods for the standard operations as equal, not equal, less than etc. but notice also the last three cases where we use Expression.Call to call the string methods Contains, StartsWith and EndsWith. Here lies a big potential for extension as you can use whatever methods you like defined on any type. You only should get their MethodInfo through reflection.
So sticking it all together (i skip the part where i populate the cars collection):

      static void Main(string[] args)
        {
            var filters = new List<ExpressionFilter> 
            { 
                new ExpressionFilter
                {
                    PropertyName="CountryOfOrigin",
                    Comparison=Comparison.StartsWith,
                    Value="Fr"
                },
                new ExpressionFilter
                {
                    PropertyName="MaxMilesPerHour",
                    Comparison=Comparison.GreaterThanOrEqual,
                    Value=190
                }
            };

            var expressionTree= ExpressionBuilderHelper.ExpressionBuilder.ConstructAndExpressionTree<Car>(filters);
            var anonymousFunc = expressionTree.Compile();
            var result = cars.Where(anonymousFunc);
        }

You can see that the result is a car that has MaxMilesPerHour>190 and CountryOfOrigin that starts with ‘Fr’
result

4 comments

  1. Andy Brown

    having used your code fairly successfully i’ve just stumbled across what i consider to be an error in the EF code – was just wondering whether you have experienced anything similar.

    When i pass a filter that states that a given field != 0 it adds 2 statements to the where clause of the resulting SQL. i.e.
    AND ( NOT ((0 = CAST( [Extent1].[System] AS bigint))
    AND ([Extent1].[System] IS NOT NULL)))

    Why is it taking it on itself to provide me with a null check …. all i want is the first where clause, why’s it trying to preempt what i want without me specifying it? Is there a way that i can pass null values through to the query as i can add the final null check myself if needs be.

    I know this isn’t your fault, just wondering if you’ve ever come across this issue yourself?

    1. Bogdan

      I have managed to solve this. You can delete the comments. If you are interested, I can give u via Twitter or something the solution.

      Regards,
      Thanks,
      Bogdan

Leave a Reply to Andy Brown Cancel reply

Your email address will not be published. Required fields are marked *