Запрос на основе состояния среды выполнения (Visual Basic)

Рассмотрим код, определяющий IQueryableили IQueryable(Of T) для источника данных:

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)

Каждый раз при выполнении этого кода выполняется один и тот же запрос. Зачастую это неэффективно, поскольку может потребоваться, чтобы код выполнял различные запросы в зависимости от условий во время выполнения. В этой статье описывается, как можно выполнить другой запрос на основе состояния среды выполнения.

IQueryable / IQueryable(Of T) и деревья выражений

В своей основе IQueryable имеет два компонента:

  • Expression— не зависят от языка и источника данных представление компонентов текущего запроса в виде дерева выражений.
  • Provider— экземпляр поставщика LINQ, который знает, как материализовать текущий запрос в значение или набор значений.

В контексте динамических запросов поставщик обычно остается неизменным; дерево выражения запроса будет отличаться от запроса к запросу.

Деревья выражений неизменяемы; Если требуется другое дерево выражений ( и, следовательно, другой запрос), необходимо перевести существующее дерево выражений в новое и таким образом в новое IQueryable.

В следующих разделах описываются конкретные методы запроса в зависимости от состояния среды выполнения.

Использование состояния среды выполнения из дерева выражений

Предположив, что поставщик LINQ поддерживает его, самый простой способ динамического запроса будет заключаться в ссылке на состояние среды выполнения непосредственно в запросе с помощью закрытой переменной, такой как length в следующем примере кода:

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

Дерево внутренних выражений (и таким образом запрос) не было изменено; запрос возвращает разные значения только из-за изменения значения length .

Вызов дополнительных методов LINQ

Как правило, встроенные методы LINQ в Queryable выполняют два действия:

  • Заключение текущего дерево выражения в оболочку в MethodCallExpression, который представляет вызов метода.
  • Передача инкапсулированного дерева выражения в поставщик, чтобы вернуть значение через метод IQueryProvider.Execute поставщика, либо чтобы вернуть объект переведенного запроса с помощью метода IQueryProvider.CreateQuery.

Исходный запрос можно заменить результатом метода IQueryable(Of T), чтобы получить новый запрос. Это можно сделать по условию на основе состояния среды выполнения, как показано в следующем примере:

' Dim sortByLength As Boolean  = ...

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

Изменение дерева выражения, переданного в методы LINQ

В методы LINQ можно передать разные выражения в зависимости от состояния среды выполнения:

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

Также может потребоваться составить различные подвыражения, используя сторонние библиотеки, такие как PredicateBuilder от 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)

Создание деревьев выражений и запросов с помощью фабричных методов

Во всех примерах до этой точки мы знали тип элемента во время компиляции (String и таким образом тип запроса).IQueryable(Of String) Может потребоваться добавить компоненты в запрос любого типа элемента или добавить различные компоненты в зависимости от типа элемента. Можно создавать деревья выражений с нуля, используя фабричные методы в System.Linq.Expressions.Expression и таким образом адаптировать выражение в среде выполнения к определенному типу элемента.

Создание выражения (of TDelegate)

При создании выражения для передачи в один из методов LINQ вы фактически создаете экземпляр Expression(Of TDelegate), где есть некоторый тип делегата, например ActionFunc(Of String, Boolean), или настраиваемый тип делегатаTDelegate.

Выражение (Of TDelegate) наследует от LambdaExpression, которое представляет полное лямбда-выражение, как показано ниже:

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

LambdaExpression имеет два компонента:

  • Список параметров,(x As String) представленный свойством Parameters .
  • Тело,x.StartsWith("a") представленное свойством Body .

Ниже приведены основные шаги по созданию выражения (Of TDelegate ).

  • Определите объекты ParameterExpression для каждого из параметров (если таковые имеются) в лямбда-выражении с помощью фабричного метода Parameter.

    Dim x As ParameterExpression = Parameter(GetType(String), "x")
    
  • Создайте текст LambdaExpression, используя заданные вами выражения ParameterExpression и фабричные методы в Expression. Например, выражение, представляющее x.StartsWith("a"), может быть построено следующим образом:

    Dim body As Expression = [Call](
        x,
        GetType(String).GetMethod("StartsWith", {GetType(String)}),
        Constant("a")
    )
    
  • Обтекайте параметры и текст в типизированное выражение во время компиляции (TDelegate) с помощью соответствующей Lambda перегрузки метода фабрики:

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

В следующих разделах описывается сценарий, в котором может потребоваться создать выражение (Of TDelegate) для передачи в метод LINQ и предоставить полный пример того, как это сделать с помощью методов фабрики.

Сценарий

Допустим, у вас есть несколько типов сущностей:

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

Для любого из этих типов сущностей необходимо отфильтровать и возвратить только те сущности, которые имеют заданный текст в одном из полей string. Для Person необходимо найти свойства FirstName и LastName:

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

Но для Car требуется найти только свойство Model:

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

Несмотря на то что можно написать одну настраиваемую функцию для IQueryable(Of Person) и другую для IQueryable(Of Car), следующая функция добавляет эту фильтрацию в любой существующий запрос независимо от конкретного типа элемента.

Пример

' 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

TextFilter Так как функция принимает и возвращает IQueryable(Of T) (а не только), IQueryableвы можете добавить дополнительные элементы запроса во время компиляции после текстового фильтра.

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)

Добавление узлов вызова метода в дерево выражения IQueryable

Если у вас есть IQueryable вместо IQueryable(Of T), вы не можете напрямую вызывать универсальные методы LINQ. В качестве альтернативы можно создать внутреннее дерево выражения, как показано выше, и использовать отражение для вызова соответствующего метода LINQ при передаче в дерево выражения.

Можно также дублировать функциональность метода LINQ, заключив все дерево в MethodCallExpression, который представляет вызов метода 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

В этом случае у вас нет универсального заполнителя времени T компиляции, поэтому вы будете использовать Lambda перегрузку, которая не требует сведений о типе компиляции, и которая создает LambdaExpression вместо выражения (Of TDelegate).

Динамическая библиотека LINQ

Построение деревьев выражений с помощью фабричных методов достаточно сложное занятие; проще создавать строки. Динамическая библиотека LINQ предоставляет набор методов расширения для IQueryable, соответствующих стандартным методам LINQ в Queryable, и который принимает строки в специальном синтаксисе вместо деревьев выражений. Библиотека создает соответствующее дерево выражения из строки и может возвращать результирующий преобразованный IQueryable.

Например, предыдущий пример (включая построение дерева выражений) можно переписать следующим образом:

' 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

См. также