• C# RulesEngine 规则引擎:从入门到看懵


    说明

    RulesEngine 是 C# 写的一个规则引擎类库,读者可以从这些地方了解它:

    仓库地址:

    https://github.com/microsoft/RulesEngine

    使用方法:

    https://microsoft.github.io/RulesEngine

    文档地址:

    https://github.com/microsoft/RulesEngine/wiki

    什么是规则引擎?

    照搬 https://github.com/microsoft/RulesEngine/wiki/Introduction#what-is-the-rules-engine

    在企业项目中,关键或核心部分总是业务逻辑或业务规则,也就是 CRUD,这些系统都有一个共同的特征是,某个模块中的一些或许多规则或策略总会发生变化,例如购物网站的顾客折扣、物流企业的运价计算等。随着这些变化而来的是大量的重复工作,如果系统没有足够的抽象,那么每当增加一种规则时,开发者需要在规则、回归测试、性能测试等方面的变化中编写代码。

    在 RulesEngine 中,微软对规则进行了抽象,这样核心逻辑总是得到稳定的、易于维护的,而规则的更改可以以一种简单的方式生成,而不需要更改代码库。此外,系统的输入本质上是动态的,因此不需要在系统中定义模型,而是可以作为扩展对象或任何其他类型的对象作为输入,系统经过预定义的规则处理后,输出结果。

    它有以下特性:

    • Json based rules definition (基于 Json 的规则定义)
    • Multiple input support (多输入支持)
    • Dynamic object input support (动态对象输入支持)
    • C# Expression support (C # 表达式支持)
    • Extending expression via custom class/type injection (通过自定义类/类型注入扩展表达式)
    • Scoped parameters (范围参数)
    • Post rule execution actions (发布规则执行操作)

    说人话就是,业务逻辑的输出结果受到多个因子影响,但是这些影响有一定规律的,那么适合将这些部分抽象出来,接着使用规则引擎处理,例如购物的各种优惠卷叠加之后的最终折扣价、跨区运输的不同类型的包裹运价计算等。

    笔者认为这个规则引擎主要由两部分构成:

    • 规则验证系统,例如根据规则验证字段、执行函数验证当前流程、输出执行结果;
    • 动态代码引擎,能够将字符串转换为动态代码,利用表达式树这些完成;

    当然,这样说起来其实很抽象的,还得多撸代码,才能明白这个 RulesEngine 到底是干嘛的。

    安装

    新建项目后,nuget 直接搜索 RulesEngine 即可安装,在 nuget 介绍中可以看到 RulesEngine 的依赖:

    FluentValidation 是一个用于构建强类型验证规则的 .NET 库,在 ASP.NET Core 项目中,我们会经常使用模型验证,例如必填字段使用 [Required]、字符串长度使用 [MaxLength] 等;但是因为是特性注解,也就是难以做到很多需要经过动态检查的验证方式,使用 FluentValidation 可以为模型类构建更加丰富的验证规则。

    而 FluentValidation 用在 RulesEngine 上,也是相同的用途,RulesEngine 最常常用做规则验证,检查模型类或业务逻辑的验证结果,利用 FluentValidation 中丰富的验证规则,可以制作各种方便的表达式树,构建动态代码。

    怎么使用

    我们通过 RulesEngine 检查模型类的字段是否符合规则,来了解 RulesEngine 的使用方法。

    创建一个这样的模型类:

    public class Buyer
    {
        public int Id { get; set; }
        public int Age { get; set; }
        // 是否为已认证用户
        public bool Authenticated { get; set; }
    }
    

    场景是这样的,用户下单购买商品,后台需要判断此用户是否已经成年是否通过了认证

    正常来看代码应该这样写:

    if(Authenticated == true && Age > 18)
    

    但是如果年龄调为 16 岁呢?如果最近公司搞活动,不需要上传身份证就能购买商品呢?

    当然定义变量存储到数据库也行,但是如果后面又新增了几个条件,那么我们就需要修改代码了,大佬说,这样不好,我们要 RulesEngine 。

    好的,那我们来研究一下这个东西。

    前面提到的 if(Authenticated == true && Age > 18),这么一个完整的验证过程,在 RulesEngine 称为 Workflow,每个 Workflow 下有多个 Rule。

    if(Authenticated == true && Age > 18) => Workflow
    	  Authenticated == true			  => Rule
    	  Age > 18						  => Rule
    

    在 RulesEngine 中,有两种方法定义这些 Workflow 和 Rule,一种是使用代码,一种是 JSON,官方是推荐使用 JSON 的,因为 JSON 可以动态生成,可以实现真正的动态。

    下面我们来看看如何使用 JSON 和代码,分别定义 if(Authenticated == true && Age > 18) 这个验证过程。

    JSON 定义:

    [
      {
        "WorkflowName": "Test",
        "Rules": [
          {
            "RuleName": "CheckAuthenticated",
            "Expression": "Authenticated == true"
          },
          {
            "RuleName": "CheckAge",
            "Expression": "Age >= 18"
          }
        ]
      }
    ] 
    
            var rulesStr = "[{... ...}]" // JSON
            var workflows = JsonConvert.DeserializeObject>(rulesStr);
    

    C# 代码:

            var workflows = new List();
            List rules = new List();
    
            Workflow exampleWorkflow = new Workflow();
            exampleWorkflow.WorkflowName = "Test";
            exampleWorkflow.Rules = rules;
            workflows.Add(exampleWorkflow);
    
            Rule authRule = new Rule();
            authRule.RuleName = "CheckAuthenticated";
            authRule.Expression = "Authenticated == true";
            rules.Add(authRule);
    
            Rule ageRule = new Rule();
            ageRule.RuleName = "CheckAge";
            ageRule.Expression = "Age >= 18";
            rules.Add(ageRule);
    

    两种方式都是一样的,每个 Workflow 下有多个 Rule,可以定义多个 Workflow。

    当前我们有两个地方要了解:

            "RuleName": "CheckAuthenticated",
            "Expression": "Authenticated == true"
    

    RuleName:规则名称;

    Expression: 真实的代码,必须是符合 C# 语法的代码;

    定义好 Workflow 和 Rule 后,我们需要生成规则引擎,直接 new RulesEngine.RulesEngine() 即可:

            var bre = new RulesEngine.RulesEngine(workflows.ToArray());
    

    生成引擎是需要一些时间的。

    生成引擎后,我们通过名称指定调用一个 Workflow,并获取每个 Rule 的验证结果:

            List resultList = await bre.ExecuteAllRulesAsync("Test", new Buyer
            {
                Id = 666,
                Age = 17,
                Authenticated = false
            });
    

    完整代码示例如下:

        static async Task Main()
        {
            // 定义
            var rulesStr = ... ...// JSON
            // 生成 Workflow[ Rule[] ]
            var workflows = JsonConvert.DeserializeObject>(rulesStr)!;
            var bre = new RulesEngine.RulesEngine(workflows.ToArray());
    
            // 调用指定的 Workflow,并传递参数,获取每个 Rule 的处理结果
            List resultList = await bre.ExecuteAllRulesAsync("Test", new Buyer
            {
                Id = 666,
                Age = 17,
                Authenticated = false
            });
    
            // 打印输出
            foreach (var item in resultList)
            {
                Console.WriteLine("规则名称:{0},    验证结果:{1}", item.Rule.RuleName, item.IsSuccess);
            }
        }
    

    多参数

    如果商品需要 VIP 才能购买呢?

    这里我们再定义一个模型类,表示一个用户是否为 VIP。

    public class VIP
    {
        public int Id { get; set; }
        public bool IsVIP { get; set; }
    }
    

    那么这个时候就需要处理两个模型类了,为了能够在 Rule 中使用所有的模型类,我们需要为每个模型类定义 RuleParameter

            var rp1 = new RuleParameter("buyer", new Buyer
            {
                Id = 666,
                Age = 20,
                Authenticated = true
            });
    
            var rp2 = new RuleParameter("vip", new VIP
            {
                Id = 666,
                IsVIP = false
            });
    

    相当于表达式树:

                ParameterExpression rp1 = Expression.Parameter(typeof(Buyer), "buyer");
                ParameterExpression rp2 = Expression.Parameter(typeof(VIP), "vip");
    

    可以参考笔者的表达式树系列文章:https://ex.whuanle.cn/

    然后重新设计 JSON,增加一个 Rule:

    [{
    	"WorkflowName": "Test",
    	"Rules": [{
    			"RuleName": "CheckAuthenticated",
    			"Expression": "buyer.Authenticated == true"
    		},
    		{
    			"RuleName": "CheckAge",
    			"Expression": "buyer.Age >= 18"
    		},
    		{
    			"RuleName": "CheckVIP",
    			"Expression": "vip.IsVIP == true"
    		}
    	]
    }]
    

    然后执行此 Workflow:

    List resultList = await bre.ExecuteAllRulesAsync("Test", rp1, rp2);
    

    完整代码:

        static async Task Main()
        {
            // 定义
            var rulesStr = ... ... // JSON
            var workflows = JsonConvert.DeserializeObject>(rulesStr)!;
            var bre = new RulesEngine.RulesEngine(workflows.ToArray());
    
            var rp1 = new RuleParameter("buyer", new Buyer
            {
                Id = 666,
                Age = 20,
                Authenticated = true
            });
    
            var rp2 = new RuleParameter("vip", new VIP
            {
                Id = 666,
                IsVIP = false
            });
    
            List resultList = await bre.ExecuteAllRulesAsync("Test", rp1, rp2);
    
            foreach (var item in resultList)
            {
                Console.WriteLine("规则名称:{0},    验证结果:{1}", item.Rule.RuleName, item.IsSuccess);
            }
        }
    

    全局参数、本地参数

    全局参数

    在 Workflow 中可以定义全局参数,参数对 Workflow 内的所有 Rule 起效,所有 Rule 都可以使用它。

    定义示例:

    	"WorkflowName": "Test",
    	"GlobalParams": [{
    		"Name": "age",
    		"Expression": "buyer.Age"
    	}],
    

    参数的值,可以定义为常量,也可以来源于传入的参数。

    修改上一个小节的示例,在 Rule CheckAge 中,使用这个全局参数。

    [{
    	"WorkflowName": "Test",
    	"GlobalParams": [{
    		"Name": "age",
    		"Expression": "buyer.Age"
    	}],
    	"Rules": [{
    			"RuleName": "CheckAuthenticated",
    			"Expression": "buyer.Authenticated == true"
    		},
    		{
    			"RuleName": "CheckAge",
    			"Expression": "age >= 18"
    		},
    		{
    			"RuleName": "CheckVIP",
    			"Expression": "vip.IsVIP == true"
    		}
    	]
    }]
    

    本地参数

    本地参数在 Rule 内定义,只对当前 Rule 起效。

    [{
    	"WorkflowName": "Test",
    	"Rules": [{
    			"RuleName": "CheckAuthenticated",
    			"LocalParams": [{
    				"Name": "age",
    				"Expression": "buyer.Age"
    			}],
    			"Expression": "buyer.Authenticated == true"
    		},
    		{
    			"RuleName": "CheckAge",
    			"Expression": "age >= 18"
    		},
    		{
    			"RuleName": "CheckVIP",
    			"Expression": "vip.IsVIP == true"
    		}
    	]
    }]
    

    在定义参数时,参数的值可以通过执行函数来获取:

          "LocalParams":[
            {
              "Name":"mylocal1",
              "Expression":"myInput.hello.ToLower()"
            }
          ],
    

    LocalParams 可以使用 GlobalParams 的参数再次生成新的变量。

      "GlobalParams":[
        {
          "Name":"myglobal1"
          "Expression":"myInput.hello"
        }
      ],
      "Rules":[
        {
          "RuleName": "checkGlobalAndLocalEqualsHello",
          "LocalParams":[
            {
              "Name": "mylocal1",
              "Expression": "myglobal1.ToLower()"
            }
          ]
        },
    

    定义验证成功、失败行为

    可以为每个 Rule 定义验证成功和失败后执行一些代码。

    格式示例:

            "Actions": {
               "OnSuccess": {
                  "Name": "OutputExpression",
                  "Context": {
                     "Expression": "input1.TotalBilled * 0.8"
                  }
               },
               "OnFailure": {
                   "Name": "EvaluateRule",
                   "Context": {
                       "WorkflowName": "inputWorkflow",
                       "ruleName": "GiveDiscount10Percent"
                   }
               }
            }
    

    OutputExpression 里面定义了执行代码:

                  "Name": "OutputExpression",
                  "Context": {
                     "Expression": "input1.TotalBilled * 0.8"
                  }
    

    EvaluateRule 定义了执行另一个 Workflow 的 Rule,

                   "Name": "EvaluateRule",
                   "Context": {
                       "WorkflowName": "inputWorkflow",
                       "ruleName": "GiveDiscount10Percent"
                   }
    

    OnSuccessOnFailure 里面,内部结构如下所示:

                  "Name": "OutputExpression",  //Name of action you want to call
                  "Context": {  //This is passed to the action as action context
                     "Expression": "input1.TotalBilled * 0.8"
                  }
    
                  "Name": "EvaluateRule",
                   "Context": {
                       "WorkflowName": "inputWorkflow",
                       "ruleName": "GiveDiscount10Percent"
                   }
                  
    

    Name:{xxx} 中的 {xxx} 是一个具体的执行器名称,不是随便定义的,OutputExpressionEvaluateRule 都是自带的执行器,所谓的执行器就是一个 Func,在后面的 自定义执行器 中,可以了解更多。

    Context 里面的内容,是一个字典,这些 Key/Value 会被当做参数传递给执行器,每个执行器要求设置的 Context 是不一样的。

    另外每个 Rule 都可以定义以下三个字段:

          "SuccessEvent": "10",
          "ErrorMessage": "One or more adjust rules failed.",
          "ErrorType": "Error",
    

    ErrorType 有两个选项,WarnError,如果这个 Rule 的表达式错误,那么是否弹出异常。如果设置为 Warn, Rule 有问题,验证结果则会是 false,而不会报异常;如果是 Error,那么这个 Rule 会中止 Workflow 的执行,程序会报错。

    SuccessEventErrorMessage 对应,只是成功、失败的提示消息。

    计算折扣

    前面提到的都是验证规则,接下来我们将会使用 RulesEngine 实现规则计算。

    这里规定,基础折扣为 1.0,如果用户小于 18 岁,打 9 折,如果用户是 VIP,打 9 折,两个规则独立。

    如果是小于 18岁,则 1.0 * 0.9
    如果是 VIP,    则 1.0 * 0.9 
    

    定义一个模型类,用于传递折扣基值。

    // 折扣
    public class Discount
    {
        public double Value
        {
            get; set;
        }
    }
    

    定义三个参数:

            var rp1 = new RuleParameter("buyer", new Buyer
            {
                Id = 666,
                Age = 16,
            });
    
            var rp2 = new RuleParameter("vip", new VIP
            {
                Id = 666,
                IsVIP = true
            });
    
            var rp3 = new RuleParameter("discount", new Discount
            {
                Value = 1.0
            });
    

    定义规则计算,每个规则计算的是自己的折扣:

    [{
    	"WorkflowName": "Test",
    	"GlobalParams": [{
    		"Name": "value",
    		"Expression": "discount.Value"
    	}],
    	"Rules": [{
    			"RuleName": "CheckAge",
    			"Expression": "buyer.age < 18",
    			"Actions": {
    				"OnSuccess": {
    					"Name": "OutputExpression",
    					"Context": {
    						"Expression": "value * 0.9"
    					}
    				}
    			}
    		},
    		{
    			"RuleName": "CheckVIP",
    			"Expression": "vip.IsVIP == true",
    			"Actions": {
    				"OnSuccess": {
    					"Name": "OutputExpression",
    					"Context": {
    						"Expression": "value * 0.9"
    					}
    				}
    			}
    		}
    	]
    }]
    

    完整代码:

        static async Task Main()
        {
            // 定义
            var rulesStr =  ... ... // JSON
            var workflows = JsonConvert.DeserializeObject>(rulesStr)!;
            var bre = new RulesEngine.RulesEngine(workflows.ToArray());
    
            var rp1 = new RuleParameter("buyer", new Buyer
            {
                Id = 666,
                Age = 16,
            });
    
            var rp2 = new RuleParameter("vip", new VIP
            {
                Id = 666,
                IsVIP = true
            });
    
            var rp3 = new RuleParameter("discount", new Discount
            {
                Value = 1.0
            });
    
            List resultList = await bre.ExecuteAllRulesAsync("Test", rp1, rp2, rp3);
            var discount = 1.0;
            foreach (var item in resultList)
            {
                if (item.ActionResult != null && item.ActionResult.Output != null)
                {
                    Console.WriteLine($"{item.Rule.RuleName} 折扣优惠:{item.ActionResult.Output}");
                    discount = discount * (double)item.ActionResult.Output;
                }
            }
            Console.WriteLine($"最终折扣:{discount}");
        }
    

    笔者这里的示例是,每个规则只计算自己的折扣,也就是每个 Rule 都是独立的,下一个 Rule 不会在上一个 Rule 结果上计算。

    < 18 : 0.9
    VIP  : 0.9
    

    如果是折扣可以叠加,那么就是 0.9*0.9 ,最终可以拿到 0.81 的折扣。

    如果折扣不能叠加,只能选择最佳的优惠,那么就是 0.9

    使用自定义函数

    自定义函数有两种静态函数和实例函数两种,我们可以在 Expression 中调用预先写好的函数。

    下面讲解如何在 Rule 中调用自定义的函数。

    静态函数

    自定义静态函数:

        public static bool CheckAge(int age)
        {
            return age >= 18;
        }
    

    注册类型:

            ReSettings reSettings = new ReSettings
            {
                CustomTypes = new[] { typeof(Program) }
            };
    
            var bre = new RulesEngine.RulesEngine(Workflows: workflows.ToArray(), reSettings: reSettings);
    

    使用静态函数:

    [{
    	"WorkflowName": "Test",
    	"Rules": [{
    		"RuleName": "CheckAge",
    		"Expression": "Program.CheckAge(buyer.Age) == true"
    	}]
    }]
    

    完整代码:

        static async Task Main()
        {
            // 定义
            var rulesStr = "[{\"WorkflowName\":\"Test\",\"Rules\":[{\"RuleName\":\"CheckAge\",\"Expression\":\"Program.CheckAge(buyer.Age) == true\"}]}]";
            var workflows = JsonConvert.DeserializeObject>(rulesStr)!;
    
            ReSettings reSettings = new ReSettings
            {
                CustomTypes = new[] { typeof(Program) }
            };
    
            var bre = new RulesEngine.RulesEngine(Workflows: workflows.ToArray(), reSettings: reSettings);
            List resultList = await bre.ExecuteAllRulesAsync("Test", new Buyer
            {
                Age = 16
            });
    
            foreach (var item in resultList)
            {
                Console.WriteLine("规则名称:{0},    验证结果:{1}", item.Rule.RuleName, item.IsSuccess);
            }
        }
    
        public static bool CheckAge(int age)
        {
            return age >= 18;
        }
    

    实例函数

    定义实例函数:

        public bool CheckAge(int age)
        {
            return age >= 18;
        }
    

    通过 RuleParameter 参数的方式,传递实例:

            var rp1 = new RuleParameter("p", new Program());
    

    通过参数的名称调用函数:

    [{
    	"WorkflowName": "Test",
    	"Rules": [{
    		"RuleName": "CheckAge",
    		"Expression": "p.CheckAge(buyer.Age) == true"
    	}]
    }]
    

    完整代码:

        static async Task Main()
        {
            // 定义
            var rulesStr = "[{\"WorkflowName\":\"Test\",\"Rules\":[{\"RuleName\":\"CheckAge\",\"Expression\":\"p.CheckAge(buyer.Age) == true\"}]}]";
            var workflows = JsonConvert.DeserializeObject>(rulesStr)!;
    
            var rp1 = new RuleParameter("p", new Program());
    
            var bre = new RulesEngine.RulesEngine(Workflows: workflows.ToArray());
            List resultList = await bre.ExecuteAllRulesAsync("Test", new Buyer
            {
                Age = 16
            }, rp1);
    
            foreach (var item in resultList)
            {
                Console.WriteLine("规则名称:{0},    验证结果:{1}", item.Rule.RuleName, item.IsSuccess);
            }
        }
    
        public bool CheckAge(int age)
        {
            return age >= 18;
        }
    

    自定义执行器

    自定义执行器就是 OnSuccessOnFailure 这部分的自定义执行代码,相比静态函数、实例函数,使用自定义执行器,可以获取 Rule 的一些数据。

    		"Actions": {
    			"OnSuccess": {
    				"Name": "MyCustomAction",
    				"Context": {
    					"customContextInput": "0.9"
    				}
    			}
    		}
    

    自定义一个执行器,执行器需要继承 ActionBase

    public class MyCustomAction : ActionBase
    {
        public override async ValueTask<object> Run(ActionContext context, RuleParameter[] ruleParameters)
        {
            var customInput = context.GetContext<string>("customContextInput");
            return await ValueTask.FromResult(new object());
        }
    }
    

    定义 ReSettings,并在构建规则引擎时,传递进去:

            var b = new Buyer
            {
                Age = 16
            };
            var reSettings = new ReSettings
            {
                CustomActions = new Dictionary<string, Func>
                {
                    {"MyCustomAction", () => new MyCustomAction() }
                }
            };
    
            var bre = new RulesEngine.RulesEngine(workflows.ToArray(), reSettings);
    
            List resultList = await bre.ExecuteAllRulesAsync("Test", b);
    

    定义 JSON 规则:

    [{
    	"WorkflowName": "Test",
    	"Rules": [{
    		"RuleName": "CheckAge",
    		"Expression": "Age <= 18 ",
    		"Actions": {
    			"OnSuccess": {
    				"Name": "MyCustomAction",
    				"Context": {
    					"customContextInput": "0.9"
    				}
    			}
    		}
    	}]
    }]
    

  • 相关阅读:
    【基础架构】part-1 高可用策略和知识点总结
    关于使用es数组的改变方式
    节流(Throttle)和防抖(Debounce)
    MIMO雷达中波形复用/分离的方法------TDMA\FDMA\DDMA\CDMA
    selenium安装和python中基本使用
    电压源的电路分析知识分享
    yolo v5 坐标相关的判断与转换,评价指标,训练结果解析
    飞桨EasyDL-Mac本地部署离线SDK-Linux集成Python
    本地如何使用HTTPS进行调试
    企业级软件定制开发的特点有哪些?
  • 原文地址:https://www.cnblogs.com/whuanle/p/16830333.html