表达式树是 .net 中一系列非常好用的类型。在一些场景中使用表达式树可以获得更好的性能和更佳的扩展性。本篇我们将通过构建一个 “模型验证器” 来理解和应用表达式树在构建动态调用方面的优势。
Newbe.Claptrap 是一个用于轻松应对并发问题的分布式开发框架。如果您是首次阅读本系列文章。建议可以先从本文末尾的入门文章开始了解。
前不久,我们发布了《如何使用 dotTrace 来诊断 netcore 应用的性能问题》,经过网友投票之后,网友们表示对其中表达式树的内容很感兴趣,因此本篇我们将展开讲讲。
动态调用是在 .net 开发是时常遇到的一种需求,即在只知道方法名或者属性名等情况下动态的调用方法或者属性。最广为人知的一种实现方式就是使用 “反射” 来实现这样的需求。当然也有一些高性能场景会使用 Emit 来完成这个需求。
本文将介绍 “使用表达式树” 来实现这种场景,因为这个方法相较于 “反射” 将拥有更好的性能和扩展性,相较于 Emit 又更容易掌握。
我们将使用一个具体的场景来逐步使用表达式来实现动态调用。
在该场景中,我们将构建一个模型验证器,这非常类似于 aspnet mvc 中 ModelState 的需求场景。
这不是一篇简单的入门文章,初次涉足该内容的读者,建议在空闲时,在手边有 IDE 可以顺便操作时边看边做。同时,也不必在意样例中出现的细节方法,只需要了解其中的大意,能够依样画瓢即可,掌握大意之后再深入了解也不迟。
为了缩短篇幅,文章中的样例代码会将没有修改的部分隐去,想要获取完整的测试代码,请打开文章末尾的代码仓库进行拉取。
首先需要确认的事情有两个:
有问题,做实验。我们采用两个单元测试来验证以上两个问题。
调用一个对象的方法:
using System;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using FluentAssertions;
using NUnit.Framework;
namespace Newbe.ExpressionsTests
{
public class X01CallMethodTest
{
private const int Count = 1_000_000;
private const int Diff = 100;
[SetUp]
public void Init()
{
_methodInfo = typeof(Claptrap).GetMethod(nameof(Claptrap.LevelUp));
Debug.Assert(_methodInfo != null, nameof(_methodInfo) + " != null");
var instance = Expression.Parameter(typeof(Claptrap), "c");
var levelP = Expression.Parameter(typeof(int), "l");
var callExpression = Expression.Call(instance, _methodInfo, levelP);
var lambdaExpression = Expression.Lambda<Action<Claptrap, int>>(callExpression, instance, levelP);
// lambdaExpression should be as (Claptrap c,int l) => { c.LevelUp(l); }
_func = lambdaExpression.Compile();
}
[Test]
public void RunReflection()
{
var claptrap = new Claptrap();
for (int i = 0; i < Count; i++)
{
_methodInfo.Invoke(claptrap, new[] {(object) Diff});
}
claptrap.Level.Should().Be(Count * Diff);
}
[Test]
public void RunExpression()
{
var claptrap = new Claptrap();
for (int i = 0; i < Count; i++)
{
_func.Invoke(claptrap, Diff);
}
claptrap.Level.Should().Be(Count * Diff);
}
[Test]
public void Directly()
{
var claptrap = new Claptrap();
for (int i = 0; i < Count; i++)
{
claptrap.LevelUp(Diff);
}
claptrap.Level.Should().Be(Count * Diff);
}
private MethodInfo _methodInfo;
private Action<Claptrap, int> _func;
public class Claptrap
{
public int Level { get; set; }
public void LevelUp(int diff)
{
Level += diff;
}
}
}
}
以上测试中,我们对第三种调用方式一百万次调用,并记录每个测试所花费的时间。可以得到类似以下的结果:
Method | Time |
---|---|
RunReflection | 217ms |
RunExpression | 20ms |
Directly | 19ms |
可以得出以下结论:
所以如果仅仅从性能上考虑,应该使用表达式树,也可以是用表达式树。
不过这是在一百万调用下体现出现的时间,对于单次调用而言其实就是纳秒级别的区别,其实无足轻重。
但其实表达式树不仅仅在性能上相较于反射更优,其更强大的扩展性其实采用最为重要的特性。
此处还有一个对属性进行操作的测试,此处将测试代码和结果罗列如下:
using System;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using FluentAssertions;
using NUnit.Framework;
namespace Newbe.ExpressionsTests
{
public class X02PropertyTest
{
private const int Count = 1_000_000;
private const int Diff = 100;
[SetUp]
public void Init()
{
_propertyInfo = typeof(Claptrap).GetProperty(nameof(Claptrap.Level));
Debug.Assert(_propertyInfo != null, nameof(_propertyInfo) + " != null");
var instance = Expression.Parameter(typeof(Claptrap), "c");
var levelProperty = Expression.Property(instance, _propertyInfo);
var levelP = Expression.Parameter(typeof(int), "l");
var addAssignExpression = Expression.AddAssign(levelProperty, levelP);
var lambdaExpression = Expression.Lambda<Action<Claptrap, int>>(addAssignExpression, instance, levelP);
// lambdaExpression should be as (Claptrap c,int l) => { c.Level += l; }
_func = lambdaExpression.Compile();
}
[Test]
public void RunReflection()
{
var claptrap = new Claptrap();
for (int i = 0; i < Count; i++)
{
var value = (int) _propertyInfo.GetValue(claptrap);
_propertyInfo.SetValue(claptrap, value + Diff);
}
claptrap.Level.Should().Be(Count * Diff);
}
[Test]
public void RunExpression()
{
var claptrap = new Claptrap();
for (int i = 0; i < Count; i++)
{
_func.Invoke(claptrap, Diff);
}
claptrap.Level.Should().Be(Count * Diff);
}
[Test]
public void Directly()
{
var claptrap = new Claptrap();
for (int i = 0; i < Count; i++)
{
claptrap.Level += Diff;
}
claptrap.Level.Should().Be(Count * Diff);
}
private PropertyInfo _propertyInfo;
private Action<Claptrap, int> _func;
public class Claptrap
{
public int Level { get; set; }
}
}
}
耗时情况:
Method | Time |
---|---|
RunReflection | 373ms |
RunExpression | 19ms |
Directly | 18ms |
由于反射多了一份装拆箱的消耗,所以比起前一个测试样例显得更慢了,使用委托是没有这种消耗的。
先通过一个测试来了解我们要创建的 “模型验证器” 究竟是一个什么样的需求。
using System.ComponentModel.DataAnnotations;
using FluentAssertions;
using NUnit.Framework;
namespace Newbe.ExpressionsTests
{
/// <summary>
/// Validate data by static method
/// </summary>
public class X03PropertyValidationTest00
{
private const int Count = 10_000;
[Test]
public void Run()
{
for (int i = 0; i < Count; i++)
{
// test 1
{
var input = new CreateClaptrapInput();
var (isOk, errorMessage) = Validate(input);
isOk.Should().BeFalse();
errorMessage.Should().Be("missing Name");
}
// test 2
{
var input = new CreateClaptrapInput
{
Name = "1"
};
var (isOk, errorMessage) = Validate(input);
isOk.Should().BeFalse();
errorMessage.Should().Be("Length of Name should be great than 3");
}
// test 3
{
var input = new CreateClaptrapInput
{
Name = "yueluo is the only one dalao"
};
var (isOk, errorMessage) = Validate(input);
isOk.Should().BeTrue();
errorMessage.Should().BeNullOrEmpty();
}
}
}
public static ValidateResult Validate(CreateClaptrapInput input)
{
return ValidateCore(input, 3);
}
public static ValidateResult ValidateCore(CreateClaptrapInput input, int minLength)
{
if (string.IsNullOrEmpty(input.Name))
{
return ValidateResult.Error("missing Name");
}
if (input.Name.Length < minLength)
{
return ValidateResult.Error($"Length of Name should be great than {minLength}");
}
return ValidateResult.Ok();
}
public class CreateClaptrapInput
{
[Required] [MinLength(3)] public string Name { get; set; }
}
public struct ValidateResult
{
public bool IsOk { get; set; }
public string ErrorMessage { get; set; }
public void Deconstruct(out bool isOk, out string errorMessage)
{
isOk = IsOk;
errorMessage = ErrorMessage;
}
public static ValidateResult Ok()
{
return new ValidateResult
{
IsOk = true
};
}
public static ValidateResult Error(string errorMessage)
{
return new ValidateResult
{
IsOk = false,
ErrorMessage = errorMessage
};
}
}
}
}
从上而下,以上代码的要点:
首先我们构建第一个表达式树,该表达式树将直接使用上一节中的静态方法 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();
}
}
}
从上而下,以上代码的要点:
虽然前一步,我们将直接调用转变了动态调用,但由于 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();
}
}
}
代码要点:
Func<CreateClaptrapInput, int, ValidateResult>
。var a
。我们来改造 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();
}
}
}
代码要点:
因为文章内容过多,无法在正常发布,想要继续阅读,请移步
https://www.newbe.pro/Newbe.Claptrap/Using-Expression-Tree-To-Build-Delegate/
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。