Запрос на основе состояния среды выполнения (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.
- Создание строк и использование динамической библиотеки 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
Как правило, встроенные методы 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), где есть некоторый тип делегата, например Action
Func(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
См. также
- Expression Trees (Visual Basic) (Деревья выражений (Visual Basic))
- Практическое руководство. Выполнение деревьев выражений (Visual Basic)