Skip to main content

只要十步,你就可以应用表达式树来优化动态调用

表达式树是 .net 中一系列非常好用的类型。在一些场景中使用表达式树可以获得更好的性能和更佳的扩展性。本篇我们将通过构建一个“模型验证器”来理解和应用表达式树在构建动态调用方面的优势。

开篇摘要

前不久,我们发布了《如何使用dotTrace来诊断netcore应用的性能问题》,经过网友投票之后,网友们表示对其中表达式树的内容很感兴趣,因此本篇我们将展开讲讲。

动态调用是在 .net 开发是时常遇到的一种需求,即在只知道方法名或者属性名等情况下动态的调用方法或者属性。最广为人知的一种实现方式就是使用“反射”来实现这样的需求。当然也有一些高性能场景会使用 Emit 来完成这个需求。

本文将介绍“使用表达式树”来实现这种场景,因为这个方法相较于“反射”将拥有更好的性能和扩展性,相较于 Emit 又更容易掌握。

我们将使用一个具体的场景来逐步使用表达式来实现动态调用。

在该场景中,我们将构建一个模型验证器,这非常类似于 aspnet mvc 中 ModelState 的需求场景。

不是一篇简单的入门文章,初次涉足该内容的读者,建议在空闲时,在手边有 IDE 可以顺便操作时边看边做。同时,也不必在意样例中出现的细节方法,只需要了解其中的大意,能够依样画瓢即可,掌握大意之后再深入了解也不迟。

为了缩短篇幅,文章中的样例代码会将没有修改的部分隐去,想要获取完整的测试代码,请打开文章末尾的代码仓库进行拉取。

居然还有视频

本系列文章配套一个十几个小时的长篇视频。记得一键三连哟!

原视频地址:https://www.bilibili.com/video/BV15y4y1r7pK

为什么要用表达式树,为什么可以用表达式树?

首先需要确认的事情有两个:

  1. 使用表达式树取代反射是否有更好的性能?
  2. 使用表达式树进行动态调用是否有很大的性能损失?

有问题,做实验。我们采用两个单元测试来验证以上两个问题。

调用一个对象的方法:

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

以上测试中,我们对第三种调用方式一百万次调用,并记录每个测试所花费的时间。可以得到类似以下的结果:

MethodTime
RunReflection217ms
RunExpression20ms
Directly19ms

可以得出以下结论:

  1. 使用表达式树创建委托进行动态调用可以得到和直接调用近乎相同的性能。
  2. 使用表达式树创建委托进行动态调用所消耗的时间约为十分之一。

所以如果仅仅从性能上考虑,应该使用表达式树,也可以是用表达式树。

不过这是在一百万调用下体现出现的时间,对于单次调用而言其实就是纳秒级别的区别,其实无足轻重。

但其实表达式树不仅仅在性能上相较于反射更优,其更强大的扩展性其实采用最为重要的特性。

此处还有一个对属性进行操作的测试,此处将测试代码和结果罗列如下:

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

耗时情况:

MethodTime
RunReflection373ms
RunExpression19ms
Directly18ms

由于反射多了一份装拆箱的消耗,所以比起前一个测试样例显得更慢了,使用委托是没有这种消耗的。

第〇步,需求演示

先通过一个测试来了解我们要创建的“模型验证器”究竟是一个什么样的需求。

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

从上而下,以上代码的要点:

  1. 主测试方法中,包含有三个基本的测试用例,并且每个都将执行一万次。后续所有的步骤都将会使用这样的测试用例。
  2. Validate 方法是被测试的包装方法,后续将会调用该方法的实现以验证效果。
  3. ValidateCore 是“模型验证器”的一个演示实现。从代码中可以看出该方法对 CreateClaptrapInput 对象进行的验证,并且得到验证结果。但是该方法的缺点也非常明显,这是一种典型的“写死”。后续我们将通过一系列改造。使得我们的“模型验证器”更加的通用,并且,很重要的,保持和这个“写死”的方法一样的高效!
  4. 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();
}
}
}

从上而下,以上代码的要点:

  1. 增加了一个单元测试的初始化方法,在单元测试启动时创建的一个表达式树将其编译为委托保存在静态字段 _func 中。
  2. 省略了主测试方法 Run 中的代码,以便读者阅读时减少篇幅。实际代码没有变化,后续将不再重复说明。可以在代码演示仓库中查看。
  3. 修改了 Validate 方法的实现,不再直接调用 ValidateCore ,而调用 _func 来进行验证。
  4. 运行该测试,开发者可以发现,其消耗的时间和上一步直接调用的耗时,几乎一样,没有额外消耗。
  5. 这里提供了一种最为简单的使用表达式进行动态调用的思路,如果可以写出一个静态方法(例如:ValidateCore)来表示动态调用的过程。那么我们只要使用类似于 Init 中的构建过程来构建表达式和委托即可。
  6. 开发者可以试着为 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();
}

}
}

代码要点:

  1. ValidateCore 方法被拆分为了 ValidateNameRequired 和 ValidateNameMinLength 两个方法,分别验证 Name 的 Required 和 MinLength。
  2. Init 方法中使用了 local function 从而实现了方法“先使用后定义”的效果。读者可以自上而下阅读,从顶层开始了解整个方法的逻辑。
  3. Init 整体的逻辑就是通过表达式将 ValidateNameRequired 和 ValidateNameMinLength 重新组合成一个形如 ValidateCore 的委托 Func<CreateClaptrapInput, int, ValidateResult>
  4. Expression.Parameter 用于标明委托表达式的参数部分。
  5. Expression.Variable 用于标明一个变量,就是一个普通的变量。类似于代码中的var a
  6. Expression.Label 用于标明一个特定的位置。在该样例中,主要用于标定 return 语句的位置。熟悉 goto 语法的开发者知道, goto 的时候需要使用 label 来标记想要 goto 的地方。而实际上,return 就是一种特殊的 goto。所以想要在多个语句块中 return 也同样需要标记后才能 return。
  7. Expression.Block 可以将多个表达式顺序组合在一起。可以理解为按顺序写代码。这里我们将 CreateDefaultResult、CreateValidateNameRequiredExpression、CreateValidateNameMinLengthExpression 和 Label 表达式组合在一起。效果就类似于把这些代码按顺序拼接在一起。
  8. CreateValidateNameRequiredExpression 和 CreateValidateNameMinLengthExpression 的结构非常类似,因为想要生成的结果表达式非常类似。
  9. 不必太在意 CreateValidateNameRequiredExpression 和 CreateValidateNameMinLengthExpression 当中的细节。可以在本样例全部阅读完之后再尝试了解更多的 Expression.XXX 方法。
  10. 经过这样的修改之后,我们就实现了扩展。假设现在需要对 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();
}
}
}

代码要点:

  1. 正如前文所述,我们修改了 ValidateNameRequired ,并重命名为 ValidateStringRequired。 ValidateNameMinLength -> ValidateStringMinLength。
  2. 修改了 CreateValidateNameRequiredExpression 和 CreateValidateNameMinLengthExpression,因为静态方法的参数发生了变化。
  3. 通过这样的改造,我们便可以将两个静态方法用于更多的属性验证。读者可以尝试增加一个 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; }
}
}
}

代码要点:

  1. 在 CreateClaptrapInput 中增加了一个属性 NickName ,测试用例也将验证该属性。
  2. 通过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; }
}
}
}

代码要点:

  1. 在构建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();

}
}

代码要点:

  1. 将静态方法换成了表达式。因此 CreateXXXExpression 相应的位置也进行了修改,代码就更短了。

第七步,柯里化

柯理化,也称为函数柯理化,是函数式编程当中的一种方法。简单的可以表述为:通过固定一个多参数函数的一个或几个参数,从而得到一个参数更少的函数。术语化一些,也可以表述为将高阶函数(函数的阶其实就是说参数的个数)转换为低阶函数的方法。

例如,现在有一个 add(int,int) 的函数,它实现了将两个数相加的功能。假如我们固定集中第一个参数为 5 ,则我们会得到一个 add(5,int) 的函数,它实现的是将一个数加 5 的功能。

这有什么意义?

函数降阶可以使得函数变得一致,得到了一致的函数之后可以做一些代码上的统一以便优化。例如上面使用到的两个表达式:

  1. Expression<Func<string, string, ValidateResult>> ValidateStringRequiredExp
  2. 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 = 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 Expression<Func<string, string, ValidateResult>> CreateValidateStringRequiredExp()
{
return (name, value) =>
string.IsNullOrEmpty(value)
? ValidateResult.Error($"missing {name}")
: ValidateResult.Ok();
}

private static Expression<Func<string, string, ValidateResult>> CreateValidateStringMinLengthExp(int minLength)
{
return (name, value) =>
value.Length < minLength
? ValidateResult.Error($"Length of {name} should be great than {minLength}")
: ValidateResult.Ok();
}
}
}

