Dotazování na základě stavu modulu runtime (Visual Basic)
Zvažte kód, který definuje IQueryable nebo IQueryable(Of T) vůči zdroji dat:
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)
Pokaždé, když tento kód spustíte, spustí se stejný přesný dotaz. Často to není velmi užitečné, protože v závislosti na podmínkách za běhu můžete chtít, aby kód spouštěl různé dotazy. Tento článek popisuje, jak můžete spustit jiný dotaz na základě stavu modulu runtime.
IQueryable / IQueryable(Of T) a stromy výrazů
IQueryable V zásadě má dvě komponenty:
- Expression— jazyková a zdrojová reprezentace komponent aktuálního dotazu ve formě stromu výrazů.
- Provider– instance zprostředkovatele LINQ, která ví, jak materializovat aktuální dotaz na hodnotu nebo sadu hodnot.
V kontextu dynamického dotazování bude poskytovatel obvykle stejný; strom výrazů dotazu se bude lišit od dotazu po dotaz.
Stromy výrazů jsou neměnné; Pokud chcete jiný strom výrazů , a tedy jiný dotaz, budete muset přeložit existující strom výrazu na nový, a tedy na nový IQueryable.
Následující části popisují konkrétní techniky pro dotazování odlišně v reakci na stav modulu runtime:
- Použití stavu modulu runtime ze stromu výrazů
- Volání dalších metod LINQ
- Různé stromy výrazů předávané do metod LINQ
- Vytvoření stromu výrazu Expression(Of TDelegate) pomocí metod továrny na adrese Expression
- Přidání uzlů volání metody do IQueryablestromu výrazů
- Vytváření řetězců a použití dynamické knihovny LINQ
Použití stavu modulu runtime ze stromu výrazů
Za předpokladu, že zprostředkovatel LINQ podporuje, nejjednodušší způsob, jak dynamicky dotazovat, je odkazovat na stav modulu runtime přímo v dotazu prostřednictvím uzavřené proměnné, například length
v následujícím příkladu kódu:
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
Strom interního výrazu ( a proto dotaz) nebyl změněn; dotaz vrátí různé hodnoty pouze proto, že hodnota length
byla změněna.
Volání dalších metod LINQ
Obecně platí, že integrované metodyQueryable LINQ provádějí dva kroky:
- Zabalte strom aktuálního výrazu do MethodCallExpression představující volání metody.
- Předejte zalomený strom výrazu zpět zprostředkovateli, a to buď k vrácení hodnoty prostřednictvím metody zprostředkovatele IQueryProvider.Execute , nebo k vrácení přeloženého objektu IQueryProvider.CreateQuery dotazu prostřednictvím metody.
Pokud chcete získat nový dotaz, můžete původní dotaz nahradit výsledkem metody IQueryable(Of T)-returning. Můžete to provést podmíněně na základě stavu modulu runtime, jak je znázorněno v následujícím příkladu:
' Dim sortByLength As Boolean = ...
Dim qry = companyNamesSource
If sortByLength Then qry = qry.OrderBy(Function(x) x.Length)
Různé stromy výrazů předávané do metod LINQ
Do metod LINQ můžete předávat různé výrazy v závislosti na stavu modulu runtime:
' 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)
Můžete také chtít vytvořit různé dílčí výrazy pomocí knihovny třetí strany, jako je 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)
Vytváření stromů výrazů a dotazů pomocí metod továrny
Ve všech příkladech až do tohoto okamžiku jsme znali typ elementu v době kompilace –String
a tím i typ dotazu.IQueryable(Of String)
Možná budete muset přidat komponenty do dotazu libovolného typu elementu nebo přidat různé komponenty v závislosti na typu elementu. Stromy výrazů můžete vytvářet od základů pomocí metod továrny a System.Linq.Expressions.Expressionpřizpůsobit tak výraz za běhu konkrétnímu typu prvku.
Vytvoření výrazu (Of TDelegate)
Při vytváření výrazu, který se má předat do jedné z metod LINQ, ve skutečnosti vytváříte instanci Expression(Of TDelegate), kde TDelegate
je nějaký typ delegáta, například Func(Of String, Boolean)
, Action
nebo vlastní typ delegáta.
Výraz(Of TDelegate) dědí z LambdaExpressionvýrazu , který představuje úplný výraz lambda, například následující:
Dim expr As Expression(Of Func(Of String, Boolean)) = Function(x As String) x.StartsWith("a")
A LambdaExpression má dvě komponenty:
- Seznam
(x As String)
parametrů – reprezentovaný Parameters vlastností. - Tělo –
x.StartsWith("a")
reprezentované Body vlastností.
Základní kroky při vytváření výrazu (Of TDelegate) jsou následující:
Definujte ParameterExpression objekty pro každý z parametrů (pokud existuje) ve výrazu lambda pomocí Parameter metody továrny.
Dim x As ParameterExpression = Parameter(GetType(String), "x")
Sestavte tělo vašeho LambdaExpression, pomocí ParameterExpression(s), které jste definovali, a metody továrny na Expressionadrese . Například výraz představující
x.StartsWith("a")
by mohl být vytvořen takto:Dim body As Expression = [Call]( x, GetType(String).GetMethod("StartsWith", {GetType(String)}), Constant("a") )
Zabalte parametry a tělo do výrazu s časovým typem kompilace(Of TDelegate) pomocí přetížení příslušné Lambda metody továrny:
Dim expr As Expression(Of Func(Of String, Boolean)) = Lambda(Of Func(Of String, Boolean))(body, x)
Následující části popisují scénář, ve kterém můžete chtít vytvořit Výraz(Of TDelegate) pro předání do metody LINQ a poskytnout úplný příklad toho, jak to provést pomocí metod továrny.
Scénář
Řekněme, že máte více typů entit:
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
U některého z těchto typů entit chcete filtrovat a vracet pouze ty entity, které mají daný text uvnitř jednoho z jejich string
polí. V Person
případě byste chtěli hledat FirstName
vlastnosti a LastName
vlastnosti:
' Dim term = ...
Dim personsQry = (New List(Of Person)).AsQueryable.
Where(Function(x) x.FirstName.Contains(term) OrElse x.LastName.Contains(term))
Ale v případě Car
, byste chtěli vyhledat pouze Model
vlastnost:
' Dim term = ...
Dim carsQry = (New List(Of Car)).AsQueryable.
Where(Function(x) x.Model.Contains(term))
I když byste mohli napsat jednu vlastní funkci pro a jinou pro IQueryable(Of Person)
IQueryable(Of Car)
, následující funkce přidá toto filtrování do jakéhokoli existujícího dotazu bez ohledu na konkrétní typ prvku.
Příklad
' 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
Vzhledem k tomu, že funkce přebírá a vrací IQueryable(Of T) (a ne jen ), IQueryablemůžete za textový filtr přidat další elementy dotazu s časovým typem kompilace.
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)
Přidání uzlů volání metody do IQueryablestromu výrazů
Pokud máte IQueryable místo IQueryable(Of T), nemůžete přímo volat obecné metody LINQ. Jednou z možností je vytvořit strom vnitřního výrazu, jak je uvedeno výše, a pomocí reflexe vyvolat příslušnou metodu LINQ při předávání stromu výrazů.
Můžete také duplikovat funkce metody LINQ tak, že celý strom zabalíte do objektu MethodCallExpression , který představuje volání metody 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
V tomto případě nemáte obecný zástupný symbol pro T
kompilaci, takže použijete Lambda přetížení, které nevyžaduje informace o typu kompilace a který vytvoří místo výrazu LambdaExpression(Of TDelegate).
Dynamická knihovna LINQ
Vytváření stromů výrazů pomocí metod továrny je poměrně složité; vytváření řetězců je jednodušší. Dynamická knihovna LINQ zveřejňuje sadu rozšiřujících metod odpovídajících IQueryable standardním metodám LINQ at Queryablea které přijímají řetězce ve speciální syntaxi místo stromů výrazů. Knihovna vygeneruje z řetězce odpovídající strom výrazů a může vrátit výslednou přeloženou .IQueryable
Například předchozí příklad (včetně konstrukce stromu výrazů) lze přepsat následujícím způsobem:
' 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