I'm building a rules engine that is giving me some number of headaches. The issue comes when I try to build the expression tree as constructed below:
public Tuple<Expression, ParameterExpression> BuildExpression<T>(string propertyName, Enums.Operator ruleOperator,
ParameterExpression parameterExpression, List<object> values)
{
ParameterExpression listExpression = Expression.Parameter(typeof(List<object>));
ParameterExpression counterExpression = Expression.Parameter(typeof(int));
ParameterExpression toExpression = Expression.Parameter(typeof(int));
ParameterExpression arrayExpression = Expression.Parameter(typeof(object[]));
ParameterExpression valueExpression = Expression.Parameter(typeof(object));
ParameterExpression checkExpression = Expression.Parameter(typeof(T));
ParameterExpression returnExpression = Expression.Parameter(typeof(bool));
MemberExpression body = null;
foreach (var member in propertyName.Split('.'))
{
body = MemberExpression.Property(parameterExpression, member);
}
Expression expression = body.Expression;
var type = expression.Type;
ParameterExpression propertyExpression = Expression.Parameter(type);
ParameterExpression localPropertyExpression = Expression.Parameter(type);
LabelTarget breakLabel = Expression.Label();
PropertyInfo result = typeof(List<object>).GetProperty("Count");
MethodInfo toArray = typeof(List<object>).GetMethod("ToArray");
MethodInfo getGetMethod = result.GetGetMethod();
ConstantExpression constantExpression = Expression.Constant(true);
if (ruleOperator == Enums.Operator.NotFoundIn)
{
constantExpression = Expression.Constant(false);
}
Expression loop = Expression.Block(
new ParameterExpression[] { toExpression, arrayExpression, valueExpression, counterExpression,
returnExpression, propertyExpression, localPropertyExpression, listExpression },
Expression.Assign(listExpression, Expression.Constant(values)),
Expression.Assign(toExpression, Expression.Call(listExpression, getGetMethod)),
Expression.Assign(arrayExpression, Expression.Call(listExpression, toArray)),
Expression.Assign(propertyExpression, expression),
Expression.Loop(
Expression.IfThenElse(
Expression.LessThan(counterExpression, toExpression),
Expression.Block(
Expression.Assign(valueExpression, Expression.ArrayAccess(arrayExpression, counterExpression)),
Expression.Assign(localPropertyExpression, expression),
Expression.IfThen(
Expression.Equal(propertyExpression, localPropertyExpression),
Expression.Block(Expression.Assign(returnExpression, constantExpression),
Expression.Break(breakLabel))),
Expression.Assign(Expression.ArrayAccess(arrayExpression, counterExpression), checkExpression),
Expression.PostIncrementAssign(counterExpression)),
Expression.Break(breakLabel)
), breakLabel
),
Expression.And(returnExpression, constantExpression)
);
return new Tuple<Expression, ParameterExpression>(Expression.Block(loop), checkExpression);
}
It takes a list of values as defined by the Criterion class:
public class Criterion
{
public List<object> Values { get; set; }
public string PropertyName { get; set; }
public Enums.Operator Operator_ { get; set; }
}
Which is then compiled by the method below:
public Func<T, bool>[] CombineRules<T>(Criterion[] criteria)
{
var list = new List<Func<T, bool>>();
foreach (var criterion in criteria)
{
var expressionBuilder = new ExpressionBuilder();
var param = Expression.Parameter(typeof(T));
var expression =
expressionBuilder.BuildExpression<T>(criterion.PropertyName, criterion.Operator_, param, criterion.Values);
var func = Expression.Lambda<Func<T, bool>>(
expression.Item1, expression.Item2).Compile();
list.Add(func);
}
return list.ToArray();
}
However, compilation fails with the following exception:
System.InvalidOperationException: variable '' of type 'SnippInteractive.Web.Common.Models.V2.LineItem' referenced from scope '', but it is not defined
If anyone has any helpful suggestions I'd be extremely grateful.
Thanks for reading.
You can use expression debug view to see what you have built. For your expression, it shows this (after assigning name "x" to your param
and called with a simple Foo
class having int
property Bar
):
.Block() {
.Block(
System.Int32 $var1,
System.Object[] $var2,
System.Object $var3,
System.Int32 $var4,
System.Boolean $var5,
ConsoleApplication6.Foo $var6,
ConsoleApplication6.Foo $var7,
System.Collections.Generic.List`1[System.Object] $var8) {
$var8 = .Constant<System.Collections.Generic.List`1[System.Object]>(System.Collections.Generic.List`1[System.Object]);
$var1 = .Call $var8.get_Count();
$var2 = .Call $var8.ToArray();
$var6 = $x;
.Loop {
.If ($var4 < $var1) {
.Block() {
$var3 = $var2[$var4];
$var7 = $x;
.If ($var6 == $var7) {
.Block() {
$var5 = True;
.Break #Label1 { }
}
} .Else {
.Default(System.Void)
};
$var2[$var4] = $var9;
$var4++
}
} .Else {
.Break #Label1 { }
}
}
.LabelTarget #Label1:;
$var5 & True
}
}
As you can see, a lot of variables are used without being assigned, which is causing the exception you are getting.
Some things to note:
Expression.Variable
for defining expression variablesFrom what I see, looks like you are trying to build something like object.property in/not in values
expression. If that's true, here is how you can do that:
public Tuple<Expression, ParameterExpression> BuildExpression<T>(string propertyName, Enums.Operator ruleOperator, ParameterExpression target, List<object> values)
{
var property = propertyName.Split('.').Aggregate((Expression)target, Expression.PropertyOrField);
var propertyValue = Expression.Variable(property.Type, "propertyValue");
var array = Expression.Variable(typeof(object[]), "array");
var length = Expression.Variable(typeof(int), "length");
var index = Expression.Variable(typeof(int), "index");
var value = Expression.Variable(typeof(object), "value");
var result = Expression.Variable(typeof(bool), "result");
var endLoop = Expression.Label("endLoop");
bool success = ruleOperator != Enums.Operator.NotFoundIn;
Expression body = Expression.Block
(
new ParameterExpression[] { propertyValue, array, length, index, result },
Expression.Assign(propertyValue, property),
Expression.Assign(array, Expression.Call(Expression.Constant(values), "ToArray", Type.EmptyTypes)),
Expression.Assign(length, Expression.ArrayLength(array)),
Expression.Assign(index, Expression.Constant(0)),
Expression.Assign(result, Expression.Constant(!success)),
Expression.Loop
(
Expression.IfThenElse
(
Expression.LessThan(index, length),
Expression.Block
(
Expression.IfThen
(
Expression.Equal(propertyValue, Expression.Convert(Expression.ArrayIndex(array, index), property.Type)),
Expression.Block
(
Expression.Assign(result, Expression.Constant(success)),
Expression.Break(endLoop)
)
),
Expression.PostIncrementAssign(index)
),
Expression.Break(endLoop)
),
endLoop
),
result
);
return Tuple.Create(body, target);
}
which outputs this:
.Block(
System.Int32 $propertyValue,
System.Object[] $array,
System.Int32 $length,
System.Int32 $index,
System.Boolean $result) {
$propertyValue = $x.Bar;
$array = .Call .Constant<System.Collections.Generic.List`1[System.Object]>(System.Collections.Generic.List`1[System.Object]).ToArray();
$length = $array.Length;
$index = 0;
$result = False;
.Loop {
.If ($index < $length) {
.Block() {
.If ($propertyValue == (System.Int32)$array[$index]) {
.Block() {
$result = True;
.Break endLoop { }
}
} .Else {
.Default(System.Void)
};
$index++
}
} .Else {
.Break endLoop { }
}
}
.LabelTarget endLoop:;
$result
}