Partilhar via


Consultando com base no estado de tempo de execução (Visual Basic)

Considere o código que define um IQueryable ou um IQueryable(Of T) contra 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 quiseres uma árvore de expressão diferente e, portanto, uma consulta diferente, precisarás traduzir a árvore de expressão existente para uma nova, e assim criar 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 árvore de expressão do IQueryable
  • 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 integrados executam Queryable duas etapas:

  • Envolva a árvore de expressão atual em um MethodCallExpression que represente a chamada de 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), Actionou 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 objetos ParameterExpression para cada um dos parâmetros (se houver) usando o método de fábrica Parameter na expressão lambda.

    Dim x As ParameterExpression = Parameter(GetType(String), "x")
    
  • Constrói o corpo do teu LambdaExpression, usando o(s) ParameterExpression que definiste, 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 "Expression(Of TDelegate)" digitado em tempo de compilação, usando a sobrecarga apropriada do método de criação :

    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, você deseja pesquisar as propriedades FirstName 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, gostaria de pesquisar apenas a Model propriedade:

' 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 à árvore de expressão do IQueryable.

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

Neste caso, não tens um espaço reservado genérico em tempo T de compilação, por isso utilizarás a Lambda sobrecarga que não requer informação sobre o tipo em tempo de compilação e que produz um LambdaExpression em vez de uma Expression(Of TDelegate).

A biblioteca de 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 sobre IQueryable, correspondentes aos métodos padrão LINQ em Queryable, e que aceitam strings de texto 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

Ver também