0

I have a basic rule engine that I've built in a very similar way to the route suggested here:

How to implement a rule engine?

Ive extended it based on further requirements, and now I need to evaluate complex classes eg

EvaluateRule("Transaction.IsOpen", "Equals", "true")  

The code in its most basic form is:

var param = inputMessageType;
left = Expression.Property(param, memberName);
tProp = typeof(T).GetProperty(r.MemberName).PropertyType;
right = Expression.Constant(Convert.ChangeType(r.TargetValue, tProp));
return Expression.MakeBinary(tBinary, left, right);    

In order to evaluate complex classes I used a method similar to here:

https://stackoverflow.com/questions/16544672/dynamically-evaluating-a-property-string-with-expressions

The problem that Im having is that when I try to evaluate the rule with a property of a class (Transaction.IsOpen), I get it with the type of the root type on the right hand side of the expression but the type of the complex object on the left hand side of the expression.

This results in the error:

System.InvalidOperationException: The binary operator Equal is not defined for the types 'System.Func`2[Transaction,System.Boolean]' and 'System.Boolean'.

How do I overcome this problem? I am no expert using Expression Trees, and many of the concepts are proving difficult to grasp when the example strays from the standard documentation.

Edit: Here is the code(Ive omitted some stuff that is environment specific so as keep focus with the problem)

public Actions EvaluateRulesFromMessage(ClientEventQueueMessage message)
    {            
        var ruleGroups = _ruleRepository.GetRuleList();

    var actions = new Actions();

    foreach (var ruleGroup  in ruleGroups)
    {
        if (message.MessageType == "UI_UPDATE")
        {
            // clean up json object
            JObject dsPayload = (JObject.Parse(message.Payload));

            var msgParams = JsonConvert.DeserializeObject<UiTransactionUpdate>(message.Payload);                    
            msgParams.RulesCompleted = msgParams.RulesCompleted ?? new List<int>(); 

            var conditionsMet = false;
            // process the rules filtering out the rules that have already been evaluated                    
            var filteredRules = ruleGroup.Rules.Where(item =>
                !msgParams.RulesCompleted.Any(r => r.Equals(item.Id)));                    

            foreach (var rule in filteredRules)                                                
            {                        
                Func<UiTransactionUpdate, bool> compiledRule = CompileRule<UiTransactionUpdate>(rule, msgParams);
                if (compiledRule(msgParams))
                {
                    conditionsMet = true;

                }
                else
                {
                    conditionsMet = false;
                    break;
                }                        

            }
            if (conditionsMet) 
            {                        
                actions = AddAction(message, ruleGroup);
                break;
            }
        }
    }                
    return actions;
}

public Func<UiTransactionUpdate, bool> CompileRule<T>(Rule r, UiTransactionUpdate msg)
{
    var expression = Expression.Parameter(typeof(UiTransactionUpdate));
    Expression expr = BuildExpr<UiTransactionUpdate>(r, expression, msg);
    // build a lambda function UiTransactionUpdate->bool and compile it
    return Expression.Lambda<Func<UiTransactionUpdate, bool>>(expr, expression).Compile();
}

static Expression Eval(object root, string propertyString, out Type tProp)
{
    Type type = null;
    var propertyNames = propertyString.Split('.');
    ParameterExpression param = Expression.Parameter(root.GetType());
    Expression property = param;
    string propName = "";
    foreach (var prop in propertyNames)
    {                           
        property = MemberExpression.PropertyOrField(property, prop);
        type = property.Type;
        propName = prop;
    }

    tProp = Type.GetType(type.UnderlyingSystemType.AssemblyQualifiedName);

    var param2 = MemberExpression.Parameter(tProp);

    var e = Expression.Lambda(property, param);

    return e;
}

