考慮定義針對資料來源的 IQueryable 或 IQueryable(Of T) 的程式碼:
Dim companyNames As String() = {
"Consolidated Messenger", "Alpine Ski House", "Southridge Video",
"City Power & Light", "Coho Winery", "Wide World Importers",
"Graphic Design Institute", "Adventure Works", "Humongous Insurance",
"Woodgrove Bank", "Margie's Travel", "Northwind Traders",
"Blue Yonder Airlines", "Trey Research", "The Phone Company",
"Wingtip Toys", "Lucerne Publishing", "Fourth Coffee"
}
' We're using an in-memory array as the data source, but the IQueryable could have come
' from anywhere -- an ORM backed by a database, a web request, Or any other LINQ provider.
Dim companyNamesSource As IQueryable(Of String) = companyNames.AsQueryable
Dim fixedQry = companyNamesSource.OrderBy(Function(x) x)
每次執行此程式代碼時,都會執行相同的確切查詢。 這通常並不十分有用,因為您可能希望程式代碼根據運行時間的條件來執行不同的查詢。 本文說明如何根據運行時間狀態執行不同的查詢。
IQueryable/IQueryable(Of T) 和表示式樹
基本上,有兩個 IQueryable 元件:
- Expression—以表達式樹的形式,這是一種與語言和數據源無關的目前查詢元件表示。
- Provider—LINQ 提供者的實例,其知道如何將目前的查詢具體化為值或一組值。
在動態查詢的環境中,提供者通常會維持不變。查詢的表達式樹狀結構會隨查詢而不同。
表達式樹狀結構不可變;如果您要不同的表示式樹狀結構,因此需要不同的查詢,您必須將現有的運算式樹狀結構轉譯為新的運算式樹狀結構,進而轉譯為新的 IQueryable。
下列各節說明以不同方式查詢以回應運行時間狀態的特定技術:
- 從表達式樹中使用執行時狀態
- 呼叫其他LINQ方法
- 改變傳遞至 LINQ 方法的運算式樹狀結構
- 使用 Factory 函式來建構 Expression(Of TDelegate) 表達式樹狀結構 Expression
- 將方法呼叫節點新增至 IQueryable的表達式樹狀結構
- 建構字串,並使用 動態LINQ連結庫
從表達式樹中使用執行時狀態
假設 LINQ 提供者支援它,動態查詢最簡單的方式是透過封閉式變數直接在查詢中參考運行時間狀態,例如 length 在下列程式代碼範例中:
Dim length = 1
Dim qry = companyNamesSource.
Select(Function(x) x.Substring(0, length)).
Distinct
Console.WriteLine(String.Join(", ", qry))
' prints: C, A, S, W, G, H, M, N, B, T, L, F
length = 2
Console.WriteLine(String.Join(", ", qry))
' prints: Co, Al, So, Ci, Wi, Gr, Ad, Hu, Wo, Ma, No, Bl, Tr, Th, Lu, Fo
內部表達式樹狀結構以及查詢本身都沒有被修改;查詢會傳回不同的值僅僅是因為 length 的值已經變更。
呼叫其他LINQ方法
一般而言,內建的LINQ方法Queryable會執行兩個步驟:
- 將目前的表達式樹包裝在代表方法呼叫的 MethodCallExpression 中。
- 將包裝的表達式樹狀結構傳回給提供者,以透過提供者的 IQueryProvider.Execute 方法來返回一個值,或透過 IQueryProvider.CreateQuery 方法返回一個翻譯過的查詢物件。
您可以將原始查詢取代為 IQueryable(Of T)傳回方法的結果,以取得新的查詢。 您可以根據執行時間狀態有條件地執行這項作業,如下列範例所示:
' Dim sortByLength As Boolean = ...
Dim qry = companyNamesSource
If sortByLength Then qry = qry.OrderBy(Function(x) x.Length)
改變傳遞至 LINQ 方法的運算式樹狀結構
您可以根據執行時間狀態,將不同的運算式傳入 LINQ 方法:
' Dim startsWith As String = ...
' Dim endsWith As String = ...
Dim expr As Expression(Of Func(Of String, Boolean))
If String.IsNullOrEmpty(startsWith) AndAlso String.IsNullOrEmpty(endsWith) Then
expr = Function(x) True
ElseIf String.IsNullOrEmpty(startsWith) Then
expr = Function(x) x.EndsWith(endsWith)
ElseIf String.IsNullOrEmpty(endsWith) Then
expr = Function(x) x.StartsWith(startsWith)
Else
expr = Function(x) x.StartsWith(startsWith) AndAlso x.EndsWith(endsWith)
End If
Dim qry = companyNamesSource.Where(expr)
您也可以使用 LinqKit 的 PredicateBuilder 等第三方連結庫來撰寫各種子表達式:
' This is functionally equivalent to the previous example.
' Imports LinqKit
' Dim startsWith As String = ...
' Dim endsWith As String = ...
Dim expr As Expression(Of Func(Of String, Boolean)) = PredicateBuilder.[New](Of String)(False)
Dim original = expr
If Not String.IsNullOrEmpty(startsWith) Then expr = expr.Or(Function(x) x.StartsWith(startsWith))
If Not String.IsNullOrEmpty(endsWith) Then expr = expr.Or(Function(x) x.EndsWith(endsWith))
If expr Is original Then expr = Function(x) True
Dim qry = companyNamesSource.Where(expr)
使用 Factory 方法建構表達式樹狀架構和查詢
到目前為止,在所有的範例中,我們已在編譯時期知道元素的類型 String,並因此得知查詢 IQueryable(Of String) 的類型。 您可能需要將元件新增至任何元素類型的查詢,或根據項目類型新增不同的元件。 您可以從頭開始使用 System.Linq.Expressions.Expression 的 Factory 方法來建立運算式樹,因此可在執行期間將運算式自訂為特定的元素類型。
建構 表達式(TDelegate)
當您建構表達式以傳遞至其中一個 LINQ 方法時,您實際上正在建構 Expression(Of TDelegate)的實例,其中 TDelegate 是一些委派類型,例如 Func(Of String, Boolean)、 Action或自定義委派類型。
Expression(Of TDelegate) 繼承自 LambdaExpression,代表完整的 Lambda 表達式,如下所示:
Dim expr As Expression(Of Func(Of String, Boolean)) = Function(x As String) x.StartsWith("a")
LambdaExpression有兩個元件:
- 參數清單
(x As String)—由 Parameters 屬性表示。 - 一個由
x.StartsWith("a")屬性表示的實體—Body。
建構 Expression(Of TDelegate) 的基本步驟如下:
ParameterExpression使用 Parameter Factory 方法,定義 Lambda 運算式中每個參數的物件(如果有的話)。
Dim x As ParameterExpression = Parameter(GetType(String), "x")使用您已定義的LambdaExpression和ParameterExpression中的工廠方法,建構Expression的主體。 例如,表示的
x.StartsWith("a")表達式可以像這樣建構:Dim body As Expression = [Call]( x, GetType(String).GetMethod("StartsWith", {GetType(String)}), Constant("a") )使用適當的 Factory 方法多載,將參數和本文包裝在編譯時間類型的 Lambda中:
Dim expr As Expression(Of Func(Of String, Boolean)) = Lambda(Of Func(Of String, Boolean))(body, x)
下列各節說明您可能想要建構 Expression(Of TDelegate) 以傳遞至 LINQ 方法的案例,並提供如何使用 Factory 方法進行此作業的完整範例。
情境
假設您有多個實體類型:
Public Class Person
Property LastName As String
Property FirstName As String
Property DateOfBirth As Date
End Class
Public Class Car
Property Model As String
Property Year As Integer
End Class
針對上述任何實體類型,您想要篩選並只傳回在其其中一個字段內具有指定文字的 string 實體。 針對 Person,您會想要搜尋 FirstName 和 LastName 屬性:
' Dim term = ...
Dim personsQry = (New List(Of Person)).AsQueryable.
Where(Function(x) x.FirstName.Contains(term) OrElse x.LastName.Contains(term))
但對於 Car,您可能只想搜尋 Model 屬性。
' Dim term = ...
Dim carsQry = (New List(Of Car)).AsQueryable.
Where(Function(x) x.Model.Contains(term))
雖然您可以為 IQueryable(Of Person) 撰寫一個自定義函式,並為 IQueryable(Of Car) 撰寫另一個,但下列函式會將此篩選新增至任何現有的查詢,不論特定元素類型為何。
範例
' Imports System.Linq.Expressions.Expression
Function TextFilter(Of T)(source As IQueryable(Of T), term As String) As IQueryable(Of T)
If String.IsNullOrEmpty(term) Then Return source
' T is a compile-time placeholder for the element type of the query
Dim elementType = GetType(T)
' Get all the string properties on this specific type
Dim stringProperties As PropertyInfo() =
elementType.GetProperties.
Where(Function(x) x.PropertyType = GetType(String)).
ToArray
If stringProperties.Length = 0 Then Return source
' Get the right overload of String.Contains
Dim containsMethod As MethodInfo =
GetType(String).GetMethod("Contains", {GetType(String)})
' Create the parameter for the expression tree --
' the 'x' in 'Function(x) x.PropertyName.Contains("term")'
' The type of the parameter is the query's element type
Dim prm As ParameterExpression =
Parameter(elementType)
' Generate an expression tree node corresponding to each property
Dim expressions As IEnumerable(Of Expression) =
stringProperties.Select(Of Expression)(Function(prp)
' For each property, we want an expression node like this:
' x.PropertyName.Contains("term")
Return [Call]( ' .Contains(...)
[Property]( ' .PropertyName
prm, ' x
prp
),
containsMethod,
Constant(term) ' "term"
)
End Function)
' Combine the individual nodes into a single expression tree node using OrElse
Dim body As Expression =
expressions.Aggregate(Function(prev, current) [OrElse](prev, current))
' Wrap the expression body in a compile-time-typed lambda expression
Dim lmbd As Expression(Of Func(Of T, Boolean)) =
Lambda(Of Func(Of T, Boolean))(body, prm)
' Because the lambda is compile-time-typed, we can use it with the Where method
Return source.Where(lmbd)
End Function
因為函數 TextFilter 採用並傳回 IQueryable(Of T)(而不只是 IQueryable),因此您可以在文字篩選之後,新增進一步的編譯時間型別查詢元素。
Dim qry = TextFilter(
(New List(Of Person)).AsQueryable,
"abcd"
).Where(Function(x) x.DateOfBirth < #1/1/2001#)
Dim qry1 = TextFilter(
(New List(Of Car)).AsQueryable,
"abcd"
).Where(Function(x) x.Year = 2010)
將方法呼叫節點新增至 IQueryable的表達式樹狀結構
如果您有 IQueryable 而非 IQueryable(Of T),則無法直接呼叫泛型 LINQ 方法。 其中一個替代方法是建置上述的內部的表達式樹,並使用反射來呼叫適當的 LINQ 方法,同時傳入表達式樹。
您也可以將整個樹狀結構包裝在用來表示 LINQ 方法呼叫的 MethodCallExpression 中,從而模擬 LINQ 方法的功能:
Function TextFilter_Untyped(source As IQueryable, term As String) As IQueryable
If String.IsNullOrEmpty(term) Then Return source
Dim elementType = source.ElementType
' The logic for building the ParameterExpression And the LambdaExpression's body is the same as in
' the previous example, but has been refactored into the ConstructBody function.
Dim x As (Expression, ParameterExpression) = ConstructBody(elementType, term)
Dim body As Expression = x.Item1
Dim prm As ParameterExpression = x.Item2
If body Is Nothing Then Return source
Dim filteredTree As Expression = [Call](
GetType(Queryable),
"Where",
{elementType},
source.Expression,
Lambda(body, prm)
)
Return source.Provider.CreateQuery(filteredTree)
End Function
在此情況下,您沒有編譯時間T的泛型佔位元,因此您將使用不需要編譯時間類型資訊的Lambda多載,而且會產生LambdaExpression而不是Expression(Of TDelegate)。
動態 LINQ 連結庫
使用 Factory 方法建構表達式樹狀結構比較複雜;撰寫字串會比較容易。 動態 LINQ 程式庫提供一組擴充方法應用於IQueryable,對應於Queryable的標準 LINQ 方法,並接受特殊語法中的字串,而非表達式樹。 函式庫會從字串中生成適當的運算式樹,並且可以返回轉譯後的 IQueryable 結果。
例如,上述範例(包括表達式樹狀結構建構)可以重寫如下:
' Imports System.Linq.Dynamic.Core
Function TextFilter_Strings(source As IQueryable, term As String) As IQueryable
If String.IsNullOrEmpty(term) Then Return source
Dim elementType = source.ElementType
Dim stringProperties = elementType.GetProperties.
Where(Function(x) x.PropertyType = GetType(String)).
ToArray
If stringProperties.Length = 0 Then Return source
Dim filterExpr = String.Join(
" || ",
stringProperties.Select(Function(prp) $"{prp.Name}.Contains(@0)")
)
Return source.Where(filterExpr, term)
End Function