Durchführen von Abfragen auf Basis des Laufzeitzustands (Visual Basic)

Der folgende Code definiert eine IQueryable- oder IQueryable(Of T)-Schnittstelle für eine Datenquelle:

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)

Bei jeder Ausführung dieses Codes wird genau dieselbe Abfrage ausgeführt. Dies ist häufig nicht sehr nützlich, da Ihr Code je nach den Bedingungen zur Laufzeit verschiedene Abfragen ausführen soll. In diesem Artikel wird beschrieben, wie Sie auf Grundlage des Laufzeitzustands eine andere Abfrage ausführen können.

IQueryable/IQueryable(Of T) und Ausdrucksbaumstrukturen

Eine IQueryable-Schnittstelle verfügt im Grunde über zwei Komponenten:

  • Expression: eine sprach- und datenquellenagnostische Darstellung der Komponenten der aktuellen Abfrage in Form einer Ausdrucksbaumstruktur.
  • Provider: eine Instanz eines LINQ-Anbieters, die weiß, wie die aktuelle Abfrage in einem Wert oder einer Wertegruppe materialisiert werden soll.

Im Kontext dynamischer Abfragen bleibt der Anbieter normalerweise immer gleich. Es ist die Ausdrucksbaumstruktur, die sich von Abfrage zu Abfrage unterscheidet.

Ausdrucksbaumstrukturen sind unveränderlich. Wenn Sie eine andere Ausdrucksbaumstruktur (und somit eine andere Abfrage) möchten, müssen Sie die bestehende Struktur in eine neue Ausdrucksbaumstruktur und somit in eine neue IQueryable-Schnittstelle übersetzen.

In den folgenden Abschnitten werden die genauen Verfahren für das unterschiedliche Abfragen in Abhängigkeit vom Laufzeitzustand beschrieben:

  • Verwenden des Laufzeitzustands innerhalb der Ausdrucksbaumstruktur
  • Aufrufen weiterer LINQ-Methoden
  • Variieren der Ausdrucksbaumstruktur, die an die LINQ-Methoden übergeben wird
  • Expression(Of TDelegate)-Ausdrucksbaumstruktur mithilfe der Factorymethoden in Expression erstellen
  • Hinzufügen von Methodenaufrufknoten zu einer Ausdrucksbaumstruktur von IQueryable
  • Erstellen von Zeichenfolgen und Verwenden der dynamischen LINQ-Bibliothek

Verwenden des Laufzeitzustands innerhalb der Ausdrucksbaumstruktur

Wenn der LINQ-Anbieter dies unterstützt, besteht der einfachste Weg, eine dynamische Abfrage durchzuführen, darin, direkt in der Abfrage über eine geschlossene Variable, wie length im folgenden Codebeispiel, auf den Laufzeitzustand zu verweisen:

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

Die interne Ausdrucksbaumstruktur (und damit die Abfrage) wurde nicht geändert. Die Abfrage gibt nur andere Werte zurück, weil der Wert von length modifiziert wurde.

Aufrufen weiterer LINQ-Methoden

Im Allgemeinen führen die integrierten LINQ-Methoden in Queryable zwei Schritte aus:

  • Umschließen der aktuellen Ausdrucksbaumstruktur in einem MethodCallExpression-Objekt, das den Methodenaufruf darstellt
  • Zurückübergeben der umschließenden Ausdrucksbaumstruktur an den Anbieter, um entweder über die Methode IQueryProvider.Execute des Anbieters einen Wert oder über die Methode IQueryProvider.CreateQuery ein übersetztes Abfrageobjekt zurückzugeben

Sie können die ursprüngliche Abfrage durch das Ergebnis einer Methode ersetzen, die IQueryable(Of T) zurückgibt, um eine neue Abfrage zu erhalten. Dies kann abhängig vom Laufzeitzustand erfolgen, wie im folgenden Beispiel gezeigt:

' Dim sortByLength As Boolean  = ...

Dim qry = companyNamesSource
If sortByLength Then qry = qry.OrderBy(Function(x) x.Length)

Variieren der Ausdrucksbaumstruktur, die an die LINQ-Methoden übergeben wird

Abhängig vom Laufzeitzustand können Sie verschiedene Ausdrücke an die LINQ-Methoden übergeben:

' 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)

Sie sollten die verschiedenen Unterausdrücke außerdem mit einer Drittanbieterbibliothek wie PredicateBuilder von LinqKit erstellen:

' 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)

Erstellen von Ausdrucksbaumstrukturen und Abfragen mit Factorymethoden

In allen bisherigen Beispielen waren der Elementtyp zur Kompilierzeit, String, und somit auch der Typ der Abfrage, IQueryable(Of String), bekannt. Möglicherweise müssen Sie einer Abfrage eines beliebigen Elementtyps Komponenten hinzufügen bzw. je nach Elementtyp verschiedene Komponenten hinzufügen. Sie können Ausdrucksbaumstrukturen mithilfe der Factorymethoden in System.Linq.Expressions.Expression von Grund auf neu erstellen und so den Ausdruck zur Laufzeit auf einen bestimmten Elementtyp zuschneiden.

Erstellen eines Expression(Of TDelegate)-Objekts

