CSI Media
 
CSI Media Website Design Company Blog

CSI Blog

15 July 2011 Posted in Development

Expressing yourself dynamically



Yet another cool feature of LINQ

For my current project I am implementing a dynamic rule system. In the admin area users will be able to setup a rule by specifying one or more conditions. A condition is setup by selecting a field (such as first name, country, DOB etc), an operator (equals, less than, greater than etc) and providing a value to compare against ("Andy", "UK", "1985-09-03" etc).

The aim is that users can specify any criteria they want for a rule, e.g. "match all people from the UK where their DOB is before 1985" or "match all people who's name begins with 'Andy' and are male".

On the front end when a user performs a search it should be able to load that rule from the database and run it against the current request. So if you image the request as an object with a child object of "Customer", and that customer object has the properties "FirstName", "Country", "Gender" and "DOB", then the rule system should be able to interparate the criteria and check them against the current instance.

So how in the world do you do that? In a previous project I made a similiar rule system, but all the fields were hard coded as enum values. The rule logic had a massive switch statement that for each entry would get the applicable value from the passed in object. E.g.

switch (ruleCondition.Field)
{
case Fields.FirstName:
return search.CustomerInfo.FirstName;
case Fields.DOB:
return search.CustomerInfo.DOB;
}


The problem with the above approach is that if a new field needs adding in the future then you need to add an enum value, update the rule logic to use the new enum value, ensure the admin area supports it etc.

My approach for the current project is to use LINQ expression builder. A while ago there was a guy who worked with me who was a guru at everything .NET. He taught me LINQ and Entity Framework. He also introduced Microsoft WorkFlow. Basically that is like using Eval() in JavaScript or classic ASP. You can pass a .NET statement as a string into work flow and it will compile it in runtime to a usable function. I don't know the ins and outs of WorkFlow, but judging by it being available for .NET 2 I doubt it uses LINQ expression builder (LINQ was introduced in .NET 3.5), it is more likely using reflection, e.g. you provide "CustomerInfo.FirstName", it is using reflection to find the property "CustomerInfo" against the current search object, and then using reflection to find the property "FirstName" against the CustomerInfo object.

Don't get me wrong, WorkFlow is brilliant, but in my opinion LINQ expression building superseeds it. So what is LINQ expression builder? If you don't know LINQ, then the rest of this blog is unlikely to make sense. Take the following statement:

List foundPeople = people.Where(x=>x.FirstName == "Andy").ToList();


Looking at the LINQ part, we have:

people.Where(x=>x.FirstName == "Andy")


Lets break that down. We have a collection of "Person". We are selecting all from that collection where the "Where" condition matches. I.e. it loops through each item in the collection and adds each item that matches the criteria to a new collection that is returned. The condition in this example is FirstName == "Andy". Using a traditional foreach approach that might be:

List foundPeople = new List();
foreach (Person x in people)
{
if (x.FirstName == "Andy")
{
foundPeople.Add(x);
}
}


All those above lines reduced to a single line in LINQ, got to love it :D Anyway, we are looping through each "Person" item in the collection and for each item, inspecting the FirstName property and checking if it equals Andy.

So breaking it down, we have:
Input param: Person
Left value: "FirstName" property
Operator: Equals
Right value: "Andy" constant

The way I just broke it down above is how LINQ expression builder works. You:
1) specify the input param(s)
2) Specify the field(s) to lookup
3) Specify the criteria
4) Join it all up

So lets take it step by step. First step, specify input param:

ParameterExpression inputPerson = Expression.Parameter(typeof(Person), "x");


The above statement is creating an input parameter. We accept an instance of type "Person" and will call that parameter "x". You can call the parameter whatever you want.

Next we do:

Expression firstNameField = Expression.PropertyOrField(inputPerson, "FirstName");

The above statement is creating an expression for getting the value from the "Person" instance via the property or field "FirstName". PropertyOrField means it will look for either a public field called "FirstName" or a get property called "FirstName". We are passing in the "inputPerson" parameter, which is our "Person" instance, so effectively we are writing "x.FirstName"

ConstantExpression nameValue = Expression.Constant("Andy", typeof(string));


The above statement is creating a constant of type string with the value "Andy". Remember in our LINQ we wrote 'x.FirstName == "Andy"', well there is the "Andy" segment. Why do we have to go through the hassle of specifying typeof? Because you can provide any value, it could be an integer, an enum, a boolean etc. The typeof allows LINQ to assert that when you specify a condition the left and right side are of the same type. E.g. you are not allowed to expictly match a string to an integer (take that, VB users!)

BinaryExpression equalsCondition = Expression.Equal(firstNameField, nameValue);


The above statement is glueing the two sides of our condition together. On our left side we have x.FirstName, on our right side we have "Andy". We are specifying the join as "equal" which equates to 'x.FirstName == "Andy"'. We are nearly there!

Expression<Func> expr = Expression.Lambda<Func>(equalsCondition, new ParameterExpression[] { inputPerson });


The above statement finally puts the rule together. We specify that the rule accepts a Person and returns a bool (Func), we build the rule using the equalsCondition and then specify all the parameters that are expected (person in this case).

That results in an expression tree of our rule, but how do we use it? We compile it!

Func compiledRule = expr.Compile();


We can then use it like any other function:

List people = this.GetAllPeople();
List matched = people.Where(x=>compiledRule(x));


The compiled rule performs the logic that we built up. Obiviously it seems a lot of work to simply check if the person's name equals "Andy", but the point is that it was built dynamically.

We could had got the name from the database and fed the constant with:

String name = this.GetName();
ConstantExpression nameValue = Expression.Constant(name, name.GetType());


We could have got the operator from the database:

OperatorKey key = this.GetOperator();
BinaryExpression condition = null;

switch (key)
{
case OperatorKey.Equals:
condition = Expression.Equal(firstNameField, nameValue);
break;
case OperatorKey.NotEquals:
condition = Expression.NotEqual(firstNameField, nameValue);
break;
case OperatorKey.LessThan:
condition = Expression.LessThan(dobField, dobValue);
break;
}


Noticed in the LessThan example I used dob, you could populate that as:

DateTime dob = this.GetDOB();
ConstantExpression dobValue = Expression.Constant(dob, dob.GetType());


We could even get the field to lookup from the database!

string lookupField = this.GetLookupField();
Expression field= Expression.PropertyOrField(inputPerson, lookupField);


Icing on the cake, you can string multiple conditions together, e.g.

BinaryExpression dobCondition = Expression.Equal(dobField, dobValue);
BinaryExpression nameCondition = Expression.NotEqual(nameField, nameValue);
BinaryExpression completeCondition = Expression.AndAlso(dobCondition, nameCondition);


In the above, we are combining the dob and name conditions together using an && statement. That might look like:
x=>x.DOB == dob && x.FirstName == name

In the future if you added an extra property to the Person class, e.g. "LastName", then you could add to the database a new record to represent the "LastName" field. No code changes required (assuming the Person class is defined in a separate dll)!

That is what I am hoping to achieve with my current project. I will have a database table filled with all the available fields. In the future I can add new fields to the table and they will appear in the admin area and will function correctly in the search logic.

Tags: LINQ   .NET 3.5   .NET 4   Expression tree   Expression builder   C#   dynamic code   rules system