In my previous two columns for CODE Magazine (Part 1: Dynamic Lambda Expressions and Part 2: Leveraging and Querying String, Object Dictionaries), I introduced you to how dynamic lambdas and expression trees work in .NET. As promised, in this installment, I'm going to share some classes that make it easier for you to re-use the concepts I've introduced in the last two issues. If you haven't read the previous articles in this series or are unfamiliar with dynamic lambda expressions, I strongly encourage you to read those articles before continuing with this one.

The most common use case for lambda expressions is with LINQ (Language Integrated Query). LINQ is a great abstraction for executing queries against data collections. If your LINQ statements rely on static lambda expressions, LINQ itself should be usable as is. But what if your development staff isn't familiar with LINQ or if you need to generate LINQ statements at run time? In these cases, you and your team may benefit from a different, less leaky abstraction. By less leaky, I mean an abstraction that doesn't expose more complexity than what's absolutely necessary to accomplish the task at hand. Let's get started.

What Do You Need?

To accomplish the task, you need the following:

  • Defined criteria that specifies a property, some value to compare to, and a comparison type such as equals, greater than, or equals, etc.
  • String multiple criteria that supports both And and Or conjunctions
  • Some level of validation
  • A lambda that can be used as the basis of a LINQ query

The following snippet illustrates these features:

_people.Where(x =>
    (x.Age > 60 && x.Address.City == "Paoli") ||
        (x.Address.Street == "Market Street"));

Read from left to right, the query fetches records where a person's age > 60 and the city is Paoli or if the street is Market Street. If the query structure is constant and the only variable parts are the comparison values, there's no problem in referencing variables:

var age = 60;
var city = "Paoli";
var street = "Market Street";

_people.Where(x =>
    (x.Age > age && x.Address.City == city) ||
        (x.Address.Street == street));

In the real world, of course, more flexibility is needed. You need some way to specify any field in the collection as part of a query. You also need to be able to specify multiple criteria elements that can be joined with any combination of And/Or logic. This is the precise use case for dynamic lambda expressions and that's the use case I'm going to tackle in this article.

Expression Criteria Class

The Expression Criteria Class encapsulates all of the functionality required to build dynamic LINQ queries without the need to understand how LINQ works. Let's step through the code:

public class ExpressionCriteria<T>

You won't know at design time what the specific data type will be. That's precisely why generics exist in .NET. After you step through the code, the next section demonstrates how to use this class.

List<ExpressionCriterion> _expressionCriterion = new
    List<ExpressionCriterion>();

The Expression Criteria Class consists of one or more criteria elements. Each criteria element manifests as an instance of an Expression Criterion Class that are discussed later in this section.

private string _andOr = "And";

Each criterion can be grouped and joined with other criteria with an And or an Or conjunction.

public ExpressionCriteria<T> And()
    {
        _andOr = "And";
        return this;
    }
    public ExpressionCriteria<T> Or()
    {
        _andOr = "Or";
        return this;
    }

The Expression Criteria Class exposes a fluent interface. By invoking the And() or Or() methods, the criterion specified will be joined with the And or Or conjunction respectively.

public ExpressionCriteria<T> Add(string propertyName,
object value, ExpressionType operator)
{
    var newCriterion = new ExpressionCriterion(propertyName, value, 
        operator, _andOr);
    _expressionCriterion.Add(newCriterion);
    return this;
}

Keeping in line with the fluent interface, the Add() method returns an instance of the Expression Criteria class. There are three critical elements of a criterion element:

  • Property Name: The property of the Expression Criteria's type that will be used as the basis of a criterion element.
  • Value: The value used, in conjunction with an expression type to compare to the property.
  • Operator: The Expression Type used to compare the Property Name with the Value.

The following is an example of the Property, Value, Operator relationship:

x.Age > 60
  • Property: Age
  • Value: 60
  • Operator: Greater Than

When the Add() method is invoked, the current state of the _andOr variable is applied as well. By default, the value is set to And.

The following is the private class definition for each criterion:

class ExpressionCriterion
{
    public ExpressionCriterion(
        string propertyName,
        object value,
        ExpressionType operator,
        string andOr = "And")
        {
            AndOr = andOr;
            PropertyName = propertyName;
            Value = value;
            Operator = op;
            validateProperty(typeof(T), propertyName);
        }
        PropertyInfo validateProperty(Type type, string propertyName)
        {
            string[] parts = propertyName.Split('.');

                var info = (parts.Length > 1)
                ? validateProperty(
                    type.GetProperty(
                        parts[0]).PropertyType,
                        parts.Skip(1).Aggregate((a, i) => 
                            a + "." + i)): type.GetProperty(propertyName);
            if (info == null)
                throw new ArgumentException(propertyName,
                    $"Property {propertyName}
                        is not a member of
                        {type.Name}");
                return info;
        }
    public string PropertyName { get; }
    public object Value { get; }
    public ExpressionType Operator { get; }
    public string AndOr { get; }
}

Each criterion has four attributes:

  • Property Name
  • Value
  • Expression Type
  • AndOr

When an Expression Criterion Expression is created, the property name is validated against the specified type.

The first step in creating a lambda expression is to first create an expression:

Expression
GetExpression(ParameterExpression parameter, 
                ExpressionCriterion ExpressionCriteria)
{
    Expression expression = parameter;
    foreach (var member in ExpressionCriteria.PropertyName.Split('.'))
    {
        expression = Expression.PropertyOrField(expression, member);
    }
    Return
    Expression.MakeBinary(ExpressionCriteria.Operator,
        expression, Expression.Constant(ExpressionCriteria.Value));
}

Once the expression is created, a lambda expression can be created:

public Expression<Func<T, bool>>
GetLambda()
{
    Expression expression = null;
    var parameterExpression = Expression.Parameter(typeof(T), 
            typeof(T).Name.ToLower());
    foreach (var item in _expressionCriterion)
    {
        if (expression == null)
        {
            expression = GetExpression(parameterExpression, item);
        }
        else
        {
        expression = item.AndOr == "And" ?
            Expression.And(expression,
                GetExpression(parameterExpression, item)) :
            Expression.Or(expression,
                GetExpression(parameterExpression, item));
        }
    }
    return expression != null ?
        Expression.Lambda<Func<T,
            bool>>(expression, parameterExpression) : null;
}

Putting It All Together

The following code replicates the first LINQ query illustrated in this article:

var lambda = new ExpressionCriteria<Person>()
    .Add("Age", 60, ExpressionType.GreaterThan)
    And()
    .Add("Address.City", "Paoli", ExpressionType.Equal)
    Or()
    .Add("Address.Street", "Market Street", ExpressionType.Equal)
    .GetLambda().Compile();

var result = _people.Where(lambda);

With the abstraction illustrated above, two important things are accomplished. First, in the event that your developers aren't comfortable with LINQ syntax, those issues are addressed with a user-friendly fluent-type interface. Second, the class illustrated here can support on-the-fly LINQ queries. In other words, at run time, this class can be leveraged to account for any field and any combination of criteria that are joined with And or Or conjunctions.

In the next column, I'll extend the Expression Criteria Class with the ability to leverage methods and arrays.

If you want a copy of the Expression Criteria Class, follow me on Twitter: @johnvpetersen and I'll send you a link to the code.

Enjoy!!