英文网站注册,网站建设基础教案,网站推广渠道的类型,seo咨询师原文#xff1a;bit.ly/3uAXliC作者#xff1a;Jeremy Likness译者#xff1a;精致码农-王亮在上一篇深入LINQ | 动态构建LINQ表达式 博文中#xff0c;我们探索了表达式的强大#xff0c;并用它来动态地构建一个基于 JSON 的规则引擎。在这篇文章中#xff0c;我们反过来… 原文bit.ly/3uAXliC作者Jeremy Likness译者精致码农-王亮在上一篇深入LINQ | 动态构建LINQ表达式 博文中我们探索了表达式的强大并用它来动态地构建一个基于 JSON 的规则引擎。在这篇文章中我们反过来从表达式开始。考虑到表达式类型的多样性和表达式树的复杂性分解表达式树有什么好的方法呢我们能否对表达式进行变异使其有不同的表现呢首先如果你还没有读过第一篇文章请花几分钟时间去看看。本系列的的源代码放在 GitHubhttps://github.com/JeremyLikness/ExpressionExplorer1准备工作首先假设我有一个普通的 CLR 实体类你可能听说过它被称为 POCO该类名为 Thing。下面是它的定义public class Thing
{public Thing(){Id Guid.NewGuid().ToString();Created DateTimeOffset.Now;Name Guid.NewGuid().ToString().Split(-)[0];}public string Id { get; set; }public string Name { get; set; }public DateTimeOffset Created { get; private set; }public string GetId() Id;public override string ToString() $({Id}: {Name}{Created});
}为了模拟我添加了一个静态方法使其很容易生成 N 个数量的 Thingpublic static IListThing Things(int count)
{var things new ListThing();while (count-- 0){things.Add(new Thing());}return things;
}现在我可以生成一个数据源并查询它。这里有一个 LINQ 表达式它可以生成 500 个 Thing 并查询它们var query Thing.Things(500).AsQueryable().Where(t t.Name.Contains(a, StringComparison.InvariantCultureIgnoreCase) t.Created DateTimeOffset.Now.AddDays(-1)).Skip(2).Take(50).OrderBy(t t.Created);如果你对 query 调用 ToString()你会得到这样的结果System.Collections.Generic.List1[ExpressionExplorer.Thing].Where(t (t.Name.Contains(a, InvariantCultureIgnoreCase)AndAlso(t.Created DateTimeOffset.Now.AddDays(-1)))).Skip(2).Take(50).OrderBy(t t.Created)你可能没有注意到query 有一个名为 Expression 的属性。表达式的构建方式不会太神秘。从列表开始Enumerable.Where 方法被调用。第一个参数是一个可枚举列表(IEnumerableT)第二个参数是一个谓词(predicate)。在 predicate 内部string.Contains 被调用。Enumerable.Skip 方法接收一个可枚举列表和一个代表计数的整数。虽然构建查询的语法看起来很简单但你可以把它想象成一系列渐进的过滤器。Skip 调用是可枚举列表的一个扩展方法它从 Where 调用中获取结果以此类推。也为帮助理解我画了一个插图来说明这点然而如果你想解析表达式树你可能会大吃一惊。有许多不同的表达式类型每一种表达式都有不同的解析方式。例如BinaryExpression 有一个 Left 和一个 Right但是 MethodCallExpression 有一个 Arguments 表达式列表。光是遍历表达式树就有很多类型检查和转换了2另一个 VisitorLINQ 提供了一个名为 ExpressionVisitor 的特殊类。它包含了递归解析表达式树所需的所有逻辑。你只需将一个表达式传入 Visit 方法中它就会访问每个节点并返回表达式后面会有更多介绍。它包含特定于节点类型的方法这些方法可以被重载以拦截这个过程。下面是一个基本的实现它简单地重写了某些方法把信息写到控制台。public class BasicExpressionConsoleWriter : ExpressionVisitor
{protected override Expression VisitBinary(BinaryExpression node){Console.Write($ binary:{node.NodeType} );return base.VisitBinary(node);}protected override Expression VisitUnary(UnaryExpression node){if (node.Method ! null){Console.Write($ unary:{node.Method.Name} );}Console.Write($ unary:{node.Operand.NodeType} );return base.VisitUnary(node);}protected override Expression VisitConstant(ConstantExpression node){Console.Write($ constant:{node.Value} );return base.VisitConstant(node);}protected override Expression VisitMember(MemberExpression node){Console.Write($ member:{node.Member.Name} );return base.VisitMember(node);}protected override Expression VisitMethodCall(MethodCallExpression node){Console.Write($ call:{node.Method.Name} );return base.VisitMethodCall(node);}protected override Expression VisitParameter(ParameterExpression node){Console.Write($ p:{node.Name} );return base.VisitParameter(node);}
}要使用它只需创建一个实例并将一个表达式传给它。在这里我们将把我们的查询表达式传递给它new BasicExpressionConsoleWriter().Visit(query.Expression);运行后它输出不是很直观的结果如下call:OrderBy call:Take call:Skip call:Where
constant:System.Collections.Generic.List1[ExpressionExplorer.Thing] unary:Lambda
binary:AndAlso call:Contains member:Name p:t constant:a
constant:InvariantCultureIgnoreCase binary:GreaterThan member:Created p:t
call:AddDays member:Now constant:-1 p:t constant:2 constant:50
unary:Lambda member:Created p:t p:t注意访问顺序。这可能需一点时间理解这个逻辑但它是有意义的OrderBy 是最外层的调用后进先出它接受一个列表和一个字段...OrderBy 的第一个参数是列表它由 Take 提供...Take 需要一个列表这是由 Skip 提供的...Skip 需要一个列表由 Where 提供...Where 需要一个列表该列表由 Thing 列表提供...Where 的第二个参数是一个 predicate lambda 表达式......它是二元逻辑的 AndAlso...二元逻辑的左边是一个 Contains 调用...(跳过一堆的逻辑)Take 的第二个参数是 50...Skip 的第二个参数是 2...OrderBy 属性是 Created...你 Get 到这里的逻辑了吗了解树是如何解析的是使我们的 Visitor 更易读的关键。这里有一个更一目了然的输出实现public class ExpressionConsoleWriter: ExpressionVisitor
{int indent;private string Indent $\r\n{new string(\t, indent)};public void Parse(Expression expression){indent 0;Visit(expression);}protected override Expression VisitConstant(ConstantExpression node){if (node.Value is Expression value){Visit(value);}else{Console.Write(${node.Value});}return node;}protected override Expression VisitParameter(ParameterExpression node){Console.Write(node.Name);return node;}protected override Expression VisitMember(MemberExpression node){if (node.Expression ! null){Visit(node.Expression);}Console.Write($.{node.Member?.Name}.);return node;}protected override Expression VisitMethodCall(MethodCallExpression node){if (node.Object ! null){Visit(node.Object);}Console.Write(${Indent}{node.Method.Name}( );var first true;indent;foreach (var arg in node.Arguments){if (first){first false;}else{indent--;Console.Write(${Indent},);indent;}Visit(arg);}indent--;Console.Write() );return node;}protected override Expression VisitBinary(BinaryExpression node){Console.Write(${Indent});indent;Visit(node.Left);indent--;Console.Write(${Indent}{node.NodeType});indent;Visit(node.Right);indent--;Console.Write();return node;}
}引入了新的入口方法 Parse 来解析并设置缩进。Indent 属性返回一个换行和基于当前缩进值的正确数量的制表符。它被各方法调用并格式化输出。重写 VisitMethodCall 和 VisitBinary 可以帮助我们了解其工作原理。在 VisitMethodCall 中方法的名称被打印出来并有一个代表参数的开括号(。然后这些参数被依次访问将继续对每个参数进行递归直到完成。然后打印闭括号)。因为该方法明确地访问了子节点而不是调用基类该节点被简单地返回。这是因为基类也会递归地访问参数并导致重复。对于二元表达式先打印一个开角然后是访问的左边节点接着是二元操作的类型然后是右边节点最后是闭合。同样基类方法没有被调用因为这些节点已经被访问过了。运行这个新的 visitornew ExpressionConsoleWriter().Visit(query.Expression);输出结果可读性更好OrderBy(Take(Skip(Where( System.Collections.Generic.List1[ExpressionExplorer.Thing],t.Name.Contains( a,InvariantCultureIgnoreCase)AndAlsot.Created.GreaterThan.Now.AddDays( -1) t),2),50)
,t.Created.t)要想查看完整的实现 LINQ 本身的 ExpressionStringBuilder 包含了以友好格式打印表达式树所需的一切。你可以在这里查看源代码https://github.com/dotnet/runtime/blob/master/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionStringBuilder.cs解析表达式树的能力是相当强大的。我将在另一篇博文中更深入地挖掘它在此之前我想解决房间里的大象除了帮助解析表达式树之外Visit 方法返回表达式的意义何在事实证明ExpressionVisitor 能做的不仅仅是检查你的查询3侵入查询ExpressionVisitor 的一个神奇的特点是能够快速形成一个查询。为了理解这点请考虑这个场景你的任务是建立一个具有强大查询功能的订单输入系统你必须快速完成它。你读了我的文章决定使用 Blazor WebAssembly 并在客户端编写 LINQ 查询。你使用一个自定义的 visitor 来巧妙地序列化查询并将其传递给服务器在那里你反序列化并运行它。一切都进行得很顺利直到安全审计。在那里它被确定为查询引擎过于开放。一个恶意的客户端可以发出极其复杂的查询返回大量的结果集从而使系统瘫痪。你会怎么做使用 visitor 方法的一个好处是你不必为了修改一个子节点而重构整个表达式树。表达式树是不可改变的但是 visitor 可以返回一个全新的表达式树。你可以写好修改表达式树的逻辑并在最后收到完整的表达式树和修改内容。为了说明这一点让我们编写一个名为 ExpressionTakeRestrainer 的特殊 Visitorpublic class ExpressionTakeRestrainer : ExpressionVisitor
{private int maxTake;public bool ExpressionHasTake { get; private set; }public Expression ParseAndConstrainTake(Expression expression, int maxTake){this.maxTake maxTake;ExpressionHasTake false;return Visit(expression);}
}特殊的 ParseAndConstrainTake 方法将调用 Visit 并返回表达式。注意它把 ExpressionHasTake 用来标记表达式是否有Take。假设我们只想返回 5 个结果。理论上说你可以在查询的最后加上 Takevar myQuery theirQuery.Take(5);
return myQuery.ToList();但这其中的乐趣在哪里呢让我们来修改一个表达式树。我们将只覆盖一个方法那就是 VisitMethodCallprotected override Expression VisitMethodCall(MethodCallExpression node)
{if (node.Method.Name nameof(Enumerable.Take)){ExpressionHasTake true;if (node.Arguments.Count 2 node.Arguments[1] is ConstantExpression constant){var takeCount (int)constant.Value;if (takeCount maxTake){var arg1 Visit(node.Arguments[0]);var arg2 Expression.Constant(maxTake);var methodCall Expression.Call(node.Object,node.Method,new[] { arg1, arg2 } );return methodCall;}}}return base.VisitMethodCall(node);
}该逻辑检查方法的调用是否是 Enumerable.Take。如果是它将设置 ExpressionHasTake 标志。第二个参数是要读取的数字所以该值被检查并与最大值比较。如果它超过了允许的最大值就会建立一个新的节点把它限制在最大值范围内。这个新节点将被返回而不是原来的节点。如果该方法不是 Enumerable.Take那么就会调用基类一切都会“像往常一样”被解析。我们可以通过运行下面代码来测试它new ExpressionConsoleWriter().Parse(new ExpressionTakeRestrainer().ParseAndConstrainTake(query.Expression, 5));看看下面的结果查询已被修改为只取 5 条数据。OrderBy(Take(Skip(Where( System.Collections.Generic.List1[ExpressionExplorer.Thing],t.Name.Contains( a,InvariantCultureIgnoreCase)AndAlsot.Created.GreaterThan.Now.AddDays(-1) t),2),5)
,t.Created.t)但是等等...有5吗试试运行这个var list query.ToList();
Console.WriteLine($\r\n---\r\nQuery results: {list.Count});而且不幸的是你将看到的是 50......原始“获取”的数量。问题是我们生成了一个新的表达式但我们没有在查询中替换它。事实上我们不能......这是一个只读的属性而表达式是不可改变的。那么现在怎么办4移花接木我们可以简单地通过实现 IOrderedQueryableT 来制作我们自己的查询器该接口是其他接口的集合。下面是该接口要求的细则。ElementType - 这是简单的被查询元素的类型。Expression - 查询背后的表达式。Provider - 这就是查询提供者它完成应用查询的实际工作。我们不实现自己的提供者而是使用内置的在这种情况下是 LINQ-to-Objects。GetEnumerator - 运行查询的时候会调用它你可以随心所欲地建立、扩展和修改但一旦调用这它查询就被物化了。这里是 TranslatingHost 的一个实现它翻译了查询public class TranslatingHostT : IOrderedQueryableT, IOrderedQueryable
{private readonly IQueryableT query;public Type ElementType typeof(T);private Expression TranslatedExpression { get; set; }public TranslatingHost(IQueryableT query, int maxTake){this.query query;var translator new ExpressionTakeRestrainer();TranslatedExpression translator.ParseAndConstrainTake(query.Expression, maxTake);}public Expression Expression TranslatedExpression;public IQueryProvider Provider query.Provider;public IEnumeratorT GetEnumerator() Provider.CreateQueryT(TranslatedExpression).GetEnumerator();IEnumerator IEnumerable.GetEnumerator() GetEnumerator();
}它相当简单。它接收了一个现有的查询然后使用 ExpressionTakeRestrainer 来生成一个新的表达式。它使用现有的提供者例如如果这是一个来自 DbSetT 的查询在 SQL Server 上使用 EF Core它将翻译成一个 SQL 语句。当枚举器被请求时它不会传递原始表达式而是传递翻译后的表达式。让我们来使用它吧var transformedQuery new TranslatingHostThing(query, 5);
var list2 transformedQuery.ToList();
Console.WriteLine($\r\n---\r\nModified query results: {list2.Count});这次的结果是我们想要的......只返回 5 条记录。到目前为止我已经介绍了检查一个现有的查询并将其换掉。这在你执行查询时是有帮助的。如果你的代码是执行 query.ToList()那么你就可以随心所欲地修改查询。但是当你的代码不负责具体化查询的时候呢如果你暴露了一个类库比如一个仓储类它有下面这个接口会怎么样public IQueryableThing QueryThings { get; }或在使用 EF Core 的情况public DbSetThing Things { get; set; }当调用者调用 ToList() 时你如何“拦截”查询这需要一个 Provider我将在本系列的下一篇文章中详细介绍这个问题。