逐步解說:建立 IQueryable LINQ 提供者

這個進階主題提供建立自訂 LINQ 提供者的逐步指示。當您完成時,您可以使用自己建立寫入 LINQ 查詢針對 TerraServer-USA Web 服務的提供者。

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 中建立新專案

  1. 在 Visual Studio,請建立新的 [類別庫] 應用程式。將專案命名為 LinqToTerraServerProvider。

  2. 在 [方案總管] 中,選取 [Class1.cs] (或 [Class1.vb]) 檔案,並將它重新命名為 QueryableTerraServerData.cs (或 QueryableTerraServerData.vb)。在出現的對話方塊中,按一下 [],重新命名程式碼項目的所有參考。

    您將提供者建立成 Visual Studio 中的 [類別庫],因為可執行用戶端應用程式將會加入提供者組件 (Assembly),做為其專案的參考。

若要加入 Web 服務的參考

  1. 在 [方案總管] 中,以滑鼠右鍵按一下 [LinqToTerraServerProvider] 專案,然後按一下 [加入服務參考]。

    [加入服務參考] 對話方塊隨即開啟。

  2. 在 [位址] 方塊中輸入 http://terraserver.microsoft.com/TerraService2.asmx。

  3. 在 [命名空間] 方塊中,輸入 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 提供者需要的其他介面。

如果想要支援排序查詢運算子 (例如 OrderByThenBy),您還必須實作 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 中宣告的三個屬性,以及 IEnumerableIEnumerable<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.WhereIQueryable 參數。因此,其型別會變更為 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 方法呼叫運算式的訪問項

  1. 將繼承 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 呼叫的運算式。這個最內部的運算式就是提供者從中擷取搜尋地點的運算式。

  2. 在檔案中為下列命名空間加入 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 提供者。

若要建立用來測試提供者的用戶端應用程式

  1. 將新的 [主控台應用程式] 專案加入至方案中,並將它命名為 ClientApp。

  2. 在新專案中,加入提供者組件的參考。

  3. 將 [app.config] 檔案從提供者專案拖曳到用戶端專案 (這是與 Web 服務進行通訊的必要檔案)。

    注意事項注意事項

    在 Visual Basic 中,您可能必須按一下 [顯示所有檔案] 按鈕,才能在 [方案總管] 中看到 [app.config] 檔案。

  4. 將下列 using 陳述式 (在 Visual Basic 中則為 Imports 陳述式) 加入至 [Program.cs] (或 Visual Basic 中的 [Module1.vb]) 檔案:

    using System;
    using System.Linq;
    using LinqToTerraServerProvider;
    
    Imports LinqToTerraServerProvider
    
  5. 在 [Program.cs] (或 Visual Basic 中的 [Module1.vb]) 檔案的 Main 方法中,插入下列程式碼:

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

    這個程式碼會建立您在提供者中定義之 IQueryable<T> 型別的新執行個體,然後再使用 LINQ 查詢該物件。此查詢會使用相等運算式指定取得資料的位置。由於資料來源實作的是 IQueryable,因此編譯器 (Compiler) 會將查詢運算式語法轉譯成 Queryable 中定義之標準查詢運算子的呼叫。這些標準查詢運算子方法會在內部建置一個運算式樹狀架構,並呼叫您在實作 IQueryProvider 時一併實作的 ExecuteCreateQuery 方法。

  6. 建置 ClientApp

  7. 將這個用戶端應用程式設定成方案的「啟始」專案。在 [方案總管] 中,以滑鼠右鍵按一下 [ClientApp] 專案,並選取 [設定為啟始專案]。

  8. 執行程式並檢視結果。產生的結果應該有三個左右。

加入較複雜的查詢功能

目前為止您所擁有的提供者只能為用戶端提供少數幾種在 LINQ 查詢中指定地點資訊的方法。具體來說,此提供者只能從諸如 Place.Name == "Seattle" 或 Place.State == "Alaska" (在 Visual Basic 中則為 Place.Name = "Seattle" 或 Place.State = "Alaska") 等相等運算式取得資訊。

下一個程序將說明如何加入對於指定地點資訊的其他方式的支援。加入這個程式碼之後,您的提供者就可以從方法呼叫運算式 (例如 place.Name.StartsWith("Seat")) 擷取地點資訊。

