Click to Rate and Give Feedback
MSDN
MSDN Library
Visual Studio 2008
Visual Studio
 Walkthrough: Creating an IQueryable...
Language-Integrated Query (LINQ)
Walkthrough: Creating an IQueryable LINQ Provider

Updated: July 2008

This advanced topic provides step-by-step instructions for creating a custom LINQ provider. When you are finished, you will be able to use the provider you create to write LINQ queries against the TerraServer-USA Web service.

The TerraServer-USA Web service provides an interface to a database of aerial images of the United States. It also exposes a method that returns information about locations in the United States, given part or all of a location name. This method, which is named GetPlaceList, is the method that your LINQ provider will call. The provider will use Windows Communication Foundation (WCF) to communicate with the Web service. For more information about the TerraServer-USA Web service, see Overview of the TerraServer-USA Web Services.

This provider is a relatively simple IQueryable provider. It expects specific information in the queries that it handles and it has a closed type system, exposing a single type to represent the result data. This provider examines only one type of method call expression in the expression tree that represents the query, that is the innermost call to Where. It extracts the data that it must have in order to query the Web service from this expression. It then calls the Web service and inserts the returned data into the expression tree in the place of the initial IQueryable data source. The rest of the query execution is handled by the Enumerable implementations of the standard query operators.

The code examples in this topic are provided in C# and Visual Basic.

This walkthrough illustrates the following tasks:

  • Creating the project in Visual Studio.

  • Implementing the interfaces that are required for an IQueryable LINQ provider: IQueryable<(Of <(T>)>)IOrderedQueryable<(Of <(T>)>), and IQueryProvider.

  • Adding a custom .NET type to represent the data from the Web service.

  • Creating a query context class and a class that obtains data from the Web service.

  • Creating an expression tree visitor subclass that finds the expression that represents the innermost call to the Queryable.Where method.

  • Creating an expression tree visitor subclass that extracts information from the LINQ query to use in the Web service request.

  • Creating an expression tree visitor subclass that modifies the expression tree that represents the complete LINQ query.

  • Using an evaluator class to partially evaluate an expression tree. This step is necessary because it translates all local variable references in the LINQ query into values.

  • Creating an expression tree helper class and a new exception class.

  • Testing the LINQ provider from a client application that contains a LINQ query.

  • Adding more complex query capabilities to the LINQ provider.

    Note:

    The LINQ provider that this walkthrough creates is available as a sample. See LINQ to TerraServer Provider Sample for more information.

You need the following components to complete this walkthrough:

  • Visual Studio 2008

Note:

Your computer might show different names or locations for some of the Visual Studio user interface elements in the following instructions. The Visual Studio edition that you have and the settings that you use determine these elements. For more information, see Visual Studio Settings.

To create the project in Visual Studio

  1. In Visual Studio, create a new Class Library application. Name the project LinqToTerraServerProvider.

  2. In Solution Explorer, select the Class1.cs (or Class1.vb) file and rename it to QueryableTerraServerData.cs (or QueryableTerraServerData.vb). In the dialog box that pops up, click Yes to rename all references to the code element.

    You create the provider as a Class Library project in Visual Studio because executable client applications will add the provider assembly as a reference to their project.

To add a service reference to the Web service

  1. In Solution Explorer, right-click the LinqToTerraServerProvider project and click Add Service Reference.

    The Add Service Reference dialog box opens.

  2. In the Address box, type http://terraserver.microsoft.com/TerraService2.asmx.

  3. In the Namespace box, type TerraServerReference and then click OK.

    The TerraServer-USA Web service is added as a service reference so that the application can communicate with the Web service by way of Windows Communication Foundation (WCF). By adding a service reference to the project, Visual Studio generates an app.config file that contains a proxy and an endpoint for the Web service. For more information, see Introduction to Windows Communication Foundation Services in Visual Studio.

You now have a project that has a file that is named app.config, a file that is named QueryableTerraServerData.cs (or QueryableTerraServerData.vb), and a service reference named TerraServerReference.

To create a LINQ provider, at a minimum you must implement the IQueryable<(Of <(T>)>) and IQueryProvider interfaces. IQueryable<(Of <(T>)>) and IQueryProvider are derived from the other required interfaces; therefore, by implementing these two interfaces, you are also implementing the other interfaces that are required for a LINQ provider.

