65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (18投票s)

2010年9月12日

CPOL

8分钟阅读

viewsIcon

79890

downloadIcon

1300

自定义规则引擎方法,可以评估业务规则的复杂组合。提供了一个遵循 MVVM 模式的 WPF 示例应用程序。

目录

引言

业务规则是任何大型企业应用程序的一部分。在我之前的一个项目中,需要评估大量业务规则以执行某些特定的业务操作。所以我开发了一个易于使用、易于配置和可扩展的规则引擎。这种规则引擎方法的优点是:

  • 一旦基本表和类准备就绪,添加新规则几乎不需要开发工作。大多数情况下,添加新规则只需要在相应表中插入一些条目。
  • 由于引入/更改规则涉及填充表,因此可以在不进行大量开发工作的情况下添加新规则或修改现有规则;业务分析师可以填充表以符合业务需求。这可以独立于发布。
  • 这种方法可以扩展以处理最复杂的业务规则组合。

必备组件

此规则引擎方法通过一个简单的 WPF 应用程序来解释,该应用程序展示了规则的不同可能组合。示例应用程序 (CustomRulesMVVM) 使用 VS2010 和 Entity Framework 4 开发。请从 Microsoft 下载 VS2010 Express 版和 SQL Server 2005/2008 Express。

下面,我将向不熟悉的人展示从 Visual Studio 连接到 SQL Server 的步骤。在 Visual Studio 中,点击“工具”中的“连接到数据库”。将显示以下窗口。在屏幕中指定数据源和数据库文件名。

AddConnection.jpg

现在,点击“测试连接”,如果您的机器中安装了 SQL Express,您应该会收到以下消息。

ConnSuccess.jpg

打开数据库资源管理器,右键单击“表”文件夹,然后单击“添加查询”。将打开一个查询窗格。将“表脚本”(来自 DB Scripts 文件夹)粘贴到查询窗口中并执行。这将创建并填充示例应用程序所需的表。单击“存储过程”文件夹,然后单击“添加新存储过程”。将“存储过程”(在 DB Scripts 文件夹中)中给出的存储过程粘贴到示例应用程序中。选择查询,右键单击并运行选择。存储过程现在将执行。成功执行所有脚本后,数据库资源管理器应包含以下表和过程:

DBExplorer.jpg

数据库设计

下图显示了不同表之间的关系

如实体图所示,Rule 表中的每一行对应一个业务规则。表中给出示例如下。

规则表中的数据

RuleData.jpg

源表中的数据

SourceData.jpg

这里,RuleID 是表的主键。ValueRHS(右侧值)是我们进行比较的值。Operator 是我们进行的比较类型。Source 来自 Source 表,它表示用于检索等式左侧 (LHS) 值的对象源。CodeLHS 用于从 Source 评估值。RuleDescription 给出规则的描述。

例如:对于 RuleID=1,我们正在检查国家是否为美国。因此,ValueRHS 是“USA”,Operator 是“=”。Source 标识哪个对象具有国家值(这里,1 对应于 Source 表中给出的国家),CountryName 是 Country 对象中包含用户在应用程序中输入的国家名称的属性。

RuleGroup 包含两个或更多规则的组合

RuleGroup 表中的数据

RuleGroupData.jpg

这里,您可以看到 RuleGroupID = 2 是两个规则(Rule 表中的 RuleID 2 和 3)的组合。它正在检查城市是否为“NY”并且温度 < 20。Rule 和 RuleGroup 之间的关系在 RuleGroupRelations 表中给出。这是 Rule 和 RuleGroup 的连接表。

RuleGroupRelations 表中的数据

RuleGroupRelationsData.jpg

RuleSeqNum 给出了单个规则连接形成 RuleGroup 的顺序。

类似地,RuleGroupGrouping 包含两个或更多 RuleGroups 的组合。

RuleGroupGrouping 表中的数据

RuleGroupGroupingData.jpg

这里,RuleGroupGroupingID = 3 是两个 RuleGroups 的组合。它检查(城市是洛杉矶 AND 温度 > 30)或(州是加利福尼亚 AND 温度 < 15)。RuleGroup 和 RuleGroupGrouping 表之间的关系在 RuleGroupGroupingRelations 中给出。

RuleGroupGroupingRelations 表中的数据

RuleGroupGroupingRelationsData.jpg

这里,RuleGroup 3 和 4 使用 OR 运算符连接以形成 RuleGroupGroupingID = 3,而 RuleGroup 3 是 RuleID 4 和 5 的组合,RuleGroup 4 是 RuleID 6 和 7 的组合。

对于给定的 RuleGroupGroupingID,可以通过连接这些表找到相应的规则。

在示例应用程序中,我们有三个选项卡用于从用户获取数据以评估规则。在第一个选项卡中,用户可以输入一个国家名称并检查它是否为美国。如果是美国,结果文本将显示在屏幕上,如下所示:

CountryCheck.jpg

从我们的表设计中,我们知道 RuleGroupGroupingID = 1 将检查输入的 CountryName 是否为 USA。因此,我们还有一个表 CountryDetails,它将 RuleGroupGroupingID 与 screenID 和要显示的结果文本连接起来。

CountryDetails 表中的数据

CountryDetailsData.jpg

现在所有的表都已准备好并填充。我们有一个存储过程,它将为给定的 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,则结果文本“嗨……城市是洛杉矶而且很热 或者 州是加利福尼亚而且很凉爽”将显示在屏幕上,如下所示:

CityStateSuccesslatest.jpg

CityStateSuccessNew.jpg

如果输入的值错误,则不会显示任何结果文本。这里,温度 = 22,但我们的规则是:州 = CA 且温度 < 20。所以它没有显示任何结果。

CityStateFail.jpg

C# 代码设计

类似于数据库设计,在我们的 C# 代码中,我们有 CustomRuleCustomRulesCustomRuleGroups,用于在 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…      
    }
}

CustomRulesCustomRulesGroup 除了 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 类具有 RuleGroupJoinOperatorRuleGroupSeqNum,因为它们是 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 类中,我们有用于填充 CustomRuleCustomRulesCustomRulesGroups 的方法。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 的评估是通过调用 CustomRulesEval() 方法完成的,而 CustomRulesEval() 方法又会调用 CustomRuleEval() 方法。

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

CustomRulesEval()

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 方法执行 ValueLHSValueRHS 的实际比较。此方法将根据传递的运算符代码比较任意两种类型的值。

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 中添加存储过程:

AddNew.jpg

ChooseModel.jpg

ChooseData.jpg

点击“下一步”,会弹出一个窗口询问是否需要将数据库文件添加到项目中。如果您希望解决方案中包含 .mdf 文件,请选择“是”。

localdata.jpg

此窗口允许您选择数据库对象

ChooseObjects.jpg

将显示带有模型资源管理器的以下窗口:

打开模型浏览器并选择函数导入

ModelExplorer.jpg

右键单击“函数导入”,然后单击“添加函数导入”。输入一个函数导入名称并选择存储过程名称。在存储过程列信息部分中,单击“获取列信息”。这将在窗口中填充存储过程的返回类型。现在,单击“创建新的复杂类型”。这将创建一个新的复杂返回类型 SelectRules_Result

AddFunctionImport.jpg

现在,可以按如下方式调用 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 条件连接。

CityTemp.jpg

第三个选项卡包含 RuleGroups,其中两个 RuleGroups 通过 OR 条件连接。

使用 MVVM 模式,为每个选项卡项创建单独的 ViewModel 类。View 是一个单独的 XAML 文件 MainWindow.xaml。应用程序的结构如下所示。模型具有用于保存来自每个屏幕的数据的对象,以及用于保存自定义规则的类。

SolnExplorer.jpg

每个选项卡项中的控件都绑定到相应 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 表,此规则引擎方法可用于不同的业务场景。其余表将保持不变。同样,对于任何业务场景,填充和评估规则将是相同的。因此,此方法可用于评估任意数量的业务规则组合。快乐编码!

© . All rights reserved.