Query's uitvoeren op basis van runtimestatus (Visual Basic)
Houd rekening met code die een IQueryable of een IQueryable(of T) definieert voor een gegevensbron:
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)
Telkens wanneer u deze code uitvoert, wordt dezelfde exacte query uitgevoerd. Dit is vaak niet erg nuttig, omdat u mogelijk wilt dat uw code verschillende query's uitvoert, afhankelijk van de omstandigheden tijdens de runtime. In dit artikel wordt beschreven hoe u een andere query kunt uitvoeren op basis van runtimestatus.
IQueryable /IQueryable(of T) en expressiestructuren
Een heeft in principe IQueryable twee onderdelen:
- Expression— een taal- en gegevensbronagnostische weergave van de onderdelen van de huidige query, in de vorm van een expressiestructuur.
- Provider— een exemplaar van een LINQ-provider, die weet hoe de huidige query moet worden gerealiseerd in een waarde of set waarden.
In de context van dynamische query's blijft de provider meestal hetzelfde; de expressiestructuur van de query verschilt van query naar query.
Expressiestructuren zijn onveranderbaar; als u een andere expressiestructuur en dus een andere query wilt, moet u de bestaande expressiestructuur vertalen naar een nieuwe, en dus naar een nieuwe IQueryable.
In de volgende secties worden specifieke technieken beschreven voor het opvragen van query's op een andere manier als reactie op runtimestatus:
- Runtimestatus gebruiken vanuit de expressiestructuur
- Aanvullende LINQ-methoden aanroepen
- De expressiestructuur variëren die is doorgegeven aan de LINQ-methoden
- Een expressiestructuur (van TDelegate) maken met behulp van de factory-methoden opExpression
- Methodeaanroepknooppunten toevoegen aan een IQueryableexpressiestructuur
- Tekenreeksen maken en de dynamische LINQ-bibliotheek gebruiken
Runtimestatus gebruiken vanuit de expressiestructuur
Ervan uitgaande dat de LINQ-provider dit ondersteunt, is de eenvoudigste manier om dynamisch te zoeken naar de runtimestatus rechtstreeks in de query via een gesloten-overvariabele, zoals length
in het volgende codevoorbeeld:
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
De interne expressiestructuur en dus de query zijn niet gewijzigd; de query retourneert alleen verschillende waarden omdat de waarde is length
gewijzigd.
Aanvullende LINQ-methoden aanroepen
Over het algemeen voeren de ingebouwde LINQ-methodenQueryable bij het uitvoeren van twee stappen uit:
- Verpakt de huidige expressiestructuur in een MethodCallExpression weergave van de methode-aanroep.
- Geef de structuur van de verpakte expressie weer door aan de provider om een waarde te retourneren via de methode van IQueryProvider.Execute de provider of om een vertaald queryobject via de IQueryProvider.CreateQuery methode te retourneren.
U kunt de oorspronkelijke query vervangen door het resultaat van een IQueryable(of T)-retourmethode om een nieuwe query op te halen. U kunt dit voorwaardelijk doen op basis van de runtimestatus, zoals in het volgende voorbeeld:
' Dim sortByLength As Boolean = ...
Dim qry = companyNamesSource
If sortByLength Then qry = qry.OrderBy(Function(x) x.Length)
De expressiestructuur variëren die is doorgegeven aan de LINQ-methoden
U kunt verschillende expressies doorgeven aan de LINQ-methoden, afhankelijk van de runtimestatus:
' 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)
U kunt ook de verschillende subexpressies opstellen met behulp van een bibliotheek van derden, zoals PredicateBuilder van 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)
Expressiestructuren en query's maken met behulp van factory-methoden
In alle voorbeelden tot nu toe kennen we het elementtype tijdens de compilatie (String
en dus het type van de query).IQueryable(Of String)
Mogelijk moet u onderdelen toevoegen aan een query van elk elementtype of verschillende onderdelen toevoegen, afhankelijk van het elementtype. U kunt expressiestructuren vanaf de grond maken met behulp van de fabrieksmethoden op System.Linq.Expressions.Expression, en zo de expressie tijdens runtime aanpassen aan een specifiek elementtype.
Een expressie maken (van TDelegate)
Wanneer u een expressie maakt die moet worden doorgegeven aan een van de LINQ-methoden, maakt u eigenlijk een exemplaar van Expressie (van TDelegate), waarbij TDelegate
een bepaald type gedelegeerde is, zoals Func(Of String, Boolean)
, Action
of een aangepast gemachtigdetype.
Expression(of TDelegate) neemt over LambdaExpressionvan , dat een volledige lambda-expressie vertegenwoordigt als de volgende:
Dim expr As Expression(Of Func(Of String, Boolean)) = Function(x As String) x.StartsWith("a")
A LambdaExpression heeft twee onderdelen:
- Een lijst met parameters,
(x As String)
vertegenwoordigd door de Parameters eigenschap. - Een hoofdtekst,
x.StartsWith("a")
vertegenwoordigd door de Body eigenschap.
De basisstappen voor het maken van een expressie (van TDelegate) zijn als volgt:
Definieer ParameterExpression objecten voor elk van de parameters (indien aanwezig) in de lambda-expressie, met behulp van de Parameter factory-methode.
Dim x As ParameterExpression = Parameter(GetType(String), "x")
Bouw de hoofdtekst van uw LambdaExpression, met behulp van de ParameterExpression(s) die u hebt gedefinieerd, en de fabrieksmethoden op Expression. Een expressie die wordt weergegeven
x.StartsWith("a")
, kan bijvoorbeeld als volgt worden samengesteld:Dim body As Expression = [Call]( x, GetType(String).GetMethod("StartsWith", {GetType(String)}), Constant("a") )
Wrap the parameters and body in a compile-time-typed Expression(Of TDelegate), using the appropriate Lambda factory method overload:
Dim expr As Expression(Of Func(Of String, Boolean)) = Lambda(Of Func(Of String, Boolean))(body, x)
In de volgende secties wordt een scenario beschreven waarin u een expressie (van TDelegate) wilt maken om door te geven aan een LINQ-methode en een volledig voorbeeld van hoe u dit doet met behulp van de factory-methoden.
Scenario
Stel dat u meerdere entiteitstypen hebt:
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
Voor een van deze entiteitstypen wilt u alleen de entiteiten filteren en retourneren die een bepaalde tekst in een van hun string
velden hebben. Voor Person
, wilt u zoeken in de FirstName
en LastName
eigenschappen:
' Dim term = ...
Dim personsQry = (New List(Of Person)).AsQueryable.
Where(Function(x) x.FirstName.Contains(term) OrElse x.LastName.Contains(term))
Maar voor Car
, wilt u alleen de Model
eigenschap doorzoeken:
' Dim term = ...
Dim carsQry = (New List(Of Car)).AsQueryable.
Where(Function(x) x.Model.Contains(term))
Hoewel u een aangepaste functie voor en een andere kunt schrijven voor IQueryable(Of Person)
IQueryable(Of Car)
, voegt de volgende functie deze filtering toe aan een bestaande query, ongeacht het specifieke elementtype.
Opmerking
' 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
Omdat de TextFilter
functie een IQueryable(of T) (en niet alleen eenIQueryable) gebruikt en retourneert, kunt u na het tekstfilter nog meer gecompileerde queryelementen met tijdstypen toevoegen.
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)
Methode-aanroepknooppunten toevoegen aan de expressiestructuur van de IQueryable
Als u een IQueryable IQueryable (of T) hebt, kunt u de algemene LINQ-methoden niet rechtstreeks aanroepen. Een alternatief is om de binnenste expressiestructuur zoals hierboven te bouwen en weerspiegeling te gebruiken om de juiste LINQ-methode aan te roepen terwijl de expressiestructuur wordt doorgegeven.
U kunt ook de functionaliteit van de LINQ-methode dupliceren door de hele structuur te verpakken in een structuur die een MethodCallExpression aanroep naar de LINQ-methode vertegenwoordigt:
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 dit geval hebt u geen tijdelijke aanduiding voor compilatietijd T
, dus u gebruikt de Lambda overbelasting waarvoor geen informatie over het type compileertijd is vereist en die een LambdaExpressionexpressie (van TDelegate) produceert.
De dynamische LINQ-bibliotheek
Het bouwen van expressiestructuren met behulp van fabrieksmethoden is relatief complex; het is eenvoudiger om tekenreeksen op te stellen. De dynamische LINQ-bibliotheek bevat een set extensiemethoden die IQueryable overeenkomen met de standaard LINQ-methoden op Queryableen die tekenreeksen in een speciale syntaxis accepteren in plaats van expressiestructuren. De bibliotheek genereert de juiste expressiestructuur van de tekenreeks en kan de resulterende vertaalde IQueryablestructuur retourneren.
Het vorige voorbeeld (inclusief de constructie van de expressiestructuur) kan bijvoorbeeld als volgt worden herschreven:
' 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