チュートリアル : IQueryable LINQ プロバイダーの作成

この上級者向けのトピックでは LINQ のカスタム プロバイダーを作成する方法の詳細な手順について説明します。終了したら、TerraServer-USA Webサービスに対する LINQ のクエリを記述するために作成するプロバイダーを使用できます。

TerraServer-USA Web サービスには、米国の航空画像のデータベースとのインターフェイスが用意されています。またこのサービスは、場所の名前の一部または全体を指定すると、米国内の場所に関する情報を返すメソッドも公開します。GetPlaceListというメソッドが、の LINQ プロバイダーが呼び出すメソッドです。プロバイダーはWebサービスとの通信に Windows Communication Foundation (WCF) を使用します。TerraServer-USA Web サービスの詳細については、「Overview of the TerraServer-USA Web Services」を参照してください。

このプロバイダーは、比較的簡単な IQueryable プロバイダーです。これは、処理するクエリの特定の情報を受け取ります。また、クローズされた型システムを持ち、結果データを表す単一の型を公開します。このプロバイダーが調べるのは、クエリを表現する式ツリー内の 1 つの型のメソッド呼び出し式、つまり Where の最も内側にある呼び出しだけです。これは、Web サービスをクエリするために必要なデータをこの式から抽出します。その後、Web サービスを呼び出し、返されたデータを最初の IQueryable データ ソースの場所にある式ツリーに挿入します。残りのクエリ実行は、標準クエリ演算子の Enumerable 実装によって処理されます。

このトピックでは、C# および Visual Basic で用意されているコード例を示します。

このチュートリアルでは、次の作業について説明します。

  • Visual Studio でプロジェクトを作成する。

  • IQueryable LINQ プロバイダーで必要なインターフェイス IQueryable<T>IOrderedQueryable<T>、および IQueryProvider を実装する。

  • Web サービスのデータを表すカスタム .NET 型を追加する。

  • クエリ コンテキスト クラスおよび Web サービスからデータを取得するクラスを作成する。

  • Queryable.Where メソッドの最も内側にある呼び出しを表す式を検索する式ツリー ビジタ サブクラスを作成する。

  • Web サービス要求で使用する情報を LINQ クエリから抽出する式ツリー ビジタ サブクラスを作成する。

  • 完全な LINQ クエリを表す式ツリーを変更する式ツリー ビジタ サブクラスを作成する。

  • エバリュエーター クラスを使用して式ツリーを部分的に評価する。これは LINQ クエリのローカル変数参照をすべて値に変換するため、この手順が必要となります。

  • 式ツリー ヘルパー クラスおよび新しい例外クラスを作成する。

  • LINQ クエリを含むクライアント アプリケーションから LINQ プロバイダーをテストする。

  • より複雑なクエリ機能を LINQ プロバイダーに追加する。

    [!メモ]

    このチュートリアルで作成する LINQ プロバイダーをサンプルとして使用できます。詳細については、「LINQ のサンプル」を参照してください。

必須コンポーネント

このチュートリアルでは Visual Studio 2008で導入された機能が必要です。

[!メモ]

お使いのマシンで、Visual Studio ユーザー インターフェイスの一部の要素の名前や場所が、次の手順とは異なる場合があります。これらの要素は、使用している Visual Studio のエディションや独自の設定によって決まります。詳細については、「Visual Studio の設定」を参照してください。

プロジェクトの作成

Visual Studio でプロジェクトを作成するには

  1. Visual Studioでは、[クラス ライブラリ] の新しいアプリケーションを作成します。プロジェクト LinqToTerraServerProviderを付けます。

  2. ソリューション エクスプローラーClass1.cs (または Class1.vb) ファイルを選択し、名前を「QueryableTerraServerData.cs」(または「QueryableTerraServerData.vb」) に変更します。ポップアップ表示されるダイアログ ボックスで [はい] をクリックして、コード要素へのすべて参照の名前を変更します。

    実行可能クライアント アプリケーションはプロバイダー アセンブリをそのプロジェクトへの参照として追加するので、プロバイダーを Visual Studio のクラス ライブラリ プロジェクトとして作成します。

Web サービスにサービス参照を追加するには

  1. ソリューション エクスプローラーLinqToTerraServerProvider プロジェクトを右クリックし、[サービス参照の追加] をクリックします。

    [サービス参照の追加] ダイアログ ボックスが表示されます。

  2. [アドレス] ボックスに「http://terraserver.microsoft.com/TerraService2.asmx」と入力します。

  3. [名前空間] ボックスに「TerraServerReference」と入力し、[OK] をクリックします。

    TerraServer-USA Web サービスがサービス参照として追加され、アプリケーションは Windows Communication Foundation (WCF) を経由して Web サービスと通信できるようになります。プロジェクトにサービス参照を追加すると、Visual Studio は app.config ファイルを生成します。これには Web サービスのプロキシとエンドポイントが含まれます。詳細については、「Visual Studio での Windows Communication Foundation サービスと WCF データ サービス」を参照してください。

これで、app.config という名前のファイル、QueryableTerraServerData.cs (または QueryableTerraServerData.vb) という名前のファイル、および TerraServerReference という名前のサービス参照を持つプロジェクトが作成されました。

必要なインターフェイスの実装

LINQ プロバイダーを作成するには、少なくとも IQueryable<T> インターフェイスと IQueryProvider インターフェイスを実装する必要があります。IQueryable<T>IQueryProvider は別の必要なインターフェイスから派生するため、これら 2 つのインターフェイスを実装することによって、LINQ プロバイダーに要求される他のインターフェイスも実装することになります。

OrderByThenBy などの並べ替えクエリ演算子をサポートする場合は、IOrderedQueryable<T> インターフェイスも実装する必要があります。IOrderedQueryable<T>IQueryable<T> から派生するので、これら両方のインターフェイスを 1 つの型で実装することができます。このプロバイダーはそのことを実現します。

