version 4.0.0
The logic-engine is a simple dotnet library to help introduce flexible logic systems.
It supports a generic set of rules that get compiled into executable code, allowing the possibility to dynamically change your business logic and adapt it to different needs without changing the core of your system.
The library deeply uses a functional programming approach implemented using Franco Melandri’s amazing Tiny FP library.
The core functionalities are encapsulated in different components, both logical and functional.
The core system offers the possibility to immediately evaluate whether a an entity satisfies the conditions imposed by a logical system, but it also permits, in case of failure, to identify the underlying reasons1.
The rule object represents the building block for the system. A rule is an abstraction for a function acting on the value of a type and returning a boolean response.
DEFINITION: A
Rule
is satisfied by an itemt
of typeT
if the associated functionf: T ──► bool
returns true iff(t)
istrue
.
Given a type to be applied to, a rule is defined by a set of fields
Property
: identifies the property against which to execute the evaluationOperator
: defines the operation to execute on the propertyValue
: identifies the value against which compare the result of the operator on the propertyCode
: the error code to be generated when the rules applied on an object fails (returns false
)The Operator
can assume different possible values depending on the Property
it is applied to and on the value, the result should be compared to.
Operators are classified based on the way they work and their behavior. The rules categorization is also influenced by some implementation details.
These operators directly compare the Property
to the Value
considered as a constant:
Equal
: equality on value types (strings, numbers, …)NotEqual
: inequality on value types (strings, numbers, …)GreaterThan
: only applies to numbersGreaterThanOrEqual
: only applies to numbersLessThan
: only applies to numbersLessThanOrEqual
: only applies to numberspublic class MyClass
{
public string StringProperty {get; set;}
public int IntegerProperty {get; set;}
}
var stringRule = new Rule("StringProperty", OperatorType.Equal, "Some Value", "code 1");
var integerRule = new Rule("IntegerProperty", OperatorType.Equal, "10", "code 2");
var myObj = new MyClass
{
StringProperty = "Some Value",
IntegerProperty = 11
}
var result1 = stringRule.Apply(myObj); // returns true
var result2 = integerRule.Apply(myObj); // returns false
sample rules with direct operators
Internal direct rules are similar to direct rules, but they are meant to be applied to values that are other fields of the same type; in this case, Value
should correspond to the name of another field in the analyzed type:
InnerEqual
: equality between two value typed fieldsInnerNotEqual
: equality between two value typed fieldsInnerGreaterThan
: only applies when Property
and Value
are numbersInnerGreaterThanOrEqual
: only applies when Property
and Value
are numbersInnerLessThan
: only applies when Property
and Value
are numbersInnerLessThanOrEqual
: only applies when Property
and Value
are numberspublic class MyClass
{
public string StringProperty1 {get; set;}
public string StringProperty2 {get; set;}
public int IntegerProperty1 {get; set;}
public int IntegerProperty2 {get; set;}
}
var stringRule = new Rule("StringProperty1", OperatorType.InnerEqual, "StringProperty2", "code 1");
var integerRule = new Rule("IntegerProperty1", OperatorType.InnerGreaterThan, "IntegerProperty2", "code 2");
sample rules with internal direct operators
These rules are specific for strings:
StringStartsWith
: checks that the string in Property
starts with Value
StringEndsWith
: checks that the string in Property
ends with Value
StringContains
: checks that the string in Property
contains Value
StringRegexIsMatch
: checks that the string in Property
matches Value
public class MyClass
{
public string StringProperty {get; set;}
}
var stringRule = new Rule("StringProperty", OperatorType.StringStartsWith, "start", "code 1");
sample rule with string direct operator
These rules apply to operand of generic enumerable type:
Contains
: checks that Property
contains Value
NotContains
: checks that Property
does not Value
Overlaps
: checks that Property
has a non empty intersection with Value
NotOverlaps
: checks that Property
has an empty intersection with Value
public class MyClass
{
public IEnumerable<string> StringEnumerableProperty {get; set;}
}
var rule1 = new Rule("StringEnumerableProperty", OperatorType.Contains, "value", "code 1");
var rule2 = new Rule("StringEnumerableProperty", OperatorType.Overlaps, "value1,value2", "code 2");
sample rules with enumerable operators
These operators act on enumerable fields by comparing them against fields of the same type:
InnerContains
: checks that Property
contains the value contained in the property Value
InnerNotContains
: checks that Property
doesn’t contain the value contained in the property Value
InnerOverlaps
: checks that Property
has a non empty intersection with the value contained in the property Value
InnerNotOverlaps
: checks that Property
has an empty intersection with the value contained in the property Value
public class MyClass
{
public IEnumerable<int> EnumerableProperty1 {get; set;}
public IEnumerable<int> EnumerableProperty2 {get; set;}
public int IntegerField {get; set;}
}
var rule1 = new Rule("EnumerableProperty1", OperatorType.InnerContains, "IntegerField");
var rule2 = new Rule("EnumerableProperty1", OperatorType.InnerOverlaps, "EnumerableProperty2");
sample rules for internal enumerable operators
These operators act on dictionary-like objects:
ContainsKey
: checks that the Property
contains the specific key defined by the Value
NotContainsKey
: checks that the Property
doesn’t contain the specific key defined by the Value
ContainsValue
: checks that the dictionary Property
contains a value defined by the Value
NotContainsValue
: checks that the dictionary Property
doesn’t contain a value defined by the Value
KeyContainsValue
: checks that the dictionary Property
has a key with a specific valueNotKeyContainsValue
: checks that the dictionary Property
doesn’t have a key with a specific valuepublic class MyClass
{
public IDictionary<string, int> DictProperty {get; set;}
}
var rule1 = new Rule("DictProperty", OperatorType.ContainsKey, "mykey");
var rule2 = new Rule("DictProperty", OperatorType.KeyContainsValue, "mykey[myvalue]");
sample rules for key-value enumerable operators
These rules apply to scalars against enumerable fields:
IsContained
: checks that Property
is contained in a specific setIsNotContained
: checks that Property
is not contained in a specific setpublic class MyClass
{
public int IntProperty {get; set;}
}
var rule1 = new Rule("IntProperty", OperatorType.IsContained, "1,2,3");
sample rules for inverse enumerable operators
A RulesSet
is basically a set of rules. From a functional point of view it represents a boolean typed function composed by a set of functions on a given type.
DEFINITION: A
RulesSet
is satisfied by an itemt
of typeT
if all the functions of the set are satisfied byt
.
A RulesSet
corresponds to the logical AND
operator on its rules.
A RulesCatalog
represents a set of RulesSet
, and functionally corresponds to a boolean typed function composed by a set of sets of functions on a given type.
DEFINITION: A
RulesCatalog
is satisfied by an itemt
of typeT
if at least one of itsRulesSet
s is satisfied byt
.
A RulesCatalog
corresponds to the logical OR
operator on its RulesSet
s.
As discussed above, composite types RulesSet
and RulesCatalog
represent logical operations on the field of functions f: T ──► bool
; it seems than possible to define an algebraic model defining the composition of different entities.
DEFINITION: The sum of two
RulesSet
s is a newRulesSet
, and its rules are a set of rules obtained by concatenating the rules of the the twoRulesSet
s
rs1 = {r1, r2, r3}
rs2 = {r4, r5}
──► rs1 * rs2 = {r1, r2, r3, r4, r5}
sum of two
RulesSet
s
The sum of two RulesCatalog
objects is a RulesCatalog
with a set of RulesSet
obtained by simply concatenating the two sets of RulesSet
:
c1 = {rs1, rs2, rs3}
c2 = {rs4, rs5}
──► c1 + c2 = {rs1, rs2, rs3, rs4, rs5}
sum of two
RulesCatalog
The product of two catalogs is a catalog with a set of all the RulesSet
obtained concatenating a set of the first catalog with one of the second.
c1 = {rs1, rs2, rs3}
c2 = {rs4, rs5}
──► c1 * c2 = {(rs1*rs4), (rs1*rs5), (rs2*rs4), (rs2*rs5), (rs3*rs4), (rs3*rs5)}
product of two
RulesCatalog
The RuleCompiler
is the component that parses and compiles a Rule
into executable code.
Every rule becomes an Option<CompiledRule<T>>
, where the None
status of the option corresponds to a Rule
that is not formally correct and hence cannot be compiled2.
A CompiledRule<T>
is the actual portion of code that can be applied to an item of type T
to provide a boolean result using its ApplyApply(T item)
method.
Sometimes the boolean result is not enough: when the rule is not satisfied it could be useful to understand the reason why it failed. For this reason, a dedicated Either<string, Unit> DetailedApply(T item)
method returns Unit
when the rule is satisfied, or a string (the rule code) in case of failure.
Like the RuleCompiler
, the RulesSetCompiler
transforms a RulesSet
into an Option<CompiledRulesSet<T>>
.
A CompiledRulesSet<T>
can logically be seen as a set of compiled rules, hence, when applied to an item of type T
it returns a boolean that is true
if all the compiled rules return true
on it. From a logical point of view, a CompiledRulesSet<T>
represents the AND
superposition of its CompiledRule<T>
.
The corresponding Either<string, Unit> DetailedApply(T item)
method of the CompiledRulesSet<T>
returns Unit
when all the rules are satisfied, or the set of codes for the rules that are not.
Finally, the RulesCatalogCompiler
transforms a RulesCatalog
into an Option<CompiledCatalog<T>>
, where the None
status represents a catalog that cannot be compiled.
A CompiledCatalog<T>
logically represents the executable code that applies a set of rule sets to an object of type T
: the result of its application can be true
if at least one set of rules returns true
, otherwise false
(this represents the logical OR
composition operations on rules joined by a logical AND
).
Similar to the Either<string, Unit> DetailedApply(T item)
of the CompiledRulesSet<T>
, it can return Unit
when at least one internal rule set returns Unit
, otherwise the flattened set of all the codes for all the rules that don’t successfully apply.
The current implementation of the rules system has some limitations:
If you want to upgrade from a version < 3.0.0 to the latest version you will need to adapt your implementation to manage the breaking changes introduced.
The main differences can be condensed in the removal of the managers: the entire logic is now completely captured by the compiled objects CompiledRule<T>
, CompiledRulesSet<T>
, CompiledCatalog<T>
, without the need of external wrappers.
This means that the typical workflow to update the library requires:
If you are using nuget.org
you can add the dependency in your project using
dotnet add package logic-engine --version <version>
To install the logic-engine library from GitHub’s packages system please refer to the packages page.