Tutorial: Crear un proveedor LINQ IQueryable

Actualización: Julio de 2008

Este tema avanzado proporciona instrucciones paso a paso para crear un proveedor LINQ personalizado. Cuando finalice, podrá utilizar el proveedor que ha creado para escribir consultas LINQ contra el servicio web TerraServer de Estados Unidos.

El servicio web TerraServer proporciona una interfaz con una base de datos de imágenes aéreas de los Estados Unidos. También expone un método que devuelve información sobre ubicaciones geográficas de los Estados Unidos a partir del nombre o parte del nombre de la ubicación. Este método, denominado GetPlaceList, es el método al que llamará su proveedor LINQ. El proveedor utilizará Windows Communication Foundation (WCF) para comunicarse con el servicio web. Para obtener más información sobre el servicio web TerraServer de Estados Unidos, vea Overview of the TerraServer-USA Web Services.

Este proveedor es un proveedor IQueryable relativamente simple. Espera información específica en las consultas que administra y presenta un sistema de tipos cerrado, que expone un único tipo para representar los datos del resultado. Este proveedor examina sólo un tipo de expresión de llamada a método en el árbol de expresión que representa la consulta, que es la llamada más interna a Where. A partir de esta expresión, extrae los datos que necesita para consultar el servicio web. A continuación, llama al servicio web e inserta los datos devueltos en el árbol de expresión en el lugar del origen de datos IQueryable inicial. Las implementaciones de Enumerable de los operadores de consulta estándar se encargan del resto de la ejecución de la consulta.

Los ejemplos de código incluidos en este tema están escritos en C# y Visual Basic.

En este tutorial se explican las siguientes tareas:

  • Crear el proyecto en Visual Studio.

  • Implementar las interfaces requeridas por un proveedor IQueryable de LINQ: IQueryable<T>,IOrderedQueryable<T> y IQueryProvider.

  • Agregar un tipo .NET personalizado para representar los datos del servicio web.

  • Crear una clase de contexto de consulta y una clase que obtiene los datos del servicio web.

  • Crear una subclase visitante del árbol de expresión que busca la expresión que representa la llamada más interna al método Queryable.Where.

  • Crear una subclase visitante del árbol de expresión que extrae información de la consulta de LINQ para utilizarla en la solicitud del servicio web.

  • Crear una subclase visitante del árbol de expresión que modifica el árbol de expresión que representa la consulta de LINQ completa.

  • Utilizar una clase de evaluador para evaluar parcialmente un árbol de expresión. Este paso es necesario porque traduce todas las referencias a variables locales de la consulta de LINQ en valores.

  • Crear una clase auxiliar de árbol de expresión y una nueva clase de excepción.

  • Probar el proveedor LINQ desde una aplicación cliente que contiene una consulta de LINQ.

  • Agregar capacidades de consulta más complejas al proveedor de LINQ.

    Nota:

    El proveedor LINQ creado por este tutorial está disponible como ejemplo. Para obtener más información, consulte Ejemplo LINQ to TerraServer Provider.

Requisitos previos

Necesita los componentes siguientes para completar este tutorial:

  • Visual Studio 2008
Nota:

Es posible que su equipo muestre nombres o ubicaciones diferentes para algunos de los elementos de la interfaz de usuario de Visual Studio incluidos en las instrucciones siguientes. La edición de Visual Studio que se tenga y la configuración que se utilice determinan estos elementos. Para obtener más información, vea Valores de configuración de Visual Studio.

Crear el proyecto

Para crear el proyecto en Visual Studio

  1. En Visual Studio, cree una nueva aplicación Biblioteca de clases. Asigne al proyecto el nombre LinqToTerraServerProvider.

  2. En el Explorador de soluciones, seleccione el archivo Class1.cs (o Class1.vb) y cámbiele el nombre a QueryableTerraServerData.cs (o QueryableTerraServerData.vb). En el cuadro de diálogo que aparece, haga clic en Sí para cambiar el nombre de todas las referencias al elemento de código.

    Creará el proveedor como un proyecto de Biblioteca de clases en Visual Studio porque las aplicaciones cliente ejecutables agregarán el ensamblado de proveedor como una referencia a su proyecto.

