Let us start thinking about Rule Engine model.
Entity 1: Price Object
To keep it simple I should define a Price Object, on which whole calculation will be performed to evaluate Transaction Fee.
public class PriceObject
{
public decimal NetProductPrice { get; set; }
public int DRM { get; set; }
public decimal TransactionFee { get; set; }
}
This object could be any thing in your scenario, we very much like it feed to the Rule Engine. I would rather say it 'Communication Channel or DTO'. Rule Engine apply defined rules on Channel Entity and provide results (in my case calculate and fill up TransactionFee).
In some cases, it makes sense to have one input channel entity and rule engine return another channel entity.
In some cases, it makes sense to have one input channel entity and rule engine return another channel entity.
Entity 2: Rule Engine
To keep rule engine simple, at high level, it contains a list of rules defined in XML per distributor. When caller request to run the rule engine, it supplies distributor identification and price object in question. Rule engine just pick the rules defined in xml in top-down order and run each one of them.
public class RuleEngine
{
IDictionary<int, IRule> Rules = new Dictionary<int, IRule>();
public void AddRule(IRule rule)
{
Rules.Add(Rules.Count(), rule);
}
public void Run(string distributorKey, PriceObject obj)
{
var rules = Rules.Where(dr => dr.Value.DistributorName == distributorKey)
.OrderBy(kv=>kv.Key)
.Select(kv=>kv.Value)
.ToList();
foreach (var r in rules)
{
r.Run(obj);
}
}
}
Entity 3: Rule Definition
Each rule is based on interface IRule that defines:
- - IRule must know Distributor Name / identification whom it belongs to
- - Can add conditions as runtime delegate (Predicate) and logical operation to inference using forward chaining mechanism
- - Can add actions taken on Price Object type at runtime delegate (Custom one defined by me)
- - Run the rule on actual Price Object
public interface IRule
{
string DistributorName { get; }
void AddCondtion(Predicate<PriceObject> condition, LogicalOperation operation = LogicalOperation.AND);
void AddAction(ActionPredicate<PriceObject> action);
void Run(PriceObject obj);
}
Then comes IRule implementation named ConcreteRule. It encapsulates a list of Conditions and List of Actions. When running a rule, it uses forward chaining and infers the final result (Boolean true or false) after evaluating each condition. Once get a conditions result it apply simple rule:
IF conditions_result is true THEN execute each action sequentially END.
Here is simple code:
public class ConcreteRule : IRule
{
IList<ICondition> conditions = new List<ICondition>();
IList<IAction> actions = new List<IAction>();
public string DistributorName { get; private set; }
public ConcreteRule(string distributorName)
{
DistributorName = distributorName;
}
public void AddCondtion(Predicate<PriceObject> condition, LogicalOperation operation = LogicalOperation.AND)
{
conditions.Add(new RuleCondition(condition, operation));
}
public void AddAction(ActionPredicate<PriceObject> action)
{
actions.Add(new RuleAction(action));
}
public void Run(PriceObject obj)
{
bool result = true;
foreach (var c in conditions)
{
var res = c.Validate(obj);
if (c.Operation == LogicalOperation.AND)
{
result = result && res;
}
else
{
result = result || res;
}
}
if (result)
{
foreach (var a in actions)
{
a.Execute(obj);
}
}
}
}
Entity 3: Condition Definition
Each condition is based on interface ICondition that defines:
- - ICondition must know Logical Operator to infer final flag (forward chaining)
- - ICondition can operate on actual Price Object for validation
public interface ICondition
{
LogicalOperation Operation { get; }
bool Validate(PriceObject obj);
}
And here is concrete Condition Implementation class named RuleCondition. You should focus on Validate method how simple it executes the delegate?
public class RuleCondition : ICondition
{
readonly Predicate<PriceObject> Conditions;
public LogicalOperation Operation
{
get;
private set;
}
public RuleCondition(Predicate<PriceObject> conditions, LogicalOperation operation)
{
this.Conditions = conditions;
this.Operation = operation;
}
public bool Validate(PriceObject priceObject)
{
return Conditions(priceObject);
}
}
Entity 4: Action Definition
Each action is based on interface IAction with simple concept:
- - IAction can execute a command on Price Object at runtime.
Just I need a custom delegate named Action Predicate to return decimal. I cannot go more generic as I always deal with Transaction Fee calculation so limit it to return decimal value. Note T parameter in constructed type is contravariant.
public delegate decimal ActionPredicate<in T>(T obj);
public interface IAction
{
decimal Execute(PriceObject obj);
}
And here is concrete Action Implementation class named RuleAction. You should remember executes method and the delegate usage.
public class RuleAction : IAction
{
readonly ActionPredicate<PriceObject> Action;
public RuleAction(ActionPredicate<PriceObject> action, string key = null)
{
this.Action = action;
this.Key = key;
}
public string Key
{
get;
private set;
}
public decimal Execute(PriceObject priceObject)
{
return Action(priceObject);
}
}
Caller Module: Let us first write a static module and define hard coded rules for execution. J
private void DoWork()
{
var engine = new RuleEngine();
IRule rule = new ConcreteRule("AMAZON");
rule.AddCondtion(po => po.NetProductPrice >= 0);
rule.AddAction(po => po.TransactionFee = Math.Round(po.NetProductPrice * 0.05M, 2));
engine.AddRule(rule);
rule = new ConcreteRule("AMAZON");
rule.AddCondtion(po => po.DRM == 2);
rule.AddCondtion(po => po.TransactionFee < 4M);
rule.AddAction(po => po.TransactionFee = 4M);
engine.AddRule(rule);
rule = new ConcreteRule("AMAZON");
rule.AddCondtion(po => po.DRM == 3);
rule.AddCondtion(po => po.TransactionFee < 8M);
rule.AddAction(po => po.TransactionFee = 8M);
engine.AddRule(rule);
rule = new ConcreteRule("AMAZON");
rule.AddCondtion(po => po.TransactionFee > 10M);
rule.AddAction(po => po.TransactionFee = 10M);
engine.AddRule(rule);
var obj = new PriceObject
{
NetProductPrice = 100,
DRM = 3
};
engine.Run("AMAZON", obj);
MessageBox.Show(obj.TransactionFee.ToString());
}
You can execute and ensure how beautifully rule engine is providing correct results.
Next is to think about possibility how to define runtime delegate so that I can wire up XML inputs to define dynamic conditions and actions.
My short target is to replace statement
rule.AddCondtion(po => po.NetProductPrice >= 0 )
with more dynamic one (assuming values come from XML):
rule.AddCondtion(Dynamic.GetConditiPredicate<PriceObject>("NetProductPrice", "GreaterOrEqual", "0"));
I will discuss these possibilities in Next Post. click here to read>>
Your comments are welcome.
Thanks for sharing. I would really like to learn more about getting some business rules engine for my company. This is great!
ReplyDeleteIt looks very nice. Something I was looking for my company.
ReplyDelete