首页 > 编程语言 >C# RulesEngine 规则引擎:从入门到看懵

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

时间:2022-12-20 12:32:01浏览次数:61  
标签:入门 C# Rule var RuleName RulesEngine new Expression

目录

  • ​​说明​​
  • ​​安装​​
  • ​​怎么使用​​
  • ​​多参数​​
  • ​全局参数、本地参数​
  • ​​全局参数​​
  • ​​本地参数​​
  • ​​定义验证成功、失败行为​​
  • ​​计算折扣​​
  • ​使用自定义函数​
  • ​​静态函数​​
  • ​​实例函数​​
  • ​​自定义执行器​​

说明

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 (发布规则执行操作)

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

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

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

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

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

安装

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

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

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<List<Workflow>>(rulesStr);

C# 代码:

var workflows = new List<Workflow>();
List<Rule> rules = new List<Rule>();

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 = "CheckAuthenticated";
ageRule.Expression = "Authenticated == true";
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<RuleResultTree> 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<List<Workflow>>(rulesStr)!;
var bre = new RulesEngine.RulesEngine(workflows.ToArray());

// 调用指定的 Workflow,并传递参数,获取每个 Rule 的处理结果
List<RuleResultTree> 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);
}
}

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

多参数

如果商品需要 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<RuleResultTree> resultList = await bre.ExecuteAllRulesAsync("Test", rp1, rp2);

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

完整代码:

static async Task Main()
{
// 定义
var rulesStr = ... ... // JSON
var workflows = JsonConvert.DeserializeObject<List<Workflow>>(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<RuleResultTree> 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​​ 中,使用这个全局参数。

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

[{
"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 起效。

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

[{
"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​​ 的参数再次生成新的变量。

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

"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"
}

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

在 ​​OnSuccess​​​ 、​​OnFailure​​ 里面,内部结构如下所示:

"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}​​​ 是一个具体的执行器名称,不是随便定义的,​​OutputExpression​​​、​​EvaluateRule​​​ 都是自带的执行器,所谓的执行器就是一个 ​​Func<ActionBase>​​,在后面的 ​​自定义执行器​​ 中,可以了解更多。

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

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

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

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

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

​SuccessEvent​​​ 跟 ​​ErrorMessage​​ 对应,只是成功、失败的提示消息。

计算折扣

前面提到的都是验证规则,接下来我们将会使用 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"
}
}
}
}
]
}]

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

完整代码:

static async Task Main()
{
// 定义
var rulesStr = ... ... // JSON
var workflows = JsonConvert.DeserializeObject<List<Workflow>>(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<RuleResultTree> 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<List<Workflow>>(rulesStr)!;

ReSettings reSettings = new ReSettings
{
CustomTypes = new[] { typeof(Program) }
};

var bre = new RulesEngine.RulesEngine(Workflows: workflows.ToArray(), reSettings: reSettings);
List<RuleResultTree> 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<List<Workflow>>(rulesStr)!;

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

var bre = new RulesEngine.RulesEngine(Workflows: workflows.ToArray());
List<RuleResultTree> 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;
}

自定义执行器

自定义执行器就是 ​​OnSuccess​​​、​​OnFailure​​ 这部分的自定义执行代码,相比静态函数、实例函数,使用自定义执行器,可以获取 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<ActionBase>>
{
{"MyCustomAction", () => new MyCustomAction() }
}
};

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

List<RuleResultTree> resultList = await bre.ExecuteAllRulesAsync("Test", b);

定义 JSON 规则:

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

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

一个逗逗的大学生



标签:入门,C#,Rule,var,RuleName,RulesEngine,new,Expression
From: https://blog.51cto.com/u_10006690/5954724

相关文章

  • Exchange灾难恢复
     从be还原邮箱数据库文件和日志后,在ecp直接挂载会提示无法挂载,然后使用esentutl命令进行修复,修复完成后,再将用户邮箱关联到该数据库即可。 创建恢复数据库:New-MailboxData......
  • 一款基于 chatGPT和WeChat的微信机器人
     1、灵活使用chatGPT首先注册openapi账号是重中之重,网址:https://chat.openai.com/chat拿到apikey   其次这个有一个使用和收费政策,根据个人情况来试用,是按......
  • AutoFac
    AutoFac的配置使用一.什么是AutoFac第三方IOC容器二.优点它是C#语言联系很紧密,也就是说C#里的很多编程方式都可以为Autofac使用,例如可以用Lambda表达式注册组件较......
  • Sql 中常用日期转换Convert(Datetime)
    Sql中常用日期转换Convert(Datetime) CONVERT(data_type,expression[,style]) convert(varchar(10),字段名,转换格式)说明:此样式一般在时间类型(datetime,smallda......
  • .Net 6 使用Code Frist EF简单数据迁移
    1.定义 EFCodeFirst上下文StuContext类和一个类作为模型 映射字段 usingSystem.Data.Entity;usingSystem.Collections.Generic;usingSy......
  • HMS Core 3D流体仿真技术,打造移动端PC级流体动效
    移动设备硬件的高速发展,让游戏行业发生翻天覆地的变化,许多酷炫的游戏效果不再局限于电脑端,玩家在移动端就能享受到场景更逼真、画质更清晰、体验更流畅的游戏服务。但由于......
  • Spring Statemachine状态机的概念(二)
    使用作用域状态机对作用域的支持非常有限,但您可以通过以下两种方式之一使用普通的Spring注释来启用范围:​​session​​​​@Scope​​如果状态机是使用构建器手动构建的......
  • 如何优化大场景实时渲染?HMS Core 3D Engine这么做
    在先前举办的华为开发者大会2022(HDC)上,华为通过3D数字溪村展示了自有3D引擎“HMSCore3DEngine”(以下简称3DEngine)的强大能力。作为一款高性能、高画质、高扩展性的3D引擎......
  • Spring Statemachine状态机的概念(三)
    状态机服务状态机服务是更高级别的实现,旨在提供更多用户级功能,简化正常运行时间操作。目前只有一个服务接口()存在。​​StateMachineService​​用​​StateMachineServi......
  • C# 数字转大写汉字
    1.数字转换成汉字大写publicstringNumToChinese(stringx){//数字转换为中文后的数组string[]P_array_num=newstring[]{"......