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