Bla*_*man 197 c# rule-engine dynamic
我有一个存储以下内容的db表:
RuleID objectProperty ComparisonOperator TargetValue
1 age 'greater_than' 15
2 username 'equal' 'some_name'
3 tags 'hasAtLeastOne' 'some_tag some_tag2'
Run Code Online (Sandbox Code Playgroud)
现在说我有这些规则的集合:
List<Rule> rules = db.GetRules();
Run Code Online (Sandbox Code Playgroud)
现在我还有一个用户的实例:
User user = db.GetUser(....);
Run Code Online (Sandbox Code Playgroud)
我将如何循环这些规则,并应用逻辑并执行比较等?
if(user.age > 15)
if(user.username == "some_name")
Run Code Online (Sandbox Code Playgroud)
由于对象的属性如'age'或'user_name'存储在表中,以及比较操作符'great_than'和'equal',我怎么可能这样做?
C#是一种静态类型语言,因此不确定如何继续前进.
Mar*_*cek 378
此代码段将规则编译为快速可执行代码(使用表达式树),并且不需要任何复杂的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();
}
Run Code Online (Sandbox Code Playgroud)
然后你可以写:
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));
}
Run Code Online (Sandbox Code Playgroud)
以下是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);
}
}
Run Code Online (Sandbox Code Playgroud)
请注意,我使用'GreaterThan'而不是'greater_than'等 - 这是因为'GreaterThan'是运算符的.NET名称,因此我们不需要任何额外的映射.
如果你真的需要自定义名称,你可以构建一个非常简单的字典,并在编译规则之前翻译所有运算符:
var nameMap = new Dictionary<string, string> {
{ "greater_than", "GreaterThan" },
{ "hasAtLeastOne", "Contains" }
};
Run Code Online (Sandbox Code Playgroud)
请注意,为简单起见,代码使用User类型.您可以使用泛型类型T替换User,以便为任何类型的对象创建通用规则编译器.
另请注意:即使在使用Reflection.Emit引入Expression树API之前,也可以动态生成代码.方法LambdaExpression.Compile()在封面下使用Reflection.Emit(您可以使用ILSpy查看).
Pet*_*nov 13
这是一些按原样编译并完成工作的代码.基本上使用两个字典,一个包含从运算符名称到布尔函数的映射,另一个包含从User类型的属性名称到PropertyInfos的映射,用于调用属性getter(如果是公共的).您将User实例以及表中的三个值传递给静态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"));
}
}
Run Code Online (Sandbox Code Playgroud)
我建了一个规则引擎,花费的时间比你在你的问题概括了不同的方法,但我想你会发现它比你目前的做法更加灵活.
您当前的做法似乎集中在一个单一的实体,"用户",而你的执着规则确定"PROPERTYNAME","经营者"和"价值".我的图案,而不是在我的数据库中的"表达"列存储的谓词(Func键<T,布尔>)C#代码.在当前的设计中,使用代码生成我从数据库查询"规则"并使用"规则"类型编译程序集,每个类型都有一个"测试"方法.以下是每个规则实现的接口的签名:
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; }
}
Run Code Online (Sandbox Code Playgroud)
当应用程序首次执行时,"Expression"被编译为"Test"方法的主体.如您所见,表中的其他列也表现为规则的第一类属性,以便开发人员可以灵活地创建用户如何获知失败或成功通知的体验.
生成的内存组件是你的应用程序中有1时发生,您不必评估规则时需要使用反射获得的性能增益.在运行时检查表达式,因为如果属性名称拼写错误,程序集将无法正确生成,等等.
创建内存中程序集的机制如下:
这实际上非常简单,因为对于大多数这个代码是构造函数中的属性实现和值初始化.除此之外,唯一的其他代码是Expression.
注意:有您的表达式必须是.NET 2.0(没有lambda表达式或其他C#3.0特征)由于在CodeDOM的限制的限制.
这是一些示例代码.
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
Run Code Online (Sandbox Code Playgroud)
除此之外,我确实创建了一个名为"DataRuleCollection"的类,它实现了ICollection>.这使我能够创建一个"TestAll"功能和一个索引器,用于按名称执行特定规则.以下是这两种方法的实现.
/// <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);
}
Run Code Online (Sandbox Code Playgroud)
更多代码:有人要求提供与代码生成相关的代码.我将功能封装在一个名为"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;
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
如果对其他代码示例有任何其他问题或意见或要求,请与我们联系.
反思是你最全面的答案.您有三列数据,需要以不同方式处理它们:
你的字段名称.反射是从编码字段名称获取值的方法.
您的比较运算符.这些应该是有限的,因此案例陈述应该最容易处理它们.特别是其中一些(有一个或多个)稍微复杂一些.
你的比较价值.如果这些都是直接值,那么这很容易,尽管你会将多个条目分开.但是,如果它们也是字段名称,您也可以使用反射.
我会采取更像的方法:
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)
Run Code Online (Sandbox Code Playgroud)
等等
它为您提供了添加更多选项以进行比较的灵活性.它还意味着您可以在比较方法中编码您可能需要的任何类型验证,并使它们尽可能复杂.此处还有一个选项可以将CompareTo评估为递归回调到另一行,或者作为字段值,可以这样做:
return IsInComparison(value, EvaluateComparison(rule.CompareTo))
Run Code Online (Sandbox Code Playgroud)
这一切都取决于未来的可能性......
如果您只有少数属性和运算符,那么阻力最小的路径就是将所有检查编码为特殊情况,如下所示:
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;
}
Run Code Online (Sandbox Code Playgroud)
如果你有很多属性,你可能会发现桌面驱动的方法更加可口.在这种情况下,您将创建一个静态Dictionary
,将属性名称映射到匹配的委托,比如说Func<User, object>
.
如果在编译时不知道属性的名称,或者您希望避免每个属性的特殊情况并且不想使用表方法,则可以使用反射来获取属性.例如:
var value = user.GetType().GetProperty("age").GetValue(user, null);
Run Code Online (Sandbox Code Playgroud)
但由于TargetValue
可能是一个string
,如果需要,您需要注意从规则表中进行类型转换.
小智 6
使用扩展方法的数据类型导向方法如何:
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");
}
}
Run Code Online (Sandbox Code Playgroud)
你可以像这样评价:
var myResults = users.Where(u => roles.All(r => r.Match(u)));
Run Code Online (Sandbox Code Playgroud)