static Expression BuildExpr<T>(Rule r, ParameterExpression param, UiTransactionUpdate msg)
{
    Expression left;
    Type tProp;
    string memberName = r.MemberName;
    if (memberName.Contains("."))
    {
        left = Eval(msg, memberName, out tProp);            
    }
    else
    {
        left = Expression.Property(param, memberName);
        tProp = typeof(T).GetProperty(r.MemberName).PropertyType;
    }

    ExpressionType tBinary;            
    if (ExpressionType.TryParse(r.Operator, out tBinary))
    {
        Expression right=null;
        switch (r.ValueType)  ///todo: this needs to be refactored to be type independent
        {
            case TargetValueType.Value:
                right = Expression.Constant(Convert.ChangeType(r.TargetValue, tProp));
                break;                    
        }
        // use a binary operation ie true/false
        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);
    }
}
Community
  • 1
  • 1
KerSplosh
  • 466
  • 8
  • 26
  • How does your current code look now? – Yacoub Massad Sep 08 '16 at 13:59
  • the left assignment in my basic code essentially uses code similar to the answer in the bottom link. that's pretty much what is producing the error - I understand why Im getting the error, I just need a strategy for overcoming it – KerSplosh Sep 08 '16 at 14:04
  • look at the member name if it contains a "." ... if so, you can split by the dots and make nested MemberExpressions ... https://msdn.microsoft.com/en-us/library/system.linq.expressions.memberexpression(v=vs.110).aspx – DarkSquirrel42 Sep 08 '16 at 14:08
  • thanks - thats what Im doing - the problem is that the types are not matching. However Ive not used Member Expressions so will explore that option – KerSplosh Sep 08 '16 at 14:10
  • actually - I was already trying that (see the second link) and this is still where my problems lie. Is there something special I should be doing with a MemberExpression? – KerSplosh Sep 08 '16 at 14:30
  • @KerSplosh, It would be a lot easier if you provide a reproducible example. – Yacoub Massad Sep 08 '16 at 14:36
  • updated the question to include sample code – KerSplosh Sep 08 '16 at 14:48
  • if you have something like variable.A.B.C, then in the end you want for "left" something like MemberExpression(MemberExpression(MemberExpression(variable,"A"),"B"),"C") ... the emphasis was on "nested" – DarkSquirrel42 Sep 08 '16 at 14:55
  • Aha. Ok will have a go with that.. – KerSplosh Sep 08 '16 at 15:00

1 Answers1

2

The sample code does not cover all data types used in your scenario, so it's hard to tell where it breaks exactly, but from the exception System.Func'2[Transaction,System.Boolean]' and 'System.Boolean it's obvious that on left hand you have a delegate that takes in Transaction and returns bool (Func<Transaction, bool>), and on the right hand you just have bool.

It's not possible to compare Func<Transaction, bool> to bool, but it's possible to call the function and compare its result:

Func<Transaction, bool> func = ...;
bool comparand = ...;
Transaction transaction = ...;
if (func(transaction) == comparand) { ... }

What translated to expression tree:

Expression funcExpression = ... /*LambdaExpression<Func<Transaction,bool>>*/;
Expression comparandExpression = Expression.Constant(true);
Expression transactionArg = /*e.g.*/Expression.Constant(transaction);
Expression funcResultExpression = Expression.Call(funcExpression, "Invoke", null, transactionArg);
Expression equalityTestExpression = Expression.Equal(funcResultExpression, comparandExpression);
Serge Semenov
  • 9,232
  • 3
  • 23
  • 24
  • Thanks for your answer @Serge Semenov but considering that bool is just an example, it could be any type - how do I make the funcExpression generic for comparison? – KerSplosh Sep 08 '16 at 16:14
  • In this case it cannot be just generic, because you have an input argument for the function - the Transaction - which should come from somewhere. If can put together a standalone console app which can be compiled and shows the problem, I can help with that. – Serge Semenov Sep 08 '16 at 16:15
  • I'll see what I can do. I think I need to rethink as I'm already quite confused about this code as it is – KerSplosh Sep 08 '16 at 16:24