我有一个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#是一种静态类型语言,所以不知道该如何发展。


当前回答

马丁的回答很好。实际上,我做了一个规则引擎,与他的想法相同。我很惊讶,这几乎是一样的。我加入了他的一些代码,以在一定程度上改进它。尽管我已经让它处理更复杂的规则。

你可以看看Yare。网

或者在Nuget下载

其他回答

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

字段名。反射是从编码字段名中获取值的方法。 比较运算符。它们的数量应该是有限的,因此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))

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

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

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可能是一个字符串,如果有必要,您需要注意从规则表进行类型转换。

我想添加微软的规则引擎库,以供将来访问;

https://github.com/microsoft/RulesEngine

尽管回答“如何实现规则引擎?”(在c#中)的问题是按顺序执行给定的一组规则,这通常被认为是naïve实现(并不意味着它不起作用:-)

在你的情况下,它似乎“足够好”,因为你的问题似乎更多的是“如何按顺序运行一组规则”,如果你有最新的c#版本,lambda/expression树(Martin的答案)肯定是最优雅的方式。

然而,对于更高级的场景,这里有一个到Rete算法的链接,它实际上在许多商业规则引擎系统中实现,还有一个到NRuler的链接,它是该算法在c#中的实现。

那么使用扩展方法的面向数据类型的方法呢?

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

然后你可以这样评价:

var myResults = users.Where(u => roles.All(r => r.Match(u)));