Wenn Sie einen Ausdruck erstellen, der an eine der LINQ-Methoden übergeben werden soll, erstellen Sie tatsächlich eine Instanz von Expression(Of TDelegate, wobei TDelegate ein Delegattyp wie Func(Of String, Boolean), Action oder ein benutzerdefinierter Delegattyp ist.

Expression(Of TDelegate erbt von einem LambdaExpression-Objekt, das einen kompletten Lambdaausdruck wie den folgenden darstellt:

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

LambdaExpression verfügt über zwei Komponenten:

  • Eine Parameterliste, (x As String), die von der Parameters-Eigenschaft dargestellt wird.
  • Einen Textkörper, x.StartsWith("a"), der durch die Body-Eigenschaft dargestellt wird.

Die grundlegenden Schritte zum Erstellen eines Expression(Of TDelegate)-Objekts lauten wie folgt:

  • Definieren Sie ParameterExpression-Objekte für jeden der Parameter (sofern vorhanden) im Lambdaausdruck mithilfe der Parameter-Factorymethode.

    Dim x As ParameterExpression = Parameter(GetType(String), "x")
    
  • Erstellen Sie den Text von LambdaExpression mit den von Ihnen definierten ParameterExpressions und den Factorymethoden unter Expression. Ein Ausdruck, der x.StartsWith("a") darstellt, könnte beispielsweise wie folgt erstellt werden:

    Dim body As Expression = [Call](
        x,
        GetType(String).GetMethod("StartsWith", {GetType(String)}),
        Constant("a")
    )
    
  • Schließen Sie Parameter und Textkörper mithilfe der Überladung der Lambda-Factorymethode in ein zur Kompilierzeit typisiertes Expression (Of TDelegate)-Objekt ein:

    Dim expr As Expression(Of Func(Of String, Boolean)) =
        Lambda(Of Func(Of String, Boolean))(body, x)
    

In den folgenden Abschnitten wird ein Szenario beschrieben, in dem Sie ein Expression(Of TDelegate)-Objekt erstellen, das an eine LINQ-Methode übergeben werden soll. Außerdem wird ein vollständiges Beispiel für die Erstellung des Objekts mithilfe der Factorymethoden geliefert.

Szenario

Angenommen, Sie haben mehrere Entitätstypen:

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

Diese Entitätstypen sollen nun gefiltert und nur die Entitäten zurückgegeben werden, die einen bestimmten Text in einem der string-Felder aufweisen. Für Person sollen die Eigenschaften FirstName und LastName durchsucht werden:

' Dim term = ...
Dim personsQry = (New List(Of Person)).AsQueryable.
    Where(Function(x) x.FirstName.Contains(term) OrElse x.LastName.Contains(term))

Für Car soll jedoch nur die Eigenschaft Model durchsucht werden:

' Dim term = ...
Dim carsQry = (New List(Of Car)).AsQueryable.
    Where(Function(x) x.Model.Contains(term))

Sie können zwar eine benutzerdefinierte Funktion für IQueryable(Of Person) und eine weitere für IQueryable(Of Car) schreiben, die folgende Funktion fügt diese Filter jedoch unabhängig vom jeweiligen Elementtyp zu jeder vorhandenen Abfrage hinzu.

Beispiel

' 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

Da die TextFilter-Funktion eine IQueryable(Of T)-Schnittstelle (und nicht nur eine IQueryable-Schnittstelle) verwendet und zurückgibt, können Sie weitere zur Kompilierzeit typisierte Abfrageelemente nach dem Textfilter hinzufügen.

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)

Hinzufügen von Methodenaufrufknoten zur Ausdrucksbaumstruktur von IQueryable

Wenn Sie IQueryable anstelle von IQueryable(Of T) verwenden, können Sie die generischen LINQ-Methoden nicht direkt aufrufen. Eine Alternative besteht darin, die innere Ausdrucksbaumstruktur wie oben zu erstellen und mithilfe der Reflexion die entsprechenden LINQ-Methode aufzurufen, während die Ausdrucksbaumstruktur übergeben wird.

Sie können auch die Funktionalität der LINQ-Methode duplizieren, indem Sie die gesamte Struktur in einem MethodCallExpression-Objekt umschließen, das einen Aufruf der LINQ-Methode darstellt:

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

In diesem Fall ist zur Kompilierzeit kein generischer T-Platzhalter verfügbar. Aus diesem Grund verwenden Sie die Lambda-Überladung, die keine Typinformationen zur Kompilierzeit benötigt und LambdaExpression anstelle eines Expression(Of TDelegate)-Objekts erzeugt.

Die dynamische LINQ-Bibliothek

Das Erstellen von Ausdrucksbaumstrukturen mit Factorymethoden ist relativ komplex. Es ist einfacher, Zeichenfolgen zu verfassen. Die dynamische LINQ-Bibliothek macht eine Reihe von Erweiterungsmethoden für IQueryable verfügbar, die den LINQ-Standardmethoden in Queryable entsprechen und die Zeichenfolgen in einer besonderen Syntax anstelle von Ausdrucksbaumstrukturen akzeptieren. Die Bibliothek generiert die entsprechende Ausdrucksbaumstruktur aus der Zeichenfolge und kann die resultierende übersetzte Schnittstelle IQueryable zurückgeben.

Das vorherige Beispiel (einschließlich der Ausdrucksbaumkonstruktion) könnte beispielsweise wie folgt umgeschrieben werden:

' 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

Siehe auch