我有一个db表,存储如下:

RuleID  objectProperty ComparisonOperator  TargetValue
1       age            'greater_than'             15
2       username       'equal'             'some_name'
3       tags           'hasAtLeastOne'     'some_tag some_tag2'

现在假设我有这些规则的集合:

List<Rule> rules = db.GetRules();

现在我也有一个用户的实例:

User user = db.GetUser(....);

我将如何循环这些规则,并应用逻辑和执行比较等?

if(user.age > 15)

if(user.username == "some_name")

由于对象的属性,如'年龄'或'user_name'存储在表中,以及比较操作符'great_than'和'equal',我怎么可能做到这一点?

c#是一种静态类型语言,所以不知道该如何发展。


当前回答

如果你只有少量的属性和操作符,那么阻力最小的方法就是将所有检查都编码为如下的特殊情况:

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

如果您有很多属性,您可能会发现表驱动方法更合适。在这种情况下,您将创建一个静态Dictionary,将属性名称映射到匹配的委托,例如Func<User,对象>。

如果在编译时不知道属性的名称,或者希望避免每个属性的特殊情况,并且不想使用表方法,则可以使用反射来获取属性。例如:

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

但是由于TargetValue可能是一个字符串,如果有必要,您需要注意从规则表进行类型转换。

其他回答

反思是最通用的答案。你有三列数据,它们需要以不同的方式处理:

字段名。反射是从编码字段名中获取值的方法。 比较运算符。它们的数量应该是有限的,因此case语句应该最容易处理它们。特别是其中一些(有一个或多个)稍微复杂一些。 你的比较值。如果这些都是直值,那么这很容易,尽管您需要将多个条目分开。但是,如果它们也是字段名,也可以使用反射。

我会采取这样的方法:

    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)

等等。

它为您提供了添加更多比较选项的灵活性。这也意味着您可以在compare方法中编写您可能想要的任何类型验证,并使它们尽可能复杂。这里还有一个选项,让CompareTo作为另一行的递归调用来计算,或者作为一个字段值,可以这样做:

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

这一切都取决于未来的可能性....

这段代码将规则编译成快速可执行的代码(使用表达式树),不需要任何复杂的switch语句:

(编辑:具有通用方法的完整工作示例)

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

你可以这样写:

List<Rule> rules = new List<Rule> {
    new Rule ("Age", "GreaterThan", "21"),
    new Rule ( "Name", "Equal", "John"),
    new Rule ( "Tags", "Contains", "C#" )
};

// compile the rules once
var compiledRules = rules.Select(r => CompileRule(r)).ToList();

public bool MatchesAllRules(User user)
{
    return compiledRules.All(rule => rule(user));
}

下面是BuildExpr的实现:

Expression BuildExpr(Rule r, ParameterExpression param)
{
    var left = MemberExpression.Property(param, r.MemberName);
    var tProp = typeof(User).GetProperty(r.MemberName).PropertyType;
    ExpressionType tBinary;
    // is the operator a known .NET operator?
    if (ExpressionType.TryParse(r.Operator, out tBinary)) {
        var right = Expression.Constant(Convert.ChangeType(r.TargetValue, tProp));
        // use a binary operation, e.g. 'Equal' -> 'u.Age == 21'
        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);
    }
}

注意,我使用了'GreaterThan'而不是'greater_than'等等——这是因为'GreaterThan'是操作符的. net名称,因此我们不需要任何额外的映射。

如果你需要自定义名称,你可以创建一个非常简单的字典,在编译规则之前翻译所有操作符:

var nameMap = new Dictionary<string, string> {
    { "greater_than", "GreaterThan" },
    { "hasAtLeastOne", "Contains" }
};

为了简单起见,代码使用User类型。您可以用泛型类型T替换User,从而为任何类型的对象提供泛型规则编译器。此外,代码应该处理错误,比如未知的操作符名称。

请注意,即使在引入表达式树API之前,使用Reflection.Emit也可以动态生成代码。LambdaExpression.Compile()方法使用Reflection。在被子下发射(您可以使用ILSpy看到这一点)。

我已经创建了一个用dotnet编写的丰富而高性能的规则引擎包,查看此repo以获取更多信息。 一旦安装,你可以简单地使用它:

var engine = new RulesService<TestModel>(new RulesCompiler(), new LazyCache.Mocks.MockCachingService());
            
    var matchingRules = engine.GetMatchingRules(
        new TestModel { NumericField = 5 },
        new[] {
            new RulesConfig {
                Id = Guid.NewGuid(),
                RulesOperator = Rule.InterRuleOperatorType.And,
                RulesGroups = new RulesGroup[] {
                    new RulesGroup {
                        RulesOperator = Rule.InterRuleOperatorType.And,
                        Rules = new[] {
                            new Rule { 
                            ComparisonOperator = Rule.ComparisonOperatorType.Equal,
                            ComparisonValue = 5.ToString(),
                            ComparisonPredicate = nameof(TestModel.NumericField) 
                            }
                        }
                    }
                }
            }
        });

我为和或规则之间添加了实现 我添加了RuleExpression类,表示树的根,它可以是叶子,可以是简单规则,也可以是和,或者二进制表达式,因为它们没有规则和表达式:

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
}

我有另一个类编译ruleExpression到一个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);
        }

    }

如果你只有少量的属性和操作符,那么阻力最小的方法就是将所有检查都编码为如下的特殊情况:

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

如果您有很多属性,您可能会发现表驱动方法更合适。在这种情况下,您将创建一个静态Dictionary,将属性名称映射到匹配的委托,例如Func<User,对象>。

如果在编译时不知道属性的名称,或者希望避免每个属性的特殊情况,并且不想使用表方法,则可以使用反射来获取属性。例如:

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

但是由于TargetValue可能是一个字符串,如果有必要,您需要注意从规则表进行类型转换。