Say we have a collection of Users (with Name, Age, ...) and rule definitions like this:
static List<Rule> rules = new List<Rule> {
new Rule ("Age", "GreaterThan", "20"),
new Rule ( "Name", "Equal", "John"),
new Rule ( "Tags", "Contains", "C#" )
};
and we want to be able to evaluate the rules:
// Returns true if User satisfies given rule (e.g. 'user.Age > 20')
bool Matches(User user, Rule rule)
{
// how to implement this?
}
One obvious solution is to use reflection.
Here we will show a solution which uses
Expression trees to
compile the rules into fast executable delegates. We can then evaluate the rules as if they were normal boolean functions.
public static Func<T, bool> CompileRule<T>(Rule r)
{
var paramUser = Expression.Parameter(typeof(User));
Expression expr = BuildExpr<T>(r, paramUser);
// build a lambda function User->bool and compile it
return Expression.Lambda<Func<T, bool>>(expr, paramUser).Compile();
}
static Expression BuildExpr<T>(Rule r, ParameterExpression param)
{
var left = MemberExpression.Property(param, r.MemberName);
var tProp = typeof(T).GetProperty(r.MemberName).PropertyType;
ExpressionType tBinary;
// is the operator a known .NET operator?
if (ExpressionType.TryParse(r.Operator, out tBinary)) {
var right = Expression.Constant(Convert.ChangeType(r.TargetValue, tProp));
// use a binary operation, e.g. 'Equal' -> 'u.Age == 15'
return Expression.MakeBinary(tBinary, left, right);
} else {
var method = tProp.GetMethod(r.Operator);
var tParam = method.GetParameters()[0].ParameterType;
var right = Expression.Constant(Convert.ChangeType(r.TargetValue, tParam));
// use a method call, e.g. 'Contains' -> 'u.Tags.Contains(some_tag)'
return Expression.Call(left, method, right);
}
}
Now we can implement the rule validation by compling the rules and then simply invoking them:
var rule = new Rule ("Age", "GreaterThan", "20");
Func<User, bool> compiledRule = CompileRule<User>(rule);
// true if someUser.Age > 20
bool isMatch = compiledRule(someUser);
Of course we compile all the rules just once and then use the compiled delegates:
// Compile all the rules once.
var compiledRules = rules.Select(r => CompileRule<User>(r)).ToList();
// Returns true if user satisfies all rules.
public bool MatchesAllRules(User user)
{
return compiledRules.All(rule => rule(user));
}
Note that the “compiler” is so simple because we are using 'GreaterThan' in the rule definition, and 'GreaterThan' is a known .NET name for the operator, so the string can be directly parsed. The same goes for 'Contains' – it is a method of List. If we need custom names we can build a very simple dictionary that just translates all operators before compiling the rules:
Dictionary<string, string> nameMap = new Dictionary<string, string> {
{ "greater_than", "GreaterThan" },
{ "hasAtLeastOne", "Contains" }
};
That’s it. As an improvement we could add error messages to the rule definitions and print the error messages for the unsatisfied rules.