目录

介绍

背景

一般而言,什么是表达式?条件表达式与它们有什么不同?

ExpressionVistor如何工作

整体情况

解决方案

基础

必要的表达式修改

修改布尔MemberAccess表达式

修改否定比较运算符

修改DateTime值

转换FilterDescriptors

示例使用

省略的功能


  • 下载源代码-11.2 KB

介绍

构建一种机制来将用代码表示的业务条件转换为可由解决方案的其他层(数据库或Web服务)使用的格式,这是比较常见的,尤其是在解决方案的基础结构层。以下两种常见情况中的任何一种都是这种情况的示例:

  1. 假设我们想将过滤条件从C#客户端内部传递到HTTP服务。这些条件可以在查询字符串集合中发送,但是通过字符串连接手动构造查询字符串,不仅看起来不干净,而且很可能难以调试和维护。
  2. 有时,我们可能需要在不使用ORM工具的情况下将过滤条件转换为SQL WHERE子句。同样,通过手动字符串操作为数据库查询构造SQL WHERE子句似乎容易出错并且难以维护。

作为一种优雅的工具,“lambda表达式”提供了描述过滤条件的简洁方便的方法,但是使用这些表达式并不是很容易。幸运的是,System.Linq.Expressions命名空间中的ExpressionVisitor类是检查、修改和翻译lambda表达式的出色工具。

在本文中,我们主要使用ExpressionVisitor类为上述第一种情况提出解决方案。

背景

在深入探讨细节之前,让我们对表达式一般概念进行非常简单的介绍,然后将条件表达式作为一种更特殊的类型,最后对ExpressionVisitor类进行非常简短的描述。这将非常简短,但绝对必要,因此,仅在事先了解这些主题的情况下,才跳过此部分。

一般而言,什么是表达式?条件表达式与它们有什么不同?

表达式通常表示委托或方法。表达式本身不是委托或方法。它表示委托或方法,即,表达式定义了委托的结构。在.NET平台中,我们使用Expression类来定义表达式。但是,在定义其委托的主体之前,必须定义将要表示的委托的签名。该签名通过名为TDelegate的泛型类型参数提供给Expression类。因此,表达式类的形式为Expression <TDelegate>。

考虑到这一点,很明显,条件表达式表示一个委托,该委托将任意类型的对象T作为输入并返回布尔值。结果,条件表达式的委托将是类型Func<T, bool>,因此Expression<Func<T, bool>>是条件表达式的类型。

ExpressionVistor如何工作

我们通常使用lambda表达式来定义一个表达式。Lambda表达式由多个不同的表达式组合在一起。考虑以下示例lambda:

p => p.Price < 1000 && p.Name.StartsWith("a-string") && !p.OutOfStock

下图标记了它的不同部分:

如您所见,此表达式是其他一些表达式和运算符的组合。

现在让我们看一下ExpressionVisitor如何对待上面的表达式。此类实现访客模式。它的主要方法(或入口点)称为Visit调度程序,该调度程序调用其他几种专用方法。将表达式传递给Visit方法时,将遍历表达式树,并根据每个节点的类型,调用专门的方法来访问(检查和修改)该节点及其子节点(如果有)。在每个方法内部,如果修改了表达式,则将返回其修改后的副本;否则为原始表达。请记住,表达式是不可变的,任何修改都会导致生成并返回一个新实例。

在Microsoft的.NET Framework 4.8 在线文档中,记录了35种特殊的访问方法。下面列出了我们的解决方案中使用的一些有趣的方法:

  • VisitConstant访问ConstantExpression。
  • VisitMember访问MemberExpression的子级。
  • VisitBinary访问BinaryExpression的子级。
  • VisitUnary拜访UnaryExpression的子级。
  • VisitMethodCall访问MethodCallExpression的子级。
  • VisitNew访问NewExpression的子级。

这35种visit方法的所有变体都是virtual的,从ExpressionVisitor继承的任何类都应覆盖必需的类并实现自己的逻辑。这就是自定义访问者的构建方式。

