C# RulesEngine 規則引擎:從入門到看懵

說明

RulesEngine 是 C# 寫的一個規則引擎類庫,讀者可以從這些地方了解它:

倉庫地址:

//github.com/microsoft/RulesEngine

使用方法:

//microsoft.github.io/RulesEngine

文檔地址:

//github.com/microsoft/RulesEngine/wiki

什麼是規則引擎?

照搬 //github.com/microsoft/RulesEngine/wiki/Introduction#what-is-the-rules-engine

在企業項目中,關鍵或核心部分總是業務邏輯或業務規則,也就是 CRUD,這些系統都有一個共同的特徵是,某個模塊中的一些或許多規則或策略總會發生變化,例如購物網站的顧客折扣、物流企業的運價計算等。隨着這些變化而來的是大量的重複工作,如果系統沒有足夠的抽象,那麼每當增加一種規則時,開發者需要在規則、回歸測試、性能測試等方面的變化中編寫代碼。

在 RulesEngine 中,微軟對規則進行了抽象,這樣核心邏輯總是得到穩定的、易於維護的,而規則的更改可以以一種簡單的方式生成,而不需要更改代碼庫。此外,系統的輸入本質上是動態的,因此不需要在系統中定義模型,而是可以作為擴展對象或任何其他類型的對象作為輸入,系統經過預定義的規則處理後,輸出結果。

它有以下特性:

  • Json based rules definition (基於 Json 的規則定義)
  • Multiple input support (多輸入支持)
  • Dynamic object input support (動態對象輸入支持)
  • C# Expression support (C # 表達式支持)
  • Extending expression via custom class/type injection (通過自定義類/類型注入擴展表達式)
  • Scoped parameters (範圍參數)
  • Post rule execution actions (發佈規則執行操作)

說人話就是,業務邏輯的輸出結果受到多個因子影響,但是這些影響有一定規律的,那麼適合將這些部分抽象出來,接着使用規則引擎處理,例如購物的各種優惠卷疊加之後的最終折扣價、跨區運輸的不同類型的包裹運價計算等。

筆者認為這個規則引擎主要由兩部分構成:

  • 規則驗證系統,例如根據規則驗證字段、執行函數驗證當前流程、輸出執行結果;
  • 動態代碼引擎,能夠將字符串轉換為動態代碼,利用表達式樹這些完成;

當然,這樣說起來其實很抽象的,還得多擼代碼,才能明白這個 RulesEngine 到底是幹嘛的。

安裝

新建項目後,nuget 直接搜索 RulesEngine 即可安裝,在 nuget 介紹中可以看到 RulesEngine 的依賴:

FluentValidation 是一個用於構建強類型驗證規則的 .NET 庫,在 ASP.NET Core 項目中,我們會經常使用模型驗證,例如必填字段使用 [Required]、字符串長度使用 [MaxLength] 等;但是因為是特性註解,也就是難以做到很多需要經過動態檢查的驗證方式,使用 FluentValidation 可以為模型類構建更加豐富的驗證規則。

而 FluentValidation 用在 RulesEngine 上,也是相同的用途,RulesEngine 最常常用做規則驗證,檢查模型類或業務邏輯的驗證結果,利用 FluentValidation 中豐富的驗證規則,可以製作各種方便的表達式樹,構建動態代碼。

怎麼使用

我們通過 RulesEngine 檢查模型類的字段是否符合規則,來了解 RulesEngine 的使用方法。

創建一個這樣的模型類:

public class Buyer
{
    public int Id { get; set; }
    public int Age { get; set; }
    // 是否為已認證用戶
    public bool Authenticated { get; set; }
}

場景是這樣的,用戶下單購買商品,後台需要判斷此用戶是否已經成年是否通過了認證

正常來看代碼應該這樣寫:

if(Authenticated == true && Age > 18)

但是如果年齡調為 16 歲呢?如果最近公司搞活動,不需要上傳身份證就能購買商品呢?

當然定義變量存儲到數據庫也行,但是如果後面又新增了幾個條件,那麼我們就需要修改代碼了,大佬說,這樣不好,我們要 RulesEngine 。

好的,那我們來研究一下這個東西。

前面提到的 if(Authenticated == true && Age > 18),這麼一個完整的驗證過程,在 RulesEngine 稱為 Workflow,每個 Workflow 下有多個 Rule。

if(Authenticated == true && Age > 18) => Workflow
	  Authenticated == true			  => Rule
	  Age > 18						  => Rule

