. NET RulesEngine (rule engine)

By chance, let me take out RulesEngine to complete a business. For the business, it is mainly to complete a business judgment of scalability (uncertain types and uncertain conditions, and the change of conditions may be continuously increased and modified). For example, after completing an achievement system, the administrator can create it. For achievements, there are one-time unlocking, daily, weekly, and reset at any time. Each time they reach Chengdu, they are triggered. In the face of the increase of achievement tasks, it is a headache for programmers to add and modify these achievement tasks every time. Well, you should have a simple understanding of this. Then follow the author to see how to use very little code to complete a simple dynamic logic processing in. NET.

RulesEngine overview

yes Microsoft A rule engine project is introduced for business logic / rules / policies abstracted from the system. In the process of our development, it is inevitable to deal with this repeated business logic. For this dynamic rule, it is an elegant way to use us to reduce the modification of our code or project.

How to use

At present, we can import the library in the form of nuget, as shown below:

dotnet add package RulesEngine 

For rule configuration, you can directly use typed parameters. The author mainly aims to make it clear to you, so JSON configuration is used for demonstration.

//Deserialize Json format rule string
var workflowRules = JsonConvert.DeserializeObject<List<WorkflowRules>>(rulesStr);
 var rulesEngine = new RulesEngine.RulesEngine(workflowRules.ToArray());
 //Define rules
            var rulesStr = @"[{
                    ""WorkflowName"": ""UserInputWorkflow"",
                    ""Rules"": [
                      {
                        ""RuleName"": ""CheckAge"",
                        ""ErrorMessage"": ""Age must be greater than 18."",
                        ""ErrorType"": ""Error"",
                        ""RuleExpressionType"": ""LambdaExpression"",
                        ""Expression"": ""Age > 18""
                      },
                       {
                        ""RuleName"": ""CheckIDNoIsEmpty"",
                        ""ErrorMessage"": ""The ID number can not be empty.."",
                         ""ErrorType"": ""Error"",
                        ""RuleExpressionType"": ""LambdaExpression"",
                        ""Expression"": ""IdNo != null""
                      }
                    ]
                  }] ";

As shown above, we have defined rule information. For this information, the author stores JSON data by default. Of course, you can store the following contents and split the following data structures into the database.

attributedescribe
RuleName Rule name
Properties Rule attribute, get or set the custom attribute or tag of the rule
Operator Operator
ErrorMessage Error message
Enabled Gets and sets whether the rule is enabled
RuleExpressionType The regular expression type is LambdaExpression by default. Of course, there is only one such expression at present
WorkflowRulesToInJect Injection workflow rules
Rules rule
LocalParams Local parameters
Expression Expression tree
Actions  
SuccessEvent Completion event, default to rule name

Let's take a look at the results generated by the code. For this content, the author creates a class as follows:

   public class UserInput
        {
            public string IdNo { get; set; }
            public int Age { get; set; }
        }
static async Task Main(string[] args)
        {
            var userInput = new UserInput
            {
                IdNo = null,
                Age = 18
            };

            //Deserialize Json format rule string
            var workflowRules = JsonConvert.DeserializeObject<List<WorkflowRules>>(rulesStr);
            
            var rulesEngine = new RulesEngine.RulesEngine(workflowRules.ToArray());

            List<RuleResultTree> resultList = await rulesEngine.ExecuteAllRulesAsync("UserInputWorkflow", userInput);
            foreach (var item in resultList)
            {               
                 Console.WriteLine("Validation succeeded:{0},Message:{1}",item.IsSuccess,item.ExceptionMessage);
            }

            Console.ReadLine();

        }

The output results are as follows:

Validation succeeded: False,Message: age must be greater than 18.
Validation succeeded: False,Message: the ID number can not be empty..

The return structure resultList is as follows:

 { "Rule":{ "RuleName":"CheckNestedSimpleProp","Properties":null,"Operator":null,"ErrorMessage":"Age must be greater than 18.",
                "ErrorType":"Error","RuleExpressionType":"LambdaExpression","WorkflowRulesToInject":null,"Rules":null,"LocalParams":null,"Expression":"Age > 18","Actions":null,"SuccessEvent":null},"IsSuccess":false,"ChildResults":null,"Inputs":{ "input1":{ "IdNo":null,"Age":18} },
                "ActionResult":{ "Output":null,"Exception":null},"ExceptionMessage":"Age must be greater than 18.","RuleEvaluatedParams":[]}

Use extension method in expression tree

I believe you have a simple understanding of the use of the rule engine. Let's take another advanced version.

For example, I think that the age of input is not accurate, I want to calculate the age by ID number, then how do I operate? Under normal circumstances, we will pass the expansion method, then pass the ID number parameter to the processing program. After the processing program is completed, it will return to our age, and how do we operate it in this? Let's look down.

