Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
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 suele no ser muy útil, ya que es posible que desee que el código ejecute consultas diferentes en función de las condiciones en tiempo de ejecución. En este artículo se describe cómo puede ejecutar una consulta diferente en función del estado en tiempo de ejecución.
IQueryable/ IQueryable(Of T) y árboles de expresión
Fundamentalmente, un 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 un conjunto de valores.
En el contexto de la consulta dinámica, el proveedor normalmente seguirá siendo el mismo; El árbol de expresión de la consulta diferirá de la consulta a la consulta.
Los árboles de expresión son inmutables; si desea un árbol de expresión diferente (y, por tanto, una consulta diferente), deberá traducir el árbol de expresiones existente a uno nuevo y, por tanto, a un nuevo IQueryable.
En las secciones siguientes se describen técnicas específicas para realizar consultas de forma diferente en respuesta al estado en tiempo de ejecución:
- Uso del estado en tiempo de ejecución desde el árbol de expresiones
- Llamada a métodos de LINQ adicionales
- Variar el árbol de expresiones pasado a los métodos LINQ
- Construir un árbol de expresiones Expression(Of TDelegate) mediante los métodos de fábrica en Expression
- Agrega nodos de llamada de método al árbol de expresiones de un IQueryable
- Construir cadenas y usar la biblioteca LINQ dinámica
Uso del estado en tiempo de ejecución desde el árbol de expresiones
Suponiendo que el proveedor LINQ lo admite, la manera más sencilla de consultar dinámicamente es hacer referencia al estado en tiempo de ejecución directamente en la consulta a través de 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
Árbol de expresión interna (y, por tanto, la consulta) no se ha modificado; la consulta devuelve valores diferentes solo porque se ha cambiado el valor de length
.
Llamada a métodos de LINQ adicionales
Por lo general, los métodos LINQ integrados en Queryable realizan dos pasos:
- Envuelva el árbol de expresión actual en un MethodCallExpression que representa la llamada al método.
- Vuelva a pasar el árbol de expresiones encapsulado al proveedor, ya sea para devolver un valor a través del método del IQueryProvider.Execute proveedor; o para devolver un objeto de consulta traducido a través del IQueryProvider.CreateQuery método .
Puede reemplazar la consulta original por el resultado de un método IQueryable(Of T) que devuelve para obtener una nueva consulta. Puede hacerlo condicionalmente en función del estado en tiempo 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)
Variar el árbol de expresiones pasado a los métodos LINQ
Puede pasar expresiones diferentes a los métodos LINQ, en función del estado en tiempo 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 componer 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 hasta este momento, hemos 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 tenga que agregar componentes a una consulta de cualquier tipo de elemento o agregar distintos componentes en función del tipo de elemento. Puede crear árboles de expresión desde cero mediante los métodos de fábrica en System.Linq.Expressions.Expressiony, por tanto, adaptar la expresión en tiempo de ejecución a un tipo de elemento específico.
Construir una expresión (de TDelegate)
Al construir una expresión para pasar a uno de los métodos LINQ, realmente está construyendo una instancia de Expression(Of TDelegate), donde TDelegate
es algún tipo de delegado como Func(Of String, Boolean)
, Action
o un tipo de delegado personalizado.
Expression(Of 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")
Un LambdaExpression tiene dos componentes.
- Una lista de parámetros,
(x As String)
representada por la Parameters propiedad . - Un cuerpo (
x.StartsWith("a")
) representado por la propiedad Body.
Los pasos básicos para construir una expresión (de TDelegate) son los siguientes:
Defina ParameterExpression objetos para cada uno de los parámetros (si los hay) en la expresión lambda mediante el método de fábrica 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 representa
x.StartsWith("a")
podría construirse 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 desee construir una expresión (de TDelegate) para pasar a un método LINQ y proporcionar un ejemplo completo de cómo hacerlo mediante los métodos de fábrica.
Escenario
Supongamos que tenéis 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
Para cualquiera de estos tipos de entidad, desea filtrar y devolver solo las entidades que tienen un texto determinado dentro de uno de sus string
campos. Para Person
, querrá 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 puede 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, independientemente 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 IQueryableárbol de expresiones
Si tiene un IQueryable en lugar de un IQueryable(Of T), no puede llamar directamente a los métodos 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 funcionalidad del método LINQ ajustando todo el árbol en un MethodCallExpression que representa una llamada al método 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).
La biblioteca LINQ dinámica
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 LINQ dinámica expone un conjunto de métodos de extensión en IQueryable correspondientes a los métodos LINQ estándar en Queryable, y que acepta cadenas en una sintaxis especial en lugar de árboles de expresión. La biblioteca genera el árbol de expresión adecuado de la cadena y puede devolver el resultado traducido IQueryable.
Por ejemplo, el ejemplo anterior (incluida la construcción del árbol de expresión) podría volver a escribirse 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