保健励志美文体育育儿作文
投稿投诉
作文动态
热点娱乐
育儿情感
教程科技
体育养生
教案探索
美文旅游
财经日志
励志范文
论文时尚
保健游戏
护肤业界

深入LINQ揭开IQueryable的面纱

  原文:bit。ly3uAXliC
  作者:JeremyLikness
  译者:精致码农王亮
  在上一篇博文中,我们探索了表达式的强大,并用它来动态地构建一个基于JSON的规则引擎。在这篇文章中,我们反过来,从表达式开始。考虑到表达式类型的多样性和表达式树的复杂性,分解表达式树有什么好的方法呢?我们能否对表达式进行变异,使其有不同的表现呢?
  首先,如果你还没有读过第一篇文章,请花几分钟时间去看看。本系列的的源代码放在GitHub:https:github。comJeremyLiknessExpressionExplorer准备工作
  首先,假设我有一个普通的CLR实体类(你可能听说过它被称为POCO),该类名为Thing。下面是它的定义:publicclassThing{publicThing(){IdGuid。NewGuid()。ToString();CreatedDateTimeOffset。Now;NameGuid。NewGuid()。ToString()。Split()〔0〕;}publicstringId{get;set;}publicstringName{get;set;}publicDateTimeOffsetCreated{get;privateset;}publicstringGetId()Id;publicoverridestringToString()34;({Id}:{Name}{Created});}
  为了模拟,我添加了一个静态方法,使其很容易生成N个数量的Thing:publicstaticIListThingThings(intcount){varthingsnewListThing();while(count0){things。Add(newThing());}returnthings;}
  现在我可以生成一个数据源并查询它。这里有一个LINQ表达式,它可以生成500个Thing并查询它们:varqueryThing。Things(500)。AsQueryable()。Where(tt。Name。Contains(a,StringComparison。InvariantCultureIgnoreCase)t。CreatedDateTimeOffset。Now。AddDays(1))。Skip(2)。Take(50)。OrderBy(tt。Created);
  如果你对query调用ToString(),你会得到这样的结果:System。Collections。Generic。List1〔ExpressionExplorer。Thing〕。Where(t(t。Name。Contains(a,InvariantCultureIgnoreCase)AndAlso(t。CreatedDateTimeOffset。Now。AddDays(1))))。Skip(2)。Take(50)。OrderBy(tt。Created)
  你可能没有注意到,query有一个名为Expression的属性。
  表达式的构建方式不会太神秘。从列表开始,Enumerable。Where方法被调用。第一个参数是一个可枚举列表(IEnumerable),第二个参数是一个谓词(predicate)。在predicate内部,string。Contains被调用。Enumerable。Skip方法接收一个可枚举列表和一个代表计数的整数。虽然构建查询的语法看起来很简单,但你可以把它想象成一系列渐进的过滤器。Skip调用是可枚举列表的一个扩展方法,它从Where调用中获取结果,以此类推。
  也为帮助理解,我画了一个插图来说明这点:
  然而,如果你想解析表达式树,你可能会大吃一惊。有许多不同的表达式类型,每一种表达式都有不同的解析方式。例如,BinaryExpression有一个Left和一个Right,但是MethodCallExpression有一个Arguments表达式列表。光是遍历表达式树,就有很多类型检查和转换了!另一个Visitor
  LINQ提供了一个名为ExpressionVisitor的特殊类。它包含了递归解析表达式树所需的所有逻辑。你只需将一个表达式传入Visit方法中,它就会访问每个节点并返回表达式(后面会有更多介绍)。它包含特定于节点类型的方法,这些方法可以被重载以拦截这个过程。下面是一个基本的实现,它简单地重写了某些方法,把信息写到控制台。publicclassBasicExpressionConsoleWriter:ExpressionVisitor{protectedoverrideExpressionVisitBinary(BinaryExpressionnode){Console。Write(34;binary:{node。NodeType});returnbase。VisitBinary(node);}protectedoverrideExpressionVisitUnary(UnaryExpressionnode){if(node。Method!null){Console。Write(34;unary:{node。Method。Name});}Console。Write(34;unary:{node。Operand。NodeType});returnbase。VisitUnary(node);}protectedoverrideExpressionVisitConstant(ConstantExpressionnode){Console。Write(34;constant:{node。Value});returnbase。VisitConstant(node);}protectedoverrideExpressionVisitMember(MemberExpressionnode){Console。Write(34;member:{node。Member。Name});returnbase。VisitMember(node);}protectedoverrideExpressionVisitMethodCall(MethodCallExpressionnode){Console。Write(34;call:{node。Method。Name});returnbase。VisitMethodCall(node);}protectedoverrideExpressionVisitParameter(ParameterExpressionnode){Console。Write(34;p:{node。Name});returnbase。VisitParameter(node);}}
  要使用它,只需创建一个实例并将一个表达式传给它。在这里,我们将把我们的查询表达式传递给它:newBasicExpressionConsoleWriter()。Visit(query。Expression);
  运行后它输出不是很直观的结果,如下:call:OrderBycall:Takecall:Skipcall:Whereconstant:System。Collections。Generic。List1〔ExpressionExplorer。Thing〕unary:Lambdabinary:AndAlsocall:Containsmember:Namep:tconstant:aconstant:InvariantCultureIgnoreCasebinary:GreaterThanmember:Createdp:tcall:AddDaysmember:Nowconstant:1p:tconstant:2constant:50unary:Lambdamember:Createdp:tp:t
  注意访问顺序。这可能需一点时间理解这个逻辑,但它是有意义的:OrderBy是最外层的调用(后进先出),它接受一个列表和一个字段。。。OrderBy的第一个参数是列表,它由Take提供。。。Take需要一个列表,这是由Skip提供的。。。Skip需要一个列表,由Where提供。。。Where需要一个列表,该列表由Thing列表提供。。。Where的第二个参数是一个predicatelambda表达式。。。。。。它是二元逻辑的AndAlso。。。二元逻辑的左边是一个Contains调用。。。(跳过一堆的逻辑)Take的第二个参数是50。。。Skip的第二个参数是2。。。OrderBy属性是Created。。。
  你Get到这里的逻辑了吗?了解树是如何解析的,是使我们的Visitor更易读的关键。这里有一个更一目了然的输出实现:publicclassExpressionConsoleWriter:ExpressionVisitor{intindent;privatestringIndent34;r{newstring(,indent)};publicvoidParse(Expressionexpression){indent0;Visit(expression);}protectedoverrideExpressionVisitConstant(ConstantExpressionnode){if(node。ValueisExpressionvalue){Visit(value);}else{Console。Write(34;{node。Value});}returnnode;}protectedoverrideExpressionVisitParameter(ParameterExpressionnode){Console。Write(node。Name);returnnode;}protectedoverrideExpressionVisitMember(MemberExpressionnode){if(node。Expression!null){Visit(node。Expression);}Console。Write(34;。{node。Member?。Name}。);returnnode;}protectedoverrideExpressionVisitMethodCall(MethodCallExpressionnode){if(node。Object!null){Visit(node。Object);}Console。Write(34;{Indent}{node。Method。Name}();varfirsttrue;indent;foreach(vararginnode。Arguments){if(first){firstfalse;}else{indent;Console。Write(34;{Indent},);indent;}Visit(arg);}indent;Console。Write());returnnode;}protectedoverrideExpressionVisitBinary(BinaryExpressionnode){Console。Write(34;{Indent});indent;Visit(node。Left);indent;Console。Write(34;{Indent}{node。NodeType});indent;Visit(node。Right);indent;Console。Write();returnnode;}}
  引入了新的入口方法Parse来解析并设置缩进。Indent属性返回一个换行和基于当前缩进值的正确数量的制表符。它被各方法调用并格式化输出。
  重写VisitMethodCall和VisitBinary可以帮助我们了解其工作原理。在VisitMethodCall中,方法的名称被打印出来,并有一个代表参数的开括号(。然后这些参数被依次访问,将继续对每个参数进行递归,直到完成。然后打印闭括号)。因为该方法明确地访问了子节点,而不是调用基类,该节点被简单地返回。这是因为基类也会递归地访问参数并导致重复。对于二元表达式,先打印一个开角,然后是访问的左边节点,接着是二元操作的类型,然后是右边节点,最后是闭合。同样,基类方法没有被调用,因为这些节点已经被访问过了。
  运行这个新的visitor:newExpressionConsoleWriter()。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。comdotnetruntimeblobmastersrclibrariesSystem。Linq。ExpressionssrcSystemLinqExpressionsExpressionStringBuilder。cs
  解析表达式树的能力是相当强大的。我将在另一篇博文中更深入地挖掘它,在此之前,我想解决房间里的大象:除了帮助解析表达式树之外,Visit方法返回表达式的意义何在?事实证明,ExpressionVisitor能做的不仅仅是检查你的查询!侵入查询
  ExpressionVisitor的一个神奇的特点是能够快速形成一个查询。为了理解这点,请考虑这个场景:你的任务是建立一个具有强大查询功能的订单输入系统,你必须快速完成它。你读了我的文章,决定使用BlazorWebAssembly并在客户端编写LINQ查询。你使用一个自定义的visitor来巧妙地序列化查询,并将其传递给服务器,在那里你反序列化并运行它。一切都进行得很顺利,直到安全审计。在那里,它被确定为查询引擎过于开放。一个恶意的客户端可以发出极其复杂的查询,返回大量的结果集,从而使系统瘫痪。你会怎么做?
  使用visitor方法的一个好处是,你不必为了修改一个子节点而重构整个表达式树。表达式树是不可改变的,但是visitor可以返回一个全新的表达式树。你可以写好修改表达式树的逻辑,并在最后收到完整的表达式树和修改内容。为了说明这一点,让我们编写一个名为ExpressionTakeRestrainer的特殊Visitor:publicclassExpressionTakeRestrainer:ExpressionVisitor{privateintmaxTake;publicboolExpressionHasTake{get;privateset;}publicExpressionParseAndConstrainTake(Expressionexpression,intmaxTake){this。maxTakemaxTake;ExpressionHasTakefalse;returnVisit(expression);}}
  特殊的ParseAndConstrainTake方法将调用Visit并返回表达式。注意,它把ExpressionHasTake用来标记表达式是否有Take。假设我们只想返回5个结果。理论上说,你可以在查询的最后加上Take:varmyQuerytheirQuery。Take(5);returnmyQuery。ToList();
  但这其中的乐趣在哪里呢?让我们来修改一个表达式树。我们将只覆盖一个方法,那就是VisitMethodCall:protectedoverrideExpressionVisitMethodCall(MethodCallExpressionnode){if(node。Method。Namenameof(Enumerable。Take)){ExpressionHasTaketrue;if(node。Arguments。Count2node。Arguments〔1〕isConstantExpressionconstant){vartakeCount(int)constant。Value;if(takeCountmaxTake){vararg1Visit(node。Arguments〔0〕);vararg2Expression。Constant(maxTake);varmethodCallExpression。Call(node。Object,node。Method,new〔〕{arg1,arg2});returnmethodCall;}}}returnbase。VisitMethodCall(node);}
  该逻辑检查方法的调用是否是Enumerable。Take。如果是,它将设置ExpressionHasTake标志。第二个参数是要读取的数字,所以该值被检查并与最大值比较。如果它超过了允许的最大值,就会建立一个新的节点,把它限制在最大值范围内。这个新节点将被返回,而不是原来的节点。如果该方法不是Enumerable。Take,那么就会调用基类,一切都会像往常一样被解析。
  我们可以通过运行下面代码来测试它:newExpressionConsoleWriter()。Parse(newExpressionTakeRestrainer()。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吗!?试试运行这个:varlistquery。ToList();Console。WriteLine(34;rrQueryresults:{list。Count});
  而且,不幸的是,你将看到的是50。。。。。。原始获取的数量。问题是,我们生成了一个新的表达式,但我们没有在查询中替换它。事实上,我们不能。。。。。。这是一个只读的属性,而表达式是不可改变的。那么现在怎么办?移花接木
  我们可以简单地通过实现IOrderedQueryable来制作我们自己的查询器,该接口是其他接口的集合。下面是该接口要求的细则。ElementType这是简单的被查询元素的类型。Expression查询背后的表达式。Provider这就是查询提供者,它完成应用查询的实际工作。我们不实现自己的提供者,而是使用内置的,在这种情况下是LINQtoObjects。GetEnumerator运行查询的时候会调用它,你可以随心所欲地建立、扩展和修改,但一旦调用这它,查询就被物化了。
  这里是TranslatingHost的一个实现,它翻译了查询:publicclassTranslatingHostT:IOrderedQueryableT,IOrderedQueryable{privatereadonlyIQueryableTquery;publicTypeElementTypetypeof(T);privateExpressionTranslatedExpression{get;set;}publicTranslatingHost(IQueryableTquery,intmaxTake){this。queryquery;vartranslatornewExpressionTakeRestrainer();TranslatedExpressiontranslator。ParseAndConstrainTake(query。Expression,maxTake);}publicExpressionExpressionTranslatedExpression;publicIQueryProviderProviderquery。Provider;publicIEnumeratorTGetEnumerator()Provider。CreateQueryT(TranslatedExpression)。GetEnumerator();IEnumeratorIEnumerable。GetEnumerator()GetEnumerator();}
  它相当简单。它接收了一个现有的查询,然后使用ExpressionTakeRestrainer来生成一个新的表达式。它使用现有的提供者(例如,如果这是一个来自DbSet的查询,在SQLServer上使用EFCore,它将翻译成一个SQL语句)。当枚举器被请求时,它不会传递原始表达式,而是传递翻译后的表达式。
  让我们来使用它吧:vartransformedQuerynewTranslatingHostThing(query,5);varlist2transformedQuery。ToList();Console。WriteLine(34;rrModifiedqueryresults:{list2。Count});
  这次的结果是我们想要的。。。。。。只返回5条记录。
  到目前为止,我已经介绍了检查一个现有的查询并将其换掉。这在你执行查询时是有帮助的。如果你的代码是执行query。ToList(),那么你就可以随心所欲地修改查询。但是当你的代码不负责具体化查询的时候呢?如果你暴露了一个类库,比如一个仓储类,它有下面这个接口会怎么样?publicIQueryableThingQueryThings{get;}
  或在使用EFCore的情况:publicDbSetThingThings{get;set;}
  当调用者调用ToList()时,你如何拦截查询?这需要一个Provider,我将在本系列的下一篇文章中详细介绍这个问题。

美国人脸识别公司和解,将对全美大部分私企和个人停售数据库当地时间5月10日,人脸识别初创公司ClearviewAI在美国伊利诺伊州的官司历时两年尘埃落定。最终,被告ClearviewAI公司接受了伊利诺伊州法院发起的和解协议。……假如我是小偷三年级作文550字大家一定都很熟悉小偷这个字眼,也非常讨厌它。因为,小偷是一种非常肮脏的职业。但是,在我看来非常神圣的职业。大家可千万不要着急哦,听我慢慢为你们解释。假如我是一个小偷,我会……能链六周年以数字化勾勒智慧能源蓝图5月9日,国内能源服务行业再添新面孔。中国检验认证集团与能链正式成立合资公司中检能链。中检能链将针对油、电等能源品类,在加油站、充电站及交通碳中和等领域,开展质量检测、星级评定……雨中游东湖早上起来,东方已一片通红,真是游玩的好天气。盼了好多天,爸爸,妈妈终于有时间陪我去绍兴东湖游玩了。一上车,我就晕乎乎的,好不容易在晕车中熬到了东湖,天却毫不客气地下起了雨……中银证券聘任葛浩为公司首席科学家北京商报讯(记者刘宇阳李海媛)5月10日,中银国际证券股份有限公司(以下简称中银证券)发布公告称,经公司董事会审议通过,同意在高级管理人员中增设公司首席科学家岗位,并对现任部分……时光摧残着我的容颜心情日记今天的我已经就二十岁,起床站在宿舍的阳台,望着窗外的天空,没有人记得今天是谁的生日,每一年的今天没有一人记得自己生日,永远都只有自己的父母记得。不知道该用什么语言来形容自己此时……嫦娥奔月后续1000字自从嫦娥飞上月亮以后,后羿终日伤心欲绝。他不再像从前那样为民除害,勇敢无畏,而是整日与酒陪伴。喝醉了,就坐在外面凝望着月亮,那深情的目光总是情不自禁地看一眼再看一眼,俗话……关于六年级的寒假作文400字(一)寒假,一个让人期盼已久的日子;寒假,一段让人难以忘怀的回忆;寒假,一次让人盛情难却的终极之旅。送走了枯燥。乏味的校园生活,终于,寒假来临了!随着寒假的来临,天……有关于描写春天用到的经典词语春:春季;一年的第一季。如四季如春,一年之计在于春。可组成春景、春光、温暖如春等。〔同义〕春天春日春季春今阳春艳阳春上大春三春九春青春艳阳天春景天〔注〕春,也可以表……父亲的手850字作文父亲离开我们整整半个世纪了,但他那两只浑厚结实、握满人世沧桑的手,却一直晃在我的脸前,好像我一伸手就能拉住。父亲满手掌像一个大茧子,大茧上又长些小茧子,每一只都像一面小铜……关于我的弟弟小学生优秀作文今天,爸爸妈妈带着我一起去奶奶家玩,我非常高兴,因为我可以在奶奶那里见到我那刚满3岁的小弟弟了,小弟弟很淘气,他常常做出一些很逗人开心的事,让我们笑得肚子痛,他可以算得上是我的……想像作文我的溜冰鞋我的溜冰鞋,只能在地上滑。虽然很时尚,但是在烈日炎炎的夏天,脚出汗就黏黏的,不容易散热,为了克服弊端,我决定发明一种未来的溜冰鞋。这种溜冰鞋装备有了很大的改善,护手掌是智……
苹果即将推出AR头盔?这些是我们目前所知道的信息文DavidPhelan距离苹果全球开发者大会(WWDC)还有两周多的时间。这是一个线上的活动,外加一个为少数幸运的开发者举办的小型现场活动假设现场部分没有被砍掉的话。……我的烦恼记事作文四篇导语:在我们的成长道路上每个人都会有不同的烦恼,大家说说都有什么呢?以下是小编为大家分享的我的烦恼记事作文四篇,欢迎借鉴!篇一我的烦恼成长的步伐到来了,成长的烦恼也……关于中秋思念家乡的作文中秋节是我国的传统节日,是阖家欢乐,团团圆圆的节日,也是有着吃月饼赏月的习俗。民间还流传着关于嫦娥奔月,玉兔捣药等神话故事,体现了人们对幸福生活的追求和向往,这么有意义的节日,……语文考试的反思200字导语:岁月如梭,眨眼间一学期,又要考试啦,下面由小编为您整理出的语文考试的反思200字内容,一起来看看吧。(一)语文考试的反思200字这个星期五,上午第二节课和第三节课,……樱花树下的樱花女孩800字春天,在一颗粉色的樱花树下有一个小女孩,她披着粉红色的头发,有一对亮晶晶的眼睛,美丽极了!可是,她是谁呢?我跑过去问她:ldquo;请问,你是?rdquo;ldquo;我是樱花……我的百草园作文500字写景作文要抓住景物的形状、大小、色彩、数量、声响、神韵、变化等这些方面的特点进行描写。下面给大家分享了我的百草园作文,一起来看看吧!我的百草园作文1百草园,是乡下姥姥家,……关于美国的作文350字篇一:美国游〔350字〕在和美国姨短暂的团聚之后,我们很快踏上了美国东部的行程。我们的第一站是纽约。7月21日,我们早上六点半起床,经过半个多小时车程,来到了华尔街……三年级关于期中考试的作文【篇一】今天,我一起床,就感觉十分不自然。怪不得呢!爸爸妈妈不停地叮嘱,周围的空气好象都凝固在一起mdash;mdash;要期中考试了。不过,我向来都不怎么怕考试的,不就……美丽广西生态乡村活动摄影大赛征稿启事为适应经济发展新常态、顺应人民群众对美好生活新期盼、推进乡村建设上台阶上水平,在巩固ldquo;美丽广西middot;清洁乡村rdquo;活动成果的基础上,自治区党委、政府于2……关于成功的名言警句集锦句子道德并不导向幸福,正如犯罪未必引来灾祸;良心有一种逻辑,命运又有另外一种逻辑,这两种逻辑是矛盾的,没有什么可以预见。雨果《海上劳工》有谁看见过杂技演员抛圆球的,就是看见了……自己不是别人的镜子在一个农夫的牧场中关着一头健壮的牛、一只肥胖的猪和洁白的羊。有一天,农夫来到牧场,将猪绑起来,带到家中,ldquo;嗷嗷嗷rdquo;猪叫着。猪使劲摇晃身体,用两只小短腿使劲跑……半画幅单反,加上50定焦头,扫街是一种什么感觉?什么感觉?非常不好扫!毕竟50定焦镜头太短了,用这个镜头去扫街,感觉有的搞笑的感觉,那么近距离拍摄街头,一个被拍摄者,你离她那么近,好拍吗?501。4拍摄个大头照半身照还勉勉强……
友情链接:易事利快生活快传网聚热点七猫云快好知快百科中准网快好找文好找中准网快软网