If you want to support sorting query operators such as OrderBy and ThenBy, you must also implement the IOrderedQueryable<(Of <(T>)>) interface. Because IOrderedQueryable<(Of <(T>)>) derives from IQueryable<(Of <(T>)>), you can implement both of these interfaces in one type, which is what this provider does.

To implement System.Linq.IQueryable`1 and System.Linq.IOrderedQueryable`1

  • In the file QueryableTerraServerData.cs (or QueryableTerraServerData.vb), add the following code.

    Visual Basic
    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
    
    
    C#
    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
        }
    }
    
    

    The IOrderedQueryable<(Of <(T>)>) implementation by the QueryableTerraServerData class implements three properties declared in IQueryable and two enumeration methods declared in IEnumerable and IEnumerable<(Of <(T>)>).

    This class has two constructors. The first constructor is called from the client application to create the object to write the LINQ query against. The second constructor is called internal to the provider library by the code in the IQueryProvider implementation.

    When the GetEnumerator method is called on an object of type QueryableTerraServerData, the query that it represents is executed and the results of the query are enumerated.

    This code, except for the name of the class, is not specific to this TerraServer-USA Web service provider. Therefore, it can be reused for any LINQ provider.

To implement System.Linq.IQueryProvider

  • Add the TerraServerQueryProvider class to your project.

    Visual Basic
    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
    
    
    C#
    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);
            }
        }
    }
    
    

    The query provider code in this class implements the four methods that are required to implement the IQueryProvider interface. The two CreateQuery methods create queries that are associated with the data source. The two Execute methods send such queries off to be executed.

    The non-generic CreateQuery method uses reflection to obtain the element type of the sequence that the query it creates would return if it was executed. It then uses the Activator class to construct a new QueryableTerraServerData instance that is constructed with the element type as its generic type argument. The result of calling the non-generic CreateQuery method is the same as if the generic CreateQuery method had been called with the correct type argument.

    Most of the query execution logic is handled in a different class that you will add later. This functionality is handled elsewhere because it is specific to the data source being queried, whereas the code in this class is generic to any LINQ provider. To use this code for a different provider, you might have to change the name of the class and the name of the query context type that is referenced in two of the methods.

You will need a .NET type to represent the data that is obtained from the Web service. This type will be used in the client LINQ query to define the results it wants. The following procedure creates such a type. This type, named Place, contains information about a single geographical location such as a city, a park, or a lake.

This code also contains an enumeration type, named PlaceType, that defines the various types of geographical location and is used in the Place class.

To create a custom result type

  • Add the Place class and the PlaceType enumeration to your project.

    Visual Basic
    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
    
    
    C#
    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
        }
    }
    
    

    The constructor for the Place type simplifies creating a result object from the type that is returned by the Web service. While the provider can return the result type defined by the Web service API directly, it would require client applications to add a reference to the Web service. By creating a new type as part of the provider library, the client does not have to know about the types and methods that the Web service exposes.

This provider implementation assumes that the innermost call to Queryable.Where contains the location information to use to query the Web service. The innermost Queryable.Where call is the where clause (Where clause in Visual Basic) or Queryable.Where method call that occurs first in a LINQ query, or the one nearest to the "bottom" of the expression tree that represents the query.

To create a query context class

  • Add the TerraServerQueryContext class to your project.

    Visual Basic
    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
    
    
    C#
    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);
            }
        }
    }
    
    

    This class organizes the work of executing a query. After finding the expression that represents the innermost Queryable.Where call, this code retrieves the lambda expression that represents the predicate that was passed to Queryable.Where. It then passes the predicate expression to a method to be partially evaluated, so that all references to local variables are translated into values. Then it calls a method to extract the requested locations from the predicate, and calls another method to obtain the result data from the Web service.

    In the next step, this code copies the expression tree that represents the LINQ query and makes one modification to the expression tree. The code uses an expression tree visitor subclass to replace the data source that the innermost query operator call is applied to with the concrete list of Place objects that were obtained from the Web service.

    Before the list of Place objects is inserted into the expression tree, its type is changed from IEnumerable to IQueryable by calling AsQueryable. This type change is necessary because when the expression tree is rewritten, the node that represents the method call to the innermost query operator method is reconstructed. The node is reconstructed because one of the arguments has changed (that is, the data source that it is applied to). The Call(Expression, MethodInfo, IEnumerable<(Of <(Expression>)>)) method, which is used to reconstruct the node, will throw an exception if any argument is not assignable to the corresponding parameter of the method that it will be passed to. In this case, the IEnumerable list of Place objects would not be assignable to the IQueryable parameter of Queryable.Where. Therefore, its type is changed to IQueryable.

    By changing its type to IQueryable, the collection also obtains an IQueryProvider member, accessed by the Provider property, that can create or execute queries. The dynamic type of the IQueryable°Place collection is EnumerableQuery, which is a type that is internal to the System.Linq API. The query provider that is associated with this type executes queries by replacing Queryable standard query operator calls with the equivalent Enumerable operators, so that effectively the query becomes a LINQ to Objects query.

    The final code in the TerraServerQueryContext class calls one of two methods on the IQueryable list of Place objects. It calls CreateQuery if the client query returns enumerable results, or Execute if the client query returns a non-enumerable result.

    The code in this class is very specific to this TerraServer-USA provider. Therefore, it is encapsulated in the TerraServerQueryContext class instead of being inserted directly into the more generic IQueryProvider implementation.

The provider you are creating requires only the information in the Queryable.Where predicate to query the Web service. Therefore, it uses LINQ to Objects to do the work of executing the LINQ query by using the internal EnumerableQuery type. An alternative way to use LINQ to Objects to execute the query is to have the client wrap the part of the query to be executed by LINQ to Objects in a LINQ to Objects query. This is accomplished by calling AsEnumerable<(Of <(TSource>)>) on the rest of the query, which is the part of the query that the provider requires for its specific purposes. The advantage of this kind of implementation is that the division of work between the custom provider and LINQ to Objects is more transparent.

Note:

The provider presented in this topic is a simple provider that has minimal query support of its own. Therefore, it relies heavily on LINQ to Objects to execute queries. A complex LINQ provider such as LINQ to SQL may support the whole query without handing any work off to LINQ to Objects.

To create a class to obtain data from the Web service

  • Add the WebServiceHelper class (or module in Visual Basic) to your project.

    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
    
    
    C#
    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;
                }
            }
        }
    }
    
    

    This class contains the functionality that obtains data from the Web service. This code uses a type named TerraServiceSoapClient, which is auto-generated for the project by Windows Communication Foundation (WCF), to call the Web service method GetPlaceList. Then, each result is translated from the return type of the Web service method to the .NET type that the provider defines for the data.

    This code contains two checks that enhance the usability of the provider library. The first check limits the maximum time that a client application will wait for a response by limiting the total number of calls that are made to the Web service, per query, to five. For each location that is specified in the client query, one Web service request is generated. Therefore, the provider throws an exception if the query contains more than five locations.

    The second check determines whether the number of results returned by the Web service is equal to the maximum number of results that it can return. If the number of results is the maximum number, it is likely that the results from the Web service are truncated. Instead of returning an incomplete list to the client, the provider throws an exception.

To create the visitor that finds the innermost Where method call expression

  1. Add the ExpressionVisitor class to your project. This code is available in How to: Implement an Expression Tree Visitor. Add using directives (Imports statements in Visual Basic) to the file for the following namespaces: System.Collections.Generic, System.Collections.ObjectModel and System.Linq.Expressions.

  2. Add the InnermostWhereFinder class, which inherits the ExpressionVisitor class, to your project.

    Visual Basic
    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
    
    
    C#
    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;
            }
        }
    }
    
    

    This class inherits the base expression tree visitor class to perform the functionality of finding a specific expression. The base expression tree visitor class is designed to be inherited and specialized for a specific task that involves traversing an expression tree. The derived class overrides the VisitMethodCall method to seek out the expression that represents the innermost call to Where in the expression tree that represents the client query. This innermost expression is the expression that the provider extracts the search locations from.

To create the visitor that extracts data to query the Web service

  • Add the LocationFinder class to your project.

    Visual Basic
    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"