Ten fragment kodu kompiluje reguły w szybki kod wykonywalny (przy użyciu drzew wyrażeń ) i nie wymaga żadnych skomplikowanych instrukcji przełączania:
(Edycja: pełny przykład roboczy z metodą ogólną )
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();
}
Następnie możesz napisać:
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));
}
Oto implementacja 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);
}
}
Zauważ, że użyłem „GreaterThan” zamiast „Greater_than” itd. - to dlatego, że „GreaterThan” to nazwa .NET dla operatora, dlatego nie potrzebujemy żadnego dodatkowego mapowania.
Jeśli potrzebujesz niestandardowych nazw, możesz zbudować bardzo prosty słownik i po prostu przetłumaczyć wszystkie operatory przed skompilowaniem reguł:
var nameMap = new Dictionary<string, string> {
{ "greater_than", "GreaterThan" },
{ "hasAtLeastOne", "Contains" }
};
Dla uproszczenia kod używa typu Użytkownik. Możesz zastąpić użytkownika ogólnym typem T, aby mieć ogólny kompilator reguł dla dowolnych typów obiektów. Ponadto kod powinien obsługiwać błędy, takie jak nieznana nazwa operatora.
Zauważ, że generowanie kodu w locie było możliwe nawet przed wprowadzeniem interfejsu API drzew wyrażeń za pomocą Reflection.Emit. Metoda LambdaExpression.Compile () używa Reflection.Emit pod okładkami (można to zobaczyć za pomocą ILSpy ).
Oto kod, który kompiluje się tak, jak jest i wykonuje zadanie. Zasadniczo używaj dwóch słowników, jeden zawierający odwzorowanie nazw operatorów na funkcje boolowskie, a drugi zawierający odwzorowanie nazw właściwości typu User na PropertyInfos używanych do wywoływania getterera (jeśli jest publiczny). Przekazujesz instancję użytkownika i trzy wartości ze swojej tabeli do statycznej metody Apply.
źródło
Zbudowałem silnik reguł, który ma inne podejście niż przedstawione w pytaniu, ale myślę, że okaże się, że będzie on znacznie bardziej elastyczny niż obecne podejście.
Twoje obecne podejście wydaje się koncentrować na pojedynczej jednostce, „Użytkownik”, a twoje trwałe reguły identyfikują „nazwa właściwości”, „operator” i „wartość”. Zamiast tego mój wzór przechowuje kod C # dla predykatu (Func <T, bool>) w kolumnie „Wyrażenie” w mojej bazie danych. W obecnym projekcie, używając generowania kodu, odpytuję „reguły” z mojej bazy danych i kompiluję zestaw z typami „Reguł”, każdy z wykorzystaniem metody „Test”. Oto podpis interfejsu implementowanego dla każdej reguły:
„Wyrażenie” jest kompilowane jako treść metody „Test” podczas pierwszego uruchomienia aplikacji. Jak widać, inne kolumny w tabeli są również wyświetlane jako pierwszorzędne właściwości reguły, dzięki czemu programista ma swobodę tworzenia sposobu, w jaki użytkownik zostanie powiadomiony o niepowodzeniu lub sukcesie.
Generowanie zestawu w pamięci jest jednorazowym wystąpieniem podczas aplikacji i zyskujesz na wydajności, ponieważ nie musisz używać refleksji podczas oceny reguł. Wyrażenia są sprawdzane w czasie wykonywania, ponieważ zestaw nie generuje się poprawnie, jeśli nazwa właściwości jest błędnie napisana itp.
Mechanizmy tworzenia zestawu w pamięci są następujące:
Jest to w rzeczywistości dość proste, ponieważ dla większości ten kod jest implementacją właściwości i inicjowaniem wartości w konstruktorze. Poza tym jedynym innym kodem jest Expression.
UWAGA: istnieje ograniczenie, że twoje wyrażenie musi być .NET 2.0 (bez lambdas lub innych funkcji C # 3.0) z powodu ograniczenia w CodeDOM.
Oto przykładowy kod do tego.
Poza tym stworzyłem klasę o nazwie „DataRuleCollection”, która zaimplementowała ICollection>. Umożliwiło mi to utworzenie funkcji „TestAll” i modułu indeksującego do wykonywania określonej reguły według nazwy. Oto implementacje tych dwóch metod.
WIĘCEJ KODU: Zgłoszono żądanie kodu związanego z generowaniem kodu. Zamknąłem funkcjonalność w klasie o nazwie „RulesAssemblyGenerator”, którą zamieściłem poniżej.
Jeśli są jakieś inne pytania lub komentarze i prośby o dalsze przykłady kodu, daj mi znać.
źródło
Refleksja to twoja najbardziej wszechstronna odpowiedź. Masz trzy kolumny danych i należy je traktować na różne sposoby:
Twoja nazwa pola. Odbicie to sposób na uzyskanie wartości z zakodowanej nazwy pola.
Twój operator porównania. Powinna być ich ograniczona liczba, więc zestawienie przypadków powinno z łatwością sobie z nimi poradzić. Zwłaszcza, że niektóre z nich (ma jeden lub więcej) są nieco bardziej złożone.
Twoja wartość porównawcza. Jeśli wszystkie są prostymi wartościami, jest to łatwe, chociaż będziesz musiał podzielić wiele wpisów w górę. Możesz także użyć refleksji, jeśli są to również nazwy pól.
Przyjąłbym bardziej podejście:
itd itd.
Daje to elastyczność dodawania większej liczby opcji porównania. Oznacza to również, że możesz zakodować w metodach porównania dowolny typ walidacji, jaki chcesz, i uczynić je tak złożonymi, jak chcesz. Istnieje również opcja, aby narzędzie CompareTo było oceniane jako rekurencyjne wywołanie zwrotne do innej linii lub jako wartość pola, co można wykonać w następujący sposób:
Wszystko zależy od możliwości na przyszłość ....
źródło
Jeśli masz tylko kilka właściwości i operatorów, ścieżką najmniejszego oporu jest po prostu zakodowanie wszystkich kontroli jako specjalnych przypadków takich jak ten:
Jeśli masz wiele właściwości, może być łatwiejsze podejście oparte na tabeli. W takim przypadku należy utworzyć statyczną
Dictionary
, która mapuje nazwy właściwości delegatom pasującymi powiedzmyFunc<User, object>
.Jeśli nie znasz nazw właściwości w czasie kompilacji lub chcesz uniknąć specjalnych przypadków dla każdej właściwości i nie chcesz używać podejścia tabelowego, możesz użyć refleksji, aby uzyskać właściwości. Na przykład:
Ale ponieważ
TargetValue
prawdopodobnie jest tostring
, musisz koniecznie wykonać konwersję typu z tabeli reguł, jeśli to konieczne.źródło
IComparable
służy do porównywania rzeczy. Oto dokumentacja: IComparable.CompareTo Method .Co z podejściem zorientowanym na typ danych z metodą rozszerzenia:
Następnie możesz ewaluować w ten sposób:
źródło
Chociaż najbardziej oczywistym sposobem odpowiedzi na pytanie „Jak zaimplementować silnik reguł? (W języku C #)” jest wykonanie danego zestawu reguł w sekwencji, ogólnie uważa się to za naiwną implementację (nie oznacza to, że nie działa :-)
Wydaje się, że w twoim przypadku jest „wystarczająco dobry”, ponieważ twoim problemem jest raczej „jak uruchomić zestaw reguł w sekwencji”, a drzewo lambda / wyrażenie (odpowiedź Martina) jest z pewnością najbardziej eleganckim sposobem, jeśli są wyposażone w najnowsze wersje C #.
Jednak w przypadku bardziej zaawansowanych scenariuszy znajduje się łącze do algorytmu Rete, który jest faktycznie zaimplementowany w wielu komercyjnych systemach silnika reguł, oraz inne łącze do NRuler , implementacji tego algorytmu w języku C #.
źródło
Odpowiedź Martina była całkiem dobra. Stworzyłem silnik reguł, który ma taki sam pomysł jak jego. Byłem zaskoczony, że jest prawie tak samo. Dołączyłem trochę jego kodu, aby go nieco poprawić. Chociaż zrobiłem to, aby obsługiwać bardziej złożone reguły.
Możesz spojrzeć na Yare.NET
Lub pobierz go w Nuget
źródło
Co powiesz na użycie silnika reguł przepływu pracy?
Możesz uruchomić reguły przepływu pracy systemu Windows bez przepływu pracy, zobacz Blog Guya Bursteina: http://blogs.microsoft.co.il/blogs/bursteg/archive/2006/10/11/RuleExecutionWithoutWorkflow.aspx
i aby programowo tworzyć reguły, zobacz WebLog Stephena Kaufmana
http://blogs.msdn.com/b/skaufman/archive/2006/05/15/programmatically-create-windows-workflow-rules.aspx
źródło
Dodałem implementację dla i, lub pomiędzy regułami, dodałem klasę RuleExpression, która reprezentuje korzeń drzewa, które może być liściem, jest prostą regułą lub może być i, lub wyrażenia binarne, ponieważ nie mają reguły i mają wyrażenia:
Mam inną klasę, która kompiluje regułę Wyrażenie do jednej
Func<T, bool>:
źródło