Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

An Event Based Rules Engine

0.00/5 (No votes)
25 Feb 2006 1  
A design for an event driven rules engine.

Sample Image - irule.jpg

Introduction

I have been in several situations where I needed to evaluate a set of rules and perform a specific action based on the results. I have never been proud of the code I wrote for these rules; either massive switch statements, or a Gordian knot of if-else commands that quickly became convoluted and difficult to maintain. Testing becomes impossible as the number of paths through the maze becomes unknowable.

What I need is a simple rules engine that allows me to write simple or complicated rules, rewire them as needed, and be able to test the individual rules without having to test the entire rule set. What I do not want is some complicated framework that forces me to have to think about how it works. I like simple things.

The idea rules engine I imagine should provide the following:

  1. There is a Rule that is to be invoked.
  2. The Rule will either Pass or Fail.
  3. If the Rule is not the last rule in the chain, then it will invoke another Rule in the chain.
  4. If the Rule is the last rule in the chain, then it will not invoke any other Rules.

One way to accomplish this is to use objects which have an Invoke method that follow the event signature. This allows for the rules to invoke other rules without coupling the rules to each other. A Rule will have two events, RulePassed and RuleFailed, that invokes the next Rule. By wiring up a Rule's events to other Rules' Invoke() methods, a chain of rules is created which, when the top most Rule is invoked, will cause the entire rule scenario to be evaluated.

The Rule Events

There are four events used by the Rule:

public event RuleDelegate RulePassed;
public event RuleDelegate RuleFailed;
public event RuleDelegate BeginRuleEvaluation;
public event RuleDelegate EndRuleEvaluation;

The BeginRuleEvaluation and the EndRuleEvaluation are there to allow the developer to perform any actions needed before and after a Rule is executed.

The RulePassed and RuleFailed events are used by the Rule to invoke other Rules. A standard RegisterRules() method is included in the BaseRule.

public void RegisterRules(IRule rulePassed, IRule ruleFailed)
{
     this.RulePassed += new RuleDelegate(rulePassed.Invoke);
     this.RuleFailed += new RuleDelegate(ruleFailed.Invoke);
}

The events are fired by calling the OnRulePassed() or OnRuleFailed() methods in the Invoke() method of the class.

public override void Invoke(object sender, RuleEventArgs e)
{
  OnBeginRuleEvaluation(this);

  if (pass)
    OnRulePassed(this);
  else
   OnRuleFailed(this);

  OnEndRuleEvaluation(this)
}

Invoking the Rule

The overloaded method Invoke() is used to execute the Rule. The first form is a simple method with no parameters that can be used to evaluate a stand alone Rule. This is usually called only on the first Rule of the chain of Rules.

The second form uses an event signature. This allows the method to be registered to another Rule's RulePassed or RuleFailed events.

public virtual void Invoke()
{
     // Do Stuff

} 
public virtual void Invoke(object sender, RuleEventArgs e)
{
     // Do Stuff

}

The BaseRule

The BaseRule implements the IRule interface and contains most of the boiler plate code that any Rule will use. The main items provided are the Name property, definitions for the event delegates, and methods to raise the events. A key method implemented is the RegisterRules(IRule rulePassed, IRule ruleFailed).

The Invoke() methods are implemented as virtual functions.

The goal with the BaseRule is to reduce the amount of work needed to implement the actual rules, and to provide an object that can be extended through inheritance for more advanced rules.

using System;

namespace Rulez.Engine
{
  public class BaseRule : IRule
  {
    public BaseRule(string Name)
    {
      _name = Name;
    }

    private readonly string _name;
    public string Name
    {
      get { return _name; }
    }

    public virtual void Invoke()
    {
      throw new NotImplementedException();
    }

    public virtual void Invoke(object sender, RuleEventArgs e)
    {
      throw new NotImplementedException();
    }

    public event RuleDelegate RulePassed;
    public void OnRulePassed(object sender)
    {
      Events.FireEvent(RulePassed, sender);
    }

    public event RuleDelegate RuleFailed;
    public void OnRuleFailed(object sender)
    {
      Events.FireEvent(RuleFailed, sender);
    }

