この記事は機械翻訳されています。英語版の記事を表示するには、[英語] のチェック ボックスをオンにしてください。また、テキストにマウス ポインターを合わせると、ポップアップ ウィンドウに英語のテキストを表示することもできます。
翻訳
英語
Visual Studio 2017 を使用することをお勧めします

チュートリアル : 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 プロバイダーに要求される他のインターフェイスも実装することになります。

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

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

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

    
    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 クラスをプロジェクトに追加します。

    
    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 列挙型を追加します。

    
    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 クラスをプロジェクトに追加します。

    
    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 ではモジュール) をプロジェクトに追加します。

    
    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 クラスを継承しています。

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

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

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

    
    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 クラスをプロジェクトに追加します。

    
    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 クラスをプロジェクトに追加します。

    
    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 ではモジュール) をプロジェクトに追加します。

    
    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 クラスをプロジェクトに追加します。

    
    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 クラスをプロジェクトに追加します。

    
    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 クエリを含むクライアント アプリケーションを作成します。

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

  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 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 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 プロバイダーの作成)」を参照してください。

表示: