logic-engine
4.0.0
dotnet add package logic-engine --version 4.0.0
NuGet\Install-Package logic-engine -Version 4.0.0
<PackageReference Include="logic-engine" Version="4.0.0" />
paket add logic-engine --version 4.0.0
#r "nuget: logic-engine, 4.0.0"
// Install logic-engine as a Cake Addin #addin nuget:?package=logic-engine&version=4.0.0 // Install logic-engine as a Cake Tool #tool nuget:?package=logic-engine&version=4.0.0
This documentation is in line with the active development, hence should be considered work in progress. To check the documentation for the latest stable version please visit https://fabiolune.github.io/logic-engine/
Logic Engine
Table of contents
- Introduction
- The Rule
- The Operators
- The RulesSets
- The RulesCatalog
- The Algebraic model
- Compilers and compiled objects
- Known limitations
- Breaking changes ⚠️
- How to install the package
Introduction
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 reasons[^0].
The Rule
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 (returnsfalse
)
The Operators
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.
Direct operators
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 numbers
public 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 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 whenProperty
andValue
are numbersInnerGreaterThanOrEqual
: only applies whenProperty
andValue
are numbersInnerLessThan
: only applies whenProperty
andValue
are numbersInnerLessThanOrEqual
: only applies whenProperty
andValue
are numbers
public 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
String direct operators
These rules are specific for strings:
StringStartsWith
: checks that the string inProperty
starts withValue
StringEndsWith
: checks that the string inProperty
ends withValue
StringContains
: checks that the string inProperty
containsValue
StringRegexIsMatch
: checks that the string inProperty
matchesValue
public class MyClass
{
public string StringProperty {get; set;}
}
var stringRule = new Rule("StringProperty", OperatorType.StringStartsWith, "start", "code 1");
sample rule with string direct operator
Enumerable operators
These rules apply to operand of generic enumerable type:
Contains
: checks thatProperty
containsValue
NotContains
: checks thatProperty
does notValue
Overlaps
: checks thatProperty
has a non empty intersection withValue
NotOverlaps
: checks thatProperty
has an empty intersection withValue
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
Internal enumerable operators
These operators act on enumerable fields by comparing them against fields of the same type:
InnerContains
: checks thatProperty
contains the value contained in the propertyValue
InnerNotContains
: checks thatProperty
doesn't contain the value contained in the propertyValue
InnerOverlaps
: checks thatProperty
has a non empty intersection with the value contained in the propertyValue
InnerNotOverlaps
: checks thatProperty
has an empty intersection with the value contained in the propertyValue
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
Key-value operators
These operators act on dictionary-like objects:
ContainsKey
: checks that theProperty
contains the specific key defined by theValue
NotContainsKey
: checks that theProperty
doesn't contain the specific key defined by theValue
ContainsValue
: checks that the dictionaryProperty
contains a value defined by theValue
NotContainsValue
: checks that the dictionaryProperty
doesn't contain a value defined by theValue
KeyContainsValue
: checks that the dictionaryProperty
has a key with a specific valueNotKeyContainsValue
: checks that the dictionaryProperty
doesn't have a key with a specific value
public 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
Inverse enumerable operators
These rules apply to scalars against enumerable fields:
IsContained
: checks thatProperty
is contained in a specific setIsNotContained
: checks thatProperty
is not contained in a specific set
public class MyClass
{
public int IntProperty {get; set;}
}
var rule1 = new Rule("IntProperty", OperatorType.IsContained, "1,2,3");
sample rules for inverse enumerable operators
The RulesSets
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.
The RulesCatalog
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.
The Algebraic model
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.
RulesSets sum
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
RulesCatalog sum
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
RulesCatalog product
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
Compilers and compiled objects
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 compiled[^1].
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.
Known limitations
The current implementation of the rules system has some limitations:
- it is designed to work on plain objects (instances of classes, records or structures) with an empty constructor
- rules can only be applied to 'first level members', no nesting is currently supported
Breaking changes ⚠️
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:
- getting the rules definition
- pass them to the appropriate compiler
- use the generated compiled objects to validate your objects according to the rules definition
How to install the package
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.
[^0]: from a technical perspective this is obtained with a concrete implementation of the railway pattern
[^1]: null or empty codes are removed because they don't carry reusable info
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net8.0
- tiny-fp (>= 2.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
4.0.0 | 115 | 7/26/2024 |
3.1.0 | 365 | 8/24/2023 |
3.1.0-rc0004 | 140 | 8/14/2023 |
3.1.0-rc0003 | 131 | 8/12/2023 |
3.1.0-rc0002 | 136 | 8/11/2023 |
3.1.0-rc0001 | 128 | 8/10/2023 |
3.0.1 | 163 | 6/21/2023 |
3.0.0 | 180 | 6/20/2023 |
3.0.0-rc0001 | 129 | 6/20/2023 |
2.4.0 | 2,880 | 12/12/2022 |
2.3.1 | 450 | 3/17/2022 |
2.3.0 | 441 | 3/14/2022 |
2.2.2 | 442 | 3/9/2022 |
2.2.1 | 430 | 3/9/2022 |
2.2.0 | 420 | 3/8/2022 |
2.1.0 | 439 | 2/24/2022 |
2.0.0 | 440 | 2/20/2022 |
1.1.0 | 440 | 2/15/2022 |
1.0.0 | 497 | 2/14/2022 |