.Net Core 环境下构建强大且易用的规则引擎
1. 引言
1.1 为什么需要规则引擎
在业务的早期时代,也许使用硬编码或者逻辑判断就可以满足要求。但随着业务的发展,越来越多的问题会暴露出来:
- 逻辑复杂度带来的编码挑战,需求变更时改变逻辑可能会引起灾难
- 重复性的需求必须可重用,否则必须重复性编码
- 运行期间无法即时修改规则,但重新部署可能会带来其他问题
- 上线前的测试变得繁琐且不可控,必须花大量的人力和时间去测试
这些困境在『 小明历险记:规则引擎 drools 教程一』 一文中可以体会一番,一开始只是简单的根据购物金额来发放积分,运行期间又要更改为更多的规则层次,如果不及时引入对应的规范化处理机制,开发人员将慢慢坠入无止尽的业务深渊。对此,聪明的做法是在系统中引入规则引擎,对业务操作员要提供尽量简单的操作页面来配置规则,规则引擎和配置尽量不要耦合到一块。
1.2 .Net Core 环境下的选择 -- Nrules
目前最流行的规则引擎应该是Drools, 用 Java 语言编写的开放源码规则引擎,使用 Rete 算法对所编写的规则求值,其操作流程如下:

对于 .Net 应用来说,可以通过 Kie 组件提供的 Rest 接口调用规则引擎运算。然而其过于庞大,仅仅只是需要规则引擎计算核心的部分。对此,查找了 .Net 中开源的规则引擎,发现只有同样实现 Rete 算法的 Nrules 满足要求(支持 .Net Core,运行时加载规则引擎)。
注:本文参考借鉴了美团技术团队 从 0 到 1:构建强大且易用的规则引擎 一文的设计思路,对 Drools 从入门到放弃。
2. Nrules 实战 -- 电商促销活动规则引擎设计
2.1 了解 Nrules
NRules 是基于 Rete 匹配算法的.NET 生产规则引擎,基于.NET Standard ,支持 4.5+ 的应用,提供 流式声明规则、运行时构建规则、专门的规则语言(开发中,不推荐使用到生产,基于.Net 4.5 而不是 .NETStandard )。
其计算机制也与其他规则引擎大同小异:

2.2 设计规则配置
前文提到 对业务操作员要提供尽量简单的操作页面来配置规则 ,所以我们定义促销活动的规则配置就要尽量简单。

