Udostępnij za pośrednictwem


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), Actionlub 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 Personelementu 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 Carelementu 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

Zobacz też