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.

Alberi delle espressioni e IQueryable/IQueryable(Of T)

Fondamentalmente, un oggetto 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 materializzare la query corrente in un valore o un set 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 non sono modificabili; se si vuole un albero delle espressioni diverso, e quindi una query diversa, sarà necessario convertire l'albero delle espressioni esistente in un nuovo albero delle espressioni e quindi in un nuovo oggetto 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 delle espressioni Expression(Of TDelegate) usando i metodi factory in Expression
  • Aggiungere nodi di chiamata al metodo a un albero delle espressioni di 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 in Queryable vengono eseguiti in due passaggi:

  • Eseguire il wrapping dell'albero delle espressioni corrente in un oggetto MethodCallExpression che rappresenta la chiamata al metodo.
  • Passare di nuovo l'albero delle espressioni di cui è stato eseguito il wrapping al provider, per restituire un valore tramite il metodo IQueryProvider.Execute del provider, 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 query, IQueryable(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 adattare l'espressione in fase di esecuzione 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 esempio Func(Of String, Boolean), Action o 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")

Un oggetto LambdaExpression ha due componenti:

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

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

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

    Dim x As ParameterExpression = Parameter(GetType(String), "x")
    
  • Costruire il corpo di LambdaExpression, usando l’oggetto o gli oggetti ParameterExpression definiti e i metodi factory 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")
    )
    
  • Eseguire il wrapping dei parametri e del corpo in un'espressione di tipo compile-time(Of TDelegate), usando l'overload del metodo factory Lambda appropriato:

    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 (Of TDelegate) da passare a un metodo LINQ e fornire un esempio completo di come eseguire questa operazione usando i metodi factory.

Scenario

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 campi string. Per Person, è consigliabile eseguire ricerche nelle proprietà FirstName 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 proprietà Model:

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

Anche se è possibile scrivere una funzione personalizzata per IQueryable(Of Person) e un'altra per 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 oggetto IQueryable(Of T) (e non solo IQueryable), è possibile aggiungere altri elementi di query tipizzato 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 nodi di chiamata al 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 oggetto MethodCallExpression 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 LambdaExpression invece di un oggetto Expression(Of TDelegate).

Libreria LINQ dinamica

La costruzione di alberi delle espressioni usando i metodi factory è relativamente complessa; è più facile comporre stringhe. La libreria LINQ dinamica espone un set di metodi di estensione su IQueryable corrispondenti ai metodi LINQ standard in Queryable e 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

Vedi anche