Adding custom types through ReSettings will extend methods, because the methods they can use are limited to [System namespace], so we need to add custom classes to the settings.

   private static readonly ReSettings reSettings = new ReSettings
        {
            CustomTypes = new[] { typeof(IdCardUtil) }
        };
        

Amend the following:

 var rulesEngine = new RulesEngine.RulesEngine(workflowRules.ToArray(), null, reSettings: reSettings);
var rulesStr = @"[{
                    ""WorkflowName"": ""UserInputWorkflow"",
                    ""Rules"": [
                      {
                        ""RuleName"": ""CheckNestedSimpleProp"",
                        ""ErrorMessage"": ""Age must be less than 18."",
                        ""ErrorType"": ""Error"",
                        ""RuleExpressionType"": ""LambdaExpression"",
                        ""Expression"": ""IdNo.GetAgeByIdCard() < 18""
                      },
                       {
                        ""RuleName"": ""CheckNestedSimpleProp1"",
                        ""ErrorMessage"": ""The ID number can not be empty.."",
                         ""ErrorType"": ""Error"",
                        ""RuleExpressionType"": ""LambdaExpression"",
                        ""Expression"": ""IdNo != null""
                      }
                    ]
                  }] ";

The output results are as follows:

Validation succeeded: False,Message: age must be less than 18.
Validation succeeded: True,Message:

Multi object combination condition

Next, we modify the previous rule content and add a class ListItem. After assigning the content, we create an anonymous type with two attributes, user and items. Finally, we make a logical judgment through our multi condition combination.

            var rulesStr = @"[{
                    ""WorkflowName"": ""UserInputWorkflow"",
                    ""Rules"": [
                      {
                        ""RuleName"": ""CheckNestedSimpleProp"",
                        ""ErrorMessage"": ""Value Value is not second."",
                        ""ErrorType"": ""Error"",
                        ""RuleExpressionType"": ""LambdaExpression"",
                        ""Expression"": ""user.UserId==1 && items[0].Value==second""
                      }
                    ]
                  }] ";


            var userInput = new UserInput
            {
                UserId = 1,
                IdNo = "11010519491230002X",
                Age = 18
            };
            var input = new
            {
                user = userInput,
                items = new List<ListItem>()
                {
                    new ListItem{ Id=1,Value="first"},
                    new ListItem{ Id=2,Value="second"}
                }
            };

The output results are as follows:

Validation succeeded: False,Message: Value Value is not second.

How?

For this, we should look at the principle according to the phenomenon. The internal dynamic tree is actually used System.Linq.Dynamic.Core , RulesEngine is built on this library and abstracted. It provides us with a rule engine. Let's try it System.Linq.Dynamic.Core.

We first query the set data and edit a condition string, as shown below:

var items = input.items.AsQueryable().Where("Id == 1").ToList();


foreach (var item in items)
{
    Console.WriteLine($"Id: {item.Id},Value: {item.Value}");
}

Output results:

Id: 1,Value: first

Let's see how we implement it through the expression tree, as shown below:

            Expression<Func<ListItem, bool>> predicate = x => x.Id == 1;
            //The input conditions are as follows
            var inputItem = new ListItem
            {
                Id = 1,
                Value = "second"
            };

            if (inputItem.Id !=null)
            {
                predicate = predicate.And(x=>x.Id==inputItem.Id);
            }

            if (inputItem.Id != null)
            {
                predicate = predicate.And(x => x.Value == inputItem.Value);
            }
            
    public static class PredicateBuilder
    {
        public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expr1,
                                                            Expression<Func<T, bool>> expr2)
        {
            var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());
            return Expression.Lambda<Func<T, bool>>
                  (Expression.And(expr1.Body, invokedExpr), expr1.Parameters);
        }
    }

Normally, it's like the above. We splice conditions. I believe you can think through these conditions to determine what is suitable for you.

If dynamic query is used, the form is as follows:

var items = input.items.AsQueryable().Where("Id ==@0  && Value==@1",inputItem.Id,inputItem.Value).ToList();

Success failure event

For logic verification, since we want to do this, we must know whether it succeeded or failed. We can get the failure and success of logical verification not only through the IsSuccess of the object, but also through two events, as shown below:

            var discountOffered = "";

            resultList.OnSuccess((eventName) =>
            {
                discountOffered = $"Success event:{eventName}.";
            });


            resultList.OnFail(() =>
            {
                discountOffered = "Failure event.";
            });

summary

If you are interested, you can have a look System.Linq.Dynamic.Core , because this project is still used for dynamic expression tree parsing. In addition, the project address is RulesEngine

https://github.com/hueifeng/BlogSample/tree/master/src/RulesEngineDemo

 

https://microsoft.github.io/RulesEngine/

Tags: C#

Posted on Mon, 01 Nov 2021 01:02:22 -0400 by psolus21