Click here to Skip to main content
15,881,898 members
Articles / Web Development / HTML
Tip/Trick

Rules Engine

Rate me:
Please Sign up or sign in to vote.
5.00/5 (9 votes)
15 Nov 2014CPOL6 min read 39.1K   1.4K   39   5
Create a Rules Engine in less than 50 lines of code which is extremely powerful, extensible and adaptable

Rules Engine

Rules engines are extremely valuable for validating business rules. Recently I needed a framework and I looked for some implementations. Some examples concentrated on translating rules in a database to rule object using expressions. I was not interested in that. Then I looked at (https://rulesengine.codeplex.com/) which is built around rule classes and relying on fluent interface. While studying this great project, I asked myself, do you really need all that stuff and can't you do it far more simple so everybody can extend more easily. In fact you can. The core of the engine exists of a few methods, a few classes and one interface. It is easily extendable and fluent and can be modified towards your validation needs.

Separation of Definition and Execution and a Fluent Interface

It is nice to separate the definitions of rules from the applying of the rules on a concrete object. Let's first study separation with a simple example:

C#
private Func<string, string> ConcatSeparation(string Add)
{
   return (x => x + " " + Add);
}
public void Test()
{
   //Definitions (once)
   var defineConcatone = ConcatSeparation("says something");
   var defineConcatonanother = ConcatSeparation("answers him");
      
   //Execution. You can execute the definitions as many times you want.
   Console.WriteLine(defineConcatone("Bart"));
   Console.WriteLine(defineConcatonanother("Eric"));
} 

A Func delegate as a return type is a natural way to construct the separation of definition and execution.

A Fluent interface is a kind of method chaining, making code more readable. For a rules engine, you chain the validation methods to apply.

How can we code a Fluent interface? I have seen a lot of approaches but the simplest one, in my opinion, is to follow the following construct. Method chaining is easy when the input argument and the return argument are of the same type or generic type. Just look at the next example:

C#
public class TestObject
{
   public string name { get; set; }
}

public static class TestObjectExt
{
   public static TestObject Bind(this TestObject source, Func<TestObject, string> changeName)
   {
     return new TestObject() { name = changeName(source) };
   }
}

var fluent = new TestObject().Bind(x => "ChangeName").Bind(x => "new changed name");

Console.WriteLine(fluent.name);

You don't need to make it more difficult than this. Although you must take into account complicating factors on covariance and contravariance, which issues I leave aside here.

Rules Engine and State

The final ingredient is State. During the execution of all the rules, we have to keep track of the validation results and collect them. State can be incorporated when we use the following delegate:

C#
public delegate IValidationPair<V, ValidationResults> 
ToValidationPaired<in I, out V, out ValidationResults>(I i);

This delegate is a definition of the following: giving an input of type I, provide an output of V and ValidationResults. This ValidationResults is our state. The output is a pair of value and state. In our case, the type V is the type of the instance to apply the rule on. An interesting part of this definition is that given a input of type I, we can return a value of another type (type V). We can start the chain with type I, and end the chain with a validation on another type.

Putting It Together

The above delegate returns a value/state interface. In our example, this interface is implemented in the Pair class.

C#
public interface IPair<out V, out S>
{
    V Val { get; }
    S State { get; }
}

public class Pair<V, S> : IPair V, S
{
    public S State { get; private set; }
    public V Val { get; private set; }

    public Pair(V val, S state)
    {
        Val = val;
        State = state;
    }
    V IPair<V, S>.Val
    {
        get { return Val; }
    }
    S IPair<V, S>.State
    {
        get { return State; }
    }
} 

The only thing left is to implement a similar TestObject Bind method for our Pair. Here it is:

C#
public static ToValidationPaired<I, I, 
ValidationResults> Init<I>(Func ValidationResults createState)
{
    return i => new Pair<I, ValidationResults> (i, createState());
}

public static ToValidationPaired<I, V, ValidationResults> Bind<I, 
V>(this ToValidationPaired<I, V, ValidationResults> pair, Func<V, ValidationResult> func)
{
    return (i =>
    {
        var statepair = pair(i);
        if (statepair.Val == null) {
            return statepair;
    }
    var validating = func(statepair.Val);
    if (validating != null && !validating.Valid)
    {
        statepair.State.Errors.Add(validating.Mgs);
    }
    return statepair; 
    });
}

public static Func<I, IEnumerable string>> 
Return<I, V>(this ToValidationPaired<I, V, ValidationResults> pair)
{
    return (i => pair(i).State.TheErrors);
}

These three functions make up the core of our Rule engine, nothing more nothing less! The init function creates the initial object that will be chained. In this Init function we create, when applied on an instance of an object, a new ValidationResults objects that holds our validation results. The init object is of the delegate type. The Bind method is of the same type, and has an input argument that is the same as the return type and therefore can be used in fluent interface chaining. The Return method unwraps the pair and returns the errors stored on our state object. It returns exactly what a rules engine is about: validation errors.

We can already write:

C#
private static Func<Basket, IEnumerable string> somerules;

somerules = ValidationPair.InitBasket(() => new Rules.Engine.ValidationResults())
           .Bind(x => x.CanAdd())
           .Bind(x => x.HasBasketOwner())
           .Return();

Executing the rules looks like:

C#
var errs = somerules(mybasket);

Extensions

Init, Bind and Return make up the core construct of our Rules engine. You can add your own extensions. Say you want to apply a rule to a collection, then you add the following method:

C#
public static ToValidationPaired<I, V, ValidationResults> Bind<I, 
V, W>(this ToValidationPaired<I, V, ValidationResults> pair, Func<I, 
IEnumerable<W>> list, Func<W, ValidationResult> func)
{            
   return (i =>
   {
       var statepair = pair(i);
       if (statepair.Val == null)
       {
          return statepair;
       }
       foreach (W item in list(i))
       {
           var validating = func(item);
           if (validating != null && !validating.Valid)
           {
               statepair.State.Errors.Add(validating.Mgs);
           }
       }
       return statepair;
   });
}

And make you call:

C#
var colrules = ValidationPair.Init<Basket>(() => new Rules.Engine.ValidationResults())
.Bind(x => x.Mylist, y => y.NotNullName(y))
.Return();

If you need to proceed with another object instead of the basket, you define another extension like:

C#
public static ToValidationPaired<I, V, ValidationResults> 
Bind<I, V>(this ToValidationPaired<I, I, ValidationResults> pair, Func<I, V> func)
{            
    return (i =>
    {
        var statepair = pair(i);
        if (statepair.Val == null)
        {
            return new Pair V, ValidationResults (default(V), statepair.State); 
        }
        return new Pair V, ValidationResults (func(statepair.Val), statepair.State);
    });   
}

And the caller code will be:

C#
var complextyperules = ValidationPair.Init<Basket>(() => new Rules.Engine.ValidationResults())
.Bind(x => x.HasPolicyOwner()) //Bind on basket
.Bind(x => x.CanAdd())
.Bind(x => x._basketholder) //Change Bind to Bind on owner
.Bind(o => o.NotNullNamc(x));

And if you want, you can add conditional branching as well. I leave that to the user, because it is more of the same.

Modify the Implementation

Because the code is so concise, you can easily modify this implementation to your needs. You can add properties on the ValidationResults object like StopValidationAtFirstFault or whatever.

Just pass the correct parameters to the init method and add some extra checks in the other extension methods (return immediately, if state object already has a failure, which is highly effective).

You can use your own ValidationResult or ValidationResults class. This will also not involve too much work. An alternative is raising validation failure events. Change your validation methods to methods with no return value, change the Func delegate to an Action delegate and use the RX-framework to subscribe and query the fired events. This approach is very valuable if you have to filter all kind of validations in combination with various strategies. Whatever your needs, I don't think it takes too much to change the current implementation. The reason for this is that this implementation is a kind of state monad and monads are extremely powerful.

Encapsulation

In the example that comes with this article, I have to pinpoint another very important aspect. A Rules Engine is most useful when it applies to business logic that is properly encapsulated.

The rules apply to a (shopping) Basket, which is normally stored in a WEB-Session object. In a lot of implementations, an object is stored in a web Session variable and is accessible and mutable from everywhere in a site. In most implementations, the member variables, fields and properties are public.

In this case, however, the Basket is put in a separate assembly with no direct coupling to any UI or persistence storage (Web session). The Basket and the items in the Basket cannot be modified directly. The Basket is initialized using a repository interface. The only way to change something in the Basket is through simple commands like (Commands.AddPhoneItemCommand). A command can only be processed by one handler and has no return value a caller can work on. Therefore the command handler that processes a command is the place where you can apply the rules of your engine. You define separate set of rules for separate commands. This makes the code more readable and easier to modify. Just like a fluent interface, this approach is attributing to the readability and maintainability of your system.

The Basket reports state changes back through messages. The code includes a separate assembly where this command/messageprocessor is implemented. Therefore the Basket is decoupled from the concrete implementation of the database- and UI layer. It can be easily tested in a test environment.

The other advantage of this approach is that the programmer of those two layers doesn’t need to study or know the internal interface or implementation of the Basket itself. The only things at his disposal are very simple commands and messages, that speak for themselves. This technique is central in a CQRS-approach, but that is an issue to be dealt with in another place and time.

Stripped Version

If you are only interested in the engine as such, and not validation and encapsulation, just download the stripped example that I have included and play with that.

Core Classes

To study this code, concentrate on the following assemblies and classes:

  • BMCommonInterfaces: IPair and Pair: encapsulate the 'state' delegate
  • BMInterestedItemsBusiness: /Rules/Engine: Validation* : Define Rule engine state/fluent interface
  • BMInterestedItemsBusiness: Handler/ItemHandler: Rule engine rules and invoking of the rules
  • BMMessenger: Command/Messenger implementation

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Netherlands Netherlands
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionDatabase Pin
mbowles20126-Nov-19 10:10
mbowles20126-Nov-19 10:10 
GeneralMy vote of 5 Pin
Prasad Khandekar19-Nov-14 3:30
professionalPrasad Khandekar19-Nov-14 3:30 
GeneralExcellent Article Pin
Suchi Banerjee, Pune18-Nov-14 0:55
Suchi Banerjee, Pune18-Nov-14 0:55 
Excellent approach, I like it.

Thanks for sharing!
QuestionHumm... Pin
Dewey15-Nov-14 11:32
Dewey15-Nov-14 11:32 
AnswerRe: Humm... Pin
Member 353162217-Nov-14 7:52
Member 353162217-Nov-14 7:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.