Para agregar una referencia de servicio al servicio web

  1. En el Explorador de soluciones, haga clic con el botón secundario en el proyecto LinqToTerraServerProvider y, a continuación, haga clic en Agregar referencia de servicio.

    Se abrirá el cuadro de diálogo Agregar referencia de servicio.

  2. En el cuadro Dirección, escriba http://terraserver.microsoft.com/TerraService2.asmx.

  3. En el cuadro Espacio de nombres, escriba TerraServerReference y, a continuación, haga clic en Aceptar.

    El servicio web TerraServer de Estados Unidos se agrega como una referencia de servicio para que la aplicación pueda comunicarse con el servicio web por medio de Windows Communication Foundation (WCF). Agregando una referencia del servicio al proyecto, Visual Studio genera un archivo app.config que contiene un proxy y un punto final para el servicio web. Para obtener más información, vea Introducción a los servicios de Windows Communication Foundation en Visual Studio.

Ahora tiene un proyecto con un archivo que se denomina app.config, un archivo que se denomina QueryableTerraServerData.cs (o QueryableTerraServerData.vb), y una referencia al servicio denominada TerraServerReference.

Implementar las interfaces necesarias

Para crear un proveedor de LINQ, como mínimo debe implementar las interfaces IQueryable<T> y IQueryProviderIQueryable<T> y IQueryProvider se derivan de las otras interfaces requeridas; por tanto, al implementar estas dos interfaces, también implementa las otras interfaces que requiere un proveedor de LINQ.

Si desea que se puedan utilizar operadores de consulta de ordenación, como OrderBy y ThenBy, también deberá implementar la interfaz IOrderedQueryable<T>. Dado que IOrderedQueryable<T> se deriva de IQueryable<T>, puede implementar ambas interfaces en un tipo, que es lo que hace este proveedor.

Para implementar System.Linq.IQueryable`1 y System.Linq.IOrderedQueryable`1

  • En el archivo QueryableTerraServerData.cs (o QueryableTerraServerData.vb), agregue el código siguiente.

    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
        }
    }
    

    La implementación de IOrderedQueryable<T> por parte de la clase QueryableTerraServerData implementa tres propiedades declaradas en IQueryable y dos métodos de enumeración declarados en IEnumerable y IEnumerable<T>.

    Esta clase tiene dos constructores. Al primer constructor se le llama desde la aplicación cliente para crear el objeto contra el que se debe escribir la consulta de LINQ. Al segundo constructor se le llama de forma interna a la biblioteca de proveedores desde el código de la implementación de IQueryProvider.

    Cuando se llama al método GetEnumerator sobre un objeto de tipo QueryableTerraServerData, se ejecuta la consulta que representa y se enumeran los resultados de la consulta.

    Este código, salvo el nombre de la clase, no es específico para este proveedor de servicio web TerraServer de Estados Unidos. Por consiguiente, se puede reutilizar para cualquier proveedor de LINQ.

Para implementar System.Linq.IQueryProvider

  • Agregue la clase TerraServerQueryProvider al proyecto.

    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);
            }
        }
    }
    

    El código de proveedor de consulta de esta clase implementa los cuatro métodos que se requieren para implementar la interfaz IQueryProvider. Los dos métodos CreateQuery crean consultas asociadas al origen de datos. Los dos métodos Execute envían esas consultas para su ejecución.

    El método CreateQuery no genérico utiliza la reflexión para obtener el tipo de elemento de la secuencia que devolvería la consulta que lo crea, si se ejecutara. A continuación, utiliza la clase Activator para construir una nueva instancia de QueryableTerraServerData que se construye con el tipo de elemento como su argumento de tipo genérico. El resultado de llamar al método CreateQuery no genérico es el mismo que si se hubiera llamado al método CreateQuery genérico con el argumento de tipo correcto.

    La mayor parte de la lógica de ejecución de la consulta se incluye en una clase diferente que se agregará más tarde. Esta funcionalidad se incluye en otra parte porque es específica del origen de datos que se va a consultar, mientras que el código de esta clase es genérico para cualquier proveedor de LINQ. Si desea utilizar este código para un proveedor diferente, quizá deba cambiar el nombre de la clase y el nombre del tipo de contexto de consulta al que se hace referencia en dos de los métodos.

