Skip to content

Commit 422d4b8

Browse files
authored
Merge pull request #2 from AmirSasson/feature/amirsasson/support-recursive
support recursive rules
2 parents 3211222 + e01bb4e commit 422d4b8

12 files changed

Lines changed: 540 additions & 490 deletions

File tree

README.md

Lines changed: 81 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
NetRuleEngine
1+
NetRuleEngine
22
==============
33
C# simple Rule Engine. High performance object rule matching. Support various complex grouped predicates.
44
available on [nuget](https://www.nuget.org/packages/NetRuleEngine/).
@@ -36,17 +36,58 @@ flowchart LR
3636
* **Business Event** - an Event that occurs on the business flow, and changes a state on your backend, or a standalone event that needs to be tested for rule matching. can be anything as a site Visit, transaction, UI event, Login, Registration or whatever.
3737
* **Business LOGIC** - this is the backend that reference the NetRuleEngine package, consumes the Rules from DB, and for each Event, run the Rule matching and acts according to the result
3838

39-
## Limitations
40-
- this solution doesn't not provide any rules editor UX or DB.
41-
- the rules supports up to 2 levels of conditions.
42-
- supported Scenarios:
43-
- A OR B
44-
- A AND B
45-
- (A AND B AND C AND D AND ...) OR (C AND D)
46-
- (A OR B) AND (C OR D)
47-
- (A OR B) AND (C OR D)
48-
- Not Supported:
49-
- (A AND OR (C AND D)) OR (X AND Y)
39+
## Features and Capabilities
40+
41+
### Nested Rules Support
42+
The engine supports unlimited nesting of rule groups, allowing for complex logical expressions. RulesGroups can contain both individual Rules and other RulesGroups, enabling sophisticated rule combinations like:
43+
- `(A AND (B OR C))`
44+
- `(A OR B) AND (C OR (D AND E))`
45+
- `((A OR B) AND C) OR (D AND (E OR F))`
46+
47+
Example of a complex nested rule:
48+
```csharp
49+
var config = new RulesConfig {
50+
RulesOperator = Rule.InterRuleOperatorType.And,
51+
RulesGroups = [
52+
new RulesGroup {
53+
Operator = Rule.InterRuleOperatorType.Or,
54+
Rules = [
55+
new Rule {
56+
ComparisonOperator = Rule.ComparisonOperatorType.Equal,
57+
ComparisonValue = "example",
58+
ComparisonPredicate = "TextField"
59+
},
60+
new RulesGroup {
61+
Operator = Rule.InterRuleOperatorType.And,
62+
Rules = [
63+
new RulesGroup {
64+
Operator = Rule.InterRuleOperatorType.Or,
65+
Rules = [
66+
new Rule {
67+
ComparisonOperator = Rule.ComparisonOperatorType.GreaterThan,
68+
ComparisonValue = "10",
69+
ComparisonPredicate = "NumericField"
70+
}
71+
]
72+
}
73+
]
74+
}
75+
]
76+
}
77+
]
78+
};
79+
```
80+
81+
### Other Features
82+
- composite objects
83+
- enums
84+
- string
85+
- numbers
86+
- datetime
87+
- Dictionaries
88+
- collections
89+
90+
and many more. See units test for full usage scenarios.
5091

5192
#### Simple usage:
5293

@@ -61,7 +102,7 @@ flowchart LR
61102
RulesOperator = Rule.InterRuleOperatorType.And,
62103
RulesGroups = new RulesGroup[] {
63104
new RulesGroup {
64-
RulesOperator = Rule.InterRuleOperatorType.And,
105+
Operator = Rule.InterRuleOperatorType.And,
65106
// every TestModel instance with NumericField Equal to 5 will match this rule
66107
Rules = new[] {
67108
new Rule {
@@ -76,6 +117,7 @@ flowchart LR
76117
});
77118
```
78119

120+
## Technical Details
79121
- depenent on [LazyCache](https://github.com/alastairtree/LazyCache) to store compiled rules for best performance.
80122
- compiles [Expression Trees](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/expression-trees/) into dynamic cached code to support high performance usage.
81123
- **dependency injection** ready, inject Either IRulesService<> or its dependencies.
@@ -87,70 +129,49 @@ Rule Editor UI Example (not included in this project):
87129
Rule Config JSON Format Example:
88130
```json
89131
{
90-
"Id": "eff37f67-9279-4fff-b2ea-9ef6f92a5de7",
132+
"Id": "123000000-0000-0000-0000-000000000000",
91133
"RulesOperator": "And",
92134
"RulesGroups": [
93135
{
94-
"RulesOperator": "Or",
136+
"Operator": "Or",
95137
"Rules": [
96138
{
97139
"ComparisonPredicate": "TextField",
98140
"ComparisonOperator": "StringStartsWith",
99-
"ComparisonValue": "NOT MATCHING PREFIX",
100-
"PredicateType": null
141+
"ComparisonValue": "NOT MATCHING PREFIX",
101142
},
102143
{
103-
"ComparisonPredicate": "NumericField",
104-
"ComparisonOperator": "GreaterThan",
105-
"ComparisonValue": "4",
106-
"PredicateType": null
107-
}
108-
]
109-
},
110-
{
111-
"RulesOperator": "Or",
112-
"Rules": [
113-
{
114-
"ComparisonPredicate": "TextField",
115-
"ComparisonOperator": "StringStartsWith",
116-
"ComparisonValue": "SomePrefix",
117-
"PredicateType": null
118-
},
119-
{
120-
"ComparisonPredicate": "NumericField",
121-
"ComparisonOperator": "GreaterThan",
122-
"ComparisonValue": "55",
123-
"PredicateType": null
144+
"Operator": "And",
145+
"Rules": [
146+
{
147+
"ComparisonPredicate": "NumericField",
148+
"ComparisonOperator": "GreaterThan",
149+
"ComparisonValue": "10",
150+
},
151+
{
152+
"ComparisonPredicate": "TextField",
153+
"ComparisonOperator": "Equal",
154+
"ComparisonValue": "example",
155+
}
156+
]
124157
}
125158
]
126159
}
127160
]
128161
}
129162
```
130-
this example represents a single rule consists on 2 groups with relation of `AND` (which means object must match both groups), on each group, at least 1 rule should match as both have `OR` operator and both have 2 criterias rules.
131-
132-
-----------------
133-
134-
Features:
135-
- composite objects
136-
- enums
137-
- string
138-
- numbers
139-
- datetime
140-
- Dictionaries
141-
- collections
142-
143-
and many more. See units test for full usage scenarios.
144-
145-
146-
#### decoupling properties names from the rule engine
147-
best practice would be to decouple the Property names from the way they would be used within the rules (the same concept that JsonPropertyAttribute follows when (de)serializing from/to json). this way, renaming the properties will not break the existing rules.
163+
This example demonstrates a nested rule structure where:
164+
- The top level uses an AND operator
165+
- First group has an OR operator and contains:
166+
- A simple string matching rule
167+
- A nested group with an AND operator containing two conditions
168+
169+
#### Decoupling properties names from the rule engine
170+
Best practice would be to decouple the Property names from the way they would be used within the rules (the same concept that JsonPropertyAttribute follows when (de)serializing from/to json). this way, renaming the properties will not break the existing rules.
148171
use RulePredicatePropertyAttribute to name the rule predicate property, otherwise the property name will be used as predicate name.
149172
```csharp
150-
151-
[RulePredicateProperty("first_name")]
152-
public string FirstName { get; set; }
153-
173+
[RulePredicateProperty("first_name")]
174+
public string FirstName { get; set; }
154175
```
155176
first_name will be used as predicate name instead of the property name (FirstName), and you will be able to rename the property name (FirstName) without breaking the rules.
156177
as your rule will be written as:
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
namespace NetRuleEngine.Abstraction
2+
{
3+
public enum ComparisonOperatorType
4+
{
5+
//
6+
// Summary:
7+
// A node that represents an equality comparison, such as (a == b) in C# or (a =
8+
// b) in Visual Basic.
9+
Equal = 13,
10+
// A "greater than" comparison, such as (a > b).
11+
GreaterThan = 15,
12+
//
13+
// Summary:
14+
// A "greater than or equal to" comparison, such as (a >= b).
15+
GreaterThanOrEqual = 16,
16+
//
17+
// Summary:
18+
// A "less than" comparison, such as (a < b).
19+
LessThan = 20,
20+
//
21+
// Summary:
22+
// A "less than or equal to" comparison, such as (a <= b).
23+
LessThanOrEqual = 21,
24+
//
25+
// Summary:
26+
// An inequality comparison, such as (a != b) in C# or (a <> b) in Visual Basic.
27+
NotEqual = 35,
28+
29+
//
30+
// Summary:
31+
// A true condition value.
32+
IsTrue = 83,
33+
//
34+
// Summary:
35+
// A false condition value.
36+
IsFalse = 84,
37+
38+
/// <summary>
39+
/// the ComparisonValue value should be a string with pipe (|) separated values like : 1|2|3
40+
/// </summary>
41+
CollectionContainsAnyOf = 900,
42+
CollectionNotContainsAnyOf = 901,
43+
CollectionContainsAll = 902,
44+
45+
In = 1000,
46+
NotIn = 1001,
47+
48+
// ignore case
49+
StringStartsWith = 1002,
50+
51+
// ignore case
52+
StringEndsWith = 1003,
53+
54+
// ignore case
55+
StringContains = 1004,
56+
57+
// ignore case
58+
StringNotContains = 1005,
59+
StringMatchesRegex = 1006,
60+
StringEqualsCaseInsensitive = 1007,
61+
StringNotEqualsCaseInsensitive = 1008,
62+
StringNullOrEmpty = 1009,
63+
StringNotNullOrEmpty = 1010
64+
}
65+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace NetRuleEngine.Abstraction
2+
{
3+
public enum InternalRuleOperatorType
4+
{
5+
And,
6+
Or
7+
}
8+
}
9+

src/NetRuleEngine/Abstraction/Rule.cs

Lines changed: 5 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,17 @@
1+
using Newtonsoft.Json;
2+
using Newtonsoft.Json.Converters;
13
using System;
2-
using System.Text.Json.Serialization;
34

45
namespace NetRuleEngine.Abstraction
56
{
6-
///
7-
/// The Rule type
8-
///
9-
public class Rule
10-
7+
public class Rule : RuleNode
118
{
12-
public enum ComparisonOperatorType
13-
{
14-
//
15-
// Summary:
16-
// A node that represents an equality comparison, such as (a == b) in C# or (a =
17-
// b) in Visual Basic.
18-
Equal = 13,
19-
// A "greater than" comparison, such as (a > b).
20-
GreaterThan = 15,
21-
//
22-
// Summary:
23-
// A "greater than or equal to" comparison, such as (a >= b).
24-
GreaterThanOrEqual = 16,
25-
//
26-
// Summary:
27-
// A "less than" comparison, such as (a < b).
28-
LessThan = 20,
29-
//
30-
// Summary:
31-
// A "less than or equal to" comparison, such as (a <= b).
32-
LessThanOrEqual = 21,
33-
//
34-
// Summary:
35-
// An inequality comparison, such as (a != b) in C# or (a <> b) in Visual Basic.
36-
NotEqual = 35,
37-
38-
//
39-
// Summary:
40-
// A true condition value.
41-
IsTrue = 83,
42-
//
43-
// Summary:
44-
// A false condition value.
45-
IsFalse = 84,
46-
47-
48-
CollectionContainsAnyOf = 900,
49-
CollectionNotContainsAnyOf = 901,
50-
CollectionContainsAll = 902,
51-
52-
In = 1000,
53-
NotIn = 1001,
54-
55-
// ignore case
56-
StringStartsWith = 1002,
57-
58-
// ignore case
59-
StringEndsWith = 1003,
60-
61-
// ignore case
62-
StringContains = 1004,
63-
64-
// ignore case
65-
StringNotContains = 1005,
66-
StringMatchesRegex = 1006,
67-
StringEqualsCaseInsensitive = 1007,
68-
StringNotEqualsCaseInsensitive = 1008,
69-
StringNullOrEmpty = 1009,
70-
StringNotNullOrEmpty = 1010
71-
}
72-
73-
public enum InterRuleOperatorType
74-
{
75-
And,
76-
Or
77-
}
789
///
79-
/// Denotes the rules predictate (e.g. Name); comparison operator(e.g. ExpressionType.GreaterThan); value (e.g. "Cole")
10+
/// Denotes the rules predicate (e.g. Name); comparison operator(e.g. ExpressionType.GreaterThan); value (e.g. "Cole")
8011
///
8112
public string ComparisonPredicate { get; set; }
8213

83-
[JsonConverter(typeof(JsonStringEnumConverter))]
14+
[JsonConverter(typeof(StringEnumConverter))]
8415
public ComparisonOperatorType ComparisonOperator { get; set; }
8516
public string ComparisonValue { get; set; }
8617
public TypeCode? PredicateType { get; set; }
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using JsonSubTypes;
2+
using Newtonsoft.Json;
3+
4+
namespace NetRuleEngine.Abstraction
5+
{
6+
[JsonConverter(typeof(JsonSubtypes), "$type")]
7+
[JsonSubtypes.KnownSubType(typeof(RulesGroup), "RulesGroup")]
8+
[JsonSubtypes.KnownSubType(typeof(Rule), "Rule")]
9+
[JsonSubtypes.FallBackSubType(typeof(Rule))]
10+
public abstract class RuleNode
11+
{
12+
[JsonProperty("$type")]
13+
public virtual string Type => "Rule";
14+
15+
// only serialize Type if it is not the default "Rule"
16+
public bool ShouldSerializeType()
17+
{
18+
return Type != "Rule";
19+
}
20+
21+
}
22+
}

0 commit comments

Comments
 (0)