代码要点:

  1. CreateValidateStringMinLengthExp 静态方法,传入一个参数创建得到一个和 CreateValidateStringRequiredExp 返回值一样的表达式。对比上一节中的 ValidateStringMinLengthExp ,实现了固定 int 参数而得到一个新表达式的操作。这就是一种柯理化的体现。
  2. 为了统一都采用静态方法,我们将上一节中的 ValidateStringRequiredExp 也改为 CreateValidateStringRequiredExp 静态方法,这仅仅只是为了看起来一致(但实际上增加了一点点开销,因为没必要重复创建一个不变的表达式)。
  3. 相应的调整一下 List<Expression> 组装过程的代码。

第八步,合并重复代码

本节,我们将合并 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 InvalidXmlDocComment

namespace Newbe.ExpressionsTests
{
/// <summary>
/// Refactor to CreateValidateExpression
/// </summary>
public class X03PropertyValidationTest08
{
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)
=> CreateValidateExpression(propertyInfo,
CreateValidateStringRequiredExp());

Expression CreateValidateStringMinLengthExpression(PropertyInfo propertyInfo,
int minlengthAttributeLength)
=> CreateValidateExpression(propertyInfo,
CreateValidateStringMinLengthExp(minlengthAttributeLength));

Expression CreateValidateExpression(PropertyInfo propertyInfo,
Expression<Func<string, 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
}
}
}

代码要点:

  1. CreateValidateExpression 就是被抽离出来的公共方法。
  2. 如果没有前一步柯理化,CreateValidateExpression 的第二个参数 validateFuncExpression 将很难确定。
  3. 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 InvalidXmlDocComment

namespace Newbe.ExpressionsTests
{
/// <summary>
/// Multiple Type
/// </summary>
public class X03PropertyValidationTest09
{
private const int Count = 10_000;

private static readonly Dictionary<Type, Func<object, ValidateResult>> ValidateFunc =
new Dictionary<Type, Func<object, ValidateResult>>();

[SetUp]
public void Init()
{
try
{
var finalExpression = CreateCore(typeof(CreateClaptrapInput));
ValidateFunc[typeof(CreateClaptrapInput)] = finalExpression.Compile();

Expression<Func<object, ValidateResult>> CreateCore(Type type)
{
// exp for input
var inputExp = Expression.Parameter(typeof(object), "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 = type
.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<object, 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,
Expression<Func<string, 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);
}

}
}

代码要点:

  1. Func<CreateClaptrapInput, ValidateResult> 替换为了 Func<object, ValidateResult>,并且将写死的 typeof(CreateClaptrapInput) 都替换为了 type。
  2. 将对应类型的验证器创建好之后保存在 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 InvalidXmlDocComment

namespace Newbe.ExpressionsTests
{
/// <summary>
/// Final
/// </summary>
public class X03PropertyValidationTest10
{
private const int Count = 10_000;

private IValidatorFactory _factory = null!;

[SetUp]
public void Init()
{
try
{
var builder = new ContainerBuilder();
builder.RegisterModule<ValidatorModule>();
var container = builder.Build();
_factory = container.Resolve<IValidatorFactory>();
}
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.RegisterType<ValidatorFactory>()
.As<IValidatorFactory>()
.SingleInstance();

builder.RegisterType<StringRequiredPropertyValidatorFactory>()
.As<IPropertyValidatorFactory>()
.SingleInstance();
builder.RegisterType<StringLengthPropertyValidatorFactory>()
.As<IPropertyValidatorFactory>()
.SingleInstance();
}
}

public interface IValidatorFactory
{
Func<object, ValidateResult> GetValidator(Type type);
}

public interface IPropertyValidatorFactory
{
IEnumerable<Expression> CreateExpression(CreatePropertyValidatorInput input);
}

public abstract class PropertyValidatorFactoryBase<TValue> : IPropertyValidatorFactory
{
public virtual IEnumerable<Expression> CreateExpression(CreatePropertyValidatorInput input)
{
if (input.PropertyInfo.PropertyType != typeof(TValue))
{
return Enumerable.Empty<Expression>();
}

var expressionCore = CreateExpressionCore(input);
return expressionCore;
}

protected abstract IEnumerable<Expression> CreateExpressionCore(CreatePropertyValidatorInput input);

protected Expression CreateValidateExpression(
CreatePropertyValidatorInput input,
Expression<Func<string, 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 : PropertyValidatorFactoryBase<string>
{
private static Expression<Func<string, string, ValidateResult>> CreateValidateStringRequiredExp()
{
return (name, value) =>
string.IsNullOrEmpty(value)
? ValidateResult.Error($"missing {name}")
: ValidateResult.Ok();
}

protected override IEnumerable<Expression> CreateExpressionCore(CreatePropertyValidatorInput input)
{
var propertyInfo = input.PropertyInfo;
if (propertyInfo.GetCustomAttribute<RequiredAttribute>() != null)
{
yield return CreateValidateExpression(input, CreateValidateStringRequiredExp());
}
}
}

public class StringLengthPropertyValidatorFactory : PropertyValidatorFactoryBase<string>
{
private static Expression<Func<string, 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 IEnumerable<Expression> CreateExpressionCore(CreatePropertyValidatorInput input)
{
var propertyInfo = input.PropertyInfo;
var minlengthAttribute = propertyInfo.GetCustomAttribute<MinLengthAttribute>();
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 IEnumerable<IPropertyValidatorFactory> _propertyValidatorFactories;

public ValidatorFactory(
IEnumerable<IPropertyValidatorFactory> propertyValidatorFactories)
{
_propertyValidatorFactories = propertyValidatorFactories;
}

private Func<object, ValidateResult> CreateValidator(Type type)
{
var finalExpression = CreateCore();
return finalExpression.Compile();

Expression<Func<object, ValidateResult>> CreateCore()
{
// exp for input
var inputExp = Expression.Parameter(typeof(object), "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 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 block
var body = Expression.Block(
new[] {resultExp},
innerExps);

// build lambda from body
var final = Expression.Lambda<Func<object, 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 ConcurrentDictionary<Type, Func<object, ValidateResult>> ValidateFunc =
new ConcurrentDictionary<Type, Func<object, ValidateResult>>();

public Func<object, ValidateResult> GetValidator(Type type)
{
var re = ValidateFunc.GetOrAdd(type, CreateValidator);
return re;
}
}
}
}

代码要点:

  1. IValidatorFactory 模型验证器工厂,表示创建特定类型的验证器委托
  2. IPropertyValidatorFactory 具体属性的验证表达式创建工厂,可以根据规则的增加,追加新的实现。
  3. 使用 Autofac 进行模块管理。

可以通过《在 C# 中使用依赖注入》来了解如何添加一些细节。

随堂小练

别走!您还有作业。

以下有一个按照难度分级的需求,开发者可以尝试完成这些任务,进一步理解和使用本样例中的代码。

增加一个验证 string max length 的规则

难度:D

思路:

和 min length 类似,别忘记注册就行。

增加一个验证 int 必须大于等于 0 的规则

难度:D

思路:

只是多了一个新的属性类型,别忘记注册就行。

增加一个IEnumerable<T>对象必须包含至少一个元素的规则

难度:C

思路:

可以用 Linq 中的 Any 方法来验证

增加一个IEnumerable<T>必须已经 ToList 或者 ToArray,类比 mvc 中的规则

难度:C

思路:

其实只要验证是否已经是 ICollection 就可以了。

支持空对象也输出验证结果

难度:C

思路:

如果 input 为空。则也要能够输出第一条不满足条件的规则。例如 Name Required。

增加一个验证 int? 必须有值的规则

难度:B

思路:

int? 其实是语法糖,实际类型是 Nullable<int>

增加一个验证枚举必须符合给定的范围

难度: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。则除了前三位和后四位之外的字符全部替换为*

建议从头开始实现属性修改器,不要在上面的代码上做变更。因为验证和替换通常来说是两个不同的业务,一个是为了输入,一个是为了输出。

这里有一些额外的要求:

  1. 在替换完成后,将此次被替换的所有值的前后情况输出在日志中。
  2. 注意,测试的性能要与直接调用方法相当,否则肯定是代码实现存在问题。

本文总结

在.net 中,表达式树可以用于两种主要的场景。一种是用于解析结果,典型的就是 EntityFramework,而另外一种就是用于构建委托。

本文通过构建委托的方式实现了一个模型验证器的需求。生产实际中还可以用于很多动态调用的地方。

掌握表达式树,就掌握了一种可以取代反射进行动态调用的方法,这种方法不仅扩展性更好,而且性能也不错。

本篇内容中的示例代码,均可以在以下链接仓库中找到:


欢迎关注的我微信公众号,第一时间获取我的最新文章。