Skip to the content.

RulesEngine is a highly extensible library to build rule based system using C# expressions

Features

Table Of Content

Installation

Nuget package: nuget

Basic Usage

Create a workflow file with rules

[
  {
    "WorkflowName": "Discount",
    "Rules": [
      {
        "RuleName": "GiveDiscount10",
        "Expression": "input1.country == \"india\" AND input1.loyalityFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2"
      },
      {
        "RuleName": "GiveDiscount20",
        "Expression": "input1.country == \"india\" AND input1.loyalityFactor == 3 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2"
      }
    ]
  }
] 

Initialise RulesEngine with the workflow:

var workflowRules = //Get list of workflow rules declared in the json
var re = new RulesEngine.RulesEngine(workflowRules);

Execute the workflow rules with input:

// Declare input1,input2,input3 
var resultList  = await re.ExecuteAllRulesAsync("Discount", input1,input2,input3);

//Check success for rule
foreach(var result in resultList){
  Console.WriteLine($"Rule - {result.Rule.RuleName}, IsSuccess - {result.IsSuccess}");
}

This will execute all the rules under Discount workflow and return ruleResultTree for all rules

Note: input passed to rulesEngine can be of a concrete type, an anonymous type or dynamic(Expandobject). In case of dynamic object, RulesEngine will internally convert to an anonymous type

Using custom names for inputs

By Default, RulesEngine will name the inputs as input1, input2, input3… respectively. It is possible to use a custom name in rules by passing input as RuleParameter

[
  {
    "WorkflowName": "DiscountWithCustomInputNames",
    "Rules": [
      {
        "RuleName": "GiveDiscount10",
        "Expression": "basicInfo.country == \"india\" AND basicInfo.loyalityFactor <= 2 AND basicInfo.totalPurchasesToDate >= 5000 AND orderInfo.totalOrders > 2 AND telemetryInfo.noOfVisitsPerMonth > 2"
      },
      {
        "RuleName": "GiveDiscount20",
        "Expression": "basicInfo.country == \"india\" AND basicInfo.loyalityFactor == 3 AND basicInfo.totalPurchasesToDate >= 10000 AND orderInfo.totalOrders > 2 AND telemetryInfo.noOfVisitsPerMonth > 2"
      }
    ]
  }
] 

Now we can call rulesEngine with the custom names:

var workflowRules = //Get list of workflow rules declared in the json
var re = new RulesEngine.RulesEngine(workflowRules);


// Declare input1,input2,input3 

var rp1 = new RuleParameter("basicInfo",input1);
var rp2 = new RuleParameter("orderInfo", input2);
var rp3 = new RuleParameter("telemetryInfo",input3);

var resultList  = await re.ExecuteAllRulesAsync("DiscountWithCustomInputNames",rp1,rp2,rp3);

C# Expression support

The lambda expression allows you to use most of C# constructs and along with some of linq features.

For more details on supported expression language refer - expression language

For supported linq operations refer - sequence operators

Extending expression via custom class/type injection

Although RulesEngine supports C# expressions, you may need to perform more complex operation.

RulesEngine supports injecting custom classes/types via ReSettings which can allow you to call properties and methods of your custom class in expressions

Example

Create a custom static class

using System;
using System.Linq;

namespace RE.HelperFunctions
{
    public static class Utils
    {
        public static bool CheckContains(string check, string valList)
        {
            if (String.IsNullOrEmpty(check) || String.IsNullOrEmpty(valList))
                return false;

            var list = valList.Split(',').ToList();
            return list.Contains(check);
        }
    }
}

Add it in your ReSettings and pass in RulesEngine constructor

  var reSettings = new ReSettings{
      CustomTypes = new Type[] { typeof(Utils) }
  };

  var rulesEngine = new RulesEngine.RulesEngine(workflowRules,reSettings);

With this you can call Utils class in your Rules

{
    "WorkflowName": "DiscountWithCustomInputNames",
    "Rules": [
      {
        "RuleName": "GiveDiscount10",
        "Expression": "Utils.CheckContains(input1.country, \"india,usa,canada,France\") == true"
      }
    ]
}

ScopedParams

Sometimes Rules can get very long and complex, scopedParams allow users to replace an expression in rule with an alias making it easier to maintain rule.

RulesEngine supports two type of ScopedParams:

GlobalParams

GlobalParams are defined at workflow level and can be used in any rule.

Example

//Rule.json
{
  "WorkflowName": "workflowWithGlobalParam",
  "GlobalParams":[
    {
      "Name":"myglobal1",
      "Expression":"myInput.hello.ToLower()"
    }
  ],
  "Rules":[
    {
      "RuleName": "checkGlobalEqualsHello",
      "Expression":"myglobal1 == \"hello\""
    },
    {
      "RuleName": "checkGlobalEqualsInputHello",
      "Expression":"myInput.hello.ToLower() == myglobal1"
    }
  ]
}

These rules when executed with the below input will return success

  var input = new RuleParameter("myInput",new {
    hello = "HELLO"
  });

  var resultList  = await re.ExecuteAllRulesAsync("workflowWithGlobalParam",rp);


LocalParams

LocalParams are defined at rule level and can be used by the rule and its child rules

Example

//Rule.json
{
  "WorkflowName": "workflowWithLocalParam",
  
  "Rules":[
    {
      "RuleName": "checkLocalEqualsHello",
      "LocalParams":[
        {
          "Name":"mylocal1",
          "Expression":"myInput.hello.ToLower()"
        }
      ],
      "Expression":"mylocal1 == \"hello\""
    },
    {
      "RuleName": "checkLocalEqualsInputHelloInNested",
      "LocalParams":[
        {
          "Name":"mylocal1", //redefined here as it is scoped at rule level
          "Expression":"myInput.hello.ToLower()"
        }
      ],
      "Operator": "And",
      "Rules":[
        {
          "RuleName": "nestedRule",
          "Expression":"myInput.hello.ToLower() == mylocal1" //mylocal1 can be used here since it is nested to Rule where mylocal1 is defined
        }
      ]
      
    }
  ]
}

These rules when executed with the below input will return success

  var rp = new RuleParameter("myInput",new {
    hello = "HELLO"
  });

  var resultList  = await re.ExecuteAllRulesAsync("workflowWithLocalParam",rp);

Referencing ScopedParams in other ScopedParams

Similar to how ScopedParams can be used in expressions, they can also be used in other scoped params that come after them. This allows us to create multi-step rule which is easier to read and maintain

//Rule.json
{
  "WorkflowName": "workflowWithReferencedRule",
  "GlobalParams":[
    {
      "Name":"myglobal1",
      "Expression":"myInput.hello"
    }
  ],
  "Rules":[
    {
      "RuleName": "checkGlobalAndLocalEqualsHello",
      "LocalParams":[
        {
          "Name": "mylocal1",
          "Expression": "myglobal1.ToLower()"
        }
      ],
      "Expression":"mylocal1 == \"hello\""
    },
    {
      "RuleName": "checklocalEqualsInputHello",
       "LocalParams":[
        {
          "Name": "mylocal1",
          "Expression": "myglobal1.ToLower()"
        },
        {
          "Name": "mylocal2",
          "Expression": "myInput.hello.ToLower() == mylocal1"
        }
      ],
      "Expression":"mylocal2 == true"
    }
  ]
}

These rules when executed with the below input will return success

  var input = new RuleParameter("myInput",new {
    hello = "HELLO"
  });

  var resultList  = await re.ExecuteAllRulesAsync("workflowWithReferencedRule",rp);


Post rule execution actions

As a part of v3, Actions have been introduced to allow custom code execution on rule result. This can be achieved by calling ExecuteAllRulesAsync method of RulesEngine

Inbuilt Actions

RulesEngine provides inbuilt action which cover major scenarios related to rule execution

OutputExpression

This action evaluates an expression based on the RuleParameters and returns its value as Output

Usage

