逐步解說:建立 IQueryable LINQ 提供者
這個進階主題提供建立自訂 LINQ 提供者的逐步指示。完成之後,您將能夠使用自己建立的提供者,針對 TerraServer-USA Web 服務撰寫 LINQ 查詢。
TerraServer-USA Web 服務提供美國空照影像資料庫的介面。 它也公開 (Expose) 一種方法,可以根據指定之部分或完整的地點名稱,傳回美國境內地點的相關資訊。 這個方法 (命名為 GetPlaceList) 是您的 LINQ 提供者所呼叫的方法。提供者將使用 Windows Communication Foundation (WCF) 與 Web 服務進行通訊。 如需 TerraServer-USA Web 服務的詳細資訊,請參閱 TerraServer-USA Web 服務概觀。
這個提供者是相當簡單的 IQueryable 提供者。 此提供者預期其本身所處理的查詢中應該有特定的資訊,而且具有封閉型別系統,以透過公開單一型別來呈現結果資料。 這個提供者只會在代表查詢的運算式樹狀架構中檢查一種方法呼叫運算式型別,也就是最內部的 Where 呼叫。 它會擷取其本身必須擁有才能透過此運算式查詢 Web 服務的資料, 接著再呼叫 Web 服務,並將傳回的資料插入運算式樹狀架構中,初始 IQueryable 資料來源所在的位置。 剩餘的查詢執行部分則由標準查詢運算子的 Enumerable 實作 (Implementation) 負責處理。
本主題中的程式碼範例提供 C# 和 Visual Basic 兩種版本。
這個逐步解說將說明下列工作:
在 Visual Studio 中建立專案。
實作 IQueryable LINQ 提供者需要的介面:IQueryable<T>、IOrderedQueryable<T> 和 IQueryProvider。
加入自訂 .NET 型別以代表 Web 服務中的資料。
建立查詢內容類別 (Class) 和包含 Web 服務中資料的類別。
建立運算式樹狀架構訪問項子類別 (Subclass),以尋找代表最內部之 Queryable.Where 方法呼叫的運算式。
建立運算式樹狀架構訪問項子類別,以便從 LINQ 查詢中擷取要在 Web 服務要求中使用的資訊。
建立運算式樹狀架構訪問項子類別,以修改代表完整 LINQ 查詢的運算式樹狀架構。
使用評估工具類別來部分評估運算式樹狀架構。 這是必要的步驟,因為它會將 LINQ 查詢中的所有區域變數參考轉譯成值。
建立運算式樹狀架構 Helper 類別和新的例外狀況 (Exception) 類別。
從包含 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 中的 [類別庫],因為可執行用戶端應用程式將會加入提供者組件 (Assembly),做為其專案的參考。
若要加入 Web 服務的參考
在 [方案總管] 中,以滑鼠右鍵按一下 [LinqToTerraServerProvider] 專案,然後按一下 [加入服務參考]。
[加入服務參考] 對話方塊隨即開啟。
在 [位址] 方塊中輸入 http://terraserver.microsoft.com/TerraService2.asmx。
在 [命名空間] 方塊中,輸入 TerraServerReference,然後按一下 [確定]。
TerraServer-USA Web 服務將新增為服務參考,使應用程式能夠透過 Windows Communication Foundation (WCF) 與 Web 服務進行通訊。 藉由加入專案的服務參考,Visual Studio 會產生 [app.config] 檔案,其中包含 Proxy 和 Web 服務的端點。 如需詳細資訊,請參閱Visual Studio 中的 Windows Communication Foundation 服務和 WCF 資料服務。
您現在已經擁有一個專案,而且專案中包含一個名稱為 [app.config] 的檔案、一個名稱為 [QueryableTerraServerData.cs] (或 [QueryableTerraServerData.vb]) 的檔案,以及一個名稱為 [TerraServerReference] 的服務參考。
實作必要介面
若要建立 LINQ 提供者,您至少必須實作 IQueryable<T> 和 IQueryProvider 介面。IQueryable<T> 和 IQueryProvider 都是衍生自其他必要介面;因此,實作這兩個介面時,您也同時實作了 LINQ 提供者需要的其他介面。
如果想要支援排序查詢運算子 (例如 OrderBy 和 ThenBy),您還必須實作 IOrderedQueryable<T> 介面。 因為 IOrderedQueryable<T> 衍生自 IQueryable<T>,所以您可以同時在一種型別中實作這兩個介面,而這也是這個提供者所採用的做法。
若要實作 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 中宣告的三個屬性,以及 IEnumerable 和 IEnumerable<T> 中宣告的兩個列舉方法。
這個類別具有兩個建構函式 (Constructor)。 第一個建構函式是從用戶端應用程式呼叫,以建立撰寫 LINQ 查詢時所針對的物件。 第二個建構函式則由 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 介面時所需要的方法。 其中兩個 CreateQuery 方法會建立與資料來源相關聯的查詢, 而兩個 Execute 方法則會傳送要執行的這類查詢。
非泛型 CreateQuery 方法會使用反映 (Reflection) 取得執行此方法時其建立之查詢所傳回的序列項目型別。 接著它會使用 Activator 類別建構使用該項目型別做為其泛型型別引數所建構的新 QueryableTerraServerData 執行個體。 呼叫非泛型 CreateQuery 方法的結果與使用正確型別引數呼叫 CreateQuery 方法時相同。
大部分的查詢執行邏輯都是在稍後所要加入的不同類別中處理。 這項功能則是在其他地方處理,因為它是正在查詢之資料來源的專屬功能,但這個類別中的程式碼卻是任何 LINQ 提供者都通用。 若要將此程式碼用於不同的提供者,您可能必須變更類別的名稱,以及其中兩個方法所參考之查詢內容型別的名稱。
加入自訂型別以表示結果資料
您需要 .NET 型別來表示從 Web 服務取得的資料。 這個型別將會在用戶端 LINQ 查詢中用來定義其所需要的結果。 下列程序會建立此類型別。這個型別的名稱為 Place,其中包含單一地理位置 (例如某個城市、公園或湖泊) 的相關資訊。
此程式碼也包含一個名稱為 PlaceType 的列舉型別;此型別定義各種不同的地理位置類型,可在 Place 類別中使用。
若要建立自訂結果型別
將 Place 類別和 PlaceType 列舉型別 (Enumeration) 加入至專案。
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 呼叫是 LINQ 呼叫中最先出現的 where 子句 (在 Visual Basic 中則為 Where 子句) 或 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 之述詞 (Predicate) 的 Lambda 運算式, 然後再將述詞運算式傳遞至要進行部分評估的方法,以便將區域變數的所有參考全部轉譯成值。 接著它還會呼叫方法,從述詞擷取要求的地點,並呼叫另一個方法,以便從 Web 服務取得結果資料。
在下一個步驟中,這個程式碼會複製表示 LINQ 查詢的運算式樹狀架構,並對運算式樹狀架構進行一項修改。 此程式碼會使用運算式樹狀架構訪問項子類別,將套用最內部查詢運算子呼叫的資料來源取代成從 Web 服務取得之 Place 物件的具體清單。
在將 Place 物件清單插入運算式樹狀架構之前,其型別會透過呼叫 AsQueryable,從 IEnumerable 變更為 IQueryable。 這種型別變更是必要的,因為在重寫運算式樹狀架構時,將會重新建構表示最內部查詢運算子方法之方法呼叫的節點。 重新建構此節點的原因是因為其中一個引數已經變更 (即其套用對象的資料來源)。 若有任何引數無法指派給其傳遞之目標方法的對應參數,用於重新建構節點的 Call(Expression, MethodInfo, IEnumerable<Expression>) 方法便會擲回例外狀況。 在這種情況下,Place 物件的 IEnumerable 清單將無法指派給 Queryable.Where 的 IQueryable 參數。 因此,其型別會變更為 IQueryable。
透過將其型別變更為 IQueryable,此集合也會取得由 Provider 屬性存取的 IQueryProvider 成員,可以用來建立或執行查詢。 IQueryable°Place 集合的動態型別是 EnumerableQuery,其為 System.Linq API 的內部型別。 與此型別相關聯的查詢提供者會透過將 Queryable 標準查詢運算子呼叫取代成對等 Enumerable 運算子來執行查詢,使得查詢實際上會變成 LINQ to Objects 查詢。
TerraServerQueryContext 類別中的最後一個程式碼會呼叫 Place 物件之 IQueryable 清單上兩個方法的其中一個。 如果用戶端查詢傳回可列舉的結果,它會呼叫 CreateQuery,而如果用戶端查詢傳回不可列舉的結果,則會呼叫 Execute。
此類別中的程式碼是這個 TerraServer-USA 提供者專用的程式碼。 因此,這個程式碼是封裝在 TerraServerQueryContext 類別中,而不是直接插入較為通用的 IQueryProvider 實作中。
您要建立的提供者只需要 Queryable.Where 述詞中的資訊,便可查詢 Web 服務。.因此,它會使用 LINQ to Objects 完成利用內部 EnumerableQuery 型別執行 LINQ 查詢的工作。 另一種使用 LINQ to Objects 執行查詢的方法是讓用戶端將要由 LINQ to Objects 執行的查詢部分,包裝在 LINQ to Objects 查詢中。 完成這項作業的方式是針對其餘的查詢部分呼叫 AsEnumerable<TSource>,而這些查詢部分就是提供者針對其特定用途所需要的部分。 這種實作的優點在於自訂提供者和 LINQ to Objects 之間的工作劃分較為清楚。
注意事項 |
---|
本主題所呈現的提供者是本身擁有最低查詢支援的簡單提供者。 因此,此提供者十分倚賴 LINQ to Objects 來執行查詢。 複雜的 LINQ 提供者 (例如 LINQ to SQL),可以支援完整的查詢,而不需要將任何工作交給 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 的型別 (它是由 Windows Communication Foundation (WCF) 自動為專案產生的型別) 來呼叫 Web 服務方法 GetPlaceList, 接著再將每項結果從 Web 服務方法的傳回型別轉譯成提供者針對資料所定義的 .NET 型別。
這個程式碼包含兩項檢查,可增強提供者程式庫的使用性。 第一項檢查將每項查詢呼叫 Web 服務的總次數限制為五次,藉以限制用戶端應用程式等候回應時間的上限。 用戶端查詢中指定的每個地點都會產生一項 Web 服務要求。 因此,如果查詢包含的地點超過五個,提供者便會擲回例外狀況。
第二項檢查會判斷 Web 服務所傳回的結果數目是否等於它可以傳回的結果數目上限。 如果結果的數目等於數目上限,則 Web 服務傳回的結果可能已經遭到刪減。 此時提供者將會擲回例外狀況,而不會將不完整的清單傳回給用戶端。
加入運算式樹狀架構訪問項類別
若要建立用來尋找最內部 Where 方法呼叫運算式的訪問項
將繼承 ExpressionVisitor 類別的 InnermostWhereFinder 類別加入至專案。
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 呼叫的運算式。 這個最內部的運算式就是提供者從中擷取搜尋地點的運算式。
在檔案中為下列命名空間加入 using 指示詞 (在 Visual Basic 中為 Imports 陳述式):System.Collections.Generic、System.Collections.ObjectModel 和 System.Linq.Expressions。
若要建立擷取資料以查詢 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 物件的具體清單。
此運算式樹狀架構修飾詞 (Modifier) 類別會使用運算式樹狀架構訪問項,因為它是專為周遊、檢查及複製運算式樹狀架構而設計的訪問項。 藉由衍生自基底運算式樹狀架構訪問項類別,這個類別只需要最少的程式碼便可執行其功能。
加入運算式評估工具
傳遞至用戶端查詢中 Queryable.Where 方法的述詞可能包含不需要 Lambda 運算式之參數的子運算式。 這些獨立的子運算式可以而且應該立即進行評估。 它們可能是必須轉譯成值的區域變數或成員變數的參考。
下一個類別會公開 PartialEval(Expression) 方法,而這個方法會找出運算式中可以立即進行評估的子樹狀架構 (如果有的話)。 接著它會透過建立及編譯 Lambda 運算式,並叫用 (Invoke) 傳回的委派 (Delegate) 等步驟,評估這些運算式。 最後,它會將子樹狀架構取代成表示常數值的新節點。 這個程序稱為部分評估。
若要加入類別以執行運算式樹狀架構的部分評估
將 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; } } } }
加入 Helper 類別
本節包含您的提供者所適用之三個 Helper 類別的程式碼。
若要加入 System.Linq.IQueryProvider 實作使用的 Helper 類別
將 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 實作會使用這個 Helper 類別。
TypeSystem.GetElementType 使用反映來取得 IEnumerable<T> (在 Visual Basic 中則為 IEnumerable(Of T)) 集合的泛型型別引數。 這個方法會從查詢提供者實作中的非泛型 CreateQuery 方法呼叫,以提供查詢結果集合的項目型別。
此 Helper 類別並非專屬於這個 TerraServer-USA Web 服務提供者, 因此任何 LINQ 提供者都可以重複使用此程式碼。
若要建立運算式樹狀架構 Helper 類別
將 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。
您現在已經加入編譯提供者時需要的所有項目。 接下來請建置 (Build) [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,因此編譯器 (Compiler) 會將查詢運算式語法轉譯成 Queryable 中定義之標準查詢運算子的呼叫。 這些標準查詢運算子方法會在內部建置一個運算式樹狀架構,並呼叫您在實作 IQueryProvider 時一併實作的 Execute 或 CreateQuery 方法。
建置 ClientApp。
將這個用戶端應用程式設定成方案的「啟始」專案。 在 [方案總管] 中,以滑鼠右鍵按一下 [ClientApp] 專案,並選取 [設定為啟始專案]。
執行程式並檢視結果。 產生的結果應該有三個左右。
加入較複雜的查詢功能
目前為止您所擁有的提供者只能為用戶端提供少數幾種在 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 個左右。
下一個程序將說明如何將功能加入提供者中,使用戶端查詢能夠利用兩個額外的方法 (也就是 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 服務的地點清單。 Enumerable 和 List<T> 中都會定義一個名稱為 Contains 的方法。 因此,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 服務的單一方法來建立 LINQ 提供者。 如果您想採用其他的 LINQ 提供者開發方式,請考慮下列可能的方案:
啟用 LINQ 提供者以處理在用戶端查詢中指定地點的其他方式。
檢閱 TerraServer-USA Web 服務公開的其他方法,並建立與其中一個方法連結的 LINQ 提供者。
尋找您所需要的其他 Web 服務,並針對該服務建立 LINQ 提供者。
針對 Web 服務以外的其他資料來源建立 LINQ 提供者。
如需如何自行建立 LINQ 提供者的詳細資訊,請參閱 MSDN 部落格上的 LINQ:建置 IQueryable 提供者 (英文)。
請參閱
工作
HOW TO:修改運算式樹狀架構 (C# 和 Visual Basic)
參考
概念
Visual Studio 中的 Windows Communication Foundation 服務和 WCF 資料服務