Agregar un tipo personalizado para representar los datos del resultado

Necesitará un tipo .NET para representar los datos que se obtienen del servicio web. Este tipo se utilizará en la consulta de LINQ del cliente para definir los resultados que desea. En el procedimiento siguiente, se crea dicho tipo. Este tipo, denominado Place, contiene información sobre una ubicación geográfica, como una ciudad, un parque o un lago.

Este código también contiene un tipo de enumeración, denominado PlaceType, que define los diversos tipos de ubicación geográfica, además es el que se utiliza en la clase Place.

Para crear un tipo de resultado personalizado

  • Agregue la clase Place y la enumeración PlaceType al proyecto.

    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
        }
    }
    

    El constructor del tipo Place simplifica la creación de un objeto de resultado a partir del tipo devuelto por el servicio web. Aunque el proveedor puede devolver directamente el tipo de resultado definido por la API del servicio web, esto exigiría que las aplicaciones cliente agregaran una referencia al servicio web. Creando un nuevo tipo como parte de la biblioteca de proveedor, el cliente no tiene que conocer los tipos y métodos que expone el servicio web.

Agregar funcionalidad para recibir datos desde el origen de datos

Esta implementación del proveedor supone que la llamada a Queryable.Where más interna contiene la información de la ubicación geográfica que se debe utilizar para la consulta al servicio web. La llamada más interna a Queryable.Where es la cláusula where (cláusula Where de Visual Basic) o la llamada al método Queryable.Where que ocurre primero en una consulta de LINQ, o la más cercana a la "base" del árbol de expresión que representa la consulta.

Para crear una clase de contexto de consulta

  • Agregue la clase TerraServerQueryContext al proyecto.

    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);
            }
        }
    }
    

    Esta clase organiza el trabajo que conlleva ejecutar una consulta. Después de encontrar la expresión que representa la llamada más interna a Queryable.Where, este código recupera la expresión lambda que representa el predicado que se pasó a Queryable.Where. A continuación, pasa la expresión de predicado a un método para su evaluación parcial, de modo que todas las referencias a variables locales se traduzcan en valores. Después, llama a un método para extraer las ubicaciones solicitadas del predicado, y llama a otro método para obtener los datos del resultado del servicio web.

    En el paso siguiente, este código copia el árbol de expresión que representa la consulta de LINQ y realiza una modificación en árbol de expresión. El código utiliza una subclase visitante del árbol de expresión para reemplazar el origen de datos al que se aplica la llamada de operador de consulta más interna con la lista concreta de los objetos Place que se obtuvieron del servicio web.

    Antes de insertar la lista de los objetos Place en el árbol de expresión, su tipo se cambia de IEnumerable a IQueryable llamando a AsQueryable. Este cambio de tipo es necesario porque cuando se rescribe el árbol de expresión, se reconstruye el nodo que representa la llamada al método de operador de consulta más interno. Ese nodo se reconstruye porque uno de los argumentos ha cambiado (es decir, el origen de datos al que se aplica). El método Call(Expression, MethodInfo, IEnumerable<Expression>), que se utiliza para reconstruir el nodo, producirá una excepción si cualquier argumento no es asignable al parámetro correspondiente del método al que se pasará. En este caso, la lista IEnumerable de los objetos Place no sería asignable al parámetro IQueryable de Queryable.Where. Por consiguiente, su tipo se cambia a IQueryable.

    Cambiando su tipo a IQueryable, la colección también obtiene un miembro IQueryProvider, al que se obtiene acceso mediante la propiedad Provider, que puede crear o ejecutar consultas. El tipo dinámico de la colección IQueryable°Place es EnumerableQuery, que es un tipo interno a la API System.Linq. El proveedor de la consulta asociado a este tipo ejecuta las consultas reemplazando las llamadas al operador de consulta estándar Queryable con los operadores Enumerable equivalentes, de modo que la consulta se convierta efectivamente en una consulta de LINQ to Objects.

    El código final de la clase TerraServerQueryContext llama a uno de dos métodos para aplicarlo sobre la lista IQueryable de los objetos Place. Llama a CreateQuery si la consulta del cliente devuelve resultados enumerables, o a Execute si la consulta del cliente devuelve un resultado no enumerable.

    El código de esta clase es muy específico del TerraServer de Estados Unidos. Por consiguiente, se encapsula en la clase TerraServerQueryContext en lugar de insertarse directamente en la implementación más genérica de IQueryProvider.

