How to implement a rule engine?

后端 未结 10 1910
借酒劲吻你
借酒劲吻你 2020-11-27 23:58

I have a db table that stores the following:

RuleID  objectProperty ComparisonOperator  TargetValue
1       age            \'greater_than\'             15
2          


        
相关标签:
10条回答
  • 2020-11-28 00:24

    I added implementation for and,or between rules i added class RuleExpression that represent the root of a tree that can be leaf the is simple rule or can be and,or binary expressions there for they dont have rule and have expressions:

    public class RuleExpression
    {
        public NodeOperator NodeOperator { get; set; }
        public List<RuleExpression> Expressions { get; set; }
        public Rule Rule { get; set; }
    
        public RuleExpression()
        {
    
        }
        public RuleExpression(Rule rule)
        {
            NodeOperator = NodeOperator.Leaf;
            Rule = rule;
        }
    
        public RuleExpression(NodeOperator nodeOperator, List<RuleExpression> expressions, Rule rule)
        {
            this.NodeOperator = nodeOperator;
            this.Expressions = expressions;
            this.Rule = rule;
        }
    }
    
    
    public enum NodeOperator
    {
        And,
        Or,
        Leaf
    }
    

    I have another class that compile the ruleExpression to one Func<T, bool>:

     public static Func<T, bool> CompileRuleExpression<T>(RuleExpression ruleExpression)
        {
            //Input parameter
            var genericType = Expression.Parameter(typeof(T));
            var binaryExpression = RuleExpressionToOneExpression<T>(ruleExpression, genericType);
            var lambdaFunc = Expression.Lambda<Func<T, bool>>(binaryExpression, genericType);
            return lambdaFunc.Compile();
        }
    
        private static Expression RuleExpressionToOneExpression<T>(RuleExpression ruleExpression, ParameterExpression genericType)
        {
            if (ruleExpression == null)
            {
                throw new ArgumentNullException();
            }
            Expression finalExpression;
            //check if node is leaf
            if (ruleExpression.NodeOperator == NodeOperator.Leaf)
            {
                return RuleToExpression<T>(ruleExpression.Rule, genericType);
            }
            //check if node is NodeOperator.And
            if (ruleExpression.NodeOperator.Equals(NodeOperator.And))
            {
                finalExpression = Expression.Constant(true);
                ruleExpression.Expressions.ForEach(expression =>
                {
                    finalExpression = Expression.AndAlso(finalExpression, expression.NodeOperator.Equals(NodeOperator.Leaf) ? 
                        RuleToExpression<T>(expression.Rule, genericType) :
                        RuleExpressionToOneExpression<T>(expression, genericType));
                });
                return finalExpression;
            }
            //check if node is NodeOperator.Or
            else
            {
                finalExpression = Expression.Constant(false);
                ruleExpression.Expressions.ForEach(expression =>
                {
                    finalExpression = Expression.Or(finalExpression, expression.NodeOperator.Equals(NodeOperator.Leaf) ?
                        RuleToExpression<T>(expression.Rule, genericType) :
                        RuleExpressionToOneExpression<T>(expression, genericType));
                });
                return finalExpression;
    
            }      
        }      
    
        public static BinaryExpression RuleToExpression<T>(Rule rule, ParameterExpression genericType)
        {
            try
            {
                Expression value = null;
                //Get Comparison property
                var key = Expression.Property(genericType, rule.ComparisonPredicate);
                Type propertyType = typeof(T).GetProperty(rule.ComparisonPredicate).PropertyType;
                //convert case is it DateTimeOffset property
                if (propertyType == typeof(DateTimeOffset))
                {
                    var converter = TypeDescriptor.GetConverter(propertyType);
                    value = Expression.Constant((DateTimeOffset)converter.ConvertFromString(rule.ComparisonValue));
                }
                else
                {
                    value = Expression.Constant(Convert.ChangeType(rule.ComparisonValue, propertyType));
                }
                BinaryExpression binaryExpression = Expression.MakeBinary(rule.ComparisonOperator, key, value);
                return binaryExpression;
            }
            catch (FormatException)
            {
                throw new Exception("Exception in RuleToExpression trying to convert rule Comparison Value");
            }
            catch (Exception e)
            {
                throw new Exception(e.Message);
            }
    
        }
    
    0 讨论(0)
  • 2020-11-28 00:25

    Although the most obvious way to answer the "How to implement a rule engine? (in C#)" question is to execute a given set of rules in sequence, this is in general considered as a naïve implementation (does not mean it does not work :-)

    It seems it's "good enough" in your case because your problem seems more to be "how to run a set of rules in sequence", and the lambda/expression tree (Martin's answer) is certainly the most elegant way in that matter if you are equiped with recent C# versions.

    However for more advanced scenarios, here is a link to the Rete Algorithm that is in fact implemented in many commercial rule engine systems, and another link to NRuler, an implementation of that algorithm in C#.

    0 讨论(0)
  • 2020-11-28 00:29

    What about a data type orientated approach with an extention method:

    public static class RoleExtension
    {
        public static bool Match(this Role role, object obj )
        {
            var property = obj.GetType().GetProperty(role.objectProperty);
            if (property.PropertyType == typeof(int))
            {
                return ApplyIntOperation(role, (int)property.GetValue(obj, null));
            }
            if (property.PropertyType == typeof(string))
            {
                return ApplyStringOperation(role, (string)property.GetValue(obj, null));
            }
            if (property.PropertyType.GetInterface("IEnumerable<string>",false) != null)
            {
                return ApplyListOperation(role, (IEnumerable<string>)property.GetValue(obj, null));
            }
            throw new InvalidOperationException("Unknown PropertyType");
        }
    
        private static bool ApplyIntOperation(Role role, int value)
        {
            var targetValue = Convert.ToInt32(role.TargetValue);
            switch (role.ComparisonOperator)
            {
                case "greater_than":
                    return value > targetValue;
                case "equal":
                    return value == targetValue;
                //...
                default:
                    throw new InvalidOperationException("Unknown ComparisonOperator");
            }
        }
    
        private static bool ApplyStringOperation(Role role, string value)
        {
            //...
            throw new InvalidOperationException("Unknown ComparisonOperator");
        }
    
        private static bool ApplyListOperation(Role role, IEnumerable<string> value)
        {
            var targetValues = role.TargetValue.Split(' ');
            switch (role.ComparisonOperator)
            {
                case "hasAtLeastOne":
                    return value.Any(v => targetValues.Contains(v));
                    //...
            }
            throw new InvalidOperationException("Unknown ComparisonOperator");
        }
    }
    

    Than you can evaulate like this:

    var myResults = users.Where(u => roles.All(r => r.Match(u)));
    
    0 讨论(0)
  • 2020-11-28 00:33

    Martin's answer was quite good. I actually made a rules engine that has the same idea as his. And I was surprised that it's almost the same. I've included some of his code to somewhat improve it. Although I've made it to handle more complex rules.

    You can look at Yare.NET

    Or download it in Nuget

    0 讨论(0)
  • 2020-11-28 00:36

    Reflection is your most versatile answer. You have three columns of data, and they need to be treated in different ways:

    1. Your field name. Reflection is the way to get the value from a coded field name.

    2. Your comparison operator. There should be a limited number of these, so a case statement should handle them most easily. Especially as some of them ( has one or more of ) is slightly more complex.

    3. Your comparison value. If these are all straight values then this is easy, although you will have divide the multiple entries up. However, you could also use reflection if they are field names too.

    I would take an approach more like:

        var value = user.GetType().GetProperty("age").GetValue(user, null);
        //Thank you Rick! Saves me remembering it;
        switch(rule.ComparisonOperator)
            case "equals":
                 return EqualComparison(value, rule.CompareTo)
            case "is_one_or_more_of"
                 return IsInComparison(value, rule.CompareTo)
    

    etc. etc.

    It gives you flexibility for adding more options for comparison. It also means that you can code within the Comparison methods any type validation that you might want, and make them as complex as you want. There is also the option here for the CompareTo to be evaluated as a recursive call back to another line, or as a field value, which could be done like:

                 return IsInComparison(value, EvaluateComparison(rule.CompareTo))
    

    It all depends on the possibilities for the future....

    0 讨论(0)
  • 2020-11-28 00:40

    If you only have a handful of properties and operators, the path of least of resistance is to just code up all the checks as special cases like this:

    public bool ApplyRules(List<Rule> rules, User user)
    {
        foreach (var rule in rules)
        {
            IComparable value = null;
            object limit = null;
            if (rule.objectProperty == "age")
            {
                value = user.age;
                limit = Convert.ToInt32(rule.TargetValue);
            }
            else if (rule.objectProperty == "username")
            {
                value = user.username;
                limit = rule.TargetValue;
            }
            else
                throw new InvalidOperationException("invalid property");
    
            int result = value.CompareTo(limit);
    
            if (rule.ComparisonOperator == "equal")
            {
                if (!(result == 0)) return false;
            }
            else if (rule.ComparisonOperator == "greater_than")
            {
                if (!(result > 0)) return false;
            }
            else
                throw new InvalidOperationException("invalid operator");
        }
        return true;
    }
    

    If you have a lot of properties, you may find a table-driven approach more palatable. In that case you would create a static Dictionary that maps property names to delegates matching, say, Func<User, object>.

    If you don't know the names of the properties at compile time, or you want to avoid special-cases for each property and don't want to use the table approach, you can use reflection to get properties. For example:

    var value = user.GetType().GetProperty("age").GetValue(user, null);
    

    But since TargetValue is probably a string, you'll need to take care to do type conversion from the rules table if necessary.

    0 讨论(0)
提交回复
热议问题