次を格納する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")
「oper」や「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", "20"),
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 == 15'
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に置き換えると、あらゆるタイプのオブジェクトに対して generic Rule compiler を使用できます。また、コードは不明なオペレーター名などのエラーを処理する必要があります。
Reflection.Emitを使用して、Expression Tree APIが導入される前でも、コードをその場で生成できることに注意してください。メソッドLambdaExpression.Compile()は、内部でReflection.Emitを使用します(これは ILSpy を使用して確認できます)。
そのままコンパイルしてジョブを実行するコードを次に示します。基本的に2つの辞書を使用します。1つは演算子名からブール関数へのマッピングを含み、もう1つはUser型のプロパティ名からプロパティゲッター(パブリックの場合)を呼び出すために使用されるPropertyInfosへのマップを含みます。 Userインスタンスと、テーブルの3つの値を静的なApplyメソッドに渡します。
class User
{
public int Age { get; set; }
public string UserName { get; set; }
}
class Operator
{
private static Dictionary<string, Func<object, object, bool>> s_operators;
private static Dictionary<string, PropertyInfo> s_properties;
static Operator()
{
s_operators = new Dictionary<string, Func<object, object, bool>>();
s_operators["greater_than"] = new Func<object, object, bool>(s_opGreaterThan);
s_operators["equal"] = new Func<object, object, bool>(s_opEqual);
s_properties = typeof(User).GetProperties().ToDictionary(propInfo => propInfo.Name);
}
public static bool Apply(User user, string op, string prop, object target)
{
return s_operators[op](GetPropValue(user, prop), target);
}
private static object GetPropValue(User user, string prop)
{
PropertyInfo propInfo = s_properties[prop];
return propInfo.GetGetMethod(false).Invoke(user, null);
}
#region Operators
static bool s_opGreaterThan(object o1, object o2)
{
if (o1 == null || o2 == null || o1.GetType() != o2.GetType() || !(o1 is IComparable))
return false;
return (o1 as IComparable).CompareTo(o2) > 0;
}
static bool s_opEqual(object o1, object o2)
{
return o1 == o2;
}
//etc.
#endregion
public static void Main(string[] args)
{
User user = new User() { Age = 16, UserName = "John" };
Console.WriteLine(Operator.Apply(user, "greater_than", "Age", 15));
Console.WriteLine(Operator.Apply(user, "greater_than", "Age", 17));
Console.WriteLine(Operator.Apply(user, "equal", "UserName", "John"));
Console.WriteLine(Operator.Apply(user, "equal", "UserName", "Bob"));
}
}
質問で説明したものとは異なるアプローチをとるルールエンジンを構築しましたが、現在のアプローチよりもはるかに柔軟であることがわかると思います。
現在のアプローチは単一のエンティティ「ユーザー」に焦点を当てているようで、永続的なルールは「プロパティ名」、「演算子」、「値」を識別します。私のパターンは、代わりに、述語(Func <T、bool>)のC#コードをデータベースの「Expression」列に保存します。現在の設計では、コード生成を使用して、データベースから「ルール」を照会し、それぞれ「テスト」メソッドを持つ「ルール」タイプでアセンブリをコンパイルしています。各ルールを実装するインターフェイスのシグネチャは次のとおりです。
public interface IDataRule<TEntity>
{
/// <summary>
/// Evaluates the validity of a rule given an instance of an entity
/// </summary>
/// <param name="entity">Entity to evaluate</param>
/// <returns>result of the evaluation</returns>
bool Test(TEntity entity);
/// <summary>
/// The unique indentifier for a rule.
/// </summary>
int RuleId { get; set; }
/// <summary>
/// Common name of the rule, not unique
/// </summary>
string RuleName { get; set; }
/// <summary>
/// Indicates the message used to notify the user if the rule fails
/// </summary>
string ValidationMessage { get; set; }
/// <summary>
/// indicator of whether the rule is enabled or not
/// </summary>
bool IsEnabled { get; set; }
/// <summary>
/// Represents the order in which a rule should be executed relative to other rules
/// </summary>
int SortOrder { get; set; }
}
「式」は、アプリケーションが最初に実行されるときに「テスト」メソッドの本体としてコンパイルされます。ご覧のとおり、テーブルの他の列もルールのファーストクラスプロパティとして表示されているため、開発者は、ユーザーが失敗または成功を通知される方法を柔軟に作成できます。
インメモリアセンブリの生成は、アプリケーション中に1回だけ発生し、ルールを評価するときにリフレクションを使用する必要がないため、パフォーマンスが向上します。プロパティ名のスペルが間違っている場合など、アセンブリは正しく生成されないため、実行時に式がチェックされます。
インメモリアセンブリを作成するメカニズムは次のとおりです。
ほとんどの場合、このコードはコンストラクターでのプロパティの実装と値の初期化であるため、これは実際には非常に簡単です。それに加えて、唯一の他のコードは式です。
注:CodeDOMの制限により、式は.NET 2.0(ラムダまたは他のC#3.0機能なし)でなければならないという制限があります。
そのためのサンプルコードを次に示します。
sb.AppendLine(string.Format("\tpublic class {0} : SomeCompany.ComponentModel.IDataRule<{1}>", className, typeName));
sb.AppendLine("\t{");
sb.AppendLine("\t\tprivate int _ruleId = -1;");
sb.AppendLine("\t\tprivate string _ruleName = \"\";");
sb.AppendLine("\t\tprivate string _ruleType = \"\";");
sb.AppendLine("\t\tprivate string _validationMessage = \"\";");
/// ...
sb.AppendLine("\t\tprivate bool _isenabled= false;");
// constructor
sb.AppendLine(string.Format("\t\tpublic {0}()", className));
sb.AppendLine("\t\t{");
sb.AppendLine(string.Format("\t\t\tRuleId = {0};", ruleId));
sb.AppendLine(string.Format("\t\t\tRuleName = \"{0}\";", ruleName.TrimEnd()));
sb.AppendLine(string.Format("\t\t\tRuleType = \"{0}\";", ruleType.TrimEnd()));
sb.AppendLine(string.Format("\t\t\tValidationMessage = \"{0}\";", validationMessage.TrimEnd()));
// ...
sb.AppendLine(string.Format("\t\t\tSortOrder = {0};", sortOrder));
sb.AppendLine("\t\t}");
// properties
sb.AppendLine("\t\tpublic int RuleId { get { return _ruleId; } set { _ruleId = value; } }");
sb.AppendLine("\t\tpublic string RuleName { get { return _ruleName; } set { _ruleName = value; } }");
sb.AppendLine("\t\tpublic string RuleType { get { return _ruleType; } set { _ruleType = value; } }");
/// ... more properties -- omitted
sb.AppendLine(string.Format("\t\tpublic bool Test({0} entity) ", typeName));
sb.AppendLine("\t\t{");
// #############################################################
// NOTE: This is where the expression from the DB Column becomes
// the body of the Test Method, such as: return "entity.Prop1 < 5"
// #############################################################
sb.AppendLine(string.Format("\t\t\treturn {0};", expressionText.TrimEnd()));
sb.AppendLine("\t\t}"); // close method
sb.AppendLine("\t}"); // close Class
さらに、ICollection>を実装する「DataRuleCollection」というクラスを作成しました。これにより、名前で特定のルールを実行するための「TestAll」機能とインデクサーを作成できました。これら2つのメソッドの実装を次に示します。
/// <summary>
/// Indexer which enables accessing rules in the collection by name
/// </summary>
/// <param name="ruleName">a rule name</param>
/// <returns>an instance of a data rule or null if the rule was not found.</returns>
public IDataRule<TEntity, bool> this[string ruleName]
{
get { return Contains(ruleName) ? list[ruleName] : null; }
}
// in this case the implementation of the Rules Collection is:
// DataRulesCollection<IDataRule<User>> and that generic flows through to the rule.
// there are also some supporting concepts here not otherwise outlined, such as a "FailedRules" IList
public bool TestAllRules(User target)
{
rules.FailedRules.Clear();
var result = true;
foreach (var rule in rules.Where(x => x.IsEnabled))
{
result = rule.Test(target);
if (!result)
{
rules.FailedRules.Add(rule);
}
}
return (rules.FailedRules.Count == 0);
}
コードの追加:コード生成に関連するコードのリクエストがありました。以下に含めた「RulesAssemblyGenerator」というクラスに機能をカプセル化しました。
namespace Xxx.Services.Utils
{
public static class RulesAssemblyGenerator
{
static List<string> EntityTypesLoaded = new List<string>();
public static void Execute(string typeName, string scriptCode)
{
if (EntityTypesLoaded.Contains(typeName)) { return; }
// only allow the Assembly to load once per entityType per execution session
Compile(new CSharpCodeProvider(), scriptCode);
EntityTypesLoaded.Add(typeName);
}
private static void Compile(CodeDom.CodeDomProvider provider, string source)
{
var param = new CodeDom.CompilerParameters()
{
GenerateExecutable = false,
IncludeDebugInformation = false,
GenerateInMemory = true
};
var path = System.Reflection.Assembly.GetExecutingAssembly().Location;
var root_Dir = System.IO.Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "Bin");
param.ReferencedAssemblies.Add(path);
// Note: This dependencies list are included as Assembly reference and they should list out all dependencies
// That you may reference in your Rules or that your entity depends on.
// some Assembly names were changed... clearly.
var dependencies = new string[] { "yyyyyy.dll", "xxxxxx.dll", "NHibernate.dll", "ABC.Helper.Rules.dll" };
foreach (var dependency in dependencies)
{
var assemblypath = System.IO.Path.Combine(root_Dir, dependency);
param.ReferencedAssemblies.Add(assemblypath);
}
// reference .NET basics for C# 2.0 and C#3.0
param.ReferencedAssemblies.Add(@"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.dll");
param.ReferencedAssemblies.Add(@"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.5\System.Core.dll");
var compileResults = provider.CompileAssemblyFromSource(param, source);
var output = compileResults.Output;
if (compileResults.Errors.Count != 0)
{
CodeDom.CompilerErrorCollection es = compileResults.Errors;
var edList = new List<DataRuleLoadExceptionDetails>();
foreach (CodeDom.CompilerError s in es)
edList.Add(new DataRuleLoadExceptionDetails() { Message = s.ErrorText, LineNumber = s.Line });
var rde = new RuleDefinitionException(source, edList.ToArray());
throw rde;
}
}
}
}
otherの質問、コメント、またはその他のコードサンプルのリクエストがある場合は、お知らせください。
反射はあなたの最も多目的な答えです。データの列が3つあり、それらを異なる方法で処理する必要があります。
あなたのフィールド名。リフレクションは、コード化されたフィールド名から値を取得する方法です。
比較演算子。これらの数は限られているはずなので、caseステートメントでそれらを最も簡単に処理する必要があります。特に、それらの一部(が1つ以上ある)は少し複雑です。
比較値。これらがすべて直線の値である場合、これは簡単ですが、複数のエントリを分割することになります。ただし、フィールド名でもある場合はリフレクションを使用することもできます。
私はもっと次のようなアプローチを取るでしょう:
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)
などなど.
比較のためにオプションを追加する柔軟性を提供します。また、比較メソッド内で必要な型検証をコーディングし、必要に応じて複雑にすることができることも意味します。 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, object>
に一致するデリゲートにマッピングします。
コンパイル時にプロパティの名前がわからない場合、または各プロパティの特殊なケースを避けてテーブルアプローチを使用したくない場合は、リフレクションを使用してプロパティを取得できます。例えば:
var value = user.GetType().GetProperty("age").GetValue(user, null);
ただし、TargetValue
はおそらくstring
であるため、必要に応じてルールテーブルから型変換を行うように注意する必要があります。
拡張メソッドを使用したデータ型指向のアプローチはどうですか:
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)));
「ルールエンジンを実装する方法(C#で)」の質問に答える最も明白な方法は、特定のルールセットを順番に実行することですが、これは一般に単純な実装と見なされます(機能しないという意味ではありません) :-)
あなたの問題は「一連のルールを順番に実行する方法」であると思われるため、あなたのケースでは「十分」であり、ラムダ/式ツリー(マーティンの答え)は確かに最もエレガントな方法です最新のC#バージョンが装備されています。
しかし、より高度なシナリオの場合、実際には多くの商用ルールエンジンシステムで実装されている Rete Algorithm へのリンクと、その実装である NRuler への別のリンクがありますC#のアルゴリズム。
ワークフロールールエンジンを使用してはどうですか?
ワークフローなしでWindowsワークフロールールを実行できます。GuyBursteinのブログを参照してください。 http://blogs.Microsoft.co.il/blogs/bursteg/archive/2006/10/11/RuleExecutionWithoutWorkflow.aspx
プログラムでルールを作成するには、Stephen KaufmanのWebLogをご覧ください
ルールの実装を追加、および/または、ルールの間に、ルールのないリーフまたはできるルールのツリーのルートを表すクラス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を1つの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);
}
}
私はMartin Konicekの答えで大文字と小文字を区別する問題がありますので、rule.MemberName
を大文字と小文字を区別しないようにするには
var tProp = typeof(User).GetProperty(r.MemberName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance).PropertyType;