考虑针对数据源定义 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 表达式树
- 向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 方法会执行两个步骤:
- 将当前表达式树包装在 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从头开始创建表达式树,从而在运行时根据特定的元素类型定制表达式。
构造 Expression(Of TDelegate)
构造要传递到 LINQ 方法之一的表达式时,实际上正在构造 Expression(Of TDelegate)的实例,其中 TDelegate
某些委托类型(例如 Func(Of String, Boolean)
, Action
或自定义委托类型)。
Expression(Of TDelegate) 继承自 LambdaExpression,它表示完整的 lambda 表达式,如下所示:
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 工厂方法为 lambda 表达式中的每个参数(如果有)定义 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") )
使用适当的 工厂方法重载,将参数和主体包装到编译时类型的 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 方法。
还可以通过将整个树包装在 MethodCallExpression 中来模拟 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,而不是 Expression(Of TDelegate)。
动态 LINQ 库
使用工厂方法构造表达式树相对复杂;编写字符串更容易。 动态 LINQ 库公开了一组与标准 LINQ 方法对应的扩展方法IQueryableQueryable,这些方法接受特殊语法中的字符串,而不是表达式树。 该库从字符串生成适当的表达式树,并返回翻译 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