Compartir a través de


Consultas basadas en el estado del entorno de ejecución (Visual Basic)

Ten en cuenta el código que define una interfaz IQueryable o IQueryable con respecto a un origen de datos:

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)

Cada vez que ejecute este código, se ejecutará la misma consulta exacta. Esto no suele ser muy útil, ya que es posible que quiera que el código ejecute otras consultas en función de las condiciones en tiempo de ejecución. En este artículo se describe cómo puede ejecutar otra consulta en función del estado del entorno de ejecución.

IQueryable/IQueryable (de T) y árboles de expresión

Fundamentalmente, una interfaz IQueryable tiene dos componentes:

  • Expression, una representación, independiente del lenguaje y del origen de datos, de los componentes de la consulta actual, en forma de árbol de expresión.
  • Provider, una instancia de un proveedor LINQ, que sabe cómo materializar la consulta actual en un valor o en un conjunto de valores.

En el contexto de las consultas dinámicas, el proveedor normalmente seguirá siendo el mismo; el árbol de expresión de la consulta variará entre consultas.

Los árboles de expresión son inmutables; si quieres otro árbol de expresión (y, por tanto, otra consulta), tendrás que convertir el existente en uno nuevo y, por tanto, en una nueva instancia de IQueryable.

En las secciones siguientes se describen técnicas específicas para realizar consultas de forma diferente en respuesta al estado del entorno de ejecución:

  • Uso del estado del entorno de ejecución desde el árbol de expresión
  • Llamada a métodos de LINQ adicionales
  • Variación del árbol de expresión que se pasa a los métodos de LINQ
  • Creación de un árbol de expresión Expresión (De TDelegate) con métodos de factorización en Expression
  • Adición de nodos de llamada de método al árbol de expresión de IQueryable
  • Construcción de cadenas y uso de la biblioteca dinámica de LINQ

Uso del estado del entorno de ejecución desde el árbol de expresión

Siempre que el proveedor LINQ lo admita, la manera más sencilla de realizar consultas dinámicas consiste en hacer referencia al estado del entorno de ejecución de forma directa en la consulta mediante una variable cerrada, como length en el ejemplo de código siguiente:

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

El árbol de expresión interno (y, por tanto, la consulta) no se han modificado; la consulta solo devuelve otros valores porque se ha cambiado el valor de length.

Llamada a métodos de LINQ adicionales

Por lo general, los métodos de LINQ integrados en Queryable realizan dos pasos:

  • Encapsulan el árbol de expresión actual en un elemento MethodCallExpression que representa la llamada de método.
  • Vuelven a pasar el árbol de expresión encapsulado al proveedor, ya sea para devolver un valor mediante el método IQueryProvider.Execute del proveedor, o bien para devolver un objeto de consulta traducido mediante el método IQueryProvider.CreateQuery.

Puedes reemplazar la consulta original con el resultado de un método que devuelva IQueryable (De T) para obtener una nueva consulta. Puede hacerlo condicionalmente en función del estado del entorno de ejecución, como en el ejemplo siguiente:

' Dim sortByLength As Boolean  = ...

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

Variación del árbol de expresión que se pasa a los métodos de LINQ

Puede pasar otras expresiones a los métodos de LINQ, en función del estado del entorno de ejecución:

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

También es posible que quiera crear las distintas subexpresiones mediante una biblioteca de terceros, como PredicateBuilder de 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)

Creación de árboles de expresión y consultas mediante métodos de generador

En todos los ejemplos vistos hasta ahora, se ha conocido el tipo de elemento en tiempo de compilación (String) y por tanto el tipo de la consulta (IQueryable(Of String)). Es posible que necesites agregar componentes a una consulta de cualquier tipo de elemento o agregar componentes diferentes, en función del tipo de elemento. Puede crear árboles de expresión desde cero, con los métodos de generador de System.Linq.Expressions.Expression y, por tanto, adaptar la expresión en tiempo de ejecución a un tipo de elemento específico.

