Поделиться через


Запрос на основе состояния среды выполнения (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
  • Изменение дерева выражений, передаваемого в методы LINQ
  • Создайте дерево выражений Expression(Of TDelegate) с помощью методов фабрики на Expression
  • Добавление узлов вызова метода в дерево выражений IQueryable
  • Создание строк и использование библиотеки Dynamic LINQ

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

Предполагая, что поставщик 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

Как правило, встроенные методы LINQQueryable включают два этапа:

  • Обрамите текущее дерево выражений с помощью 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)

Кроме того, может быть полезно составить различные подвыражения, используя стороннюю библиотеку, такую как LinqKit с PredicateBuilder:

' 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), где TDelegate есть некоторый тип делегата, например Func(Of String, Boolean)Action, или настраиваемый тип делегата.

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

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

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")
    )
    
  • Оборачивайте параметры и основную часть в типизированное на этапе компиляции выражение Expression(Of TDelegate) с помощью соответствующей Lambda перегрузки фабричного метода.

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

В следующих разделах описывается сценарий, в котором может потребоваться создать Expression(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.

Библиотека Dynamic LINQ

Создание деревьев выражений с использованием фабричных методов является относительно сложным; проще составлять строки. Библиотека Dynamic 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

См. также