1. ホーム
  2. c#

[解決済み】ルールエンジンの実装方法は?

2022-04-11 12:20:21

質問

私は以下を格納する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")

age' や 'user_name' といったオブジェクトのプロパティは、比較演算子 'great_than' や 'equal' と共にテーブルに格納されているので、どうすればこれを実行できるのでしょうか?

C#は静的型付け言語なので、どのように進めればよいかわからない。

どうすればいい?

このスニペット は、ルールを高速な実行可能コードにコンパイルします。 (を使用)。 表現ツリー ) であり、複雑な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);
    }
}

greater_than」等の代わりに「GreaterThan」を使用したことに注意してください。- これは、'GreaterThan'が演算子の.NET名であり、余分なマッピングが必要ないためです。

カスタム名が必要な場合は、非常にシンプルな辞書を作成して、ルールをコンパイルする前にすべての演算子を翻訳するだけでよいのです。

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

このコードでは、簡単のためにUser型を使用しています。User を一般的な型 T に置き換えて 汎用ルールコンパイラ は、どのような種類のオブジェクトにも対応します。また、不明な演算子名などのエラーも処理する必要があります。

なお、オンザフライでのコード生成は、Expression trees APIが導入される以前からReflection.Emitを使用して可能でした。LambdaExpression.Compile()というメソッドは、Reflection.Emitを隠れて使っています(これを見るには ILSpy ).