El proveedor que está creando requiere sólo la información del predicado Queryable.Where para consultar al servicio web. Por tanto, utiliza LINQ to Objects para realizar el trabajo de ejecutar la consulta de LINQ con el tipo EnumerableQuery interno. Una manera alternativa de utilizar LINQ to Objects para ejecutar la consulta es hacer que el cliente encapsule la parte de la consulta que va a ser ejecutada por LINQ to Objects en una consulta de LINQ to Objects. Esto se logra llamando a AsEnumerable<TSource> sobre el resto de la consulta, es decir, la parte de la consulta que el proveedor necesita para sus propósitos específicos. La ventaja de este tipo de implementación es que la división del trabajo entre el proveedor personalizado y LINQ to Objects es más transparente.

Nota:

El proveedor presentado en este tema es un proveedor sencillo que posee una compatibilidad mínima para consultas. Por consiguiente, descarga casi todo el trabajo en LINQ to Objects para ejecutar las consultas. Un proveedor de LINQ complejo como LINQ to SQL puede encargarse de la consulta entera sin pasar ningún trabajo a LINQ to Objects.

Para crear una clase para obtener datos del servicio web

  • Agregue la clase WebServiceHelper (o el módulo en Visual Basic) a su proyecto.

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

    Esta clase contiene la funcionalidad para obtener datos del servicio web. Este código utiliza un tipo denominado TerraServiceSoapClient, que se genera automáticamente para el proyecto mediante Windows Communication Foundation (WCF), para llamar al método GetPlaceList del servicio web. A continuación, cada resultado se traduce del tipo de valor devuelto del método de servicio web al tipo .NET que el proveedor define para los datos.

    Este código contiene dos comprobaciones que facilitan el uso de la biblioteca de proveedor. La primera comprobación limita el tiempo máximo que una aplicación cliente esperará una respuesta limitando el número total de llamadas que se realizan al servicio web a cinco por cada consulta. Para cada ubicación que se especifica en la consulta del cliente, se genera una solicitud de servicio web. Por consiguiente, el proveedor inicia una excepción si la consulta contiene más de cinco ubicaciones.

    La segunda comprobación determina si el número de resultados devueltos por el servicio web es igual al número máximo de resultados que puede devolver. Si el número de resultados es el número máximo, es probable que los resultados del servicio web aparezcan truncados. En lugar de devolver una lista incompleta al cliente, el proveedor inicia una excepción.

Agregar clases de visitante de árbol de expresión

