c# lambda 表达式
It is relatively common to build a mechanism for converting business conditions expressed in code to a format usable by other tiers of a solution, be it a database or a Web service, especially in the infrastructure layers of a solution. Either of the two following common scenarios is an example of such case:
建立一种将代码表示的业务条件转换为解决方案的其他层(数据库或Web服务)可用的格式的机制是相对普遍的,尤其是在解决方案的基础结构层中。 以下两种常见方案中的任何一种都是这种情况的示例:
There can be times that we might want to translate filter conditions into SQL WHERE
clauses without using an ORM tool. Again, constructing SQL WHERE
clauses for database queries through manual string manipulation seems error prone and hard to maintain.
有时候,我们可能希望在不使用ORM工具的情况下将过滤条件转换为SQL WHERE
子句。 同样,通过手动字符串操作为数据库查询构造SQL WHERE
子句似乎容易出错并且难以维护。
As an elegant tool, "lambda expressions" give a concise and convenient means of describing filter conditions but working with these expressions is not very easy. Luckily, ExpressionVisitor class in System.Linq.Expressions namespace is an excellent tool for inspecting, modifying and translating lambda expressions.
作为一种出色的工具,“ lambda表达式”提供了描述过滤条件的简洁方便的方法,但是使用这些表达式并不是很容易。 幸运的是, System.Linq.Expressions命名空间中的ExpressionVisitor类是检查,修改和翻译lambda表达式的出色工具。
In this article, we mainly use ExpressionVisitor class to propose a solution to the first scenario above.
在本文中,我们主要使用ExpressionVisitor类为上述第一种情况提出解决方案。
Before diving into the details, let us have a very brief introduction to the general concept of expressions, then the condition expressions as a more special type, and finally a very short description of ExpressionVisitor class. It will be very short but absolutely necessary, so please skip this section only if you know these subjects beforehand.
在深入探讨细节之前,让我们对表达式的一般概念进行非常简单的介绍,然后将条件表达式作为一种更特殊的类型,最后对ExpressionVisitor类进行非常简短的描述。 这将非常简短,但绝对必要,因此,仅在事先了解这些主题的情况下,才跳过此部分。
Expressions in general represent a delegate or method. An expression itself is not a delegate or method. It represents a delegate or method, i.e., an expression defines a delegate's structure. In .NET platform, we use Expression
class to define an expression. However, before defining its delegate's body, it is necessary to define the signature of the delegate it is going to represent. This signature is given to the Expression
class via a generic type parameter named TDelegate
. Therefore, the form of the expression class is Expression
表达式通常表示委托或方法。 表达式本身不是委托或方法。 它表示委托或方法,即,表达式定义了委托的结构。 在.NET平台中,我们使用Expression
类定义表达式。 但是,在定义其委托人的主体之前,有必要定义将要代表的委托人的签名。 该签名通过名为TDelegate
的通用类型参数提供给Expression
类。 因此,表达式类的形式为Expression
Having this in mind, it is obvious that a condition expression represents a delegate that takes an object of an arbitrary type T
as the input and returns a Boolean value. As a result, the delegate of a condition expression will be of type Func
, hence Expression
the type of the condition expression.
考虑到这一点,很明显,条件表达式表示一个委托,该委托将任意类型T
的对象作为输入并返回布尔值。 结果,条件表达式的委托将是Func
,因此Expression
是条件表达式的类型。
We usually use a lambda expression to define an expression. A lambda expression consists of multiple different expressions combined together. Consider this example lambda:
我们通常使用lambda表达式来定义一个表达式。 Lambda表达式由多个不同的表达式组合在一起。 考虑以下示例lambda:
p => p.Price < 1000 && p.Name.StartsWith("a-string") && !p.OutOfStock
The below figure marks different parts of it:
下图标记了它的不同部分:
As you can see, this expression is a combination of some other expressions and operators.
如您所见,此表达式是其他一些表达式和运算符的组合。
Now let us see how ExpressionVisitor
treats an expression like the one above. This class implements visitor pattern. Its main method, or entry point, called Visit
is a dispatcher that calls several other specialized methods. When an expression is passed to the Visit
method, the expression tree is traversed and depending on the type of each node, a specialized method is called to visit (inspect and modify) that node, and its children, if any. Inside each method, if the expression is modified, the modified copy of it will be returned; otherwise, the original expression. Please keep in mind that expressions are immutable and any modification would result in a new instance being built and returned.
现在让我们看看ExpressionVisitor
如何像上面的表达式一样对待表达式。 此类实现访客模式 。 它的主要方法(称为“ Visit
点”)是一个调度程序,它调用其他几种专门的方法。 将表达式传递给Visit
方法时,将遍历表达式树,并根据每个节点的类型,调用专门的方法来访问(检查和修改)该节点及其子节点(如果有)。 在每个方法内部,如果表达式被修改,则将返回其修改后的副本; 否则为原始表达。 请记住,表达式是不可变的,任何修改都会导致生成并返回一个新实例。
In Microsoft’s online documentation for .NET framework 4.8, 35 special visit methods are documented. A few interesting ones that are used in our solution are listed here:
在Microsoft的.NET Framework 4.8 在线文档中, 记录了35种特殊的访问方法。 下面列出了我们的解决方案中使用的一些有趣的方法:
VisitConstant
visits the ConstantExpression
.
VisitConstant
访问ConstantExpression
。
VisitMember
visits the children of the MemberExpression
.
VisitMember
访问VisitMember
的子MemberExpression
。
VisitBinary
visits the children of the BinaryExpression
.
VisitBinary
访问BinaryExpression
。
VisitUnary
visits the children of the UnaryExpression
.
VisitUnary
探访UnaryExpression
。
VisitMethodCall
visits the children of the MethodCallExpression
.
VisitMethodCall
访问MethodCallExpression
。
VisitNew
visits the children of a NewExpression
.
VisitNew
探视的子女NewExpression
。
All those 35 variants of visit
method are virtual and any class inheriting from ExpressionVisitor should override the necessary ones and implement its own logic. This is how a custom visitor is built.
visit
方法的所有这35个变体都是虚拟的,从ExpressionVisitor继承的任何类都应覆盖必需的类并实现其自己的逻辑。 这就是定制访问者的构建方式。
For those readers who might be willing to obtain a good understanding of how our solution works, having at least a minimum familiarity with the following subjects is necessary.
对于那些可能希望很好地理解我们的解决方案工作原理的读者,有必要至少了解以下主题。
Expression Trees (1) & (2)
表达式树(1)和(2)
Tree Traversals (Inorder, Preorder and Postorder)
树遍历(顺序,预排序和后排序)
Visitor Design Pattern
访客设计模式
ExpressionVisitor Class
ExpressionVisitor类
Reverse Polish Notation (RPN)
反向波兰语符号(RPN)
As the below figure shows, we have a FilterBuilder
class that takes an expression of type Expression
as the input. This class is the main part of the solution. At the first step, the FilterBuilder
examines the input expression and outputs a collection of FilterDescriptor
s (IEnumerable
). In the next step, a converter, converts this collection of FilterDescriptors
to a desired form, e.g., query string key-value pairs to be used in an HTTP request, or a string to be used as a SQL WHERE clause. For every type of conversion, a separate converter is needed.
如下图所示,我们有一个FilterBuilder
类,该类以Expression
类型的Expression
作为输入。 此类是解决方案的主要部分。 第一步, FilterBuilder
检查输入表达式并输出FilterDescriptor
的集合( IEnumerable
)。 在下一步中,转换器将FilterDescriptors
此集合转换为所需的形式,例如,要在HTTP请求中使用的查询字符串键/值对,或要用作SQL WHERE子句的字符串。 对于每种类型的转换,都需要一个单独的转换器。
One question may arise here: why do we not convert the input expression directly to a query string? Is it necessary to take on the burden of generating FilterDescriptors
? Can this extra step be skipped? The answer is if all you need is to generate query strings and no more than that, and if you are not looking for a general solution, you are free to do so. However, this way, you will end up having a very specific ExpressionVisitor suitable for only one type of output. For that goal, a good article is written here. However, what this article tries to do is exactly the opposite: proposing a more general solution.
这里可能会出现一个问题:为什么不将输入表达式直接转换为查询字符串? 是否有必要承担生成FilterDescriptors
的负担? 可以跳过此额外步骤吗? 答案是,如果您只需要生成查询字符串,而不仅仅是生成查询字符串,并且如果您不寻找通用解决方案,那么您可以自由地这样做。 但是,通过这种方式,您最终将获得非常特定的ExpressionVisitor ,仅适用于一种类型的输出。 为了这个目标, 这里写了一篇很好的文章。 但是,本文试图做的恰恰相反:提出一个更通用的解决方案。
At the heart of our solution is the FilterBuilder
class, which inherits from ExpressionVisitor. The constructor of this class takes an expression of type Expresion
. This class has a public
method named Build
that returns a collection of FilterDescriptor
objects. FiterDescriptor
is defined as follows:
解决方案的核心是FilterBuilder
类,该类继承自ExpressionVisitor 。 此类的构造函数采用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"}";
}
}
Type of the FilterOperator
property of this class is an enumeration. This property specifies the operator of the filter.
此类的FilterOperator
属性的类型是一个枚举。 此属性指定过滤器的运算符。
public enum FilterOperator
{
NOT_SET,
// Logical
And,
Or,
Not,
// Comparison
Equal,
NotEqual,
LessThan,
LessThanOrEqual,
GreaterThan,
GreaterThanOrEqual,
// String
StartsWith,
Contains,
EndsWith,
NotStartsWith,
NotContains,
NotEndsWith
}
Expressions nodes are not directly converted to FilterDescriptor
objects. Instead, each overridden method that visits an expression node, creates an object named token and adds it to a private list. Tokens in this list are arranged according to Reverse Polish Notation (RPN). What is a token? A token encapsulates node data required to build FilterDescriptor
s. Tokens are defined by classes that inherit from an abstract Token
class.
表达式节点不会直接转换为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();
}
}
After all nodes of the expression are traversed and their equivalent tokens are created, FilterDescriptor
s can be created. This will be done by calling a method named Build
.
遍历表达式的所有节点并创建了它们的等效标记后,就可以创建FilterDescriptor
。 这将通过调用名为Build
的方法来完成。
As stated before in "How ExpressionVisitor Works" section, every part of the expression comprises multiple subexpressions. For example, p.Price < 1000
is a binary expression that is made up of the three parts:
如前面“ ExpressionVisitor的工作原理”部分所述,表达式的每个部分都包含多个子表达式。 例如, p.Price < 1000
是由三个部分组成的二进制表达式:
p.Price
(member expression)
p.Price
(成员表达式)
<
("less than" binary operator)
<
(“小于”二进制运算符)
1000
(constant expression)
1000
(常量表达式)
When visited, this 3-part binary expression will produce three different tokens:
当访问时,此三部分二进制表达式将产生三个不同的标记:
A MemberToken
for p.Price
by VisitMember
method
甲MemberToken
用于p.Price
通过VisitMember
方法
A BinaryOperatorToken
for <
by VisitBinary
method
甲BinaryOperatorToken
用于<
由VisitBinary
方法
A ConstantToken
for 1000
by VisitConstant
method
甲ConstantToken
为1000
通过VisitConstant
方法
When the Builder
method is called, it first creates a Stack
object. Then iterates over the tokens list and based on the type of the current token in the loop, pushes and pops descriptors to and from the stack. This way different tokens, like the three ones in the above example, are combined together to build a single FilterDescriptor
.
调用Builder
方法时,它首先创建一个Stack
对象。 然后遍历令牌列表,并根据循环中当前令牌的类型,将描述符推入和弹出堆栈。 这样,将不同的令牌(如上例中的三个令牌)组合在一起以构建单个FilterDescriptor
。
public IEnumerable Build()
{
var filters = new Stack();
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();
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();
}
}
When the Build
method returns, all descriptors are ready to be converted to whatever form that is needed.
当Build
方法返回时,所有描述符都准备好转换为所需的任何形式。
Three modifications to the original expression are introduced here, which help a lot in simplifying things. These three changes are my own solution to make the code simpler and more practical. They are not theoretically necessary and one can further develop this example to solve the problem another way and keep the original expression intact.
这里介绍了对原始表达式的三个修改,它们在简化方面有很大帮助。 这三个更改是我自己的解决方案,使代码更简单,更实用。 从理论上讲,它们不是必需的,可以进一步开发此示例,以另一种方式解决问题并保持原始表达完整无缺。
Every condition is defined with three things: a parameter, its value and an operator, which relates the parameter to that value. Now consider this expression: p.OutOfStock
where OutOfStock
is a Boolean property of the object p
. It lacks two of the three parts at the first glance: an operator and a Boolean value; but matter of fact is that it is a short form of this expression: p.OutOfStock == true
. On the other hand, the algorithm in this article expects all three parts in order to function as expected. As I have experienced, without the operator and a Boolean value explicitly stated, trying to use this kind of expression as it is, tends to add unnecessary complexity to the solution. For this reason, we visit the expression in two passes. For the first pass, a separate class named BooleanVisitor
, which also inherits from ExpressionVisitor
, is used. It only overrides VisitMember
method. This class is privately nested in the FilterBuilder
.
每个条件都由三件事定义:一个参数,其值和一个运算符,该运算符将参数与该值相关联。 现在考虑以下表达式: p.OutOfStock
其中OutOfStock
是对象p
的布尔属性。 乍一看,它缺少三个部分中的两个:运算符和布尔值; 但事实上,这是该表达式的p.OutOfStock == true
: 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);
}
}
This overridden method adds two missing parts of a Boolean member access expression to it and returns the modified copy. The second pass needs to be performed afterwards. This is done in the constructor of the FilterBuilder
.
此重写的方法向其添加布尔成员访问表达式的两个缺失部分,并返回修改后的副本。 此后需要执行第二遍。 这是在FilterBuilder
的构造函数中完成的。
// ctor of the FilterBuilder
public FilterBuilder(Expression expression)
{
var fixer = new BooleanVisitor();
var fixedExpression = fixer.Visit(expression);
base.Visit(fixedExpression);
}
Sometimes relation of a variable to a value in a condition contains a comparison operator combined with a negation operator. An example is !(p.Price > 30000)
. In such cases, replacing this combination with a single equivalent operator makes things simpler. For example, instead of a !
(not) and >
(greater than) operators combined, a <=
(less than or equal) operator can be used. The same is valid for string comparison operators too. Any combination of the negation operator and string comparison operators will be replaced by a single equivalent operator that is defined in the FilterOperator
enumeration.
有时,条件中变量与值的关系包含比较运算符和否定运算符。 一个例子是!(p.Price > 30000)
。 在这种情况下,用单个等效运算符替换此组合将使事情变得更简单。 例如,代替!
(非)和>
(大于)运算符结合使用,可以使用<=
(小于或等于)运算符。 字符串比较运算符也是如此。 否定运算符和字符串比较运算符的任何组合都将替换为FilterOperator
枚举中定义的单个等效运算符。
Two important things should be noted here. First, DateTime
values need special attention while visiting an expression tree because a DateTime
value can appear in many forms in an expression. Some of those forms that are covered in this solution are:
这里应该注意两个重要的事情。 首先, DateTime
值需要特别注意,而来访的表达式树,因为DateTime
值可在表达式多种形式出现。 该解决方案涵盖的一些表格包括:
A simple MemberAccess
expression: DateTime.Now
or DateTime.Date
一个简单的MemberAccess
表达式: DateTime.Now
或DateTime.Date
Nested MemberAccess
expressions: DateTime.Now.Date
嵌套的MemberAccess
表达式: DateTime.Now.Date
A NewExpression
: new DateTime(1989, 3, 25)
A NewExpression
: new DateTime(1989, 3, 25)
NewExpression
new DateTime(1989, 3, 25)
A NewExpression
followed by a MemberAccess
expression: new DateTime(1989, 3, 25).Date
甲NewExpression
后跟一个MemberAccess
表达式: new DateTime(1989, 3, 25).Date
When a DateTime
value appears as a MemberAccess
expression, it should be handled in the VisitMember
method. When it appears as a NewExpression
, it should be handled in the VisitNew
method.
当DateTime
值显示为MemberAccess
表达式时,应在VisitMember
方法中对其进行处理。 当它显示为NewExpression
,应在VisitNew
方法中进行处理。
Second, one can transfer a DateTime
value over the wire in many forms. For instance, it can be converted to a string
and formatted arbitrarily; or it can be converted to a long integer (Ticks) and sent as a number. Choosing a specific data type and format is a matter of business requirement or technical constraint. Anyway, here, the Ticks property of the DateTime structure is chosen because of simplicity and also because it can be platform independent.
其次,可以通过多种形式通过网络传输DateTime
值。 例如,可以将其转换为string
并任意格式化; 或者可以将其转换为长整数( Ticks )并作为数字发送。 选择特定的数据类型和格式是业务需求或技术约束的问题。 无论如何,这里选择DateTime结构的Ticks属性是因为简单,也因为它可以独立于平台。
For these two reasons, our expression visitor replaces instances of DateTime structure, with their Ticks equivalent. This means that we have to obtain the value of the Ticks property of DateTime
values when the expression visitor code is run. Thus, the expression containing the DateTime
value should be compiled to a method and run as in the code below:
由于这两个原因,我们的表达式访问者用其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>(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>(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>(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>(node);
var dateTime = lambda.Compile()();
base.Visit(Expression.Constant(dateTime.Ticks));
return node;
}
return base.VisitNew(node);
}
As mentioned earlier, when the Build
method returns, a collection of FilterDescriptor
s is ready to be fed to any class or method to be converted to any desired form. In case of a query string, this method can simply be an extension method or a separate class depending on the programmer's preference. Note that every server program will be expecting a predefined set of key-value pairs. For example, suppose there is a server that will look for different parameters of filters in separate array-like key-value pairs. The following extension method will do the job.
如前所述,当Build
方法返回时,可以将FilterDescriptor
的集合准备馈送到任何类或方法,以将其转换为任何所需的形式。 对于查询字符串,根据程序员的喜好,此方法可以只是扩展方法或单独的类。 请注意,每个服务器程序都将期望一组预定义的键值对。 例如,假设有一个服务器将在类似数组的键/值对中查找过滤器的不同参数。 以下扩展方法将完成此工作。
public static class FilterBuilderExtensions
{
public static string GetQueryString(this IList 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();
}
}
This simple console program demonstrates how the FilterBuilder
can be used.
这个简单的控制台程序演示了如何使用FilterBuilder
。
ToString
method of the FilterDescriptor
and all token classes is overridden so their properties can be inspected in the console.
FilterDescriptor
和所有令牌类的ToString
方法将被覆盖,以便可以在控制台中检查其属性。
class Program
{
static void Main(string[] args)
{
Expression> 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;
}
The output:
输出:
There surely are many potential improvements to make this solution more robust, but they are intentionally omitted through this article for the sake of brevity. One necessary feature would be supporting parentheses in the expression through a new class that would wrap a collection of FilterDescriptor
s. Such features require more time and effort that might be covered at a later time. However, I hope the readers are able to grasp the core concepts presented here and develop a better solution on top of this work.
当然,有许多潜在的改进可以使此解决方案更强大,但为简洁起见,本文特意将其省略。 一个必要的功能是通过一个新类支持表达式中的括号,该新类将包装FilterDescriptor
的集合。 这样的功能需要更多的时间和精力,以后可能会涉及到。 但是,我希望读者能够掌握此处介绍的核心概念,并在此工作的基础上开发更好的解决方案。
The full source code of the solution is available in the ZIP file attached to this article.
本文所附的ZIP文件中提供了该解决方案的完整源代码。
翻译自: https://www.codeproject.com/Articles/5260863/Translating-Csharp-Lambda-Expressions-to-General-P
c# lambda 表达式