Condividi tramite


Esecuzione di query in base allo stato di runtime (Visual Basic)

Si consideri il codice che definisce un oggetto IQueryable o un oggetto IQueryable(Of T) rispetto a un'origine dati:

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)

Ogni volta che si esegue questo codice, verrà eseguita la stessa query esatta. Questo non è spesso molto utile, perché è possibile che il codice esegua query diverse a seconda delle condizioni in fase di esecuzione. Questo articolo descrive come eseguire una query diversa in base allo stato di runtime.

IQueryable/IQueryable(Of T) e alberi delle espressioni

Fondamentalmente, un IQueryable ha due componenti:

  • Expression— una rappresentazione indipendente dal linguaggio e dall'origine dati dei componenti della query corrente, sotto forma di albero delle espressioni.
  • Provider: un'istanza di un provider LINQ, che sa come convertire la query corrente in un valore o un insieme di valori.

Nel contesto dell'esecuzione dinamica di query, il provider rimarrà in genere lo stesso; L'albero delle espressioni della query sarà diverso dalla query alla query.

Gli alberi delle espressioni sono immutabili; se si desidera un albero delle espressioni diverso, e quindi una query diversa, è necessario convertire l'albero delle espressioni esistente in uno nuovo, e dunque ottenere un nuovo IQueryable.

Le sezioni seguenti descrivono tecniche specifiche per l'esecuzione di query in modo diverso in risposta allo stato di runtime:

  • Usare lo stato di runtime dall'interno dell'albero delle espressioni
  • Chiamare metodi LINQ aggiuntivi
  • Variare l'albero delle espressioni passato nei metodi LINQ
  • Costruire un albero dell'espressione Expression(Of TDelegate) usando i metodi factory in Expression
  • Aggiungere nodi di chiamata di metodo all'albero delle espressioni di un IQueryable.
  • Costruire stringhe e usare la libreria LINQ dinamica

Usare lo stato di runtime dall'interno dell'albero delle espressioni

Supponendo che il provider LINQ lo supporti, il modo più semplice per eseguire query in modo dinamico consiste nel fare riferimento allo stato di runtime direttamente nella query tramite una variabile closed-over, ad esempio length nell'esempio di codice seguente:

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

L'albero delle espressioni interne, e quindi la query, non sono state modificate; la query restituisce valori diversi solo perché il valore di length è stato modificato.

Chiamare metodi LINQ aggiuntivi

In genere, i metodi LINQ predefiniti vengono Queryable eseguiti in due passaggi:

  • Avvolgere l'albero di espressioni corrente in un MethodCallExpression che rappresenta la chiamata al metodo.
  • Passare nuovamente l'albero delle espressioni di cui è stato eseguito il wrapping al provider, per restituire un valore tramite il metodo del provider IQueryProvider.Execute, oppure per restituire un oggetto query convertito tramite il metodo IQueryProvider.CreateQuery.

È possibile sostituire la query originale con il risultato di un metodo IQueryable(Of T)-returning, per ottenere una nuova query. È possibile eseguire questa operazione in modo condizionale in base allo stato di runtime, come nell'esempio seguente:

' Dim sortByLength As Boolean  = ...

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

Variare l'albero delle espressioni passato nei metodi LINQ

È possibile passare espressioni diverse ai metodi LINQ, a seconda dello stato di runtime:

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

È anche possibile comporre le varie sottoespressioni usando una libreria di terze parti, ad esempio PredicateBuilder di 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)

Costruire alberi delle espressioni e query usando i metodi factory

In tutti gli esempi fino a questo punto, è stato noto il tipo di elemento in fase di compilazione,String e quindi il tipo della queryIQueryable(Of String). Potrebbe essere necessario aggiungere componenti a una query di qualsiasi tipo di elemento o aggiungere componenti diversi a seconda del tipo di elemento. È possibile creare alberi delle espressioni da zero, usando i metodi factory in System.Linq.Expressions.Expression e quindi personalizzare l'espressione in fase di esecuzione per adattarla a un tipo di elemento specifico.

Costruzione di un'espressione (di TDelegate)