对于那些可能希望对我们的解决方案的工作方式有很好的了解的读者,至少需要对以下主题有最少的了解。

  • 表达式树(1)和(2)

    • 我们要翻译的lambda表达式背后的一般概念
  • 树遍历(顺序,前序和后序)
    • 用于迭代树的算法
  • 访问者设计模式
    • 一种用于解析表达式树的设计模式
  • ExpressionVisitor类
    • Microsoft .NET平台提供的类,它使用访问者设计模式来公开检查、​​修改和翻译表达式树的方法。我们将使用这些方法来检查树中感兴趣的每个节点,并从中提取所需的数据。
  • 逆波兰式(RPN)
    • 在逆波兰式中,运算符遵循其操作数;例如,将3和4相加,便会写成“ 3 4 +”而不是“ 3 + 4”。

整体情况

如下图所示,我们有一使用类型表达式Expression<Func<T, bool>>作为输入的FilterBuilder类。此类是解决方案的主要部分。在第一步,FilterBuilder检查输入表达式并输出FilterDescriptor(IEnumerable<FilterDescriptor>)的集合。在下一步中,转换器将这个FilterDescriptors集合转换为所需的形式,例如,要在HTTP请求中使用的查询字符串键值对,或要用作SQL WHERE子句的字符串。对于每种类型的转换,都需要一个单独的转换器。

这里可能会出现一个问题:为什么不将输入表达式直接转换为查询字符串?是否有必要承担产生FilterDescriptors的负担?可以跳过此额外步骤吗?答案是,如果您所需要的只是生成查询字符串,而不是更多,而且如果您不是在寻找通用解决方案,那么您可以自由地这样做。但是,通过这种方式,您最终将获得非常特定的ExpressionVisitor,仅适用于一种类型的输出。但是,本文试图做的恰恰相反:提出一个更通用的解决方案。

解决方案

基础

该解决方案的核心是继承自ExpressionVisitor的FilterBuilder类。此类的构造函数采用Expresion<Func<T, bool>>类型的表达式。此类具有一个名为Build的public方法,该方法返回FilterDescriptor对象的集合。FiterDescriptor定义如下:

public class FilterDescriptor
{public FilterDescriptor(){CompositionOperator = FilterOperator.And;}private FilterOperator _compositionOperator;public FilterOperator CompositionOperator{get => _compositionOperator;set{if (value != FilterOperator.And && value != FilterOperator.Or)throw new ArgumentOutOfRangeException();_compositionOperator = value;}}public string FieldName { get; set; }public object Value { get; set; }public FilterOperator Operator { get; set; }     // For demo purposespublic override string ToString(){return$"{CompositionOperator} {FieldName ?? "FieldName"} {Operator} {Value ?? "Value"}";}
}

FilterOperator类的属性类型是一个枚举。此属性指定过滤器的运算符。

public enum FilterOperator
{NOT_SET,// LogicalAnd,Or,Not,// ComparisonEqual,NotEqual,LessThan,LessThanOrEqual,GreaterThan,GreaterThanOrEqual,// StringStartsWith,Contains,EndsWith,NotStartsWith,NotContains,NotEndsWith
}

表达式节点不会直接转换为FilterDescriptor对象。取而代之的是,每个访问表达式节点的重写方法,都创建一个名为token的对象并将其添加到私有列表中。该列表中的令牌是根据逆波兰式(RPN)排列的。什么是令牌?令牌封装了构建FilterDescriptor所需的节点数据。令牌由继承自抽象Token类的类定义。

