小编典典

如何实现规则引擎?

all

我有一个存储以下内容的数据库表:

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

由于对象的“年龄”或“用户名”等属性与比较运算符“great_than”和“等于”一起存储在表中,我怎么可能做到这一点?

C# 是一种静态类型的语言,所以不知道如何前进。


阅读 70

收藏
2022-06-21

共1个答案

小编典典

这个片段 将规则编译成快速的可执行代码 (使用表达式树)并且不需要任何复杂的 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 类型。您可以将 User 替换为泛型类型 T 以拥有适用于任何类型对象的泛型 Rule
编译器。
此外,代码应该处理错误,例如未知的操作员名称。

请注意,甚至在引入表达式树 API 之前,使用 Reflection.Emit 就可以动态生成代码。LambdaExpression.Compile()
方法在幕后使用
Reflection.Emit(您可以使用ILSpy看到这一点)。

2022-06-21