在设计模型时,我们必须先参考现实生活中遇到的电商促销活动,大致可以想到有这么几种活动类型:满减促销、单品促销、套装促销、赠品促销、满赠促销、多买优惠促销、定金促销等。
在这里,我选择对多买优惠促销做分析,多买促销优惠即所谓的阶梯打折,如买一件9折,买两件8折,其模型大致如下:
public class LadderDiscountPromotion
{
public List<LadderDiscountRuleItem> Rules { get; set; }
public string Name { get; set; }
public DateTime StarTime { get; set; }
public DateTime EndTime { get; set; }
public PromotionState State { get; set; }
public List<string> ProductIdRanges { get; set; }
public bool IsSingle { get; set; }
public string Id { get; set; }
}
public class LadderDiscountRuleItem
{
/// <summary>
/// 数量
/// </summary>
public Int32 Quantity { get; set; }
/// <summary>
/// 打折的百分比
/// </summary>
public Decimal DiscountOff { get; set; }
}
这里为了简化设计,设计的模型并不会去约束平台、活动范围、会员等级等,仅仅约束了使用的产品 id 范围。为了匹配现实中可能出现的组合优惠(类似满减活动后还可以使用优惠券等)现象和相反的独斥现象(如该商品参与xx活动后不支持X券),设置了一个字段来判断是否可以组合优惠,也可以理解为所有活动都为组合优惠,只是有些组合优惠只有一个促销活动。
注:想了解更多关于电商促销系统设计可参考脑图
2.3 规则配置转换
为了实现 规则引擎和配置尽量不要耦合到一块,必须有中间层对规则配置进行转换为 Nrules 能够接受的规则描述。联系前文的计算机制,我们可以得到这样一个描述模型:
public class RuleDefinition
{
/// <summary>
/// 规则的名称
/// </summary>
public String Name { get; set; }
/// <summary>
/// 约束条件
/// </summary>
public List<LambdaExpression> Conditions { get; set; }
/// <summary>
/// 执行行动
/// </summary>
public List<LambdaExpression> Actions { get; set; }
}
由于 Nrules 支持流式声明,所以约束条件和产生的结果都可以用 LambdaExpression 表达式实现。现在我们需要把阶梯打折的配置转换成规则描述,那我们需要先分析一下。假设满一件9折,满两件8折,满三件7折,那我们可以将其分解为:
- 大于等于三件打 7 折
- 大于等于两件且小于三件打 8 折
- 大于等于一件且小于两件 9 折
基于此分析,我们可以看出,只有第一个最多的数量规则是不一样的,其他规则都是比前一个规则的数量小且大于等于当前规则的数量,那么我们可以这样转换我们的规则配置:
List<RuleDefinition> BuildLadderDiscountDefinition(LadderDiscountPromotion promotion)
{
var ruleDefinitions = new List<RuleDefinition>();
//按影响的数量倒叙
var ruleLimits = promotion.Rules.OrderByDescending(r => r.Quantity).ToList();
var currentIndex = 0;
var previousLimit = ruleLimits.FirstOrDefault();
foreach (var current in ruleLimits)
{
//约束表达式
var conditions = new List<LambdaExpression>();
var actions = new List<LambdaExpression>();
if (currentIndex == 0)
{
Expression<Func<Order, bool>> conditionPart =
o => o.GetRangesTotalCount(promotion.ProductIdRanges) >= current.Quantity;
conditions.Add(conditionPart);
}
else
{
var limit = previousLimit;
Expression<Func<Order, bool>> conditionPart = o =>
o.GetRangesTotalCount(promotion.ProductIdRanges) >= current.Quantity
&& o.GetRangesTotalCount(promotion.ProductIdRanges) < limit.Quantity;
conditions.Add(conditionPart);
}
currentIndex = currentIndex + 1;
//触发的行为表达式
Expression<Action<Order>> actionPart =
o => o.DiscountOrderItems(promotion.ProductIdRanges, current.DiscountOff, promotion.Name, promotion.Id);
actions.Add(actionPart);
// 增加描述
ruleDefinitions.Add(new RuleDefinition
{
Actions = actions,
Conditions = conditions,
Name = promotion.Name
});
previousLimit = current;
}
return ruleDefinitions;
}
2.4 生成规则集合
在 Nrules 的 wiki 中,为了实现运行时加载规则引擎,我们需要引入实现 IRuleRepository ,所以我们需要将描述模型转换成 Nrules 中的 RuleSet:
public class ExecuterRepository : IRuleRepository, IExecuterRepository
{
private readonly IRuleSet _ruleSet;
public ExecuterRepository()
{
_ruleSet = new RuleSet("default");
}
public IEnumerable<IRuleSet> GetRuleSets()
{
//合并
var sets = new List<IRuleSet>();
sets.Add(_ruleSet);
return sets;
}
public void AddRule(RuleDefinition definition)
{
var builder = new RuleBuilder();
builder.Name(definition.Name);
foreach (var condition in definition.Conditions)
{
ParsePattern(builder, condition);
}
foreach (var action in definition.Actions)
{
var param = action.Parameters.FirstOrDefault();
var obj = GetObject(param.Type);
builder.RightHandSide().Action(ParseAction(obj, action, param.Name));
}
_ruleSet.Add(new[] { builder.Build() });
}
PatternBuilder ParsePattern(RuleBuilder builder, LambdaExpression condition)
{
var parameter = condition.Parameters.FirstOrDefault();
var type = parameter.Type;
var customerPattern = builder.LeftHandSide().Pattern(type, parameter.Name);
customerPattern.Condition(condition);
return customerPattern;
}
LambdaExpression ParseAction<TEntity>(TEntity entity, LambdaExpression action, String param) where TEntity : class, new()
{
return NRulesHelper.AddContext(action as Expression<Action<TEntity>>);
}
}
2.5 执行规则引擎
做了转换处理仅仅是第一步,我们还必须创建一个规则引擎的处理会话,并把相关的事实对象(fact)传递到会话,执行触发的代码,相关对象发生了变化,其简单代码如下:
var repository = new ExecuterRepository();
//加载规则
repository.AddRule(new RuleDefinition());
repository.LoadRules();
// 生成规则
ISessionFactory factory = repository.Compile();
// 创建会话
ISession session = factory.CreateSession();
// 加载事实对象
session.Insert(new Order());
// 执行
session.Fire();
2.6 应用场景示例
我们假设有这么一个应用入口:传入一个购物车(这里等价于订单)id,获取其可以参加的促销活动,返回对应活动优惠后的结果,并按总价的最低依次升序,那么可以这么写:
public IEnumerable<AllPromotionForOrderOutput> AllPromotionForOrder([FromQuery]String id)
{
var result = new List<AllPromotionForOrderOutput>();
var order = _orderService.Get(id) ?? throw new ArgumentNullException("_orderService.Get(id)");
var promotionGroup = _promotionService.GetActiveGroup();
var orderjson = JsonConvert.SerializeObject(order);
foreach (var promotions in promotionGroup)
{
var tempOrder = JsonConvert.DeserializeObject<Order>(orderjson);
var ruleEngineService = HttpContext.RequestServices.GetService(typeof(RuleEngineService)) as RuleEngineService;
ruleEngineService.AddAssembly(typeof(OrderRemarkRule).Assembly);
ruleEngineService.ExecutePromotion(promotions, new List<object>
{
tempOrder
});
result.Add(new AllPromotionForOrderOutput(tempOrder));
}
return result.OrderBy(i => i.Order.GetTotalPrice());
}
假设这么一个购物车id,买一件时最优惠是参加 A 活动,买两件时最优惠是参加 B 和 C 活动,那么其效果图可能如下:

3. 结语
本文只是对规则引擎及 Nrules 的简单介绍及应用,过程中隐藏了很多细节。在体会到规则引擎的强大的同时,还必须指出其局限性,规则引擎同样不是银弹,必须结合实际出发。
.Net Core 环境下构建强大且易用的规则引擎的更多相关文章
- 在Windows环境下构建Lua 入门
在Windows环境下构建Lua 一:准备软件 1.C-compiler(TDM GCC) http://tdm-gcc.tdragon.net/download 2.Lua源代码 http:// ...
- 【高并发】高并发环境下构建缓存服务需要注意哪些问题?我和阿里P9聊了很久!
写在前面 周末,跟阿里的一个朋友(去年晋升为P9了)聊了很久,聊的内容几乎全是技术,当然了,两个技术男聊得最多的话题当然就是技术了.从基础到架构,从算法到AI,无所不谈.中间又穿插着不少天马行空的想象 ...
- 在Core环境下用WebRequest连接上远程的web Api 实现数据的简单CRUD(附Git地址)
本文所有的东西都是在dot Net Core 1.1环境+VS2017保证测试通过. 本文接着上次文章接着写的,不了解上篇文章的可能看着有点吃力.我尽量让大家都能看懂.这是上篇文章的连接http:// ...
- maven tomcat1.7环境下构建javaweb 项目
tomcat用户权限设置 在tomcat安装路径\conf目录下tomcat-users.xml添加: <role rolename="admin-gui"/> < ...
- .NET Core 环境下使用命令行移除某个 nuget 包的具体方法
.NET Core 使用Nuget包一般是先写入csproj文件,然后下载保存在\bin\Debug\netcoreapp2.0\publish\下,所以可以采用如下方案进行删除操作(其中netcor ...
- Windows 环境下使用强大的wget工具
安装 下载[http://www.interlog.com/~tcharron/wgetwin.html] 解压到目录 比如我解压到D:\Tool\wget 添加wget环境变量,这样使用就更方便了, ...
- docker环境下构建flannel 网络
flannel 是coreos 开发的网络解决方案,为每一台主机分配一个 subnet,容器从此subnet 中分配ip,ip可以在主机间路由.每个subnet从更大的ip池中划分,为了在各个主机间共 ...
- Windows环境下使用强大的wget工具
安装 下载[http://www.interlog.com/~tcharron/wgetwin.html] 解压到目录 比如我解压到D:\Tool\wget 添加wget环境变量,这样使用就更方便了, ...
- (.net core环境下)图形验证,人机交互,一个不够我给你两个
做软件,遇到一些通用性的功能,我想绝大多数同学都是去网上(或自己之前的项目中)搜一段代码出来,贴到项目中,修修改改,完成任务. 但身为一个有追求的软件工程师,怎么能一直忍受这种低级的操作呢?插件化,模 ...
随机推荐
- 使用 FFT 分析周期性数据
可以使用傅里叶变换来分析数据中的变化,例如一个时间段内的自然事件. 天文学家使用苏黎世太阳黑子相对数将几乎 300 年的太阳黑子的数量和大小制成表格.对大约 1700 至 2000 年间的苏黎世数绘图 ...
- C# 默认访问权限
声明类.方法.字段.属性时不加访问权限修饰符时的访问权限是什么呢?1. 声明命名空间.类,前面不加限制访问修饰符时,默认访问权限为internal——访问仅限于当前程序集. 2. 声明类成员(域.属性 ...
- iview起步
ivew是一套基于vue的高质量的ui组件库.使用它我们可以非常简单的得到非常美观的页面和非常棒的用户体验. 1. 获取源码 前往github下载源码,下载地址:https://github.com/ ...
- c#上位机与三菱PLC(FX3U)串口通讯
项目中会经常用到上位机与PLC之间的串口通信,本文介绍一下C#如何编写上位机代码 与三菱FX3U进行通讯 1. 第一种方法是自己写代码实现,主要代码如下: //对PLC的Y7进行置1 byte[] Y ...
- 折腾Java设计模式之备忘录模式
原文地址:折腾Java设计模式之备忘录模式 备忘录模式 Without violating encapsulation, capture and externalize an object's int ...
- 使用Boostrap框架写一个登录\注册界面
Bootstrap是一个Web前端开发框架,使用它提供的css.js文件可以简单.方便地美化HTML控件.一般情况下,对控件的美化需要我们自己编写css代码,并通过标签选择器.类选择器.ID选择器为指 ...
- arcgis api 3.x for js 热力图优化篇-不依赖地图服务(附源码下载)
前言 关于本篇功能实现用到的 api 涉及类看不懂的,请参照 esri 官网的 arcgis api 3.x for js:esri 官网 api,里面详细的介绍 arcgis api 3.x 各个类 ...
- Android为TV端助力 EventBus出现has no public methods called onEvent的问题
Caused by: de.greenrobot.event.EventBusException: Subscriber class com.hhzt.iptv.lvb_w.socket.MyMsgS ...
- spark als scala实现(二)
Vi t1.txt1,101,5.01,102,3.01,103,2.52,101,2.02,102,2.52,103,5.02,104,2.03,101,2.53,104,4.03,105,4.5 ...
- Mysql事务与锁详解
脏读: 不可重复读: 幻读: 锁: 表级别的意向锁为了提高效率, 我们能给一张表成功加上一个表锁的前提是:没有任何一个事务对这张表的某些行加了锁. 如果没有意向表锁: 如果现在要给一个表加上表锁. 如 ...