    public event RuleDelegate BeginRuleEvaluation;

    public void OnBeginRuleEvaluation(object sender)
    {
      Events.FireEvent(BeginRuleEvaluation, sender);
    }

    public event RuleDelegate EndRuleEvaluation;
    public void OnEndRuleEvaluation(object sender)
    {
      Events.FireEvent(EndRuleEvaluation, sender);
    }

    public void RegisterRules(IRule rulePassed, IRule ruleFailed)
    {
      if (rulePassed == null)
          throw new ArgumentNullException("rulePassed");
      if (ruleFailed == null)
          throw new ArgumentNullException("ruleFailed");

      this.RulePassed += new RuleDelegate(rulePassed.Invoke);
      this.RuleFailed += new RuleDelegate(ruleFailed.Invoke);
    }
  }
}

And that is pretty much it. Simple, easy to understand, and easy to implement.

Random Rule Example

One of the examples included builds a rule tree up to a maximum depth of MaxDepth. Each rule is identical in that it randomly passes or fails. The Random Rule Tree is built using a static method which recursively calls itself until the tree is full:

public static RandomRule BuildRuleTree(int CurrentDepth, 
              int MaxDepth, SimpleLogger logger, string Name)
{
  RandomRule rr = new RandomRule(logger, Name);
  CurrentDepth += 1;

  if (CurrentDepth <= MaxDepth)
    rr.RegisterRules(
      BuildRuleTree(CurrentDepth, MaxDepth, 
                    logger, Name + "Passed"), 
      BuildRuleTree(CurrentDepth, MaxDepth, 
                    logger, Name + "Failed"));

  return rr;
}

The root Rule of the tree, when invoked, simply calls the event signature Invoke():

public override void Invoke()
{
  Invoke(this, EventArgs.Empty as RuleEventArgs);
}

The Rule itself will pass or fail by randomly selecting a number between 0 and 999 and determining if it is even or odd:

public override void Invoke(object sender, RuleEventArgs e)
{
  bool pass = (GenerateRandomNumbers.GetRandomNumber(999))%2 == 0;

  if (pass)
    OnRulePassed(this);
  else
    OnRuleFailed(this);
}

An included test fixture demonstrates how the Random Rule Builder is executed. It should be noted that the deeper the depth, the longer it takes to build the tree.

static void Main(string[] args)
{
    SimpleLogger log = new SimpleLogger("RandomRuleBuilder");
    log.StartTimer("Building Rules");

    RandomRule baseRule = RandomRule.BuildRuleTree(0, 10, log, "BaseRule");
    log.StopTimer("Rules Built");

    Console.WriteLine(log.ToString());


    log.StartTimer("Begin Processing Rules");
    baseRule.Invoke();
    log.StopTimer("All Rules Processed");

    Console.WriteLine(log.ToString());

    Console.Read();
}

Employee Overtime Example

Say that you want to calculate an employee�s overtime using the following rules:

  1. If the Employee worked 40 or less hours:

      1.1 OTM = 0

  2. If the Employee worked over 40 hours and less than 60 hours:

      2.1. If the Employee is on Hourly basis, OTH = 1.5 * (Total Hours - 40)

      2.2. If the Employee is on Salary basis,

        2.2.1 If the Employee worked 45 or less hours, OTH = 0

        2.2.2 If the Employee worked more than 45 hours, OTH = 1.5 * (Total Hours - 45)

  3. If the Employee worked over 60 hrs

      3.1. If the Employee is on Hourly basis, OTH = 1.5 * 20 + 2 * (Total Hours - 60)

      3.2. If the Employee is on Salary basis, OTH = 1.5 * 15 + 2 * (Total Hours - 60)

The first step is to create an Employee object and a BaseRule object.

public class BaseBizRule : BaseRule
{
  protected readonly Employee employee;

  public BaseBizRule(string Name, Employee employee) : base(Name)
  {
    this.employee = employee;
  }

  public override void Invoke()
  {
    Invoke(this, EventArgs.Empty as RuleEventArgs);
  }

}

public enum EmployeeType
{
  Hourly,
  Salary
}

