更新 :
2008 年 7 月
この上級者向けのトピックでは、カスタム LINQ プロバイダを作成するための詳細な手順について説明します。すべて読み終えると、作成したプロバイダを使用して、TerraServer-USA Web サービスに対する LINQ クエリを作成できるようになります。
TerraServer-USA Web サービスには、米国の航空画像のデータベースとのインターフェイスが用意されています。またこのサービスは、場所の名前の一部または全体を指定すると、米国内の場所に関する情報を返すメソッドも公開します。この GetPlaceList という名前のメソッドが、LINQ プロバイダが呼び出すメソッドです。プロバイダは、Windows Communication Foundation (WCF) を使用して、Web サービスと通信します。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<(Of <(T>)>)、IOrderedQueryable<(Of <(T>)>)、および IQueryProvider を実装する。
Web サービスのデータを表すカスタム .NET 型を追加する。
クエリ コンテキスト クラスおよび Web サービスからデータを取得するクラスを作成する。
Queryable.Where メソッドの最も内側にある呼び出しを表す式を検索する式ツリー ビジタ サブクラスを作成する。
Web サービス要求で使用する情報を LINQ クエリから抽出する式ツリー ビジタ サブクラスを作成する。
完全な LINQ クエリを表す式ツリーを変更する式ツリー ビジタ サブクラスを作成する。
エバリュエータ クラスを使用して式ツリーを部分的に評価する。これは LINQ クエリのローカル変数参照をすべて値に変換するため、この手順が必要となります。
式ツリー ヘルパー クラスおよび新しい例外クラスを作成する。
LINQ クエリを含むクライアント アプリケーションから LINQ プロバイダをテストする。
より複雑なクエリ機能を LINQ プロバイダに追加する。
このチュートリアルを完了するには、次のコンポーネントが必要です。
メモ : |
|---|
| お使いのマシンで、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 サービスの概要」を参照してください。
これで、app.config という名前のファイル、QueryableTerraServerData.cs (または QueryableTerraServerData.vb) という名前のファイル、および TerraServerReference という名前のサービス参照を持つプロジェクトが作成されました。
LINQ プロバイダを作成するには、少なくとも IQueryable<(Of <(T>)>) インターフェイスと IQueryProvider インターフェイスを実装する必要があります。IQueryable<(Of <(T>)>) と IQueryProvider は別の必要なインターフェイスから派生するため、これら 2 つのインターフェイスを実装することによって、LINQ プロバイダに要求される他のインターフェイスも実装することになります。
OrderBy や ThenBy などの並べ替えクエリ演算子をサポートする場合は、IOrderedQueryable<(Of <(T>)>) インターフェイスも実装する必要があります。IOrderedQueryable<(Of <(T>)>) は IQueryable<(Of <(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<(Of <(T>)>) 実装は、IQueryable で宣言される 3 つのプロパティと、IEnumerable および IEnumerable<(Of <(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 です。これには、市、公園、湖など、1 か所の地理的な場所の情報が格納されます。
また、このコードには PlaceType という名前の列挙型も含まれます。これは地理的な場所のさまざまな型を定義し、Place クラスで使用されます。
カスタム結果型を作成するには
プロジェクトに Place クラスと PlaceType 列挙型を追加します。
Public Class Place
' Properties.
Private _Name As String
Private _State As String
Private _PlaceType As PlaceType
Public Property Name() As String
Get
Return _Name
End Get
Private Set(ByVal value As String)
_Name = value
End Set
End Property
Public Property State() As String
Get
Return _State
End Get
Private Set(ByVal value As String)
_State = value
End Set
End Property
Public Property PlaceType() As PlaceType
Get
Return _PlaceType
End Get
Private Set(ByVal value As PlaceType)
_PlaceType = value
End Set
End Property
' 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.CopyAndModify(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.CopyAndModify(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<(Of <(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<(Of <(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 メソッド呼び出し式を検索するビジタを作成するには
ExpressionVisitor クラスをプロジェクトに追加します。このコードは「方法 : 式ツリー ビジタを実装する」にあります。System.Collections.Generic、System.Collections.ObjectModel、および System.Linq.Expressions 名前空間のための using ディレクティブ (Visual Basic では Imports ステートメント) をファイルに追加します。
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 の最も内側にある呼び出しを表す式を検索します。この最も内側にある式は、プロバイダによる検索場所の抽出元となる式です。
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 に渡す述語から場所情報を抽出するために使用されます。これは基本の式ツリー ビジタ クラスから派生したクラスで、VisitBinary メソッドだけをオーバーライドします。
式ツリー ビジタ基本クラスは、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
Friend Function CopyAndModify(ByVal expression As Expression) As Expression
Return Me.Visit(expression)
End Function
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;
}
internal Expression CopyAndModify(Expression expression)
{
return this.Visit(expression);
}
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;
}
}
}
このクラスは基本の式ツリー ビジタ クラスから派生し、VisitConstant メソッドをオーバーライドします。このメソッドでは、最も内側にある標準クエリ演算子呼び出しが適用されるオブジェクトを Place オブジェクトの具体的なリストに置き換えます。
CopyAndModify メソッドは Visit メソッドの基本クラス実装を呼び出します。Visit メソッドは protected (Visual Basic では Protected) であり、クエリ コンテキスト クラスから直接呼び出すことができないので、この CopyAndModify メソッドが必要です。
この式ツリー修飾子クラスが式ツリー ビジタを使用します。ビジタは式ツリーを走査、確認、およびコピーするために設計されているためです。基本の式ツリー ビジタ クラスから派生することで、このクラスがその機能の実行で必要になるコードが最小限で済みます。
クライアント クエリの Queryable.Where メソッドに渡される述語には、ラムダ式のパラメータに依存しないサブ式が含まれる場合があります。分離されたこれらのサブ式は直ちに評価することができ、そうする必要があります。このサブ式は、値に変換する必要のあるローカル変数またはメンバ変数の参照の場合もあります。
次のクラスはメソッド PartialEval(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 And _
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 プロバイダでも再利用できます。
式ツリー ヘルパー クラスを作成するには
無効なクエリの例外型を追加するには
これで、プロバイダのコンパイルに必要なすべての部分が追加されました。LinqToTerraServerProvider プロジェクトをビルドし、コンパイル エラーがないことを確認します。
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<(Of <(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") Or _
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<(Of <(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") Or _
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") Or _
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") Or _
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<(Of <(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 プロバイダを作成する。
処理手順
概念
参照
その他の技術情報
日付 | 履歴 | 理由 |
|---|
2008 年 7 月 | TerraServer サンプルへのリンクを追加 | コンテンツ バグ修正 |