Search code examples
c#.netexpression-trees

Expression Tree compilation failing due to undefined variable?


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.


Solution

  • 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:

    • The code is very hard to follow due to variable naming
    • You should use Expression.Variable for defining expression variables
    • It's good to assign names to your expression parameters/variables for better readability of the debug view output
    • The code is definitely not doing what is intended

    From 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
    }