基于 XAF Blazor 的规则引擎编辑器 - 实战篇

时间:2024-03-15 08:24:06

示例项目:https://gitee.com/easyxaf/recharge-rules-engine-sample

前言

继上一篇文章对规则引擎编辑器进行了初步介绍之后,本文将通过实际应用案例深入探讨规则引擎编辑器的使用方法。编辑器的操作相对简单,我们将重点放在RulesEngine的讲解上。请注意,本文不是RulesEngine的入门教程,如果您对RulesEngine尚不熟悉,建议先行查阅其官方文档, https://microsoft.github.io/RulesEngine

RulesEngine

这里要说一下在使用RulesEngine时的一些注意事项

RulesEngine中的Workflow类是规则信息的核心载体。它不仅包含了一个规则列表(Rules),而且每个Rule内部同样嵌套着一个规则列表。这样的设计形成了一个多层次的树状结构。然而,值得注意的是,在这个结构中,只有叶节点的表达式会被实际执行。也就是说,如果一个Rule内部的Rules列表非空,那么即使该Rule定义了表达式,它也不会被执行,它的运行结果由子Rule来决定。

对于嵌套的Rule(即子Rule),其执行方式可以通过NestedRuleExecutionMode进行配置。默认情况下,该模式设置为All,意味着所有规则都将被执行,而不考虑Rule中设置的运算符(Operator)。另一种模式是Performance,即性能模式,它会根据Rule中Operator的值来决定执行逻辑:当Operator为And或AndAlso时,如果任一子Rule返回false,则停止执行;当Operator为Or或OrElse时,如果任一子Rule返回true,则停止执行。这种模式是全局性的,适用于所有子Rule。需要注意的是,Workflow中的Rules是*Rule,不是嵌套Rule,不受这个设置的限制。除非有特殊需求,否则通常建议保持默认的All设置。后文将进一步介绍这两种模式的具体应用场景。

每个Rule都包含一个Actions属性,Actions同时又包含OnSuccess和OnFailure这两个子属性。需要注意的是,Workflow中的所有Rule执行完毕后,才会根据结果执行相应的OnSuccess或OnFailure动作。当Rule的结果IsSuccess为true时,将执行OnSuccess;反之,则执行OnFailure。RulesEngine内部默认提供了OutputExpressionAction和EvaluateRuleAction这两种动作。通过OutputExpressionAction,我们可以设置输出表达式。每个Rule都保存有自己的输出值,因此在规则执行完毕后,我们需要自行遍历并检索这些输出值,需要注意的是,输出结果只有一个Output属性,如果我们想区分不同的输出值,我们需要在Contenxt中设置类型信息,在读取值时再通过这个类型信息用于区分不同的值。

示例

在深入探讨之前,我想向大家推荐一个项目:http://waitmoon.com/zh/guide 。这是一个基于Java语言开发的规则引擎,该项目的设计理念和功能实现在我设计规则引擎编辑器的过程中给予了我极大的启发。接下来的示例将借鉴它文档中的案例,以助于我们更好地理解和应用规则引擎的概念。如果您对规则引擎感兴趣,或者正在寻求灵感,这个项目绝对值得一看。

示例是一个充值活动,充值返现或送积分,我先从简单开始,一步步的丰富它。

上面是一个最简单的规则,"充100返现5元" 与 "充50送10积分" 这两个规则在RulesEngine是*规则,就是它们都会被执行,如果 "充100" 那两个优惠会被叠加。如果不想被叠加,我们需要给它们创建一个父规则,如下图

你会看到"充值活动"的操作符是"或"(OR),同时它底下有"一个"的字样,它还有一个选项是"全部",这是"嵌套规则输出方式",它主要针对OR操作符,这是扩展出来的功能,在上面的介绍中我们知道RulesEngine默认会执行所有规则,同时输出值会存储在每个规则结果中,这样我们可以取一个也可以取全部,你可以把"嵌套规则输出方式"看作是取输出值的标识,需要注意的是,AND操作符是没有这个选项的,因为只要一个子规则失败,父级规则就是失败的,所以也不会执行OnSuccess动作了。如上面的示例,取全部就是叠加。如下图

但这里有一个注意事项,前面提到的NestedRuleExecutionMode设置,如果设置为Performance,则上面的"全部"选项则不起作用,它只会执行一个,所以如果想更灵活的使用RulesEngine,建议使用默认设置,除非确认没有上面示例中的叠加场景。

下面我们再给这个规则加个日期限制,我们可以直接修改"充值活动"为"活动日期为10.1到10.7"

现在面临一个问题,我们是否可以为"活动日期为10.1到10.7",直接设定一个表达式呢?根据我们之前对RulesEngine的了解,它仅执行树状结构中的叶节点表达式。这意味着,对于"活动日期为10.1到10.7"这一节点,其内部的表达式不会被执行,除非它是叶节点。然而,如果我们有一个具有多层次节点的复杂规则结构,那么为每个叶节点添加父级规则的条件将变得异常繁琐。这不仅增加了配置的复杂性,还可能导致维护上的困难。因此,我们需要寻找一种更为高效和简洁的方法来处理这种情况,来简化规则的设置过程。RulesEngine的默认执行方式我们改变不了,但我们可以在编译规则之前对规则进行一次预处理。下面是预处理代码

