Udostępnij za pomocą


Wykonywanie zapytań na podstawie stanu środowiska uruchomieniowego (Visual Basic)

Rozważ użycie kodu definiującego element IQueryableIQueryable(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
  • Różnicuj drzewo wyrażeń przekazane do metod LINQ
  • Konstruowanie drzewa wyrażeń Expression(Of TDelegate) przy użyciu metod fabrycznych w Expression
  • Dodaj węzły wywołania metody do drzewa wyrażeń IQueryable
  • 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 LINQQueryable wykonują dwa kroki:

  • Umieść bieżące drzewo wyrażeń w MethodCallExpression, które reprezentuje 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)

Różnicuj 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 zewnętrznej biblioteki, takiej jak LinqKit's 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)

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.Expression, co pozwala na dostosowanie wyrażenia w czasie wykonywania do konkretnego typu elementu.

Konstruowanie wyrażenia (TDelegate)

Podczas konstruowania wyrażenia, które ma być przekazane do jednej z metod LINQ, faktycznie konstruujesz wystąpienie Expression(Of TDelegate), gdzie TDelegate jest jakimś typem delegata, takim jak Func(Of String, Boolean), Action, lub niestandardowym typem delegata.

Wyrażenie(Of TDelegate) dziedziczy po LambdaExpression, które reprezentuje kompletną funkcję lambda, podobną do następującej:

Dim expr As Expression(Of Func(Of String, Boolean)) = Function(x As String) x.StartsWith("a")

Komponent LambdaExpression ma dwa składniki.

  • Lista parametrów—(x As String)—reprezentowana przez właściwość Parameters.
  • Ciało—x.StartsWith("a")—reprezentowane przez właściwość Body.

Podstawowe kroki konstruowania Expression(Of 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ść swojego LambdaExpression, używając zdefiniowanych ParameterExpression(s) oraz metod fabrycznych znajdujących się w Expression. 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")
    )
    
  • Owiń parametry i treść w wyrażeniu typizowanym w czasie kompilacji Expression(Of TDelegate), używając odpowiedniego przeciążenia metody fabrykacyjnej:

    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, należy wyszukać właściwości FirstName 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 chciałbyś wyszukać tylko właściwość Model:

' 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

Ponieważ funkcja przyjmuje i zwraca TextFilter (a nie tylko ), możesz dodać kolejne elementy zapytania typowane w czasie kompilacji 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 tym przypadku nie masz uniwersalnego zastępnika czasu kompilacji T, dlatego użyjesz przeciążenia Lambda, 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 znaków. Dynamiczna biblioteka LINQ udostępnia zestaw metod rozszerzeń odpowiadających standardowym metodom IQueryable LINQ w Queryable, które akceptują ciągi w specjalnej składni zamiast drzew wyrażeń. Biblioteka generuje odpowiednie drzewo wyrażeń z ciągu znaków oraz może zwrócić rezultat przetłumaczony IQueryable.

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 także