根據執行階段狀態來查詢 (Visual Basic)
想一想針對資料來源定義 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 方法的運算式樹狀架構
- 使用 Expression 中的 Factory 方法來建構一個 Expression(Of TDelegate) 運算式樹狀結構
- 將方法呼叫節點新增至 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 方法
一般來說,Queryable 的內建 LINQ 方法會執行兩個步驟:
- 將目前的運算式樹狀架構包裝在表示方法呼叫的 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 方法),進而在執行階段將運算式訂製為特定的元素類型。
建構 Expression(Of 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) 的基本步驟如下:
使用 Parameter Factory 方法,為 Lambda 運算式中的每個參數 (若有的話) 定義 ParameterExpression 物件。
Dim x As ParameterExpression = Parameter(GetType(String), "x")
使用您定義的 ParameterExpression,以及 Expression 的 Factory 方法,建構 LambdaExpression 的主體。 例如,表示
x.StartsWith("a")
的運算可以像這樣建構:Dim body As Expression = [Call]( x, GetType(String).GetMethod("StartsWith", {GetType(String)}), Constant("a") )
將參數和主體包裝在編譯時類型化的 Expression(Of TDelegate) 中 (使用適當的 Lambda Factory 方法多載):
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 方法的功能 (藉由將整個樹狀結構包裝在表示對 LINQ 方法呼叫的 MethodCallExpression 中):
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
泛型預留位置,因此您將使用不需要編譯時間類型資訊且會產生 LambdaExpression 而不是 Expression(Of TDelegate) 的 Lambda 多載。
動態 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