実行時の状態に基づくクエリの実行 (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 でファクトリ メソッドを使用して、Expression(Of TDelegate) 式ツリー式を作成する
  • 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 でファクトリ メソッドを使用して、最初から式ツリーを作成し、実行時に特定の要素型に合わせて式を調整することができます。

Expression(Of 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 つのコンポーネントがあります。

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

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 フィールドの 1 つに特定のテキストが含まれているエンティティのみをフィルター処理して返す必要があります。 Person については、FirstNameLastName のプロパティを検索する必要があります。

' 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) 用にカスタム関数を 1 つと、IQueryable(Of Car) 用にもう 1 つを記述することもできますが、次の関数では、特定の要素型に関係なく、このフィルターを既存のすべてのクエリに追加します。

' 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 汎用プレースホルダーがないため、コンパイル時に型情報が必要なく、Expression(Of TDelegate) の代わりに LambdaExpression が生成される、Lambda のオーバーロードを使用します。

動的 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

関連項目