只要十步,你就可以應用表示式樹來優化動態呼叫
表示式樹是 .net 中一系列非常好用的型別。在一些場景中使用表示式樹可以獲得更好的效能和更佳的擴充套件性。本篇我們將通過構建一個 “模型驗證器” 來理解和應用表示式樹在構建動態呼叫方面的優勢。
Newbe.Claptrap 是一個用於輕鬆應對併發問題的分散式開發框架。如果您是首次閱讀本系列文章。建議可以先從本文末尾的入門文章開始瞭解。
開篇摘要
前不久,我們釋出了《如何使用 dotTrace 來診斷 netcore 應用的效能問題》,經過網友投票之後,網友們表示對其中表達式樹的內容很感興趣,因此本篇我們將展開講講。
動態呼叫是在 .net 開發是時常遇到的一種需求,即在只知道方法名或者屬性名等情況下動態的呼叫方法或者屬性。最廣為人知的一種實現方式就是使用 “反射” 來實現這樣的需求。當然也有一些高效能場景會使用 Emit 來完成這個需求。
本文將介紹 “使用表示式樹” 來實現這種場景,因為這個方法相較於 “反射” 將擁有更好的效能和擴充套件性,相較於 Emit 又更容易掌握。
我們將使用一個具體的場景來逐步使用表示式來實現動態呼叫。
在該場景中,我們將構建一個模型驗證器,這非常類似於 aspnet mvc 中 ModelState 的需求場景。
這不是一篇簡單的入門文章,初次涉足該內容的讀者,建議在空閒時,在手邊有 IDE 可以順便操作時邊看邊做。同時,也不必在意樣例中出現的細節方法,只需要瞭解其中的大意,能夠依樣畫瓢即可,掌握大意之後再深入瞭解也不遲。
為了縮短篇幅,文章中的樣例程式碼會將沒有修改的部分隱去,想要獲取完整的測試程式碼,請開啟文章末尾的程式碼倉庫進行拉取。
為什麼要用表示式樹,為什麼可以用表示式樹?
首先需要確認的事情有兩個:
- 使用表示式樹取代反射是否有更好的效能?
- 使用表示式樹進行動態呼叫是否有很大的效能損失?
有問題,做實驗。我們採用兩個單元測試來驗證以上兩個問題。
呼叫一個物件的方法:
using System; using System.Diagnostics; using System.Linq.Expressions; using System.Reflection; using FluentAssertions; using NUnit.Framework; namespace Newbe.ExpressionsTests { public class X01CallMethodTest { private const int Count = 1_000_000; private const int Diff = 100; [SetUp] public void Init() { _methodInfo = typeof(Claptrap).GetMethod(nameof(Claptrap.LevelUp)); Debug.Assert(_methodInfo != null, nameof(_methodInfo) + " != null"); var instance = Expression.Parameter(typeof(Claptrap), "c"); var levelP = Expression.Parameter(typeof(int), "l"); var callExpression = Expression.Call(instance, _methodInfo, levelP); var lambdaExpression = Expression.Lambda<Action<Claptrap, int>>(callExpression, instance, levelP); // lambdaExpression should be as (Claptrap c,int l) => { c.LevelUp(l); } _func = lambdaExpression.Compile(); } [Test] public void RunReflection() { var claptrap = new Claptrap(); for (int i = 0; i < Count; i++) { _methodInfo.Invoke(claptrap, new[] {(object) Diff}); } claptrap.Level.Should().Be(Count * Diff); } [Test] public void RunExpression() { var claptrap = new Claptrap(); for (int i = 0; i < Count; i++) { _func.Invoke(claptrap, Diff); } claptrap.Level.Should().Be(Count * Diff); } [Test] public void Directly() { var claptrap = new Claptrap(); for (int i = 0; i < Count; i++) { claptrap.LevelUp(Diff); } claptrap.Level.Should().Be(Count * Diff); } private MethodInfo _methodInfo; private Action<Claptrap, int> _func; public class Claptrap { public int Level { get; set; } public void LevelUp(int diff) { Level += diff; } } } }
以上測試中,我們對第三種呼叫方式一百萬次呼叫,並記錄每個測試所花費的時間。可以得到類似以下的結果:
Method | Time |
---|---|
RunReflection | 217ms |
RunExpression | 20ms |
Directly | 19ms |
可以得出以下結論:
- 使用表示式樹建立委託進行動態呼叫可以得到和直接呼叫近乎相同的效能。
- 使用表示式樹建立委託進行動態呼叫所消耗的時間約為十分之一。
所以如果僅僅從效能上考慮,應該使用表示式樹,也可以是用表示式樹。
不過這是在一百萬呼叫下體現出現的時間,對於單次呼叫而言其實就是納秒級別的區別,其實無足輕重。
但其實表示式樹不僅僅在效能上相較於反射更優,其更強大的擴充套件性其實採用最為重要的特性。
此處還有一個對屬性進行操作的測試,此處將測試程式碼和結果羅列如下:
using System; using System.Diagnostics; using System.Linq.Expressions; using System.Reflection; using FluentAssertions; using NUnit.Framework; namespace Newbe.ExpressionsTests { public class X02PropertyTest { private const int Count = 1_000_000; private const int Diff = 100; [SetUp] public void Init() { _propertyInfo = typeof(Claptrap).GetProperty(nameof(Claptrap.Level)); Debug.Assert(_propertyInfo != null, nameof(_propertyInfo) + " != null"); var instance = Expression.Parameter(typeof(Claptrap), "c"); var levelProperty = Expression.Property(instance, _propertyInfo); var levelP = Expression.Parameter(typeof(int), "l"); var addAssignExpression = Expression.AddAssign(levelProperty, levelP); var lambdaExpression = Expression.Lambda<Action<Claptrap, int>>(addAssignExpression, instance, levelP); // lambdaExpression should be as (Claptrap c,int l) => { c.Level += l; } _func = lambdaExpression.Compile(); } [Test] public void RunReflection() { var claptrap = new Claptrap(); for (int i = 0; i < Count; i++) { var value = (int) _propertyInfo.GetValue(claptrap); _propertyInfo.SetValue(claptrap, value + Diff); } claptrap.Level.Should().Be(Count * Diff); } [Test] public void RunExpression() { var claptrap = new Claptrap(); for (int i = 0; i < Count; i++) { _func.Invoke(claptrap, Diff); } claptrap.Level.Should().Be(Count * Diff); } [Test] public void Directly() { var claptrap = new Claptrap(); for (int i = 0; i < Count; i++) { claptrap.Level += Diff; } claptrap.Level.Should().Be(Count * Diff); } private PropertyInfo _propertyInfo; private Action<Claptrap, int> _func; public class Claptrap { public int Level { get; set; } } } }
耗時情況:
Method | Time |
---|---|
RunReflection | 373ms |
RunExpression | 19ms |
Directly | 18ms |
由於反射多了一份裝拆箱的消耗,所以比起前一個測試樣例顯得更慢了,使用委託是沒有這種消耗的。
第〇步,需求演示
先通過一個測試來了解我們要建立的 “模型驗證器” 究竟是一個什麼樣的需求。
using System.ComponentModel.DataAnnotations; using FluentAssertions; using NUnit.Framework; namespace Newbe.ExpressionsTests { /// <summary> /// Validate data by static method /// </summary> public class X03PropertyValidationTest00 { private const int Count = 10_000; [Test] public void Run() { for (int i = 0; i < Count; i++) { // test 1 { var input = new CreateClaptrapInput(); var (isOk, errorMessage) = Validate(input); isOk.Should().BeFalse(); errorMessage.Should().Be("missing Name"); } // test 2 { var input = new CreateClaptrapInput { Name = "1" }; var (isOk, errorMessage) = Validate(input); isOk.Should().BeFalse(); errorMessage.Should().Be("Length of Name should be great than 3"); } // test 3 { var input = new CreateClaptrapInput { Name = "yueluo is the only one dalao" }; var (isOk, errorMessage) = Validate(input); isOk.Should().BeTrue(); errorMessage.Should().BeNullOrEmpty(); } } } public static ValidateResult Validate(CreateClaptrapInput input) { return ValidateCore(input, 3); } public static ValidateResult ValidateCore(CreateClaptrapInput input, int minLength) { if (string.IsNullOrEmpty(input.Name)) { return ValidateResult.Error("missing Name"); } if (input.Name.Length < minLength) { return ValidateResult.Error($"Length of Name should be great than {minLength}"); } return ValidateResult.Ok(); } public class CreateClaptrapInput { [Required] [MinLength(3)] public string Name { get; set; } } public struct ValidateResult { public bool IsOk { get; set; } public string ErrorMessage { get; set; } public void Deconstruct(out bool isOk, out string errorMessage) { isOk = IsOk; errorMessage = ErrorMessage; } public static ValidateResult Ok() { return new ValidateResult { IsOk = true }; } public static ValidateResult Error(string errorMessage) { return new ValidateResult { IsOk = false, ErrorMessage = errorMessage }; } } } }
從上而下,以上程式碼的要點:
- 主測試方法中,包含有三個基本的測試用例,並且每個都將執行一萬次。後續所有的步驟都將會使用這樣的測試用例。
- Validate 方法是被測試的包裝方法,後續將會呼叫該方法的實現以驗證效果。
- ValidateCore 是 “模型驗證器” 的一個演示實現。從程式碼中可以看出該方法對 CreateClaptrapInput 物件進行的驗證,並且得到驗證結果。但是該方法的缺點也非常明顯,這是一種典型的 “寫死”。後續我們將通過一系列改造。使得我們的 “模型驗證器” 更加的通用,並且,很重要的,保持和這個 “寫死” 的方法一樣的高效!
- ValidateResult 是驗證器輸出的結果。後續將不斷重複的用到該結果。
第一步,呼叫靜態方法
首先我們構建第一個表示式樹,該表示式樹將直接使用上一節中的靜態方法 ValidateCore。
using System; using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Linq.Expressions; using FluentAssertions; using NUnit.Framework; namespace Newbe.ExpressionsTests { /// <summary> /// Validate date by func created with Expression /// </summary> public class X03PropertyValidationTest01 { private const int Count = 10_000; private static Func<CreateClaptrapInput, int, ValidateResult> _func; [SetUp] public void Init() { try { var method = typeof(X03PropertyValidationTest01).GetMethod(nameof(ValidateCore)); Debug.Assert(method != null, nameof(method) + " != null"); var pExp = Expression.Parameter(typeof(CreateClaptrapInput)); var minLengthPExp = Expression.Parameter(typeof(int)); var body = Expression.Call(method, pExp, minLengthPExp); var expression = Expression.Lambda<Func<CreateClaptrapInput, int, ValidateResult>>(body, pExp, minLengthPExp); _func = expression.Compile(); } catch (Exception e) { Console.WriteLine(e); throw; } } [Test] public void Run() { // see code in demo repo } public static ValidateResult Validate(CreateClaptrapInput input) { return _func.Invoke(input, 3); } public static ValidateResult ValidateCore(CreateClaptrapInput input, int minLength) { if (string.IsNullOrEmpty(input.Name)) { return ValidateResult.Error("missing Name"); } if (input.Name.Length < minLength) { return ValidateResult.Error($"Length of Name should be great than {minLength}"); } return ValidateResult.Ok(); } } }
從上而下,以上程式碼的要點:
- 增加了一個單元測試的初始化方法,在單元測試啟動時建立的一個表示式樹將其編譯為委託儲存在靜態欄位 _func 中。
- 省略了主測試方法 Run 中的程式碼,以便讀者閱讀時減少篇幅。實際程式碼沒有變化,後續將不再重複說明。可以在程式碼演示倉庫中檢視。
- 修改了 Validate 方法的實現,不再直接呼叫 ValidateCore ,而呼叫 _func 來進行驗證。
- 執行該測試,開發者可以發現,其消耗的時間和上一步直接呼叫的耗時,幾乎一樣,沒有額外消耗。
- 這裡提供了一種最為簡單的使用表示式進行動態呼叫的思路,如果可以寫出一個靜態方法(例如:ValidateCore)來表示動態呼叫的過程。那麼我們只要使用類似於 Init 中的構建過程來構建表示式和委託即可。
- 開發者可以試著為 ValidateCore 增加第三個引數 name 以便拼接在錯誤資訊中,從而瞭解如果構建這種簡單的表示式。
第二步,組合表示式
雖然前一步,我們將直接呼叫轉變了動態呼叫,但由於 ValidateCore 還是寫死的,因此還需要進一步修改。
本步驟,我們將會把 ValidateCore 中寫死的三個 return 路徑拆分為不同的方法,然後再採用表示式拼接在一起。
如果我們實現了,那麼我們就有條件將更多的方法拼接在一起,實現一定程度的擴充套件。
注意:演示程式碼將瞬間邊長,不必感受太大壓力,可以輔助後面的程式碼要點說明進行檢視。
using System; using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Linq.Expressions; using FluentAssertions; using NUnit.Framework; // ReSharper disable InvalidXmlDocComment namespace Newbe.ExpressionsTests { /// <summary> /// Block Expression /// </summary> public class X03PropertyValidationTest02 { private const int Count = 10_000; private static Func<CreateClaptrapInput, int, ValidateResult> _func; [SetUp] public void Init() { try { var finalExpression = CreateCore(); _func = finalExpression.Compile(); Expression<Func<CreateClaptrapInput, int, ValidateResult>> CreateCore() { // exp for input var inputExp = Expression.Parameter(typeof(CreateClaptrapInput), "input"); var minLengthPExp = Expression.Parameter(typeof(int), "minLength"); // exp for output var resultExp = Expression.Variable(typeof(ValidateResult), "result"); // exp for return statement var returnLabel = Expression.Label(typeof(ValidateResult)); // build whole block var body = Expression.Block( new[] {resultExp}, CreateDefaultResult(), CreateValidateNameRequiredExpression(), CreateValidateNameMinLengthExpression(), Expression.Label(returnLabel, resultExp)); // build lambda from body var final = Expression.Lambda<Func<CreateClaptrapInput, int, ValidateResult>>( body, inputExp, minLengthPExp); return final; Expression CreateDefaultResult() { var okMethod = typeof(ValidateResult).GetMethod(nameof(ValidateResult.Ok)); Debug.Assert(okMethod != null, nameof(okMethod) + " != null"); var methodCallExpression = Expression.Call(okMethod); var re = Expression.Assign(resultExp, methodCallExpression); /** * final as: * result = ValidateResult.Ok() */ return re; } Expression CreateValidateNameRequiredExpression() { var requireMethod = typeof(X03PropertyValidationTest02).GetMethod(nameof(ValidateNameRequired)); var isOkProperty = typeof(ValidateResult).GetProperty(nameof(ValidateResult.IsOk)); Debug.Assert(requireMethod != null, nameof(requireMethod) + " != null"); Debug.Assert(isOkProperty != null, nameof(isOkProperty) + " != null"); var requiredMethodExp = Expression.Call(requireMethod, inputExp); var assignExp = Expression.Assign(resultExp, requiredMethodExp); var resultIsOkPropertyExp = Expression.Property(resultExp, isOkProperty); var conditionExp = Expression.IsFalse(resultIsOkPropertyExp); var ifThenExp = Expression.IfThen(conditionExp, Expression.Return(returnLabel, resultExp)); var re = Expression.Block( new[] {resultExp}, assignExp, ifThenExp); /** * final as: * result = ValidateNameRequired(input); * if (!result.IsOk) * { * return result; * } */ return re; } Expression CreateValidateNameMinLengthExpression() { var minLengthMethod = typeof(X03PropertyValidationTest02).GetMethod(nameof(ValidateNameMinLength)); var isOkProperty = typeof(ValidateResult).GetProperty(nameof(ValidateResult.IsOk)); Debug.Assert(minLengthMethod != null, nameof(minLengthMethod) + " != null"); Debug.Assert(isOkProperty != null, nameof(isOkProperty) + " != null"); var requiredMethodExp = Expression.Call(minLengthMethod, inputExp, minLengthPExp); var assignExp = Expression.Assign(resultExp, requiredMethodExp); var resultIsOkPropertyExp = Expression.Property(resultExp, isOkProperty); var conditionExp = Expression.IsFalse(resultIsOkPropertyExp); var ifThenExp = Expression.IfThen(conditionExp, Expression.Return(returnLabel, resultExp)); var re = Expression.Block( new[] {resultExp}, assignExp, ifThenExp); /** * final as: * result = ValidateNameMinLength(input, minLength); * if (!result.IsOk) * { * return result; * } */ return re; } } } catch (Exception e) { Console.WriteLine(e); throw; } } [Test] public void Run() { // see code in demo repo } public static ValidateResult Validate(CreateClaptrapInput input) { return _func.Invoke(input, 3); } public static ValidateResult ValidateNameRequired(CreateClaptrapInput input) { return string.IsNullOrEmpty(input.Name) ? ValidateResult.Error("missing Name") : ValidateResult.Ok(); } public static ValidateResult ValidateNameMinLength(CreateClaptrapInput input, int minLength) { return input.Name.Length < minLength ? ValidateResult.Error($"Length of Name should be great than {minLength}") : ValidateResult.Ok(); } } }
程式碼要點:
- ValidateCore 方法被拆分為了 ValidateNameRequired 和 ValidateNameMinLength 兩個方法,分別驗證 Name 的 Required 和 MinLength。
- Init 方法中使用了 local function 從而實現了方法 “先使用後定義” 的效果。讀者可以自上而下閱讀,從頂層開始瞭解整個方法的邏輯。
- Init 整體的邏輯就是通過表示式將 ValidateNameRequired 和 ValidateNameMinLength 重新組合成一個形如 ValidateCore 的委託
Func<CreateClaptrapInput, int, ValidateResult>
。 - Expression.Parameter 用於標明委託表示式的引數部分。
- Expression.Variable 用於標明一個變數,就是一個普通的變數。類似於程式碼中的
var a
。 - Expression.Label 用於標明一個特定的位置。在該樣例中,主要用於標定 return 語句的位置。熟悉 goto 語法的開發者知道, goto 的時候需要使用 label 來標記想要 goto 的地方。而實際上,return 就是一種特殊的 goto。所以想要在多個語句塊中 return 也同樣需要標記後才能 return。
- Expression.Block 可以將多個表示式順序組合在一起。可以理解為按順序寫程式碼。這裡我們將 CreateDefaultResult、CreateValidateNameRequiredExpression、CreateValidateNameMinLengthExpression 和 Label 表示式組合在一起。效果就類似於把這些程式碼按順序拼接在一起。
- CreateValidateNameRequiredExpression 和 CreateValidateNameMinLengthExpression 的結構非常類似,因為想要生成的結果表示式非常類似。
- 不必太在意 CreateValidateNameRequiredExpression 和 CreateValidateNameMinLengthExpression 當中的細節。可以在本樣例全部閱讀完之後再嘗試瞭解更多的 Expression.XXX 方法。
- 經過這樣的修改之後,我們就實現了擴充套件。假設現在需要對 Name 增加一個 MaxLength 不得超過 16 的驗證。只需要增加一個 ValidateNameMaxLength 的靜態方法,新增一個 CreateValidateNameMaxLengthExpression 的方法,並且加入到 Block 中即可。讀者可以嘗試動手操作一波實現這個效果。
第三步,讀取屬性
我們來改造 ValidateNameRequired 和 ValidateNameMinLength 兩個方法。因為現在這兩個方法接收的是 CreateClaptrapInput 作為引數,內部的邏輯也被寫死為驗證 Name,這很不優秀。
我們將改造這兩個方法,使其傳入 string name 表示驗證的屬性名稱,string value 表示驗證的屬性值。這樣我們就可以將這兩個驗證方法用於不限於 Name 的更多屬性。
using System; using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Linq.Expressions; using FluentAssertions; using NUnit.Framework; // ReSharper disable InvalidXmlDocComment namespace Newbe.ExpressionsTests { /// <summary> /// Property Expression /// </summary> public class X03PropertyValidationTest03 { private const int Count = 10_000; private static Func<CreateClaptrapInput, int, ValidateResult> _func; [SetUp] public void Init() { try { var finalExpression = CreateCore(); _func = finalExpression.Compile(); Expression<Func<CreateClaptrapInput, int, ValidateResult>> CreateCore() { // exp for input var inputExp = Expression.Parameter(typeof(CreateClaptrapInput), "input"); var nameProp = typeof(CreateClaptrapInput).GetProperty(nameof(CreateClaptrapInput.Name)); Debug.Assert(nameProp != null, nameof(nameProp) + " != null"); var namePropExp = Expression.Property(inputExp, nameProp); var nameNameExp = Expression.Constant(nameProp.Name); var minLengthPExp = Expression.Parameter(typeof(int), "minLength"); // exp for output var resultExp = Expression.Variable(typeof(ValidateResult), "result"); // exp for return statement var returnLabel = Expression.Label(typeof(ValidateResult)); // build whole block var body = Expression.Block( new[] {resultExp}, CreateDefaultResult(), CreateValidateNameRequiredExpression(), CreateValidateNameMinLengthExpression(), Expression.Label(returnLabel, resultExp)); // build lambda from body var final = Expression.Lambda<Func<CreateClaptrapInput, int, ValidateResult>>( body, inputExp, minLengthPExp); return final; Expression CreateDefaultResult() { var okMethod = typeof(ValidateResult).GetMethod(nameof(ValidateResult.Ok)); Debug.Assert(okMethod != null, nameof(okMethod) + " != null"); var methodCallExpression = Expression.Call(okMethod); var re = Expression.Assign(resultExp, methodCallExpression); /** * final as: * result = ValidateResult.Ok() */ return re; } Expression CreateValidateNameRequiredExpression() { var requireMethod = typeof(X03PropertyValidationTest03).GetMethod(nameof(ValidateStringRequired)); var isOkProperty = typeof(ValidateResult).GetProperty(nameof(ValidateResult.IsOk)); Debug.Assert(requireMethod != null, nameof(requireMethod) + " != null"); Debug.Assert(isOkProperty != null, nameof(isOkProperty) + " != null"); var requiredMethodExp = Expression.Call(requireMethod, nameNameExp, namePropExp); var assignExp = Expression.Assign(resultExp, requiredMethodExp); var resultIsOkPropertyExp = Expression.Property(resultExp, isOkProperty); var conditionExp = Expression.IsFalse(resultIsOkPropertyExp); var ifThenExp = Expression.IfThen(conditionExp, Expression.Return(returnLabel, resultExp)); var re = Expression.Block( new[] {resultExp}, assignExp, ifThenExp); /** * final as: * result = ValidateNameRequired("Name", input.Name); * if (!result.IsOk) * { * return result; * } */ return re; } Expression CreateValidateNameMinLengthExpression() { var minLengthMethod = typeof(X03PropertyValidationTest03).GetMethod(nameof(ValidateStringMinLength)); var isOkProperty = typeof(ValidateResult).GetProperty(nameof(ValidateResult.IsOk)); Debug.Assert(minLengthMethod != null, nameof(minLengthMethod) + " != null"); Debug.Assert(isOkProperty != null, nameof(isOkProperty) + " != null"); var requiredMethodExp = Expression.Call(minLengthMethod, nameNameExp, namePropExp, minLengthPExp); var assignExp = Expression.Assign(resultExp, requiredMethodExp); var resultIsOkPropertyExp = Expression.Property(resultExp, isOkProperty); var conditionExp = Expression.IsFalse(resultIsOkPropertyExp); var ifThenExp = Expression.IfThen(conditionExp, Expression.Return(returnLabel, resultExp)); var re = Expression.Block( new[] {resultExp}, assignExp, ifThenExp); /** * final as: * result = ValidateNameMinLength("Name", input.Name, minLength); * if (!result.IsOk) * { * return result; * } */ return re; } } } catch (Exception e) { Console.WriteLine(e); throw; } } [Test] public void Run() { // see code in demo repo } public static ValidateResult Validate(CreateClaptrapInput input) { return _func.Invoke(input, 3); } public static ValidateResult ValidateStringRequired(string name, string value) { return string.IsNullOrEmpty(value) ? ValidateResult.Error($"missing {name}") : ValidateResult.Ok(); } public static ValidateResult ValidateStringMinLength(string name, string value, int minLength) { return value.Length < minLength ? ValidateResult.Error($"Length of {name} should be great than {minLength}") : ValidateResult.Ok(); } } }
程式碼要點:
- 正如前文所述,我們修改了 ValidateNameRequired ,並重命名為 ValidateStringRequired。 ValidateNameMinLength -> ValidateStringMinLength。
- 修改了 CreateValidateNameRequiredExpression 和 CreateValidateNameMinLengthExpression,因為靜態方法的引數發生了變化。
- 通過這樣的改造,我們便可以將兩個靜態方法用於更多的屬性驗證。讀者可以嘗試增加一個 NickName 屬性。並且進行相同的驗證。
第四步,支援多個屬性驗證
接下來,我們通過將驗證 CreateClaptrapInput 所有的 string 屬性。
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; using FluentAssertions; using NUnit.Framework; // ReSharper disable InvalidXmlDocComment namespace Newbe.ExpressionsTests { /// <summary> /// Reflect Properties /// </summary> public class X03PropertyValidationTest04 { private const int Count = 10_000; private static Func<CreateClaptrapInput, int, ValidateResult> _func; [SetUp] public void Init() { try { var finalExpression = CreateCore(); _func = finalExpression.Compile(); Expression<Func<CreateClaptrapInput, int, ValidateResult>> CreateCore() { // exp for input var inputExp = Expression.Parameter(typeof(CreateClaptrapInput), "input"); var minLengthPExp = Expression.Parameter(typeof(int), "minLength"); // exp for output var resultExp = Expression.Variable(typeof(ValidateResult), "result"); // exp for return statement var returnLabel = Expression.Label(typeof(ValidateResult)); var innerExps = new List<Expression> {CreateDefaultResult()}; var stringProps = typeof(CreateClaptrapInput) .GetProperties() .Where(x => x.PropertyType == typeof(string)); foreach (var propertyInfo in stringProps) { innerExps.Add(CreateValidateStringRequiredExpression(propertyInfo)); innerExps.Add(CreateValidateStringMinLengthExpression(propertyInfo)); } innerExps.Add(Expression.Label(returnLabel, resultExp)); // build whole block var body = Expression.Block( new[] {resultExp}, innerExps); // build lambda from body var final = Expression.Lambda<Func<CreateClaptrapInput, int, ValidateResult>>( body, inputExp, minLengthPExp); return final; Expression CreateDefaultResult() { var okMethod = typeof(ValidateResult).GetMethod(nameof(ValidateResult.Ok)); Debug.Assert(okMethod != null, nameof(okMethod) + " != null"); var methodCallExpression = Expression.Call(okMethod); var re = Expression.Assign(resultExp, methodCallExpression); /** * final as: * result = ValidateResult.Ok() */ return re; } Expression CreateValidateStringRequiredExpression(PropertyInfo propertyInfo) { var requireMethod = typeof(X03PropertyValidationTest04).GetMethod(nameof(ValidateStringRequired)); var isOkProperty = typeof(ValidateResult).GetProperty(nameof(ValidateResult.IsOk)); Debug.Assert(requireMethod != null, nameof(requireMethod) + " != null"); Debug.Assert(isOkProperty != null, nameof(isOkProperty) + " != null"); var namePropExp = Expression.Property(inputExp, propertyInfo); var nameNameExp = Expression.Constant(propertyInfo.Name); var requiredMethodExp = Expression.Call(requireMethod, nameNameExp, namePropExp); var assignExp = Expression.Assign(resultExp, requiredMethodExp); var resultIsOkPropertyExp = Expression.Property(resultExp, isOkProperty); var conditionExp = Expression.IsFalse(resultIsOkPropertyExp); var ifThenExp = Expression.IfThen(conditionExp, Expression.Return(returnLabel, resultExp)); var re = Expression.Block( new[] {resultExp}, assignExp, ifThenExp); return re; } Expression CreateValidateStringMinLengthExpression(PropertyInfo propertyInfo) { var minLengthMethod = typeof(X03PropertyValidationTest04).GetMethod(nameof(ValidateStringMinLength)); var isOkProperty = typeof(ValidateResult).GetProperty(nameof(ValidateResult.IsOk)); Debug.Assert(minLengthMethod != null, nameof(minLengthMethod) + " != null"); Debug.Assert(isOkProperty != null, nameof(isOkProperty) + " != null"); var namePropExp = Expression.Property(inputExp, propertyInfo); var nameNameExp = Expression.Constant(propertyInfo.Name); var requiredMethodExp = Expression.Call(minLengthMethod, nameNameExp, namePropExp, minLengthPExp); var assignExp = Expression.Assign(resultExp, requiredMethodExp); var resultIsOkPropertyExp = Expression.Property(resultExp, isOkProperty); var conditionExp = Expression.IsFalse(resultIsOkPropertyExp); var ifThenExp = Expression.IfThen(conditionExp, Expression.Return(returnLabel, resultExp)); var re = Expression.Block( new[] {resultExp}, assignExp, ifThenExp); return re; } } } catch (Exception e) { Console.WriteLine(e); throw; } } [Test] public void Run() { // see code in demo repo } public static ValidateResult Validate(CreateClaptrapInput input) { return _func.Invoke(input, 3); } public static ValidateResult ValidateStringRequired(string name, string value) { return string.IsNullOrEmpty(value) ? ValidateResult.Error($"missing {name}") : ValidateResult.Ok(); } public static ValidateResult ValidateStringMinLength(string name, string value, int minLength) { return value.Length < minLength ? ValidateResult.Error($"Length of {name} should be great than {minLength}") : ValidateResult.Ok(); } public class CreateClaptrapInput { [Required] [MinLength(3)] public string Name { get; set; } [Required] [MinLength(3)] public string NickName { get; set; } } } }
程式碼要點:
- 在 CreateClaptrapInput 中增加了一個屬性 NickName ,測試用例也將驗證該屬性。
- 通過
List<Expression>
我們將更多動態生成的表示式加入到了 Block 中。因此,我們可以對 Name 和 NickName 都生成驗證表示式。
第五步,通過 Attribute 決定驗證內容
儘管前面我們已經支援驗證多種屬性了,但是關於是否進行驗證以及驗證的引數依然是寫死的(例如:MinLength 的長度)。
本節,我們將通過 Attribute 來決定驗證的細節。例如被標記為 Required 是屬性才會進行必填驗證。
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; using FluentAssertions; using NUnit.Framework; // ReSharper disable InvalidXmlDocComment namespace Newbe.ExpressionsTests { /// <summary> /// Using Attribute /// </summary> public class X03PropertyValidationTest05 { private const int Count = 10_000; private static Func<CreateClaptrapInput, ValidateResult> _func; [SetUp] public void Init() { try { var finalExpression = CreateCore(); _func = finalExpression.Compile(); Expression<Func<CreateClaptrapInput, ValidateResult>> CreateCore() { // exp for input var inputExp = Expression.Parameter(typeof(CreateClaptrapInput), "input"); // exp for output var resultExp = Expression.Variable(typeof(ValidateResult), "result"); // exp for return statement var returnLabel = Expression.Label(typeof(ValidateResult)); var innerExps = new List<Expression> {CreateDefaultResult()}; var stringProps = typeof(CreateClaptrapInput) .GetProperties() .Where(x => x.PropertyType == typeof(string)); foreach (var propertyInfo in stringProps) { if (propertyInfo.GetCustomAttribute<RequiredAttribute>() != null) { innerExps.Add(CreateValidateStringRequiredExpression(propertyInfo)); } var minlengthAttribute = propertyInfo.GetCustomAttribute<MinLengthAttribute>(); if (minlengthAttribute != null) { innerExps.Add( CreateValidateStringMinLengthExpression(propertyInfo, minlengthAttribute.Length)); } } innerExps.Add(Expression.Label(returnLabel, resultExp)); // build whole block var body = Expression.Block( new[] {resultExp}, innerExps); // build lambda from body var final = Expression.Lambda<Func<CreateClaptrapInput, ValidateResult>>( body, inputExp); return final; Expression CreateDefaultResult() { var okMethod = typeof(ValidateResult).GetMethod(nameof(ValidateResult.Ok)); Debug.Assert(okMethod != null, nameof(okMethod) + " != null"); var methodCallExpression = Expression.Call(okMethod); var re = Expression.Assign(resultExp, methodCallExpression); /** * final as: * result = ValidateResult.Ok() */ return re; } Expression CreateValidateStringRequiredExpression(PropertyInfo propertyInfo) { var requireMethod = typeof(X03PropertyValidationTest05).GetMethod(nameof(ValidateStringRequired)); var isOkProperty = typeof(ValidateResult).GetProperty(nameof(ValidateResult.IsOk)); Debug.Assert(requireMethod != null, nameof(requireMethod) + " != null"); Debug.Assert(isOkProperty != null, nameof(isOkProperty) + " != null"); var namePropExp = Expression.Property(inputExp, propertyInfo); var nameNameExp = Expression.Constant(propertyInfo.Name); var requiredMethodExp = Expression.Call(requireMethod, nameNameExp, namePropExp); var assignExp = Expression.Assign(resultExp, requiredMethodExp); var resultIsOkPropertyExp = Expression.Property(resultExp, isOkProperty); var conditionExp = Expression.IsFalse(resultIsOkPropertyExp); var ifThenExp = Expression.IfThen(conditionExp, Expression.Return(returnLabel, resultExp)); var re = Expression.Block( new[] {resultExp}, assignExp, ifThenExp); return re; } Expression CreateValidateStringMinLengthExpression(PropertyInfo propertyInfo, int minlengthAttributeLength) { var minLengthMethod = typeof(X03PropertyValidationTest05).GetMethod(nameof(ValidateStringMinLength)); var isOkProperty = typeof(ValidateResult).GetProperty(nameof(ValidateResult.IsOk)); Debug.Assert(minLengthMethod != null, nameof(minLengthMethod) + " != null"); Debug.Assert(isOkProperty != null, nameof(isOkProperty) + " != null"); var namePropExp = Expression.Property(inputExp, propertyInfo); var nameNameExp = Expression.Constant(propertyInfo.Name); var requiredMethodExp = Expression.Call(minLengthMethod, nameNameExp, namePropExp, Expression.Constant(minlengthAttributeLength)); var assignExp = Expression.Assign(resultExp, requiredMethodExp); var resultIsOkPropertyExp = Expression.Property(resultExp, isOkProperty); var conditionExp = Expression.IsFalse(resultIsOkPropertyExp); var ifThenExp = Expression.IfThen(conditionExp, Expression.Return(returnLabel, resultExp)); var re = Expression.Block( new[] {resultExp}, assignExp, ifThenExp); return re; } } } catch (Exception e) { Console.WriteLine(e); throw; } } [Test] public void Run() { // see code in demo repo } public class CreateClaptrapInput { [Required] [MinLength(3)] public string Name { get; set; } [Required] [MinLength(3)] public string NickName { get; set; } } } }
程式碼要點:
- 在構建
List<Expression>
時通過屬性上的 Attribute 上的決定是否加入特定的表示式。
第六步,將靜態方法換為表示式
ValidateStringRequired 和 ValidateStringMinLength 兩個靜態方法的內部實際上只包含一個判斷三目表示式,而且在 C# 中,可以將 Lambda 方法賦值個一個表示式。
因此,我們可以直接將 ValidateStringRequired 和 ValidateStringMinLength 改換為表示式,這樣就不需要反射來獲取靜態方法再去構建表示式了。
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; using FluentAssertions; using NUnit.Framework; // ReSharper disable InvalidXmlDocComment namespace Newbe.ExpressionsTests { /// <summary> /// Static Method to Expression /// </summary> public class X03PropertyValidationTest06 { private const int Count = 10_000; private static Func<CreateClaptrapInput, ValidateResult> _func; [SetUp] public void Init() { try { var finalExpression = CreateCore(); _func = finalExpression.Compile(); Expression<Func<CreateClaptrapInput, ValidateResult>> CreateCore() { // exp for input var inputExp = Expression.Parameter(typeof(CreateClaptrapInput), "input"); // exp for output var resultExp = Expression.Variable(typeof(ValidateResult), "result"); // exp for return statement var returnLabel = Expression.Label(typeof(ValidateResult)); var innerExps = new List<Expression> {CreateDefaultResult()}; var stringProps = typeof(CreateClaptrapInput) .GetProperties() .Where(x => x.PropertyType == typeof(string)); foreach (var propertyInfo in stringProps) { if (propertyInfo.GetCustomAttribute<RequiredAttribute>() != null) { innerExps.Add(CreateValidateStringRequiredExpression(propertyInfo)); } var minlengthAttribute = propertyInfo.GetCustomAttribute<MinLengthAttribute>(); if (minlengthAttribute != null) { innerExps.Add( CreateValidateStringMinLengthExpression(propertyInfo, minlengthAttribute.Length)); } } innerExps.Add(Expression.Label(returnLabel, resultExp)); // build whole block var body = Expression.Block( new[] {resultExp}, innerExps); // build lambda from body var final = Expression.Lambda<Func<CreateClaptrapInput, ValidateResult>>( body, inputExp); return final; Expression CreateDefaultResult() { var okMethod = typeof(ValidateResult).GetMethod(nameof(ValidateResult.Ok)); Debug.Assert(okMethod != null, nameof(okMethod) + " != null"); var methodCallExpression = Expression.Call(okMethod); var re = Expression.Assign(resultExp, methodCallExpression); /** * final as: * result = ValidateResult.Ok() */ return re; } Expression CreateValidateStringRequiredExpression(PropertyInfo propertyInfo) { var isOkProperty = typeof(ValidateResult).GetProperty(nameof(ValidateResult.IsOk)); Debug.Assert(isOkProperty != null, nameof(isOkProperty) + " != null"); var namePropExp = Expression.Property(inputExp, propertyInfo); var nameNameExp = Expression.Constant(propertyInfo.Name); var requiredMethodExp = Expression.Invoke(ValidateStringRequiredExp, nameNameExp, namePropExp); var assignExp = Expression.Assign(resultExp, requiredMethodExp); var resultIsOkPropertyExp = Expression.Property(resultExp, isOkProperty); var conditionExp = Expression.IsFalse(resultIsOkPropertyExp); var ifThenExp = Expression.IfThen(conditionExp, Expression.Return(returnLabel, resultExp)); var re = Expression.Block( new[] {resultExp}, assignExp, ifThenExp); return re; } Expression CreateValidateStringMinLengthExpression(PropertyInfo propertyInfo, int minlengthAttributeLength) { var isOkProperty = typeof(ValidateResult).GetProperty(nameof(ValidateResult.IsOk)); Debug.Assert(isOkProperty != null, nameof(isOkProperty) + " != null"); var namePropExp = Expression.Property(inputExp, propertyInfo); var nameNameExp = Expression.Constant(propertyInfo.Name); var requiredMethodExp = Expression.Invoke(ValidateStringMinLengthExp, nameNameExp, namePropExp, Expression.Constant(minlengthAttributeLength)); var assignExp = Expression.Assign(resultExp, requiredMethodExp); var resultIsOkPropertyExp = Expression.Property(resultExp, isOkProperty); var conditionExp = Expression.IsFalse(resultIsOkPropertyExp); var ifThenExp = Expression.IfThen(conditionExp, Expression.Return(returnLabel, resultExp)); var re = Expression.Block( new[] {resultExp}, assignExp, ifThenExp); return re; } } } catch (Exception e) { Console.WriteLine(e); throw; } } [Test] public void Run() { // see code in demo repo } private static readonly Expression<Func<string, string, ValidateResult>> ValidateStringRequiredExp = (name, value) => string.IsNullOrEmpty(value) ? ValidateResult.Error($"missing {name}") : ValidateResult.Ok(); private static readonly Expression<Func<string, string, int, ValidateResult>> ValidateStringMinLengthExp = (name, value, minLength) => value.Length < minLength ? ValidateResult.Error($"Length of {name} should be great than {minLength}") : ValidateResult.Ok(); } }
程式碼要點:
- 將靜態方法換成了表示式。因此 CreateXXXExpression 相應的位置也進行了修改,程式碼就更短了。
第七步,柯里化
柯理化,也稱為函式柯理化,是函數語言程式設計當中的一種方法。簡單的可以表述為:通過固定一個多引數函式的一個或幾個引數,從而得到一個引數更少的函式。術語化一些,也可以表述為將高階函式(函式的階其實就是說引數的個數)轉換為低階函式的方法。
例如,現在有一個 add (int,int) 的函式,它實現了將兩個數相加的功能。假如我們固定集中第一個引數為 5 ,則我們會得到一個 add (5,int) 的函式,它實現的是將一個數加 5 的功能。
這有什麼意義?
函式降階可以使得函式變得一致,得到了一致的函式之後可以做一些程式碼上的統一以便優化。例如上面使用到的兩個表示式:
Expression<Func<string, string, ValidateResult>> ValidateStringRequiredExp
Expression<Func<string, string, int, ValidateResult>> ValidateStringMinLengthExp
這兩個表示式中第二個表示式和第一個表示式之間僅僅區別在第三引數上。如果我們使用柯理化固定第三個 int 引數,則可以使得兩個表示式的簽名完全一樣。這其實和麵向物件中的抽象非常類似。
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; using FluentAssertions; using NUnit.Framework; // ReSharper disable InvalidXmlDocComment namespace Newbe.ExpressionsTests { /// <summary> /// Currying /// </summary> public class X03PropertyValidationTest07 { private const int Count = 10_000; private static Func<CreateClaptrapInput, ValidateResult> _func; [SetUp] public void Init() { try { var finalExpression = CreateCore(); _func = finalExpression.Compile(); Expression<Func<CreateClaptrapInput, ValidateResult>> CreateCore() { // exp for input var inputExp = Expression.Parameter(typeof(CreateClaptrapInput), "input"); // exp for output var resultExp = Expression.Variable(typeof(ValidateResult), "result"); // exp for return statement var returnLabel = Expression.Label(typeof(ValidateResult)); var innerExps = new List<Expression> {CreateDefaultResult()}; var stringProps = typeof(CreateClaptrapInput) .GetProperties() .Where(x => x.PropertyType == typeof(string)); foreach (var propertyInfo in stringProps) { if (propertyInfo.GetCustomAttribute<RequiredAttribute>() != null) { innerExps.Add(CreateValidateStringRequiredExpression(propertyInfo)); } var minlengthAttribute = propertyInfo.GetCustomAttribute<MinLengthAttribute>(); if (minlengthAttribute != null) { innerExps.Add( CreateValidateStringMinLengthExpression(propertyInfo, minlengthAttribute.Length)); } } innerExps.Add(Expression.Label(returnLabel, resultExp)); // build whole block var body = Expression.Block( new[] {resultExp}, innerExps); // build lambda from body var final = Expression.Lambda<Func<CreateClaptrapInput, ValidateResult>>( body, inputExp); return final; Expression CreateDefaultResult() { var okMethod = typeof(ValidateResult).GetMethod(nameof(ValidateResult.Ok)); Debug.Assert(okMethod != null, nameof(okMethod) + " != null"); var methodCallExpression = Expression.Call(okMethod); var re = Expression.Assign(resultExp, methodCallExpression); /** * final as: * result = ValidateResult.Ok() */ return re; } Expression CreateValidateStringRequiredExpression(PropertyInfo propertyInfo) { var isOkProperty = typeof(ValidateResult).GetProperty(nameof(ValidateResult.IsOk)); Debug.Assert(isOkProperty != null, nameof(isOkProperty) + " != null"); var namePropExp = Expression.Property(inputExp, propertyInfo); var nameNameExp = Expression.Constant(propertyInfo.Name); var requiredMethodExp = Expression.Invoke(CreateValidateStringRequiredExp(), nameNameExp, namePropExp); var assignExp = Expression.Assign(resultExp, requiredMethodExp); var resultIsOkPropertyExp = Expression.Property(resultExp, isOkProperty); var conditionExp = Expr