Para crear el visitante que busca la expresión de llamada al método Where más interna

  1. Agregue la clase ExpressionVisitor al proyecto. Este código está disponible en Cómo: Implementar un visitante de árbol de expresión. Agregue directivas using (o instrucciones Imports en Visual Basic) al archivo para los espacios de nombres siguientes: System.Collections.Generic, System.Collections.ObjectModel y System.Linq.Expressions.

  2. Agregue la clase InnermostWhereFinder, que hereda la clase ExpressionVisitor, al proyecto.

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

    Esta clase hereda la clase base de visitante de árbol de expresión para llevar a cabo la funcionalidad de encontrar una expresión específica. La clase base de visitante de árbol de expresión se ha diseñado para ser heredada, y se especializa en una tarea concreta que implica recorrer un árbol de expresión. La clase derivada invalida el método VisitMethodCall para buscar la expresión que constituye la llamada a Where más interna en el árbol de expresión que representa la consulta del cliente. Esta expresión más interna es la expresión de la que el proveedor extrae las ubicaciones de búsqueda.

Para crear el visitante que extrae los datos para consultar el servicio web

  • Agregue la clase LocationFinder al proyecto.

    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);
            }
        }
    }
    

    Esta clase se utiliza para extraer la información de las ubicación geográficas en el predicado que el cliente pasa a Queryable.Where. Se deriva de la clase base de visitante de árbol de expresión e invalida sólo el método VisitBinary.

    La clase base de visitante de árbol de expresión envía expresiones binarias de igualdad tales como place.Name == "Seattle" (place.Name = "Seattle" en Visual Basic) al método VisitBinary. En este método VisitBinary de invalidación, si la expresión coincide con el modelo de expresión de igualdad que proporciona información de la ubicación, esa información se extrae y se almacena en una lista de ubicaciones.

    Esta clase utiliza un visitante de árbol de expresión para buscar la información de la ubicación en el árbol de expresión, ya que un visitante está diseñado para recorrer y examinar árboles de expresión. El código resultante es más claro y menos propenso a errores que si se hubiera implementado sin utilizar el visitante.

    En esta fase del tutorial, el proveedor sólo admite ciertas maneras limitadas de proporcionar información de la ubicación en la consulta. Más adelante en este tema, agregará funcionalidad para poder utilizar otras formas de proporcionar información de la ubicación.

Para crear el visitante que modifica el árbol de expresión

  • Agregue la clase ExpressionTreeModifier al proyecto.

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

    Esta clase se deriva de la clase base de visitante de árbol de expresión e invalida el método VisitConstant. En este método, reemplaza el objeto al que se aplica la llamada del operador de consulta estándar más interno con una lista concreta de objetos Place.

    El método CopyAndModify llama a la implementación de la clase base del método Visit. Este método CopyAndModify es necesario porque al método Visit, que es protected (Protected en Visual Basic), no se le puede llamar directamente desde la clase de contexto de consulta.

    Esta clase modificadora de árbol de expresión utiliza el visitante de árbol de expresión, ya que el visitante está diseñado para recorrer, examinar y copiar árboles de expresión. Al derivarse de la clase base de visitante de árbol de expresión, esta clase requiere un mínimo de código para realizar su función.

Agregar el evaluador de expresión

El predicado que se pasa al método Queryable.Where en la consulta del cliente puede contener subexpresiones que no dependen del parámetro de la expresión lambda. Estas subexpresiones aisladas pueden y deben evaluarse inmediatamente. Podrían ser referencias a variables locales o variables miembro que se deben traducir a valores.

La clase siguiente expone un método, PartialEval(Expression), que determina cual de los subárboles de la expresión, si existe alguno, se puede evaluar inmediatamente. A continuación, evalúa esas expresiones creando una expresión lambda, compilándola e invocando el delegado devuelto. Finalmente, reemplaza el subárbol con un nuevo nodo que representa un valor constante. Esto se conoce como evaluación parcial.

Para agregar una clase para realizar una evaluación parcial de un árbol de expresión

  • Agregue la clase Evaluator al proyecto.

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

Agregar las clases auxiliares

Esta sección contiene el código de tres clases auxiliares para su proveedor.

