Consultando com base no estado de tempo de execução (Visual Basic)
Considere o código que define um IQueryable IQueryable (Of T) em relação a uma fonte de dados:
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)
Toda vez que você executar esse código, a mesma consulta exata será executada. Isso frequentemente não é muito útil, pois você pode querer que seu código execute consultas diferentes, dependendo das condições em tempo de execução. Este artigo descreve como você pode executar uma consulta diferente com base no estado de tempo de execução.
IQueryable / IQueryable (Of T) e árvores de expressão
Fundamentalmente, um IQueryable tem duas componentes:
- Expression—uma representação independente de linguagem e fonte de dados dos componentes da consulta atual, na forma de uma árvore de expressão.
- Provider—uma instância de um provedor LINQ, que sabe como materializar a consulta atual em um valor ou conjunto de valores.
No contexto da consulta dinâmica, o provedor geralmente permanecerá o mesmo; A árvore de expressões da consulta será diferente de consulta para consulta.
As árvores de expressão são imutáveis; Se você quiser uma árvore de expressão diferente e, portanto, uma consulta diferente, precisará traduzir a árvore de expressões existente para uma nova e, portanto, para uma nova IQueryable.
As seções a seguir descrevem técnicas específicas para consultar de forma diferente em resposta ao estado de tempo de execução:
- Usar o estado de tempo de execução de dentro da árvore de expressão
- Chamar métodos LINQ adicionais
- Variar a árvore de expressão passada para os métodos LINQ
- Construa uma árvore de expressão Expression(Of TDelegate) usando os métodos de fábrica em Expression
- Adicionar nós de chamada de método a uma IQueryableárvore de expressão do
- Construa cadeias de caracteres e use a biblioteca Dynamic LINQ
Usar o estado de tempo de execução de dentro da árvore de expressão
Supondo que o provedor LINQ ofereça suporte a ele, a maneira mais simples de consultar dinamicamente é fazer referência ao estado de tempo de execução diretamente na consulta por meio de uma variável fechada, como length
no exemplo de código a seguir:
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
A árvore de expressão interna — e, portanto, a consulta — não foram modificadas; A consulta retorna valores diferentes somente porque o valor de length
foi alterado.
Chamar métodos LINQ adicionais
Geralmente, os métodos LINQ internos executam Queryable duas etapas:
- Envolva a árvore de expressão atual em uma MethodCallExpression chamada que represente o método.
- Passe a árvore de expressão encapsulada de volta para o provedor, seja para retornar um valor por meio do método do IQueryProvider.Execute provedor ou para retornar um objeto de consulta traduzido por meio do IQueryProvider.CreateQuery método.
Você pode substituir a consulta original pelo resultado de um método de retorno IQueryable(Of T), para obter uma nova consulta. Você pode fazer isso condicionalmente com base no estado de tempo de execução, como no exemplo a seguir:
' Dim sortByLength As Boolean = ...
Dim qry = companyNamesSource
If sortByLength Then qry = qry.OrderBy(Function(x) x.Length)
Variar a árvore de expressão passada para os métodos LINQ
Você pode passar expressões diferentes para os métodos LINQ, dependendo do estado de tempo de execução:
' 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)
Você também pode querer compor as várias subexpressões usando uma biblioteca de terceiros, como o PredicateBuilder do 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)
Construir árvores de expressão e consultas usando métodos de fábrica
Em todos os exemplos até este ponto, conhecemos o tipo de elemento em tempo de compilação —String
e, portanto, o tipo da consulta —IQueryable(Of String)
. Talvez seja necessário adicionar componentes a uma consulta de qualquer tipo de elemento ou adicionar componentes diferentes, dependendo do tipo de elemento. Você pode criar árvores de expressão do zero, usando os métodos de fábrica em System.Linq.Expressions.Expression, e, assim, adaptar a expressão em tempo de execução para um tipo de elemento específico.
Construindo uma expressão(Of TDelegate)
Quando você constrói uma expressão para passar para um dos métodos LINQ, na verdade está construindo uma instância de Expression(Of TDelegate), onde TDelegate
é algum tipo de delegado, como Func(Of String, Boolean)
, Action
ou um tipo de delegado personalizado.
Expression(Of TDelegate) herda de LambdaExpression, que representa uma expressão lambda completa como a seguinte:
Dim expr As Expression(Of Func(Of String, Boolean)) = Function(x As String) x.StartsWith("a")
A LambdaExpression tem duas componentes:
- Uma lista de parâmetros—
(x As String)
—representada pela Parameters propriedade. - Um corpo—
x.StartsWith("a")
—representado pela Body propriedade.
As etapas básicas na construção de uma Expressão (Of TDelegate) são as seguintes:
Defina ParameterExpression objetos para cada um dos parâmetros (se houver) na expressão lambda, usando o Parameter método factory.
Dim x As ParameterExpression = Parameter(GetType(String), "x")
Construa o corpo do seu LambdaExpression, usando o ParameterExpression(s) que você definiu, e os métodos de fábrica em Expression. Por exemplo, uma expressão que representa
x.StartsWith("a")
poderia ser construída assim:Dim body As Expression = [Call]( x, GetType(String).GetMethod("StartsWith", {GetType(String)}), Constant("a") )
Envolva os parâmetros e o corpo em um compile-time-typed Expression(Of TDelegate), usando a sobrecarga de método de fábrica apropriada Lambda :
Dim expr As Expression(Of Func(Of String, Boolean)) = Lambda(Of Func(Of String, Boolean))(body, x)
As seções a seguir descrevem um cenário no qual você pode querer construir um Expression(Of TDelegate) para passar para um método LINQ e fornecer um exemplo completo de como fazer isso usando os métodos de fábrica.
Cenário
Digamos que você tenha vários tipos de entidade:
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 qualquer um desses tipos de entidade, você deseja filtrar e retornar apenas as entidades que têm um determinado texto dentro de um de seus string
campos. Para Person
o , você deseja pesquisar as FirstName
propriedades e LastName
:
' Dim term = ...
Dim personsQry = (New List(Of Person)).AsQueryable.
Where(Function(x) x.FirstName.Contains(term) OrElse x.LastName.Contains(term))
Mas para Car
o , você deve pesquisar apenas o Model
estabelecimento:
' Dim term = ...
Dim carsQry = (New List(Of Car)).AsQueryable.
Where(Function(x) x.Model.Contains(term))
Embora você possa escrever uma função personalizada para IQueryable(Of Person)
e outra para IQueryable(Of Car)
, a função a seguir adiciona essa filtragem a qualquer consulta existente, independentemente do tipo de elemento específico.
Exemplo
' 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 a TextFilter
função usa e retorna um IQueryable(Of T) (e não apenas um IQueryable), você pode adicionar mais elementos de consulta digitados em tempo de compilação após o 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)
Adicionar nós de chamada de método à IQueryableárvore de expressão do
Se você tiver um IQueryable em vez de um IQueryable(Of T), você não pode chamar diretamente os métodos LINQ genéricos. Uma alternativa é construir a árvore de expressão interna como acima e usar a reflexão para invocar o método LINQ apropriado ao passar na árvore de expressão.
Você também pode duplicar a funcionalidade do método LINQ, encapsulando toda a árvore em um MethodCallExpression que representa uma chamada para o 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
Nesse caso, você não tem um espaço reservado genérico em tempo T
de compilação, portanto, usará a Lambda sobrecarga que não requer informações de tipo em tempo de compilação e que produz um LambdaExpression em vez de um Expression(Of TDelegate).
A biblioteca Dynamic LINQ
A construção de árvores de expressão usando métodos de fábrica é relativamente complexa; é mais fácil compor cordas. A biblioteca Dynamic LINQ expõe um conjunto de métodos de extensão correspondentes IQueryable aos métodos LINQ padrão em Queryable, e que aceitam cadeias de caracteres em uma sintaxe especial em vez de árvores de expressão. A biblioteca gera a árvore de expressão apropriada a partir da cadeia de caracteres e pode retornar o resultado traduzido IQueryable.
Por exemplo, o exemplo anterior (incluindo a construção da árvore de expressão) pode ser reescrito da seguinte forma:
' 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