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

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), Actionof 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

Zie ook