Para agregar la clase auxiliar utilizada por la implementación de System.Linq.IQueryProvider

  • Agregue la clase TypeSystem (o el módulo en Visual Basic) a su proyecto.

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

    La implementación de IQueryProvider que agregó anteriormente utiliza esta clase auxiliar.

    TypeSystem.GetElementType utiliza la reflexión para obtener el argumento de tipo genérico de una colección IEnumerable<T> (IEnumerable(Of T) en Visual Basic). Este método recibe la llamada desde el método CreateQuery no genérico de la implementación de proveedor de consulta para proporcionar el tipo de elemento de la colección de resultados de la consulta.

    Esta clase auxiliar no es específica de este proveedor de servicios web TerraServer de Estados Unidos. Por consiguiente, se puede reutilizar para cualquier proveedor de LINQ.

Para crear una clase auxiliar de árbol de expresión

  • Agregue la clase ExpressionTreeHelpers al proyecto.

    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));
            }
        }
    }
    

    Esta clase contiene métodos que se pueden utilizar para determinar información sobre datos de tipos específicos de árboles de expresión y extraer esos datos. En este proveedor, la clase LocationFinder utiliza estos métodos para extraer información de la ubicación a partir del árbol de expresión que representa la consulta.

Para agregar un tipo de excepción a consultas no válidas

  • Agregue la clase InvalidQueryException al proyecto.

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

    Esta clase define un tipo Exception que su proveedor puede iniciar cuando no entienda la consulta de LINQ procedente del cliente. Definiendo este tipo de excepción de consulta no válida, el proveedor puede iniciar una excepción más específica que Exception desde diversos lugares del código.

Ahora, ya ha agregado todos los elementos que se requieren para compilar el proveedor. Genere el proyecto LinqToTerraServerProvider y compruebe que no hay ningún error de compilación.

Probar el proveedor LINQ

Puede probar su proveedor LINQ creando una aplicación cliente que contenga una consulta de LINQ contra su origen de datos.

Para crear una aplicación cliente para probar su proveedor

  1. Agregue un nuevo proyecto Aplicación de consola a su solución y asígnele el nombre ClientApp.

  2. En el nuevo proyecto, agregue una referencia al ensamblado del proveedor.

  3. Arrastre el archivo app.config desde su proyecto de proveedor hasta el proyecto de cliente. (Este archivo es necesario para comunicarse con el servicio web.)

    Nota:

    En Visual Basic, puede que tenga que hacer clic en el botón Mostrar todos los archivos para ver el archivo app.config en el Explorador de soluciones.

  4. Agregue las siguientes instrucciones using (instrucción Imports en Visual Basic) al archivo Program.cs (o Module1.vb en Visual Basic):

    using System;
    using System.Linq;
    using LinqToTerraServerProvider;
    
    Imports LinqToTerraServerProvider
    
  5. En el método Main del archivo Program.cs (o Module1.vb en Visual Basic), inserte el código siguiente:

    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
    

    Este código crea una nueva instancia del tipo IQueryable<T> que definió en su proveedor y, a continuación, consulta ese objeto mediante LINQ. La consulta especifica una ubicación, para la que se deben obtener los datos, utilizando una expresión de igualdad. Dado que el origen de datos implementa IQueryable, el compilador traduce la sintaxis de la expresión de consulta en llamadas a los operadores de consulta estándar definidos en Queryable. Internamente, estos métodos de operador de consulta estándar generan un árbol de expresión y llaman a los métodos Execute o CreateQuery que se implementaron como parte de su implementación de IQueryProvider.

  6. Genere ClientApp.

  7. Establezca esta aplicación cliente como el proyecto "de inicio" para su solución. En el Explorador de soluciones, haga clic con el botón secundario del mouse en el proyecto ClientApp y seleccione Establecer como proyecto de inicio.

  8. Ejecute el programa y vea los resultados. Deberían aparecer aproximadamente tres resultados.

Agregar capacidades de consulta más complejas

El proveedor que tiene hasta ahora proporciona un medio muy limitado para que los clientes especifiquen información de la ubicación geográfica en la consulta de LINQ. Específicamente, el proveedor sólo puede obtener información de la ubicación a partir de expresiones de igualdad tales como Place.Name == "Seattle" o Place.State == "Alaska" (Place.Name = "Seattle" o Place.State = "Alaska" en Visual Basic).

