diff --git a/README.md b/README.md
index 6c1a8ed..959da7a 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
- NetRuleEngine
+NetRuleEngine
==============
C# simple Rule Engine. High performance object rule matching. Support various complex grouped predicates.
available on [nuget](https://www.nuget.org/packages/NetRuleEngine/).
@@ -36,17 +36,58 @@ flowchart LR
* **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.
* **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
-## Limitations
-- this solution doesn't not provide any rules editor UX or DB.
-- the rules supports up to 2 levels of conditions.
- - supported Scenarios:
- - A OR B
- - A AND B
- - (A AND B AND C AND D AND ...) OR (C AND D)
- - (A OR B) AND (C OR D)
- - (A OR B) AND (C OR D)
- - Not Supported:
- - (A AND OR (C AND D)) OR (X AND Y)
+## Features and Capabilities
+
+### Nested Rules Support
+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:
+- `(A AND (B OR C))`
+- `(A OR B) AND (C OR (D AND E))`
+- `((A OR B) AND C) OR (D AND (E OR F))`
+
+Example of a complex nested rule:
+```csharp
+var config = new RulesConfig {
+ RulesOperator = Rule.InterRuleOperatorType.And,
+ RulesGroups = [
+ new RulesGroup {
+ Operator = Rule.InterRuleOperatorType.Or,
+ Rules = [
+ new Rule {
+ ComparisonOperator = Rule.ComparisonOperatorType.Equal,
+ ComparisonValue = "example",
+ ComparisonPredicate = "TextField"
+ },
+ new RulesGroup {
+ Operator = Rule.InterRuleOperatorType.And,
+ Rules = [
+ new RulesGroup {
+ Operator = Rule.InterRuleOperatorType.Or,
+ Rules = [
+ new Rule {
+ ComparisonOperator = Rule.ComparisonOperatorType.GreaterThan,
+ ComparisonValue = "10",
+ ComparisonPredicate = "NumericField"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+};
+```
+
+### Other Features
+- composite objects
+- enums
+- string
+- numbers
+- datetime
+- Dictionaries
+- collections
+
+and many more. See units test for full usage scenarios.
#### Simple usage:
@@ -61,7 +102,7 @@ flowchart LR
RulesOperator = Rule.InterRuleOperatorType.And,
RulesGroups = new RulesGroup[] {
new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.And,
+ Operator = Rule.InterRuleOperatorType.And,
// every TestModel instance with NumericField Equal to 5 will match this rule
Rules = new[] {
new Rule {
@@ -76,6 +117,7 @@ flowchart LR
});
```
+## Technical Details
- depenent on [LazyCache](https://github.com/alastairtree/LazyCache) to store compiled rules for best performance.
- 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.
- **dependency injection** ready, inject Either IRulesService<> or its dependencies.
@@ -87,70 +129,49 @@ Rule Editor UI Example (not included in this project):
Rule Config JSON Format Example:
```json
{
- "Id": "eff37f67-9279-4fff-b2ea-9ef6f92a5de7",
+ "Id": "123000000-0000-0000-0000-000000000000",
"RulesOperator": "And",
"RulesGroups": [
{
- "RulesOperator": "Or",
+ "Operator": "Or",
"Rules": [
{
"ComparisonPredicate": "TextField",
"ComparisonOperator": "StringStartsWith",
- "ComparisonValue": "NOT MATCHING PREFIX",
- "PredicateType": null
+ "ComparisonValue": "NOT MATCHING PREFIX",
},
{
- "ComparisonPredicate": "NumericField",
- "ComparisonOperator": "GreaterThan",
- "ComparisonValue": "4",
- "PredicateType": null
- }
- ]
- },
- {
- "RulesOperator": "Or",
- "Rules": [
- {
- "ComparisonPredicate": "TextField",
- "ComparisonOperator": "StringStartsWith",
- "ComparisonValue": "SomePrefix",
- "PredicateType": null
- },
- {
- "ComparisonPredicate": "NumericField",
- "ComparisonOperator": "GreaterThan",
- "ComparisonValue": "55",
- "PredicateType": null
+ "Operator": "And",
+ "Rules": [
+ {
+ "ComparisonPredicate": "NumericField",
+ "ComparisonOperator": "GreaterThan",
+ "ComparisonValue": "10",
+ },
+ {
+ "ComparisonPredicate": "TextField",
+ "ComparisonOperator": "Equal",
+ "ComparisonValue": "example",
+ }
+ ]
}
]
}
]
}
```
-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.
-
------------------
-
-Features:
-- composite objects
-- enums
-- string
-- numbers
-- datetime
-- Dictionaries
-- collections
-
-and many more. See units test for full usage scenarios.
-
-
-#### decoupling properties names from the rule engine
-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.
+This example demonstrates a nested rule structure where:
+- The top level uses an AND operator
+- First group has an OR operator and contains:
+ - A simple string matching rule
+ - A nested group with an AND operator containing two conditions
+
+#### Decoupling properties names from the rule engine
+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.
use RulePredicatePropertyAttribute to name the rule predicate property, otherwise the property name will be used as predicate name.
```csharp
-
- [RulePredicateProperty("first_name")]
- public string FirstName { get; set; }
-
+[RulePredicateProperty("first_name")]
+public string FirstName { get; set; }
```
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.
as your rule will be written as:
diff --git a/src/NetRuleEngine/Abstraction/ComparisonOperatorType.cs b/src/NetRuleEngine/Abstraction/ComparisonOperatorType.cs
new file mode 100644
index 0000000..2e873b4
--- /dev/null
+++ b/src/NetRuleEngine/Abstraction/ComparisonOperatorType.cs
@@ -0,0 +1,65 @@
+namespace NetRuleEngine.Abstraction
+{
+ public enum ComparisonOperatorType
+ {
+ //
+ // Summary:
+ // A node that represents an equality comparison, such as (a == b) in C# or (a =
+ // b) in Visual Basic.
+ Equal = 13,
+ // A "greater than" comparison, such as (a > b).
+ GreaterThan = 15,
+ //
+ // Summary:
+ // A "greater than or equal to" comparison, such as (a >= b).
+ GreaterThanOrEqual = 16,
+ //
+ // Summary:
+ // A "less than" comparison, such as (a < b).
+ LessThan = 20,
+ //
+ // Summary:
+ // A "less than or equal to" comparison, such as (a <= b).
+ LessThanOrEqual = 21,
+ //
+ // Summary:
+ // An inequality comparison, such as (a != b) in C# or (a <> b) in Visual Basic.
+ NotEqual = 35,
+
+ //
+ // Summary:
+ // A true condition value.
+ IsTrue = 83,
+ //
+ // Summary:
+ // A false condition value.
+ IsFalse = 84,
+
+ ///
+ /// the ComparisonValue value should be a string with pipe (|) separated values like : 1|2|3
+ ///
+ CollectionContainsAnyOf = 900,
+ CollectionNotContainsAnyOf = 901,
+ CollectionContainsAll = 902,
+
+ In = 1000,
+ NotIn = 1001,
+
+ // ignore case
+ StringStartsWith = 1002,
+
+ // ignore case
+ StringEndsWith = 1003,
+
+ // ignore case
+ StringContains = 1004,
+
+ // ignore case
+ StringNotContains = 1005,
+ StringMatchesRegex = 1006,
+ StringEqualsCaseInsensitive = 1007,
+ StringNotEqualsCaseInsensitive = 1008,
+ StringNullOrEmpty = 1009,
+ StringNotNullOrEmpty = 1010
+ }
+}
diff --git a/src/NetRuleEngine/Abstraction/InternalRuleOperatorType.cs b/src/NetRuleEngine/Abstraction/InternalRuleOperatorType.cs
new file mode 100644
index 0000000..5a9fec2
--- /dev/null
+++ b/src/NetRuleEngine/Abstraction/InternalRuleOperatorType.cs
@@ -0,0 +1,9 @@
+namespace NetRuleEngine.Abstraction
+{
+ public enum InternalRuleOperatorType
+ {
+ And,
+ Or
+ }
+}
+
diff --git a/src/NetRuleEngine/Abstraction/Rule.cs b/src/NetRuleEngine/Abstraction/Rule.cs
index 461dd8f..03a1915 100644
--- a/src/NetRuleEngine/Abstraction/Rule.cs
+++ b/src/NetRuleEngine/Abstraction/Rule.cs
@@ -1,86 +1,17 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
using System;
-using System.Text.Json.Serialization;
namespace NetRuleEngine.Abstraction
{
- ///
- /// The Rule type
- ///
- public class Rule
-
+ public class Rule : RuleNode
{
- public enum ComparisonOperatorType
- {
- //
- // Summary:
- // A node that represents an equality comparison, such as (a == b) in C# or (a =
- // b) in Visual Basic.
- Equal = 13,
- // A "greater than" comparison, such as (a > b).
- GreaterThan = 15,
- //
- // Summary:
- // A "greater than or equal to" comparison, such as (a >= b).
- GreaterThanOrEqual = 16,
- //
- // Summary:
- // A "less than" comparison, such as (a < b).
- LessThan = 20,
- //
- // Summary:
- // A "less than or equal to" comparison, such as (a <= b).
- LessThanOrEqual = 21,
- //
- // Summary:
- // An inequality comparison, such as (a != b) in C# or (a <> b) in Visual Basic.
- NotEqual = 35,
-
- //
- // Summary:
- // A true condition value.
- IsTrue = 83,
- //
- // Summary:
- // A false condition value.
- IsFalse = 84,
-
-
- CollectionContainsAnyOf = 900,
- CollectionNotContainsAnyOf = 901,
- CollectionContainsAll = 902,
-
- In = 1000,
- NotIn = 1001,
-
- // ignore case
- StringStartsWith = 1002,
-
- // ignore case
- StringEndsWith = 1003,
-
- // ignore case
- StringContains = 1004,
-
- // ignore case
- StringNotContains = 1005,
- StringMatchesRegex = 1006,
- StringEqualsCaseInsensitive = 1007,
- StringNotEqualsCaseInsensitive = 1008,
- StringNullOrEmpty = 1009,
- StringNotNullOrEmpty = 1010
- }
-
- public enum InterRuleOperatorType
- {
- And,
- Or
- }
///
- /// Denotes the rules predictate (e.g. Name); comparison operator(e.g. ExpressionType.GreaterThan); value (e.g. "Cole")
+ /// Denotes the rules predicate (e.g. Name); comparison operator(e.g. ExpressionType.GreaterThan); value (e.g. "Cole")
///
public string ComparisonPredicate { get; set; }
- [JsonConverter(typeof(JsonStringEnumConverter))]
+ [JsonConverter(typeof(StringEnumConverter))]
public ComparisonOperatorType ComparisonOperator { get; set; }
public string ComparisonValue { get; set; }
public TypeCode? PredicateType { get; set; }
diff --git a/src/NetRuleEngine/Abstraction/RuleNode.cs b/src/NetRuleEngine/Abstraction/RuleNode.cs
new file mode 100644
index 0000000..49bfbaf
--- /dev/null
+++ b/src/NetRuleEngine/Abstraction/RuleNode.cs
@@ -0,0 +1,22 @@
+using JsonSubTypes;
+using Newtonsoft.Json;
+
+namespace NetRuleEngine.Abstraction
+{
+ [JsonConverter(typeof(JsonSubtypes), "$type")]
+ [JsonSubtypes.KnownSubType(typeof(RulesGroup), "RulesGroup")]
+ [JsonSubtypes.KnownSubType(typeof(Rule), "Rule")]
+ [JsonSubtypes.FallBackSubType(typeof(Rule))]
+ public abstract class RuleNode
+ {
+ [JsonProperty("$type")]
+ public virtual string Type => "Rule";
+
+ // only serialize Type if it is not the default "Rule"
+ public bool ShouldSerializeType()
+ {
+ return Type != "Rule";
+ }
+
+ }
+}
diff --git a/src/NetRuleEngine/Abstraction/RulesConfig.cs b/src/NetRuleEngine/Abstraction/RulesConfig.cs
index d24c7be..d0801df 100644
--- a/src/NetRuleEngine/Abstraction/RulesConfig.cs
+++ b/src/NetRuleEngine/Abstraction/RulesConfig.cs
@@ -1,36 +1,43 @@
using System;
using System.Collections.Generic;
-using System.Text.Json;
-using System.Text.Json.Serialization;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
namespace NetRuleEngine.Abstraction
{
+ ///
+ /// Rule Root
+ ///
public class RulesConfig
{
- public class RulesGroup
- {
- [JsonConverter(typeof(JsonStringEnumConverter))]
- public Rule.InterRuleOperatorType RulesOperator { get; set; }
- public IEnumerable Rules { get; set; }
- }
public Guid Id { get; set; }
[JsonIgnore]
public string CacheKey => ToJson().GetHashCode().ToString();
- [JsonConverter(typeof(JsonStringEnumConverter))]
- public Rule.InterRuleOperatorType RulesOperator { get; set; }
+ [JsonConverter(typeof(StringEnumConverter))]
+ public InternalRuleOperatorType RulesOperator { get; set; }
- public IEnumerable RulesGroups { get; set; }
+ public IEnumerable RulesGroups { get; set; }
public static RulesConfig FromJson(string json)
{
- return JsonSerializer.Deserialize(json);
+ var settings = new JsonSerializerSettings
+ {
+ Converters = { new StringEnumConverter() }
+ };
+ return JsonConvert.DeserializeObject(json, settings);
}
public string ToJson()
{
- return JsonSerializer.Serialize(this);
+ var settings = new JsonSerializerSettings
+ {
+ Converters = { new StringEnumConverter() },
+ NullValueHandling = NullValueHandling.Ignore,
+ DefaultValueHandling = DefaultValueHandling.Ignore
+ };
+ return JsonConvert.SerializeObject(this, settings);
}
}
}
diff --git a/src/NetRuleEngine/Abstraction/RulesGroup.cs b/src/NetRuleEngine/Abstraction/RulesGroup.cs
new file mode 100644
index 0000000..dde9b48
--- /dev/null
+++ b/src/NetRuleEngine/Abstraction/RulesGroup.cs
@@ -0,0 +1,17 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using System.Collections.Generic;
+
+namespace NetRuleEngine.Abstraction
+{
+ public class RulesGroup : RuleNode
+ {
+ public override string Type => "RulesGroup";
+
+ [JsonProperty("Operator", Required = Required.Default, DefaultValueHandling = DefaultValueHandling.Include, Order = int.MinValue)]
+ [JsonConverter(typeof(StringEnumConverter))]
+ public InternalRuleOperatorType Operator { get; set; }
+
+ public IEnumerable Rules { get; set; }
+ }
+}
diff --git a/src/NetRuleEngine/Domains/RulesCompiler.cs b/src/NetRuleEngine/Domains/RulesCompiler.cs
index 920de7a..e65d74e 100644
--- a/src/NetRuleEngine/Domains/RulesCompiler.cs
+++ b/src/NetRuleEngine/Domains/RulesCompiler.cs
@@ -5,7 +5,6 @@
using System.Linq.Expressions;
using System.Reflection;
using System.Text.RegularExpressions;
-using static NetRuleEngine.Abstraction.Rule;
namespace NetRuleEngine.Domains
{
@@ -22,24 +21,12 @@ public class RulesCompiler : IRulesCompiler
Expression combinedExp = null;
var genericType = Expression.Parameter(typeof(T));
- foreach (var rulesGroup in rulesConfig.RulesGroups)
+ foreach (var ruleNode in rulesConfig.RulesGroups)
{
- Expression groupExpression = null;
- foreach (var rule in rulesGroup.Rules)
- {
- Expression binaryExpression = getRuleExpression(genericType, rule);
- if (rulesGroup.RulesOperator == InterRuleOperatorType.And)
- {
- groupExpression = groupExpression != null ? Expression.AndAlso(groupExpression, binaryExpression) : binaryExpression;
- }
- else // OR
- {
- groupExpression = groupExpression != null ? Expression.OrElse(groupExpression, binaryExpression) : binaryExpression;
- }
- }
+ var groupExpression = ProcessRuleOrGroup(genericType, ruleNode);
// Stitching the rules into 1 statement
- if (rulesConfig.RulesOperator == InterRuleOperatorType.And)
+ if (rulesConfig.RulesOperator == InternalRuleOperatorType.And)
{
combinedExp = combinedExp != null ? Expression.AndAlso(combinedExp, groupExpression) : groupExpression;
}
@@ -48,7 +35,34 @@ public class RulesCompiler : IRulesCompiler
combinedExp = combinedExp != null ? Expression.OrElse(combinedExp, groupExpression) : groupExpression;
}
}
- return (CompliedRule: Expression.Lambda>(combinedExp ?? Expression.Constant(true), genericType).Compile(), RuleDescription: combinedExp.ToString());
+ return (CompliedRule: Expression.Lambda>(combinedExp ?? Expression.Constant(true), genericType).Compile(), RuleDescription: combinedExp?.ToString() ?? "Empty rule");
+ }
+
+ private Expression ProcessRuleOrGroup(ParameterExpression genericType, RuleNode ruleNode)
+ {
+ // If it's a group, process its rules recursively
+ if (ruleNode is RulesGroup group)
+ {
+ Expression groupExp = null;
+ foreach (var childRule in group.Rules)
+ {
+ var childExp = ProcessRuleOrGroup(genericType, childRule);
+
+ // Combine with group operator
+ if (group.Operator == InternalRuleOperatorType.And)
+ {
+ groupExp = groupExp != null ? Expression.AndAlso(groupExp, childExp) : childExp;
+ }
+ else // OR
+ {
+ groupExp = groupExp != null ? Expression.OrElse(groupExp, childExp) : childExp;
+ }
+ }
+ return groupExp ?? Expression.Constant(true);
+ }
+
+ // If it's a simple rule, use existing logic
+ return getRuleExpression(genericType, ruleNode as Rule);
}
private static Expression getRuleExpression(ParameterExpression genericType, Rule rule)
@@ -134,7 +148,6 @@ private static Expression getRuleExpression(ParameterExpression genericType,
}
return binaryExpression;
-
}
// translating ComparisonPredicate to the property name by ComparisonPredicateNameAttribute if exists.
@@ -301,4 +314,4 @@ private static Expression createCollectionExpression(Rule rule, MemberExpression
return binaryExpression;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/NetRuleEngine/NetRuleEngine.csproj b/src/NetRuleEngine/NetRuleEngine.csproj
index 1147d3e..93ba2ec 100644
--- a/src/NetRuleEngine/NetRuleEngine.csproj
+++ b/src/NetRuleEngine/NetRuleEngine.csproj
@@ -3,7 +3,7 @@
netstandard2.0
NetRuleEngine
- 2.2.0
+ 3.0.0
AmirSasson
C# Rule Engine. High performance object rule matching. Support various
complex grouped predicates.
@@ -19,6 +19,8 @@
+
+
diff --git a/tests/NetRuleEngineTests/NetRuleEngineTests.csproj b/tests/NetRuleEngineTests/NetRuleEngineTests.csproj
index d418a93..d073462 100644
--- a/tests/NetRuleEngineTests/NetRuleEngineTests.csproj
+++ b/tests/NetRuleEngineTests/NetRuleEngineTests.csproj
@@ -8,6 +8,7 @@
+
diff --git a/tests/NetRuleEngineTests/RulesTests.cs b/tests/NetRuleEngineTests/RulesTests.cs
index e8f8561..0d6aaac 100644
--- a/tests/NetRuleEngineTests/RulesTests.cs
+++ b/tests/NetRuleEngineTests/RulesTests.cs
@@ -1,13 +1,11 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
-using Microsoft.VisualStudio.TestPlatform.TestHost;
using NetRuleEngine.Abstraction;
using NetRuleEngine.Domains;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
-using static NetRuleEngine.Abstraction.RulesConfig;
namespace NetRuleEngineTests
{
@@ -18,72 +16,101 @@ public RulesTests()
{
var loggerFactory = LoggerFactory.Create(builder =>
{
- builder.AddConsole(); // This line now works with the added namespace
+ builder.AddConsole();
});
_logger = loggerFactory.CreateLogger();
}
- [Theory]
- [InlineData(5, Rule.ComparisonOperatorType.Equal, 5, true)]
- [InlineData(5, Rule.ComparisonOperatorType.LessThan, 4, true)]
- [InlineData(5, Rule.ComparisonOperatorType.LessThan, 6, false)]
- [InlineData(5, Rule.ComparisonOperatorType.GreaterThan, 6, true)]
- public void GetMatchingRules_NumericValueMatch_ShouldMatchByOperatorAndValue(int ruleVal, Rule.ComparisonOperatorType op, int objectVal, bool shouldMatch)
+ // First add the new nested rules tests
+ [Fact]
+ public void GetMatchingRules_NestedGroups_RuleReturned()
{
// Arrange
- var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), _logger);
+ var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
// Act
- var numericValueTest = objectVal;
var matching = engine.GetMatchingRules(
- new TestModel { NumericField = numericValueTest },
+ new TestModel { TextField = "example", NumericField = 15 },
[
new RulesConfig {
- Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
+ Id = Guid.NewGuid(),
+ RulesOperator = InternalRuleOperatorType.And,
RulesGroups = [
new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.And,
+ Operator = InternalRuleOperatorType.Or,
Rules = [
- new Rule { ComparisonOperator = op, ComparisonValue = ruleVal.ToString(), ComparisonPredicate = nameof(TestModel.NumericField) }
+ new Rule {
+ ComparisonOperator = ComparisonOperatorType.Equal,
+ ComparisonValue = "not matching",
+ ComparisonPredicate = nameof(TestModel.TextField)
+ },
+ new RulesGroup {
+ Operator = InternalRuleOperatorType.And,
+ Rules = [
+ new Rule {
+ ComparisonOperator = ComparisonOperatorType.Equal,
+ ComparisonValue = "example",
+ ComparisonPredicate = nameof(TestModel.TextField)
+ },
+ new Rule {
+ ComparisonOperator = ComparisonOperatorType.GreaterThan,
+ ComparisonValue = "10",
+ ComparisonPredicate = nameof(TestModel.NumericField)
+ }
+ ]
+ }
]
}
]
}
]);
- // Assert
- if (shouldMatch)
- {
- Assert.Single(matching.Data);
- }
- else
- {
- Assert.Empty(matching.Data);
- }
+ // Assert
+ Assert.Single(matching.Data);
}
+
+ //{"Id":"1b82df80-d215-4fc1-bd49-7329e7f584e1","RulesOperator":"And","RulesGroups":[{"$type":"RulesGroup","Rules":[{"$type":"Rule","Operator":"And","ComparisonPredicate":"TextField","ComparisonOperator":"Equal","ComparisonValue":"not matching","PredicateType":null},{"$type":"RulesGroup","Rules":[{"$type":"RulesGroup","Rules":[{"$type":"Rule","Operator":"And","ComparisonPredicate":"TextField","ComparisonOperator":"Equal","ComparisonValue":"example","PredicateType":null}],"Operator":"Or","ComparisonPredicate":null,"ComparisonOperator":0,"ComparisonValue":null,"PredicateType":null},{"$type":"RulesGroup","Rules":[{"$type":"Rule","Operator":"And","ComparisonPredicate":"NumericField","ComparisonOperator":"GreaterThan","ComparisonValue":"10","PredicateType":null}],"Operator":"Or","ComparisonPredicate":null,"ComparisonOperator":0,"ComparisonValue":null,"PredicateType":null}],"Operator":"And","ComparisonPredicate":null,"ComparisonOperator":0,"ComparisonValue":null,"PredicateType":null}],"Operator":"Or","ComparisonPredicate":null,"ComparisonOperator":0,"ComparisonValue":null,"PredicateType":null}]}
+
+
[Fact]
- public void GetMatchingRules_NumericValueNotMatch_RuleNotReturned()
+ public void GetMatchingRules_NestedGroups_RuleNotReturned()
{
// Arrange
- IRulesService engine = RulesService.CreateDefault();
+ var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
// Act
- var numericValueTest = 5;
- var numericValueOtherValue = 6;
var matching = engine.GetMatchingRules(
- new TestModel { NumericField = numericValueTest },
+ new TestModel { TextField = "example", NumericField = 5 }, // NumericField < 10 should cause the nested group to not match
[
new RulesConfig {
Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
+ RulesOperator = InternalRuleOperatorType.And,
RulesGroups = [
new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.And,
+ Operator = InternalRuleOperatorType.Or,
Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.Equal, ComparisonValue = numericValueOtherValue.ToString(), ComparisonPredicate = nameof(TestModel.NumericField) }
+ new Rule {
+ ComparisonOperator = ComparisonOperatorType.Equal,
+ ComparisonValue = "not matching",
+ ComparisonPredicate = nameof(TestModel.TextField)
+ },
+ new RulesGroup {
+ Operator = InternalRuleOperatorType.And,
+ Rules = [
+ new Rule {
+ ComparisonOperator = ComparisonOperatorType.Equal,
+ ComparisonValue = "example",
+ ComparisonPredicate = nameof(TestModel.TextField)
+ },
+ new Rule {
+ ComparisonOperator = ComparisonOperatorType.GreaterThan,
+ ComparisonValue = "10",
+ ComparisonPredicate = nameof(TestModel.NumericField)
+ }
+ ]
+ }
]
}
]
@@ -95,23 +122,52 @@ public void GetMatchingRules_NumericValueNotMatch_RuleNotReturned()
}
[Fact]
- public void GetMatchingRules_StringStartsWithMatch_RuleReturned()
+ public void GetMatchingRules_DeepNestedGroups_RuleReturned()
{
// Arrange
var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
// Act
var matching = engine.GetMatchingRules(
- new TestModel { TextField = "SomePrefixBlahBlah" },
+ new TestModel { TextField = "example", NumericField = 15 },
[
new RulesConfig {
- Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
+ Id = Guid.NewGuid(),
+ RulesOperator = InternalRuleOperatorType.And,
RulesGroups = [
new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.And,
+ Operator = InternalRuleOperatorType.Or,
Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.StringStartsWith, ComparisonValue = "someprefix", ComparisonPredicate = nameof(TestModel.TextField) }
+ new Rule {
+ ComparisonOperator = ComparisonOperatorType.Equal,
+ ComparisonValue = "not matching",
+ ComparisonPredicate = nameof(TestModel.TextField)
+ },
+ new RulesGroup {
+ Operator = InternalRuleOperatorType.And,
+ Rules = [
+ new RulesGroup {
+ Operator = InternalRuleOperatorType.Or,
+ Rules = [
+ new Rule {
+ ComparisonOperator = ComparisonOperatorType.Equal,
+ ComparisonValue = "example",
+ ComparisonPredicate = nameof(TestModel.TextField)
+ }
+ ]
+ },
+ new RulesGroup {
+ Operator = InternalRuleOperatorType.Or,
+ Rules = [
+ new Rule {
+ ComparisonOperator = ComparisonOperatorType.GreaterThan,
+ ComparisonValue = "10",
+ ComparisonPredicate = nameof(TestModel.NumericField)
+ }
+ ]
+ }
+ ]
+ }
]
}
]
@@ -123,57 +179,62 @@ public void GetMatchingRules_StringStartsWithMatch_RuleReturned()
}
[Fact]
- public void GetMatchingRules_MultiRuleAndMatch_RuleReturned()
+ public void GetMatchingRules_DeserializedRule_RuleReturned()
{
// Arrange
var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
- // Act
- var matching = engine.GetMatchingRules(
- new TestModel { TextField = "SomePrefixBlahBlah", NumericField = 10 },
- [
- new RulesConfig {
- Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
- RulesGroups = [
+ var rule = new RulesConfig
+ {
+ Id = Guid.NewGuid(),
+ RulesOperator = InternalRuleOperatorType.And,
+ RulesGroups = [
new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.And,
+ Operator = InternalRuleOperatorType.Or,
Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.StringStartsWith, ComparisonValue = "someprefix", ComparisonPredicate = nameof(TestModel.TextField) },
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.GreaterThan, ComparisonValue = 4.ToString(), ComparisonPredicate = nameof(TestModel.NumericField) }
+ new Rule {
+ ComparisonOperator = ComparisonOperatorType.Equal,
+ ComparisonValue = "not matching",
+ ComparisonPredicate = nameof(TestModel.TextField)
+ },
+ new RulesGroup {
+ Operator = InternalRuleOperatorType.And,
+ Rules = [
+ new RulesGroup {
+ Operator = InternalRuleOperatorType.Or,
+ Rules = [
+ new Rule {
+ ComparisonOperator = ComparisonOperatorType.Equal,
+ ComparisonValue = "example",
+ ComparisonPredicate = nameof(TestModel.TextField)
+ }
+ ]
+ },
+ new RulesGroup {
+ Operator = InternalRuleOperatorType.Or,
+ Rules = [
+ new Rule {
+ ComparisonOperator = ComparisonOperatorType.GreaterThan,
+ ComparisonValue = "10",
+ ComparisonPredicate = nameof(TestModel.NumericField)
+ }
+ ]
+ }
+ ]
+ }
]
}
]
- }
- ]);
-
- // Assert
- Assert.Single(matching.Data);
- }
+ };
- [Fact]
- public void GetMatchingRules_MultiRuleOrMatch_RuleReturned()
- {
- // Arrange
- var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
+ var stringifiedRule = rule.ToJson(); // Serialize to JSON to simulate deserialization
+ var deserializedRule = RulesConfig.FromJson(stringifiedRule);
// Act
var matching = engine.GetMatchingRules(
- new TestModel { TextField = "SomePrefixBlahBlah", NumericField = 10 },
+ new TestModel { TextField = "example", NumericField = 15 },
[
- new RulesConfig {
- Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
- RulesGroups = [
- new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.Or,
- Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.StringStartsWith, ComparisonValue = "NOT MATCHING PREFIX", ComparisonPredicate = nameof(TestModel.TextField) },
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.GreaterThan, ComparisonValue = 4.ToString(), ComparisonPredicate = nameof(TestModel.NumericField) }
- ]
- }
- ]
- }
+ deserializedRule
]);
// Assert
@@ -181,227 +242,149 @@ public void GetMatchingRules_MultiRuleOrMatch_RuleReturned()
}
[Fact]
- public void GetMatchingRules_MultiGroupAnMatch_RuleReturned()
+ public void GetMatchingRules_DeserializedRule_RuleMatch()
{
- // Arrange
- var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
-
- // Act
- var ruleConfig =
- new RulesConfig
+ var serializedRule = /*lang=json,strict*/ """
+{
+ "Id": "d952df97-7d54-45db-acf4-90f723e7bdf0",
+ "RulesOperator": "And",
+ "RulesGroups": [
+ {
+ "$type": "RulesGroup",
+ "Rules": [
+ {
+ "ComparisonPredicate": "TextField",
+ "ComparisonOperator": "Equal",
+ "ComparisonValue": "not matching"
+ },
+ {
+ "$type": "RulesGroup",
+ "Rules": [
{
- Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
- RulesGroups = [
- new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.Or,
- Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.StringStartsWith, ComparisonValue = "NOT MATCHING PREFIX", ComparisonPredicate = nameof(TestModel.TextField) },
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.GreaterThan, ComparisonValue = 4.ToString(), ComparisonPredicate = nameof(TestModel.NumericField) }
- ]
+ "$type": "RulesGroup",
+ "Rules": [
+ {
+ "ComparisonPredicate": "TextField",
+ "ComparisonOperator": "Equal",
+ "ComparisonValue": "example"
},
- new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.Or,
- Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.StringStartsWith, ComparisonValue = "SomePrefix", ComparisonPredicate = nameof(TestModel.TextField) },
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.GreaterThan, ComparisonValue = 55.ToString(), ComparisonPredicate = nameof(TestModel.NumericField) }
- ]
+ {
+ "ComparisonPredicate": "TextField",
+ "ComparisonOperator": "StringStartsWith",
+ "ComparisonValue": "ex"
}
- ]
- };
- var text = ruleConfig.ToJson();
- var deserializedRules = FromJson(text);
-
- var matching = engine.GetMatchingRules(
- new TestModel { TextField = "SomePrefixBlahBlah", NumericField = 10 },
- [deserializedRules]);
- // Assert
- Assert.Single(matching.Data);
- }
-
- [Fact]
- public void GetMatchingRules_MultiGroupFirstGroupNotMatch_RuleNotReturned()
- {
- // Arrange
- var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
-
- // Act
- var matching = engine.GetMatchingRules(
- new TestModel { TextField = "SomePrefixBlahBlah", NumericField = 10 },
- [
- new RulesConfig {
- Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
- RulesGroups = [
- new RulesGroup { // this group does not match!
- RulesOperator = Rule.InterRuleOperatorType.Or,
- Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.StringStartsWith, ComparisonValue = "NOT MATCHING PREFIX", ComparisonPredicate = nameof(TestModel.TextField) },
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.LessThan, ComparisonValue = 4.ToString(), ComparisonPredicate = nameof(TestModel.NumericField) }
- ]
+ ],
+ "Operator": "And"
+ },
+ {
+ "$type": "RulesGroup",
+ "Rules": [
+ {
+ "ComparisonPredicate": "NumericField",
+ "ComparisonOperator": "GreaterThan",
+ "ComparisonValue": "10"
},
- new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.Or,
- Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.StringStartsWith, ComparisonValue = "SomePrefix", ComparisonPredicate = nameof(TestModel.TextField) },
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.GreaterThan, ComparisonValue = 55.ToString(), ComparisonPredicate = nameof(TestModel.NumericField) }
- ]
+ {
+ "ComparisonPredicate": "NumericField",
+ "ComparisonOperator": "GreaterThanOrEqual",
+ "ComparisonValue": "15"
}
- ]
+ ],
+ "Operator": "And"
}
- ]);
-
- // Assert
- Assert.Empty(matching.Data);
- }
+ ],
+ "Operator": "And"
+ }
+ ],
+ "Operator": "Or"
+ }
+ ]
+}
+""";
- [Fact]
- public void GetMatchingRules_CompositePropertyMatch_RuleReturned()
- {
// Arrange
var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
+ var deserializedRule = RulesConfig.FromJson(serializedRule);
- // Act
- var matching = engine.GetMatchingRules(
- new TestModel { Composit = new TestModel.CompositeInnerClass { NumericField = 10 } },
- [
- new RulesConfig {
- Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
- RulesGroups = [
- new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.Or,
- Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.GreaterThanOrEqual, ComparisonValue = 4.ToString(), ComparisonPredicate = $"{nameof(TestModel.Composit)}.{nameof(TestModel.Composit.NumericField)}"}
- ]
- }
- ]
- }
- ]);
-
- // Assert
- Assert.Single(matching.Data);
- }
-
- [Fact]
- public void GetMatchingRules_CaluculatedCOllectionCollectionContainsAnyOfMatch_RuleReturned()
- {
- // Arrange
- var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
// Act
var matching = engine.GetMatchingRules(
- new TestModel { CompositeCollection = [new TestModel.CompositeInnerClass { NumericField = 10 }] },
+ new TestModel { TextField = "example", NumericField = 15 },
[
- new RulesConfig {
- Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
- RulesGroups = [
- new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.Or,
- Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.CollectionContainsAnyOf, ComparisonValue = "10|11|12", ComparisonPredicate = $"{nameof(TestModel.CaluculatedCollection)}"}
- ]
- }
- ]
- }
+ deserializedRule
]);
// Assert
Assert.Single(matching.Data);
}
- [Fact]
- public void GetMatchingRules_CaluculatedCollectionNotContainsAnyOfNotMatch_RuleNotReturned()
- {
- // Arrange
- var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
- // Act
- var matching = engine.GetMatchingRules(
- new TestModel { CompositeCollection = [new TestModel.CompositeInnerClass { NumericField = 10 }] },
- [
- new RulesConfig {
- Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
- RulesGroups = [
- new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.Or,
- Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.CollectionNotContainsAnyOf, ComparisonValue = "10|11|12", ComparisonPredicate = $"{nameof(TestModel.CaluculatedCollection)}"}
- ]
- }
- ]
- }
- ]);
-
- // Assert
- Assert.Empty(matching.Data);
- }
- [Fact]
- public void GetMatchingRules_KeyValueCollectionMatch_RuleReturned()
+ // Then include all the original tests, updated to use RulesGroup.Operator instead of RulesOperator
+ [Theory]
+ [InlineData(5, ComparisonOperatorType.Equal, 5, true)]
+ [InlineData(5, ComparisonOperatorType.LessThan, 4, true)]
+ [InlineData(5, ComparisonOperatorType.LessThan, 6, false)]
+ [InlineData(5, ComparisonOperatorType.GreaterThan, 6, true)]
+ public void GetMatchingRules_NumericValueMatch_ShouldMatchByOperatorAndValue(int ruleVal, ComparisonOperatorType op, int objectVal, bool shouldMatch)
{
// Arrange
- var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
+ var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), _logger);
// Act
+ var numericValueTest = objectVal;
var matching = engine.GetMatchingRules(
- new TestModel { KeyValueCollection = new Dictionary { { "DateOfBirth", DateTime.Now } } },
+ new TestModel { NumericField = numericValueTest },
[
new RulesConfig {
- Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
+ Id = Guid.NewGuid(),
+ RulesOperator = InternalRuleOperatorType.And,
RulesGroups = [
new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.Or,
+ Operator = InternalRuleOperatorType.And,
Rules = [
- new Rule
- {
- ComparisonOperator = Rule.ComparisonOperatorType.GreaterThan,
- ComparisonValue = DateTime.Now.AddSeconds(-2).ToString("o"),
- ComparisonPredicate = $"{nameof(TestModel.KeyValueCollection)}[DateOfBirth]",
- PredicateType = TypeCode.DateTime
- }
+ new Rule { ComparisonOperator = op, ComparisonValue = ruleVal.ToString(), ComparisonPredicate = nameof(TestModel.NumericField) }
]
}
]
}
]);
- // Assert
- Assert.Single(matching.Data);
+ // Assert
+ if (shouldMatch)
+ {
+ Assert.Single(matching.Data);
+ }
+ else
+ {
+ Assert.Empty(matching.Data);
+ }
}
[Fact]
- public void GetMatchingRules_KeyValueCollectionNotMatch_RuleReturned()
+ public void GetMatchingRules_NumericValueNotMatch_RuleNotReturned()
{
// Arrange
- var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
+ IRulesService engine = RulesService.CreateDefault();
// Act
+ var numericValueTest = 5;
+ var numericValueOtherValue = 6;
var matching = engine.GetMatchingRules(
- new TestModel { KeyValueCollection = new Dictionary { { "DateOfBirth", DateTime.Now } } },
+ new TestModel { NumericField = numericValueTest },
[
new RulesConfig {
Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
+ RulesOperator = InternalRuleOperatorType.And,
RulesGroups = [
new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.Or,
+ Operator = InternalRuleOperatorType.And,
Rules = [
- // PredicateType is needed here to be able to determine value type which is string
- new Rule
- {
- ComparisonOperator = Rule.ComparisonOperatorType.LessThan,
- ComparisonValue = DateTime.Now.AddMinutes(-5).ToString("o"),
- ComparisonPredicate = $"{nameof(TestModel.KeyValueCollection)}[DateOfBirth]" ,
- PredicateType = TypeCode.DateTime
- }
- ]
- }
- ]
+ new Rule { ComparisonOperator = ComparisonOperatorType.Equal, ComparisonValue = numericValueOtherValue.ToString(), ComparisonPredicate = nameof(TestModel.NumericField) }
+ ]
+ }
+ ]
}
]);
@@ -410,23 +393,23 @@ public void GetMatchingRules_KeyValueCollectionNotMatch_RuleReturned()
}
[Fact]
- public void GetMatchingRules_EnumValueMatch_RuleReturned()
+ public void GetMatchingRules_StringStartsWithMatch_RuleReturned()
{
// Arrange
var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
// Act
var matching = engine.GetMatchingRules(
- new TestModel { SomeEnumValue = TestModel.SomeEnum.Yes },
+ new TestModel { TextField = "SomePrefixBlahBlah" },
[
new RulesConfig {
- Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
+ Id = Guid.NewGuid(),
+ RulesOperator = InternalRuleOperatorType.And,
RulesGroups = [
new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.Or,
+ Operator = InternalRuleOperatorType.And,
Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.Equal, ComparisonValue = TestModel.SomeEnum.Yes.ToString(), ComparisonPredicate = $"{nameof(TestModel.SomeEnumValue)}"}
+ new Rule { ComparisonOperator = ComparisonOperatorType.StringStartsWith, ComparisonValue = "someprefix", ComparisonPredicate = nameof(TestModel.TextField) }
]
}
]
@@ -438,23 +421,24 @@ public void GetMatchingRules_EnumValueMatch_RuleReturned()
}
[Fact]
- public void GetMatchingRules_PrimitiveInCollectionMatch_RuleReturned()
+ public void GetMatchingRules_MultiRuleAndMatch_RuleReturned()
{
// Arrange
var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
// Act
var matching = engine.GetMatchingRules(
- new TestModel { NumericField = 3 },
+ new TestModel { TextField = "SomePrefixBlahBlah", NumericField = 10 },
[
new RulesConfig {
Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
+ RulesOperator = InternalRuleOperatorType.And,
RulesGroups = [
new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.Or,
+ Operator = InternalRuleOperatorType.And,
Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.In, ComparisonValue ="1|2|3|4|5", ComparisonPredicate = $"{nameof(TestModel.NumericField)}"}
+ new Rule { ComparisonOperator = ComparisonOperatorType.StringStartsWith, ComparisonValue = "someprefix", ComparisonPredicate = nameof(TestModel.TextField) },
+ new Rule { ComparisonOperator = ComparisonOperatorType.GreaterThan, ComparisonValue = 4.ToString(), ComparisonPredicate = nameof(TestModel.NumericField) }
]
}
]
@@ -466,23 +450,23 @@ public void GetMatchingRules_PrimitiveInCollectionMatch_RuleReturned()
}
[Fact]
- public void GetMatchingRules_PrimitiveNotInCollectionMatch_RuleReturned()
+ public void GetMatchingRules_CaluculatedCollectionContainsAnyOfMatch_RuleReturned()
{
// Arrange
var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
// Act
var matching = engine.GetMatchingRules(
- new TestModel { NumericField = 10 },
+ new TestModel { CompositeCollection = [new TestModel.CompositeInnerClass { NumericField = 10 }] },
[
new RulesConfig {
Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
+ RulesOperator = InternalRuleOperatorType.And,
RulesGroups = [
new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.Or,
+ Operator = InternalRuleOperatorType.Or,
Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.In, ComparisonValue ="1|2|3|4|5", ComparisonPredicate = $"{nameof(TestModel.NumericField)}"}
+ new Rule { ComparisonOperator = ComparisonOperatorType.CollectionContainsAnyOf, ComparisonValue = "10|11|12", ComparisonPredicate = $"{nameof(TestModel.CaluculatedCollection)}"}
]
}
]
@@ -490,40 +474,27 @@ public void GetMatchingRules_PrimitiveNotInCollectionMatch_RuleReturned()
]);
// Assert
- Assert.Empty(matching.Data);
+ Assert.Single(matching.Data);
}
-
[Fact]
- public void GetMatchingRules_MultiRulesAllMatch_AllRulesReturned()
+ public void GetMatchingRules_CaluculatedCollectionNotContainsAnyOfNotMatch_RuleNotReturned()
{
// Arrange
var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
// Act
var matching = engine.GetMatchingRules(
- new TestModel { NumericField = 10, TextField = "test1" },
+ new TestModel { CompositeCollection = [new TestModel.CompositeInnerClass { NumericField = 10 }] },
[
new RulesConfig {
Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
+ RulesOperator = InternalRuleOperatorType.And,
RulesGroups = [
new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.Or,
+ Operator = InternalRuleOperatorType.Or,
Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.NotIn, ComparisonValue ="1|2|3|4|5", ComparisonPredicate = $"{nameof(TestModel.NumericField)}"}
- ]
- }
- ]
- },
- new RulesConfig {
- Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
- RulesGroups = [
- new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.Or,
- Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.StringEndsWith, ComparisonValue ="1", ComparisonPredicate = $"{nameof(TestModel.TextField)}"}
+ new Rule { ComparisonOperator = ComparisonOperatorType.CollectionNotContainsAnyOf, ComparisonValue = "10|11|12", ComparisonPredicate = $"{nameof(TestModel.CaluculatedCollection)}"}
]
}
]
@@ -531,11 +502,11 @@ public void GetMatchingRules_MultiRulesAllMatch_AllRulesReturned()
]);
// Assert
- Assert.Equal(2, matching.Data.Count());
+ Assert.Empty(matching.Data);
}
[Fact]
- public void GetMatchingRulesPrimitiveCollectionMatch_RuleReturned()
+ public void GetMatchingRules_PrimitiveCollectionMatch_RuleReturned()
{
// Arrange
var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
@@ -546,12 +517,12 @@ public void GetMatchingRulesPrimitiveCollectionMatch_RuleReturned()
[
new RulesConfig {
Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
+ RulesOperator = InternalRuleOperatorType.And,
RulesGroups = [
new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.Or,
+ Operator = InternalRuleOperatorType.Or,
Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.CollectionContainsAll, ComparisonValue ="1|2", ComparisonPredicate = $"{nameof(TestModel.PrimitivesCollection)}"}
+ new Rule { ComparisonOperator = ComparisonOperatorType.CollectionContainsAll, ComparisonValue = "1|2", ComparisonPredicate = $"{nameof(TestModel.PrimitivesCollection)}"}
]
}
]
@@ -562,9 +533,8 @@ public void GetMatchingRulesPrimitiveCollectionMatch_RuleReturned()
Assert.Single(matching.Data);
}
-
[Fact]
- public void GetMatchingRulesPrimitiveCollectionNoMatch_RuleNotReturned()
+ public void GetMatchingRules_PrimitiveCollectionNoMatch_RuleNotReturned()
{
// Arrange
var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
@@ -575,12 +545,12 @@ public void GetMatchingRulesPrimitiveCollectionNoMatch_RuleNotReturned()
[
new RulesConfig {
Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
+ RulesOperator = InternalRuleOperatorType.And,
RulesGroups = [
new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.Or,
+ Operator = InternalRuleOperatorType.Or,
Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.CollectionContainsAll, ComparisonValue ="1|2|10", ComparisonPredicate = $"{nameof(TestModel.PrimitivesCollection)}"}
+ new Rule { ComparisonOperator = ComparisonOperatorType.CollectionContainsAll, ComparisonValue = "1|2|10", ComparisonPredicate = $"{nameof(TestModel.PrimitivesCollection)}"}
]
}
]
@@ -591,9 +561,8 @@ public void GetMatchingRulesPrimitiveCollectionNoMatch_RuleNotReturned()
Assert.Empty(matching.Data);
}
-
[Fact]
- public void GetMatchingRulesPrimitiveCollectionNoMatch_ComparisonPredicateNameAttribute_RuleReturned()
+ public void GetMatchingRules_ComparisonPredicateNameAttribute_RuleReturned()
{
// Arrange
var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
@@ -609,15 +578,15 @@ public void GetMatchingRulesPrimitiveCollectionNoMatch_ComparisonPredicateNameAt
[
new RulesConfig {
Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
+ RulesOperator = InternalRuleOperatorType.And,
RulesGroups = [
new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.And,
+ Operator = InternalRuleOperatorType.And,
Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.StringEqualsCaseInsensitive, ComparisonValue ="john", ComparisonPredicate = "first_name"},
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.Equal, ComparisonValue ="123456789", ComparisonPredicate = "userDetails[SSN]"},
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.Equal, ComparisonValue ="over the rainbow", ComparisonPredicate = "userAddress.home_address"},
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.Equal, ComparisonValue ="1st", ComparisonPredicate = "userAddress.StreetAddress"}
+ new Rule { ComparisonOperator = ComparisonOperatorType.StringEqualsCaseInsensitive, ComparisonValue = "john", ComparisonPredicate = "first_name"},
+ new Rule { ComparisonOperator = ComparisonOperatorType.Equal, ComparisonValue = "123456789", ComparisonPredicate = "userDetails[SSN]"},
+ new Rule { ComparisonOperator = ComparisonOperatorType.Equal, ComparisonValue = "over the rainbow", ComparisonPredicate = "userAddress.home_address"},
+ new Rule { ComparisonOperator = ComparisonOperatorType.Equal, ComparisonValue = "1st", ComparisonPredicate = "userAddress.StreetAddress"}
]
}
]
@@ -628,9 +597,8 @@ public void GetMatchingRulesPrimitiveCollectionNoMatch_ComparisonPredicateNameAt
Assert.Single(matching.Data);
}
-
[Fact]
- public void GetMatchingRulesP_FIrstMatchingRule_RuleReturned()
+ public void GetMatchingRules_FirstMatchingRule_RuleReturned()
{
// Arrange
var engine = new RulesService(new RulesCompiler(), new LazyCache.Mocks.MockCachingService(), NullLogger.Instance);
@@ -644,24 +612,24 @@ public void GetMatchingRulesP_FIrstMatchingRule_RuleReturned()
[
new RulesConfig {
Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
+ RulesOperator = InternalRuleOperatorType.And,
RulesGroups = [
new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.And,
+ Operator = InternalRuleOperatorType.And,
Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.StringEqualsCaseInsensitive, ComparisonValue ="john", ComparisonPredicate = "first_name"},
+ new Rule { ComparisonOperator = ComparisonOperatorType.StringEqualsCaseInsensitive, ComparisonValue = "john", ComparisonPredicate = "first_name"}
]
}
]
},
new RulesConfig {
Id = Guid.NewGuid(),
- RulesOperator = Rule.InterRuleOperatorType.And,
+ RulesOperator = InternalRuleOperatorType.And,
RulesGroups = [
new RulesGroup {
- RulesOperator = Rule.InterRuleOperatorType.And,
+ Operator = InternalRuleOperatorType.And,
Rules = [
- new Rule { ComparisonOperator = Rule.ComparisonOperatorType.StringContains, ComparisonValue ="jo", ComparisonPredicate = "first_name"},
+ new Rule { ComparisonOperator = ComparisonOperatorType.StringContains, ComparisonValue = "jo", ComparisonPredicate = "first_name"}
]
}
]
@@ -672,6 +640,7 @@ public void GetMatchingRulesP_FIrstMatchingRule_RuleReturned()
Assert.Single(matching.Data);
}
}
+
public class TestModelWithRulePredicatePropertyAttribute : TestModel
{
[RulePredicateProperty("first_name")]
diff --git a/tests/NetRuleEngineTests/TestModel.cs b/tests/NetRuleEngineTests/TestModel.cs
index 8f1fdbf..a239acd 100644
--- a/tests/NetRuleEngineTests/TestModel.cs
+++ b/tests/NetRuleEngineTests/TestModel.cs
@@ -1,36 +1,29 @@
-using NetRuleEngine.Domains;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
namespace NetRuleEngineTests
{
public class TestModel
{
+ public int NumericField { get; set; }
+ public string TextField { get; set; }
+ public CompositeInnerClass Composit { get; set; }
+ public IEnumerable CompositeCollection { get; set; }
+ public IEnumerable PrimitivesCollection { get; set; }
+ public Dictionary KeyValueCollection { get; set; }
+ public SomeEnum SomeEnumValue { get; set; }
+
+ public IEnumerable CaluculatedCollection => CompositeCollection?.Select(x => x.NumericField);
+
public class CompositeInnerClass
{
public int NumericField { get; set; }
-
- public string TextField { get; set; }
}
public enum SomeEnum
{
Yes,
- No,
- Maybe
+ No
}
- public CompositeInnerClass Composit { get; set; }
- public int NumericField { get; set; }
-
- public string TextField { get; set; }
-
- public List PrimitivesCollection { get; set; }
-
- public Dictionary KeyValueCollection { get; set; }
-
- public List CompositeCollection { get; set; }
- public SomeEnum SomeEnumValue { get; set; }
-
- public IEnumerable CaluculatedCollection => CompositeCollection.Select(c => c.NumericField);
}
}