Saturday, April 16, 2011

Lambda Wrangling For Fun : Part 3 – Filtering Multiple Columns

 

We now know how to generate a dynamic predicate to filter a collection of items by a single property value.

It is often useful to be able to apply multiple filters to a collection to support more sophisticated searching and filtering operations.

Let’s define a general-purpose filter interface to encapsulate the property, the target value and the comparison function.

public interface IFilter
{
string PropertyName { get; }
dynamic TargetValue { get; }
Expression<Func<dynamic, dynamic, bool>> Comparison { get; }
Expression<Func<TSource, bool>> GetPredicateLambda<TSource>();
Expression BuildComparisonExpression<TSource>(Expression parameterExpression = null);
}

The three properties are what we expect to see, and we’ve already seen a variant of the GetPredicateLambda function before.


Before we see what the other method is needed for, let’s analyse what it means to filter multiple columns first:


One Fish, Two Fish…


A filter with a single predicate comparing against a value looks thus:

….Where(_ => predicate0(_.property0, value0))

and if we had to filter by more than one predicate would look like:

….Where(_ => predicate0(_.property0, value0) 
&& predicate1(_.property1, value1))

Notice that what we need to do is to create just the comparison part part of the lambda for each filter, with the same parameter injected into each predicate call.


This is exactly what the BuildComparisonExpression function does. Here is the implementation:

public virtual Expression BuildComparisonExpression<TSource>(Expression parameterExpression = null)
{
// This gives us * _ *
parameterExpression = parameterExpression ?? Expression.Parameter(typeof (TSource), "_");

var propertyInfo = typeof (TSource).GetProperty(PropertyName, true);
if (propertyInfo == null)
{
throw new MemberAccessException(String.Format("No such property exists: {0}", PropertyName));
}

// This gives us * _.property *
var accessorExpression = Expression.Property(parameterExpression, propertyInfo);

// This gives us * value *
var valueExpression = Expression.Constant(TargetValue);

// This gives us * comparison(_property, value) *
return Expression.Invoke(Comparison, accessorExpression, valueExpression);
}

The key to this method is the last line. Expression.Invoke generates an expression which invokes the Comparison expression with the accessorExpression and valueExpression as its arguments.


Also, we do not generate the Lambda with this expression, as we may have more than one Comparison to chain together.


We do that as follows:

public static Expression<Func<TSource, bool>> GetFilterExpression<TSource>(this IEnumerable<IFilter> filterCollection)
{
if (filterCollection == null) return _ => true;

var parameterExpression = Expression.Parameter(typeof (TSource), "_");

var aggregateFilterExpression = filterCollection.Aggregate(
(Expression) Expression.Constant(true),
(current, filter) => Expression.AndAlso(current,
filter.BuildComparisonExpression<TSource>(parameterExpression)));

return Expression.Lambda<Func<TSource, bool>>(aggregateFilterExpression, parameterExpression);
}

This function aggregates all the filter expressions, threading through the same parameterExpression, and builds the lambda function to call.


This is what a complex filter expression looks like:


Figure 1


Clean and straightforward lambdas.


We can now extend this approach to use conjunctions other than AndAlso, and perhaps special-case some comparison operations.

No comments:

Post a Comment