El procedimiento siguiente muestra cómo agregar un medio adicional de especificar información de la ubicación. Cuando haya agregado este código, su proveedor podrá extraer información de la ubicación a partir de expresiones de llamada a método tales como place.Name.StartsWith("Seat").

Para agregar compatibilidad con predicados que contienen String.StartsWith

  1. En el proyecto LinqToTerraServerProvider, agregue el método VisitMethodCall a la definición de la clase 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. Vuelva a compilar el proyecto LinqToTerraServerProvider.

  3. Para probar la nueva capacidad de su proveedor, abra el archivo Program.cs (o Module1.vb en Visual Basic) en el proyecto ClientApp. Reemplace el código del método Main por el siguiente código:

    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. Ejecute el programa y vea los resultados. Deberían aparecer unos 29 resultados.

El procedimiento siguiente muestra cómo agregar funcionalidad a su proveedor para permitir que la consulta del cliente especifique información de la ubicación mediante dos métodos adicionales, en concreto Enumerable.Contains y List<T>.Contains. Cuando haya agregado este código, su proveedor podrá extraer información de la ubicación a partir de las expresiones de llamada a método incluidas en la consulta del cliente, tales como placeList.Contains(place.Name), donde la colección placeList es una lista concreta proporcionada por el cliente. La ventaja de permitir que los clientes utilicen el método Contains es que pueden especificar cualquier número de ubicaciones simplemente agregándolas a placeList. Al variar el número de ubicaciones, no cambia la sintaxis de la consulta.

Para agregar compatibilidad con las consultas que presentan el método Contains en su cláusula 'where'

  1. En el proyecto LinqToTerraServerProvider, en la definición de la clase LocationFinder, reemplace el método VisitMethodCall por el código siguiente:

    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);
    }
    

    Este método agrega cada cadena de la colección a la que se aplica Contains a la lista de ubicaciones con la que se consulta el servicio web. Un método denominado Contains se define en Enumerable y en List<T>. Por consiguiente, el método VisitMethodCall debe comprobar ambos tipos declarativos. Enumerable.Contains se define como un método de extensión; por consiguiente, la colección a la que se aplica es realmente el primer argumento del método. List.Contains se define como un método de instancia; por consiguiente, la colección a la que se aplica es el objeto receptor del método.

  2. Vuelva a compilar el proyecto LinqToTerraServerProvider.

  3. Para probar la nueva capacidad de su proveedor, abra el archivo Program.cs (o Module1.vb en Visual Basic) en el proyecto ClientApp. Reemplace el código del método Main por el siguiente código:

    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. Ejecute el programa y vea los resultados. Deberían aparecer unos 5 resultados.

Pasos siguientes

En este tema del tutorial se mostró cómo crear un proveedor de LINQ para un método de un servicio web. Si desea conseguir un mayor desarrollo de un proveedor de LINQ, considere estas posibilidades:

  • Permita al proveedor de LINQ administrar otras maneras de especificar una ubicación geográfica en la consulta del cliente.

  • Investigue los otros métodos que expone el servicio web TerraServer de Estados Unidos y cree un proveedor de LINQ que se comunique con uno de esos métodos.

  • Busque un servicio web diferente en el que esté interesado y cree un proveedor LINQ para él.

  • Cree un proveedor de LINQ para un origen de datos distinto de un servicio web.

Vea también

Tareas

Ejemplo LINQ to TerraServer Provider

Cómo: Implementar un visitante de árbol de expresión

Cómo: Modificar árboles de expresiones

Conceptos

Habilitar un origen de datos para realizar consultas LINQ

Referencia

IQueryable<T>

IOrderedQueryable<T>

Otros recursos

Servicios de Windows Communication Foundation y servicios de datos de ADO.NET

Historial de cambios

Fecha

Historial

Motivo

Julio de 2008

Se ha agregado un vínculo al ejemplo de TerraServer.

Corrección de errores de contenido.