A simple rules engine written in C#
Regla has 3 core Module
- Rule Module
- Engine Module
- Result Module
Rule encapsulates a method to call and its attributes
- Rules can be given names to identify them
- Rules can be part of a group
- Rules can specify whether to stop on failure or exception
- Rules can be composed of AndRules or OrRules for short circuiting
RuleMethod can be provided in 3 forms
- A delegate function
- A lambda expression
- A class inherited from Rule class
Class Name | Description |
---|---|
RuleMethod | Encapsulation of a delegate or Func<> |
RuleAttributes | Various options for the rule |
Rule | Class which hold RuleMethod and RuleAttributes |
AndRule | Rule short-circuiting until a rule fails |
OrRule | Rule short-circuiting until a rule succeed |
Class Name | Description |
---|---|
EngineAttributes | Various options for the Rules Engine |
RulesEngine | Holds EngineAttributes and Rules List |
Class Name | Description |
---|---|
RunResultAttribute | Result of Rules execution |
RuleResultAttribute | Result of individual rule execution |
Result | Holds RunResult and RuleResult Attribute classes |
Pass EngineAttributes as constructor parameter or set properties after instantiating
Either create Rules before instantiating the engine and pass them as constructor parameter or use AddRule() functions to add them after creating the engine
The Engine can run in different modes e.g. RunAllRules, RunNamedRules, RunGroupRules.
- Create an empty RuleEngine
- Add a Rule (we are using Lambda here)
- Run the rules
- Inspect Result graph
class RulesEngineDemo
{
static void BasicExample()
{
// Create an empty RulesEngine
var myRules = new RulesEngine();
// Add a lambda rule to the engine
myRules.AddRule(new Rule((component, output) => { Console.WriteLine("Hello, world"); return true; }));
// Run the engine, it will print Hello, world
var result = myRules.RunAllRules();
// Hello, world
// Result class contains detailed object graph, which can be accessed thru properties
// It's ToString() returns a Json string
Console.WriteLine(result);
}
public static void Main()
{
BasicExample();
}
}
/* The output
Hello, world
"Result": {
"EngineAttributes": {
"Name": "Engine_1",
"StopOnException": true,
"StopOnRuleFailure": false,
"Component": null,
"Output": null
},
"RunResultAttributes": {
"TotalRulesCount": 1,
"RulesExecutedCount": 1,
"ExecutionType": "All",
"FailedRuleName": null,
"FailureReason": null
},
"RuleResultAttributes": [
{
"Rule": {
"RuleMethod": "<BasicExample>b__3_0",
"RuleAttributes": {
"Name": "<BasicExample>b__3_0",
"Group": "default",
"Enabled": true,
"StopOnException": true,
"StopOnRuleFailure": false
}
},
"ReturnValue": true,
"Exception": null
}
]
}*/
While the output looks very detailed, we don't have to worry about it yet. The Result object graph can be used to analyze and debug our RulesEngine
Rules can be delegates, Func<>, lambda or even classes
class RulesEngineDemo
{
// Define a rule delegate method
static bool ruleDelegate(object component, object output)
{
Console.WriteLine("ruleDelegate called");
return false;
}
static void DelegateExample()
{
// Create EngineAttributes with Name
var engineOptions = new EngineAttributes { Name = "MyEngine" };
// Instantiate the RulesEngine with option
var myEngine = new RulesEngine(engineOptions);
// Now add Lambda rule
myEngine.AddRule(new Rule((c, o) => { Console.WriteLine("Hello, world"); return true; }));
// Next, add a delegate method
myEngine.AddRule(new Rule(ruleDelegate));
myEngine.RunAllRules();
// Output:
// Hello, world
// ruleDelegate called
}
public static void Main()
{
DelegateExample();
}
}
As we have seen, we can easily pass a method or lambda to the Rules Engine, but to do complex processing, we may require a class with parameters and multiple functions
class RulesEngineDemo
{
/**
* Sample Component for RulesEngine to work on
*/
class Customer
{
public int ID { get; set; }
public string Name { get; set; }
public DateTime Birthdate { get; set; }
public DateTime FirstPurchase { get; set; }
}
/**
* Sample "Output" object, which rules can use for writing data
*/
class CustomerDiscount
{
public decimal Discount { get; set; };
}
/**
* Rules can be written as methods, lambdas or even classes
* Class rules allow us to pass constructor params, which can be used later
* e.g.
* engine.AddRule(new LoyaltyDiscount(1, 0.05));
* engine.AddRule(new LoyaltyDiscount(5, 0.08);
* engine.AddRule(new LoyaltyDiscount(10, 0.10));
*/
public class LoyaltyDiscount : Rule
{
public int Years { get; private set; }
public decimal Discount { get; private set; }
// We can set RuleAttributes by calling base Rule constructor
public LoyaltyDiscount(int years, decimal discount)
{
// We can also set the RulesAttributes inside the constructor
base.RuleMethod = CalculateDiscount;
Years = years;
Discount = discount;
}
/**
* The actual rule can be called anything, but must have matching signature
*/
public bool CalculateDiscount(object input, object output)
{
var customer = (Customer)input;
var customerDiscount = (CustomerDiscount)output;
if ((DateTime.Today - customer.FirstPurchase).Days / 365 > Years)
customerDiscount.Discount = Math.Max(customerDiscount.Discount, 50);
return true;
}
}
static void RuleClassExample()
{
/**
* Create the component to initialize the RulesEngine
* We don't HAVE to have a component, the rules can still run
*/
var bigB = new Customer
{
ID = 1,
Name = "Amitabh",
Birthdate = new DateTime(1942, 10, 11),
FirstPurchase = new DateTime(2005, 1, 2),
};
/**
* Create an output object for Rules Engine
* We don't HAVE to have an output object
*/
var discount = new CustomerDiscount();
// Create Engine Attributes
var engineAttributes = new EngineAttributes { Component = bigB, Output = discount };
// We can add individual rules or an array of rules
var rulesArray = new Rule[] { new Rule(seniorCitizenDiscount, ruleName: "Senior") };
// Start the engine with attributes and initial rules list
var engine = new RulesEngine(engineAttributes, rulesArray);
// We are adding multiple rules based on the same class, so we must name them explicitly
engine.AddRule(new Rule(new LoyaltyDiscount(1, 5), ruleName: "L1"));
engine.AddRule(new Rule(new LoyaltyDiscount(5, 8), ruleName: "L2"));
engine.AddRule(new Rule(new LoyaltyDiscount(10, 10), ruleName: "L3"));
Console.WriteLine(engine.RunAllRules());
// Now print the discount
Console.WriteLine(discount);
}
}
public static void Main()
{
RuleClassExample();
}
}
EngineAttributes has many options to control the engine
Attribute | Description |
---|---|
Name | User defined name for the engine |
Component | Object on which the rules will execute |
Output | Object will can be used for rules to store values |
StopOnException | Stop rules processing if execution is throw (default: true) |
StopOnRuleFailure | Stop rules processing if a the current rule returns false (default: false) |
Rule encapsulates the user provide method (or class object) and holds its attributes A rule can have a user defined name and/or a group, which can be used to run named rules or run group rules. These names can also be used for removing rules dynamically
:
:
// Define a new rule using a delegate method
var seniorRule = new Rule(seniorCitizenDiscount, name:"senior", group:"discount", stopOnException:true, stopOnRuleFailure:true);
// Add another rule for birthday
var birthdayRule = new Rule(birthdayDiscount, group:"discount");
// Add a rule for seasonal discount (not part of group:discount)
var seasonalDiscount = new Rule(new SeasonDiscount(), group:"seasonal"));
// Instantiate the engine with component and output
var engine=new RulesEngine(component:customer, output:discount, name:"CustomerRule");
// Run only those rules which are part of "discount" group
var groupResult = engine.RunGroupRules("discount");
// Check the discount here
Console.WriteLine("Customer: {0} got {1} discount", customer.Name, discount);
// We can also run named rules
var namedResult = engine.RunNamedRules(new string[] {"senior", "seasonal"});
// We can remove individual rules or grouped rules
engine.RemoveRule("senior"); // By name
engine.RemoveRule(birthdayDiscount); // By delegate
engine.RemoveRule(seasonalDiscount); // By Rule reference
engine.RemoveGroup("discount"); // By Group name
// Or we can clear all rules
engine.clearRules();
Regla is WIP (Work In Progress) and the architecture and implementation will change with feedback. You may use this code to understand the architecture and implementation but are strongly advised against using this library for any production work.