reglas negocio elaborar ejemplos como c# dynamic rule-engine

c# - ejemplos - como elaborar reglas de negocio



¿Cómo implementar un motor de reglas? (11)

Tengo una tabla db que almacena lo siguiente:

RuleID objectProperty ComparisonOperator TargetValue 1 age ''greater_than'' 15 2 username ''equal'' ''some_name'' 3 tags ''hasAtLeastOne'' ''some_tag some_tag2''

Ahora di que tengo una colección de estas reglas:

List<Rule> rules = db.GetRules();

Ahora tengo una instancia de un usuario también:

User user = db.GetUser(....);

¿Cómo analizaría estas reglas y aplicaría la lógica y realizaría las comparaciones, etc.?

if(user.age > 15) if(user.username == "some_name")

Dado que la propiedad del objeto como ''edad'' o ''nombre_usuario'' está almacenada en la tabla, junto con el operador de comparación ''great_than'' e ''igual'', ¿cómo podría hacerlo?

C # es un lenguaje estáticamente tipado, por lo que no estoy seguro de cómo seguir adelante.



¿Qué pasa con un enfoque orientado al tipo de datos con un método de extensión?

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

De lo que puedes evacuar de esta manera:

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


Agregué la implementación para y, o entre las reglas, agregué la clase RuleExpression que representa la raíz de un árbol que se puede mostrar como una regla simple o puede ser y, o expresiones binarias porque no tienen regla y tienen expresiones:

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 }

Tengo otra clase que compila la reglaExpresión a un 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); } }


Aquí hay un código que compila como está y hace el trabajo. Básicamente, use dos diccionarios, uno que contenga una asignación de nombres de operador a funciones booleanas, y otro que contenga un mapa de los nombres de propiedad del tipo Usuario a PropertyInfos utilizado para invocar el getter de propiedad (si es público). Pasas la instancia de Usuario y los tres valores de tu tabla al método de Aplicación estático.

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


Aunque la forma más obvia de responder la pregunta "¿Cómo implementar un motor de reglas? (En C #)" es ejecutar un conjunto determinado de reglas en secuencia, esto se considera en general como una implementación ingenua (no significa que no funcione). :-)

Parece que es "lo suficientemente bueno" en su caso porque su problema parece ser más "cómo ejecutar un conjunto de reglas en secuencia", y el árbol lambda / expresión (respuesta de Martin) es sin duda la forma más elegante en ese asunto si están equipados con versiones recientes de C #.

Sin embargo, para escenarios más avanzados, aquí hay un enlace al algoritmo Rete que de hecho se implementa en muchos sistemas de motor de reglas comerciales, y otro enlace a NRuler , una implementación de ese algoritmo en C #.


Creé un motor de reglas que toma un enfoque diferente del que describió en su pregunta, pero creo que encontrará que es mucho más flexible que su enfoque actual.

Su enfoque actual parece estar enfocado en una sola entidad, "Usuario", y sus reglas persistentes identifican "nombre de propiedad", "operador" y "valor". Mi patrón, en cambio, almacena el código C # para un predicado (Func <T, bool>) en una columna "Expresión" en mi base de datos. En el diseño actual, usando la generación de código, estoy consultando las "reglas" de mi base de datos y compilando un ensamblado con tipos de "Regla", cada uno con un método de "Prueba". Aquí está la firma para la interfaz que se implementa cada regla:

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

La "Expresión" se compila como el cuerpo del método "Prueba" cuando la aplicación se ejecuta por primera vez. Como puede ver, las otras columnas de la tabla también aparecen como propiedades de primera clase en la regla, de modo que un desarrollador tiene flexibilidad para crear una experiencia de cómo el usuario recibe una notificación de falla o éxito.

Generar un ensamblaje en memoria es una ocurrencia de una vez durante su aplicación y obtiene una ganancia de rendimiento al no tener que usar la reflexión al evaluar sus reglas. Sus expresiones se verifican en tiempo de ejecución ya que el ensamblaje no se generará correctamente si el nombre de una propiedad está mal escrito, etc.

La mecánica de crear un ensamblaje en memoria es la siguiente:

  • Cargue sus reglas de la base de datos
  • iterar sobre las reglas y para cada una, usando un StringBuilder y algunas concatenaciones de cadenas escriba el texto que representa una clase que hereda de IDataRule
  • compilar usando CodeDOM - más información

Esto es bastante simple porque para la mayoría este código es implementaciones de propiedades e inicialización de valores en el constructor. Además de eso, el único otro código es la Expresión.
NOTA: existe una limitación de que su expresión debe ser .NET 2.0 (sin lambdas u otras características de C # 3.0) debido a una limitación en CodeDOM.

Aquí hay un código de muestra para eso.

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

Más allá de esto hice una clase que llamé "DataRuleCollection", que implementó ICollection>. Esto me permitió crear una capacidad "TestAll" y un indexador para ejecutar una regla específica por nombre. Aquí están las implementaciones para esos dos métodos.

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

MÁS CÓDIGO: Hubo una solicitud para el código relacionado con la generación de código. Encapsulé la funcionalidad en una clase llamada ''RulesAssemblyGenerator'' que he incluido a continuación.

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

Si hay alguna otra pregunta o comentario o solicitud de más ejemplos de código, házmelo saber.


Este fragmento compila las Reglas en un código ejecutable rápido (usando árboles de Expresión ) y no necesita ninguna instrucción de cambio complicada:

(Edición: ejemplo de trabajo completo con método genérico )

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

Luego puedes escribir:

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

Aquí está la implementación de 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); } }

Tenga en cuenta que utilicé ''GreaterThan'' en lugar de ''greater_than'', etc. Esto se debe a que ''GreaterThan'' es el nombre de .NET para el operador, por lo tanto, no necesitamos ningún mapeo adicional.

Si realmente necesita nombres personalizados, puede compilar un diccionario muy simple y simplemente traducir todos los operadores antes de compilar las reglas:

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

Tenga en cuenta que el código usa el tipo Usuario por simplicidad. Puede reemplazar al usuario con un tipo genérico T para tener un compilador de reglas genérico para cualquier tipo de objeto.

También tenga en cuenta que la generación de código sobre la marcha fue posible incluso antes de que se introdujera la API de Expression trees, utilizando Reflection.Emit. El método LambdaExpression.Compile () usa Reflection.Emit debajo de las cubiertas (puede ver esto usando ILSpy ).


La reflexión es tu respuesta más versátil. Tiene tres columnas de datos y deben tratarse de diferentes maneras:

  1. Tu nombre de campo La reflexión es la forma de obtener el valor de un nombre de campo codificado.

  2. Su operador de comparación. Debe haber un número limitado de estos, por lo que una declaración de caso debe manejarlos más fácilmente. Especialmente porque algunos de ellos (tiene uno o más de) es un poco más complejo.

  3. Su valor de comparación. Si estos son todos valores rectos, entonces esto es fácil, aunque habrá dividido las múltiples entradas. Sin embargo, también podría usar la reflexión si también son nombres de campo.

Yo tomaría un enfoque más como:

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)

etcétera etcétera.

Le da flexibilidad para agregar más opciones de comparación. También significa que puede codificar en los métodos de comparación cualquier validación de tipo que desee y hacerlos tan complejos como desee. También existe la opción aquí para que CompareTo se evalúe como una llamada recursiva a otra línea, o como un valor de campo, que podría hacerse como:

return IsInComparison(value, EvaluateComparison(rule.CompareTo))

Todo depende de las posibilidades para el futuro ...


La respuesta de Martin fue bastante buena. De hecho, hice un motor de reglas que tiene la misma idea que él. Y me sorprendió que sea casi lo mismo. He incluido parte de su código para mejorarlo de alguna manera. Aunque he llegado a manejar reglas más complejas.

Puedes mirar Yare.NET

O descárguelo en Nuget


Si solo tiene un puñado de propiedades y operadores, la ruta de menor resistencia consiste simplemente en codificar todas las comprobaciones como casos especiales como este:

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

Si tiene muchas propiedades, puede encontrar un método basado en tablas más aceptable. En ese caso, crearía un Dictionary estático que correlaciona los nombres de propiedad con delegados que coinciden, por ejemplo, Func<User, object> .

Si no conoce los nombres de las propiedades en tiempo de compilación, o si desea evitar casos especiales para cada propiedad y no desea utilizar el enfoque de tabla, puede usar reflection para obtener propiedades. Por ejemplo:

var value = user.GetType().GetProperty("age").GetValue(user, null);

Pero dado que TargetValue es probablemente una string , deberá tener cuidado de hacer la conversión de tipo de la tabla de reglas si es necesario.


Tengo un problema de mayúsculas y minúsculas en la respuesta de Martin Konicek, así que si quieres que la rule.MemberName no rule.MemberName entre mayúsculas y minúsculas solo agrega

var tProp = typeof(User).GetProperty(r.MemberName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance).PropertyType;