Quando si costruisce un'espressione da passare a uno dei metodi LINQ, si costruisce effettivamente un'istanza di Expression(Of TDelegate), dove TDelegate è un tipo delegato, ad Func(Of String, Boolean)esempio , Actiono un tipo delegato personalizzato.

Expression(Of TDelegate) eredita da LambdaExpression, che rappresenta un'espressione lambda completa simile alla seguente:

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

A LambdaExpression ha due componenti:

  • Elenco di parametri,(x As String) rappresentato dalla Parameters proprietà .
  • Un corpo—x.StartsWith("a")—rappresentato dalla proprietà Body.

I passaggi di base per la creazione di un'espressione (Di TDelegate) sono i seguenti:

  • Definire ParameterExpression gli oggetti per ognuno dei parametri (se presenti) nell'espressione lambda, usando il Parameter metodo factory.

    Dim x As ParameterExpression = Parameter(GetType(String), "x")
    
  • Costruisci il corpo del LambdaExpression, utilizzando i ParameterExpression(s) che hai definito e i metodi di fabbrica in Expression. Ad esempio, un'espressione che rappresenta x.StartsWith("a") può essere costruita come segue:

    Dim body As Expression = [Call](
        x,
        GetType(String).GetMethod("StartsWith", {GetType(String)}),
        Constant("a")
    )
    
  • Incorpora i parametri e il corpo in una Expression(Of TDelegate) tipizzata a tempo di compilazione, utilizzando il sovraccarico del metodo factory appropriato Lambda.

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

Le sezioni seguenti descrivono uno scenario in cui è possibile creare un'espressione (Di TDelegate) da passare a un metodo LINQ e fornire un esempio completo di come eseguire questa operazione usando i metodi factory.

Sceneggiatura

Si supponga di avere più tipi di entità:

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

Per uno di questi tipi di entità, è necessario filtrare e restituire solo le entità con un testo specificato all'interno di uno dei relativi string campi. Per Person, è consigliabile eseguire ricerche nelle FirstName proprietà e LastName :

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

Ma per Car, si vuole cercare solo la Model proprietà :

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

Anche se è possibile scrivere una funzione personalizzata per e un'altra per IQueryable(Of Person)IQueryable(Of Car), la funzione seguente aggiunge questo filtro a qualsiasi query esistente, indipendentemente dal tipo di elemento specifico.

Esempio

' 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

Poiché la funzione TextFilter accetta e restituisce un IQueryable(Of T) (e non solo un IQueryable), è possibile aggiungere ulteriori elementi di query tipizzati in fase di compilazione dopo il filtro di testo.

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)

Aggiungere i nodi di chiamata di metodo all'albero delle espressioni di IQueryable

Se si dispone di un oggetto IQueryable invece di IQueryable(Of T), non è possibile chiamare direttamente i metodi LINQ generici. Un'alternativa consiste nel compilare l'albero delle espressioni interne come sopra e usare la reflection per richiamare il metodo LINQ appropriato durante il passaggio dell'albero delle espressioni.

È anche possibile duplicare la funzionalità del metodo LINQ eseguendo il wrapping dell'intero albero in un MethodCallExpression oggetto che rappresenta una chiamata al metodo LINQ:

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 questo caso non si dispone di un segnaposto generico T in fase di compilazione, quindi si userà l'overload Lambda che non richiede informazioni sul tipo in fase di compilazione e che produce un oggetto LambdaExpression invece di un oggetto Expression(Of TDelegate).

Libreria LINQ dinamica

La costruzione di alberi delle espressioni utilizzando i metodi delle fabbriche è relativamente complessa; è più facile assemblare stringhe. La libreria LINQ dinamica espone un set di metodi IQueryable di estensione corrispondenti ai metodi LINQ standard in Queryablee che accettano stringhe in una sintassi speciale anziché alberi delle espressioni. La libreria genera l'albero delle espressioni appropriato dalla stringa e può restituire il risultato tradotto IQueryable.

Ad esempio, l'esempio precedente (inclusa la costruzione dell'albero delle espressioni) può essere riscritto come segue:

' 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

Vedere anche