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), Actionnebo 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 Personpří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

Viz také