public static void PreProcess(this Rule rule, Rule parentRule = null)
{
    if (!string.IsNullOrWhiteSpace(parentRule?.Expression))
    {
        if (!string.IsNullOrWhiteSpace(rule.Expression))
        {
            rule.Expression = $"({parentRule.Expression}) && ({rule.Expression})";
        }
        else
        {
            rule.Expression = parentRule.Expression;
        }
    }

    if (rule.Rules != null)
    {
        foreach (var childRule in rule.Rules.ToList())
        {
            PreProcess(childRule, rule);
        }
    }
}

通过上面的扩展方法,我们可以将父级的表达式与其合并,这样叶节点就可以拥有其父级表达式了。

那如果我们再给"充50送10积分"添加一个时间限制,如"活动日期为10.5到10.7",就非常简单了,添加"活动日期为10.5到10.7"节点并为其设置表达式就可以了,如下图

我们又有新的需求了,如果老客户在充值100元后,他会得到5积分,如下图

大家想想上面的规则可以吗?RulesEngine总是执行叶节点,这个一定要谨记。如果新客户充100元,"老客户送5积分"不会被执行,那"充100返5元"也不会被执行,最终是选择下面的节点。

这里我们有两个处理方案

1、在不改变"充100返5元"节点的情况下,直接在其下面创建一个子规则,子规则的表达式直接返回true,这样"老客户送5积分"返回false,也不影响"充100返5元"的执行,如下图

2、我们可以再优化一下,将"返现5元"放到子规则中,需要注意,当前操作符为"或",同时"嵌套规则输出方式"为"全部",如下图

关于规则创建的基本概念,我们的讨论就先进行到这里。请记住,无论规则逻辑多么复杂,它们都可以通过这些基本元素逐步组合起来。通过巧妙地拼接简单的规则节点,我们可以创造出功能强大、逻辑清晰的规则逻辑。

接下来,让我们探讨一下输出。在前述示例中,涉及到了两种输出类型:"现金"和"积分",我们可以在Workflow节点下配置相应的输出类型,配置完后,我们可以在输出表达式动作(OutputExpressionAction)中选择输出类型。如下图

输出表达式动作中的表达式,是 DynamicLinq的表达式语法 https://dynamic-linq.net/expression-language ,下面我们基于该表达式创建一个新的规则需求,如上面的示例"充100返5元",我们把它改为每充100返5元,也就是充值200直接返10元。如下图

通过上面的表达式就可以实现"每充100返5元"

当我们设置完输出后,我们如何在执行完规则后,获取到输出值呢,下面是结合输出类型获取输出值的代码,它会返回一个字典,Key是输出类型,Value是输出值列表(每一个成功的规则结果值),后续大家可以根据自己的业务逻辑组织这一些值,上述示例,我们是对"现金"返回最大值,对"积分"是求和。

public static Dictionary<string, List<object>> GetOutputResults(this RuleResultTree resultTree)
{
    var outputResults = new Dictionary<string, List<object>>();

    if (resultTree.IsSuccess)
    {
        if (resultTree.ActionResult?.Output != null)
        {
            var context = resultTree.Rule.Actions.OnSuccess.Context;
            var outputType = context.GetValueOrDefault("type", "default") as string;
            if (!outputResults.ContainsKey(outputType))
            {
                outputResults[outputType] = [];
            }
            outputResults[outputType].Add(resultTree.ActionResult.Output);
        }
    }

    if (resultTree.ChildResults != null)
    {
        var outputMode = resultTree.Rule.Properties?.GetValueOrDefault("nestedRuleOutputMode") as string;
        foreach (var childResult in resultTree.ChildResults)
        {
            var childOutputResults = GetOutputResults(childResult);

            foreach (var childOutputResult in childOutputResults)
            {
                if (!outputResults.ContainsKey(childOutputResult.Key))
                {
                    outputResults[childOutputResult.Key] = [];
                }
                outputResults[childOutputResult.Key].AddRange(childOutputResult.Value);
            }

            if (childOutputResults.Any() && outputMode == "one")
            {
                break;
            }
        }
    }

    return outputResults;
}

下面是对输出值的处理

var outputResults = ruleResults.First().GetOutputResults();

Console.Write("共返");

if (outputResults.TryGetValue("现金", out List<object> moneyList))
{
    var money = moneyList.Select(m => double.Parse(m.ToString())).Max();
    Console.Write($"  {money}元现金");
}

if (outputResults.TryGetValue("积分", out List<object> scoreList))
{
    var score = scoreList.Select(m => double.Parse(m.ToString())).Sum();
    Console.Write($"  {score}积分");
}

写在最后

RulesEngine是一款轻量的规则引擎类库,它不仅提供了一套核心的基础功能,而且其设计具有卓越的扩展性。这使得开发者得以在此基础上构建更为强大和定制化的功能,满足各种复杂的业务逻辑需求。然而,手动编辑RulesEngine的规则文件无疑是一项耗时且繁琐的任务。正是为了减轻这一工作负担,开发规则编辑器的想法应运而生。编辑器的引入旨在简化规则的创建和管理过程,使得规则的维护变得更加高效和直观,从而将开发者从重复且繁杂的手工编辑工作中解放出来。

https://www.cnblogs.com/haoxj/p/18073710