Esse trecho compila as regras em código executável rápido (usando árvores de expressão ) e não precisa de nenhuma instrução de chave complicada:
(Editar: exemplo de trabalho completo com 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();
}
Você pode então escrever:
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));
}
Aqui está a implementação do 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);
}
}
Observe que eu usei 'GreaterThan' em vez de 'Greater_than' etc. etc. - isso ocorre porque 'GreaterThan' é o nome do .NET para o operador, portanto, não precisamos de nenhum mapeamento extra.
Se você precisar de nomes personalizados, crie um dicionário muito simples e traduza todos os operadores antes de compilar as regras:
var nameMap = new Dictionary<string, string> {
{ "greater_than", "GreaterThan" },
{ "hasAtLeastOne", "Contains" }
};
O código usa o tipo Usuário para simplificar. Você pode substituir Usuário por um tipo genérico T para ter um compilador de Regras genérico para qualquer tipo de objeto. Além disso, o código deve manipular erros, como nome de operador desconhecido.
Observe que a geração rápida de código era possível mesmo antes da API de árvores de expressão ser introduzida, usando Reflection.Emit. O método LambdaExpression.Compile () usa Reflection.Emit nos bastidores (você pode ver isso usando ILSpy ).
Aqui está um código que compila como está e faz o trabalho. Basicamente, use dois dicionários, um contendo um mapeamento de nomes de operadores para funções booleanas e outro contendo um mapa dos nomes de propriedades do tipo Usuário para PropertyInfos usado para chamar o getter de propriedade (se público). Você passa a instância do usuário e os três valores da sua tabela para o método Apply estático.
fonte
Criei um mecanismo de regras que adota uma abordagem diferente da descrita na sua pergunta, mas acho que você achará muito mais flexível do que sua abordagem atual.
Sua abordagem atual parece estar focada em uma única entidade, "Usuário", e suas regras persistentes identificam "nome da propriedade", "operador" e "valor". Meu padrão, em vez disso, armazena o código C # para um predicado (Func <T, bool>) em uma coluna "Expressão" no meu banco de dados. No design atual, usando a geração de código, estou consultando as "regras" do meu banco de dados e compilando um assembly com os tipos "Rule", cada um com o método "Test". Aqui está a assinatura da interface implementada em cada regra:
A "Expressão" é compilada como o corpo do método "Teste" quando o aplicativo é executado pela primeira vez. Como você pode ver, as outras colunas da tabela também são exibidas como propriedades de primeira classe na regra, para que um desenvolvedor tenha flexibilidade para criar uma experiência de como o usuário é notificado sobre falha ou sucesso.
A geração de um assembly na memória é uma ocorrência única durante o aplicativo e você obtém um ganho de desempenho por não precisar usar a reflexão ao avaliar suas regras. Suas expressões são verificadas em tempo de execução, pois o assembly não será gerado corretamente se um nome de propriedade estiver incorreto, etc.
Os mecanismos de criação de um assembly na memória são os seguintes:
Na verdade, isso é bastante simples porque, para a maioria, esse código é implementações de propriedades e inicialização de valor no construtor. Além disso, o único outro código é a expressão.
NOTA: existe uma limitação de que sua expressão deve ser .NET 2.0 (sem lambdas ou outros recursos do C # 3.0) devido a uma limitação no CodeDOM.
Aqui está um código de exemplo para isso.
Além disso, criei uma classe chamada "DataRuleCollection", que implementou o ICollection>. Isso me permitiu criar um recurso "TestAll" e um indexador para executar uma regra específica por nome. Aqui estão as implementações para esses dois métodos.
MAIS CÓDIGO: Houve uma solicitação para o código relacionado à Geração de Código. Encapsulei a funcionalidade em uma classe chamada 'RulesAssemblyGenerator' que incluí abaixo.
Se houver outras perguntas, comentários ou solicitações de mais amostras de código, entre em contato.
fonte
A reflexão é a sua resposta mais versátil. Você tem três colunas de dados e elas precisam ser tratadas de maneiras diferentes:
O seu nome de campo. Reflexão é a maneira de obter o valor de um nome de campo codificado.
O seu operador de comparação. Deve haver um número limitado deles, portanto, uma declaração de caso deve lidar com eles mais facilmente. Especialmente porque alguns deles (tem um ou mais deles) são um pouco mais complexos.
O seu valor de comparação. Se todos esses valores são retos, isso é fácil, embora você tenha dividido as várias entradas acima. No entanto, você também pode usar a reflexão se eles também forem nomes de campos.
Eu adotaria uma abordagem mais como:
etc etc.
Dá flexibilidade para adicionar mais opções de comparação. Isso também significa que você pode codificar nos métodos de comparação qualquer validação de tipo que desejar e torná-los tão complexos quanto desejar. Há também a opção aqui para que o CompareTo seja avaliado como uma chamada recursiva de volta para outra linha ou como um valor de campo, o que poderia ser feito como:
Tudo depende das possibilidades para o futuro ....
fonte
Se você possui apenas algumas propriedades e operadores, o caminho de menor resistência é apenas codificar todas as verificações como casos especiais como este:
Se você tiver muitas propriedades, poderá achar uma abordagem orientada a tabelas mais agradável. Nesse caso, você criaria uma estática
Dictionary
que mapeia nomes de propriedades para delegados correspondentes, digamosFunc<User, object>
,.Se você não souber os nomes das propriedades no momento da compilação, ou se desejar evitar casos especiais para cada propriedade e não quiser usar a abordagem de tabela, poderá usar a reflexão para obter propriedades. Por exemplo:
Mas como
TargetValue
provavelmente é umstring
, você precisará fazer a conversão de tipos da tabela de regras, se necessário.fonte
IComparable
é usado para comparar coisas. Aqui estão os documentos: Método IComparable.CompareTo .Que tal uma abordagem orientada ao tipo de dados com um método de extensão:
Do que você pode avaliar assim:
fonte
Embora a maneira mais óbvia de responder à pergunta "Como implementar um mecanismo de regras? (Em C #)" seja executar um determinado conjunto de regras em sequência, isso geralmente é considerado uma implementação ingênua (não significa que não funcione). :-)
Parece que é "bom o suficiente" no seu caso, porque seu problema parece ser "como executar um conjunto de regras em sequência", e a árvore lambda / expression (resposta de Martin) é certamente a maneira mais elegante nesse assunto, se você são equipados com versões recentes do C #.
No entanto, para cenários mais avançados, aqui está um link para o algoritmo Rete, que é de fato implementado em muitos sistemas de mecanismo de regras comerciais, e outro link para NRuler , uma implementação desse algoritmo em C #.
fonte
A resposta de Martin foi muito boa. Na verdade, criei um mecanismo de regras que tem a mesma ideia que a dele. E fiquei surpreso que é quase o mesmo. Incluí parte do seu código para melhorá-lo. Embora eu tenha feito isso para lidar com regras mais complexas.
Você pode olhar para o Yare.NET
Ou faça o download em Nuget
fonte
Que tal usar o mecanismo de regras do fluxo de trabalho?
Você pode executar regras de fluxo de trabalho do Windows sem fluxo de trabalho, consulte o Blog de Guy Burstein: http://blogs.microsoft.co.il/blogs/bursteg/archive/2006/10/11/RuleExecutionWithoutWorkflow.aspx
e para criar programaticamente suas regras, consulte o WebLog de Stephen Kaufman
http://blogs.msdn.com/b/skaufman/archive/2006/05/15/programmatically-create-windows-workflow-rules.aspx
fonte
Eu adicionei a implementação para e, ou entre regras, adicionei a classe RuleExpression que representa a raiz de uma árvore que pode ser folha a regra é simples ou pode ser e, ou expressões binárias lá porque elas não têm regra e têm expressões:
Eu tenho outra classe que compila o ruleExpression para um
Func<T, bool>:
fonte