做移动网站优化排名首页,济南智能网站建设费用,论学院网站建设项目的进度管理,网站系统建站表达式树是 .net 中一系列非常好用的类型。在一些场景中使用表达式树可以获得更好的性能和更佳的扩展性。本篇我们将通过构建一个 “模型验证器” 来理解和应用表达式树在构建动态调用方面的优势。Newbe.Claptrap 是一个用于轻松应对并发问题的分布式开发框架。如果您是首次阅读… 表达式树是 .net 中一系列非常好用的类型。在一些场景中使用表达式树可以获得更好的性能和更佳的扩展性。本篇我们将通过构建一个 “模型验证器” 来理解和应用表达式树在构建动态调用方面的优势。Newbe.Claptrap 是一个用于轻松应对并发问题的分布式开发框架。如果您是首次阅读本系列文章。建议可以先从本文末尾的入门文章开始了解。开篇摘要前不久我们发布了《如何使用 dotTrace 来诊断 netcore 应用的性能问题》https://www.newbe.pro/Newbe.Claptrap/How-to-Use-DotTrace/ 经过网友投票之后网友们表示对其中表达式树的内容很感兴趣因此本篇我们将展开讲讲。动态调用是在 .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.LambdaActionClaptrap, 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 ActionClaptrap, int _func;public class Claptrap{public int Level { get; set; }public void LevelUp(int diff){Level diff;}}}
}
MethodTimeRunReflection217msRunExpression20msDirectly19ms以上测试中我们对第三种调用方式一百万次调用并记录每个测试所花费的时间。可以得到类似以下的结果可以得出以下结论使用表达式树创建委托进行动态调用可以得到和直接调用近乎相同的性能。使用表达式树创建委托进行动态调用所消耗的时间约为十分之一。所以如果仅仅从性能上考虑应该使用表达式树也可以是用表达式树。不过这是在一百万调用下体现出现的时间对于单次调用而言其实就是纳秒级别的区别其实无足轻重。但其实表达式树不仅仅在性能上相较于反射更优其更强大的扩展性其实采用最为重要的特性。此处还有一个对属性进行操作的测试此处将测试代码和结果罗列如下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.LambdaActionClaptrap, 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 ActionClaptrap, int _func;public class Claptrap{public int Level { get; set; }}}
}MethodTimeRunReflection373msRunExpression19msDirectly18ms耗时情况由于反射多了一份装拆箱的消耗所以比起前一个测试样例显得更慢了使用委托是没有这种消耗的。第〇步需求演示先通过一个测试来了解我们要创建的 “模型验证器” 究竟是一个什么样的需求。using System.ComponentModel.DataAnnotations;
using FluentAssertions;
using NUnit.Framework;namespace Newbe.ExpressionsTests
{/// summary/// Validate data by static method/// /summarypublic 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/// /summarypublic class X03PropertyValidationTest01{private const int Count 10_000;private static FuncCreateClaptrapInput, 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.LambdaFuncCreateClaptrapInput, 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 InvalidXmlDocCommentnamespace Newbe.ExpressionsTests
{/// summary/// Block Expression/// /summarypublic class X03PropertyValidationTest02{private const int Count 10_000;private static FuncCreateClaptrapInput, int, ValidateResult _func;[SetUp]public void Init(){try{var finalExpression CreateCore();_func finalExpression.Compile();ExpressionFuncCreateClaptrapInput, int, ValidateResult CreateCore(){// exp for inputvar inputExp Expression.Parameter(typeof(CreateClaptrapInput), input);var minLengthPExp Expression.Parameter(typeof(int), minLength);// exp for outputvar resultExp Expression.Variable(typeof(ValidateResult), result);// exp for return statementvar returnLabel Expression.Label(typeof(ValidateResult));// build whole blockvar body Expression.Block(new[] {resultExp},CreateDefaultResult(),CreateValidateNameRequiredExpression(),CreateValidateNameMinLengthExpression(),Expression.Label(returnLabel, resultExp));// build lambda from bodyvar final Expression.LambdaFuncCreateClaptrapInput, 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 的委托 FuncCreateClaptrapInput, 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 InvalidXmlDocCommentnamespace Newbe.ExpressionsTests
{/// summary/// Property Expression/// /summarypublic class X03PropertyValidationTest03{private const int Count 10_000;private static FuncCreateClaptrapInput, int, ValidateResult _func;[SetUp]public void Init(){try{var finalExpression CreateCore();_func finalExpression.Compile();ExpressionFuncCreateClaptrapInput, int, ValidateResult CreateCore(){// exp for inputvar 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 outputvar resultExp Expression.Variable(typeof(ValidateResult), result);// exp for return statementvar returnLabel Expression.Label(typeof(ValidateResult));// build whole blockvar body Expression.Block(new[] {resultExp},CreateDefaultResult(),CreateValidateNameRequiredExpression(),CreateValidateNameMinLengthExpression(),Expression.Label(returnLabel, resultExp));// build lambda from bodyvar final Expression.LambdaFuncCreateClaptrapInput, 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 InvalidXmlDocCommentnamespace Newbe.ExpressionsTests
{/// summary/// Reflect Properties/// /summarypublic class X03PropertyValidationTest04{private const int Count 10_000;private static FuncCreateClaptrapInput, int, ValidateResult _func;[SetUp]public void Init(){try{var finalExpression CreateCore();_func finalExpression.Compile();ExpressionFuncCreateClaptrapInput, int, ValidateResult CreateCore(){// exp for inputvar inputExp Expression.Parameter(typeof(CreateClaptrapInput), input);var minLengthPExp Expression.Parameter(typeof(int), minLength);// exp for outputvar resultExp Expression.Variable(typeof(ValidateResult), result);// exp for return statementvar returnLabel Expression.Label(typeof(ValidateResult));var innerExps new ListExpression {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 blockvar body Expression.Block(new[] {resultExp},innerExps);// build lambda from bodyvar final Expression.LambdaFuncCreateClaptrapInput, 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 测试用例也将验证该属性。代码要点通过 ListExpression 我们将更多动态生成的表达式加入到了 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 InvalidXmlDocCommentnamespace Newbe.ExpressionsTests
{/// summary/// Using Attribute/// /summarypublic class X03PropertyValidationTest05{private const int Count 10_000;private static FuncCreateClaptrapInput, ValidateResult _func;[SetUp]public void Init(){try{var finalExpression CreateCore();_func finalExpression.Compile();ExpressionFuncCreateClaptrapInput, ValidateResult CreateCore(){// exp for inputvar inputExp Expression.Parameter(typeof(CreateClaptrapInput), input);// exp for outputvar resultExp Expression.Variable(typeof(ValidateResult), result);// exp for return statementvar returnLabel Expression.Label(typeof(ValidateResult));var innerExps new ListExpression {CreateDefaultResult()};var stringProps typeof(CreateClaptrapInput).GetProperties().Where(x x.PropertyType typeof(string));foreach (var propertyInfo in stringProps){if (propertyInfo.GetCustomAttributeRequiredAttribute() ! null){innerExps.Add(CreateValidateStringRequiredExpression(propertyInfo));}var minlengthAttribute propertyInfo.GetCustomAttributeMinLengthAttribute();if (minlengthAttribute ! null){innerExps.Add(CreateValidateStringMinLengthExpression(propertyInfo, minlengthAttribute.Length));}}innerExps.Add(Expression.Label(returnLabel, resultExp));// build whole blockvar body Expression.Block(new[] {resultExp},innerExps);// build lambda from bodyvar final Expression.LambdaFuncCreateClaptrapInput, 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; }}}
}在构建 ListExpression 时通过属性上的 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 InvalidXmlDocCommentnamespace Newbe.ExpressionsTests
{/// summary/// Static Method to Expression/// /summarypublic class X03PropertyValidationTest06{private const int Count 10_000;private static FuncCreateClaptrapInput, ValidateResult _func;[SetUp]public void Init(){try{var finalExpression CreateCore();_func finalExpression.Compile();ExpressionFuncCreateClaptrapInput, ValidateResult CreateCore(){// exp for inputvar inputExp Expression.Parameter(typeof(CreateClaptrapInput), input);// exp for outputvar resultExp Expression.Variable(typeof(ValidateResult), result);// exp for return statementvar returnLabel Expression.Label(typeof(ValidateResult));var innerExps new ListExpression {CreateDefaultResult()};var stringProps typeof(CreateClaptrapInput).GetProperties().Where(x x.PropertyType typeof(string));foreach (var propertyInfo in stringProps){if (propertyInfo.GetCustomAttributeRequiredAttribute() ! null){innerExps.Add(CreateValidateStringRequiredExpression(propertyInfo));}var minlengthAttribute propertyInfo.GetCustomAttributeMinLengthAttribute();if (minlengthAttribute ! null){innerExps.Add(CreateValidateStringMinLengthExpression(propertyInfo, minlengthAttribute.Length));}}innerExps.Add(Expression.Label(returnLabel, resultExp));// build whole blockvar body Expression.Block(new[] {resultExp},innerExps);// build lambda from bodyvar final Expression.LambdaFuncCreateClaptrapInput, 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 ExpressionFuncstring, string, ValidateResult ValidateStringRequiredExp (name, value) string.IsNullOrEmpty(value)? ValidateResult.Error($missing {name}): ValidateResult.Ok();private static readonly ExpressionFuncstring, 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 的功能。这有什么意义函数降阶可以使得函数变得一致得到了一致的函数之后可以做一些代码上的统一以便优化。例如上面使用到的两个表达式ExpressionFuncstring, string, ValidateResult ValidateStringRequiredExpExpressionFuncstring, 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 InvalidXmlDocCommentnamespace Newbe.ExpressionsTests
{/// summary/// Currying/// /summarypublic class X03PropertyValidationTest07{private const int Count 10_000;private static FuncCreateClaptrapInput, ValidateResult _func;[SetUp]public void Init(){try{var finalExpression CreateCore();_func finalExpression.Compile();ExpressionFuncCreateClaptrapInput, ValidateResult CreateCore(){// exp for inputvar inputExp Expression.Parameter(typeof(CreateClaptrapInput), input);// exp for outputvar resultExp Expression.Variable(typeof(ValidateResult), result);// exp for return statementvar returnLabel Expression.Label(typeof(ValidateResult));var innerExps new ListExpression {CreateDefaultResult()};var stringProps typeof(CreateClaptrapInput).GetProperties().Where(x x.PropertyType typeof(string));foreach (var propertyInfo in stringProps){if (propertyInfo.GetCustomAttributeRequiredAttribute() ! null){innerExps.Add(CreateValidateStringRequiredExpression(propertyInfo));}var minlengthAttribute propertyInfo.GetCustomAttributeMinLengthAttribute();if (minlengthAttribute ! null){innerExps.Add(CreateValidateStringMinLengthExpression(propertyInfo, minlengthAttribute.Length));}}innerExps.Add(Expression.Label(returnLabel, resultExp));// build whole blockvar body Expression.Block(new[] {resultExp},innerExps);// build lambda from bodyvar final Expression.LambdaFuncCreateClaptrapInput, 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 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(CreateValidateStringMinLengthExp(minlengthAttributeLength),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;}}}catch (Exception e){Console.WriteLine(e);throw;}}[Test]public void Run(){// see code in demo repo}private static ExpressionFuncstring, string, ValidateResult CreateValidateStringRequiredExp(){return (name, value) string.IsNullOrEmpty(value)? ValidateResult.Error($missing {name}): ValidateResult.Ok();}private static ExpressionFuncstring, string, ValidateResult CreateValidateStringMinLengthExp(int minLength){return (name, value) value.Length minLength? ValidateResult.Error($Length of {name} should be great than {minLength}): ValidateResult.Ok();}}
}CreateValidateStringMinLengthExp 静态方法传入一个参数创建得到一个和 CreateValidateStringRequiredExp 返回值一样的表达式。对比上一节中的 ValidateStringMinLengthExp 实现了固定 int 参数而得到一个新表达式的操作。这就是一种柯理化的体现。代码要点为了统一都采用静态方法我们将上一节中的 ValidateStringRequiredExp 也改为 CreateValidateStringRequiredExp 静态方法这仅仅只是为了看起来一致但实际上增加了一点点开销因为没必要重复创建一个不变的表达式。相应的调整一下 ListExpression 组装过程的代码。第八步合并重复代码本节我们将合并 CreateValidateStringRequiredExpression 和 CreateValidateStringMinLengthExpression 中重复的代码。其中只有 requiredMethodExp 的创建方式不同。因此只要将这个参数从方法外面传入就可以抽离出公共部分。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 InvalidXmlDocCommentnamespace Newbe.ExpressionsTests
{/// summary/// Refactor to CreateValidateExpression/// /summarypublic class X03PropertyValidationTest08{private const int Count 10_000;private static FuncCreateClaptrapInput, ValidateResult _func;[SetUp]public void Init(){try{var finalExpression CreateCore();_func finalExpression.Compile();ExpressionFuncCreateClaptrapInput, ValidateResult CreateCore(){// exp for inputvar inputExp Expression.Parameter(typeof(CreateClaptrapInput), input);// exp for outputvar resultExp Expression.Variable(typeof(ValidateResult), result);// exp for return statementvar returnLabel Expression.Label(typeof(ValidateResult));var innerExps new ListExpression {CreateDefaultResult()};var stringProps typeof(CreateClaptrapInput).GetProperties().Where(x x.PropertyType typeof(string));foreach (var propertyInfo in stringProps){if (propertyInfo.GetCustomAttributeRequiredAttribute() ! null){innerExps.Add(CreateValidateStringRequiredExpression(propertyInfo));}var minlengthAttribute propertyInfo.GetCustomAttributeMinLengthAttribute();if (minlengthAttribute ! null){innerExps.Add(CreateValidateStringMinLengthExpression(propertyInfo, minlengthAttribute.Length));}}innerExps.Add(Expression.Label(returnLabel, resultExp));// build whole blockvar body Expression.Block(new[] {resultExp},innerExps);// build lambda from bodyvar final Expression.LambdaFuncCreateClaptrapInput, 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) CreateValidateExpression(propertyInfo,CreateValidateStringRequiredExp());Expression CreateValidateStringMinLengthExpression(PropertyInfo propertyInfo,int minlengthAttributeLength) CreateValidateExpression(propertyInfo,CreateValidateStringMinLengthExp(minlengthAttributeLength));Expression CreateValidateExpression(PropertyInfo propertyInfo,ExpressionFuncstring, string, ValidateResult validateFuncExpression){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(validateFuncExpression,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;}}}catch (Exception e){Console.WriteLine(e);throw;}}[Test]public void Run(){// see code in demo repo}}
}CreateValidateExpression 就是被抽离出来的公共方法。代码要点如果没有前一步柯理化CreateValidateExpression 的第二个参数 validateFuncExpression 将很难确定。CreateValidateStringRequiredExpression 和 CreateValidateStringMinLengthExpression 内部调用了 CreateValidateExpression但是固定了几个参数。这其实也可以被认为是一种柯理化因为返回值是表达式其实可以被认为是一种函数的表现形式当然理解为重载也没有问题不必太过纠结。第九步支持更多模型到现在我们已经得到了一个支持验证 CreateClaptrapInput 多个 string 字段的验证器。并且即使要扩展多更多类型也不是太难只要增加表达式即可。本节我们将 CreateClaptrapInput 抽象为更抽象的类型毕竟没有模型验证器是专门只能验证一个 class 的。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 InvalidXmlDocCommentnamespace Newbe.ExpressionsTests
{/// summary/// Multiple Type/// /summarypublic class X03PropertyValidationTest09{private const int Count 10_000;private static readonly DictionaryType, Funcobject, ValidateResult ValidateFunc new DictionaryType, Funcobject, ValidateResult();[SetUp]public void Init(){try{var finalExpression CreateCore(typeof(CreateClaptrapInput));ValidateFunc[typeof(CreateClaptrapInput)] finalExpression.Compile();ExpressionFuncobject, ValidateResult CreateCore(Type type){// exp for inputvar inputExp Expression.Parameter(typeof(object), input);// exp for outputvar resultExp Expression.Variable(typeof(ValidateResult), result);// exp for return statementvar returnLabel Expression.Label(typeof(ValidateResult));var innerExps new ListExpression {CreateDefaultResult()};var stringProps type.GetProperties().Where(x x.PropertyType typeof(string));foreach (var propertyInfo in stringProps){if (propertyInfo.GetCustomAttributeRequiredAttribute() ! null){innerExps.Add(CreateValidateStringRequiredExpression(propertyInfo));}var minlengthAttribute propertyInfo.GetCustomAttributeMinLengthAttribute();if (minlengthAttribute ! null){innerExps.Add(CreateValidateStringMinLengthExpression(propertyInfo, minlengthAttribute.Length));}}innerExps.Add(Expression.Label(returnLabel, resultExp));// build whole blockvar body Expression.Block(new[] {resultExp},innerExps);// build lambda from bodyvar final Expression.LambdaFuncobject, 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) CreateValidateExpression(propertyInfo,CreateValidateStringRequiredExp());Expression CreateValidateStringMinLengthExpression(PropertyInfo propertyInfo,int minlengthAttributeLength) CreateValidateExpression(propertyInfo,CreateValidateStringMinLengthExp(minlengthAttributeLength));Expression CreateValidateExpression(PropertyInfo propertyInfo,ExpressionFuncstring, string, ValidateResult validateFuncExpression){var isOkProperty typeof(ValidateResult).GetProperty(nameof(ValidateResult.IsOk));Debug.Assert(isOkProperty ! null, nameof(isOkProperty) ! null);var convertedExp Expression.Convert(inputExp, type);var namePropExp Expression.Property(convertedExp, propertyInfo);var nameNameExp Expression.Constant(propertyInfo.Name);var requiredMethodExp Expression.Invoke(validateFuncExpression,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;}}}catch (Exception e){Console.WriteLine(e);throw;}}[Test]public void Run(){// see code in demo repo}public static ValidateResult Validate(CreateClaptrapInput input){return ValidateFunc[typeof(CreateClaptrapInput)].Invoke(input);}}
}将 FuncCreateClaptrapInput, ValidateResult 替换为了 Funcobject, ValidateResult并且将写死的 typeof (CreateClaptrapInput) 都替换为了 type。代码要点将对应类型的验证器创建好之后保存在 ValidateFunc 中。这样就不需要每次都重建整个 Func。第十步加入一些细节最后的最后我们又到了令人愉快的 “加入一些细节” 阶段按照业务特性对抽象接口和实现进行调整。于是我们就得到了本示例最终的版本。using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Autofac;
using FluentAssertions;
using NUnit.Framework;
using Module Autofac.Module;// ReSharper disable InvalidXmlDocCommentnamespace Newbe.ExpressionsTests
{/// summary/// Final/// /summarypublic class X03PropertyValidationTest10{private const int Count 10_000;private IValidatorFactory _factory null!;[SetUp]public void Init(){try{var builder new ContainerBuilder();builder.RegisterModuleValidatorModule();var container builder.Build();_factory container.ResolveIValidatorFactory();}catch (Exception e){Console.WriteLine(e);throw;}}[Test]public void Run(){for (int i 0; i Count; i){// test 1{var input new CreateClaptrapInput{NickName newbe36524};var (isOk, errorMessage) Validate(input);isOk.Should().BeFalse();errorMessage.Should().Be(missing Name);}// test 2{var input new CreateClaptrapInput{Name 1,NickName newbe36524};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,NickName newbe36524};var (isOk, errorMessage) Validate(input);isOk.Should().BeTrue();errorMessage.Should().BeNullOrEmpty();}}}public ValidateResult Validate(CreateClaptrapInput input){Debug.Assert(_factory ! null, nameof(_factory) ! null);var validator _factory.GetValidator(typeof(CreateClaptrapInput));return validator.Invoke(input);}public class CreateClaptrapInput{[Required] [MinLength(3)] public string Name { get; set; }[Required] [MinLength(3)] public string NickName { 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};}}private class ValidatorModule : Module{protected override void Load(ContainerBuilder builder){base.Load(builder);builder.RegisterTypeValidatorFactory().AsIValidatorFactory().SingleInstance();builder.RegisterTypeStringRequiredPropertyValidatorFactory().AsIPropertyValidatorFactory().SingleInstance();builder.RegisterTypeStringLengthPropertyValidatorFactory().AsIPropertyValidatorFactory().SingleInstance();}}public interface IValidatorFactory{Funcobject, ValidateResult GetValidator(Type type);}public interface IPropertyValidatorFactory{IEnumerableExpression CreateExpression(CreatePropertyValidatorInput input);}public abstract class PropertyValidatorFactoryBaseTValue : IPropertyValidatorFactory{public virtual IEnumerableExpression CreateExpression(CreatePropertyValidatorInput input){if (input.PropertyInfo.PropertyType ! typeof(TValue)){return Enumerable.EmptyExpression();}var expressionCore CreateExpressionCore(input);return expressionCore;}protected abstract IEnumerableExpression CreateExpressionCore(CreatePropertyValidatorInput input);protected Expression CreateValidateExpression(CreatePropertyValidatorInput input,ExpressionFuncstring, TValue, ValidateResult validateFuncExpression){var propertyInfo input.PropertyInfo;var isOkProperty typeof(ValidateResult).GetProperty(nameof(ValidateResult.IsOk));Debug.Assert(isOkProperty ! null, nameof(isOkProperty) ! null);var convertedExp Expression.Convert(input.InputExpression, input.InputType);var propExp Expression.Property(convertedExp, propertyInfo);var nameExp Expression.Constant(propertyInfo.Name);var requiredMethodExp Expression.Invoke(validateFuncExpression,nameExp,propExp);var assignExp Expression.Assign(input.ResultExpression, requiredMethodExp);var resultIsOkPropertyExp Expression.Property(input.ResultExpression, isOkProperty);var conditionExp Expression.IsFalse(resultIsOkPropertyExp);var ifThenExp Expression.IfThen(conditionExp,Expression.Return(input.ReturnLabel, input.ResultExpression));var re Expression.Block(new[] {input.ResultExpression},assignExp,ifThenExp);return re;}}public class StringRequiredPropertyValidatorFactory : PropertyValidatorFactoryBasestring{private static ExpressionFuncstring, string, ValidateResult CreateValidateStringRequiredExp(){return (name, value) string.IsNullOrEmpty(value)? ValidateResult.Error($missing {name}): ValidateResult.Ok();}protected override IEnumerableExpression CreateExpressionCore(CreatePropertyValidatorInput input){var propertyInfo input.PropertyInfo;if (propertyInfo.GetCustomAttributeRequiredAttribute() ! null){yield return CreateValidateExpression(input, CreateValidateStringRequiredExp());}}}public class StringLengthPropertyValidatorFactory : PropertyValidatorFactoryBasestring{private static ExpressionFuncstring, string, ValidateResult CreateValidateStringMinLengthExp(int minLength){return (name, value) string.IsNullOrEmpty(value) || value.Length minLength? ValidateResult.Error($Length of {name} should be great than {minLength}): ValidateResult.Ok();}protected override IEnumerableExpression CreateExpressionCore(CreatePropertyValidatorInput input){var propertyInfo input.PropertyInfo;var minlengthAttribute propertyInfo.GetCustomAttributeMinLengthAttribute();if (minlengthAttribute ! null){yield return CreateValidateExpression(input,CreateValidateStringMinLengthExp(minlengthAttribute.Length));}}}public class CreatePropertyValidatorInput{public Type InputType { get; set; } null!;public Expression InputExpression { get; set; } null!;public PropertyInfo PropertyInfo { get; set; } null!;public ParameterExpression ResultExpression { get; set; } null!;public LabelTarget ReturnLabel { get; set; } null!;}public class ValidatorFactory : IValidatorFactory{private readonly IEnumerableIPropertyValidatorFactory _propertyValidatorFactories;public ValidatorFactory(IEnumerableIPropertyValidatorFactory propertyValidatorFactories){_propertyValidatorFactories propertyValidatorFactories;}private Funcobject, ValidateResult CreateValidator(Type type){var finalExpression CreateCore();return finalExpression.Compile();ExpressionFuncobject, ValidateResult CreateCore(){// exp for inputvar inputExp Expression.Parameter(typeof(object), input);// exp for outputvar resultExp Expression.Variable(typeof(ValidateResult), result);// exp for return statementvar returnLabel Expression.Label(typeof(ValidateResult));var innerExps new ListExpression {CreateDefaultResult()};var validateExpressions type.GetProperties().SelectMany(p _propertyValidatorFactories.SelectMany(f f.CreateExpression(new CreatePropertyValidatorInput{InputExpression inputExp,PropertyInfo p,ResultExpression resultExp,ReturnLabel returnLabel,InputType type,}))).ToArray();innerExps.AddRange(validateExpressions);innerExps.Add(Expression.Label(returnLabel, resultExp));// build whole blockvar body Expression.Block(new[] {resultExp},innerExps);// build lambda from bodyvar final Expression.LambdaFuncobject, 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;}}}private static readonly ConcurrentDictionaryType, Funcobject, ValidateResult ValidateFunc new ConcurrentDictionaryType, Funcobject, ValidateResult();public Funcobject, ValidateResult GetValidator(Type type){var re ValidateFunc.GetOrAdd(type, CreateValidator);return re;}}}
}IValidatorFactory 模型验证器工厂表示创建特定类型的验证器委托代码要点IPropertyValidatorFactory 具体属性的验证表达式创建工厂可以根据规则的增加追加新的实现。使用 Autofac 进行模块管理。可以通过《在 C# 中使用依赖注入》来了解如何添加一些细节。https://www.newbe.pro/Use-Dependency-Injection/Use-Dependency-Injection-In-CSharp/随堂小练别走您还有作业。以下有一个按照难度分级的需求开发者可以尝试完成这些任务进一步理解和使用本样例中的代码。增加一个验证 string max length 的规则难度D思路和 min length 类似别忘记注册就行。增加一个验证 int 必须大于等于 0 的规则难度D思路只是多了一个新的属性类型别忘记注册就行。增加一个 IEnumerableT 对象必须包含至少一个元素的规则难度C思路可以用 Linq 中的 Any 方法来验证增加一个 IEnumerableT 必须已经 ToList 或者 ToArray类比 mvc 中的规则难度C思路其实只要验证是否已经是 ICollection 就可以了。支持空对象也输出验证结果难度C思路如果 input 为空。则也要能够输出第一条不满足条件的规则。例如 Name Required。增加一个验证 int? 必须有值的规则难度B思路int? 其实是语法糖实际类型是 Nullableint。增加一个验证枚举必须符合给定的范围难度B思路枚举是可以被赋值以任意数值范围的例如定义了 Enum TestEnum {None 0;} 但是强行赋值 233 给这样的属性并不会报错。该验证需要验证属性值只能是定义的值。也可以增加自己的难度例如支持验证标记为 Flags 的枚举的混合值范围。添加一个验证 int A 属性必须和 int B 属性大难度A思路需要有两个属性参与。啥都别管先写一个静态函数来比较两个数值的大小。然后在考虑如何表达式化如何柯理化。可以参考前面思路。额外限定条件不能修改现在接口定义。添加一个验证 string A 属性必须和 string B 属性相等忽略大小写难度A思路和前一个类似。但是string 的比较比 int 特殊并且需要忽略大小写。支持返回全部的验证结果难度S思路调整验证结果返回值从返回第一个不满足的规则修改为返回所有不满足的规则类比 mvc model state 的效果。需要修改组合结果的表达式可以有两种办法一种是内部创建 List 然后将结果放入更为简单的一种是使用 yield return 的方法进行返回。需要而外注意的是由于所有规则都运行一些判断就需要进行防御性判断。例如在 string 长度判断时需要先判断其是否为空。至于 string 为空是否属于满足最小长度要求开发者可以自由决定不是重点。支持对象的递归验证难度SS思路即如果对象包含一个属性又是一个对象则子对象也需要被验证。有两种思路一是修改 ValidatorFactory 使其支持从 ValidateFunc 中获取验证器作为表达式的一部分。该思路需要解决的主要问题是ValidateFunc 集合中可能提前不存在子模型的验证器。可以使用 Lazy 来解决这个问题。二是创建一个 IPropertyValidatorFactory 实现使其能够从 ValidatorFactory 中获取 ValidateFunc 来验证子模型。该思路主要要解决的问题是直接实现可能会产生循环依赖。可以保存和生成 ValidateFunc 划分在两个接口中解除这种循环依赖。该方案较为简单。另外晋级难度为 SSS验证 IEnumerable 中所有的元素。开发者可以尝试。支持链式 API难度SSS思路形如 EntityFramework 中同时支持 Attribute 和链式 API 一样添加链式设置验证的特性。这需要增加新的接口以便进行链式注册并且原来使用 Attribute 直接生成表达式的方法也应该调整为 Attribute - 注册数据 - 生成表达式。实现一个属性修改器难度SSS思路实现一条规则手机号码加密当对象的某个属性是满足长度为 11 的字符串并且开头是 1。则除了前三位和后四位之外的字符全部替换为 *。建议从头开始实现属性修改器不要在上面的代码上做变更。因为验证和替换通常来说是两个不同的业务一个是为了输入一个是为了输出。这里有一些额外的要求在替换完成后将此次被替换的所有值的前后情况输出在日志中。注意测试的性能要与直接调用方法相当否则肯定是代码实现存在问题。本文总结在.net 中表达式树可以用于两种主要的场景。一种是用于解析结果典型的就是 EntityFramework而另外一种就是用于构建委托。本文通过构建委托的方式实现了一个模型验证器的需求。生产实际中还可以用于很多动态调用的地方。掌握表达式树就掌握了一种可以取代反射进行动态调用的方法这种方法不仅扩展性更好而且性能也不错。本篇内容中的示例代码均可以在以下链接仓库中找到https://github.com/newbe36524/Newbe.Demohttps://gitee.com/yks/Newbe.Demo最后但是最重要如果读者对该内容感兴趣欢迎转发、评论、收藏文章以及项目。最近作者正在构建以反应式、Actor模式和事件溯源为理论基础的一套服务端开发框架。希望为开发者提供能够便于开发出 “分布式”、“可水平扩展”、“可测试性高” 的应用系统 ——Newbe.Claptrap本篇文章是该框架的一篇技术选文属于技术构成的一部分。联系方式Github IssueGitee Issue公开邮箱 newbe-claptrapgooglegroups.com 发送到该邮箱的内容将被公开GitterQQ 群 610394020您还可以查阅本系列的其他选文理论入门篇Newbe.Claptrap - 一套以 “事件溯源” 和 “Actor 模式” 作为基本理论的服务端开发框架术语介绍篇Actor 模式事件溯源Event SourcingClaptrapMinion事件 Event状态 State状态快照 State SnapshotClaptrap 设计图 Claptrap DesignClaptrap 工厂 Claptrap FactoryClaptrap IdentityClaptrap BoxClaptrap 生命周期Claptrap Lifetime Scope序列化Serialization实现入门篇Newbe.Claptrap 框架入门第一步 —— 创建项目实现简易购物车Newbe.Claptrap 框架入门第二步 —— 简单业务清空购物车Newbe.Claptrap 框架入门第三步 —— 定义 Claptrap管理商品库存Newbe.Claptrap 框架入门第四步 —— 利用 Minion商品下单样例实践篇构建一个简易的火车票售票系统Newbe.Claptrap 框架用例第一步 —— 业务分析在线体验火车票售票系统其他番外篇谈反应式编程在服务端中的应用数据库操作优化从 20 秒到 0.5 秒谈反应式编程在服务端中的应用数据库操作优化提速 Upsert十万同时在线用户需要多少内存——Newbe.Claptrap 框架水平扩展实验docker-mcr 助您全速下载 dotnet 镜像十多位全球技术专家为你献上近十个小时的.Net 微服务介绍年轻的樵夫哟你掉的是这个免费 8 核 4G 公网服务器还是这个随时可用的 Docker 实验平台如何使用 dotTrace 来诊断 netcore 应用的性能问题只要十步你就可以应用表达式树来优化动态调用GitHub 项目地址https://github.com/newbe36524/Newbe.ClaptrapGitee 项目地址https://gitee.com/yks/Newbe.Claptrap您当前查看的是先行发布于 www.newbe.pro 上的博客文章实际开发文档随版本而迭代。若要查看最新的开发文档需要移步 claptrap.newbe.pro。