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