I have a db table that stores the following:
RuleID objectProperty ComparisonOperator TargetValue
1 age 'greater_than' 15
2 username 'equal' 'some_name'
3 tags 'hasAtLeastOne' 'some_tag some_tag2'
Now say I have a collection of these rules:
List<Rule> rules = db.GetRules();
Now I have an instance of a user also:
User user = db.GetUser(....);
How would I loop through these rules, and apply the logic and perform the comparisons etc?
if(user.age > 15)
if(user.username == "some_name")
Since the object's property like 'age' or 'user_name' is stored in the table, along with the comparison operater 'great_than' and 'equal', how could I possible do this?
C# is a statically typed language, so not sure how to go forward.
You can build a simple rules engine yourself. All you need is to create a bunch of objects with conditions and actions, store them in a collection, and run through them to evaluate the conditions and execute the actions.
A rule engine combines a set of facts that are inserted in to the system with its own Rule Set to reach a conclusion of triggering one or several actions. These rules typically describe in a declarative manner the business logic which needs to be implemented in our environment (which we assume rarely changes).
This snippet compiles the Rules into fast executable code (using Expression trees) and does not need any complicated switch statements:
(Edit : full working example with generic method)
public Func<User, bool> CompileRule(Rule r)
{
var paramUser = Expression.Parameter(typeof(User));
Expression expr = BuildExpr(r, paramUser);
// build a lambda function User->bool and compile it
return Expression.Lambda<Func<User, bool>>(expr, paramUser).Compile();
}
You can then write:
List<Rule> rules = new List<Rule> {
new Rule ("Age", "GreaterThan", "21"),
new Rule ( "Name", "Equal", "John"),
new Rule ( "Tags", "Contains", "C#" )
};
// compile the rules once
var compiledRules = rules.Select(r => CompileRule(r)).ToList();
public bool MatchesAllRules(User user)
{
return compiledRules.All(rule => rule(user));
}
Here is the implementation of BuildExpr:
Expression BuildExpr(Rule r, ParameterExpression param)
{
var left = MemberExpression.Property(param, r.MemberName);
var tProp = typeof(User).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 == 21'
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);
}
}
Note that I used 'GreaterThan' instead of 'greater_than' etc. - this is because 'GreaterThan' is the .NET name for the operator, therefore we don't need any extra mapping.
If you need custom names you can build a very simple dictionary and just translate all operators before compiling the rules:
var nameMap = new Dictionary<string, string> {
{ "greater_than", "GreaterThan" },
{ "hasAtLeastOne", "Contains" }
};
The code uses the type User for simplicity. You can replace User with a generic type T to have a generic Rule compiler for any types of objects. Also, the code should handle errors, like unknown operator name.
Note that generating code on the fly was possible even before the Expression trees API was introduced, using Reflection.Emit. The method LambdaExpression.Compile() uses Reflection.Emit under the covers (you can see this using ILSpy).
Here is some code that compiles as is and does the job. Basically use two dictionaries, one containing a mapping from operator names to boolean functions, and another containing a map from the property names of the User type to PropertyInfos used to invoke the property getter (if public). You pass the User instance, and the three values from your table to the static Apply method.
class User
{
public int Age { get; set; }
public string UserName { get; set; }
}
class Operator
{
private static Dictionary<string, Func<object, object, bool>> s_operators;
private static Dictionary<string, PropertyInfo> s_properties;
static Operator()
{
s_operators = new Dictionary<string, Func<object, object, bool>>();
s_operators["greater_than"] = new Func<object, object, bool>(s_opGreaterThan);
s_operators["equal"] = new Func<object, object, bool>(s_opEqual);
s_properties = typeof(User).GetProperties().ToDictionary(propInfo => propInfo.Name);
}
public static bool Apply(User user, string op, string prop, object target)
{
return s_operators[op](GetPropValue(user, prop), target);
}
private static object GetPropValue(User user, string prop)
{
PropertyInfo propInfo = s_properties[prop];
return propInfo.GetGetMethod(false).Invoke(user, null);
}
#region Operators
static bool s_opGreaterThan(object o1, object o2)
{
if (o1 == null || o2 == null || o1.GetType() != o2.GetType() || !(o1 is IComparable))
return false;
return (o1 as IComparable).CompareTo(o2) > 0;
}
static bool s_opEqual(object o1, object o2)
{
return o1 == o2;
}
//etc.
#endregion
public static void Main(string[] args)
{
User user = new User() { Age = 16, UserName = "John" };
Console.WriteLine(Operator.Apply(user, "greater_than", "Age", 15));
Console.WriteLine(Operator.Apply(user, "greater_than", "Age", 17));
Console.WriteLine(Operator.Apply(user, "equal", "UserName", "John"));
Console.WriteLine(Operator.Apply(user, "equal", "UserName", "Bob"));
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With