public class Employee
{
  public Employee(string Name, EmployeeType employeeType, 
                  double HoursWorked)
  {
    _hoursWorked = HoursWorked;
    _name = Name;
    _employeeType = employeeType;
    _overTimeHours = 0;
  }

  private readonly EmployeeType _employeeType;
  public EmployeeType EmployeeType
  {
    get { return _employeeType;}
  }

  private readonly string _name;
  public string Name
  {
    get { return _name; }
  }

  private readonly double _hoursWorked;

  public double HoursWorked
  {
    get { return _hoursWorked; }
  }

  private double _overTimeHours;

  public double OverTimeHours
  {
    get { return _overTimeHours; }
    set { _overTimeHours = value; }
  }
}

An example Rule would be:

public class Rule1 : BaseBizRule
{
  public Rule1(Employee employee) : base("Rule1", employee)
  {}

  public override void Invoke(object sender, RuleEventArgs e)
  {
    OnBeginRuleEvaluation(this);

    if (employee.HoursWorked <= 40)
    {
      employee.OverTimeHours = 0;
      OnRulePassed(this);
    }
    else
    {
      OnRuleFailed(this);
    }

    OnEndRuleEvaluation(this);
  }
}

Note that Rule 2.1 is the last rule in the chain, so when it is invoked, it will set the employee's overtime but does not need to invoke any other rules.

public class Rule2_1 : BaseBizRule
{
  public Rule2_1(Employee employee) : base("Rule2_1", employee)
  {}

  public override void Invoke(object sender, RuleEventArgs e)
  {
    OnBeginRuleEvaluation(this);

    employee.OverTimeHours = 1.5*(employee.HoursWorked - 40.0);

    OnEndRuleEvaluation(this);
  }
}

Registering the rules is simple and, well, tedious. In this case, I create a rule, TheRuleList, which is the top rule of the chain, and register the rules in the rule class constructor:

public TheRuleList(Employee employee) : base("TheRuleList", employee)
{
  r1 = new Rule1(employee);
  r2 = new Rule2(employee);
  r3 = new Rule3(employee);

  r2_1 = new Rule2_1(employee);
  r2_2 = new Rule2_2(employee);
  r2_2_1 = new Rule2_2_1(employee);
  r2_2_2 = new Rule2_2_2(employee);

  r3_1 = new Rule3_1(employee);
  r3_2 = new Rule3_2(employee);

  r_isSalary1 = new RuleIsSalary(employee);
  r_isSalary2 = new RuleIsSalary(employee);

  rt = new RuleTerminator(employee);

  r1.RegisterRules(rt, r2);
  r2.RegisterRules(r_isSalary1, r3);
  r3.RegisterRules(r_isSalary2, rt);

  r_isSalary1.RegisterRules(r2_2, r2_1);
  r_isSalary2.RegisterRules(r3_2, r3_1);

  r2_2.RegisterRules(r2_2_1, r2_2_2);

  r2_1.RegisterRules(rt, rt);
  r2_2_1.RegisterRules(rt, rt);
  r2_2_2.RegisterRules(rt, rt);

  r3_1.RegisterRules(rt, rt);
  r3_2.RegisterRules(rt, rt);
}

To use the rule list, you would create an employee object, the rule list then invokes the rule:

Employee emp = new Employee("Darren", EmployeeType.Hourly, 55);

TheRuleList theRuleList = new TheRuleList(emp);
theRuleList.Invoke();

return emp.OverTimeHours;

Conclusion

In nature, some of the most complicated systems have the simplest of building blocks. The DNA is based on only four chemical building blocks that can only be combined in four possible ways. The IRule allows for two options, pass or fail. Using this simple interface, one can develop elaborate rule chains.

The one caveat to this simplicity is the registering of the rules. Even though the individual rules maybe easy to build and maintain, the process of wiring them together could get complicated fast as can be seen in the employee example above. Since the goal of this whole system is ease of maintenance, this one point must be addressed.

Currently, I do not have a good solution for the registering of the rule events. One option I want to explore is to use an IOC container or an XML file. I have not tried either way, but I will be exploring these options in the future.

History

  • 2006-02-24: Initial release.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here