次の方法で共有


ランタイムの状態に基づくクエリ (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 には 2 つのコンポーネントがあります。

  • 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 メソッドを呼び出す

一般に、Queryableの組み込みの LINQ メソッドは、次の 2 つの手順を実行します。

  • 現在の式ツリーを、メソッド呼び出しを表す 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)

LinqKitPredicateBuilder などのサード パーティ製ライブラリを使用して、さまざまな部分式を作成することもできます。

' 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のファクトリ メソッドを使用して、一から式ツリーを作成し、実行時の式を特定の要素の種類に合わせて調整できます。

式の構築 (TDelegate の場合)

LINQ メソッドのいずれかに渡す式を構築するときは、実際には Expression(Of TDelegate)のインスタンスを構築します。ここで、 TDelegate は、 Func(Of String, Boolean)Action、カスタム デリゲート型などのデリゲート型です。

Expression(Of TDelegate) は、次のような完全なラムダ式を表す LambdaExpressionから継承します。

Dim expr As Expression(Of Func(Of String, Boolean)) = Function(x As String) x.StartsWith("a")

LambdaExpressionには、次の 2 つのコンポーネントがあります。

  • Parameters プロパティによって表されるパラメーター リスト ((x As String))。
  • Body プロパティによって表される本文 (x.StartsWith("a"))。

Expression(Of TDelegate) を構築する基本的な手順は次のとおりです。

  • Parameter ファクトリ メソッドを使用して、ラムダ式の各パラメーター (存在する場合) のParameterExpressionオブジェクトを定義します。

    Dim x As ParameterExpression = Parameter(GetType(String), "x")
    
  • 定義したParameterExpressionと、Expressionのファクトリ メソッドを使用して、LambdaExpressionの本体を構築します。 たとえば、 x.StartsWith("a") を表す式は、次のように構築できます。

    Dim body As Expression = [Call](
        x,
        GetType(String).GetMethod("StartsWith", {GetType(String)}),
        Constant("a")
    )
    
  • 適切なLambdaファクトリ メソッドのオーバーロードを使用して、コンパイル時型の Expression(Of TDelegate)でパラメーターと本文をラップします。

    Dim expr As Expression(Of Func(Of String, Boolean)) =
        Lambda(Of Func(Of String, Boolean))(body, x)
    

次のセクションでは、LINQ メソッドに渡す Expression(Of TDelegate) を構築し、ファクトリ メソッドを使用してこれを行う方法の完全な例を示すシナリオについて説明します。

シナリオ

複数のエンティティ型があるとします。

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(Of T)の代わりにIQueryableがある場合、ジェネリック LINQ メソッドを直接呼び出すことはできません。 もう 1 つの方法は、上記のように内部式ツリーを構築し、リフレクションを使用して式ツリーを渡しながら適切な 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オーバーロードを使用します。これにより、Expression(Of TDelegate) の代わりにLambdaExpressionが生成されます。

動的 LINQ ライブラリ

ファクトリ メソッドを使用した式ツリーの構築は比較的複雑です。文字列を作成する方が簡単です。 動的 LINQ ライブラリは、Queryableの標準 LINQ メソッドに対応する一連の拡張メソッドをIQueryableで公開し、式ツリーではなく特殊な構文で文字列を受け入れます。 ライブラリは、文字列から適切な式ツリーを生成し、結果として変換された 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

こちらも参照ください