Este fragmento compila las Reglas en un código ejecutable rápido (usando árboles de Expresión ) y no necesita ninguna declaración de cambio complicada:
(Editar: 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();
}
Entonces 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 usé 'GreaterThan' en lugar de 'Greater_Than', etc. Esto se debe a que 'GreaterThan' es el nombre .NET para el operador, por lo tanto, no necesitamos ninguna asignación adicional.
Si necesita nombres personalizados, puede crear 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" }
};
El código usa el tipo Usuario por simplicidad. Puede reemplazar el usuario con un tipo genérico T para tener un compilador de reglas genérico para cualquier tipo de objeto. Además, el código debe manejar errores, como el nombre del operador desconocido.
Tenga en cuenta que era posible generar código sobre la marcha incluso antes de que se introdujera la API de árboles de expresión, utilizando Reflection.Emit. El método LambdaExpression.Compile () usa Reflection.Emit debajo de las cubiertas (puede ver esto usando ILSpy ).
Aquí hay un código que se compila tal cual y hace el trabajo. Básicamente, use dos diccionarios, uno que contenga un mapeo de nombres de operadores a funciones booleanas, y otro que contenga un mapeo de los nombres de propiedad del tipo de Usuario a PropertyInfos usado para invocar el captador de propiedades (si es público). Pasa la instancia de usuario y los tres valores de su tabla al método de aplicación estático.
fuente
Creé un motor de reglas que adopta un enfoque diferente al que usted describió en su pregunta, pero creo que encontrará que es mucho más flexible que su enfoque actual.
Su enfoque actual parece centrarse 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 ensamblaje con los tipos de "Regla", cada uno con un método de "Prueba". Aquí está la firma de la interfaz que implementa cada regla:
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 para que un desarrollador tenga flexibilidad para crear una experiencia sobre cómo se notifica al usuario sobre el fracaso o el éxito.
La generación de 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 ensamblado 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:
Esto es realmente 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.
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.
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.
Si hay otras preguntas o comentarios o solicitudes de más ejemplos de código, avíseme.
fuente
La reflexión es tu respuesta más versátil. Tiene tres columnas de datos y deben tratarse de diferentes maneras:
Tu nombre de campo. La reflexión es la forma de obtener el valor de un nombre de campo codificado.
Tu operador de comparación. Debería haber un número limitado de estos, por lo que una declaración de caso debería manejarlos más fácilmente. Especialmente porque algunos de ellos (tiene uno o más de) son ligeramente más complejos.
Tu valor de comparación. Si todos estos son valores rectos, entonces esto es fácil, aunque habrá dividido las entradas múltiples. Sin embargo, también podría usar la reflexión si también son nombres de campo.
Tomaría un enfoque más como:
etcétera etcétera.
Le brinda flexibilidad para agregar más opciones de comparación. También significa que puede codificar dentro de 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:
Todo depende de las posibilidades para el futuro ...
fuente
Si solo tiene un puñado de propiedades y operadores, la ruta de menor resistencia es codificar todas las comprobaciones como casos especiales como este:
Si tiene muchas propiedades, puede encontrar un enfoque basado en tablas más apetecible. En ese caso, se crearía una estática
Dictionary
que asigna nombres de propiedad a los delegados a juego, por ejemplo,Func<User, object>
.Si no conoce los nombres de las propiedades en el momento de la compilación, o desea evitar casos especiales para cada propiedad y no desea utilizar el enfoque de tabla, puede usar la reflexión para obtener propiedades. Por ejemplo:
Pero dado
TargetValue
que probablemente sea unstring
, deberá realizar la conversión de tipo de la tabla de reglas si es necesario.fuente
IComparable
se usa para comparar cosas. Aquí están los documentos: Método IComparable.CompareTo .¿Qué pasa con un enfoque orientado al tipo de datos con un método de extensión?
De lo que puedes evacuar así:
fuente
Aunque la forma más obvia de responder a la pregunta "¿Cómo implementar un motor de reglas? (En C #)" es ejecutar un conjunto dado de reglas en secuencia, esto generalmente se considera 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 bien "cómo ejecutar un conjunto de reglas en secuencia", y el árbol lambda / expresión (la respuesta de Martin) es sin duda la forma más elegante en ese caso si usted 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 motores de reglas comerciales, y otro enlace a NRuler , una implementación de ese algoritmo en C #.
fuente
La respuesta de Martin fue bastante buena. Realmente hice un motor de reglas que tiene la misma idea que la suya. Y me sorprendió que sea casi lo mismo. He incluido parte de su código para mejorarlo un poco. Aunque lo hice para manejar reglas más complejas.
Puedes mirar Yare.NET
O descárgalo en Nuget
fuente
¿Qué tal usar el motor de reglas de flujo de trabajo?
Puede ejecutar las reglas de flujo de trabajo de Windows sin flujo de trabajo, consulte el blog de Guy Burstein: http://blogs.microsoft.co.il/blogs/bursteg/archive/2006/10/11/RuleExecutionWithoutWorkflow.aspx
y para crear programáticamente sus reglas, vea el Weblog de Stephen Kaufman
http://blogs.msdn.com/b/skaufman/archive/2006/05/15/programmatic-create-windows-workflow-rules.aspx
fuente
Agregué implementación para y, o entre reglas, agregué la clase RuleExpression que representa la raíz de un árbol que puede ser hoja, es una regla simple o puede ser y, o expresiones binarias allí porque no tienen regla y tienen expresiones:
Tengo otra clase que compila la regla Expresión a una
Func<T, bool>:
fuente