<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入LINQ | 揭開IQueryable的面紗

          共 12463字,需瀏覽 25分鐘

           ·

          2021-06-09 04:20

          原文:bit.ly/3uAXliC
          作者:Jeremy Likness
          譯者:精致碼農(nóng)-王亮

          上一篇博文中,我們探索了表達式的強大,并用它來動態(tài)地構(gòu)建一個基于 JSON 的規(guī)則引擎。在這篇文章中,我們反過來,從表達式開始。考慮到表達式類型的多樣性和表達式樹的復雜性,分解表達式樹有什么好的方法呢?我們能否對表達式進行變異,使其有不同的表現(xiàn)呢?

          首先,如果你還沒有讀過第一篇文章,請花幾分鐘時間去看看。本系列的的源代碼放在 GitHub:

          https://github.com/JeremyLikness/ExpressionExplorer

          1準備工作

          首先,假設我有一個普通的 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})";
          }

          為了模擬,我添加了一個靜態(tài)方法,使其很容易生成 N 個數(shù)量的 Thing

          public static IList<Thing> Things(int count)
          {
          var things = new List<Thing>();
          while (count-- > 0)
          {
          things.Add(new Thing());
          }
          return things;
          }

          現(xiàn)在我可以生成一個數(shù)據(jù)源并查詢它。這里有一個 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 調(diào)用 ToString(),你會得到這樣的結(jié)果:

          System.Collections.Generic.List`1[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 的屬性。

          表達式的構(gòu)建方式不會太神秘。從列表開始,Enumerable.Where 方法被調(diào)用。第一個參數(shù)是一個可枚舉列表(IEnumerable<T>),第二個參數(shù)是一個謂詞(predicate)。在 predicate 內(nèi)部,string.Contains 被調(diào)用。Enumerable.Skip 方法接收一個可枚舉列表和一個代表計數(shù)的整數(shù)。雖然構(gòu)建查詢的語法看起來很簡單,但你可以把它想象成一系列漸進的過濾器。Skip 調(diào)用是可枚舉列表的一個擴展方法,它從 Where 調(diào)用中獲取結(jié)果,以此類推。

          也為幫助理解,我畫了一個插圖來說明這點:

          然而,如果你想解析表達式樹,你可能會大吃一驚。有許多不同的表達式類型,每一種表達式都有不同的解析方式。例如,BinaryExpression 有一個 Left 和一個 Right,但是 MethodCallExpression 有一個 Arguments 表達式列表。光是遍歷表達式樹,就有很多類型檢查和轉(zhuǎn)換了!

          2另一個 Visitor

          LINQ 提供了一個名為 ExpressionVisitor 的特殊類。它包含了遞歸解析表達式樹所需的所有邏輯。你只需將一個表達式傳入 Visit 方法中,它就會訪問每個節(jié)點并返回表達式(后面會有更多介紹)。它包含特定于節(jié)點類型的方法,這些方法可以被重載以攔截這個過程。下面是一個基本的實現(xiàn),它簡單地重寫了某些方法,把信息寫到控制臺。

          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);
          }
          }

          要使用它,只需創(chuàng)建一個實例并將一個表達式傳給它。在這里,我們將把我們的查詢表達式傳遞給它:

          new BasicExpressionConsoleWriter().Visit(query.Expression);

          運行后它輸出不是很直觀的結(jié)果,如下:

          call:OrderBy  call:Take  call:Skip  call:Where
          constant:System.Collections.Generic.List`1[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

          注意訪問順序。這可能需一點時間理解這個邏輯,但它是有意義的:

          1. OrderBy 是最外層的調(diào)用(后進先出),它接受一個列表和一個字段...

          2. OrderBy 的第一個參數(shù)是列表,它由 Take 提供...

          3. Take 需要一個列表,這是由 Skip 提供的...

          4. Skip 需要一個列表,由 Where 提供...

          5. Where 需要一個列表,該列表由 Thing 列表提供...

          6. Where 的第二個參數(shù)是一個 predicate lambda 表達式...

          7. ...它是二元邏輯的 AndAlso...

          8. 二元邏輯的左邊是一個 Contains 調(diào)用...

          9. (跳過一堆的邏輯)

          10. Take 的第二個參數(shù)是 50...

          11. Skip 的第二個參數(shù)是 2...

          12. OrderBy 屬性是 Created...

          你 Get 到這里的邏輯了嗎?了解樹是如何解析的,是使我們的 Visitor 更易讀的關(guān)鍵。這里有一個更一目了然的輸出實現(xiàn):

          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 屬性返回一個換行和基于當前縮進值的正確數(shù)量的制表符。它被各方法調(diào)用并格式化輸出。

          重寫 VisitMethodCallVisitBinary 可以幫助我們了解其工作原理。在 VisitMethodCall 中,方法的名稱被打印出來,并有一個代表參數(shù)的開括號(。然后這些參數(shù)被依次訪問,將繼續(xù)對每個參數(shù)進行遞歸,直到完成。然后打印閉括號)。因為該方法明確地訪問了子節(jié)點,而不是調(diào)用基類,該節(jié)點被簡單地返回。這是因為基類也會遞歸地訪問參數(shù)并導致重復。對于二元表達式,先打印一個開角<,然后是訪問的左邊節(jié)點,接著是二元操作的類型,然后是右邊節(jié)點,最后是閉合。同樣,基類方法沒有被調(diào)用,因為這些節(jié)點已經(jīng)被訪問過了。

          運行這個新的 visitor:

          new ExpressionConsoleWriter().Visit(query.Expression);

          輸出結(jié)果可讀性更好:

          OrderBy(
          Take(
          Skip(
          Where( System.Collections.Generic.List`1[ExpressionExplorer.Thing]
          ,
          <t.Name.
          Contains( a
          ,InvariantCultureIgnoreCase)
          AndAlso
          <t.Created.
          GreaterThan.Now.
          AddDays( -1) >>t)
          ,2)
          ,50)
          ,t.Created.t)

          要想查看完整的實現(xiàn), LINQ 本身的 ExpressionStringBuilder 包含了以友好格式打印表達式樹所需的一切。你可以在這里查看源代碼:

          https://github.com/dotnet/runtime/blob/master/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionStringBuilder.cs

          解析表達式樹的能力是相當強大的。我將在另一篇博文中更深入地挖掘它,在此之前,我想解決房間里的大象:除了幫助解析表達式樹之外,Visit 方法返回表達式的意義何在?事實證明,ExpressionVisitor 能做的不僅僅是檢查你的查詢!

          3侵入查詢

          ExpressionVisitor 的一個神奇的特點是能夠快速形成一個查詢。為了理解這點,請考慮這個場景:你的任務是建立一個具有強大查詢功能的訂單輸入系統(tǒng),你必須快速完成它。你讀了我的文章,決定使用 Blazor WebAssembly 并在客戶端編寫 LINQ 查詢。你使用一個自定義的 visitor 來巧妙地序列化查詢,并將其傳遞給服務器,在那里你反序列化并運行它。一切都進行得很順利,直到安全審計。在那里,它被確定為查詢引擎過于開放。一個惡意的客戶端可以發(fā)出極其復雜的查詢,返回大量的結(jié)果集,從而使系統(tǒng)癱瘓。你會怎么做?

          使用 visitor 方法的一個好處是,你不必為了修改一個子節(jié)點而重構(gòu)整個表達式樹。表達式樹是不可改變的,但是 visitor 可以返回一個全新的表達式樹。你可以寫好修改表達式樹的邏輯,并在最后收到完整的表達式樹和修改內(nèi)容。為了說明這一點,讓我們編寫一個名為 ExpressionTakeRestrainer 的特殊 Visitor:

          public 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 方法將調(diào)用 Visit 并返回表達式。注意,它把 ExpressionHasTake 用來標記表達式是否有Take。假設我們只想返回 5 個結(jié)果。理論上說,你可以在查詢的最后加上 Take

          var myQuery = theirQuery.Take(5);
          return myQuery.ToList();

          但這其中的樂趣在哪里呢?讓我們來修改一個表達式樹。我們將只覆蓋一個方法,那就是 VisitMethodCall

          protected 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);
          }

          該邏輯檢查方法的調(diào)用是否是 Enumerable.Take。如果是,它將設置 ExpressionHasTake 標志。第二個參數(shù)是要讀取的數(shù)字,所以該值被檢查并與最大值比較。如果它超過了允許的最大值,就會建立一個新的節(jié)點,把它限制在最大值范圍內(nèi)。這個新節(jié)點將被返回,而不是原來的節(jié)點。如果該方法不是 Enumerable.Take,那么就會調(diào)用基類,一切都會“像往常一樣”被解析。

          我們可以通過運行下面代碼來測試它:

          new ExpressionConsoleWriter().Parse(
          new ExpressionTakeRestrainer()
          .ParseAndConstrainTake(query.Expression, 5));

          看看下面的結(jié)果:查詢已被修改為只取 5 條數(shù)據(jù)。

          OrderBy(
          Take(
          Skip(
          Where( System.Collections.Generic.List`1[ExpressionExplorer.Thing]
          ,
          <t.Name.
          Contains( a
          ,InvariantCultureIgnoreCase)
          AndAlso
          <t.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......原始“獲取”的數(shù)量。問題是,我們生成了一個新的表達式,但我們沒有在查詢中替換它。事實上,我們不能......這是一個只讀的屬性,而表達式是不可改變的。那么現(xiàn)在怎么辦?

          4移花接木

          我們可以簡單地通過實現(xiàn) IOrderedQueryable<T> 來制作我們自己的查詢器,該接口是其他接口的集合。下面是該接口要求的細則。

          1. ElementType - 這是簡單的被查詢元素的類型。

          2. Expression - 查詢背后的表達式。

          3. Provider - 這就是查詢提供者,它完成應用查詢的實際工作。我們不實現(xiàn)自己的提供者,而是使用內(nèi)置的,在這種情況下是 LINQ-to-Objects。

          4. GetEnumerator - 運行查詢的時候會調(diào)用它,你可以隨心所欲地建立、擴展和修改,但一旦調(diào)用這它,查詢就被物化了。

          這里是 TranslatingHost 的一個實現(xiàn),它翻譯了查詢:

          public class TranslatingHost<T> : IOrderedQueryable<T>, IOrderedQueryable
          {
          private readonly IQueryable<T> query;

          public Type ElementType => typeof(T);

          private Expression TranslatedExpression { get; set; }

          public TranslatingHost(IQueryable<T> 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 IEnumerator<T> GetEnumerator()
          => Provider.CreateQuery<T>(TranslatedExpression)
          .GetEnumerator();

          IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
          }

          它相當簡單。它接收了一個現(xiàn)有的查詢,然后使用 ExpressionTakeRestrainer 來生成一個新的表達式。它使用現(xiàn)有的提供者(例如,如果這是一個來自 DbSet<T> 的查詢,在 SQL Server 上使用 EF Core,它將翻譯成一個 SQL 語句)。當枚舉器被請求時,它不會傳遞原始表達式,而是傳遞翻譯后的表達式。

          讓我們來使用它吧:

          var transformedQuery =
          new TranslatingHost<Thing>(query, 5);
          var list2 = transformedQuery.ToList();
          Console.WriteLine($"\r\n---\r\nModified query results: {list2.Count}");

          這次的結(jié)果是我們想要的......只返回 5 條記錄。

          到目前為止,我已經(jīng)介紹了檢查一個現(xiàn)有的查詢并將其換掉。這在你執(zhí)行查詢時是有幫助的。如果你的代碼是執(zhí)行 query.ToList(),那么你就可以隨心所欲地修改查詢。但是當你的代碼不負責具體化查詢的時候呢?如果你暴露了一個類庫,比如一個倉儲類,它有下面這個接口會怎么樣?

          public IQueryable<Thing> QueryThings { get; }

          或在使用 EF Core 的情況:

          public DbSet<Thing> Things { get; set; }

          當調(diào)用者調(diào)用 ToList() 時,你如何“攔截”查詢?這需要一個 Provider,我將在本系列的下一篇文章中詳細介紹這個問題。







          回復 【關(guān)閉】關(guān)
          回復 【實戰(zhàn)】獲取20套實戰(zhàn)源碼
          回復 【被刪】
          回復 【訪客】
          回復 【小程序】學獲取15套【入門+實戰(zhàn)+賺錢】小程序源碼
          回復 【python】學微獲取全套0基礎Python知識手冊
          回復 【2019】獲取2019 .NET 開發(fā)者峰會資料PPT
          回復 【加群】加入dotnet微信交流群


          去TM收費,我要在線 Vip 視頻解析!


          【限時刪】劉*55頁ppt大瓜,比項*醒的還要精彩!



          瀏覽 97
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美极品网站 | 人人夜夜i日日 | 国产一区二三区免费A片惊变 | 91香蕉| 国产91aaa |