Delen via


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 expressiebomen

Een IQueryable heeft fundamenteel twee componenten:

  • 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 omgezet in een waarde of waardenset.

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 expressieboom

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 twee stappen uit:

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 construeren (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), Actionof een aangepast gemachtigdetype.

Expression(Of TDelegate) erft LambdaExpression van, dat een volledige lambda-expressie vertegenwoordigt zoals 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 lichaam—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 het lichaam van uw LambdaExpression op met behulp van de ParameterExpression(s) die u hebt gedefinieerd en de fabrieksmethoden bij Expression. Een expressie die x.StartsWith("a") vertegenwoordigt, kan bijvoorbeeld als volgt worden samengesteld:

    Dim body As Expression = [Call](
        x,
        GetType(String).GetMethod("StartsWith", {GetType(String)}),
        Constant("a")
    )
    
  • Omhul de parameters en het lichaam in een 'compile-time'-getypeerde Expression(Of TDelegate), door gebruik te maken van de juiste Lambda overbelasting van de fabrieksmethode.

    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.

Scenariobeschrijving

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 kunt schrijven voor IQueryable(Of Person) en een andere voor IQueryable(Of Car), voegt de volgende functie deze filtering toe aan elke bestaande query, ongeacht het specifieke elementtype.

Voorbeeld

' 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 een IQueryable) gebruikt en retourneert, kunt u na het tekstfilter nog meer compile-time getypeerde query-elementen 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 hebt in plaats van een IQueryable(Of T), kunt u de generieke 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 generieke tijdelijke aanduiding voor compilatietijd T, dus gebruikt u de Lambda overbelasting die geen informatie over het type tijdens compilatietijd vereist, en die een LambdaExpression produceert in plaats van een Expression(Of TDelegate).

De dynamische LINQ-bibliotheek

Het opbouwen van expressiebomen met behulp van fabrieksmethoden is relatief complex; het is eenvoudiger om tekenreeksen samen 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

Zie ook