public abstract class Token {}public class BinaryOperatorToken : Token
{public FilterOperator Operator { get; set; }public BinaryOperatorToken(FilterOperator op){Operator = op;}public override string ToString(){return "Binary operator token:\t" + Operator.ToString();}
}public class ConstantToken : Token
{public object Value { get; set; }public ConstantToken(object value){Value = value;}public override string ToString(){return "Constant token:\t\t" + Value.ToString();}
}public class MemberToken : Token
{public Type Type { get; set; }public string MemberName { get; set; }public MemberToken(string memberName, Type type){MemberName = memberName;Type = type;}public override string ToString(){return "Member token:\t\t" + MemberName;}
}public class MethodCallToken : Token
{public string MethodName { get; set; }public MethodCallToken(string methodName){MethodName = methodName;}public override string ToString(){return "Method call token:\t" + MethodName;}
}public class ParameterToken : Token
{public string ParameterName { get; set; }public Type Type { get; set; }public ParameterToken(string name, Type type){ParameterName = name;Type = type;}public override string ToString(){return "Parameter token:\t\t" + ParameterName;}
}public class UnaryOperatorToken : Token
{public FilterOperator Operator { get; set; }public UnaryOperatorToken(FilterOperator op){Operator = op;}public override string ToString(){return "Unary operator token:\t\t" + Operator.ToString();}
}

遍历表达式的所有节点并创建它们的等效标记后,FilterDescriptor就可以创建。这将通过调用Build名为的方法来完成。

如前面“ExpressionVisitor的工作原理”部分所述,表达式的每个部分都包含多个子表达式。例如,p.Price < 1000是一个由三部分组成的二进制表达式:

  1. p.Price (成员表达)
  2. < (“小于”二进制运算符)
  3. 1000 (恒定表达)

当访问时,此三部分二进制表达式将产生三个不同的标记:

  1. VisitMember方法用于p.Price的MemberToken
  2. TokenVisitBinary方法用于<的BinaryOperator
  3. VisitConstant方法用于1000的ConstantToken

调用Builder方法时,它首先创建一个Stack<FilterDescriptor>对象。然后遍历令牌列表,并基于循环中当前令牌的类型,将描述符推入和弹出堆栈。这样,将不同的令牌(如上例中的三个令牌)组合在一起以构建单个FilterDescriptor。

public IEnumerable<FilterDescriptor> Build()
{var filters = new Stack<FilterDescriptor>();for (var i = 0; i < _tokens.Count; i++){var token = _tokens[i];switch (token){case ParameterToken p:var f = getFilter();f.FieldName = p.ParameterName;filters.Push(f);break;case BinaryOperatorToken b:var f1 = getFilter();switch (b.Operator){case FilterOperator.And:case FilterOperator.Or:var ff = filters.Pop();ff.CompositionOperator = b.Operator;filters.Push(ff);break;case FilterOperator.Equal:case FilterOperator.NotEqual:case FilterOperator.LessThan:case FilterOperator.LessThanOrEqual:case FilterOperator.GreaterThan:case FilterOperator.GreaterThanOrEqual:f1.Operator = b.Operator;filters.Push(f1);break;}break;case ConstantToken c:var f2 = getFilter();f2.Value = c.Value;filters.Push(f2);break;case MemberToken m:var f3 = getFilter();f3.FieldName = m.MemberName;filters.Push(f3);break;case UnaryOperatorToken u:var f4 = getFilter();f4.Operator = u.Operator;f4.Value = true;filters.Push(f4);break;case MethodCallToken mc:var f5 = getFilter();f5.Operator = _methodCallMap[mc.MethodName];filters.Push(f5);break;}}var output = new Stack<FilterDescriptor>();while (filters.Any()){output.Push(filters.Pop());}return output;FilterDescriptor getFilter(){if (filters.Any()){var f = filters.First();var incomplete = f.Operator == default ||f.CompositionOperator == default ||f.FieldName == default ||f.Value == default;if (incomplete)return filters.Pop();return new FilterDescriptor();}return new FilterDescriptor();}
}

当Build方法返回时,所有描述符都准备好转换为所需的任何形式。

必要的表达式修改

这里介绍了对原始表达式的三个修改,它们在简化方面有很大帮助。这三个更改是我自己的解决方案,可以使代码更简单、更实用。从理论上讲,它们不是必需的,可以进一步开发此示例以另一种方式解决问题并保持原始表达完整无缺。

修改布尔MemberAccess表达式

每个条件都由三件事定义:一个参数,其值和一个运算符,该运算符将参数与该值相关联。现在考虑以下表达式:p.OutOfStock其中OutOfStock是对象p的布尔属性。乍一看,它缺少三个部分中的两个:运算符和布尔值;但实际上是这是该表达形式的缩写:p.OutOfStock == true。另一方面,本文中的算法要求所有这三个部分都能正常运行。根据我的经验,在没有明确说明运算符和布尔值的情况下,尝试按原样使用这种表达式会给解决方案增加不必要的复杂性。因此,我们分两次访问了该表达式。对于第一次传递,使用了一个名为BooleanVisitor的单独类,它也从ExpressionVisitor中继承。它仅覆盖VisitMember方法。此类是私有嵌套在FilterBuilder中的。

private class BooleanVisitor : ExpressionVisitor
{protected override Expression VisitMember(MemberExpression node){if (node.Type == typeof(bool)){return Expression.MakeBinary(ExpressionType.Equal, node, Expression.Constant(true));}return base.VisitMember(node);}
}

此重写的方法向其添加布尔成员访问表达式的两个缺失部分,并返回修改后的副本。第二遍需要在之后执行。这是在FilterBuilder的构造函数中完成的。

// ctor of the FilterBuilderpublic FilterBuilder(Expression expression)
{var fixer = new BooleanVisitor();var fixedExpression = fixer.Visit(expression);base.Visit(fixedExpression);
}

修改否定比较运算符

有时,条件中变量与值的关系包含比较运算符和否定运算符。一个例子是!(p.Price > 30000)。在这种情况下,用单个等效运算符替换此组合将使事情变得更简单。例如,可以使用<=(小于或等于)运算符代替!(not)和>(大于)运算符的组合。字符串比较运算符也是如此。否定运算符和字符串比较运算符的任何组合都将被定义为FilterOperator枚举的单个等效运算符替换。

修改DateTime

这里应该注意两个重要的事情。首先,在访问表达式树时需要特别注意DateTime值,因为DateTime值可以以多种形式出现在表达式中。本解决方案涉及的一些形式如下:

  1. 一个简单的MemberAccess表达式:DateTime.Now或DateTime.Date
  2. 嵌套的MemberAccess表达式:DateTime.Now.Date
  3. NewExpression:new DateTime(1989, 3, 25)
  4. NewExpression后接一个MemberAccess表达式:new DateTime(1989, 3, 25).Date

当DateTime值显示为MemberAccess表达式时,应在VisitMember方法中进行处理。当它显示为NewExpression时,应在VisitNew方法中进行处理。

其次,可以通过多种形式通过网络传输DateTime值。例如,可以将其转换为string任意格式并格式化;或者可以将其转换为长整数(Ticks)并作为数字发送。选择特定的数据类型和格式是业务需求或技术约束的问题。无论如何,这里选择DateTime结构的Ticks属性是因为简单,而且因为它可以独立于平台。

由于这两个原因,我们的表达式访问者用其Ticks替换了DateTime结构的实例。这意味着在运行表达式访问者代码时,我们必须获取DateTime值的Ticks属性的值。因此,包含该DateTime值的表达式应编译为方法,并按以下代码运行:

protected override Expression VisitMember(MemberExpression node)
{if (node.Type == typeof(DateTime)){if (node.Expression == null) // Simple MemberAccess like DateTime.Now{var lambda = Expression.Lambda<Func<DateTime>>(node);var dateTime = lambda.Compile()();base.Visit(Expression.Constant(dateTime.Ticks));return node;}else{switch (node.Expression.NodeType){case ExpressionType.New:var lambda = Expression.Lambda<Func<DateTime>>(node.Expression);var dateTime = lambda.Compile()();base.Visit(Expression.Constant(dateTime.Ticks));return node;case ExpressionType.MemberAccess: // Nested MemberAccess            if (node.Member.Name != ((MemberExpression)node.Expression).Member.Name) {var lambda2 = Expression.Lambda<Func<DateTime>>(node);var dateTime2 = lambda2.Compile()();base.Visit(Expression.Constant(dateTime2.Ticks));return node;}break;}}}_tokens.Add(new MemberToken(node.Expression + "." + node.Member.Name, node.Type));return node;
}protected override Expression VisitNew(NewExpression node)
{if (node.Type == typeof(DateTime)){var lambda = Expression.Lambda<Func<DateTime>>(node);var dateTime = lambda.Compile()();base.Visit(Expression.Constant(dateTime.Ticks));return node;}return base.VisitNew(node);
}

转换FilterDescriptors

如前所述,当Build方法返回时,FilterDescriptor准备好将s 的集合提供给任何类或方法,以将其转换为任何所需的形式。在查询字符串的情况下,根据程序员的喜好,此方法可以只是扩展方法或单独的类。请注意,每个服务器程序都将期待一组预定义的键值对。例如,假设有一台服务器将在单独的类似数组的键值对中查找过滤器的不同参数。以下扩展方法将完成此工作。

public static class FilterBuilderExtensions
{public static string GetQueryString(this IList<FilterDescriptor> filters){var sb = new StringBuilder();for (var i = 0; i < filters.Count; i++){sb.Append($"filterField[{i}]={filters[i].FieldName}&" +$"filterOp[{i}]={filters[i].Operator}&" +$"filterVal[{i}]={filters[i].Value}&" +$"filterComp[{i}]={filters[i].CompositionOperator}");if (i < filters.Count - 1)sb.Append("&");}return sb.ToString();}
}

示例使用

这个简单的控制台程序演示了如何使用FilterBuilder。

将覆盖FilterDescriptor和所有令牌类的ToString方法,以便在控制台中检查它们的属性。

class Program
{static void Main(string[] args){Expression<Func<Product, bool>> exp = p =>p.Id == 1009 &&!p.OutOfStock &&!(p.Price > 30000) &&!p.Name.Contains("BMW") &&p.ProductionDate > new DateTime(1999, 6, 20).Date;var visitor = new FilterBuilder(exp);var filters = visitor.Build().ToList();Console.WriteLine("Tokens");Console.WriteLine("------\n");foreach (var t in visitor.Tokens){Console.WriteLine(t);}Console.WriteLine("\nFilter Descriptors");Console.WriteLine("------------------\n");foreach (var f in filters){Console.WriteLine(f);}Console.WriteLine($"\nQuery string");Console.WriteLine("------------\n");Console.WriteLine(filters.GetQueryString());Console.ReadLine();}
}public class Product
{public int Id { get; set; }public string Name { get; set; }public decimal Price { get; set; }public DateTime ProductionDate { get; set; }public bool OutOfStock { get; set; } = false;
}

省略的功能

当然,有许多潜在的改进可以使此解决方案更强大,但为简洁起见,本文特意将其省略。一个必要的功能是通过包装FilterDescriptor 集合的新类在表达式中支持括号。这样的功能需要更多的时间和精力,以后可能会涉及到。但是,我希望读者能够掌握这里介绍的核心概念,并在此基础上开发更好的解决方案。

本文所附的ZIP文件中提供了该解决方案的完整源代码。

将C#Lambda表达式转换为通用过滤器描述符和HTTP查询字符串相关推荐

  1. 如何在Java 8中将Lambda表达式转换为方法引用?

    如果您使用Java 8进行编码,那么您会知道使用方法引用代替lambda表达式会使您的代码更具可读性,因此建议尽可能使用方法引用替换lambda表达式,但是,最大的问题是,您如何查找是否可以用方法引用 ...

  2. 无法将 lambda 表达式 转换为类型“System.Delegate”,因为它不是委托类型

    今天写winform的时候遇到一个问题,提示: 无法将 lambda 表达式 转换为类型"System.Delegate",因为它不是委托类型, 主要是为了在子线程中更新UI线程, ...

  3. CVPR2021|SpinNet:学习用于3D点云配准的通用表面描述符

    SpinNet: Learning a General Surface Descriptor for 3D Point Cloud Registration 论文地址:在公众号「3D视觉工坊」,后台回 ...

  4. Java 核心技术卷1 --第六章 接口、lambda表达式和内部类

    吧Github代码链接: https://github.com/deyou123/corejava.git 第六章 接口.lambda表达式和内部类 6.1 接口 6.1.1 接口概念 接口不是类,而 ...

  5. Lambda 表达式(=):网络摘抄,自学用,侵删。

    Lambda 表达式 Lambda 表达式"是一个匿名函数,它可以包含表达式和语句,并且可用于创建委托或表达式目录树类型. 所有 Lambda 表达式都使用 Lambda 运算符 => ...

  6. C#学习基本概念之匿名方法及Lambda表达式

    在 2.0 之前的 C# 版本中,声明委托的唯一方法是使用命名方法.  C# 2.0 引入了匿名方法,而在 C# 3.0 及更高版本中,Lambda 表达式取代了匿名方法,作为编写内联代码的首选方式. ...

  7. CoreJava 笔记总结-第六章 接口、lambda表达式与内部类

    文章目录 第六章 接口.lambda表达式与内部类 ==接口== 接口的概念 接口的属性 接口与抽象类 静态和私有方法 默认方法 解决默认方法冲突 接口与回调 `Comparator`接口 对象克隆 ...

  8. Lambda表达式表达式树

    在C#3.0中,继匿名方法之后出现了Lambda 表达式,使表达更为简洁.快捷.Lambda 表达式使用Lambda 运算符 "=>"来定义,语法如下: (参数列表) =&g ...

  9. (zz)Lambda 表达式(C# 编程指南)

    https://msdn.microsoft.com/zh-cn/library/bb397687.aspx Lambda 表达式是一种可用于创建委托或表达式目录树类型的匿名函数.通过使用 lambd ...

最新文章

  1. 需求简报_代码简报:有史以来最怪诞的丑毛衣
  2. 机器学习中的异常检测手段
  3. 在deepin系统中制作桌面快捷方式
  4. git提交代码到github时出现everything up-to-date,但是代码没有上传成功
  5. excel通过js导入到页面_基于Excel和Java自动化工作流程:发票生成器示例
  6. java验证码画布类型,【Java工具类】使用Kaptcha生成验证码写回页面中
  7. 程序员的成功是否有规律可循?
  8. Socket的3次握手链接与4次断开握手
  9. 《开源框架那点事儿33》极限挑战:用一条循环语句正确输出99表!【前两名奖图书一本】...
  10. 错误笔记:在OleDb执行下Access ,程序不报错,但是Update也更新不成功的
  11. c语言回调函数构架程序,c语言函数回调函数回调
  12. 《机器学习实战》学习总结(四)逻辑回归原理
  13. c语言酒店管理系统,基于C#的酒店管理系统(V3.1)最新版
  14. linux天气软件,Ubuntu 18.04 6款查询天气的小工具推荐(适用于其它Linux)
  15. 个人学习计划(计算机专业),大学生个人学习计划范文
  16. logiscope系列-使用说明书
  17. 微服初识/优缺点2020-09-03
  18. ChatGPT账号注册,中国手机号为什么不行?
  19. 什么是RFID技术?
  20. Android Notification消息提示

热门文章

  1. 若某计算机字长为16位,题目来源于王道论坛 某计算机字长为16位,主存地址空间...
  2. mysql like in 数组_Web前端学习教程之常用的MySQL优化技巧
  3. java的观察模式链式,design-pattern-java
  4. 手机照片局部放大镜_手机摄影,竟然有3种对焦方式,想拍出专业水准,你必须了解...
  5. 实惠星扫地机器人不能开机_扫地机器人不能承受的重量,14kg法斗坐在上面,它旋转后死机...
  6. 稀缺时尚男模促销海报|PSD分层,简单搞定设计稿
  7. 疯狂电商购物节日,设计师受虐加班? | 美妆促销页面设计技巧
  8. 品质LOGO模板素材|想知道平面设计师如何设计徽标的秘密吗?
  9. mysql动态top_MySQL 之 MyTop实时监控MySQL
  10. F-Stack:ff_run函数详解