目录
介绍
背景
一般而言,什么是表达式?条件表达式与它们有什么不同?
ExpressionVistor如何工作
整体情况
解决方案
基础
必要的表达式修改
修改布尔MemberAccess表达式
修改否定比较运算符
修改DateTime值
转换FilterDescriptors
示例使用
省略的功能
- 下载源代码-11.2 KB
构建一种机制来将用代码表示的业务条件转换为可由解决方案的其他层(数据库或Web服务)使用的格式,这是比较常见的,尤其是在解决方案的基础结构层。以下两种常见情况中的任何一种都是这种情况的示例:
- 假设我们想将过滤条件从C#客户端内部传递到HTTP服务。这些条件可以在查询字符串集合中发送,但是通过字符串连接手动构造查询字符串,不仅看起来不干净,而且很可能难以调试和维护。
- 有时,我们可能需要在不使用ORM工具的情况下将过滤条件转换为SQL WHERE子句。同样,通过手动字符串操作为数据库查询构造SQL WHERE子句似乎容易出错并且难以维护。
作为一种优雅的工具,“lambda表达式”提供了描述过滤条件的简洁方便的方法,但是使用这些表达式并不是很容易。幸运的是,System.Linq.Expressions命名空间中的ExpressionVisitor类是检查、修改和翻译lambda表达式的出色工具。
在本文中,我们主要使用ExpressionVisitor类为上述第一种情况提出解决方案。
背景在深入探讨细节之前,让我们对表达式的一般概念进行非常简单的介绍,然后将条件表达式作为一种更特殊的类型,最后对ExpressionVisitor类进行非常简短的描述。这将非常简短,但绝对必要,因此,仅在事先了解这些主题的情况下,才跳过此部分。
一般而言,什么是表达式?条件表达式与它们有什么不同?表达式通常表示委托或方法。表达式本身不是委托或方法。它表示委托或方法,即,表达式定义了委托的结构。在.NET平台中,我们使用Expression类来定义表达式。但是,在定义其委托的主体之前,必须定义将要表示的委托的签名。该签名通过名为TDelegate的泛型类型参数提供给Expression类。因此,表达式类的形式为Expression 。
考虑到这一点,很明显,条件表达式表示一个委托,该委托将任意类型的对象T作为输入并返回布尔值。结果,条件表达式的委托将是类型Func,因此Expression是条件表达式的类型。
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作为输入的FilterBuilder类。此类是解决方案的主要部分。在第一步,FilterBuilder检查输入表达式并输出FilterDescriptor(IEnumerable)的集合。在下一步中,转换器将这个FilterDescriptors集合转换为所需的形式,例如,要在HTTP请求中使用的查询字符串键值对,或要用作SQL WHERE子句的字符串。对于每种类型的转换,都需要一个单独的转换器。
这里可能会出现一个问题:为什么不将输入表达式直接转换为查询字符串?是否有必要承担产生FilterDescriptors的负担?可以跳过此额外步骤吗?答案是,如果您所需要的只是生成查询字符串,而不是更多,而且如果您不是在寻找通用解决方案,那么您可以自由地这样做。但是,通过这种方式,您最终将获得非常特定的ExpressionVisitor,仅适用于一种类型的输出。但是,本文试图做的恰恰相反:提出一个更通用的解决方案。
解决方案 基础该解决方案的核心是继承自ExpressionVisitor的FilterBuilder类。此类的构造函数采用Expresion类型的表达式。此类具有一个名为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 purposes
public override string ToString()
{
return
$"{CompositionOperator} {FieldName ?? "FieldName"} {Operator} {Value ?? "Value"}";
}
}
FilterOperator类的属性类型是一个枚举。此属性指定过滤器的运算符。
public enum FilterOperator
{
NOT_SET,
// Logical
And,
Or,
Not,
// Comparison
Equal,
NotEqual,
LessThan,
LessThanOrEqual,
GreaterThan,
GreaterThanOrEqual,
// String
StartsWith,
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是一个由三部分组成的二进制表达式:
- p.Price (成员表达)
- 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文件中提供了该解决方案的完整源代码。