チュートリアル : 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 でプロジェクトを作成するには
Visual Studioでは、[クラス ライブラリ] の新しいアプリケーションを作成します。プロジェクト LinqToTerraServerProviderを付けます。
ソリューション エクスプローラーで Class1.cs (または Class1.vb) ファイルを選択し、名前を「QueryableTerraServerData.cs」(または「QueryableTerraServerData.vb」) に変更します。ポップアップ表示されるダイアログ ボックスで [はい] をクリックして、コード要素へのすべて参照の名前を変更します。
実行可能クライアント アプリケーションはプロバイダー アセンブリをそのプロジェクトへの参照として追加するので、プロバイダーを Visual Studio のクラス ライブラリ プロジェクトとして作成します。
Web サービスにサービス参照を追加するには
ソリューション エクスプローラーで LinqToTerraServerProvider プロジェクトを右クリックし、[サービス参照の追加] をクリックします。
[サービス参照の追加] ダイアログ ボックスが表示されます。
[アドレス] ボックスに「http://terraserver.microsoft.com/TerraService2.asmx」と入力します。
[名前空間] ボックスに「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) に、次のコードを追加します。
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.Where の IQueryable パラメーターに割り当てられません。そのため、その型は 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 メソッド呼び出し式を検索するビジタを作成するには
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 の最も内側にある呼び出しを表す式を検索します。この最も内側にある式は、プロバイダーによる検索場所の抽出元となる式です。
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 クエリを含むクライアント アプリケーションを作成します。
プロバイダーをテストするためにクライアント アプリケーションを作成するには
ソリューションに新しいコンソール アプリケーション プロジェクトを追加し、「ClientApp」という名前を付けます。
新しいプロジェクトに、プロバイダー アセンブリへの参照を追加します。
app.config ファイルをプロバイダー プロジェクトからクライアント プロジェクトにドラッグします(このファイルは Web サービスとの通信のために必要です)。
[!メモ]
Visual Basic では、ソリューション エクスプローラーで app.config ファイルを表示するために、[すべてのファイルを表示] ボタンをクリックすることが必要になる場合があります。
次の using ステートメント (Visual Basic では Imports ステートメント) を Program.cs (Visual Basic では Module1.vb) ファイルに追加します。
using System; using System.Linq; using LinqToTerraServerProvider;
Imports LinqToTerraServerProvider
ファイル 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 メソッドを呼び出します。
ClientApp をビルドします。
このクライアント アプリケーションをソリューションの「スタートアップ」プロジェクトに設定します。ソリューション エクスプローラーで、ClientApp プロジェクトを右クリックし、[スタートアップ プロジェクトに設定] をクリックします。
プログラムを実行し、結果を表示します。3 個前後の結果が表示されます。
より複雑なクエリ機能の追加
ここまでの説明では、プロバイダーを使用してクライアントが LINQ クエリに場所情報を指定する方法は非常に限られています。具体的に言えば、プロバイダーが場所情報を取得するための方法は、Place.Name == "Seattle"、Place.State == "Alaska" (Visual Basic では Place.Name = "Seattle"、Place.State = "Alaska") などの等価式しかありません。
場所情報を別の方法で指定できるようにするための方法を次の手順で示します。このコードを追加すると、プロバイダーは place.Name.StartsWith("Seat") などのメソッド呼び出し式から場所情報を抽出できるようになります。
String.StartsWith を含む述語のサポートを追加するには
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); }
LinqToTerraServerProvider プロジェクトを再コンパイルします。
プロバイダーの新しい機能をテストするには、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
プログラムを実行し、結果を表示します。29 個前後の結果が表示されます。
クライアント クエリが 2 つの追加メソッド (具体的には Enumerable.Contains と List<T>.Contains) を使用して場所情報を指定できるようにする機能をプロバイダーに追加する方法を次の手順で示します。このコードを追加すると、プロバイダーは placeList.Contains(place.Name) (placeList コレクションはクライアントが指定する具体的なリスト) などのクライアント クエリのメソッド呼び出し式から場所情報を抽出できるようになります。クライアントが Contains メソッドを使用できるようにすることの利点は、placeList に追加するだけで場所をいくつでも指定できるようになることです。場所の数を変更しても、クエリの構文は変更されません。
'where' 句に Contains メソッドを含むクエリに対するサポートを追加するには
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 という名前のメソッドは、Enumerable と List<T> の両方で定義されています。したがって、これらの宣言型の両方を VisitMethodCall メソッドでチェックする必要があります。Enumerable.Contains は拡張メソッドとして定義されているため、メソッドの適用先のコレクションは、実際にはメソッドの最初の引数になります。List.Contains はインスタンス メソッドとして定義されているため、メソッドの適用先のコレクションが、そのメソッドを受け取るオブジェクトです。
LinqToTerraServerProvider プロジェクトを再コンパイルします。
プロバイダーの新しい機能をテストするには、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
プログラムを実行し、結果を表示します。5 個前後ほどの結果が表示されます。
次の手順
このチュートリアルのトピックでは、Web サービスの 1 つのメソッドに対して LINQ プロバイダーを作成する方法について説明しました。引き続き LINQ プロバイダーの開発を行う場合は、次のようなタスクの開発を検討してみてください。
LINQ プロバイダーがクライアント クエリの場所を指定するための別の方法を処理できるようにする。
TerraServer-USA Web サービスが公開する別のメソッドを調べ、それらの 1 つと連結する LINQ プロバイダーを作成する。
関心のある別の Web サービスを見つけ、そのための LINQ プロバイダーを作成する。
Web サービス以外のデータ ソース用の LINQ プロバイダーを作成する。
独自の LINQ プロバイダーを作成する方法の詳細については、MSDN ブログの「LINQ: Building an IQueryable Provider (LINQ: IQueryable プロバイダーの作成)」を参照してください。
参照
処理手順
方法: 式ツリーを変更する (C# および Visual Basic)
関連項目
概念
Visual Studio での Windows Communication Foundation サービスと WCF データ サービス