在开发软件项目时,我们经常希望项目在不同条件下表现出不同的行为。例如,在处理取款操作时,如果用户请求的金额超过了他们可以取款的金额,我们返回一个错误消息;如果金额较小,则执行相应的操作。同样,根据用户提供的数据,我们可能希望表现出基于条件的行为,例如在某些情况下生成“文档A”,而在其他情况则生成“文档B”。另一个常见的例子是活动规则中经常需要条件语句。事实上,某些活动场景中条件语句会被用到极致。
为了满足这些需求,我们经常使用条件语句。比如If-Else、Switch-Case或三元运算符这样的条件语句引导应用程序根据不同的条件采取不同的行为。
然而,有时候这些条件会变得非常复杂或者随时间改变。有时,它们的存在也可能随情况变化。例如,活动过程本质上是条件判断,可能简单也可能复杂。有时它们特定于某个时间段,而在另一些时候,它们的条件或结果可能会发生变化。
当我们想要调整我们开发的应用程序的行为时,我们并不总是希望在代码层面进行干预,因为即使是在代码层面的小改动也可能导致应用程序的新版本。一个新的版本反过来会带来新的部署流程,这可能导致应用程序暂时不可用。虽然我们可以通过CI/CD流程尽量减少这个问题,然而,随之而来的是另一个问题。在代码层面做出的更改可能会随着时间的推移导致代码退化。今天看似简单的修改,可能到了明天甚至几个小时后就变成了一个不受欢迎的改动。此外,如果参与开发过程的开发者离开了团队,对于接手的新开发者来说,情况会变得更加棘手。
由于以上种种原因,我们在开发流程中尽可能地创建动态方法。回到我们的主题,让我们设想一个复杂的活动策划。这个活动可能取决于诸如日期、购物车内商品的数量、购物车中的总金额,甚至包括购物车内类别A中的商品数量或该类别商品的总金额等条件。可以预见,在这种情况下,依赖于简单的if-else语句会使我们陷入困境。因此,我们需要一个活动管理引擎。
假设我们开发了一个成功的 campaign 发动机,其中包括了密集的条件判断和算法基础设施,经过了艰苦的努力。我们部署了系统,一切看起来都很正常。有一天,营销团队来找你,请求发起一个新的活动。经过检查,你意识到这个新活动无法通过你目前开发的系统来支持。
工作在活动引擎上的团队重组,准备开始一个新的开发流程。在最好的情况下,团队成员没有变化。在最坏的情况下,情况会变得稍微复杂一些,因为需要让新团队成员熟悉活动引擎使用的数据库表结构等关键领域。假设我们成功地克服了这些挑战,我们对活动引擎进行了一些代码层面的调整。或许我们使事情稍微复杂了一点,但现在我们可以生成所需的活动。然而,随着时间推移,这个过程会越来越痛苦。我们希望将我们的代码与活动引擎的变化隔离开。
我以前在一个项目中处理过类似的需求。当时我们采用的方法是:我们在SQL存储过程上构建了活动引擎。我们希望在代码层面执行的唯一任务就是调用用于查找相关活动的存储过程。由于我们在存储过程中处理了其余的操作,因此在需要更改时只需要修改存储过程,而不需要在应用程序代码中进行更改或经历重新编译或重新部署的流程。这种方法很好地满足了复杂活动和动态需求。然而,这并不适用于每个项目。在我们的项目中,我们选择了MSSQL作为数据库,并且我们确定这一选择在长期内不会改变,因此我们可以依赖存储过程并严格遵循它们。当然,这种方法不适合每个项目的原因之一是:如果选择了不支持此类功能(如存储过程)的数据库,会如何处理?在这种情况下,你需要开发一个替代方案。
最近我感觉到在一个其他项目中需要使用一个更通用的解决方法。我选择使用的方法在很多方面更易于处理。在本文的剩余部分,我们将一起讨论这种方法。在这个方法中,我们将定义我们的规则为JSON格式。我们的规则引擎将根据我们定义的规则做出各种决策。我们将把我们的JSON规则存储在文件存储中。事实上,我们甚至会在规则定义中指定所需的响应。这样,如果我们需要进行任何修改,只需修改JSON结构中的几个字段即可,而无需修改任何代码。当然,我们也可以选择不在文件存储中存储规则定义,而是采用其他存储方法。然而,在本文中,我将通过文件存储来举例说明。
虽然文章的开头部分因为需要定义问题和提供一些实例而显得有些冗长,我们可以动起手来开始写点代码了。
首先,我们需要设计一个规则引擎。在此,我们将使用一个软件包,它允许我们用JSON格式定义规则。我们使用的软件包是Microsoft开发的RulesEngine(规则引擎)。该软件包的描述如下:
一个使用JSON的规则引擎,支持多种动态表达。
RulesEngine 包程序可以通过 NuGet 包管理器 轻松集成到 .NET 项目 中。如果您想,可以通过 nuget install
命令将其添加到您的项目中。
dotnet 安装包 RulesEngine --version 5.0.3
您可以点击以下链接访问相关的NuGet页面:。
RulesEngine 5.0.3规则引擎是一个用于将业务逻辑/规则/政策从系统中提取出来的包。它以一种非常简单的方式运行……我将从创建一个ASP.NET Core API项目开始着手,该项目位于一个解决方案中。我们的基于规则的引擎将作为同一个解决方案中的一个类库来实现。然后,我们会把这个类库添加为API项目的一个依赖项。我们的初始结构将大致如下:
现在,我们需要一个场景。虽然我们会在后面的部分对这套规则进行一些调整,但我将先使用RulesEngine文档中提供的场景。假设我们有一个活动方案。根据不同的条件,支付金额将分别获得10%,20%,25%,30%或35%的折扣。我们假设定义了如下的JSON结构:
[
{
"WorkflowName": "折扣",
"Rules": [
{
"RuleName": "提供10%折扣",
"SuccessEvent": "成功事件",
"ErrorMessage": "一个或多个调整规则失败。",
"ErrorType": "错误类型",
"RuleExpressionType": "规则表达式类型",
"Expression": "input1.country == \"印度\" 且 input1.忠诚度系数 <= 2 且 input1.累计购买总额 >= 5000 且 input2.总订单数 > 2 且 input3.每月访问次数 > 2"
},
{
"RuleName": "提供20%折扣",
"SuccessEvent": "成功事件",
"ErrorMessage": "一个或多个调整规则失败。",
"ErrorType": "错误类型",
"RuleExpressionType": "规则表达式类型",
"Expression": "input1.country == \"印度\" 且 input1.忠诚度系数 == 3 且 input1.累计购买总额 >= 10000 且 input2.总订单数 > 2 且 input3.每月访问次数 > 2"
},
{
"RuleName": "提供25%折扣",
"SuccessEvent": "成功事件",
"ErrorMessage": "一个或多个调整规则失败。",
"ErrorType": "错误类型",
"RuleExpressionType": "规则表达式类型",
"Expression": "input1.country != \"印度\" 且 input1.忠诚度系数 >= 2 且 input1.累计购买总额 >= 10000 且 input2.总订单数 > 2 且 input3.每月访问次数 > 5"
},
{
"RuleName": "提供30%折扣",
"SuccessEvent": "成功事件",
"ErrorMessage": "一个或多个调整规则失败。",
"ErrorType": "错误类型",
"RuleExpressionType": "规则表达式类型",
"Expression": "input1.忠诚度系数 > 3 且 input1.累计购买总额 >= 50000 且 input1.累计购买总额 <= 100000 且 input2.总订单数 > 5 且 input3.每月访问次数 > 15"
},
{
"RuleName": "提供35%折扣",
"SuccessEvent": "成功事件",
"ErrorMessage": "一个或多个调整规则失败。",
"ErrorType": "错误类型",
"RuleExpressionType": "规则表达式类型",
"Expression": "input1.忠诚度系数 > 3 且 input1.累计购买总额 >= 100000 且 input2.总订单数 > 15 且 input3.每月访问次数 > 25"
}
]
}
]
我们怎么知道在什么情况下会得到什么结果呢?让我们以 GiveDiscount10 规则为例。如果 Expression 字段中指定的表达式符合,我们就返回在 SuccessEvent 里定义的那个值。换句话说:如果满足 input1.country == India AND input1.loyaltyFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2
,返回的结果就是 "10"。
我们的规则引擎会评估这里定义的所有规则,并根据哪个条件与给定的数据相匹配,返回相应的响应。如果有多个规则匹配,引擎将根据最先匹配的规则进行处理。当然,你可以选择设计一个级联活动结构,或者选择提供最高折扣的那个。或者想象你在文档生成系统中使用此引擎。你可能不想只取第一个匹配的规则,而是想检索所有匹配的规则并生成相应的文档。在这种情况下,你需要访问所有匹配的规则来获取相关信息。在本文后面,我还将介绍我们如何实现这一点。
首先,我们需定义由我们的规则引擎评估后将创建的对象。在我们的类库项目的Models文件夹下,我们创建了一个名为RuleCheckResponse的模型,如下图所示。如果规则检查成功执行,第一个匹配规则的SuccessEvent值将对我们可用。该模型采用工厂方法设计模式设计。对象的创建责任委托给子类,而基类则抽象了对象创建的过程。静态方法CheckedSuccessfully和CheckedOnFailure充当命名构造函数,使得创建RuleCheckResponse对象变得简单而易读。
public record RuleCheckResponse
{
/// <summary>成功事件</summary>
public string? 成功事件 { get; set; }
/// <summary>是否成功</summary>
public bool 是否成功 { get; set; }
/// <summary>检查通过</summary>
/// <param name="successEvent">成功事件</param>
public static RuleCheckResponse 检查通过(string? 成功事件) => new() { 是否成功 = true, 成功事件 = 成功事件 };
/// <summary>检查失败</summary>
public static RuleCheckResponse 检查失败() => new() { 是否成功 = false };
}
现在我们可以开始开发一个服务来利用我们的规则引擎功能。让我们从在项目类库的Services文件夹中创建一个名为RuleService的类开始。此服务将在应用程序中所有需要评估规则的部分使用。
该服务将通过依赖注入接收一个名为RuleEngine的实例。这个对象是包含我们创建的所有工作流的规则引擎。我们将从JSON模板中获取所有工作流,创建一个包含这些工作流的RuleEngine实例,并在将我们的服务添加到DI容器中时,传递这个RuleEngine实例。然而,这些步骤将在后续阶段中处理。
目前,我们暂时仅专注于此服务,并假设我们的规则引擎将通过依赖注入的方式提供给我们。
在这里,CheckRuleAsync 方法会评估指定的工作流中的所有规则,并使用提供的参数值返回一个类型为 RuleCheckResponse 的响应,这是我们之前创建的。
规则结果树列表很重要,因为它包含了我们正在检查的工作流下的所有规则及其执行结果。当下面的代码执行完毕后,所有的规则都将被评估,并且它们的结果将被添加到列表中。
List<RuleResultTree> resultList = await _rulesEngine.ExecuteAllRulesAsync(workflowName, inputs); // 执行所有规则并将结果存储在 resultList 中
在接下来的代码里,如果至少有一条规则被满足,就执行OnSuccess回调;如果没有任何规则被满足条件,就执行OnFail回调。如前所述,如果有多个规则被满足,将返回第一个匹配的规则的匹配结果。
我们的服务结构如下:
public class RuleService (IRulesEngine rulesEngine)
{
private readonly IRulesEngine _rulesEngine = rulesEngine;
public async Task<RuleCheckResponse> CheckRuleAsync(string workflowName, dynamic[] inputs)
{
List<RuleResultTree> resultList = await _rulesEngine.ExecuteAllRulesAsync(workflowName, inputs);
RuleCheckResponse? response = null;
resultList.OnSuccess((eventName) =>
{
response = RuleCheckResponse.CheckedSuccessfully(eventName);
});
resultList.OnFail(() =>
{
response = RuleCheckResponse.CheckedOnFailure();
});
return response ?? throw new ArgumentNullException("规则响应不能为 null");
}
}
如前所述,在某些情况下,我们可能想查看所有成功规则或与所有成功场景相关的成功事件。毕竟,我们的目标是应用能为客户带来最大好处的活动。
使用 RuleResultTree 列表,我们可以轻松提取这些信息,如下所示。下面是一个简单的示例。您可以根据需要修改该方法的其余部分以实现您的目标。我将采用 标准方法 ,该方法会返回 第一个匹配规则的结果。
// 所有成功匹配的规则
List<RuleResultTree> succeededResults = resultList.Where(r => r.IsSuccess).ToList();
// 所有未成功匹配的规则
List<RuleResultTree> failedResults = resultList.Where(r => !r.IsSuccess).ToList();
// 所有成功的事件
List<string> allSuccessEvents = resultList.Where(r => r.IsSuccess).Select(r => r.Rule.SuccessEvent).ToList();
现在我们的服务已经准备好,我们需要将它添加到DI容器中,并创建它所需的RuleEngine对象。为了避免使Program.cs文件变得复杂,我们将在类库中创建一个名为ServiceRegistration的类,并在其中添加一个扩展方法。当然,在这样做之前,我们不要忘记在我们的类库项目中添加以下几行代码,以便在项目中使用ASP.NET Core API。
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <!-- 引入Microsoft.AspNetCore.App框架 -->
</ItemGroup>
在扩展方法中,为了创建RuleEngine对象,我们需要从我们的JSON文件中提取工作流。我们更喜欢将工作流存储在不同文件中并进行分类,而不是将它们全部写在一个大的JSON文件里。因此,我们需要一些辅助函数。
首先,我在类库项目的Helpers文件夹内创建了一个名为RuleFileReader的类。代码如下所示:这些辅助方法可以简要说明如下:
GetRuleFiles:我们将会把所有规则文件存放在一个单独的文件夹里。该方法会扫描这个文件夹,并返回规则文件的位置列表或路径。
GetRuleFromFile : 此方法通过传入的文件路径参数读取文件,并将其转换为工作流规则列表,然后返回该列表。
public 类 RuleFileReader
{
public 静态方法加载工作流文件(字符串文件路径)string filePath)
{
var fileData = 文件.ReadAllText(文件路径);
return JsonConvert.DeserializeObject<List<Workflow>>(fileData);
}
public 静态方法获取规则文件(字符串文件夹路径)string folderPath)
{
return Directory.GetFiles(文件夹路径, "*.json");
}
}
现在一切准备就绪,我们可以开始创建扩展方法(extension method)了。这个方法将会接收包含规则文件的文件夹的路径,读取所有文件,将它们转换为工作流的列表,然后创建一个RuleEngine对象,然后将创建的对象通过依赖注入(dependency injection)传递给RuleService。最后一步是将RuleService注册为单例(Singleton)在DI容器(DI container)中。
public static class 服务注册类
{
public static void 添加规则引擎(this IServiceCollection services, string rulesFolder)
{
services.AddSingleton<规则服务>(sp =>
{
string[] ruleFiles = 规则文件读取器.GetRuleFiles(rulesFolder);
List<Workflow>? workflows = ruleFiles.SelectMany(filePath => 规则文件读取器.LoadWorkflowsFromFile(filePath) ?? []).ToList();
IRulesEngine ruleEngine = new RulesEngine.RulesEngine([.. workflows]);
return new 规则服务(ruleEngine);
});
}
}
现在,一切都准备得差不多了。让我们在我们的API项目中创建一个名为Rules的文件夹,并将规则文件以Campaign.json的名称保存为JSON格式。之后,我们可以在Program.cs中添加以下代码来使服务可用。请注意,例如,将Rules文件夹的位置作为参数传递。
builder.Services.AddRulesEngine(Path.Combine(Directory.GetCurrentDirectory(), "Rules"));
在最后阶段,你可以查看以下代码以获取示例用法。我们只是将输入添加到类型为 dynamic
的数组里。数组中第一个添加的输入在 JSON 文件中,input1 表示第一个输入,input2 表示第二个输入,等等。我们通过依赖注入来添加服务。在调用 CheckRuleAsync 方法时,我们传递要检查的工作流名称和动态数组。这就是全部过程。
public class OrdersController(RuleService ruleService) : ControllerBase
{
private readonly RuleService _ruleService = ruleService;
[HttpPost]
public async Task<IActionResult> PostAsync(OrderCreateRequestDto dto)
{
var userDetails = new
{
Country = "Turkiye",
忠诚度因素 = 2,
累计购买总额 = 20000
};
var visitDetails = new
{
NoOfVisitsPerMonth = 20
};
dynamic[] inputs = [userDetails, dto, visitDetails];
RuleCheckResponse response = await _ruleService.CheckRuleAsync("Discount", inputs);
if (response.Success && response.SuccessEvent != null)
{
return Ok($"您获得了 {response.SuccessEvent}% 折扣");
}
return Ok("您未能获得折扣");
}
}
我还想再提一点。假设你有一个在 JSON 文件中表达过于复杂的条件需要处理。你需要编写自己的自定义方法,希望规则引擎能够调用这个方法。这也是可能的。我们来看一个简单的例子。
我们将在类库中创建一个CustomMethods文件夹。该文件夹将存放我们为特定目的编写的自定义方法。接下来,我们创建一个名为CampaignMethods的静态类,并在其中编写一个简单的方法。下面的代码结构就是例子。
public static class 活动方法
{
private static Dictionary<string, int> 国家和总订单数 = new()
{
{ "Turkiye", 3 },
{ "USA", 2 },
{ "UK", 1 }
};
public static bool 自定义折扣方法(string country, int totalOrders)
{
if(国家和总订单数.TryGetValue(country, out int value))
{
return totalOrders >= value;
};
return false;
}
}
这并不是一个非常有意义的方法,但对我们来说重要的是它可以使用JSON定义调用。在生成RuleEngine对象时,我们需要做一些特别的调整。例如,它需要辨识这个类。每次创建新类时,都需要重新引入它。然而,我们可以通过使用反射来避免这种麻烦。我们将编写一个额外的方法来,使用反射在CustomMethods文件夹下找到的所有静态类并自动注册。
让我们去更新一下我们的扩展方法,如下所示。GetCustomTypes 方法会执行上述操作,并将找到的类作为数组返回。现在,我们只需要向 RuleEngine 提供一个ReSettings 对象,将我们的自定义方法提供给 RuleEngine。
public static class 服务注册类
{
public static void AddRulesEngine(this IServiceCollection services, string rulesFolder)
{
services.AddSingleton<规则引擎服务>(sp =>
{
string[] 规则文件 = 规则文件读取器.GetRuleFiles(rulesFolder);
List<工作流>? 工作流集合 = 规则文件.SelectMany(filePath => 规则文件读取器.LoadWorkflowsFromFile(filePath) ?? []).ToList();
ReSettings reSettingsWith自定义类型 = new()
{
自定义类型 = 获取自定义类型()
};
IRulesEngine 规则引擎 = new RulesEngine.RulesEngine([.. 工作流集合], reSettingsWith自定义类型);
return new 规则引擎服务(规则引擎);
});
}
private static Type[] 获取自定义类型()
{
var 程序集 = Assembly.GetExecutingAssembly();
var 目标命名空间 = typeof(CampaignMethods).Namespace;
var 类型 = 程序集.GetTypes()
.Where(t =>
t.IsClass &&
t.IsAbstract &&
t.IsSealed &&
t.Namespace == 目标命名空间
);
return [.. 类型];
}
}
我在下面的JSON文件中添加了一个示例规则。通过查看它,你可以很容易地看到我们是如何使用自定义方法的。
{
"RuleName": "GiveDiscount40",
"SuccessEvent": "40",
"ErrorMessage": "一个或多个规则的调整失败了。",
"ErrorType": "Error",
"RuleExpressionType": "LambdaExpression",
"Expression": "input1.loyalityFactor > 4 AND input1.totalPurchasesToDate >= 200000 AND CampaignMethods.CustomDiscountMethod(input1.country, input2.totalOrders) == true"
}
RulesEngine包提供了许多自定义功能选项。您可以通过此链接查阅相关文档。
基于 JSON 的规则引擎,支持丰富的动态表达式 microsoft.github.io 总结RulesEngine 在许多需要基于规则的处理或工作流创建的场景中非常有用。如前所述,其定义可以存储在文件系统中,也可以通过其他方式存储。除了可以使用 自定义方法 进行扩展外,它自身还提供了许多功能,如微软文档中所述的那样。但在使用过程中,一个重要的考虑因素是调试功能有限。由于表达式及其参数是动态传递的,因此在定义和使用时必须格外小心,以确保类型安全的正确性。
你可以通过以下链接访问包含文章中提到的代码的Git仓库(仓库指代存储代码的版本控制系统):
这是一个 GitHub 仓库链接,提供了规则引擎的相关内容:https://github.com/basturkerhan/rulesengine-medium
共同学习,写下你的评论
评论加载中...
作者其他优质文章