快捷搜索:  汽车  科技

表达式树2动态图解:你就可以应用表达式树来优化动态调用

表达式树2动态图解:你就可以应用表达式树来优化动态调用这两个表达式中第二个表达式和第一个表达式之间仅仅区别在第三参数上。如果我们使用柯理化固定第三个 int 参数,则可以使得两个表达式的签名完全一样。这其实和面向对象中的抽象非常类似。函数降阶可以使得函数变得一致,得到了一致的函数之后可以做一些代码上的统一以便优化。例如上面使用到的两个表达式:柯理化,也称为函数柯理化,是函数式编程当中的一种方法。简单的可以表述为:通过固定一个多参数函数的一个或几个参数,从而得到一个参数更少的函数。术语化一些,也可以表述为将高阶函数(函数的阶其实就是说参数的个数)转换为低阶函数的方法。例如,现在有一个 add (int int) 的函数,它实现了将两个数相加的功能。假如我们固定集中第一个参数为 5 ,则我们会得到一个 add (5 int) 的函数,它实现的是将一个数加 5 的功能。这有什么意义?

上接《只要十步,你就可以应用表达式树来优化动态调用(一)》

第六步,将静态方法换为表达式

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,而另外一种就是用于构建委托。

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

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

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

  • https://github.com/newbe36524/Newbe.Demo
  • https://gitee.com/yks/Newbe.Demo
最后但是最重要!

如果读者对该内容感兴趣,欢迎转发、评论、收藏文章以及项目。

最近作者正在构建以反应式、Actor模式和事件溯源为理论基础的一套服务端开发框架。希望为开发者提供能够便于开发出 “分布式”、“可水平扩展”、“可测试性高” 的应用系统 ——Newbe.Claptrap

本篇文章是该框架的一篇技术选文,属于技术构成的一部分。

  • Github Issue
  • Gitee Issue
  • 公开邮箱 newbe-claptrap@googlegroups.com (发送到该邮箱的内容将被公开)
  • Gitter
  • QQ 群 610394020

您还可以查阅本系列的其他选文:

理论入门篇

  1. Newbe.Claptrap - 一套以 “事件溯源” 和 “Actor 模式” 作为基本理论的服务端开发框架

术语介绍篇

  1. Actor 模式
  2. 事件溯源(Event Sourcing)
  3. Claptrap
  4. Minion
  5. 事件 (Event)
  6. 状态 (State)
  7. 状态快照 (State Snapshot)
  8. Claptrap 设计图 (Claptrap Design)
  9. Claptrap 工厂 (Claptrap Factory)
  10. Claptrap Identity
  11. Claptrap Box
  12. Claptrap 生命周期(Claptrap Lifetime Scope)
  13. 序列化(Serialization)

实现入门篇

  1. Newbe.Claptrap 框架入门,第一步 —— 创建项目,实现简易购物车
  2. Newbe.Claptrap 框架入门,第二步 —— 简单业务,清空购物车
  3. Newbe.Claptrap 框架入门,第三步 —— 定义 Claptrap,管理商品库存
  4. Newbe.Claptrap 框架入门,第四步 —— 利用 Minion,商品下单

样例实践篇

  1. 构建一个简易的火车票售票系统,Newbe.Claptrap 框架用例,第一步 —— 业务分析
  2. 在线体验火车票售票系统

其他番外篇

  1. 谈反应式编程在服务端中的应用,数据库操作优化,从 20 秒到 0.5 秒
  2. 谈反应式编程在服务端中的应用,数据库操作优化,提速 Upsert
  3. 十万同时在线用户,需要多少内存?——Newbe.Claptrap 框架水平扩展实验
  4. docker-mcr 助您全速下载 dotnet 镜像
  5. 十多位全球技术专家,为你献上近十个小时的.Net 微服务介绍
  6. 年轻的樵夫哟,你掉的是这个免费 8 核 4G 公网服务器,还是这个随时可用的 Docker 实验平台?
  7. 如何使用 dotTrace 来诊断 netcore 应用的性能问题
  8. 只要十步,你就可以应用表达式树来优化动态调用

GitHub 项目地址:https://github.com/newbe36524/Newbe.Claptrap

Gitee 项目地址:https://gitee.com/yks/Newbe.Claptrap

您当前查看的是先行发布于 www.newbe.pro 上的博客文章,实际开发文档随版本而迭代。若要查看最新的开发文档,需要移步 claptrap.newbe.pro。

表达式树2动态图解:你就可以应用表达式树来优化动态调用(1)

猜您喜欢: