Wykonywanie zapytań na podstawie stanu środowiska uruchomieniowego (Visual Basic)
Rozważ użycie kodu definiującego element IQueryable IQueryable(Of T) względem źródła danych:
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)
Za każdym razem, gdy uruchomisz ten kod, zostanie wykonane dokładnie to samo zapytanie. Często nie jest to bardzo przydatne, ponieważ kod może wykonywać różne zapytania w zależności od warunków w czasie wykonywania. W tym artykule opisano sposób wykonywania innego zapytania na podstawie stanu środowiska uruchomieniowego.
IQueryable /IQueryable(Of T) i drzewa wyrażeń
Zasadniczo element IQueryable ma dwa składniki:
- Expression— niezależna od języka i źródła danych reprezentacja składników bieżącego zapytania w postaci drzewa wyrażeń.
- Provider— wystąpienie dostawcy LINQ, które wie, jak zmaterializować bieżące zapytanie w wartość lub zestaw wartości.
W kontekście dynamicznego wykonywania zapytań dostawca zwykle pozostanie taki sam; drzewo wyrażeń zapytania będzie się różnić od zapytania do zapytania.
Drzewa wyrażeń są niezmienne; jeśli chcesz użyć innego drzewa wyrażeń , a tym samym innego zapytania, musisz przetłumaczyć istniejące drzewo wyrażeń na nowe, a tym samym na nowe IQueryable.
W poniższych sekcjach opisano konkretne techniki wykonywania zapytań inaczej w odpowiedzi na stan środowiska uruchomieniowego:
- Używanie stanu środowiska uruchomieniowego z poziomu drzewa wyrażeń
- Wywoływanie dodatkowych metod LINQ
- Zmienia drzewo wyrażeń przekazane do metod LINQ
- Konstruowanie drzewa wyrażeń Expression(Of TDelegate) przy użyciu metod fabrycznych w Expression
- Dodawanie węzłów wywołania metody do IQueryabledrzewa wyrażeń
- Konstruowanie ciągów i używanie dynamicznej biblioteki LINQ
Używanie stanu środowiska uruchomieniowego z poziomu drzewa wyrażeń
Przy założeniu, że dostawca LINQ obsługuje go, najprostszym sposobem na dynamiczne wykonywanie zapytań jest odwołanie się do stanu środowiska uruchomieniowego bezpośrednio w zapytaniu za pośrednictwem zmiennej zamkniętej, na length
przykład w poniższym przykładzie kodu:
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
Drzewo wyrażeń wewnętrznych — w związku z tym zapytanie — nie zostało zmodyfikowane; zapytanie zwraca różne wartości tylko dlatego, że wartość length
została zmieniona.
Wywoływanie dodatkowych metod LINQ
Ogólnie rzecz biorąc, wbudowane metody LINQ w Queryable wykonaniu dwóch kroków:
- Zawijaj bieżące drzewo wyrażeń w MethodCallExpression obiekcie reprezentującym wywołanie metody.
- Przekaż opakowane drzewo wyrażeń z powrotem do dostawcy, aby zwrócić wartość za pośrednictwem metody dostawcy IQueryProvider.Execute lub zwrócić przetłumaczony obiekt zapytania za pośrednictwem IQueryProvider.CreateQuery metody .
Możesz zastąpić oryginalne zapytanie wynikiem metody IQueryable(Of T)-returning, aby uzyskać nowe zapytanie. Można to zrobić warunkowo na podstawie stanu środowiska uruchomieniowego, jak w poniższym przykładzie:
' Dim sortByLength As Boolean = ...
Dim qry = companyNamesSource
If sortByLength Then qry = qry.OrderBy(Function(x) x.Length)
Zmienia drzewo wyrażeń przekazane do metod LINQ
W zależności od stanu środowiska uruchomieniowego można przekazać różne wyrażenia do metod 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)
Możesz również utworzyć różne podwyrażenia przy użyciu biblioteki innej firmy, takiej jak PredykateBuilder linqKit:
' 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)
Konstruowanie drzew wyrażeń i zapytań przy użyciu metod fabrycznych
We wszystkich przykładach do tego momentu znaliśmy typ elementu w czasie kompilacji —String
i w związku z tym typ zapytania —IQueryable(Of String)
. Może być konieczne dodanie składników do zapytania dowolnego typu elementu lub dodanie różnych składników w zależności od typu elementu. Drzewa wyrażeń można tworzyć od podstaw przy użyciu metod fabrycznych w System.Linq.Expressions.Expressionlokalizacji , a tym samym dostosować wyrażenie w czasie wykonywania do określonego typu elementu.
Konstruowanie wyrażenia (TDelegate)
Podczas konstruowania wyrażenia, które ma być przekazywane do jednej z metod LINQ, faktycznie konstruujesz wystąpienie wyrażenia (TDelegate), gdzie TDelegate
jest jakiś typ delegata, taki jak Func(Of String, Boolean)
, Action
lub niestandardowy typ delegata.
Wyrażenie(Of TDelegate) dziedziczy z LambdaExpressionelementu , które reprezentuje pełne wyrażenie lambda podobne do następującego:
Dim expr As Expression(Of Func(Of String, Boolean)) = Function(x As String) x.StartsWith("a")
Element ma LambdaExpression dwa składniki:
- Lista parametrów —
(x As String)
reprezentowana Parameters przez właściwość . - Treść —
x.StartsWith("a")
reprezentowana Body przez właściwość .
Podstawowe kroki tworzenia wyrażenia (TDelegate) są następujące:
Zdefiniuj ParameterExpression obiekty dla każdego z parametrów (jeśli istnieją) w wyrażeniu lambda przy użyciu Parameter metody factory.
Dim x As ParameterExpression = Parameter(GetType(String), "x")
Skonstruuj treść obiektu przy użyciu zdefiniowanych LambdaExpressionmetod (s) i metod fabrycznych pod adresem Expression.ParameterExpression Na przykład wyrażenie reprezentujące
x.StartsWith("a")
może być skonstruowane w następujący sposób:Dim body As Expression = [Call]( x, GetType(String).GetMethod("StartsWith", {GetType(String)}), Constant("a") )
Zawijaj parametry i treść w wyrażeniu typu kompilatora (TDelegate) przy użyciu odpowiedniego Lambda przeciążenia metody fabryki:
Dim expr As Expression(Of Func(Of String, Boolean)) = Lambda(Of Func(Of String, Boolean))(body, x)
W poniższych sekcjach opisano scenariusz, w którym można utworzyć wyrażenie (Of TDelegate) w celu przekazania do metody LINQ i przedstawić pełny przykład tego, jak to zrobić przy użyciu metod fabrycznych.
Scenariusz
Załóżmy, że masz wiele typów jednostek:
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
W przypadku dowolnego z tych typów jednostek chcesz filtrować i zwracać tylko te jednostki, które mają dany tekst w jednym z pól string
. W przypadku Person
elementu należy wyszukać FirstName
właściwości i LastName
:
' Dim term = ...
Dim personsQry = (New List(Of Person)).AsQueryable.
Where(Function(x) x.FirstName.Contains(term) OrElse x.LastName.Contains(term))
Jednak w przypadku Car
elementu należy wyszukać tylko Model
właściwość :
' Dim term = ...
Dim carsQry = (New List(Of Car)).AsQueryable.
Where(Function(x) x.Model.Contains(term))
Chociaż można napisać jedną funkcję niestandardową dla IQueryable(Of Person)
i drugą dla IQueryable(Of Car)
, następująca funkcja dodaje to filtrowanie do dowolnego istniejącego zapytania, niezależnie od określonego typu elementu.
Przykład
' 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
Ponieważ funkcja przyjmuje i zwraca element IQueryable(Of T) (a nie tylko ), IQueryablemożesz dodać kolejne elementy zapytania w czasie kompilowania po filtrze tekstu.
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)
Dodawanie węzłów wywołania metody do IQueryabledrzewa wyrażeń
Jeśli masz IQueryable zamiast IQueryable (Of T), nie możesz bezpośrednio wywołać ogólnych metod LINQ. Jedną z alternatyw jest utworzenie drzewa wyrażeń wewnętrznych, jak powyżej, i użycie odbicia w celu wywołania odpowiedniej metody LINQ podczas przekazywania drzewa wyrażeń.
Można również zduplikować funkcjonalność metody LINQ, opakowując całe drzewo w obiekcie MethodCallExpression reprezentującym wywołanie metody 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
W takim przypadku nie masz ogólnego symbolu zastępczego czasu T
kompilacji, dlatego użyjesz Lambda przeciążenia, które nie wymaga informacji o typie czasu kompilacji i które generuje LambdaExpression zamiast wyrażenia (TDelegate).
Dynamiczna biblioteka LINQ
Konstruowanie drzew wyrażeń przy użyciu metod fabrycznych jest stosunkowo złożone; łatwiej jest tworzyć ciągi. Dynamiczna biblioteka LINQ uwidacznia zestaw metod rozszerzeń odpowiadających standardowym metodom IQueryable LINQ w Queryablelokalizacji , i który akceptuje ciągi w specjalnej składni zamiast drzew wyrażeń. Biblioteka generuje odpowiednie drzewo wyrażeń z ciągu i może zwrócić wynikowy przetłumaczony IQueryableelement .
Na przykład poprzedni przykład (w tym konstrukcja drzewa wyrażeń) może zostać przepisany w następujący sposób:
' 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