自定义规则引擎 (WPF - MVVM)






4.98/5 (18投票s)
自定义规则引擎方法,可以评估业务规则的复杂组合。提供了一个遵循 MVVM 模式的 WPF 示例应用程序。
目录
引言
业务规则是任何大型企业应用程序的一部分。在我之前的一个项目中,需要评估大量业务规则以执行某些特定的业务操作。所以我开发了一个易于使用、易于配置和可扩展的规则引擎。这种规则引擎方法的优点是:
- 一旦基本表和类准备就绪,添加新规则几乎不需要开发工作。大多数情况下,添加新规则只需要在相应表中插入一些条目。
- 由于引入/更改规则涉及填充表,因此可以在不进行大量开发工作的情况下添加新规则或修改现有规则;业务分析师可以填充表以符合业务需求。这可以独立于发布。
- 这种方法可以扩展以处理最复杂的业务规则组合。
必备组件
此规则引擎方法通过一个简单的 WPF 应用程序来解释,该应用程序展示了规则的不同可能组合。示例应用程序 (CustomRulesMVVM) 使用 VS2010 和 Entity Framework 4 开发。请从 Microsoft 下载 VS2010 Express 版和 SQL Server 2005/2008 Express。
下面,我将向不熟悉的人展示从 Visual Studio 连接到 SQL Server 的步骤。在 Visual Studio 中,点击“工具”中的“连接到数据库”。将显示以下窗口。在屏幕中指定数据源和数据库文件名。
现在,点击“测试连接”,如果您的机器中安装了 SQL Express,您应该会收到以下消息。
打开数据库资源管理器,右键单击“表”文件夹,然后单击“添加查询”。将打开一个查询窗格。将“表脚本”(来自 DB Scripts 文件夹)粘贴到查询窗口中并执行。这将创建并填充示例应用程序所需的表。单击“存储过程”文件夹,然后单击“添加新存储过程”。将“存储过程”(在 DB Scripts 文件夹中)中给出的存储过程粘贴到示例应用程序中。选择查询,右键单击并运行选择。存储过程现在将执行。成功执行所有脚本后,数据库资源管理器应包含以下表和过程:
数据库设计
下图显示了不同表之间的关系
如实体图所示,Rule 表中的每一行对应一个业务规则。表中给出示例如下。
这里,RuleID 是表的主键。ValueRHS(右侧值)是我们进行比较的值。Operator 是我们进行的比较类型。Source 来自 Source 表,它表示用于检索等式左侧 (LHS) 值的对象源。CodeLHS 用于从 Source 评估值。RuleDescription 给出规则的描述。
例如:对于 RuleID=1,我们正在检查国家是否为美国。因此,ValueRHS 是“USA”,Operator 是“=”。Source 标识哪个对象具有国家值(这里,1 对应于 Source 表中给出的国家),CountryName 是 Country 对象中包含用户在应用程序中输入的国家名称的属性。
RuleGroup 包含两个或更多规则的组合
这里,您可以看到 RuleGroupID = 2 是两个规则(Rule 表中的 RuleID 2 和 3)的组合。它正在检查城市是否为“NY”并且温度 < 20。Rule 和 RuleGroup 之间的关系在 RuleGroupRelations 表中给出。这是 Rule 和 RuleGroup 的连接表。
RuleSeqNum 给出了单个规则连接形成 RuleGroup 的顺序。
类似地,RuleGroupGrouping 包含两个或更多 RuleGroups 的组合。
这里,RuleGroupGroupingID = 3 是两个 RuleGroups 的组合。它检查(城市是洛杉矶 AND 温度 > 30)或(州是加利福尼亚 AND 温度 < 15)。RuleGroup 和 RuleGroupGrouping 表之间的关系在 RuleGroupGroupingRelations 中给出。
这里,RuleGroup 3 和 4 使用 OR 运算符连接以形成 RuleGroupGroupingID = 3,而 RuleGroup 3 是 RuleID 4 和 5 的组合,RuleGroup 4 是 RuleID 6 和 7 的组合。
对于给定的 RuleGroupGroupingID,可以通过连接这些表找到相应的规则。
在示例应用程序中,我们有三个选项卡用于从用户获取数据以评估规则。在第一个选项卡中,用户可以输入一个国家名称并检查它是否为美国。如果是美国,结果文本将显示在屏幕上,如下所示:
从我们的表设计中,我们知道 RuleGroupGroupingID = 1 将检查输入的 CountryName 是否为 USA。因此,我们还有一个表 CountryDetails,它将 RuleGroupGroupingID 与 screenID 和要显示的结果文本连接起来。
现在所有的表都已准备好并填充。我们有一个存储过程,它将为给定的 screenID 选择一组规则,如下所示:
CREATE PROCEDURE dbo.SelectRules
@screenID INT
AS
SELECT CD.ScreenID, CD.ResultText, R.CodeLHS, R.Operator,
R.ValueRHS, R.Source, RGR.RuleGroupID,
RGR.RuleJoinOperator, RGR.RuleSeqNum,
RGGR.RuleGroupJoinOperator, RGGR.RuleGroupSeqNum,
R.RuleDescription FROM CountryDetails AS CD INNER JOIN
RulesGroupGroupingRelations AS RGGR ON
CD.RuleGroupGroupingID = RGGR.RuleGroupGroupingID
INNER JOIN RulesGroupRelations AS RGR ON
RGGR.RuleGroupID = RGR.RuleGroupID INNER JOIN
Rules AS R ON R.RuleID = RGR.RuleID WHERE CD.ScreenID = @screenID
ORDER BY RGGR.RuleGroupGroupingID, RGGR.RuleGroupSeqNum,
RGR.RuleGroupID, RGR.RuleSeqNum
对于 @ScreenID = 3,存储过程的结果集如下所示:
从这个结果我们可以看到,检查城市 = 洛杉矶和温度 > 30 的前两条规则(RuleGroupID = 3)使用 AND 运算符连接,检查州 = 加利福尼亚和温度 < 15 的后两条规则(RuleGroupID = 4)使用 AND 运算符连接。这两个 RuleGroups 使用 OR 运算符连接。RuleSeqNum 和 RuleGroupSeqNum 给出了规则和 RuleGroup 的连接顺序。Source = 2 表示城市名称和温度的值将在 City 对象中检查,Source = 3 表示州名称和温度的值将在 State 对象中检查。如果此规则组合评估为 True,则结果文本“嗨……城市是洛杉矶而且很热 或者 州是加利福尼亚而且很凉爽”将显示在屏幕上,如下所示:
如果输入的值错误,则不会显示任何结果文本。这里,温度 = 22,但我们的规则是:州 = CA 且温度 < 20。所以它没有显示任何结果。
C# 代码设计
类似于数据库设计,在我们的 C# 代码中,我们有 CustomRule
、CustomRules
和 CustomRuleGroups
,用于在 Rule、RuleGroup 和 RuleGroupGrouping 表中进行映射条目。所有这些类都实现了 IRule
接口。
interface IRule
{
bool IsSelected { get; set; }
string SelectedItem { get; set; }
bool Eval(Dictionary<string, > collection);
}
IsSelected
将包含规则评估的结果。SelectedItem
是规则评估为 true
时的结果。Eval()
评估 Rule/Rules 或 RuleGroups。
CustomRule
类如下所示。
class CustomRule:IRule
{
#region Members
private const string COUNTRY = "COUNTRY";
private const string CITY = "CITY";
private const string STATE = "STATE";
Country country;
City city;
State state;
#endregion
#region Properties
public bool IsSelected { get; set; }
public string SelectedItem { get; set; }
public string CodeLHS { get; set; }
public string Operator { get; set; }
public string ValueRHS { get; set; }
public int Source { get; set; }
public string RuleJoinOperator { get; set; }
public int? RuleSeqNum { get; set; }
public string RuleDescription { get; set; }
#endregion
#region Public methods
/// This function evluates each custom rule by calling method in RuleHelper
public bool Eval(Dictionary<string, > collection)
{
//Implementation…
}
}
CustomRules
和 CustomRulesGroup
除了 IRule
外,还实现了 CollectionBase
。
class CustomRules : CollectionBase, IRule
{
#region Properties
public bool IsSelected { get; set; }
public string SelectedItem { get; set; }
public string RuleGroupJoinOperator { get; set; }
public int? RuleGroupSeqNum { get; set; }
#endregion
//Other members……
#region CollectionBase methods
public void Add(CustomRule item)
{
this.List.Add(item);
}
public void Remove(CustomRule item)
{
this.List.Remove(item);
}
public CustomRule Item(int index)
{
return this.List[index] as CustomRule;
}
#endregion
}
CustomRules
类具有 RuleGroupJoinOperator
和 RuleGroupSeqNum
,因为它们是 RuleGroup 的属性。
class CustomRulesGroups:CollectionBase,IRule
{
#region Properties
public bool IsSelected { get; set; }
public string SelectedItem { get; set; }
#endregion
//Other members……
#region CollectionBase methods
public void Add(CustomRules item)
{
this.List.Add(item);
}
public void Remove(CustomRules item)
{
this.List.Remove(item);
}
public CustomRules Item(int index)
{
return this.List[index] as CustomRules;
}
#endregion
}
在 Converter
类中,我们有用于填充 CustomRule
、CustomRules
和 CustomRulesGroups
的方法。BuildCustomRule
扩展方法如下所示:
public static CustomRule BuildCustomRule(this SelectRules_Result entity)
{
CustomRule custRule = new CustomRule();
if (null != entity)
{
custRule.SelectedItem = entity.ResultText;
custRule.CodeLHS = entity.CodeLHS;
custRule.Operator = entity.Operator;
custRule.ValueRHS = entity.ValueRHS;
custRule.Source = entity.Source;
custRule.RuleJoinOperator = entity.RuleJoinOperator;
custRule.RuleSeqNum = entity.RuleSeqNum;
custRule.RuleDescription = entity.RuleDescription;
}
return custRule;
}
通过调用每个 customrule.BuildCustomRule()
方法来填充 CustomRules
。
public static CustomRules BuildCustomRules(IList<selectrules_result /> entities)
{
CustomRules custRules = new CustomRules();
if (null != entities)
{
foreach (SelectRules_Result item in entities)
{
custRules.Add(item.BuildCustomRule());
custRules.SelectedItem = item.ResultText;
custRules.RuleGroupJoinOperator = item.RuleGroupJoinOperator;
custRules.RuleGroupSeqNum = item.RuleGroupSeqNum;
}
}
return custRules;
}
类似地,通过调用每个组的 BuildCustomRules()
方法来填充 CustomRulesGroup
。
public static IRule BuildCustomRulesGroups(IList<selectrules_result > entities,
string resultText)
{
CustomRulesGroups customRulesGroups = new CustomRulesGroups();
if (null != entities)
{
//Get distinct group ids from collection
var list = (from r in entities
select r.RuleGroupID).Distinct();
foreach (int groupID in list.ToList ())
{
var listRules = from rule in entities
where rule.RuleGroupID == groupID
select rule;
customRulesGroups.Add (Converter.BuildCustomRules(listRules.ToList()));
customRulesGroups.SelectedItem = resultText;
}
}
return customRulesGroups;
}
CustomRulesGroup
的评估是通过调用 CustomRules
的 Eval()
方法完成的,而 CustomRules
的 Eval()
方法又会调用 CustomRule
的 Eval()
方法。
public bool Eval(Dictionary<string,> collection)
{
return EvaluateCustomRulesGroups(collection);
}
/// Evaluate two or more rules groups
private bool EvaluateCustomRulesGroups(Dictionary<string, > collection)
{
//In collection of RuleGroups evaluate between first
//two rules groups by calling Eval method of each rule group.
if (null != this.Item(0).RuleGroupJoinOperator)
{
switch (this.Item(0).RuleGroupJoinOperator)
{
case AND:
if (this.Item(0).Eval(collection) && this.Item(1).Eval(collection))
this.IsSelected = true;
break;
case OR:
if (this.Item(0).Eval(collection) || this.Item(1).Eval(collection))
this.IsSelected = true;
break;
default:
this.IsSelected = false;
break;
}
//Evaluate till last rule group if more than two rules groups present
if (this.Count > 2)
{
for (int i = 1; i < this.Count - 2; i++)
//Loop till count-2 as last rule will not have RuleJoinOperator
{
switch (this.Item(i).RuleGroupJoinOperator)
{
case AND:
if (this.IsSelected && this.Item(i).Eval(collection))
this.IsSelected = true;
else
this.IsSelected = false;
break;
case OR:
if (this.IsSelected || this.Item(i).Eval(collection))
this.IsSelected = true;
else
this.IsSelected = false;
break;
default:
this.IsSelected = false;
break;
}
}
}
}
else
{
this.IsSelected = this.Item(0).Eval(collection);
}
return this.IsSelected;
}
CustomRules
的 Eval()
是
public bool Eval(Dictionary<string, > collection)
{
return EvaluateCustomRules(collection);
}
#endregion
#region Private Methods
/// Evaluate Two or More rules joined by AND/OR operator
private bool EvaluateCustomRules(Dictionary<string, > collection)
{
//In Rule collection evaluate between first
//two rules by calling Eval method of each rule.
if (null != this.Item(0).RuleJoinOperator)
{
switch (this.Item(0).RuleJoinOperator)
{
case AND:
if (this.Item(0).Eval(collection) && this.Item(1).Eval(collection))
this.IsSelected = true;
break;
case OR:
if (this.Item(0).Eval(collection) || this.Item(1).Eval(collection))
this.IsSelected = true;
break;
default:
this.IsSelected = false;
break;
}
//Evaluate if more than two rules present till last rule
if (this.Count > 2)
{
for (int i = 1; i < this.Count - 2; i++)
//Loop till count-2 as last rule will not have RuleJoinOperator
{
switch (this.Item(i).RuleJoinOperator)
{
case AND:
if (this.IsSelected && this.Item(i).Eval(collection))
this.IsSelected = true;
else
this.IsSelected = false;
break;
case OR:
if (this.IsSelected || this.Item(i).Eval(collection))
this.IsSelected = true;
else
this.IsSelected = false;
break;
default:
this.IsSelected = false;
break;
}
}
}
}
else
{
this.IsSelected = this.Item(0).Eval(collection);
}
return this.IsSelected;
}
每个规则的评估发生在 Eval()
方法中
public bool Eval(Dictionary<string, > collection)
{
if (collection.ContainsKey(COUNTRY))
country = (Country)collection[COUNTRY];
if (collection.ContainsKey(CITY))
city = (City)collection[CITY];
if (collection.ContainsKey(STATE))
state = (State)collection[STATE];
switch (this.Source)
{
case 1 :
this.IsSelected = RuleHelper.EvaluatePropertyValue(this,country);
break;
case 2:
this.IsSelected = RuleHelper.EvaluatePropertyValue(this, city);
break;
case 3:
this.IsSelected = RuleHelper.EvaluatePropertyValue(this, state);
break;
default:
this.IsSelected = false;
break;
}
return this.IsSelected;
}
RuleHelper
类中的 EvalutePropertyValue()
如下所示:
public static bool EvaluatePropertyValue(CustomRule customRule,object objSource)
{
object valueLHS = GetValueFromObject(objSource, customRule.CodeLHS);
return ComapareValues(Convert.ToString(valueLHS),
customRule.ValueRHS, customRule.Operator);
}
private static object GetValueFromObject(object objSource, object propertyName)
{
if (null != objSource)
{
PropertyInfo[] properties = objSource.GetType().GetProperties();
foreach (PropertyInfo info in properties)
{
if (info.Name.ToUpper() == propertyName.ToString().ToUpper())
{
return info.GetValue(objSource, null);
}
}
}
return null;
}
RuleHelper
中的 CompareValues
方法执行 ValueLHS
和 ValueRHS
的实际比较。此方法将根据传递的运算符代码比较任意两种类型的值。
private static bool ComapareValues(string valueLHS,string valueRHS,
string operatorCode)
{
bool isBool, isNumeric, isDateTime;
bool boolValue1, boolValue2 = false;
double numericValue1, numericValue2 = 0.0;
DateTime dateValue1, dateValue2 = DateTime.Today;
try
{
//Check if incoming values are boolean/Numeric/Date else it is string
isBool = Boolean.TryParse(valueLHS, out boolValue1) &&
Boolean.TryParse(valueRHS, out boolValue2);
isNumeric = Double.TryParse(valueLHS, out numericValue1) &&
Double.TryParse(valueRHS, out numericValue2);
isDateTime = DateTime.TryParse(valueLHS, out dateValue1) &&
DateTime.TryParse(valueRHS, out dateValue2);
//Do comparisons based on value types
if (operatorCode == EQUAL &&
(!isBool && !isNumeric && !isDateTime))
//Comparing equal condition of two string values
return valueLHS.Equals(valueRHS,
StringComparison.InvariantCultureIgnoreCase);
else if (operatorCode == EQUAL && isNumeric)
//Comparing if two numbers are equal
return numericValue1 == numericValue2;
else if (operatorCode == GREATER_THAN && isNumeric)
//Checking if Number 1> Number 2
return numericValue1 > numericValue2;
else if (operatorCode == LESSER_THAN && isNumeric)
//Checking if Number 1 < Number 2
return numericValue1 < numericValue2;
else if (operatorCode == EQUAL && isBool)
//Checkingif two boolean values are equal
return boolValue1 == boolValue2;
else if (operatorCode == EQUAL && isDateTime)
//Checking if two datetime values are equal
return dateValue1.Equals(dateValue2);
else
return false;
}
catch (Exception)
{
return false;
}
}
如果需要其他评估条件(例如数字/日期时间的 >=、<= 等),则需要在该方法中添加相应的条件。
在 Entity Framework 中添加存储过程
以下步骤描述了如何在 Entity Framework 中添加存储过程:
点击“下一步”,会弹出一个窗口询问是否需要将数据库文件添加到项目中。如果您希望解决方案中包含 .mdf 文件,请选择“是”。
此窗口允许您选择数据库对象
将显示带有模型资源管理器的以下窗口:
打开模型浏览器并选择函数导入
右键单击“函数导入”,然后单击“添加函数导入”。输入一个函数导入名称并选择存储过程名称。在存储过程列信息部分中,单击“获取列信息”。这将在窗口中填充存储过程的返回类型。现在,单击“创建新的复杂类型”。这将创建一个新的复杂返回类型 SelectRules_Result。
现在,可以按如下方式调用 SelectRules
:
IList<selectrules_result /> rulesCollection = new List<selectrules_result />();
//Populate custom rules by calling stored procedure for a screenID
using (CustomRuleEntities context = new CustomRuleEntities())
{
var rules = context.SelectRules(screenID);
foreach (var item in rules)
{
rulesCollection.Add(item);
}
}
使应用程序遵循 MVVM 模式
示例应用程序有一个包含三个选项卡项的 Tab 控件。如前所示,第一个选项卡项检查输入的 CountryName 是否为 USA。这是一个单一规则评估。第二个选项卡项检查输入的 CityName 是否为 NY AND Temperature 是否 < 20,如下所示。这是一个 RuleGroup 评估,其中两个规则通过 AND 条件连接。
第三个选项卡包含 RuleGroups,其中两个 RuleGroups 通过 OR 条件连接。
使用 MVVM 模式,为每个选项卡项创建单独的 ViewModel 类。View 是一个单独的 XAML 文件 MainWindow.xaml。应用程序的结构如下所示。模型具有用于保存来自每个屏幕的数据的对象,以及用于保存自定义规则的类。
每个选项卡项中的控件都绑定到相应 ViewModel 中的属性。因此,MainWindow.xaml.cs 仅包含构造函数:
using System.Collections.Generic;
using System.Windows;
namespace CustomRulesMVVM
{
/// <summary >
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
此处使用的是 Command 而不是按钮的 Click 事件。CountryViewModel
中的 Command 如下所示。对于 Command,使用了此处解释的 RelayCommand。
/// Command for button to search country
public ICommand SearchCountry
{
get
{
if (null == this._searchCountry)
{
this._searchCountry =
new RelayCommand(param => this.SearchCountryDetails());
}
return this._searchCountry;
}
}
#endregion
#region Private Methods
/// Searching country by first populating the custom rules
/// and evaluating rules by passing the Country object
private void SearchCountryDetails()
{
//Clear the result
this.SearchCountryResult = string.Empty;
//Populate custom rules applicable for this
//screen by passing the ScreenID (here ScreenID =1)
this.PopulateCustomRules(1);
collection[COUNTRY] = _country;
//Evaluate the rules for Country screen
this.SearchCountryResult = this.EvaluateCustomRules(collection);
}
#endregion
结论
通过用根据业务需求选择的适当表替换 CountryDetails 表,此规则引擎方法可用于不同的业务场景。其余表将保持不变。同样,对于任何业务场景,填充和评估规则将是相同的。因此,此方法可用于评估任意数量的业务规则组合。快乐编码!