System.Linq.IQueryable`1 および System.Linq.IOrderedQueryable`1 を実装するには

  • ファイル QueryableTerraServerData.cs (または QueryableTerraServerData.vb) に、次のコードを追加します。

    Imports System.Linq.Expressions
    
    Public Class QueryableTerraServerData(Of TData)
        Implements IOrderedQueryable(Of TData)
    
    #Region "Private members"
    
        Private _provider As TerraServerQueryProvider
        Private _expression As Expression
    
    #End Region
    
    #Region "Constructors"
    
        ''' <summary>
        ''' This constructor is called by the client to create the data source.
        ''' </summary>
        Public Sub New()
            Me._provider = New TerraServerQueryProvider()
            Me._expression = Expression.Constant(Me)
        End Sub
    
        ''' <summary>
        ''' This constructor is called by Provider.CreateQuery().
        ''' </summary>
        ''' <param name="_expression"></param>
        Public Sub New(ByVal _provider As TerraServerQueryProvider, ByVal _expression As Expression)
    
            If _provider Is Nothing Then
                Throw New ArgumentNullException("provider")
            End If
    
            If _expression Is Nothing Then
                Throw New ArgumentNullException("expression")
            End If
    
            If Not GetType(IQueryable(Of TData)).IsAssignableFrom(_expression.Type) Then
                Throw New ArgumentOutOfRangeException("expression")
            End If
    
            Me._provider = _provider
            Me._expression = _expression
        End Sub
    
    #End Region
    
    #Region "Properties"
    
        Public ReadOnly Property ElementType(
            ) As Type Implements IQueryable(Of TData).ElementType
    
            Get
                Return GetType(TData)
            End Get
        End Property
    
        Public ReadOnly Property Expression(
            ) As Expression Implements IQueryable(Of TData).Expression
    
            Get
                Return _expression
            End Get
        End Property
    
        Public ReadOnly Property Provider(
            ) As IQueryProvider Implements IQueryable(Of TData).Provider
    
            Get
                Return _provider
            End Get
        End Property
    
    #End Region
    
    #Region "Enumerators"
    
        Public Function GetGenericEnumerator(
            ) As IEnumerator(Of TData) Implements IEnumerable(Of TData).GetEnumerator
    
            Return (Me.Provider.
                    Execute(Of IEnumerable(Of TData))(Me._expression)).GetEnumerator()
        End Function
    
        Public Function GetEnumerator(
            ) As IEnumerator Implements IEnumerable.GetEnumerator
    
            Return (Me.Provider.
                    Execute(Of IEnumerable)(Me._expression)).GetEnumerator()
        End Function
    
    #End Region
    
    End Class
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        public class QueryableTerraServerData<TData> : IOrderedQueryable<TData>
        {
            #region Constructors
            /// <summary>
            /// This constructor is called by the client to create the data source.
            /// </summary>
            public QueryableTerraServerData()
            {
                Provider = new TerraServerQueryProvider();
                Expression = Expression.Constant(this);
            }
    
            /// <summary>
            /// This constructor is called by Provider.CreateQuery().
            /// </summary>
            /// <param name="expression"></param>
            public QueryableTerraServerData(TerraServerQueryProvider provider, Expression expression)
            {
                if (provider == null)
                {
                    throw new ArgumentNullException("provider");
                }
    
                if (expression == null)
                {
                    throw new ArgumentNullException("expression");
                }
    
                if (!typeof(IQueryable<TData>).IsAssignableFrom(expression.Type))
                {
                    throw new ArgumentOutOfRangeException("expression");
                }
    
                Provider = provider;
                Expression = expression;
            }
            #endregion
    
            #region Properties
    
            public IQueryProvider Provider { get; private set; }
            public Expression Expression { get; private set; }
    
            public Type ElementType
            {
                get { return typeof(TData); }
            }
    
            #endregion
    
            #region Enumerators
            public IEnumerator<TData> GetEnumerator()
            {
                return (Provider.Execute<IEnumerable<TData>>(Expression)).GetEnumerator();
            }
    
            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return (Provider.Execute<System.Collections.IEnumerable>(Expression)).GetEnumerator();
            }
            #endregion
        }
    }
    

    QueryableTerraServerData クラスによる IOrderedQueryable<T> 実装は、IQueryable で宣言される 3 つのプロパティと、IEnumerable および IEnumerable<T> で宣言される 2 つの列挙型メソッドを実装します。

    このクラスには、2 つのコンストラクターがあります。最初のコンストラクターはクライアント アプリケーションから呼び出され、LINQ クエリを記述する対象のオブジェクトを作成します。2 番目のコンストラクターは IQueryProvider 実装のコードによって、プロバイダー ライブラリの内部で呼び出されます。

    QueryableTerraServerData 型のオブジェクトで GetEnumerator メソッドを呼び出すと、それが表すクエリが実行され、クエリの結果が列挙されます。

    このコードは、クラスの名前を除いて、この TerraServer-USA Web サービス プロバイダーに固有のものではありません。したがって、どの LINQ プロバイダーでも再利用できます。

System.Linq.IQueryProvider を実装するには

  • TerraServerQueryProvider クラスをプロジェクトに追加します。

    Imports System.Linq.Expressions
    Imports System.Reflection
    
    Public Class TerraServerQueryProvider
        Implements IQueryProvider
    
        Public Function CreateQuery(
            ByVal expression As Expression
            ) As IQueryable Implements IQueryProvider.CreateQuery
    
            Dim elementType As Type = TypeSystem.GetElementType(expression.Type)
    
            Try
                Dim qType = GetType(QueryableTerraServerData(Of )).MakeGenericType(elementType)
                Dim args = New Object() {Me, expression}
                Dim instance = Activator.CreateInstance(qType, args)
    
                Return CType(instance, IQueryable)
            Catch tie As TargetInvocationException
                Throw tie.InnerException
            End Try
        End Function
    
        ' Queryable's collection-returning standard query operators call this method.
        Public Function CreateQuery(Of TResult)(
            ByVal expression As Expression
            ) As IQueryable(Of TResult) Implements IQueryProvider.CreateQuery
    
            Return New QueryableTerraServerData(Of TResult)(Me, expression)
        End Function
    
        Public Function Execute(
            ByVal expression As Expression
            ) As Object Implements IQueryProvider.Execute
    
            Return TerraServerQueryContext.Execute(expression, False)
        End Function
    
        ' Queryable's "single value" standard query operators call this method.
        ' It is also called from QueryableTerraServerData.GetEnumerator().
        Public Function Execute(Of TResult)(
            ByVal expression As Expression
            ) As TResult Implements IQueryProvider.Execute
    
            Dim IsEnumerable As Boolean = (GetType(TResult).Name = "IEnumerable`1")
    
            Dim result = TerraServerQueryContext.Execute(expression, IsEnumerable)
            Return CType(result, TResult)
        End Function
    End Class
    
    using System;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        public class TerraServerQueryProvider : IQueryProvider
        {
            public IQueryable CreateQuery(Expression expression)
            {
                Type elementType = TypeSystem.GetElementType(expression.Type);
                try
                {
                    return (IQueryable)Activator.CreateInstance(typeof(QueryableTerraServerData<>).MakeGenericType(elementType), new object[] { this, expression });
                }
                catch (System.Reflection.TargetInvocationException tie)
                {
                    throw tie.InnerException;
                }
            }
    
            // Queryable's collection-returning standard query operators call this method.
            public IQueryable<TResult> CreateQuery<TResult>(Expression expression)
            {
                return new QueryableTerraServerData<TResult>(this, expression);
            }
    
            public object Execute(Expression expression)
            {
                return TerraServerQueryContext.Execute(expression, false);
            }
    
            // Queryable's "single value" standard query operators call this method.
            // It is also called from QueryableTerraServerData.GetEnumerator().
            public TResult Execute<TResult>(Expression expression)
            {
                bool IsEnumerable = (typeof(TResult).Name == "IEnumerable`1");
    
                return (TResult)TerraServerQueryContext.Execute(expression, IsEnumerable);
            }
        }
    }
    

    このクラスのクエリ プロバイダー コードは、IQueryProvider インターフェイスの実装に必要な 4 つのメソッドを実装します。2 つの CreateQuery メソッドが、データ ソースに関連付けられたクエリを作成します。2 つの Execute メソッドが、そのクエリを送って実行します。

    非ジェネリック CreateQuery メソッドは、リフレクションを使用して、作成したクエリが実行された場合に返すシーケンスの要素型を取得します。その後、Activator クラスを使用して、新しい QueryableTerraServerData インスタンスを構築します。構築されるこのインスタンスの要素型はジェネリック型引数です。非ジェネリック CreateQuery メソッドを呼び出したときの結果は、ジェネリック CreateQuery メソッドを正しい型引数で呼び出したときのものと同じようになります。

    クエリ実行ロジックのほとんどは、後で追加する別のクラスで処理されます。この機能はクエリされるデータ ソースに固有のものであるため、他の場所で処理されますが、このクラスのコードはどの LINQ プロバイダーでも使用できます。別のプロバイダーでこのコードを使用するには、クラスの名前と、メソッドの 2 つで参照されるクエリ コンテキスト型の名前を変更する必要があります。

結果データを表すカスタム型の追加

Web サービスから取得したデータを表すために、.NET 型が必要です。この型は、必要な結果を定義するためにクライアント LINQ クエリで使用されます。次の手順では、この型を作成します。Placeという型は、都市、公園、湖などの単一の地理的場所に関する情報が含まれます。

また、このコードには PlaceType という名前の列挙型も含まれます。これは地理的な場所のさまざまな型を定義し、Place クラスで使用されます。

カスタム結果型を作成するには

  • プロジェクトに Place クラスと PlaceType 列挙型を追加します。

    Public Class Place
        ' Properties.
        Public Property Name As String
        Public Property State As String
        Public Property PlaceType As PlaceType
    
        ' Constructor.
        Friend Sub New(ByVal name As String, 
                       ByVal state As String, 
                       ByVal placeType As TerraServerReference.PlaceType)
    
            Me.Name = name
            Me.State = state
            Me.PlaceType = CType(placeType, PlaceType)
        End Sub
    End Class
    
    Public Enum PlaceType
        Unknown
        AirRailStation
        BayGulf
        CapePeninsula
        CityTown
        HillMountain
        Island
        Lake
        OtherLandFeature
        OtherWaterFeature
        ParkBeach
        PointOfInterest
        River
    End Enum
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace LinqToTerraServerProvider
    {
        public class Place
        {
            // Properties.
            public string Name { get; private set; }
            public string State { get; private set; }
            public PlaceType PlaceType { get; private set; }
    
            // Constructor.
            internal Place(string name,
                            string state,
                            LinqToTerraServerProvider.TerraServerReference.PlaceType placeType)
            {
                Name = name;
                State = state;
                PlaceType = (PlaceType)placeType;
            }
        }
    
        public enum PlaceType
        {
            Unknown,
            AirRailStation,
            BayGulf,
            CapePeninsula,
            CityTown,
            HillMountain,
            Island,
            Lake,
            OtherLandFeature,
            OtherWaterFeature,
            ParkBeach,
            PointOfInterest,
            River
        }
    }
    

    Place 型のコンストラクターは、Web サービスによって返される型から結果オブジェクトを簡単に作成できるようにします。プロバイダーは直接 Web サービス API によって定義される結果型を返すことができますが、それにはクライアント アプリケーションが Web サービスに参照を追加することが必要です。プロバイダー ライブラリの一部として新しい型を作成することにより、クライアントは Web サービスが公開する型やメソッドについて知る必要がなくなります。

データ ソースからデータを取得する機能の追加

このプロバイダー実装は、Queryable.Where の最も内側にある呼び出しに Web サービスのクエリで使用する場所情報が含まれていることを前提としています。最も内側にある Queryable.Where 呼び出しは、where 句 (Visual Basic では Where 句) か、LINQ クエリで最初に発生する Queryable.Where メソッド呼び出しか、クエリを表す式ツリーの「下端」に最も近い呼び出しです。

クエリ コンテキスト クラスを作成するには

  • TerraServerQueryContext クラスをプロジェクトに追加します。

    Imports System.Linq.Expressions
    
    Public Class TerraServerQueryContext
    
        ' Executes the expression tree that is passed to it.
        Friend Shared Function Execute(ByVal expr As Expression, 
                                       ByVal IsEnumerable As Boolean) As Object
    
            ' The expression must represent a query over the data source.
            If Not IsQueryOverDataSource(expr) Then
                Throw New InvalidProgramException("No query over the data source was specified.")
            End If
    
            ' Find the call to Where() and get the lambda expression predicate.
            Dim whereFinder As New InnermostWhereFinder()
            Dim whereExpression As MethodCallExpression = 
                whereFinder.GetInnermostWhere(expr)
            Dim lambdaExpr As LambdaExpression
            lambdaExpr = CType(CType(whereExpression.Arguments(1), UnaryExpression).Operand, LambdaExpression)
    
            ' Send the lambda expression through the partial evaluator.
            lambdaExpr = CType(Evaluator.PartialEval(lambdaExpr), LambdaExpression)
    
            ' Get the place name(s) to query the Web service with.
            Dim lf As New LocationFinder(lambdaExpr.Body)
            Dim locations As List(Of String) = lf.Locations
            If locations.Count = 0 Then
                Dim s = "You must specify at least one place name in your query."
                Throw New InvalidQueryException(s)
            End If
    
            ' Call the Web service and get the results.
            Dim places() = WebServiceHelper.GetPlacesFromTerraServer(locations)
    
            ' Copy the IEnumerable places to an IQueryable.
            Dim queryablePlaces = places.AsQueryable()
    
            ' Copy the expression tree that was passed in, changing only the first
            ' argument of the innermost MethodCallExpression.
            Dim treeCopier As New ExpressionTreeModifier(queryablePlaces)
            Dim newExpressionTree = treeCopier.Visit(expr)
    
            ' This step creates an IQueryable that executes by replacing 
            ' Queryable methods with Enumerable methods.
            If (IsEnumerable) Then
                Return queryablePlaces.Provider.CreateQuery(newExpressionTree)
            Else
                Return queryablePlaces.Provider.Execute(newExpressionTree)
            End If
        End Function
    
        Private Shared Function IsQueryOverDataSource(ByVal expression As Expression) As Boolean
            ' If expression represents an unqueried IQueryable data source instance,
            ' expression is of type ConstantExpression, not MethodCallExpression.
            Return (TypeOf expression Is MethodCallExpression)
        End Function
    End Class
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        class TerraServerQueryContext
        {
            // Executes the expression tree that is passed to it.
            internal static object Execute(Expression expression, bool IsEnumerable)
            {
                // The expression must represent a query over the data source.
                if (!IsQueryOverDataSource(expression))
                    throw new InvalidProgramException("No query over the data source was specified.");
    
                // Find the call to Where() and get the lambda expression predicate.
                InnermostWhereFinder whereFinder = new InnermostWhereFinder();
                MethodCallExpression whereExpression = whereFinder.GetInnermostWhere(expression);
                LambdaExpression lambdaExpression = (LambdaExpression)((UnaryExpression)(whereExpression.Arguments[1])).Operand;
    
                // Send the lambda expression through the partial evaluator.
                lambdaExpression = (LambdaExpression)Evaluator.PartialEval(lambdaExpression);
    
                // Get the place name(s) to query the Web service with.
                LocationFinder lf = new LocationFinder(lambdaExpression.Body);
                List<string> locations = lf.Locations;
                if (locations.Count == 0)
                    throw new InvalidQueryException("You must specify at least one place name in your query.");
    
                // Call the Web service and get the results.
                Place[] places = WebServiceHelper.GetPlacesFromTerraServer(locations);
    
                // Copy the IEnumerable places to an IQueryable.
                IQueryable<Place> queryablePlaces = places.AsQueryable<Place>();
    
                // Copy the expression tree that was passed in, changing only the first
                // argument of the innermost MethodCallExpression.
                ExpressionTreeModifier treeCopier = new ExpressionTreeModifier(queryablePlaces);
                Expression newExpressionTree = treeCopier.Visit(expression);
    
                // This step creates an IQueryable that executes by replacing Queryable methods with Enumerable methods.
                if (IsEnumerable)
                    return queryablePlaces.Provider.CreateQuery(newExpressionTree);
                else
                    return queryablePlaces.Provider.Execute(newExpressionTree);
            }
    
            private static bool IsQueryOverDataSource(Expression expression)
            {
                // If expression represents an unqueried IQueryable data source instance,
                // expression is of type ConstantExpression, not MethodCallExpression.
                return (expression is MethodCallExpression);
            }
        }
    }
    

    このクラスは、クエリの実行作業を整理します。最も内側にある Queryable.Where 呼び出しを表す式を検出した後、このコードは、Queryable.Where に渡された述語を表すラムダ式を取得します。次に、部分的に評価するために述語式をメソッドに渡します。これにより、ローカル変数へのすべての参照が値に変換されます。その後、メソッドを呼び出して要求された場所を述語から抽出し、別のメソッドを呼び出して結果データを Web サービスから取得します。

    次のステップでこのコードは、LINQ クエリを表す式ツリーをコピーし、式ツリーに対して 1 か所変更を加えます。コードは式ツリー ビジタ サブクラスを使用して、最も内側にあるクエリ演算子呼び出しが適用されるデータ ソースを、Web サービスから取得した Place オブジェクトの具体的なリストに置き換えます。

    Place オブジェクトのリストを式ツリーに挿入する前に、AsQueryable を呼び出してその型を IEnumerable から IQueryable に変更します。この型の変更が必要なのは、式ツリーを書き直すときに、最も内側にあるクエリ演算子メソッドのメソッド呼び出しを表すノードが再構築されるためです。引数の 1 つが変更されるためにノードが再構築されます (つまり、それが適用されるデータ ソース)。ノードの再構築には、Call(Expression, MethodInfo, IEnumerable<Expression>) というメソッドが使用されます。このメソッドは、引数が渡されるメソッドの対応するパラメーターに引数を割り当てることができない場合に例外をスローします。この場合、Place オブジェクトの IEnumerable リストは Queryable.WhereIQueryable パラメーターに割り当てられません。そのため、その型は IQueryable に変更されます。

    その型を IQueryable に変更することにより、コレクションは、クエリを作成または実行できる IQueryProvider メンバーも取得します (Provider プロパティによってアクセスされる)。IQueryable°Place コレクションの動的型は EnumerableQuery です。これは、System.Linq API 内部の型です。クエリ プロバイダーがこの型と関連付けられていると、Queryable 標準クエリ演算子呼び出しを同等の Enumerable 演算子に置き換えて、クエリを実行します。これにより、そのクエリは実質的に LINQ to Objects クエリになります。

    TerraServerQueryContext クラスの最後のコードは、2 つのメソッドのうちの 1 つを Place オブジェクトの IQueryable リストに対して呼び出します。クライアント クエリが列挙可能な結果を返す場合は CreateQuery を、列挙可能でない結果を返す場合は Execute を呼び出します。

    このクラスのコードは、この TerraServer-USA プロバイダーに固有のものです。そのため、より汎用の IQueryProvider 実装に直接挿入せずに、TerraServerQueryContext クラスでカプセル化します。

作成中のプロバイダーが Web サービスのクエリのために必要とするのは Queryable.Where 述語にある情報だけです。したがって、LINQ to Objects を使用し、内部 EnumerableQuery 型を使用して LINQ クエリの実行作業を行います。LINQ to Objects を使用してクエリを実行する別の方法として、LINQ to Objects が実行するクエリの一部をクライアントに LINQ to Objects クエリでラップさせるようにする方法があります。これを行うには、残りのクエリに対して AsEnumerable<TSource> を呼び出します。それはプロバイダーがその特定の目的のために必要とするクエリの一部です。この種の実装の利点は、カスタム プロバイダーと LINQ to Objects の間の作業分担がより透過的になることです。

[!メモ]

このトピックで説明しているプロバイダーは、それ自体の最小限のクエリ サポートを持つ簡単なプロバイダーです。そのため、クエリを実行する場合は LINQ to Objects に大きく依存します。LINQ to SQL のような複雑な LINQ プロバイダーは、作業を LINQ to Objects に渡さずに、クエリ全体をサポートすることができます。

Web サービスからデータを取得するためのクラスを作成するには

  • WebServiceHelper クラス (Visual Basic ではモジュール) をプロジェクトに追加します。

    Imports System.Collections.Generic
    Imports LinqToTerraServerProvider.TerraServerReference
    
    Friend Module WebServiceHelper
        Private numResults As Integer = 200
        Private mustHaveImage As Boolean = False
    
        Friend Function GetPlacesFromTerraServer(ByVal locations As List(Of String)) As Place()
            ' Limit the total number of Web service calls.
            If locations.Count > 5 Then
                Dim s = "This query requires more than five separate calls to the Web service. Please decrease the number of places."
                Throw New InvalidQueryException(s)
            End If
    
            Dim allPlaces As New List(Of Place)
    
            ' For each location, call the Web service method to get data.
            For Each location In locations
                Dim places = CallGetPlaceListMethod(location)
                allPlaces.AddRange(places)
            Next
    
            Return allPlaces.ToArray()
        End Function
    
        Private Function CallGetPlaceListMethod(ByVal location As String) As Place()
    
            Dim client As New TerraServiceSoapClient()
            Dim placeFacts() As PlaceFacts
    
            Try
                ' Call the Web service method "GetPlaceList".
                placeFacts = client.GetPlaceList(location, numResults, mustHaveImage)
    
                ' If we get exactly 'numResults' results, they are probably truncated.
                If (placeFacts.Length = numResults) Then
                    Dim s = "The results have been truncated by the Web service and would not be complete. Please try a different query."
                    Throw New Exception(s)
                End If
    
                ' Create Place objects from the PlaceFacts objects returned by the Web service.
                Dim places(placeFacts.Length - 1) As Place
                For i = 0 To placeFacts.Length - 1
                    places(i) = New Place(placeFacts(i).Place.City, 
                                          placeFacts(i).Place.State, 
                                          placeFacts(i).PlaceTypeId)
                Next
    
                ' Close the WCF client.
                client.Close()
    
                Return places
            Catch timeoutException As TimeoutException
                client.Abort()
                Throw
            Catch communicationException As System.ServiceModel.CommunicationException
                client.Abort()
                Throw
            End Try
        End Function
    End Module
    
    using System;
    using System.Collections.Generic;
    using LinqToTerraServerProvider.TerraServerReference;
    
    namespace LinqToTerraServerProvider
    {
        internal static class WebServiceHelper
        {
            private static int numResults = 200;
            private static bool mustHaveImage = false;
    
            internal static Place[] GetPlacesFromTerraServer(List<string> locations)
            {
                // Limit the total number of Web service calls.
                if (locations.Count > 5)
                    throw new InvalidQueryException("This query requires more than five separate calls to the Web service. Please decrease the number of locations in your query.");
    
                List<Place> allPlaces = new List<Place>();
    
                // For each location, call the Web service method to get data.
                foreach (string location in locations)
                {
                    Place[] places = CallGetPlaceListMethod(location);
                    allPlaces.AddRange(places);
                }
    
                return allPlaces.ToArray();
            }
    
            private static Place[] CallGetPlaceListMethod(string location)
            {
                TerraServiceSoapClient client = new TerraServiceSoapClient();
                PlaceFacts[] placeFacts = null;
    
                try
                {
                    // Call the Web service method "GetPlaceList".
                    placeFacts = client.GetPlaceList(location, numResults, mustHaveImage);
    
                    // If there are exactly 'numResults' results, they are probably truncated.
                    if (placeFacts.Length == numResults)
                        throw new Exception("The results have been truncated by the Web service and would not be complete. Please try a different query.");
    
                    // Create Place objects from the PlaceFacts objects returned by the Web service.
                    Place[] places = new Place[placeFacts.Length];
                    for (int i = 0; i < placeFacts.Length; i++)
                    {
                        places[i] = new Place(
                            placeFacts[i].Place.City,
                            placeFacts[i].Place.State,
                            placeFacts[i].PlaceTypeId);
                    }
    
                    // Close the WCF client.
                    client.Close();
    
                    return places;
                }
                catch (TimeoutException timeoutException)
                {
                    client.Abort();
                    throw;
                }
                catch (System.ServiceModel.CommunicationException communicationException)
                {
                    client.Abort();
                    throw;
                }
            }
        }
    }
    

    このクラスには、Web サービスからデータを取得する機能が含まれます。このコードは、TerraServiceSoapClient という名前の型を使用して、Web サービス メソッド GetPlaceList を呼び出します。この型はプロジェクト用に Windows Communication Foundation (WCF) が自動生成します。その後、それぞれの結果は、Web サービス メソッドの戻り値の型からプロバイダーがデータに対して定義する .NET 型に変換されます。

    このコードには、プロバイダー ライブラリを使いやすくする 2 つのチェックが含まれます。最初のチェックは、1 つのクエリにつき Web サービスに対して行われる呼び出しの合計数を 5 つに制限して、クライアント アプリケーションが応答を待機する最大時間を制限します。クライアント クエリで指定されるそれぞれの場所ごとに 1 つの Web サービス要求が生成されます。そのため、クエリに含まれる場所が 5 つを超える場合、プロバイダーは例外をスローします。

    2 番目のチェックは、Web サービスによって返される結果の数が、返すことのできる結果の最大数と同じかどうかを確認します。結果の数が最大数である場合、Web サービスからの結果が切り捨てられている可能性があります。プロバイダーはクライアントに不完全なリストを返すのではなく、例外をスローします。

式ツリー ビジタ クラスの追加

最も内側にある Where メソッド呼び出し式を検索するビジタを作成するには

  1. InnermostWhereFinder クラスをプロジェクトに追加します。このクラスは ExpressionVisitor クラスを継承しています。

    Imports System.Linq.Expressions
    
    Class InnermostWhereFinder
        Inherits ExpressionVisitor
    
        Private innermostWhereExpression As MethodCallExpression
    
        Public Function GetInnermostWhere(ByVal expr As Expression) As MethodCallExpression
            Me.Visit(expr)
            Return innermostWhereExpression
        End Function
    
        Protected Overrides Function VisitMethodCall(ByVal expr As MethodCallExpression) As Expression
            If expr.Method.Name = "Where" Then
                innermostWhereExpression = expr
            End If
    
            Me.Visit(expr.Arguments(0))
    
            Return expr
        End Function
    End Class
    
    using System;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        internal class InnermostWhereFinder : ExpressionVisitor
        {
            private MethodCallExpression innermostWhereExpression;
    
            public MethodCallExpression GetInnermostWhere(Expression expression)
            {
                Visit(expression);
                return innermostWhereExpression;
            }
    
            protected override Expression VisitMethodCall(MethodCallExpression expression)
            {
                if (expression.Method.Name == "Where")
                    innermostWhereExpression = expression;
    
                Visit(expression.Arguments[0]);
    
                return expression;
            }
        }
    }
    

    このクラスは、特定の式を検索する機能を実行するために、基本の式ツリー ビジタ クラスを継承しています。基本の式ツリー ビジタ クラスは、式ツリーの走査が関係する特定のタスク用に特化されて継承が行われるようにデザインされています。派生クラスは VisitMethodCall メソッドをオーバーライドし、クライアント クエリを表す式ツリーの Where の最も内側にある呼び出しを表す式を検索します。この最も内側にある式は、プロバイダーによる検索場所の抽出元となる式です。

  2. System.Collections.Generic、System.Collections.ObjectModel 名前空間および System.Linq.Expressions 名前空間に対する using ディレクティブ (Visual Basic では Imports ステートメント) を、ファイルに追加します。

Web サービスをクエリするためにデータを抽出するビジタを作成するには

  • LocationFinder クラスをプロジェクトに追加します。

    Imports System.Linq.Expressions
    Imports ETH = LinqToTerraServerProvider.ExpressionTreeHelpers
    
    Friend Class LocationFinder
        Inherits ExpressionVisitor
    
        Private _expression As Expression
        Private _locations As List(Of String)
    
        Public Sub New(ByVal exp As Expression)
            Me._expression = exp
        End Sub
    
        Public ReadOnly Property Locations() As List(Of String)
            Get
                If _locations Is Nothing Then
                    _locations = New List(Of String)()
                    Me.Visit(Me._expression)
                End If
                Return Me._locations
            End Get
        End Property
    
        Protected Overrides Function VisitBinary(ByVal be As BinaryExpression) As Expression
            ' Handles Visual Basic String semantics.
            be = ETH.ConvertVBStringCompare(be)
    
            If be.NodeType = ExpressionType.Equal Then
                If (ETH.IsMemberEqualsValueExpression(be, GetType(Place), "Name")) Then
                    _locations.Add(ETH.GetValueFromEqualsExpression(be, GetType(Place), "Name"))
                    Return be
                ElseIf (ETH.IsMemberEqualsValueExpression(be, GetType(Place), "State")) Then
                    _locations.Add(ETH.GetValueFromEqualsExpression(be, GetType(Place), "State"))
                    Return be
                Else
                    Return MyBase.VisitBinary(be)
                End If
            Else
                Return MyBase.VisitBinary(be)
            End If
        End Function
    End Class
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        internal class LocationFinder : ExpressionVisitor
        {
            private Expression expression;
            private List<string> locations;
    
            public LocationFinder(Expression exp)
            {
                this.expression = exp;
            }
    
            public List<string> Locations
            {
                get
                {
                    if (locations == null)
                    {
                        locations = new List<string>();
                        this.Visit(this.expression);
                    }
                    return this.locations;
                }
            }
    
            protected override Expression VisitBinary(BinaryExpression be)
            {
                if (be.NodeType == ExpressionType.Equal)
                {
                    if (ExpressionTreeHelpers.IsMemberEqualsValueExpression(be, typeof(Place), "Name"))
                    {
                        locations.Add(ExpressionTreeHelpers.GetValueFromEqualsExpression(be, typeof(Place), "Name"));
                        return be;
                    }
                    else if (ExpressionTreeHelpers.IsMemberEqualsValueExpression(be, typeof(Place), "State"))
                    {
                        locations.Add(ExpressionTreeHelpers.GetValueFromEqualsExpression(be, typeof(Place), "State"));
                        return be;
                    }
                    else
                        return base.VisitBinary(be);
                }
                else
                    return base.VisitBinary(be);
            }
        }
    }
    

    このクラスは、クライアントが Queryable.Where に渡す述語から場所情報を抽出するために使用されます。これは ExpressionVisitor クラスから派生し、VisitBinary メソッドだけをオーバーライドします。

    ExpressionVisitor クラスは、place.Name == "Seattle" (Visual Basic では place.Name = "Seattle") のような等価式などの二項式を VisitBinary メソッドに送ります。オーバーライドするこの VisitBinary メソッドでは、場所情報を指定できる等価式パターンと式が一致した場合、その情報が抽出され、場所のリストに格納されます。

    このクラスは、式ツリーの場所情報の検索に式ツリー ビジタを使用します。ビジタは式ツリーの走査および確認を行うために設計されているためです。結果として生成されるコードは、ビジタを使用せずに実装した場合と比べてより整然となり、エラーの原因にもなりにくくなります。

    チュートリアルのこの段階では、プロバイダーは限られた方法でしかクエリの場所情報を指定することができません。トピックの後半では、より多くの方法で場所情報を指定できるようにするための機能を追加します。

式ツリーを変更するビジタを作成するには

  • ExpressionTreeModifier クラスをプロジェクトに追加します。

    Imports System.Linq.Expressions
    
    Friend Class ExpressionTreeModifier
        Inherits ExpressionVisitor
    
        Private queryablePlaces As IQueryable(Of Place)
    
        Friend Sub New(ByVal places As IQueryable(Of Place))
            Me.queryablePlaces = places
        End Sub
    
        Protected Overrides Function VisitConstant(ByVal c As ConstantExpression) As Expression
            ' Replace the constant QueryableTerraServerData arg with the queryable Place collection.
            If c.Type Is GetType(QueryableTerraServerData(Of Place)) Then
                Return Expression.Constant(Me.queryablePlaces)
            Else
                Return c
            End If
        End Function
    End Class
    
    using System;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        internal class ExpressionTreeModifier : ExpressionVisitor
        {
            private IQueryable<Place> queryablePlaces;
    
            internal ExpressionTreeModifier(IQueryable<Place> places)
            {
                this.queryablePlaces = places;
            }
    
            protected override Expression VisitConstant(ConstantExpression c)
            {
                // Replace the constant QueryableTerraServerData arg with the queryable Place collection.
                if (c.Type == typeof(QueryableTerraServerData<Place>))
                    return Expression.Constant(this.queryablePlaces);
                else
                    return c;
            }
        }
    }
    

    このクラスは ExpressionVisitor クラスから派生し、VisitConstant メソッドをオーバーライドします。このメソッドでは、最も内側にある標準クエリ演算子呼び出しが適用されるオブジェクトを Place オブジェクトの具体的なリストに置き換えます。

    この式ツリー修飾子クラスが式ツリー ビジタを使用します。ビジタは式ツリーを走査、確認、およびコピーするために設計されているためです。基本の式ツリー ビジタ クラスから派生することで、このクラスがその機能の実行で必要になるコードが最小限で済みます。

式エバリュエーターの追加

クライアント クエリの Queryable.Where メソッドに渡される述語には、ラムダ式のパラメーターに依存しないサブ式が含まれる場合があります。分離されたこれらのサブ式は直ちに評価することができ、そうする必要があります。このサブ式は、値に変換する必要のあるローカル変数またはメンバー変数の参照の場合もあります。

次のクラスはメソッド PartialEval(Expression) を公開します。これは、式の中のどのサブ式を直ちに評価することができるか確認します (存在する場合)。次に、ラムダ式を作成し、コンパイルし、返されたデリゲートを呼び出してその式を評価します。最後にサブツリーを、定数値を表す新しいノードに置き換えます。これを部分評価と呼びます。

式ツリーの部分評価を実行するクラスを追加するには

  • Evaluator クラスをプロジェクトに追加します。

    Imports System.Linq.Expressions
    
    Public Module Evaluator
        ''' <summary>Performs evaluation and replacement of independent sub-trees</summary>
        ''' <param name="expr">The root of the expression tree.</param>
        ''' <param name="fnCanBeEvaluated">A function that decides whether a given expression node can be part of the local function.</param>
        ''' <returns>A new tree with sub-trees evaluated and replaced.</returns>
        Public Function PartialEval(
            ByVal expr As Expression, 
            ByVal fnCanBeEvaluated As Func(Of Expression, Boolean)
            )  As Expression
    
            Return New SubtreeEvaluator(New Nominator(fnCanBeEvaluated).Nominate(expr)).Eval(expr)
        End Function
    
        ''' <summary>
        ''' Performs evaluation and replacement of independent sub-trees
        ''' </summary>
        ''' <param name="expression">The root of the expression tree.</param>
        ''' <returns>A new tree with sub-trees evaluated and replaced.</returns>
        Public Function PartialEval(ByVal expression As Expression) As Expression
            Return PartialEval(expression, AddressOf Evaluator.CanBeEvaluatedLocally)
        End Function
    
        Private Function CanBeEvaluatedLocally(ByVal expression As Expression) As Boolean
            Return expression.NodeType <> ExpressionType.Parameter
        End Function
    
        ''' <summary>
        ''' Evaluates and replaces sub-trees when first candidate is reached (top-down)
        ''' </summary>
        Class SubtreeEvaluator
            Inherits ExpressionVisitor
    
            Private candidates As HashSet(Of Expression)
    
            Friend Sub New(ByVal candidates As HashSet(Of Expression))
                Me.candidates = candidates
            End Sub
    
            Friend Function Eval(ByVal exp As Expression) As Expression
                Return Me.Visit(exp)
            End Function
    
            Public Overrides Function Visit(ByVal exp As Expression) As Expression
                If exp Is Nothing Then
                    Return Nothing
                ElseIf Me.candidates.Contains(exp) Then
                    Return Me.Evaluate(exp)
                End If
    
                Return MyBase.Visit(exp)
            End Function
    
            Private Function Evaluate(ByVal e As Expression) As Expression
                If e.NodeType = ExpressionType.Constant Then
                    Return e
                End If
    
                Dim lambda = Expression.Lambda(e)
                Dim fn As [Delegate] = lambda.Compile()
    
                Return Expression.Constant(fn.DynamicInvoke(Nothing), e.Type)
            End Function
        End Class
    
    
        ''' <summary>
        ''' Performs bottom-up analysis to determine which nodes can possibly
        ''' be part of an evaluated sub-tree.
        ''' </summary>
        Class Nominator
            Inherits ExpressionVisitor
    
            Private fnCanBeEvaluated As Func(Of Expression, Boolean)
            Private candidates As HashSet(Of Expression)
            Private cannotBeEvaluated As Boolean
    
            Friend Sub New(ByVal fnCanBeEvaluated As Func(Of Expression, Boolean))
                Me.fnCanBeEvaluated = fnCanBeEvaluated
            End Sub
    
            Friend Function Nominate(ByVal expr As Expression) As HashSet(Of Expression)
                Me.candidates = New HashSet(Of Expression)()
                Me.Visit(expr)
    
                Return Me.candidates
            End Function
    
            Public Overrides Function Visit(ByVal expr As Expression) As Expression
                If expr IsNot Nothing Then
    
                    Dim saveCannotBeEvaluated = Me.cannotBeEvaluated
                    Me.cannotBeEvaluated = False
    
                    MyBase.Visit(expr)
    
                    If Not Me.cannotBeEvaluated Then
                        If Me.fnCanBeEvaluated(expr) Then
                            Me.candidates.Add(expr)
                        Else
                            Me.cannotBeEvaluated = True
                        End If
                    End If
    
                    Me.cannotBeEvaluated = Me.cannotBeEvaluated Or 
                                           saveCannotBeEvaluated
                End If
    
                Return expr
            End Function
        End Class
    End Module
    
    using System;
    using System.Collections.Generic;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        public static class Evaluator
        {
            /// <summary>
            /// Performs evaluation & replacement of independent sub-trees
            /// </summary>
            /// <param name="expression">The root of the expression tree.</param>
            /// <param name="fnCanBeEvaluated">A function that decides whether a given expression node can be part of the local function.</param>
            /// <returns>A new tree with sub-trees evaluated and replaced.</returns>
            public static Expression PartialEval(Expression expression, Func<Expression, bool> fnCanBeEvaluated)
            {
                return new SubtreeEvaluator(new Nominator(fnCanBeEvaluated).Nominate(expression)).Eval(expression);
            }
    
            /// <summary>
            /// Performs evaluation & replacement of independent sub-trees
            /// </summary>
            /// <param name="expression">The root of the expression tree.</param>
            /// <returns>A new tree with sub-trees evaluated and replaced.</returns>
            public static Expression PartialEval(Expression expression)
            {
                return PartialEval(expression, Evaluator.CanBeEvaluatedLocally);
            }
    
            private static bool CanBeEvaluatedLocally(Expression expression)
            {
                return expression.NodeType != ExpressionType.Parameter;
            }
    
            /// <summary>
            /// Evaluates & replaces sub-trees when first candidate is reached (top-down)
            /// </summary>
            class SubtreeEvaluator : ExpressionVisitor
            {
                HashSet<Expression> candidates;
    
                internal SubtreeEvaluator(HashSet<Expression> candidates)
                {
                    this.candidates = candidates;
                }
    
                internal Expression Eval(Expression exp)
                {
                    return this.Visit(exp);
                }
    
                public override Expression Visit(Expression exp)
                {
                    if (exp == null)
                    {
                        return null;
                    }
                    if (this.candidates.Contains(exp))
                    {
                        return this.Evaluate(exp);
                    }
                    return base.Visit(exp);
                }
    
                private Expression Evaluate(Expression e)
                {
                    if (e.NodeType == ExpressionType.Constant)
                    {
                        return e;
                    }
                    LambdaExpression lambda = Expression.Lambda(e);
                    Delegate fn = lambda.Compile();
                    return Expression.Constant(fn.DynamicInvoke(null), e.Type);
                }
            }
    
            /// <summary>
            /// Performs bottom-up analysis to determine which nodes can possibly
            /// be part of an evaluated sub-tree.
            /// </summary>
            class Nominator : ExpressionVisitor
            {
                Func<Expression, bool> fnCanBeEvaluated;
                HashSet<Expression> candidates;
                bool cannotBeEvaluated;
    
                internal Nominator(Func<Expression, bool> fnCanBeEvaluated)
                {
                    this.fnCanBeEvaluated = fnCanBeEvaluated;
                }
    
                internal HashSet<Expression> Nominate(Expression expression)
                {
                    this.candidates = new HashSet<Expression>();
                    this.Visit(expression);
                    return this.candidates;
                }
    
                public override Expression Visit(Expression expression)
                {
                    if (expression != null)
                    {
                        bool saveCannotBeEvaluated = this.cannotBeEvaluated;
                        this.cannotBeEvaluated = false;
                        base.Visit(expression);
                        if (!this.cannotBeEvaluated)
                        {
                            if (this.fnCanBeEvaluated(expression))
                            {
                                this.candidates.Add(expression);
                            }
                            else
                            {
                                this.cannotBeEvaluated = true;
                            }
                        }
                        this.cannotBeEvaluated |= saveCannotBeEvaluated;
                    }
                    return expression;
                }
            }
        }
    }
    

ヘルパー クラスの追加

このセクションには、プロバイダーの 3 つのヘルパー クラスのコードが含まれています。

System.Linq.IQueryProvider 実装で使用されるヘルパー クラスを追加するには

  • TypeSystem クラス (Visual Basic ではモジュール) をプロジェクトに追加します。

    Imports System.Collections.Generic
    
    Friend Module TypeSystem
    
        Friend Function GetElementType(ByVal seqType As Type) As Type
            Dim ienum As Type = FindIEnumerable(seqType)
    
            If ienum Is Nothing Then
                Return seqType
            End If
    
            Return ienum.GetGenericArguments()(0)
        End Function
    
        Private Function FindIEnumerable(ByVal seqType As Type) As Type
    
            If seqType Is Nothing Or seqType Is GetType(String) Then
                Return Nothing
            End If
    
            If (seqType.IsArray) Then
                Return GetType(IEnumerable(Of )).MakeGenericType(seqType.GetElementType())
            End If
    
            If (seqType.IsGenericType) Then
                For Each arg As Type In seqType.GetGenericArguments()
                    Dim ienum As Type = GetType(IEnumerable(Of )).MakeGenericType(arg)
    
                    If (ienum.IsAssignableFrom(seqType)) Then
                        Return ienum
                    End If
                Next
            End If
    
            Dim ifaces As Type() = seqType.GetInterfaces()
    
            If ifaces IsNot Nothing And ifaces.Length > 0 Then
                For Each iface As Type In ifaces
                    Dim ienum As Type = FindIEnumerable(iface)
    
                    If (ienum IsNot Nothing) Then
                        Return ienum
                    End If
                Next
            End If
    
            If seqType.BaseType IsNot Nothing AndAlso
               seqType.BaseType IsNot GetType(Object) Then
    
                Return FindIEnumerable(seqType.BaseType)
            End If
    
            Return Nothing
        End Function
    End Module
    
    using System;
    using System.Collections.Generic;
    
    namespace LinqToTerraServerProvider
    {
        internal static class TypeSystem
        {
            internal static Type GetElementType(Type seqType)
            {
                Type ienum = FindIEnumerable(seqType);
                if (ienum == null) return seqType;
                return ienum.GetGenericArguments()[0];
            }
    
            private static Type FindIEnumerable(Type seqType)
            {
                if (seqType == null || seqType == typeof(string))
                    return null;
    
                if (seqType.IsArray)
                    return typeof(IEnumerable<>).MakeGenericType(seqType.GetElementType());
    
                if (seqType.IsGenericType)
                {
                    foreach (Type arg in seqType.GetGenericArguments())
                    {
                        Type ienum = typeof(IEnumerable<>).MakeGenericType(arg);
                        if (ienum.IsAssignableFrom(seqType))
                        {
                            return ienum;
                        }
                    }
                }
    
                Type[] ifaces = seqType.GetInterfaces();
                if (ifaces != null && ifaces.Length > 0)
                {
                    foreach (Type iface in ifaces)
                    {
                        Type ienum = FindIEnumerable(iface);
                        if (ienum != null) return ienum;
                    }
                }
    
                if (seqType.BaseType != null && seqType.BaseType != typeof(object))
                {
                    return FindIEnumerable(seqType.BaseType);
                }
    
                return null;
            }
        }
    }
    

    以前に追加した IQueryProvider 実装がこのヘルパー クラスを使用します。

    TypeSystem.GetElementType は、リフレクションを使用して IEnumerable<T> (Visual Basic では IEnumerable(Of T)) コレクションのジェネリック型引数を取得します。このメソッドはクエリ プロバイダー実装の非ジェネリック CreateQuery メソッドから呼び出され、クエリ結果コレクションの要素型を指定します。

    このヘルパー クラスはこの TerraServer-USA Web サービス プロバイダーに固有のものではありません。したがって、どの LINQ プロバイダーでも再利用できます。

式ツリー ヘルパー クラスを作成するには

  • ExpressionTreeHelpers クラスをプロジェクトに追加します。

    Imports System.Linq.Expressions
    
    Friend Class ExpressionTreeHelpers
        ' Visual Basic encodes string comparisons as a method call to
        ' Microsoft.VisualBasic.CompilerServices.Operators.CompareString.
        ' This method will convert the method call into a binary operation instead.
        ' Note that this makes the string comparison case sensitive.
        Friend Shared Function ConvertVBStringCompare(ByVal exp As BinaryExpression) As BinaryExpression
    
            If exp.Left.NodeType = ExpressionType.Call Then
                Dim compareStringCall = CType(exp.Left, MethodCallExpression)
    
                If compareStringCall.Method.DeclaringType.FullName = 
                    "Microsoft.VisualBasic.CompilerServices.Operators" AndAlso 
                    compareStringCall.Method.Name = "CompareString" Then
    
                    Dim arg1 = compareStringCall.Arguments(0)
                    Dim arg2 = compareStringCall.Arguments(1)
    
                    Select Case exp.NodeType
                        Case ExpressionType.LessThan
                            Return Expression.LessThan(arg1, arg2)
                        Case ExpressionType.LessThanOrEqual
                            Return Expression.GreaterThan(arg1, arg2)
                        Case ExpressionType.GreaterThan
                            Return Expression.GreaterThan(arg1, arg2)
                        Case ExpressionType.GreaterThanOrEqual
                            Return Expression.GreaterThanOrEqual(arg1, arg2)
                        Case Else
                            Return Expression.Equal(arg1, arg2)
                    End Select
                End If
            End If
            Return exp
        End Function
    
        Friend Shared Function IsMemberEqualsValueExpression(
            ByVal exp As Expression, 
            ByVal declaringType As Type, 
            ByVal memberName As String) As Boolean
    
            If exp.NodeType <> ExpressionType.Equal Then
                Return False
            End If
    
            Dim be = CType(exp, BinaryExpression)
    
            ' Assert.
            If IsSpecificMemberExpression(be.Left, declaringType, memberName) AndAlso 
               IsSpecificMemberExpression(be.Right, declaringType, memberName) Then
    
                Throw New Exception("Cannot have 'member' = 'member' in an expression!")
            End If
    
            Return IsSpecificMemberExpression(be.Left, declaringType, memberName) OrElse 
                   IsSpecificMemberExpression(be.Right, declaringType, memberName)
        End Function
    
    
        Friend Shared Function IsSpecificMemberExpression(
            ByVal exp As Expression, 
            ByVal declaringType As Type, 
            ByVal memberName As String) As Boolean
    
            Return (TypeOf exp Is MemberExpression) AndAlso 
                   (CType(exp, MemberExpression).Member.DeclaringType Is declaringType) AndAlso 
                   (CType(exp, MemberExpression).Member.Name = memberName)
        End Function
    
    
        Friend Shared Function GetValueFromEqualsExpression(
            ByVal be As BinaryExpression, 
            ByVal memberDeclaringType As Type, 
            ByVal memberName As String) As String
    
            If be.NodeType <> ExpressionType.Equal Then
                Throw New Exception("There is a bug in this program.")
            End If
    
            If be.Left.NodeType = ExpressionType.MemberAccess Then
                Dim mEx = CType(be.Left, MemberExpression)
    
                If mEx.Member.DeclaringType Is memberDeclaringType AndAlso 
                   mEx.Member.Name = memberName Then
                    Return GetValueFromExpression(be.Right)
                End If
            ElseIf be.Right.NodeType = ExpressionType.MemberAccess Then
                Dim mEx = CType(be.Right, MemberExpression)
    
                If mEx.Member.DeclaringType Is memberDeclaringType AndAlso 
                   mEx.Member.Name = memberName Then
                    Return GetValueFromExpression(be.Left)
                End If
            End If
    
            ' We should have returned by now.
            Throw New Exception("There is a bug in this program.")
        End Function
    
        Friend Shared Function GetValueFromExpression(ByVal expr As expression) As String
            If expr.NodeType = ExpressionType.Constant Then
                Return CStr(CType(expr, ConstantExpression).Value)
            Else
                Dim s = "The expression type {0} is not supported to obtain a value."
                Throw New InvalidQueryException(String.Format(s, expr.NodeType))
            End If
        End Function
    End Class
    
    using System;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        internal class ExpressionTreeHelpers
        {
            internal static bool IsMemberEqualsValueExpression(Expression exp, Type declaringType, string memberName)
            {
                if (exp.NodeType != ExpressionType.Equal)
                    return false;
    
                BinaryExpression be = (BinaryExpression)exp;
    
                // Assert.
                if (ExpressionTreeHelpers.IsSpecificMemberExpression(be.Left, declaringType, memberName) &&
                    ExpressionTreeHelpers.IsSpecificMemberExpression(be.Right, declaringType, memberName))
                    throw new Exception("Cannot have 'member' == 'member' in an expression!");
    
                return (ExpressionTreeHelpers.IsSpecificMemberExpression(be.Left, declaringType, memberName) ||
                    ExpressionTreeHelpers.IsSpecificMemberExpression(be.Right, declaringType, memberName));
            }
    
            internal static bool IsSpecificMemberExpression(Expression exp, Type declaringType, string memberName)
            {
                return ((exp is MemberExpression) &&
                    (((MemberExpression)exp).Member.DeclaringType == declaringType) &&
                    (((MemberExpression)exp).Member.Name == memberName));
            }
    
            internal static string GetValueFromEqualsExpression(BinaryExpression be, Type memberDeclaringType, string memberName)
            {
                if (be.NodeType != ExpressionType.Equal)
                    throw new Exception("There is a bug in this program.");
    
                if (be.Left.NodeType == ExpressionType.MemberAccess)
                {
                    MemberExpression me = (MemberExpression)be.Left;
    
                    if (me.Member.DeclaringType == memberDeclaringType && me.Member.Name == memberName)
                    {
                        return GetValueFromExpression(be.Right);
                    }
                }
                else if (be.Right.NodeType == ExpressionType.MemberAccess)
                {
                    MemberExpression me = (MemberExpression)be.Right;
    
                    if (me.Member.DeclaringType == memberDeclaringType && me.Member.Name == memberName)
                    {
                        return GetValueFromExpression(be.Left);
                    }
                }
    
                // We should have returned by now.
                throw new Exception("There is a bug in this program.");
            }
    
            internal static string GetValueFromExpression(Expression expression)
            {
                if (expression.NodeType == ExpressionType.Constant)
                    return (string)(((ConstantExpression)expression).Value);
                else
                    throw new InvalidQueryException(
                        String.Format("The expression type {0} is not supported to obtain a value.", expression.NodeType));
            }
        }
    }
    

    このクラスには、式ツリーの特定の型に関する情報の確認やそのデータの抽出のために使用できるメソッドが含まれています。このプロバイダーでは、それらのメソッドは LocationFinder クラスで使用され、クエリを表す式ツリーから場所情報を抽出します。

無効なクエリの例外型を追加するには

  • InvalidQueryException クラスをプロジェクトに追加します。

    Public Class InvalidQueryException
        Inherits Exception
    
        Private _message As String
    
        Public Sub New(ByVal message As String)
            Me._message = message & " "
        End Sub
    
        Public Overrides ReadOnly Property Message() As String
            Get
                Return "The client query is invalid: " & _message
            End Get
        End Property
    End Class
    
    using System;
    
    namespace LinqToTerraServerProvider
    {
        class InvalidQueryException : System.Exception
        {
            private string message;
    
            public InvalidQueryException(string message)
            {
                this.message = message + " ";
            }
    
            public override string Message
            {
                get
                {
                    return "The client query is invalid: " + message;
                }
            }
        }
    }
    

    このクラスは、プロバイダーがクライアントからの LINQ クエリを理解できないときにスローできる Exception 型を定義します。この無効なクエリの例外型を定義することで、プロバイダーは単なる Exception よりも具体的な例外をコード内のさまざまな場所からスローすることができます。

これで、プロバイダーのコンパイルに必要なすべての部分が追加されました。LinqToTerraServerProvider プロジェクトをビルドし、コンパイル エラーがないことを確認します。

LINQ プロバイダーのテスト

LINQ プロバイダーをテストするには、データ ソースに対する LINQ クエリを含むクライアント アプリケーションを作成します。

プロバイダーをテストするためにクライアント アプリケーションを作成するには

  1. ソリューションに新しいコンソール アプリケーション プロジェクトを追加し、「ClientApp」という名前を付けます。

  2. 新しいプロジェクトに、プロバイダー アセンブリへの参照を追加します。

  3. app.config ファイルをプロバイダー プロジェクトからクライアント プロジェクトにドラッグします(このファイルは Web サービスとの通信のために必要です)。

    [!メモ]

    Visual Basic では、ソリューション エクスプローラーapp.config ファイルを表示するために、[すべてのファイルを表示] ボタンをクリックすることが必要になる場合があります。

  4. 次の using ステートメント (Visual Basic では Imports ステートメント) を Program.cs (Visual Basic では Module1.vb) ファイルに追加します。

    using System;
    using System.Linq;
    using LinqToTerraServerProvider;
    
    Imports LinqToTerraServerProvider
    
  5. ファイル Program.cs (Visual Basic では Module1.vb) の Main メソッドに、次のコードを挿入します。

    QueryableTerraServerData<Place> terraPlaces = new QueryableTerraServerData<Place>();
    
    var query = from place in terraPlaces
                where place.Name == "Johannesburg"
                select place.PlaceType;
    
    foreach (PlaceType placeType in query)
        Console.WriteLine(placeType);
    
    Dim terraPlaces As New QueryableTerraServerData(Of Place)
    
    Dim query = From place In terraPlaces 
                Where place.Name = "Johannesburg" 
                Select place.PlaceType
    
    For Each placeType In query
        Console.WriteLine(placeType.ToString())
    Next
    

    このコードは、プロバイダーで定義した IQueryable<T> 型の新しいインスタンスを作成し、その後 LINQ を使用してそのオブジェクトをクエリします。クエリは、等価式を使用して、データを取得する場所を指定します。データ ソースは IQueryable を実装するため、コンパイラはクエリ式構文を、Queryable で定義した標準クエリ演算子の呼び出しに変換します。この標準クエリ演算子メソッドは内部的に式ツリーをビルドし、IQueryProvider 実装の一部として実装した Execute メソッドまたは CreateQuery メソッドを呼び出します。

  6. ClientApp をビルドします。

  7. このクライアント アプリケーションをソリューションの「スタートアップ」プロジェクトに設定します。ソリューション エクスプローラーで、ClientApp プロジェクトを右クリックし、[スタートアップ プロジェクトに設定] をクリックします。

  8. プログラムを実行し、結果を表示します。3 個前後の結果が表示されます。

より複雑なクエリ機能の追加

ここまでの説明では、プロバイダーを使用してクライアントが LINQ クエリに場所情報を指定する方法は非常に限られています。具体的に言えば、プロバイダーが場所情報を取得するための方法は、Place.Name == "Seattle"、Place.State == "Alaska" (Visual Basic では Place.Name = "Seattle"、Place.State = "Alaska") などの等価式しかありません。

場所情報を別の方法で指定できるようにするための方法を次の手順で示します。このコードを追加すると、プロバイダーは place.Name.StartsWith("Seat") などのメソッド呼び出し式から場所情報を抽出できるようになります。

String.StartsWith を含む述語のサポートを追加するには

  1. LinqToTerraServerProvider プロジェクトで、VisitMethodCall メソッドを LocationFinder クラス定義に追加します。

    Protected Overrides Function VisitMethodCall(ByVal m As MethodCallExpression) As Expression
        If m.Method.DeclaringType Is GetType(String) And m.Method.Name = "StartsWith" Then
            If ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "Name") OrElse
               ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "State") Then
                _locations.Add(ETH.GetValueFromExpression(m.Arguments(0)))
                Return m
            End If
        End If
    
        Return MyBase.VisitMethodCall(m)
    End Function
    
    protected override Expression VisitMethodCall(MethodCallExpression m)
    {
        if (m.Method.DeclaringType == typeof(String) && m.Method.Name == "StartsWith")
        {
            if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "Name") ||
            ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "State"))
            {
                locations.Add(ExpressionTreeHelpers.GetValueFromExpression(m.Arguments[0]));
                return m;
            }
        }
    
        return base.VisitMethodCall(m);
    }
    
  2. LinqToTerraServerProvider プロジェクトを再コンパイルします。

  3. プロバイダーの新しい機能をテストするには、ClientApp プロジェクトのファイル Program.cs (Visual Basic では Module1.vb) を開きます。Main メソッドのコードを次のコードで置き換えます。

    QueryableTerraServerData<Place> terraPlaces = new QueryableTerraServerData<Place>();
    
    var query = from place in terraPlaces
                where place.Name.StartsWith("Lond")
                select new { place.Name, place.State };
    
    foreach (var obj in query)
        Console.WriteLine(obj);
    
    Dim terraPlaces As New QueryableTerraServerData(Of Place)
    
    Dim query = From place In terraPlaces 
                Where place.Name.StartsWith("Lond") 
                Select place.Name, place.State
    
    For Each obj In query
        Console.WriteLine(obj)
    Next
    
  4. プログラムを実行し、結果を表示します。29 個前後の結果が表示されます。

クライアント クエリが 2 つの追加メソッド (具体的には Enumerable.ContainsList<T>.Contains) を使用して場所情報を指定できるようにする機能をプロバイダーに追加する方法を次の手順で示します。このコードを追加すると、プロバイダーは placeList.Contains(place.Name) (placeList コレクションはクライアントが指定する具体的なリスト) などのクライアント クエリのメソッド呼び出し式から場所情報を抽出できるようになります。クライアントが Contains メソッドを使用できるようにすることの利点は、placeList に追加するだけで場所をいくつでも指定できるようになることです。場所の数を変更しても、クエリの構文は変更されません。

'where' 句に Contains メソッドを含むクエリに対するサポートを追加するには

  1. LinqToTerraServerProvider プロジェクトの LocationFinder クラス定義で、VisitMethodCall メソッドを次のコードに置き換えます。

    Protected Overrides Function VisitMethodCall(ByVal m As MethodCallExpression) As Expression
        If m.Method.DeclaringType Is GetType(String) And m.Method.Name = "StartsWith" Then
            If ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "Name") OrElse
               ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "State") Then
                _locations.Add(ETH.GetValueFromExpression(m.Arguments(0)))
                Return m
            End If
        ElseIf m.Method.Name = "Contains" Then
            Dim valuesExpression As Expression = Nothing
    
            If m.Method.DeclaringType Is GetType(Enumerable) Then
                If ETH.IsSpecificMemberExpression(m.Arguments(1), GetType(Place), "Name") OrElse
                   ETH.IsSpecificMemberExpression(m.Arguments(1), GetType(Place), "State") Then
                    valuesExpression = m.Arguments(0)
                End If
    
            ElseIf m.Method.DeclaringType Is GetType(List(Of String)) Then
                If ETH.IsSpecificMemberExpression(m.Arguments(0), GetType(Place), "Name") OrElse
                   ETH.IsSpecificMemberExpression(m.Arguments(0), GetType(Place), "State") Then
                    valuesExpression = m.Object
                End If
            End If
    
            If valuesExpression Is Nothing OrElse valuesExpression.NodeType <> ExpressionType.Constant Then
                Throw New Exception("Could not find the location values.")
            End If
    
            Dim ce = CType(valuesExpression, ConstantExpression)
    
            Dim placeStrings = CType(ce.Value, IEnumerable(Of String))
            ' Add each string in the collection to the list of locations to obtain data about.
            For Each place In placeStrings
                _locations.Add(place)
            Next
    
            Return m
        End If
    
        Return MyBase.VisitMethodCall(m)
    End Function
    
    protected override Expression VisitMethodCall(MethodCallExpression m)
    {
        if (m.Method.DeclaringType == typeof(String) && m.Method.Name == "StartsWith")
        {
            if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "Name") ||
            ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "State"))
            {
                locations.Add(ExpressionTreeHelpers.GetValueFromExpression(m.Arguments[0]));
                return m;
            }
    
        }
        else if (m.Method.Name == "Contains")
        {
            Expression valuesExpression = null;
    
            if (m.Method.DeclaringType == typeof(Enumerable))
            {
                if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[1], typeof(Place), "Name") ||
                ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[1], typeof(Place), "State"))
                {
                    valuesExpression = m.Arguments[0];
                }
            }
            else if (m.Method.DeclaringType == typeof(List<string>))
            {
                if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[0], typeof(Place), "Name") ||
                ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[0], typeof(Place), "State"))
                {
                    valuesExpression = m.Object;
                }
            }
    
            if (valuesExpression == null || valuesExpression.NodeType != ExpressionType.Constant)
                throw new Exception("Could not find the location values.");
    
            ConstantExpression ce = (ConstantExpression)valuesExpression;
    
            IEnumerable<string> placeStrings = (IEnumerable<string>)ce.Value;
            // Add each string in the collection to the list of locations to obtain data about.
            foreach (string place in placeStrings)
                locations.Add(place);
    
            return m;
        }
    
        return base.VisitMethodCall(m);
    }
    

    このメソッドは、Contains が適用されるコレクションの各文字列を、Web サービスのクエリ対象となる場所のリストに追加します。Contains という名前のメソッドは、EnumerableList<T> の両方で定義されています。したがって、これらの宣言型の両方を VisitMethodCall メソッドでチェックする必要があります。Enumerable.Contains は拡張メソッドとして定義されているため、メソッドの適用先のコレクションは、実際にはメソッドの最初の引数になります。List.Contains はインスタンス メソッドとして定義されているため、メソッドの適用先のコレクションが、そのメソッドを受け取るオブジェクトです。

  2. LinqToTerraServerProvider プロジェクトを再コンパイルします。

  3. プロバイダーの新しい機能をテストするには、ClientApp プロジェクトのファイル Program.cs (Visual Basic では Module1.vb) を開きます。Main メソッドのコードを次のコードで置き換えます。

    QueryableTerraServerData<Place> terraPlaces = new QueryableTerraServerData<Place>();
    
    string[] places = { "Johannesburg", "Yachats", "Seattle" };
    
    var query = from place in terraPlaces
                where places.Contains(place.Name)
                orderby place.State
                select new { place.Name, place.State };
    
    foreach (var obj in query)
        Console.WriteLine(obj);
    
    Dim terraPlaces As New QueryableTerraServerData(Of Place)
    
    Dim places = New String() {"Johannesburg", "Yachats", "Seattle"}
    
    Dim query = From place In terraPlaces 
                Where places.Contains(place.Name) 
                Order By place.State 
                Select place.Name, place.State
    
    For Each obj In query
        Console.WriteLine(obj)
    Next
    
  4. プログラムを実行し、結果を表示します。5 個前後ほどの結果が表示されます。

次の手順

このチュートリアルのトピックでは、Web サービスの 1 つのメソッドに対して LINQ プロバイダーを作成する方法について説明しました。引き続き LINQ プロバイダーの開発を行う場合は、次のようなタスクの開発を検討してみてください。

  • LINQ プロバイダーがクライアント クエリの場所を指定するための別の方法を処理できるようにする。

  • TerraServer-USA Web サービスが公開する別のメソッドを調べ、それらの 1 つと連結する LINQ プロバイダーを作成する。

  • 関心のある別の Web サービスを見つけ、そのための LINQ プロバイダーを作成する。

  • Web サービス以外のデータ ソース用の LINQ プロバイダーを作成する。

独自の LINQ プロバイダーを作成する方法の詳細については、MSDN ブログの「LINQ: Building an IQueryable Provider (LINQ: IQueryable プロバイダーの作成)」を参照してください。

参照

処理手順

LINQ のサンプル

方法: 式ツリーを変更する (C# および Visual Basic)

関連項目

IQueryable<T>

IOrderedQueryable<T>

概念

データ ソースの LINQ クエリの有効化

Visual Studio での Windows Communication Foundation サービスと WCF データ サービス