若要加入包含 String.StartsWith 之述詞的支援

  1. 在 [LinqToTerraServerProvider] 專案中,將 VisitMethodCall 方法加入至 LocationFinder 類別定義。

    Protected Overrides Function VisitMethodCall(ByVal m As MethodCallExpression) As Expression
        If m.Method.DeclaringType Is GetType(String) And m.Method.Name = "StartsWith" Then
            If ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "Name") OrElse
               ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "State") Then
                _locations.Add(ETH.GetValueFromExpression(m.Arguments(0)))
                Return m
            End If
        End If
    
        Return MyBase.VisitMethodCall(m)
    End Function
    
    protected override Expression VisitMethodCall(MethodCallExpression m)
    {
        if (m.Method.DeclaringType == typeof(String) && m.Method.Name == "StartsWith")
        {
            if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "Name") ||
            ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "State"))
            {
                locations.Add(ExpressionTreeHelpers.GetValueFromExpression(m.Arguments[0]));
                return m;
            }
        }
    
        return base.VisitMethodCall(m);
    }
    
  2. 重新編譯 LinqToTerraServerProvider 專案。

  3. 若要測試提供者的新功能,請開啟 [ClientApp] 專案中的 [Program.cs] (在 Visual Basic 中則為 [Module1.vb]) 檔案。將 Main 方法中的程式碼取代為下列程式碼:

    QueryableTerraServerData<Place> terraPlaces = new QueryableTerraServerData<Place>();
    
    var query = from place in terraPlaces
                where place.Name.StartsWith("Lond")
                select new { place.Name, place.State };
    
    foreach (var obj in query)
        Console.WriteLine(obj);
    
    Dim terraPlaces As New QueryableTerraServerData(Of Place)
    
    Dim query = From place In terraPlaces 
                Where place.Name.StartsWith("Lond") 
                Select place.Name, place.State
    
    For Each obj In query
        Console.WriteLine(obj)
    Next
    
  4. 執行程式並檢視結果。產生的結果應該有 29 個左右。

下一個程序將說明如何將功能加入提供者中,使用戶端查詢能夠利用兩個額外的方法 (也就是 Enumerable.ContainsList<T>.Contains) 來指定地點資訊。加入這個程式碼之後,您的提供者就可以從用戶端查詢中的方法呼叫運算式 (例如 placeList.Contains(place.Name),其中 placeList 集合是用戶端所提供的具體清單) 擷取資訊。讓用戶端使用 Contains 方法的好處在於它們可以直接將地點加入至 placeList 中來指定任何數目的地點。修改地點的數目並不會變更查詢的語法。

若要加入 'where' 子句中具有 Contains 方法之查詢的支援

  1. 在 [LinqToTerraServerProvider] 專案的 LocationFinder 類別定義中,將 VisitMethodCall 方法取代成下列程式碼:

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

    這個方法會將已套用 Contains 之集合中的每個字串加入至要用來查詢 Web 服務的地點清單。EnumerableList<T> 中都會定義一個名稱為 Contains 的方法。因此,VisitMethodCall 方法必須檢查這兩個宣告型別。Enumerable.Contains 定義為擴充方法,因此其套用的集合實際上是方法的第一個引數。List.Contains 定義為執行個體方法,因此其套用的集合是方法的接收物件。

  2. 重新編譯 LinqToTerraServerProvider 專案。

  3. 若要測試提供者的新功能,請開啟 [ClientApp] 專案中的 [Program.cs] (在 Visual Basic 中則為 [Module1.vb]) 檔案。將 Main 方法中的程式碼取代為下列程式碼:

    QueryableTerraServerData<Place> terraPlaces = new QueryableTerraServerData<Place>();
    
    string[] places = { "Johannesburg", "Yachats", "Seattle" };
    
    var query = from place in terraPlaces
                where places.Contains(place.Name)
                orderby place.State
                select new { place.Name, place.State };
    
    foreach (var obj in query)
        Console.WriteLine(obj);
    
    Dim terraPlaces As New QueryableTerraServerData(Of Place)
    
    Dim places = New String() {"Johannesburg", "Yachats", "Seattle"}
    
    Dim query = From place In terraPlaces 
                Where places.Contains(place.Name) 
                Order By place.State 
                Select place.Name, place.State
    
    For Each obj In query
        Console.WriteLine(obj)
    Next
    
  4. 執行程式並檢視結果。產生的結果應該有 5 個左右。

後續步驟

本逐步解說主題已說明如何針對 Web 服務的單一方法來建立 LINQ 提供者。如果您想採用其他的 LINQ 提供者開發方式,請考慮下列可能的方案:

  • 啟用 LINQ 提供者以處理在用戶端查詢中指定地點的其他方式。

  • 檢閱 TerraServer-USA Web 服務公開的其他方法,並建立與其中一個方法連結的 LINQ 提供者。

  • 尋找您所需要的其他 Web 服務,並針對該服務建立 LINQ 提供者。

  • 針對 Web 服務以外的其他資料來源建立 LINQ 提供者。

如需如何自行建立 LINQ 提供者的詳細資訊,請參閱 MSDN 部落格上的 LINQ:建置 IQueryable 提供者 (英文)。

請參閱

工作

LINQ 範例

HOW TO:修改運算式樹狀架構 (C# 和 Visual Basic)

參考

IQueryable<T>

IOrderedQueryable<T>

概念

啟用資料來源以進行 LINQ 查詢

Visual Studio 中的 Windows Communication Foundation 服務和 WCF 資料服務