演练:创建 IQueryable LINQ 提供程序

更新:2007 年 11 月

此高级主题提供了分步说明,介绍如何创建自定义 LINQ 提供程序。完成时,您将能够使用所创建的提供程序依据 TerraServer-USA Web 服务编写 LINQ 查询。

TerraServer-USA Web 服务提供了一个接口,指向美国航空像片的数据库。如果给出部分或全部地点名称,它还会公开一个返回有关美国各地点信息的方法。这个名为 GetPlaceList 的方法是 LINQ 提供程序将调用的方法。提供程序将使用 Windows Communication Foundation (WCF) 与 Web 服务通信。有关 TerraServer-USA Web 服务的更多信息,请参见 Overview of the TerraServer-USA Web Services(TerraServer-USA Web 服务概述)。

此提供程序是一个相对简单的 IQueryable 提供程序。此提供程序需要所处理的查询中有具体信息,并且它具有封闭类型系统,公开用于表示结果数据的单一类型。此提供程序只会检查表示查询的表达式树中的一种类型的方法调用表达式,也就是最里层的 Where 调用。它将提取必须具有的数据以便从此表达式中查询 Web 服务。然后,它将调用 Web 服务并将返回的数据插入表达式树中,替代初始的 IQueryable 数据源。标准查询运算符的 Enumerable 实现将处理查询执行的其余部分。

本主题中的代码示例在 C# 和 Visual Basic 中提供。

本演练阐释以下任务:

  • 在 Visual Studio 中创建项目。

  • 实现 IQueryable LINQ 提供程序所需的接口:IQueryable<T>IOrderedQueryable<T>IQueryProvider

  • 添加自定义 .NET 类型以表示 Web 服务中的数据。

  • 创建一个查询上下文类,以及一个从 Web 服务中获取数据的类。

  • 创建一个表达式树访问器子类,该子类将查找表示最里层 Queryable.Where 方法调用的表达式。

  • 创建一个表达式树访问器子类,该子类将从 LINQ 查询中提取要在 Web 服务请求中使用的信息。

  • 创建一个表达式树访问器子类,该子类将修改表示完整 LINQ 查询的表达式树。

  • 使用计算器类来部分计算表达式树。此步骤是必需的,因为它会将 LINQ 查询中的所有局部变量引用转换为值。

  • 创建一个表达式树帮助器类,以及一个新的异常类。

  • 从包含 LINQ 查询的客户端应用程序中测试 LINQ 提供程序。

  • 向 LINQ 提供程序中添加更复杂的查询功能。

先决条件

您需要以下组件来完成本演练:

  • Visual Studio 2008
说明:

对于在以下说明中使用的某些 Visual Studio 用户界面元素,您的计算机可能会显示不同的名称或位置。这些元素取决于您使用的 Visual Studio 版本及设置。有关更多信息,请参见Visual Studio 设置

创建项目

在 Visual Studio 中创建项目

  1. 在 Visual Studio 中创建一个新的“类库”应用程序。将项目命名为 LinqToTerraServerProvider。

  2. 在“解决方案资源管理器”中,选择“Class1.cs”(或“Class1.vb”)文件,并将其重命名为 QueryableTerraServerData.cs(或 QueryableTerraServerData.vb)。在弹出的对话框中,单击“是”以重命名对代码元素的所有引用。

    您在 Visual Studio 中以“类库”项目的形式创建提供程序,因为可执行客户端应用程序将添加提供程序程序集作为对其项目的引用。

添加对 Web 服务的服务引用

  1. 在“解决方案资源管理器”中,右击“LinqToTerraServerProvider”项目,并单击“添加服务引用”。

    “添加服务引用”对话框将打开。

  2. 在“地址”框中,键入 http://terraserver.microsoft.com/TerraService2.asmx。

  3. 在“命名空间”框中,键入 TerraServerReference,然后单击“确定”。

    即会添加 TerraServer-USA Web 服务作为服务引用,以便应用程序能够通过 Windows Communication Foundation (WCF) 与 Web 服务通信。通过向项目中添加服务引用,Visual Studio 将生成一个“app.config”文件,该文件包含 Web 服务的代理和终结点。有关更多信息,请参见 Visual Studio 中的 Windows Communication Foundation 服务简介

现在有了一个项目,其中包含一个名为“app.config”的文件、一个名为“QueryableTerraServerData.cs”(或“QueryableTerraServerData.vb”)的文件,以及一个名为“TerraServerReference”的服务引用。

实现必要的接口

若要创建 LINQ 提供程序,至少必须实现 IQueryable<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> 中声明)。

    此类有两个构造函数。将从客户端应用程序中调用第一个构造函数,以创建编写 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 方法使用反射获取所创建的查询在执行时将返回的序列的元素类型。然后,该方法将使用 Activator 类构造一个用该元素类型构造的新 QueryableTerraServerData 实例,作为其泛型类型参数。对于非泛型 CreateQuery 方法,调用该方法的结果就好像用正确类型参数调用泛型 CreateQuery 方法一样。

    大多数查询执行逻辑都是在您将稍后添加的另一个类中处理的。此功能将在其他地方进行处理,因为它特定于所查询的数据源,而此类中的代码对于任何 LINQ 提供程序都是通用的。若要为其他提供程序使用此代码,您可能必须更改类的名称以及在其中两个方法中引用的查询上下文类型的名称。

添加自定义类型以表示结果数据

您将需要一个 .NET 类型来表示从 Web 服务中获取的数据。将在客户端 LINQ 查询中使用此类型以定义查询所需的结果。下面的过程将创建这样的类型。此类型(名为 Place)包含有关单一地理位置(比如城市、公园或湖泊)的信息。

此代码还包含一个名为 PlaceType 的枚举类型,该类型定义各种类型的地理位置,并用在 Place 类中。

创建自定义结果类型

  • 向项目中添加 Place 类和 PlaceType 枚举。

    Public Class Place
        ' Properties.
        Private _Name As String
        Private _State As String
        Private _PlaceType As PlaceType
    
        Public Property Name() As String
            Get
                Return _Name
            End Get
            Private Set(ByVal value As String)
                _Name = value
            End Set
        End Property
    
        Public Property State() As String
            Get
                Return _State
            End Get
            Private Set(ByVal value As String)
                _State = value
            End Set
        End Property
    
        Public Property PlaceType() As PlaceType
            Get
                Return _PlaceType
            End Get
            Private Set(ByVal value As PlaceType)
                _PlaceType = value
            End Set
        End Property
    
        ' Constructor.
        Friend Sub New(ByVal name As String, _
                       ByVal state As String, _
                       ByVal placeType As TerraServerReference.PlaceType)
    
            Me.Name = name
            Me.State = state
            Me.PlaceType = CType(placeType, PlaceType)
        End Sub
    End Class
    
    Public Enum PlaceType
        Unknown
        AirRailStation
        BayGulf
        CapePeninsula
        CityTown
        HillMountain
        Island
        Lake
        OtherLandFeature
        OtherWaterFeature
        ParkBeach
        PointOfInterest
        River
    End Enum
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace LinqToTerraServerProvider
    {
        public class Place
        {
            // Properties.
            public string Name { get; private set; }
            public string State { get; private set; }
            public PlaceType PlaceType { get; private set; }
    
            // Constructor.
            internal Place(string name,
                            string state,
                            LinqToTerraServerProvider.TerraServerReference.PlaceType placeType)
            {
                Name = name;
                State = state;
                PlaceType = (PlaceType)placeType;
            }
        }
    
        public enum PlaceType
        {
            Unknown,
            AirRailStation,
            BayGulf,
            CapePeninsula,
            CityTown,
            HillMountain,
            Island,
            Lake,
            OtherLandFeature,
            OtherWaterFeature,
            ParkBeach,
            PointOfInterest,
            River
        }
    }
    

    Place 类型的构造函数简化了依据 Web 服务返回的类型创建结果对象的过程。尽管提供程序可以直接返回 Web 服务 API 定义的结果类型,但它将需要客户端应用程序添加对 Web 服务的引用。通过创建新类型作为提供程序库的一部分,客户端不必知道 Web 服务公开的类型和方法。

添加功能以从数据源获取数据

此提供程序实现假定最里层的 Queryable.Where 调用包含要用于查询 Web 服务的位置信息。最里层的 Queryable.Where 调用是 where 子句(在 Visual Basic 中为 Where 子句)或在 LINQ 查询中首先进行的 Queryable.Where 方法调用,或者是最靠近表示查询的表达式树“底部”的调用。

创建查询上下文类

  • 将 TerraServerQueryContext 类添加到项目中。

    Imports System.Linq.Expressions
    
    Public Class TerraServerQueryContext
    
        ' Executes the expression tree that is passed to it.
        Friend Shared Function Execute(ByVal expr As Expression, _
                                       ByVal IsEnumerable As Boolean) As Object
    
            ' The expression must represent a query over the data source.
            If Not IsQueryOverDataSource(expr) Then
                Throw New InvalidProgramException("No query over the data source was specified.")
            End If
    
            ' Find the call to Where() and get the lambda expression predicate.
            Dim whereFinder As New InnermostWhereFinder()
            Dim whereExpression As MethodCallExpression = _
                whereFinder.GetInnermostWhere(expr)
            Dim lambdaExpr As LambdaExpression
            lambdaExpr = CType(CType(whereExpression.Arguments(1), UnaryExpression).Operand, LambdaExpression)
    
            ' Send the lambda expression through the partial evaluator.
            lambdaExpr = CType(Evaluator.PartialEval(lambdaExpr), LambdaExpression)
    
            ' Get the place name(s) to query the Web service with.
            Dim lf As New LocationFinder(lambdaExpr.Body)
            Dim locations As List(Of String) = lf.Locations
            If locations.Count = 0 Then
                Dim s = "You must specify at least one place name in your query."
                Throw New InvalidQueryException(s)
            End If
    
            ' Call the Web service and get the results.
            Dim places() = WebServiceHelper.GetPlacesFromTerraServer(locations)
    
            ' Copy the IEnumerable places to an IQueryable.
            Dim queryablePlaces = places.AsQueryable()
    
            ' Copy the expression tree that was passed in, changing only the first
            ' argument of the innermost MethodCallExpression.
            Dim treeCopier As New ExpressionTreeModifier(queryablePlaces)
            Dim newExpressionTree = treeCopier.CopyAndModify(expr)
    
            ' This step creates an IQueryable that executes by replacing 
            ' Queryable methods with Enumerable methods.
            If (IsEnumerable) Then
                Return queryablePlaces.Provider.CreateQuery(newExpressionTree)
            Else
                Return queryablePlaces.Provider.Execute(newExpressionTree)
            End If
        End Function
    
        Private Shared Function IsQueryOverDataSource(ByVal expression As Expression) As Boolean
            ' If expression represents an unqueried IQueryable data source instance,
            ' expression is of type ConstantExpression, not MethodCallExpression.
            Return (TypeOf expression Is MethodCallExpression)
        End Function
    End Class
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        class TerraServerQueryContext
        {
            // Executes the expression tree that is passed to it.
            internal static object Execute(Expression expression, bool IsEnumerable)
            {
                // The expression must represent a query over the data source.
                if (!IsQueryOverDataSource(expression))
                    throw new InvalidProgramException("No query over the data source was specified.");
    
                // Find the call to Where() and get the lambda expression predicate.
                InnermostWhereFinder whereFinder = new InnermostWhereFinder();
                MethodCallExpression whereExpression = whereFinder.GetInnermostWhere(expression);
                LambdaExpression lambdaExpression = (LambdaExpression)((UnaryExpression)(whereExpression.Arguments[1])).Operand;
    
                // Send the lambda expression through the partial evaluator.
                lambdaExpression = (LambdaExpression)Evaluator.PartialEval(lambdaExpression);
    
                // Get the place name(s) to query the Web service with.
                LocationFinder lf = new LocationFinder(lambdaExpression.Body);
                List<string> locations = lf.Locations;
                if (locations.Count == 0)
                    throw new InvalidQueryException("You must specify at least one place name in your query.");
    
                // Call the Web service and get the results.
                Place[] places = WebServiceHelper.GetPlacesFromTerraServer(locations);
    
                // Copy the IEnumerable places to an IQueryable.
                IQueryable<Place> queryablePlaces = places.AsQueryable<Place>();
    
                // Copy the expression tree that was passed in, changing only the first
                // argument of the innermost MethodCallExpression.
                ExpressionTreeModifier treeCopier = new ExpressionTreeModifier(queryablePlaces);
                Expression newExpressionTree = treeCopier.CopyAndModify(expression);
    
                // This step creates an IQueryable that executes by replacing Queryable methods with Enumerable methods.
                if (IsEnumerable)
                    return queryablePlaces.Provider.CreateQuery(newExpressionTree);
                else
                    return queryablePlaces.Provider.Execute(newExpressionTree);
            }
    
            private static bool IsQueryOverDataSource(Expression expression)
            {
                // If expression represents an unqueried IQueryable data source instance,
                // expression is of type ConstantExpression, not MethodCallExpression.
                return (expression is MethodCallExpression);
            }
        }
    }
    

    此类组织执行查询的工作。找到表示最里层 Queryable.Where 调用的表达式后,此代码将检索 Lambda 表达式,该表达式表示传递到 Queryable.Where 的谓词。然后,代码会将谓词表达式传递到要部分计算的方法,以便将对局部变量的所有引用转换为值。接着,代码将调用一个方法以从谓词中提取请求的位置,并调用另一个方法从 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 类添加到项目中。这些代码可在如何:实现表达式目录树访问器中获得。将 using 指令(在 Visual Basic 中为 Imports 语句)添加到以下命名空间的文件中:System.Collections.Generic、System.Collections.ObjectModel 和 System.Linq.Expressions。

  2. 将继承 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 调用的表达式,该表达式树表示客户端查询。这个最里层的表达式是提供程序从中提取搜索位置的表达式。

创建可提取数据以查询 Web 服务的访问器

  • 将 LocationFinder 类添加到项目中。

    Imports System.Linq.Expressions
    Imports ETH = LinqToTerraServerProvider.ExpressionTreeHelpers
    
    Friend Class LocationFinder
        Inherits ExpressionVisitor
    
        Private _expression As Expression
        Private _locations As List(Of String)
    
        Public Sub New(ByVal exp As Expression)
            Me._expression = exp
        End Sub
    
        Public ReadOnly Property Locations() As List(Of String)
            Get
                If _locations Is Nothing Then
                    _locations = New List(Of String)()
                    Me.Visit(Me._expression)
                End If
                Return Me._locations
            End Get
        End Property
    
        Protected Overrides Function VisitBinary(ByVal be As BinaryExpression) As Expression
            ' Handles Visual Basic String semantics.
            be = ETH.ConvertVBStringCompare(be)
    
            If be.NodeType = ExpressionType.Equal Then
                If (ETH.IsMemberEqualsValueExpression(be, GetType(Place), "Name")) Then
                    _locations.Add(ETH.GetValueFromEqualsExpression(be, GetType(Place), "Name"))
                    Return be
                ElseIf (ETH.IsMemberEqualsValueExpression(be, GetType(Place), "State")) Then
                    _locations.Add(ETH.GetValueFromEqualsExpression(be, GetType(Place), "State"))
                    Return be
                Else
                    Return MyBase.VisitBinary(be)
                End If
            Else
                Return MyBase.VisitBinary(be)
            End If
        End Function
    End Class
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        internal class LocationFinder : ExpressionVisitor
        {
            private Expression expression;
            private List<string> locations;
    
            public LocationFinder(Expression exp)
            {
                this.expression = exp;
            }
    
            public List<string> Locations
            {
                get
                {
                    if (locations == null)
                    {
                        locations = new List<string>();
                        this.Visit(this.expression);
                    }
                    return this.locations;
                }
            }
    
            protected override Expression VisitBinary(BinaryExpression be)
            {
                if (be.NodeType == ExpressionType.Equal)
                {
                    if (ExpressionTreeHelpers.IsMemberEqualsValueExpression(be, typeof(Place), "Name"))
                    {
                        locations.Add(ExpressionTreeHelpers.GetValueFromEqualsExpression(be, typeof(Place), "Name"));
                        return be;
                    }
                    else if (ExpressionTreeHelpers.IsMemberEqualsValueExpression(be, typeof(Place), "State"))
                    {
                        locations.Add(ExpressionTreeHelpers.GetValueFromEqualsExpression(be, typeof(Place), "State"));
                        return be;
                    }
                    else
                        return base.VisitBinary(be);
                }
                else
                    return base.VisitBinary(be);
            }
        }
    }
    

    此类用于从客户端传递到 Queryable.Where 的谓词中提取位置信息。它派生自基表达式树访问器类,并且只会重写 VisitBinary 方法。

    表达式树访问器基类会将二进制表达式,比如像 place.Name == "Seattle" (在 Visual Basic 中为 place.Name = "Seattle")这样的相等表达式发送到 VisitBinary 方法。在这个重写 VisitBinary 方法中,如果表达式与可提供位置信息的相等表达式模式匹配,则会提取该信息并将其存储在位置列表中。

    此类使用表达式树访问器在表达式树中查找位置信息,因为访问器可用于遍历和检查表达式树。与未使用访问器实现的情况相比,生成的代码更为简洁,而且不容易出错。

    在演练的此阶段,提供程序仅支持有限的在查询中提供位置信息的方式。在本主题的后面,您将添加功能以便能够采用更多方式提供位置信息。

创建可修改表达式树的访问器

  • 将 ExpressionTreeModifier 类添加到项目中。

    Imports System.Linq.Expressions
    
    Friend Class ExpressionTreeModifier
        Inherits ExpressionVisitor
    
        Private queryablePlaces As IQueryable(Of Place)
    
        Friend Sub New(ByVal places As IQueryable(Of Place))
            Me.queryablePlaces = places
        End Sub
    
        Friend Function CopyAndModify(ByVal expression As Expression) As Expression
            Return Me.Visit(expression)
        End Function
    
        Protected Overrides Function VisitConstant(ByVal c As ConstantExpression) As Expression
            ' Replace the constant QueryableTerraServerData arg with the queryable Place collection.
            If c.Type Is GetType(QueryableTerraServerData(Of Place)) Then
                Return Expression.Constant(Me.queryablePlaces)
            Else
                Return c
            End If
        End Function
    End Class
    
    using System;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        internal class ExpressionTreeModifier : ExpressionVisitor
        {
            private IQueryable<Place> queryablePlaces;
    
            internal ExpressionTreeModifier(IQueryable<Place> places)
            {
                this.queryablePlaces = places;
            }
    
            internal Expression CopyAndModify(Expression expression)
            {
                return this.Visit(expression);
            }
    
            protected override Expression VisitConstant(ConstantExpression c)
            {
                // Replace the constant QueryableTerraServerData arg with the queryable Place collection.
                if (c.Type == typeof(QueryableTerraServerData<Place>))
                    return Expression.Constant(this.queryablePlaces);
                else
                    return c;
            }
        }
    }
    

    此类派生自基表达式树访问器类,并重写 VisitConstant 方法。在此方法中,它会将应用了最里层标准查询运算符调用的对象替换为 Place 对象的具体列表。

    CopyAndModify 方法将调用 Visit 方法的基类实现。必须提供此 CopyAndModify 方法,原因是无法从查询上下文类中直接调用 Visit 方法,即 protected(在 Visual Basic 中为 Protected)。

    表达式树修饰符类之所以使用表达式树访问器,原因是访问器可以遍历、检查和复制表达式树。由于此类从基表达式树访问器类中派生,因此它只需很少的代码就可执行其功能。

添加表达式计算器

在客户端查询中传递到 Queryable.Where 方法的谓词可能包含不依赖于 Lambda 表达式参数的子表达式。可以并且应当立即计算这些独立的子表达式。它们可能是对必须转换为值的局部变量或成员变量的引用。

下一个类公开 PartialEval(Expression) 方法,该方法确定可以立即计算表达式中的哪些子树(如果有)。然后,它将通过创建 Lambda 表达式、编译该表达式并调用返回的委托来计算那些表达式。最后,它将子树替换为一个表示常量值的新节点。这被称作部分计算。

添加一个类以执行表达式树的部分计算

  • 将 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
    
            Protected 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
    
            Protected 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);
                }
    
                protected 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;
                }
    
                protected 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;
                }
            }
        }
    }
    

添加帮助器类

本节包含提供程序的三个帮助器类的代码。

添加由 System.Linq.IQueryProvider 实现使用的帮助器类

  • 向项目中添加 TypeSystem 类(或 Visual Basic 中的模块)。

    Imports System.Collections.Generic
    
    Friend Module TypeSystem
    
        Friend Function GetElementType(ByVal seqType As Type) As Type
            Dim ienum As Type = FindIEnumerable(seqType)
    
            If ienum Is Nothing Then
                Return seqType
            End If
    
            Return ienum.GetGenericArguments()(0)
        End Function
    
        Private Function FindIEnumerable(ByVal seqType As Type) As Type
    
            If seqType Is Nothing Or seqType Is GetType(String) Then
                Return Nothing
            End If
    
            If (seqType.IsArray) Then
                Return GetType(IEnumerable(Of )).MakeGenericType(seqType.GetElementType())
            End If
    
            If (seqType.IsGenericType) Then
                For Each arg As Type In seqType.GetGenericArguments()
                    Dim ienum As Type = GetType(IEnumerable(Of )).MakeGenericType(arg)
    
                    If (ienum.IsAssignableFrom(seqType)) Then
                        Return ienum
                    End If
                Next
            End If
    
            Dim ifaces As Type() = seqType.GetInterfaces()
    
            If ifaces IsNot Nothing And ifaces.Length > 0 Then
                For Each iface As Type In ifaces
                    Dim ienum As Type = FindIEnumerable(iface)
    
                    If (ienum IsNot Nothing) Then
                        Return ienum
                    End If
                Next
            End If
    
            If seqType.BaseType IsNot Nothing And _
               seqType.BaseType IsNot GetType(Object) Then
    
                Return FindIEnumerable(seqType.BaseType)
            End If
    
            Return Nothing
        End Function
    End Module
    
    using System;
    using System.Collections.Generic;
    
    namespace LinqToTerraServerProvider
    {
        internal static class TypeSystem
        {
            internal static Type GetElementType(Type seqType)
            {
                Type ienum = FindIEnumerable(seqType);
                if (ienum == null) return seqType;
                return ienum.GetGenericArguments()[0];
            }
    
            private static Type FindIEnumerable(Type seqType)
            {
                if (seqType == null || seqType == typeof(string))
                    return null;
    
                if (seqType.IsArray)
                    return typeof(IEnumerable<>).MakeGenericType(seqType.GetElementType());
    
                if (seqType.IsGenericType)
                {
                    foreach (Type arg in seqType.GetGenericArguments())
                    {
                        Type ienum = typeof(IEnumerable<>).MakeGenericType(arg);
                        if (ienum.IsAssignableFrom(seqType))
                        {
                            return ienum;
                        }
                    }
                }
    
                Type[] ifaces = seqType.GetInterfaces();
                if (ifaces != null && ifaces.Length > 0)
                {
                    foreach (Type iface in ifaces)
                    {
                        Type ienum = FindIEnumerable(iface);
                        if (ienum != null) return ienum;
                    }
                }
    
                if (seqType.BaseType != null && seqType.BaseType != typeof(object))
                {
                    return FindIEnumerable(seqType.BaseType);
                }
    
                return null;
            }
        }
    }
    

    您之前添加的 IQueryProvider 实现将使用此帮助器类。

    TypeSystem.GetElementType 使用反射来获取 IEnumerable<T>(Visual Basic 中为 IEnumerable(Of T))集合的泛型类型参数。将从查询提供程序实现的非泛型 CreateQuery 方法中调用此方法,以提供查询结果集合的元素类型。

    此帮助器类并不特定于此 TerraServer-USA Web 服务提供程序。因此,可以为任何 LINQ 提供程序重用该类。

创建表达式树帮助器类

  • 将 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;
                }
            }
        }
    }
    

    此类定义一个 Exception 类型,当提供程序无法识别来自客户端的 LINQ 查询时,它将引发该类型。通过定义此无效查询异常类型,提供程序可以从代码中的不同位置引发更具体的异常,而不只是 Exception

现在已经添加了编译提供程序所需的所有片断。生成“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,因此编译器会将查询表达式语法转换为对 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") Or _
               ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "State") Then
                _locations.Add(ETH.GetValueFromExpression(m.Arguments(0)))
                Return m
            End If
        End If
    
        Return MyBase.VisitMethodCall(m)
    End Function
    
    protected override Expression VisitMethodCall(MethodCallExpression m)
    {
        if (m.Method.DeclaringType == typeof(String) && m.Method.Name == "StartsWith")
        {
            if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "Name") ||
            ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "State"))
            {
                locations.Add(ExpressionTreeHelpers.GetValueFromExpression(m.Arguments[0]));
                return m;
            }
        }
    
        return base.VisitMethodCall(m);
    }
    
  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") Or _
               ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "State") Then
                _locations.Add(ETH.GetValueFromExpression(m.Arguments(0)))
                Return m
            End If
        ElseIf m.Method.Name = "Contains" Then
            Dim valuesExpression As Expression = Nothing
    
            If m.Method.DeclaringType Is GetType(Enumerable) Then
                If ETH.IsSpecificMemberExpression(m.Arguments(1), GetType(Place), "Name") Or _
                   ETH.IsSpecificMemberExpression(m.Arguments(1), GetType(Place), "State") Then
                    valuesExpression = m.Arguments(0)
                End If
    
            ElseIf m.Method.DeclaringType Is GetType(List(Of String)) Then
                If ETH.IsSpecificMemberExpression(m.Arguments(0), GetType(Place), "Name") Or _
                   ETH.IsSpecificMemberExpression(m.Arguments(0), GetType(Place), "State") Then
                    valuesExpression = m.Object
                End If
            End If
    
            If valuesExpression Is Nothing OrElse valuesExpression.NodeType <> ExpressionType.Constant Then
                Throw New Exception("Could not find the location values.")
            End If
    
            Dim ce = CType(valuesExpression, ConstantExpression)
    
            Dim placeStrings = CType(ce.Value, IEnumerable(Of String))
            ' Add each string in the collection to the list of locations to obtain data about.
            For Each place In placeStrings
                _locations.Add(place)
            Next
    
            Return m
        End If
    
        Return MyBase.VisitMethodCall(m)
    End Function
    
    protected override Expression VisitMethodCall(MethodCallExpression m)
    {
        if (m.Method.DeclaringType == typeof(String) && m.Method.Name == "StartsWith")
        {
            if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "Name") ||
            ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "State"))
            {
                locations.Add(ExpressionTreeHelpers.GetValueFromExpression(m.Arguments[0]));
                return m;
            }
    
        }
        else if (m.Method.Name == "Contains")
        {
            Expression valuesExpression = null;
    
            if (m.Method.DeclaringType == typeof(Enumerable))
            {
                if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[1], typeof(Place), "Name") ||
                ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[1], typeof(Place), "State"))
                {
                    valuesExpression = m.Arguments[0];
                }
            }
            else if (m.Method.DeclaringType == typeof(List<string>))
            {
                if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[0], typeof(Place), "Name") ||
                ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[0], typeof(Place), "State"))
                {
                    valuesExpression = m.Object;
                }
            }
    
            if (valuesExpression == null || valuesExpression.NodeType != ExpressionType.Constant)
                throw new Exception("Could not find the location values.");
    
            ConstantExpression ce = (ConstantExpression)valuesExpression;
    
            IEnumerable<string> placeStrings = (IEnumerable<string>)ce.Value;
            // Add each string in the collection to the list of locations to obtain data about.
            foreach (string place in placeStrings)
                locations.Add(place);
    
            return m;
        }
    
        return base.VisitMethodCall(m);
    }
    

    此方法将 Contains 所应用于的集合中的每个字符串添加到位置列表中,以便用该列表查询 Web 服务。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 查询

参考

IQueryable<T>

IOrderedQueryable<T>

其他资源

Windows Communication Foundation 服务和 ADO.NET 数据服务