在 RulesEngine 中,有兩種方法定義這些 Workflow 和 Rule,一種是使用代碼,一種是 JSON,官方是推薦使用 JSON 的,因為 JSON 可以動態生成,可以實現真正的動態。

下面我們來看看如何使用 JSON 和代碼,分別定義 if(Authenticated == true && Age > 18) 這個驗證過程。

JSON 定義:

[
  {
    "WorkflowName": "Test",
    "Rules": [
      {
        "RuleName": "CheckAuthenticated",
        "Expression": "Authenticated == true"
      },
      {
        "RuleName": "CheckAge",
        "Expression": "Age >= 18"
      }
    ]
  }
] 
        var rulesStr = "[{... ...}]" // JSON
        var workflows = JsonConvert.DeserializeObject<List<Workflow>>(rulesStr);

C# 代碼:

        var workflows = new List<Workflow>();
        List<Rule> rules = new List<Rule>();

        Workflow exampleWorkflow = new Workflow();
        exampleWorkflow.WorkflowName = "Test";
        exampleWorkflow.Rules = rules;
        workflows.Add(exampleWorkflow);

        Rule authRule = new Rule();
        authRule.RuleName = "CheckAuthenticated";
        authRule.Expression = "Authenticated == true";
        rules.Add(authRule);

        Rule ageRule = new Rule();
        ageRule.RuleName = "CheckAuthenticated";
        ageRule.Expression = "Authenticated == true";
        rules.Add(ageRule);

兩種方式都是一樣的,每個 Workflow 下有多個 Rule,可以定義多個 Workflow。

當前我們有兩個地方要了解:

        "RuleName": "CheckAuthenticated",
        "Expression": "Authenticated == true"

RuleName:規則名稱;

Expression: 真實的代碼,必須是符合 C# 語法的代碼;

定義好 Workflow 和 Rule 後,我們需要生成規則引擎,直接 new RulesEngine.RulesEngine() 即可:

        var bre = new RulesEngine.RulesEngine(workflows.ToArray());

生成引擎是需要一些時間的。

生成引擎後,我們通過名稱指定調用一個 Workflow,並獲取每個 Rule 的驗證結果:

        List<RuleResultTree> resultList = await bre.ExecuteAllRulesAsync("Test", new Buyer
        {
            Id = 666,
            Age = 17,
            Authenticated = false
        });

完整代碼示例如下:

    static async Task Main()
    {
        // 定義
        var rulesStr = ... ...// JSON
        // 生成 Workflow[ Rule[] ]
        var workflows = JsonConvert.DeserializeObject<List<Workflow>>(rulesStr)!;
        var bre = new RulesEngine.RulesEngine(workflows.ToArray());

        // 調用指定的 Workflow,並傳遞參數,獲取每個 Rule 的處理結果
        List<RuleResultTree> resultList = await bre.ExecuteAllRulesAsync("Test", new Buyer
        {
            Id = 666,
            Age = 17,
            Authenticated = false
        });

        // 打印輸出
        foreach (var item in resultList)
        {
            Console.WriteLine("規則名稱:{0},    驗證結果:{1}", item.Rule.RuleName, item.IsSuccess);
        }
    }

多參數

如果商品需要 VIP 才能購買呢?

這裡我們再定義一個模型類,表示一個用戶是否為 VIP。

public class VIP
{
    public int Id { get; set; }
    public bool IsVIP { get; set; }
}

那麼這個時候就需要處理兩個模型類了,為了能夠在 Rule 中使用所有的模型類,我們需要為每個模型類定義 RuleParameter

        var rp1 = new RuleParameter("buyer", new Buyer
        {
            Id = 666,
            Age = 20,
            Authenticated = true
        });

        var rp2 = new RuleParameter("vip", new VIP
        {
            Id = 666,
            IsVIP = false
        });

相當於表達式樹:

            ParameterExpression rp1 = Expression.Parameter(typeof(Buyer), "buyer");
            ParameterExpression rp2 = Expression.Parameter(typeof(VIP), "vip");

可以參考筆者的表達式樹系列文章://ex.whuanle.cn/

然後重新設計 JSON,增加一個 Rule:

[{
	"WorkflowName": "Test",
	"Rules": [{
			"RuleName": "CheckAuthenticated",
			"Expression": "buyer.Authenticated == true"
		},
		{
			"RuleName": "CheckAge",
			"Expression": "buyer.Age >= 18"
		},
		{
			"RuleName": "CheckVIP",
			"Expression": "vip.IsVIP == true"
		}
	]
}]

然後執行此 Workflow:

List<RuleResultTree> resultList = await bre.ExecuteAllRulesAsync("Test", rp1, rp2);

完整代碼:

    static async Task Main()
    {
        // 定義
        var rulesStr = ... ... // JSON
        var workflows = JsonConvert.DeserializeObject<List<Workflow>>(rulesStr)!;
        var bre = new RulesEngine.RulesEngine(workflows.ToArray());

        var rp1 = new RuleParameter("buyer", new Buyer
        {
            Id = 666,
            Age = 20,
            Authenticated = true
        });

        var rp2 = new RuleParameter("vip", new VIP
        {
            Id = 666,
            IsVIP = false
        });

        List<RuleResultTree> resultList = await bre.ExecuteAllRulesAsync("Test", rp1, rp2);

        foreach (var item in resultList)
        {
            Console.WriteLine("規則名稱:{0},    驗證結果:{1}", item.Rule.RuleName, item.IsSuccess);
        }
    }

全局參數、本地參數

全局參數

在 Workflow 中可以定義全局參數,參數對 Workflow 內的所有 Rule 起效,所有 Rule 都可以使用它。

定義示例:

	"WorkflowName": "Test",
	"GlobalParams": [{
		"Name": "age",
		"Expression": "buyer.Age"
	}],

參數的值,可以定義為常量,也可以來源於傳入的參數。

修改上一個小節的示例,在 Rule CheckAge 中,使用這個全局參數。

[{
	"WorkflowName": "Test",
	"GlobalParams": [{
		"Name": "age",
		"Expression": "buyer.Age"
	}],
	"Rules": [{
			"RuleName": "CheckAuthenticated",
			"Expression": "buyer.Authenticated == true"
		},
		{
			"RuleName": "CheckAge",
			"Expression": "age >= 18"
		},
		{
			"RuleName": "CheckVIP",
			"Expression": "vip.IsVIP == true"
		}
	]
}]

本地參數

本地參數在 Rule 內定義,只對當前 Rule 起效。

[{
	"WorkflowName": "Test",
	"Rules": [{
			"RuleName": "CheckAuthenticated",
			"LocalParams": [{
				"Name": "age",
				"Expression": "buyer.Age"
			}],
			"Expression": "buyer.Authenticated == true"
		},
		{
			"RuleName": "CheckAge",
			"Expression": "age >= 18"
		},
		{
			"RuleName": "CheckVIP",
			"Expression": "vip.IsVIP == true"
		}
	]
}]

在定義參數時,參數的值可以通過執行函數來獲取:

      "LocalParams":[
        {
          "Name":"mylocal1",
          "Expression":"myInput.hello.ToLower()"
        }
      ],