Define OnSuccess or OnFailure Action for your Rule:

{
  "WorkflowName": "inputWorkflow",
  "Rules": [
    {
      "RuleName": "GiveDiscount10Percent",
      "SuccessEvent": "10",
      "ErrorMessage": "One or more adjust rules failed.",
      "ErrorType": "Error",
      "RuleExpressionType": "LambdaExpression",
      "Expression": "input1.couy == \"india\" AND input1.loyalityFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input2.noOfVisitsPerMonth > 2",
      "Actions": {
         "OnSuccess": {
            "Name": "OutputExpression",  //Name of action you want to call
            "Context": {  //This is passed to the action as action context
               "Expression": "input1.TotalBilled * 0.9"
            }
         }
      }
    }
  ]
}

Call ExecuteAllRulesAsync with the workflowName, ruleName and ruleParameters

   var ruleResultList = await rulesEngine.ExecuteAllRulesAsync("inputWorkflow",ruleParameters);
   foreach(var ruleResult in ruleResultList){
      if(ruleResult.ActionResult != null){
          Console.WriteLine(ruleResult.ActionResult.Output); //ActionResult.Output contains the evaluated value of the action
      }
   }
   

EvaluateRule

This action allows chaining of rules along with their actions. It also supports filtering inputs provided to chained rule as well as providing custom inputs

Usage

Define OnSuccess or OnFailure Action for your Rule:

{
  "WorkflowName": "inputWorkflow",
  "Rules": [
    {
        "RuleName": "GiveDiscount20Percent",
        "Expression": "input1.couy == \"india\" AND input1.loyalityFactor <= 5 AND input1.totalPurchasesToDate >= 20000",
        "Actions": {
           "OnSuccess": {
              "Name": "OutputExpression",  //Name of action you want to call
              "Context": {  //This is passed to the action as action context
                 "Expression": "input1.TotalBilled * 0.8"
              }
           },
           "OnFailure": { // This will execute if the Rule evaluates to failure
               "Name": "EvaluateRule",
               "Context": {
                   "workflowName": "inputWorkflow",
                   "ruleName": "GiveDiscount10Percent"
               }
           }
        }
    },
    {
      "RuleName": "GiveDiscount10Percent",
      "SuccessEvent": "10",
      "ErrorMessage": "One or more adjust rules failed.",
      "ErrorType": "Error",
      "RuleExpressionType": "LambdaExpression",
      "Expression": "input1.couy == \"india\" AND input1.loyalityFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input2.noOfVisitsPerMonth > 2",
      "Actions": {
         "OnSuccess": {
            "Name": "OutputExpression",  //Name of action you want to call
            "Context": {  //This is passed to the action as action context
               "Expression": "input1.TotalBilled * 0.9"
            }
         }
      }
    }
  ]
}

Call ExecuteActionWorkflowAsync with the workflowName, ruleName and ruleParameters

   var result = await rulesEngine.ExecuteActionWorkflowAsync("inputWorkflow","GiveDiscount20Percent",ruleParameters);
   Console.WriteLine(result.Output); //result.Output contains the evaluated value of the action

In the above scenario if GiveDiscount20Percent succeeds, it will return 20 percent discount in output. If it fails, EvaluateRule action will call GiveDiscount10Percent internally and if it succeeds, it will return 10 percent discount in output.

EvaluateRule also supports passing filtered inputs and computed inputs to chained rule

 "Actions": {
         "OnSuccess": {
            "Name": "EvaluateRule",
               "Context": {
                   "workflowName": "inputWorkflow",
                   "ruleName": "GiveDiscount10Percent",
                   "inputFilter": ["input2"], //will only pass input2 from existing inputs,scopedparams to the chained rule
                   "additionalInputs":[ // will pass a new input named currentDiscount with the result of the expression to the chained rule
                     {
                       "Name": "currentDiscount",
                       "Expression": "input1.TotalBilled * 0.9"
                     }
                   ]
               }
         }
      }

Custom Actions

RulesEngine allows registering custom actions which can be used in the rules workflow.