Construir una expresión (de TDelegate)

Cuando se crea una expresión para pasarla a uno de los métodos LINQ, en realidad se crea una instancia de Expresión (De TDelegate) , en la que TDelegate es un tipo de delegado como Func(Of String, Boolean), Action o un tipo de delegado personalizado.

Expresión (De TDelegate) hereda de LambdaExpression, que representa una expresión lambda completa como la siguiente:

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

LambdaExpression tiene dos componentes:

  • Una lista de parámetros ((x As String)) representada por la propiedad Parameters.
  • Un cuerpo (x.StartsWith("a")) representado por la propiedad Body.

Los pasos básicos para crear una instancia de Expresión (De TDelegate) son los siguientes:

  • Defina objetos ParameterExpression para cada uno de los parámetros (si existen) de la expresión lambda, mediante el método generador Parameter.

    Dim x As ParameterExpression = Parameter(GetType(String), "x")
    
  • Construya el cuerpo de LambdaExpression utilizando los valores ParameterExpression definidos por el usuario y los métodos de generador en Expression. Por ejemplo, una expresión que represente x.StartsWith("a") se podría crear de la siguiente manera:

    Dim body As Expression = [Call](
        x,
        GetType(String).GetMethod("StartsWith", {GetType(String)}),
        Constant("a")
    )
    
  • Ajusta los parámetros y el cuerpo en una instancia de Expresión (De TDelegate) con tipo de tiempo de compilación, mediante la sobrecarga apropiada de Factory Method Lambda:

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

En las secciones siguientes se describe un escenario en el que es posible que quiera crear una instancia de Expresión (De TDelegate) para pasarla a un método LINQ, y se proporciona un ejemplo completo de cómo hacerlo mediante Factory Methods.

Escenario

Imagine que tiene varios tipos de entidad:

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

En cualquiera de estos tipos de entidad, quiere filtrar y devolver solo las entidades que contengan un texto concreto dentro de uno de sus campos string. Para Person, le interesa buscar las propiedades FirstName y LastName:

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

Pero para Car, solo quiere buscar la propiedad Model:

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

Aunque podría escribir una función personalizada para IQueryable(Of Person) y otra para IQueryable(Of Car), la siguiente función agrega este filtrado a cualquier consulta existente, con independencia del tipo de elemento específico.

Ejemplo

' 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

Como la función TextFilter toma y devuelve una interfaz IQueryable (De T) (y no solo una interfaz IQueryable), puedes agregar más elementos de consulta con tipo de tiempo de compilación después del filtro de texto.

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)

Adición de nodos de llamada de método al árbol de expresión de IQueryable

Si tienes una interfaz IQueryable en lugar de IQueryable (De T), no puedes llamar directamente a los métodos de LINQ genéricos. Una alternativa consiste en crear el árbol de expresión interno como se ha indicado antes y usar la reflexión para invocar el método de LINQ adecuado mientras se pasa el árbol de expresión.

También puede duplicar la función del método de LINQ y encapsular todo el árbol en un elemento MethodCallExpression que represente una llamada al método de 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

En este caso no tienes un marcador de posición genérico T en tiempo de compilación, por lo que usarás la sobrecarga de Lambda que no necesita información de tipos de tiempo de compilación y que genera un elemento LambdaExpression en lugar de una Expresión (De TDelegate).

Biblioteca dinámica de LINQ

La creación de árboles de expresión mediante métodos de generador es relativamente compleja; es más fácil crear cadenas. La biblioteca dinámica de LINQ expone un conjunto de métodos de extensión en IQueryable correspondiente a los métodos estándar de LINQ en Queryable y que aceptan cadenas en una sintaxis especial en lugar de árboles de expresión. La biblioteca genera el árbol de expresión adecuado a partir de la cadena y puede devolver la interfaz IQueryable traducida resultante.

El ejemplo anterior (incluida la construcción del árbol de expresión) se podría reescribir de la siguiente manera:

' 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

Consulte también