LocalParams 可以使用 GlobalParams 的參數再次生成新的變量。

  "GlobalParams":[
    {
      "Name":"myglobal1"
      "Expression":"myInput.hello"
    }
  ],
  "Rules":[
    {
      "RuleName": "checkGlobalAndLocalEqualsHello",
      "LocalParams":[
        {
          "Name": "mylocal1",
          "Expression": "myglobal1.ToLower()"
        }
      ]
    },

定義驗證成功、失敗行為

可以為每個 Rule 定義驗證成功和失敗後執行一些代碼。

格式示例:

        "Actions": {
           "OnSuccess": {
              "Name": "OutputExpression",
              "Context": {
                 "Expression": "input1.TotalBilled * 0.8"
              }
           },
           "OnFailure": {
               "Name": "EvaluateRule",
               "Context": {
                   "WorkflowName": "inputWorkflow",
                   "ruleName": "GiveDiscount10Percent"
               }
           }
        }

OutputExpression 裏面定義了執行代碼:

              "Name": "OutputExpression",
              "Context": {
                 "Expression": "input1.TotalBilled * 0.8"
              }

EvaluateRule 定義了執行另一個 Workflow 的 Rule,

               "Name": "EvaluateRule",
               "Context": {
                   "WorkflowName": "inputWorkflow",
                   "ruleName": "GiveDiscount10Percent"
               }

OnSuccessOnFailure 裏面,內部結構如下所示:

              "Name": "OutputExpression",  //Name of action you want to call
              "Context": {  //This is passed to the action as action context
                 "Expression": "input1.TotalBilled * 0.8"
              }

              "Name": "EvaluateRule",
               "Context": {
                   "WorkflowName": "inputWorkflow",
                   "ruleName": "GiveDiscount10Percent"
               }
              

Name:{xxx} 中的 {xxx} 是一個具體的執行器名稱,不是隨便定義的,OutputExpressionEvaluateRule 都是自帶的執行器,所謂的執行器就是一個 Func<ActionBase>,在後面的 自定義執行器 中,可以了解更多。

Context 裏面的內容,是一個字典,這些 Key/Value 會被當做參數傳遞給執行器,每個執行器要求設置的 Context 是不一樣的。

另外每個 Rule 都可以定義以下三個字段:

      "SuccessEvent": "10",
      "ErrorMessage": "One or more adjust rules failed.",
      "ErrorType": "Error",

ErrorType 有兩個選項,WarnError,如果這個 Rule 的表達式錯誤,那麼是否彈出異常。如果設置為 Warn, Rule 有問題,驗證結果則會是 false,而不會報異常;如果是 Error,那麼這個 Rule 會中止 Workflow 的執行,程序會報錯。

SuccessEventErrorMessage 對應,只是成功、失敗的提示消息。

計算折扣

前面提到的都是驗證規則,接下來我們將會使用 RulesEngine 實現規則計算。

這裡規定,基礎折扣為 1.0,如果用戶小於 18 歲,打 9 折,如果用戶是 VIP,打 9 折,兩個規則獨立。

如果是小於 18歲,則 1.0 * 0.9
如果是 VIP,    則 1.0 * 0.9 

定義一個模型類,用於傳遞折扣基值。

// 折扣
public class Discount
{
    public double Value
    {
        get; set;
    }
}

定義三個參數:

        var rp1 = new RuleParameter("buyer", new Buyer
        {
            Id = 666,
            Age = 16,
        });

        var rp2 = new RuleParameter("vip", new VIP
        {
            Id = 666,
            IsVIP = true
        });

        var rp3 = new RuleParameter("discount", new Discount
        {
            Value = 1.0
        });

定義規則計算,每個規則計算的是自己的折扣:

[{
	"WorkflowName": "Test",
	"GlobalParams": [{
		"Name": "value",
		"Expression": "discount.Value"
	}],
	"Rules": [{
			"RuleName": "CheckAge",
			"Expression": "buyer.age < 18",
			"Actions": {
				"OnSuccess": {
					"Name": "OutputExpression",
					"Context": {
						"Expression": "value * 0.9"
					}
				}
			}
		},
		{
			"RuleName": "CheckVIP",
			"Expression": "vip.IsVIP == true",
			"Actions": {
				"OnSuccess": {
					"Name": "OutputExpression",
					"Context": {
						"Expression": "value * 0.9"
					}
				}
			}
		}
	]
}]

完整代碼:

    static async Task Main()
    {
        // 定義
        var rulesStr =  ... ... // JSON
        var workflows = JsonConvert.DeserializeObject<List<Workflow>>(rulesStr)!;
        var bre = new RulesEngine.RulesEngine(workflows.ToArray());

        var rp1 = new RuleParameter("buyer", new Buyer
        {
            Id = 666,
            Age = 16,
        });

        var rp2 = new RuleParameter("vip", new VIP
        {
            Id = 666,
            IsVIP = true
        });

        var rp3 = new RuleParameter("discount", new Discount
        {
            Value = 1.0
        });

        List<RuleResultTree> resultList = await bre.ExecuteAllRulesAsync("Test", rp1, rp2, rp3);
        var discount = 1.0;
        foreach (var item in resultList)
        {
            if (item.ActionResult != null && item.ActionResult.Output != null)
            {
                Console.WriteLine($"{item.Rule.RuleName} 折扣優惠:{item.ActionResult.Output}");
                discount = discount * (double)item.ActionResult.Output;
            }
        }
        Console.WriteLine($"最終折扣:{discount}");
    }

筆者這裡的示例是,每個規則只計算自己的折扣,也就是每個 Rule 都是獨立的,下一個 Rule 不會在上一個 Rule 結果上計算。

< 18 : 0.9
VIP  : 0.9

如果是折扣可以疊加,那麼就是 0.9*0.9 ,最終可以拿到 0.81 的折扣。

如果折扣不能疊加,只能選擇最佳的優惠,那麼就是 0.9

使用自定義函數

自定義函數有兩種靜態函數和實例函數兩種,我們可以在 Expression 中調用預先寫好的函數。

下面講解如何在 Rule 中調用自定義的函數。

靜態函數

自定義靜態函數:

    public static bool CheckAge(int age)
    {
        return age >= 18;
    }

註冊類型:

        ReSettings reSettings = new ReSettings
        {
            CustomTypes = new[] { typeof(Program) }
        };

        var bre = new RulesEngine.RulesEngine(Workflows: workflows.ToArray(), reSettings: reSettings);

使用靜態函數:

[{
	"WorkflowName": "Test",
	"Rules": [{
		"RuleName": "CheckAge",
		"Expression": "Program.CheckAge(buyer.Age) == true"
	}]
}]

完整代碼:

    static async Task Main()
    {
        // 定義
        var rulesStr = "[{\"WorkflowName\":\"Test\",\"Rules\":[{\"RuleName\":\"CheckAge\",\"Expression\":\"Program.CheckAge(buyer.Age) == true\"}]}]";
        var workflows = JsonConvert.DeserializeObject<List<Workflow>>(rulesStr)!;

        ReSettings reSettings = new ReSettings
        {
            CustomTypes = new[] { typeof(Program) }
        };

        var bre = new RulesEngine.RulesEngine(Workflows: workflows.ToArray(), reSettings: reSettings);
        List<RuleResultTree> resultList = await bre.ExecuteAllRulesAsync("Test", new Buyer
        {
            Age = 16
        });

        foreach (var item in resultList)
        {
            Console.WriteLine("規則名稱:{0},    驗證結果:{1}", item.Rule.RuleName, item.IsSuccess);
        }
    }

    public static bool CheckAge(int age)
    {
        return age >= 18;
    }

實例函數

定義實例函數:

    public bool CheckAge(int age)
    {
        return age >= 18;
    }

通過 RuleParameter 參數的方式,傳遞實例:

        var rp1 = new RuleParameter("p", new Program());

通過參數的名稱調用函數:

[{
	"WorkflowName": "Test",
	"Rules": [{
		"RuleName": "CheckAge",
		"Expression": "p.CheckAge(buyer.Age) == true"
	}]
}]

完整代碼:

    static async Task Main()
    {
        // 定義
        var rulesStr = "[{\"WorkflowName\":\"Test\",\"Rules\":[{\"RuleName\":\"CheckAge\",\"Expression\":\"p.CheckAge(buyer.Age) == true\"}]}]";
        var workflows = JsonConvert.DeserializeObject<List<Workflow>>(rulesStr)!;

        var rp1 = new RuleParameter("p", new Program());

        var bre = new RulesEngine.RulesEngine(Workflows: workflows.ToArray());
        List<RuleResultTree> resultList = await bre.ExecuteAllRulesAsync("Test", new Buyer
        {
            Age = 16
        }, rp1);

        foreach (var item in resultList)
        {
            Console.WriteLine("規則名稱:{0},    驗證結果:{1}", item.Rule.RuleName, item.IsSuccess);
        }
    }

    public bool CheckAge(int age)
    {
        return age >= 18;
    }

自定義執行器

自定義執行器就是 OnSuccessOnFailure 這部分的自定義執行代碼,相比靜態函數、實例函數,使用自定義執行器,可以獲取 Rule 的一些數據。

		"Actions": {
			"OnSuccess": {
				"Name": "MyCustomAction",
				"Context": {
					"customContextInput": "0.9"
				}
			}
		}

自定義一個執行器,執行器需要繼承 ActionBase

public class MyCustomAction : ActionBase
{
    public override async ValueTask<object> Run(ActionContext context, RuleParameter[] ruleParameters)
    {
        var customInput = context.GetContext<string>("customContextInput");
        return await ValueTask.FromResult(new object());
    }
}

定義 ReSettings,並在構建規則引擎時,傳遞進去:

        var b = new Buyer
        {
            Age = 16
        };
        var reSettings = new ReSettings
        {
            CustomActions = new Dictionary<string, Func<ActionBase>>
            {
                {"MyCustomAction", () => new MyCustomAction() }
            }
        };

        var bre = new RulesEngine.RulesEngine(workflows.ToArray(), reSettings);

        List<RuleResultTree> resultList = await bre.ExecuteAllRulesAsync("Test", b);

定義 JSON 規則:

[{
	"WorkflowName": "Test",
	"Rules": [{
		"RuleName": "CheckAge",
		"Expression": "Age <= 18 ",
		"Actions": {
			"OnSuccess": {
				"Name": "MyCustomAction",
				"Context": {
					"customContextInput": "0.9"
				}
			}
		}
	}]
}]