Dela via


Köra frågor baserat på körningstillstånd (Visual Basic)

Överväg kod som definierar en IQueryable eller en IQueryable(Av T) mot en datakälla:

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)

Varje gång du kör den här koden körs samma exakta fråga. Detta är ofta inte särskilt användbart eftersom du kanske vill att koden ska köra olika frågor beroende på villkor vid körning. Den här artikeln beskriver hur du kan köra en annan fråga baserat på körningstillstånd.

IQueryable/IQueryable(Av T) och uttrycksträd

I grunden har en IQueryable två komponenter:

  • Expression— en språk- och datakälla-agnostisk representation av den aktuella frågans komponenter, i form av ett uttrycksträd.
  • Provider– en instans av en LINQ-provider som vet hur den aktuella frågan ska materialiseras till ett värde eller en uppsättning värden.

I samband med dynamiska frågor förblir providern vanligtvis densamma. frågeuttrycksträdet skiljer sig från fråga till fråga.

Uttrycksträd är oföränderliga. Om du vill ha ett annat uttrycksträd – och därmed en annan fråga – måste du översätta det befintliga uttrycksträdet till ett nytt och därmed till ett nytt IQueryable.

I följande avsnitt beskrivs specifika tekniker för att fråga på olika sätt som svar på körningstillstånd:

Använda körningstillstånd inifrån uttrycksträdet

Förutsatt att LINQ-providern stöder det är det enklaste sättet att fråga dynamiskt att referera till körningstillståndet direkt i frågan via en sluten variabel, till exempel length i följande kodexempel:

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

Det interna uttrycksträdet – och därmed frågan – har inte ändrats. frågan returnerar olika värden bara på grund av length att värdet för har ändrats.

Anropa ytterligare LINQ-metoder

I allmänhet utför de inbyggda LINQ-metoderna Queryable två steg:

Du kan ersätta den ursprungliga frågan med resultatet av en IQueryable(Av T)-returnerande metod för att hämta en ny fråga. Du kan göra detta villkorligt baserat på körningstillstånd, som i följande exempel:

' Dim sortByLength As Boolean  = ...

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

Variera uttrycksträdet som skickas till LINQ-metoderna

Du kan skicka in olika uttryck till LINQ-metoderna, beroende på körningstillstånd:

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

Du kanske också vill skriva de olika underuttrycken med hjälp av ett bibliotek från tredje part, till exempel LinqKits PredicateBuilder:

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

Konstruera uttrycksträd och frågor med hjälp av fabriksmetoder

I alla exempel hittills har vi känt till elementtypen vid kompileringstillfället –String och därmed frågans typ –IQueryable(Of String) . Du kan behöva lägga till komponenter i en fråga av valfri elementtyp eller lägga till olika komponenter beroende på elementtyp. Du kan skapa uttrycksträd från grunden med hjälp av fabriksmetoderna på System.Linq.Expressions.Expressionoch på så sätt anpassa uttrycket vid körning till en viss elementtyp.

Konstruera ett uttryck(av TDelegate)

När du skapar ett uttryck för att överföra till någon av LINQ-metoderna skapar du faktiskt en instans av Expression(Of TDelegate), där TDelegate är någon delegattyp som Func(Of String, Boolean), Actioneller en anpassad ombudstyp.

Expression(Of TDelegate) ärver från LambdaExpression, som representerar ett fullständigt lambda-uttryck som följande:

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

A LambdaExpression har två komponenter:

  • En parameterlista –(x As String) som representeras av egenskapen Parameters .
  • En brödtext –x.StartsWith("a") representerad av egenskapen Body .

De grundläggande stegen i att konstruera ett uttryck (av TDelegate) är följande:

  • Definiera ParameterExpression objekt för var och en av parametrarna (om några) i lambda-uttrycket med hjälp Parameter av fabriksmetoden.

    Dim x As ParameterExpression = Parameter(GetType(String), "x")
    
  • Konstruera brödtexten i , LambdaExpressionmed hjälp av de ParameterExpression(er) som du har definierat och fabriksmetoderna på Expression. Ett uttryck som representerar x.StartsWith("a") kan till exempel konstrueras så här:

    Dim body As Expression = [Call](
        x,
        GetType(String).GetMethod("StartsWith", {GetType(String)}),
        Constant("a")
    )
    
  • Omslut parametrarna och brödtexten i ett kompilerat uttryck (av TDelegate) med lämplig Lambda överlagring av fabriksmetoden:

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

I följande avsnitt beskrivs ett scenario där du kanske vill skapa ett uttryck (av TDelegate) för att överföra till en LINQ-metod och ge ett fullständigt exempel på hur du gör det med hjälp av fabriksmetoderna.

Scenario

Anta att du har flera entitetstyper:

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

För någon av dessa entitetstyper vill du bara filtrera och returnera de entiteter som har en viss text i ett av deras string fält. För Personvill du söka i FirstName egenskaperna och LastName :

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

Men för Carvill du bara Model söka efter egenskapen:

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

Du kan skriva en anpassad funktion för IQueryable(Of Person) och en annan för IQueryable(Of Car), men följande funktion lägger till den här filtreringen i alla befintliga frågor, oavsett vilken typ av element som är specifik.

Exempel

' 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

TextFilter Eftersom funktionen tar och returnerar en IQueryable(Av T) (och inte bara en IQueryable) kan du lägga till ytterligare kompilerade frågeelement efter textfiltret.

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)

Lägga till metodanropsnoder i uttrycksträdet IQueryable

Om du har en IQueryable i stället för en IQueryable(Av T), kan du inte anropa de allmänna LINQ-metoderna direkt. Ett alternativ är att skapa det inre uttrycksträdet enligt ovan och använda reflektion för att anropa lämplig LINQ-metod när du skickar in uttrycksträdet.

Du kan också duplicera LINQ-metodens funktioner genom att omsluta hela trädet i ett MethodCallExpression som representerar ett anrop till LINQ-metoden:

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

I det här fallet har du ingen allmän platshållare för kompileringstid T , så du använder den Lambda överlagring som inte kräver information av kompileringstyp och som genererar en LambdaExpression i stället för ett uttryck (av TDelegate).

Det dynamiska LINQ-biblioteket

Det är relativt komplext att konstruera uttrycksträd med hjälp av fabriksmetoder. det är lättare att skriva strängar. Det dynamiska LINQ-biblioteket exponerar en uppsättning tilläggsmetoder som IQueryable motsvarar linq-standardmetoderna i Queryable, och som accepterar strängar i en särskild syntax i stället för uttrycksträd. Biblioteket genererar lämpligt uttrycksträd från strängen och kan returnera den resulterande översatta IQueryable.

Till exempel kan det föregående exemplet (inklusive uttryckets trädkonstruktion) skrivas om på följande sätt:

' 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

Se även