Steps to use a custom Action

  1. Create a class which extends ActionBase class and implement the run method
     public class MyCustomAction: ActionBase
     {
         
         public MyCustomAction(SomeInput someInput)
         {
             ....
         }
    
         public override ValueTask<object> Run(ActionContext context, RuleParameter[] ruleParameters)
         {
             var customInput = context.GetContext<string>("customContextInput");
             //Add your custom logic here and return a ValueTask
         }
    

    Actions can have async code as well

     public class MyCustomAction: ActionBase
     {
         
         public MyCustomAction(SomeInput someInput)
         {
             ....
         }
    
         public override async ValueTask<object> Run(ActionContext context, RuleParameter[] ruleParameters)
         {
             var customInput = context.GetContext<string>("customContextInput");
             //Add your custom logic here
             return await MyCustomLogicAsync();
         }
    
  2. Register them in ReSettings and pass it to RulesEngine
    var reSettings = new ReSettings{
                         CustomActions = new Dictionary<string, Func<ActionBase>>{
                                              {"MyCustomAction", () => new MyCustomAction(someInput) }
                                          }
                      };
    
    var re = new RulesEngine(workflowRules,reSettings);
    
  3. You can now use the name you registered in the Rules json in success or failure actions
    {
      "WorkflowName": "inputWorkflow",
      "Rules": [
     {
       "RuleName": "GiveDiscount10Percent",
       "SuccessEvent": "10",
       "ErrorMessage": "One or more adjust rules failed.",
       "ErrorType": "Error",
       "RuleExpressionType": "LambdaExpression",
       "Expression": "input1.couy == \"india\" AND input1.loyalityFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input2.noOfVisitsPerMonth > 2",
       "Actions": {
          "OnSuccess": {
             "Name": "MyCustomAction",  //Name context
             "Context": {  //This is passed to the action as action context
                "customContextInput": "input1.TotalBilled * 0.9"
             }
          }
       }
     }
      ]
    }
    

Standalone Expression Evaluator

If you are not looking for a full fledged RulesEngine and need only an expression evaluator. RulesEngine offers RuleExpressionParser which handles expression parsing and evaluation.

Usage

using System;
using RulesEngine.Models;
using RulesEngine.ExpressionBuilders;
					
public class Program
{
	public static void Main()
	{
		var reParser = new RuleExpressionParser(new ReSettings());
		var result = reParser.Evaluate<string>("a+b", new RuleParameter[]{
			new RuleParameter("a","Hello "),
			new RuleParameter("b","World")
		});
		Console.WriteLine(result);
	}
}

This will output “Hello World”

For more advanced usage, refer - https://dotnetfiddle.net/KSX8i0

Settings

RulesEngine allows you to pass optional ReSettings in constructor to specify certain configuration for RulesEngine.

Here are the all the options available:-

Property Type Default Value Description
CustomTypes Type[] N/A Custom types to be used in rule expressions.
CustomActions Dictionary<string, Func<ActionBase>> N/A Custom actions that can be used in the rules.
EnableExceptionAsErrorMessage bool true If true, returns any exception occurred while rule execution as an error message. Otherwise, throws an exception. This setting is only applicable if IgnoreException is set to false.
IgnoreException bool false If true, it will ignore any exception thrown with rule compilation/execution.
EnableFormattedErrorMessage bool true Enables error message formatting.
EnableScopedParams bool true Enables global parameters and local parameters for rules.
IsExpressionCaseSensitive bool false Sets whether expressions are case sensitive.
AutoRegisterInputType bool true Auto registers input type in custom type to allow calling method on type.
NestedRuleExecutionMode NestedRuleExecutionMode All Sets the mode for nested rule execution.
CacheConfig MemCacheConfig N/A Configures the memory cache.
UseFastExpressionCompiler bool true Whether to use FastExpressionCompiler for rule compilation.

NestedRuleExecutionMode

Value Description
All Executes all nested rules.
Performance Skips nested rules whose execution does not impact parent rule’s result.