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