Behind the scene, LINQ uses Expression Trees for each of its operations0
Example:
var userIds = users.Where(u => u.dtContractEnd < DateTime.Now).Select(u => u.id);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^
// Expression<Func<User, bool>> Expression<Func<User, int>>
[0] Not entirely true, but let's assume it is
While powerful, LINQ has some limitations Example:
class OfficeRoom {
RepairStatus status { get; set; }
bool isBeingRepaired() { return status == RepairStatus.Repairing; }
}
var officelessUsers = users.Where(u => u.officeRoom.isBeingRepaired());
With LINQ To Objects, no problem, this works!
With LINQ To Entities, we get:
LINQ To Entities does not recognize the method 'Boolean isBeingRepaired()' method, and this method cannot be translated into a store expression.
// DOESN'T WORK
var officelessUsers = users.Where(u => u.officeRoom.isBeingRepaired());
// DOES WORK
var officelessUsers = users.Where(u => u.officeRoom.status == RepairStatus.Repairing);
// DOESN'T WORK
var officelessUsers = users.Where(u => u.officeRoom.isBeingRepaired());
// DOES WORK
var officelessUsers = users.Where(u => u.officeRoom.status == RepairStatus.Repairing);
So there must be a way to combine both dynamically
Microsoft provides a way to explore the expression tree and replace some of its nodes
ExpressionVisitor
class OfficeRoom {
RepairStatus status { get; set; }
static readonly Expression<Func<OfficeRoom, bool>> isBeingRepairedExpr =
(o => o.status == RepairStatus.Repairing);
}
var officelessUsers = users.Where(
ExprHelper.ApplyExprToProp<User, OfficeRoom>(u => u.OfficeRoom, OfficeRoom.isBeingRepairedExpr)
);
class ReplacementVisitor : ExpressionVisitor
{
readonly Expression _oldExpr;
readonly Expression _newExpr;
public ReplacementVisitor(Expression oldExpr, Expression newExpr)
{
_oldExpr = oldExpr;
_newExpr = newExpr;
}
public override Expression Visit(Expression node)
{
return node == _oldExpr ? _newExpr : base.Visit(node);
}
}
class ReplacementVisitor : ExpressionVisitor
{
readonly Expression _oldExpr;
readonly Expression _newExpr;
public ReplacementVisitor(Expression oldExpr, Expression newExpr)
{
_oldExpr = oldExpr;
_newExpr = newExpr;
}
public override Expression Visit(Expression node)
{
return node == _oldExpr ? _newExpr : base.Visit(node);
}
}
public class ExprHelper
{
public static Expression<Func<T, bool>> ApplyExprToProp<T, TNav>(
Expression<Func<T, TNav>> parent,
Expression<Func<TNav, bool>> nav)
{
// For Info: parent: u => u.OfficeRoom
// nav: o => o.status == RepairStatus.Repairing
var visitor = new ReplacementVisitor(nav.Parameters[0], parent.Body);
var body = visitor.Visit(nav.Body);
return Expression.Lambda<Func<T, bool>>(body, parent.Parameters[0]);
}
}
We can still use the Expression as a function, this way:
class OfficeRoom {
RepairStatus status { get; set; }
static readonly Expression<Func<OfficeRoom, bool>> isBeingRepairedExpr =
(o => o.status == RepairStatus.Repairing);
bool isBeingRepaired() { return OfficeRoom.isBeingRepairedExpr.Compile()(this); }
}
A Lex/Yacc type of tools with a grammar
But as powerful as they are, it's cumbersome
From this:
We get this:
{
"return": "MAX",
"elements": [
{
"return": 1,
"criterion": {
"unit": "year",
"name": "seniority",
"operator": ">=",
"value": 5
}
},
{
"return": 2,
"criterion": {
"unit": "year",
"name": "seniority",
"operator": ">=",
"value": 10
}
},
{
"return": 3,
"criterion": {
"unit": "year",
"name": "seniority",
"operator": ">=",
"value": 15
}
},
{
"return": 4,
"criterion": {
"unit": "year",
"name": "seniority",
"operator": ">=",
"value": 20
}
}
]
}
// For info: "criterion": { "unit": "year", "name": "seniority", "operator": ">=", "value": 5 }
var ruleValuesProviderType = typeof(RuleValuesProvider);
var parameter = Expression.Parameter(ruleValuesProviderType);
// For info: "criterion": { "unit": "year", "name": "seniority", "operator": ">=", "value": 5 }
var ruleValuesProviderType = typeof(RuleValuesProvider);
var parameter = Expression.Parameter(ruleValuesProviderType);
var methodInfo = ruleValuesProviderType.GetMethod("GetValue")
.MakeGenericMethod(new Type[] { typeof(int) });
var memberExpression = Expression.Call(parameter, methodInfo,
Expression.Constant(c.name), Expression.Constant(c.unit));
// For info: "criterion": { "unit": "year", "name": "seniority", "operator": ">=", "value": 5 }
var ruleValuesProviderType = typeof(RuleValuesProvider);
var parameter = Expression.Parameter(ruleValuesProviderType);
var methodInfo = ruleValuesProviderType.GetMethod("GetValue")
.MakeGenericMethod(new Type[] { typeof(int) });
var memberExpression = Expression.Call(parameter, methodInfo,
Expression.Constant(c.name), Expression.Constant(c.unit));
var constant = Expression.Constant(int.Parse(c.value));
switch (c.@operator)
{
case ">=":
body = Expression.GreaterThanOrEqual(memberExpression, constant);
break;
case "<=":
body = Expression.LessThanOrEqual(memberExpression, constant);
break;
case ">":
body = Expression.GreaterThan(memberExpression, constant);
break;
case "<":
body = Expression.LessThan(memberExpression, constant);
break;
case "==":
body = Expression.Equal(memberExpression, constant);
break;
case "!=":
body = Expression.NotEqual(memberExpression, constant);
break;
default:
throw new NotImplementedException();
}
We can implement "OR" and "AND" as well
// CompileCriterion is the function shown on the previous slide
var leftExpression = CompileCriterion(c.conditions[0], parameter);
var rightExpression = CompileCriterion(c.conditions[1], parameter);
switch (c.logicalOperator)
{
case "OR":
body = Expression.OrElse(leftExpression.Body, rightExpression.Body);
break;
case "AND":
body = Expression.AndAlso(leftExpression.Body, rightExpression.Body);
break;
default:
throw new NotImplementedException();
}
In both case, the final statement is to wrap the Expression in a lambda
return Expression.Lambda<Func<RuleValuesProvider, bool>>(body, parameter);
In both case, the final statement is to wrap the Expression in a lambda
return Expression.Lambda<Func<RuleValuesProvider, bool>>(body, parameter);
In around 100 lines of code, we have a rule engine with a DSL