日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

深入LINQ | 揭开IQueryable的面纱

發布時間:2023/12/4 编程问答 53 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深入LINQ | 揭开IQueryable的面纱 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

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

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

首先,如果你還沒有讀過第一篇文章,請花幾分鐘時間去看看。本系列的的源代碼放在 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})"; }

為了模擬,我添加了一個靜態方法,使其很容易生成 N 個數量的 Thing:

public?static IList<Thing> Things(int count) {var things = new List<Thing>();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.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 的屬性。

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

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

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

2另一個 Visitor

LINQ 提供了一個名為 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.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

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

  • 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 中,方法的名稱被打印出來,并有一個代表參數的開括號(。然后這些參數被依次訪問,將繼續對每個參數進行遞歸,直到完成。然后打印閉括號)。因為該方法明確地訪問了子節點,而不是調用基類,該節點被簡單地返回。這是因為基類也會遞歸地訪問參數并導致重復。對于二元表達式,先打印一個開角<,然后是訪問的左邊節點,接著是二元操作的類型,然后是右邊節點,最后是閉合。同樣,基類方法沒有被調用,因為這些節點已經被訪問過了。

    運行這個新的 visitor:

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

    輸出結果可讀性更好:

    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)

    要想查看完整的實現, 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 的特殊 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 方法將調用 Visit 并返回表達式。注意,它把 ExpressionHasTake 用來標記表達式是否有Take。假設我們只想返回 5 個結果。理論上說,你可以在查詢的最后加上 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); }

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

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

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

    看看下面的結果:查詢已被修改為只取 5 條數據。

    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......原始“獲取”的數量。問題是,我們生成了一個新的表達式,但我們沒有在查詢中替換它。事實上,我們不能......這是一個只讀的屬性,而表達式是不可改變的。那么現在怎么辦?

    4移花接木

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

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

  • Expression - 查詢背后的表達式。

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

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

  • 這里是 TranslatingHost 的一個實現,它翻譯了查詢:

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

    它相當簡單。它接收了一個現有的查詢,然后使用 ExpressionTakeRestrainer 來生成一個新的表達式。它使用現有的提供者(例如,如果這是一個來自 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}");

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

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

    public IQueryable<Thing> QueryThings { get; }

    或在使用 EF Core 的情況:

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

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

    總結

    以上是生活随笔為你收集整理的深入LINQ | 揭开IQueryable的面纱的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。