Detailed explanation of the use of .NET RulesEngine (rule engine)

Time:2022-9-13

An accidental opportunity, let me take out RulesEngine to complete a business. For the business, it is mainly to complete a business of scalability (uncertain types, as well as uncertain conditions, changes in conditions may be continuously increased and modified) judge. For example, when an achievement system is completed, administrators can create it. For achievements, there are one-time unlocking, daily, weekly, and reset at any time. It is triggered every time it is achieved. Facing the increase of achievement tasks, it will For programmers, it is a headache to add and modify these achievement tasks every time. Well, everyone should have a simple understanding of this, then follow the author down, let's see how to use very little code to complete a simple dynamic logic processing in .NET.

RulesEngine overview

RulesEngine is a rule engine project launched by Microsoft, which is used for business logic/rules/policies abstracted in the system. In the process of our development, it is inevitable to deal with this repeated business logic, and for this kind of dynamic rules, it is a more elegant way, using us to reduce our code Or the modification of the project.

how to use

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


dotnet add package RulesEngine 

For the configuration of the rules, you can directly pass the typed parameters. The author mainly uses the JSON configuration to demonstrate it for everyone to understand clearly.

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

As shown above, we define the rule information. For this information, the author stores JSON data by default for the rule information. Of course, you can store the following content, and split the following data structure into the database.

Attributes describe
RuleName rule name
Properties Rule property, get or set the custom property or tag of the rule
Operator operator
ErrorMessage wrong information
Enabled Get and set whether the rule is enabled
RuleExpressionType Regular expression type, the default is LambdaExpression, of course, there is only one such
WorkflowRulesToInJect Inject workflow rules
Rules rule
LocalParams local parameters
Expression expression tree
Actions
SuccessEvent Completion event, defaults to the rule name

Let's take a look at the result of this code, for which I created a class like this:


   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 the 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(&quot;Verification successful: {0}, message: {1}&quot;, item.IsSuccess, item.ExceptionMessage);
            }

            Console.ReadLine();

        }

The output is as follows:

Validation succeeded: False, Message: Age must be greater than 18.
Verification successful: False, message: ID number cannot be empty.

return structureresultListAs follows:

{ &quot;Rule&quot;:{ &quot;RuleName&quot;:&quot;CheckNestedSimpleProp&quot;,&quot;Properties&quot;:null,&quot;Operator&quot;:null,&quot;ErrorMessage&quot;:&quot;Age must be greater than 18.&quot;,
                "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} },
                &quot;ActionResult&quot;:{ &quot;Output&quot;:null,&quot;Exception&quot;:null},&quot;ExceptionMessage&quot;:&quot;Age must be greater than 18.&quot;,&quot;RuleEvaluatedParams&quot;:[]}

Using extension methods within expression trees

The above I believe that everyone has a simple understanding of the use of the rule engine, let's take another advanced version of the content.

For example, I think that the age entered is not accurate, and I want to calculate the age through the ID number, then how should I operate, under normal circumstances, we will pass the extension method, and then pass the ID number parameter to the handler to process After the program calculation is completed, it will return us the age, and how should we operate in this? Let's look down.

Add custom types through ReSettings and 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) }
        };

Modify the following:


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

The output is as follows:

Validation succeeded: False, Message: Age must be less than 18 years old.
Validation succeeded: True, message:

Multiple Object Combination Conditions

Next, we modified the content of the previous rules and added a class ListItem. After assigning the content, we create an anonymous type with two attributes, user and items, and finally make logical judgments through our multi-condition combination.

var rulesStr = @"[{
                    ""WorkflowName"": ""UserInputWorkflow"",
                    ""Rules"": [
                      {
                        ""RuleName"": ""CheckNestedSimpleProp"",
                        &quot;&quot;ErrorMessage&quot;&quot;: &quot;&quot;Value is not second.&quot;&quot;,
                        ""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 is as follows:

Validation succeeded: False, message: Value value is not second.

How?

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

We first query the collection data and edit a condition string as follows:


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


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

Output result:

Id:1,Value: first

Then let's take a look at how we implement it through the expression tree, as follows:

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 is like the above. We splicing the conditions. I believe that everyone can think through one of these conditions to determine what suits you.

If you use the dynamic query form as follows:


var items = input.items.AsQueryable().Where("Id [email protected]  && [email protected]",inputItem.Id,inputItem.Value).ToList();

success failure event

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

var discountOffered = "";

            resultList.OnSuccess((eventName) =>
            {
                discountOffered = $&quot;Success Event:{eventName}.&quot;;
            });


            resultList.OnFail(() =>
            {
                discountOffered = &quot;Failed event.&quot;;
            });

Summarize

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

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

The above is the detailed content of the use of .NET RulesEngine (rule engine). For more information on the use of .NET RulesEngine (rule engine), please pay attention to other related articles on developpaer!