Procedura dettagliata: creazione di un provider LINQ IQueryable

In questo argomento avanzato vengono fornite le istruzioni dettagliate per la creazione di un provider LINQ personalizzato. Al termine, sarà possibile utilizzare il provider creato per scrivere le query LINQ sul servizio Web TerraServer-USA.

Il servizio Web TerraServer-USA fornisce un'interfaccia a un database di immagini aeree degli Stati Uniti. Espone anche un metodo che restituisce informazioni sui luoghi degli Stati Uniti, fornendo una parte o tutto il nome di un luogo. Questo metodo, denominato GetPlaceList, rappresenta il metodo che verrà chiamato dal provider LINQ. Il provider utilizzerà Windows Communication Foundation (WCF) per comunicare con il servizio Web. Per ulteriori informazioni sul servizio Web TerraServer-USA, vedere Overview of the TerraServer-USA Web Services (informazioni in lingua inglese).

Si tratta di un provider IQueryable relativamente semplice. Prevede informazioni specifiche nelle query che gestisce e ha un sistema del tipo chiuso, esponendo un solo tipo per rappresentare i dati del risultato. Questo provider esamina solo uno tipo di espressione della chiamata al metodo nella struttura ad albero dell'espressione che rappresenta la query, ovvero la chiamata più interna a Where. Estrae i dati necessari per eseguire una query sul servizio Web da questa espressione. Chiama quindi il servizio Web e inserisce i dati restituiti nella struttura ad albero dell'espressione al posto dell'origine dati IQueryable iniziale. L'esecuzione rimanente della query viene gestita dalle implementazioni Enumerable degli operatori di query standard.

Gli esempi di codice presenti in questo argomento sono forniti in C# e Visual Basic.

In questa procedura dettagliata vengono illustrate le attività seguenti:

  • Creazione del progetto in Visual Studio.

  • Implementazione delle interfacce richieste per un provider IQueryable LINQ: IQueryable<T>, IOrderedQueryable<T> e IQueryProvider.

  • Aggiunta di un tipo .NET personalizzato per rappresentare i dati dal servizio Web.

  • Creazione di una classe di contesto della query e una classe che ottiene i dati dal servizio Web.

  • Creazione di una sottoclasse del visitatore della struttura ad albero dell'espressione che cerca l'espressione che rappresenta la chiamata più interna al metodo Queryable.Where.

  • Creazione di una sottoclasse del visitatore della struttura ad albero dell'espressione che estrae le informazioni dalla query LINQ da utilizzare nella richiesta del servizio Web.

  • Creazione di una sottoclasse del visitatore della struttura ad albero dell'espressione che modifica la struttura ad albero dell'espressione che rappresenta la query LINQ completa.

  • Utilizzo di una classe dell'analizzatore per valutare parzialmente una struttura ad albero dell'espressione. Questo passaggio è necessario poiché converte tutti i riferimenti alle variabili locali nella query LINQ in valori.

  • Creazione di una classe di supporto della struttura ad albero dell'espressione e di una nuova classe di eccezioni.

  • Test del provider LINQ da un'applicazione client che contiene una query LINQ.

  • Aggiunta di funzionalità di query più complesse al provider LINQ.

    Nota

    Il provider LINQ creato con questa procedura dettagliata è disponibile come esempio. Per ulteriori informazioni, vedere Esempi LINQ.

Prerequisiti

Per completare la procedura dettagliata, è necessario disporre dei componenti seguenti:

  • Visual Studio 2008

Nota

Nel computer in uso è possibile che vengano visualizzati nomi o percorsi diversi per alcuni elementi dell'interfaccia utente di Visual Studio nelle istruzioni seguenti. La versione di Visual Studio in uso e le impostazioni configurate determinano questi elementi. Per ulteriori informazioni vedere Impostazioni di Visual Studio.

Creazione del progetto

Per creare il progetto in Visual Studio

  1. In Visual Studio creare una nuova applicazione Libreria di classi. Assegnare al progetto il nome LinqToTerraServerProvider.

  2. In Esplora soluzioni selezionare il file Class1.cs o Class1.vb e rinominarlo come QueryableTerraServerData.cs o QueryableTerraServerData.vb. Nella finestra di dialogo visualizzata fare clic su per rinominare tutti i riferimenti all'elemento di codice.

    Il provider viene creato come progetto Libreria di classi in Visual Studio poiché le applicazioni client eseguibili aggiungeranno l'assembly di provider come riferimento al progetto.

Per aggiungere un riferimento al servizio Web

  1. In Esplora soluzioni fare clic con il pulsante destro del mouse sul progetto LinqToTerraServerProvider e scegliere Aggiungi riferimento a servizio.

    Verrà visualizzata la finestra di dialogo Aggiungi riferimento a servizio.

  2. Nella casella Indirizzo digitare http://terraserver.microsoft.com/TerraService2.asmx.

  3. Nella casella Spazio dei nomi digitare TerraServerReference, quindi scegliere OK.

    Il servizio Web TerraServer-USA viene aggiunto come riferimento al servizio in modo che l'applicazione possa comunicare con il servizio Web tramite Windows Communication Foundation (WCF). Aggiungendo un riferimento al servizio nel progetto, Visual Studio genera un file app.config che contiene un proxy e un endpoint per il servizio Web. Per ulteriori informazioni, vedere Servizi Windows Communication Foundation e dati WCF in Visual Studio.

È stato così creato un progetto contenente un file denominato app.config, un file denominato QueryableTerraServerData.cs o QueryableTerraServerData.vb e un riferimento al servizio denominato TerraServerReference.

Implementazione delle interfacce necessarie

Per creare un provider LINQ, è necessario implementare almeno le interfacce IQueryable<T> e IQueryProvider. IQueryable<T> e IQueryProvider vengono derivate dalle altre interfacce necessarie; pertanto, implementando queste due interfacce, si implementano anche le altre interfacce necessarie per un provider LINQ.

Se si desidera supportare operatori di query di ordinamento, ad esempio OrderBy e ThenBy, è necessario implementare anche l'interfaccia IOrderedQueryable<T>. Poiché IOrderedQueryable<T> deriva da IQueryable<T>, è possibile implementare entrambe queste interfacce in un unico tipo. Tale operazione viene in pratica effettuata dal provider.

Per implementare System.Linq.IQueryable'1 e System.Linq.IOrderedQueryable'1

  • Nel file QueryableTerraServerData.cs o QueryableTerraServerData.vb, aggiungere il codice seguente.

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

    L'implementazione IOrderedQueryable<T> mediante la classe QueryableTerraServerData implementa tre proprietà dichiarate in IQueryable e due metodi di enumerazione dichiarati in IEnumerable e IEnumerable<T>.

    Questa classe dispone di due costruttori. Il primo costruttore viene chiamato dall'applicazione client per creare l'oggetto su cui scrivere la query LINQ. Il secondo costruttore viene chiamato all'interno della libreria del provider dal codice nell'implementazione IQueryProvider.

    Quando il metodo GetEnumerator viene chiamato su un oggetto di tipo QueryableTerraServerData, viene eseguita la relativa query e vengono enumerati i risultati della query.

    Questo codice, ad eccezione del nome della classe, non è specifico di questo provider di servizi TerraServer-USA. Pertanto, può essere riutilizzato per qualsiasi provider LINQ.

Per implementare System.Linq.IQueryProvider

  • Aggiungere la classe TerraServerQueryProvider al progetto.

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

    Il codice del provider della query in questa classe implementa i quattro metodi richiesti per implementare l'interfaccia IQueryProvider. I due metodi CreateQuery creano query associate all'origine dati. I due metodi Execute inviano tali query da eseguire.

    Il metodo CreateQuery non generico utilizza la reflection per ottenere il tipo di elemento della sequenza che la query creata restituisce quando viene eseguita. Utilizza quindi la classe Activator per costruire una nuova istanza QueryableTerraServerData costruita con il tipo di elemento come argomento di tipo generico. Il risultato della chiamata al metodo CreateQuery non generico corrisponde al metodo CreateQuery generico chiamato con l'argomento di tipo corretto.

    La maggior parte della logica di esecuzione della query viene gestita in una classe diversa che verrà aggiunta successivamente. Questa funzionalità viene gestita altrove poiché è specifica dell'origine dati su cui eseguire una query, mentre il codice in questa classe è generico per qualsiasi provider LINQ. Per utilizzare questo codice per un provider diverso, è possibile dovere modificare il nome della classe e il nome del tipo di contesto della query a cui viene fatto riferimento nei due metodi.

Aggiunta di un tipo personalizzato per rappresentare i dati dei risultati

È necessario un tipo .NET per rappresentare i dati ottenuti dal servizio Web. Questo tipo verrà utilizzato nella query LINQ client per definire i risultati desiderati. Nella procedura descritta di seguito viene creato tale tipo. Questo tipo, denominato Place, contiene le informazioni su una singola località geografica, ad esempio una città, un parco o un lago.

Questo codice contiene anche un tipo di enumerazione, denominato PlaceType, che definisce i vari tipi di località geografica e che viene utilizzato nella classe Place.

Per creare un tipo di risultati personalizzato

  • Aggiungere la classe Place e l'enumerazione PlaceType al progetto.

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

    Il costruttore per il tipo Place semplifica la creazione di un oggetto risultato dal tipo restituito dal servizio Web. Sebbene il provider possa restituire il tipo di risultati definito direttamente dall'API del servizio Web, le applicazioni client devono aggiungere un riferimento al servizio Web. Creando un nuovo tipo come parte della libreria del provider, il client non deve necessariamente conoscere i tipi e i metodi esposti dal servizio Web.

Aggiunta della funzionalità per ottenere i dati dall'origine dati

Questa implementazione del provider presuppone che la chiamata più interna a Queryable.Where contenga le informazioni sul percorso da utilizzare per eseguire una query sul servizio Web. La chiamata Queryable.Where più interna è la clausola where (clausola Where in Visual Basic) o la chiamata al metodo Queryable.Where che si verifica prima in una query LINQ o in quella più vicino alla fine della struttura ad albero dell'espressione che rappresenta la query.

Per creare una classe di contesto della query

  • Aggiungere la classe TerraServerQueryContext al progetto.

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

    Questa classe organizza l'esecuzione di una query. Dopo avere trovato l'espressione che rappresenta la chiamata Queryable.Where più interna, questo codice recupera l'espressione lambda che rappresenta il predicato passato a Queryable.Where. Passa quindi l'espressione di predicato a un metodo da valutare parzialmente, in modo che tutti i riferimenti alle variabili locali vengano convertiti in valori. Chiama quindi un metodo per estrarre i percorsi richiesti dal predicato e chiama un altro metodo per ottenere i dati dei risultati dal servizio Web.

    Nel passaggio successivo questo codice copia la struttura ad albero dell'espressione che rappresenta la query LINQ ed effettua una modifica alla struttura ad albero dell'espressione. Il codice utilizza una sottoclasse del visitatore della struttura ad albero dell'espressione per sostituire l'origine dati a cui viene applicata la chiamata dell'operatore di query più interna con l'elenco concreto di oggetti Place ottenuto dal servizio Web.

    Prima di inserire l'elenco di oggetti Place nella struttura ad albero dell'espressione, il tipo viene impostato da IEnumerable su IQueryable chiamando AsQueryable. Questa modifica al tipo è necessaria poiché quando viene riscritta la struttura ad albero dell'espressione, viene ricostruito il nodo che rappresenta la chiamata al metodo dell'operatore di query più interno. Il nodo viene ricostruito poiché è stato modificato uno degli argomenti, ovvero l'origine dati a cui è applicato. Il metodo Call(Expression, MethodInfo, IEnumerable<Expression>), utilizzato per ricostruire il nodo, genererà un'eccezione se un argomento non può essere assegnato al parametro corrispondente del metodo al quale verrà passato. In questo caso, l'elenco IEnumerable di oggetti Place non potrà essere assegnato al parametro IQueryable di Queryable.Where. Pertanto, il tipo viene impostato su IQueryable.

    Impostando il tipo su IQueryable, l'insieme ottiene anche un membro IQueryProvider, a cui è possibile accedere dalla proprietà Provider che può creare o eseguire query. Il tipo dinamico dell'insieme IQueryable Place è EnumerableQuery, ovvero un tipo all'interno dell'API System.Linq. Il provider della query associato a questo tipo esegue query sostituendo le chiamate degli operatori di query standard Queryable con gli operatori Enumerable equivalenti, in modo che la query diventi effettivamente una query LINQ to Objects.

    Il codice finale nella classe TerraServerQueryContext chiama uno dei due metodi nell'elenco IQueryable di oggetti Place. Chiama CreateQuery se la query client restituisce risultati enumerabili o Execute se la query client restituisce un risultato non enumerabile.

    Il codice in questa classe è specifico di questo provider TerraServer-USA. Pertanto, è incapsulato nella classe TerraServerQueryContext anziché inserito direttamente nell'implementazione IQueryProvider più generica.

Il provider che si sta creando richiede solo le informazioni nel predicato Queryable.Where per eseguire una query sul servizio Web. Pertanto, utilizza LINQ to Objects per eseguire la query LINQ utilizzando il tipo EnumerableQuery interno. Un modo alternativo per utilizzare LINQ to Objects per eseguire la query è che il client esegua il wrapping della parte della query da eseguire mediante LINQ to Objects in una query LINQ to Objects. Ciò è possibile chiamando AsEnumerable<TSource> sul resto della query, ovvero la parte della query che il provider richiede per scopi specifici. Il vantaggio di questo tipo di implementazione è che la divisione del lavoro tra il provider personalizzato e LINQ to Objects è più trasparente.

Nota

Il provider illustrato in questo argomento è un semplice provider con un supporto specifico minimo delle query. Pertanto, si basa in modo rilevante su LINQ to Objects per eseguire query. Un provider LINQ complesso come LINQ to SQL può supportare l'intera query senza passare il lavoro a LINQ to Objects.

Per creare una classe per ottenere i dati dal servizio Web

  • Aggiungere al progetto la classe WebServiceHelper o il modulo in Visual Basic.

    Imports System.Collections.Generic
    Imports LinqToTerraServerProvider.TerraServerReference
    
    Friend Module WebServiceHelper
        Private numResults As Integer = 200
        Private mustHaveImage As Boolean = False
    
        Friend Function GetPlacesFromTerraServer(ByVal locations As List(Of String)) As Place()
            ' Limit the total number of Web service calls.
            If locations.Count > 5 Then
                Dim s = "This query requires more than five separate calls to the Web service. Please decrease the number of places."
                Throw New InvalidQueryException(s)
            End If
    
            Dim allPlaces As New List(Of Place)
    
            ' For each location, call the Web service method to get data.
            For Each location In locations
                Dim places = CallGetPlaceListMethod(location)
                allPlaces.AddRange(places)
            Next
    
            Return allPlaces.ToArray()
        End Function
    
        Private Function CallGetPlaceListMethod(ByVal location As String) As Place()
    
            Dim client As New TerraServiceSoapClient()
            Dim placeFacts() As PlaceFacts
    
            Try
                ' Call the Web service method "GetPlaceList".
                placeFacts = client.GetPlaceList(location, numResults, mustHaveImage)
    
                ' If we get exactly 'numResults' results, they are probably truncated.
                If (placeFacts.Length = numResults) Then
                    Dim s = "The results have been truncated by the Web service and would not be complete. Please try a different query."
                    Throw New Exception(s)
                End If
    
                ' Create Place objects from the PlaceFacts objects returned by the Web service.
                Dim places(placeFacts.Length - 1) As Place
                For i = 0 To placeFacts.Length - 1
                    places(i) = New Place(placeFacts(i).Place.City, 
                                          placeFacts(i).Place.State, 
                                          placeFacts(i).PlaceTypeId)
                Next
    
                ' Close the WCF client.
                client.Close()
    
                Return places
            Catch timeoutException As TimeoutException
                client.Abort()
                Throw
            Catch communicationException As System.ServiceModel.CommunicationException
                client.Abort()
                Throw
            End Try
        End Function
    End Module
    
    using System;
    using System.Collections.Generic;
    using LinqToTerraServerProvider.TerraServerReference;
    
    namespace LinqToTerraServerProvider
    {
        internal static class WebServiceHelper
        {
            private static int numResults = 200;
            private static bool mustHaveImage = false;
    
            internal static Place[] GetPlacesFromTerraServer(List<string> locations)
            {
                // Limit the total number of Web service calls.
                if (locations.Count > 5)
                    throw new InvalidQueryException("This query requires more than five separate calls to the Web service. Please decrease the number of locations in your query.");
    
                List<Place> allPlaces = new List<Place>();
    
                // For each location, call the Web service method to get data.
                foreach (string location in locations)
                {
                    Place[] places = CallGetPlaceListMethod(location);
                    allPlaces.AddRange(places);
                }
    
                return allPlaces.ToArray();
            }
    
            private static Place[] CallGetPlaceListMethod(string location)
            {
                TerraServiceSoapClient client = new TerraServiceSoapClient();
                PlaceFacts[] placeFacts = null;
    
                try
                {
                    // Call the Web service method "GetPlaceList".
                    placeFacts = client.GetPlaceList(location, numResults, mustHaveImage);
    
                    // If there are exactly 'numResults' results, they are probably truncated.
                    if (placeFacts.Length == numResults)
                        throw new Exception("The results have been truncated by the Web service and would not be complete. Please try a different query.");
    
                    // Create Place objects from the PlaceFacts objects returned by the Web service.
                    Place[] places = new Place[placeFacts.Length];
                    for (int i = 0; i < placeFacts.Length; i++)
                    {
                        places[i] = new Place(
                            placeFacts[i].Place.City,
                            placeFacts[i].Place.State,
                            placeFacts[i].PlaceTypeId);
                    }
    
                    // Close the WCF client.
                    client.Close();
    
                    return places;
                }
                catch (TimeoutException timeoutException)
                {
                    client.Abort();
                    throw;
                }
                catch (System.ServiceModel.CommunicationException communicationException)
                {
                    client.Abort();
                    throw;
                }
            }
        }
    }
    

    Questa classe contiene la funzionalità che ottiene i dati dal servizio Web. Questo codice utilizza un tipo denominato TerraServiceSoapClient, generato automaticamente per il progetto da Windows Communication Foundation (WCF), per chiamare il metodo del servizio Web GetPlaceList. Quindi, ogni risultato viene convertito dal tipo restituito del metodo del servizio Web nel tipo .NET che il provider definisce per i dati.

    Questo codice contiene due controlli che migliorano l'utilizzabilità della libreria del provider. Il primo controllo limita il tempo massimo di attesa di una risposta da parte di un'applicazione client limitando a cinque il numero totale di chiamate effettuate al servizio Web per query. Per ogni percorso specificato nella query client, viene generata una richiesta del servizio Web. Pertanto, il provider genera un'eccezione se la query contiene più di cinque percorsi.

    Il secondo controllo determina se il numero di risultati restituiti dal servizio Web è uguale al numero massimo di risultati che può restituire. Se il numero di risultati è il numero massimo, è probabile che i risultati del servizio Web vengano troncati. Anziché restituire un elenco incompleto al client, il provider genera un'eccezione.

Aggiunta di classi del visitatore della struttura ad albero dell'espressione

Per creare il visitatore che cerca l'espressione della chiamata al metodo Where più interna

  1. Aggiungere al progetto la classe InnermostWhereFinder, che eredita la classe ExpressionVisitor.

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

    Questa classe eredita la classe del visitatore della struttura ad albero dell'espressione di base per eseguire la funzionalità di ricerca di un'espressione specifica. La classe del visitatore della struttura ad albero dell'espressione di base è progettata per essere ereditata e specializzata per un'attività specifica che implica il passaggio di una struttura ad albero dell'espressione. La classe derivata esegue l'override del metodo VisitMethodCall per cercare l'espressione che rappresenta la chiamata più interna a Where nella struttura ad albero dell'espressione che rappresenta la query client. Questa espressione più interna è l'espressione da cui il provider estrae i percorsi di ricerca.

  2. Aggiungere le direttive using (istruzioni Imports in Visual Basic) al file per gli spazi dei nomi seguenti: System.Collections.Generic, System.Collections.ObjectModel e System.Linq.Expressions.

Per creare il visitatore che estrae i dati per eseguire una query sul servizio Web

  • Aggiungere la classe LocationFinder al progetto.

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

    Questa classe viene utilizzata per estrarre le informazioni sul percorso dal predicato che il client passa a Queryable.Where. Deriva dalla classe ExpressionVisitor ed esegue l'override solo del metodo VisitBinary.

    La classe ExpressionVisitor invia le espressioni binarie, ad esempio le espressioni di uguaglianza come place.Name == "Seattle" (place.Name = "Seattle" in Visual Basic), al metodo VisitBinary. In questo metodo di overriding VisitBinary, se l'espressione corrisponde al modello dell'espressione di uguaglianza che può fornire informazioni sul percorso, tali informazioni vengono estratte e archiviate in un elenco di percorsi.

    Questa classe utilizza un visitatore della struttura ad albero dell'espressione per cercare le informazioni sul percorso nella struttura ad albero dell'espressione poiché un visitatore è progettato per trasferire ed esaminare le strutture ad albero dell'espressione. Il codice risultante è più preciso e meno soggetto a errori rispetto all'implementazione senza utilizzare il visitatore.

    In questa fase della procedura dettagliata, il provider supporta solo alcune modalità per fornire le informazioni sul percorso nella query. Più avanti in questo argomento verrà aggiunta la funzionalità per consentire ulteriori modalità per fornire informazioni sul percorso.

Per creare il visitatore che modifica la struttura ad albero dell'espressione

  • Aggiungere la classe ExpressionTreeModifier al progetto.

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

    Questa classe deriva dalla classe ExpressionVisitor ed esegue l'override del metodo VisitConstant. In questo metodo, sostituisce l'oggetto a cui viene applicata la chiamata dell'operatore di query standard più interna con un elenco concreto di oggetti Place.

    Questa classe di modificatori della struttura ad albero dell'espressione utilizza il visitatore della struttura ad albero dell'espressione poiché il visitatore è progettato per trasferire, esaminare e copiare le strutture ad albero dell'espressione. Derivando dalla classe del visitatore della struttura ad albero dell'espressione di base, questa classe richiede una quantità minima di codice per eseguire la relativa funzione.

Aggiunta dell'analizzatore di espressioni

Il predicato passato al metodo Queryable.Where nella query client può contenere sottoespressioni che non dipendono dal parametro dell'espressione lambda. Queste sottoespressioni isolate possono e devono essere valutate immediatamente. Possono essere riferimenti a variabili locali o variabili membro che devono essere convertite in valori.

La classe successiva espone un metodo, PartialEval(Expression), che determina la sottostruttura ad albero nell'espressione, se presente, da valutare immediatamente. Valuta quindi tali espressioni creando un'espressione lambda, compilandola e richiamando il delegato restituito. Infine, sostituisce la sottostruttura ad albero con un nuovo nodo che rappresenta un valore costante. Questa operazione è nota come valutazione parziale.

Per aggiungere una classe per eseguire la valutazione parziale di una struttura ad albero dell'espressione

  • Aggiungere la classe Evaluator al progetto.

    Imports System.Linq.Expressions
    
    Public Module Evaluator
        ''' <summary>Performs evaluation and replacement of independent sub-trees</summary>
        ''' <param name="expr">The root of the expression tree.</param>
        ''' <param name="fnCanBeEvaluated">A function that decides whether a given expression node can be part of the local function.</param>
        ''' <returns>A new tree with sub-trees evaluated and replaced.</returns>
        Public Function PartialEval(
            ByVal expr As Expression, 
            ByVal fnCanBeEvaluated As Func(Of Expression, Boolean)
            )  As Expression
    
            Return New SubtreeEvaluator(New Nominator(fnCanBeEvaluated).Nominate(expr)).Eval(expr)
        End Function
    
        ''' <summary>
        ''' Performs evaluation and replacement of independent sub-trees
        ''' </summary>
        ''' <param name="expression">The root of the expression tree.</param>
        ''' <returns>A new tree with sub-trees evaluated and replaced.</returns>
        Public Function PartialEval(ByVal expression As Expression) As Expression
            Return PartialEval(expression, AddressOf Evaluator.CanBeEvaluatedLocally)
        End Function
    
        Private Function CanBeEvaluatedLocally(ByVal expression As Expression) As Boolean
            Return expression.NodeType <> ExpressionType.Parameter
        End Function
    
        ''' <summary>
        ''' Evaluates and replaces sub-trees when first candidate is reached (top-down)
        ''' </summary>
        Class SubtreeEvaluator
            Inherits ExpressionVisitor
    
            Private candidates As HashSet(Of Expression)
    
            Friend Sub New(ByVal candidates As HashSet(Of Expression))
                Me.candidates = candidates
            End Sub
    
            Friend Function Eval(ByVal exp As Expression) As Expression
                Return Me.Visit(exp)
            End Function
    
            Public Overrides Function Visit(ByVal exp As Expression) As Expression
                If exp Is Nothing Then
                    Return Nothing
                ElseIf Me.candidates.Contains(exp) Then
                    Return Me.Evaluate(exp)
                End If
    
                Return MyBase.Visit(exp)
            End Function
    
            Private Function Evaluate(ByVal e As Expression) As Expression
                If e.NodeType = ExpressionType.Constant Then
                    Return e
                End If
    
                Dim lambda = Expression.Lambda(e)
                Dim fn As [Delegate] = lambda.Compile()
    
                Return Expression.Constant(fn.DynamicInvoke(Nothing), e.Type)
            End Function
        End Class
    
    
        ''' <summary>
        ''' Performs bottom-up analysis to determine which nodes can possibly
        ''' be part of an evaluated sub-tree.
        ''' </summary>
        Class Nominator
            Inherits ExpressionVisitor
    
            Private fnCanBeEvaluated As Func(Of Expression, Boolean)
            Private candidates As HashSet(Of Expression)
            Private cannotBeEvaluated As Boolean
    
            Friend Sub New(ByVal fnCanBeEvaluated As Func(Of Expression, Boolean))
                Me.fnCanBeEvaluated = fnCanBeEvaluated
            End Sub
    
            Friend Function Nominate(ByVal expr As Expression) As HashSet(Of Expression)
                Me.candidates = New HashSet(Of Expression)()
                Me.Visit(expr)
    
                Return Me.candidates
            End Function
    
            Public Overrides Function Visit(ByVal expr As Expression) As Expression
                If expr IsNot Nothing Then
    
                    Dim saveCannotBeEvaluated = Me.cannotBeEvaluated
                    Me.cannotBeEvaluated = False
    
                    MyBase.Visit(expr)
    
                    If Not Me.cannotBeEvaluated Then
                        If Me.fnCanBeEvaluated(expr) Then
                            Me.candidates.Add(expr)
                        Else
                            Me.cannotBeEvaluated = True
                        End If
                    End If
    
                    Me.cannotBeEvaluated = Me.cannotBeEvaluated Or 
                                           saveCannotBeEvaluated
                End If
    
                Return expr
            End Function
        End Class
    End Module
    
    using System;
    using System.Collections.Generic;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        public static class Evaluator
        {
            /// <summary>
            /// Performs evaluation & replacement of independent sub-trees
            /// </summary>
            /// <param name="expression">The root of the expression tree.</param>
            /// <param name="fnCanBeEvaluated">A function that decides whether a given expression node can be part of the local function.</param>
            /// <returns>A new tree with sub-trees evaluated and replaced.</returns>
            public static Expression PartialEval(Expression expression, Func<Expression, bool> fnCanBeEvaluated)
            {
                return new SubtreeEvaluator(new Nominator(fnCanBeEvaluated).Nominate(expression)).Eval(expression);
            }
    
            /// <summary>
            /// Performs evaluation & replacement of independent sub-trees
            /// </summary>
            /// <param name="expression">The root of the expression tree.</param>
            /// <returns>A new tree with sub-trees evaluated and replaced.</returns>
            public static Expression PartialEval(Expression expression)
            {
                return PartialEval(expression, Evaluator.CanBeEvaluatedLocally);
            }
    
            private static bool CanBeEvaluatedLocally(Expression expression)
            {
                return expression.NodeType != ExpressionType.Parameter;
            }
    
            /// <summary>
            /// Evaluates & replaces sub-trees when first candidate is reached (top-down)
            /// </summary>
            class SubtreeEvaluator : ExpressionVisitor
            {
                HashSet<Expression> candidates;
    
                internal SubtreeEvaluator(HashSet<Expression> candidates)
                {
                    this.candidates = candidates;
                }
    
                internal Expression Eval(Expression exp)
                {
                    return this.Visit(exp);
                }
    
                public override Expression Visit(Expression exp)
                {
                    if (exp == null)
                    {
                        return null;
                    }
                    if (this.candidates.Contains(exp))
                    {
                        return this.Evaluate(exp);
                    }
                    return base.Visit(exp);
                }
    
                private Expression Evaluate(Expression e)
                {
                    if (e.NodeType == ExpressionType.Constant)
                    {
                        return e;
                    }
                    LambdaExpression lambda = Expression.Lambda(e);
                    Delegate fn = lambda.Compile();
                    return Expression.Constant(fn.DynamicInvoke(null), e.Type);
                }
            }
    
            /// <summary>
            /// Performs bottom-up analysis to determine which nodes can possibly
            /// be part of an evaluated sub-tree.
            /// </summary>
            class Nominator : ExpressionVisitor
            {
                Func<Expression, bool> fnCanBeEvaluated;
                HashSet<Expression> candidates;
                bool cannotBeEvaluated;
    
                internal Nominator(Func<Expression, bool> fnCanBeEvaluated)
                {
                    this.fnCanBeEvaluated = fnCanBeEvaluated;
                }
    
                internal HashSet<Expression> Nominate(Expression expression)
                {
                    this.candidates = new HashSet<Expression>();
                    this.Visit(expression);
                    return this.candidates;
                }
    
                public override Expression Visit(Expression expression)
                {
                    if (expression != null)
                    {
                        bool saveCannotBeEvaluated = this.cannotBeEvaluated;
                        this.cannotBeEvaluated = false;
                        base.Visit(expression);
                        if (!this.cannotBeEvaluated)
                        {
                            if (this.fnCanBeEvaluated(expression))
                            {
                                this.candidates.Add(expression);
                            }
                            else
                            {
                                this.cannotBeEvaluated = true;
                            }
                        }
                        this.cannotBeEvaluated |= saveCannotBeEvaluated;
                    }
                    return expression;
                }
            }
        }
    }
    

Aggiunta delle classi di supporto

In questa sezione è contenuto il codice per tre classi di supporto del provider.

Per aggiungere la classe di supporto utilizzata dall'implementazione System.Linq.IQueryProvider

  • Aggiungere al progetto la classe TypeSystem o il modulo in Visual Basic.

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

    Questa classe di supporto viene utilizzata dall'implementazione IQueryProvider aggiunta precedentemente.

    TypeSystem.GetElementType utilizza la reflection per ottenere l'argomento di tipo generico di un insieme IEnumerable<T> (IEnumerable(Of T) in Visual Basic). Questo metodo viene chiamato dal metodo CreateQuery non generico nell'implementazione del provider della query per fornire il tipo di elemento dell'insieme di risultati della query.

    Questa classe di supporto non è specifica di questo provider del servizio Web TerraServer-USA. Pertanto, può essere riutilizzato per qualsiasi provider LINQ.

Per creare una classe di supporto della struttura ad albero dell'espressione

  • Aggiungere la classe ExpressionTreeHelpers al progetto.

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

    Questa classe contiene i metodi che possono essere utilizzati per determinarne le informazioni ed estrarre i dati dai tipi specifici di strutture ad albero dell'espressione. In questo provider tali metodi vengono utilizzati dalla classe LocationFinder per estrarre le informazioni sul percorso dalla struttura ad albero dell'espressione che rappresenta la query.

Per aggiungere un tipo di eccezione per le query non valide

  • Aggiungere la classe InvalidQueryException al progetto.

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

    Questa classe definisce un tipo Exception che il provider può generare quando non riconosce la query LINQ dal client. Definendo questo tipo di eccezione per le query non valide, il provider può generare un'eccezione più specifica rispetto a Exception da vari punti del codice.

A questo punto sono stati aggiunti tutti i componenti necessari per compilare il provider. Compilare il progetto LinqToTerraServerProvider e assicurarsi che non si verifichino errori di compilazione.

Test del provider LINQ

È possibile testare il provider LINQ creando un'applicazione client contenente una query LINQ sull'origine dati.

Per creare un'applicazione client per testare il provider

  1. Aggiungere un nuovo progetto Applicazione console alla soluzione e denominarlo ClientApp.

  2. Nel nuovo progetto aggiungere un riferimento all'assembly di provider.

  3. Trascinare il file app.config dal progetto del provider al progetto client. Questo file è necessario per comunicare con il servizio Web.

    Nota

    In Visual Basic è necessario fare clic sul pulsante Mostra tutti i file per visualizzare il file app.config in Esplora soluzioni.

  4. Aggiungere le istruzioni using seguenti (istruzioneImports in Visual Basic) al file Program.cs (o Module1.vb in Visual Basic):

    using System;
    using System.Linq;
    using LinqToTerraServerProvider;
    
    Imports LinqToTerraServerProvider
    
  5. Nel metodo Main del file Program.cs (o Module1.vb in Visual Basic) inserire il codice seguente:

    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
    

    Questo codice crea una nuova istanza del tipo IQueryable<T> definito nel provider e quindi esegue una query sull'oggetto utilizzando LINQ. La query specifica un percorso per ottenere i dati utilizzando un'espressione di uguaglianza. Poiché l'origine dati implementa IQueryable, il compilatore converte la sintassi di espressione della query in chiamate agli operatori di query standard definiti in Queryable. Internamente, questi metodi degli operatori di query standard compilano una struttura ad albero dell'espressione e chiamano i metodi Execute o CreateQuery implementati come parte dell'implementazione IQueryProvider.

  6. Compilare ClientApp.

  7. Impostare questa applicazione client come progetto di avvio per la soluzione. In Esplora soluzioni fare clic con il pulsante destro del mouse sul progetto ClientApp e scegliere Imposta come progetto di avvio.

  8. Eseguire il programma e visualizzare i risultati. Dovrebbero essere presenti approssimativamente tre risultati.

Aggiunta di funzionalità di query più complesse

Il provider disponibile a questo punto fornisce una modalità molto limitata per consentire ai client di specificare le informazioni sul percorso nella query LINQ. In particolare il provider è solo in grado di ottenere le informazioni sul percorso da espressioni di uguaglianza come Place.Name == "Seattle" o Place.State == "Alaska" (Place.Name = "Seattle" o Place.State = "Alaska" in Visual Basic).

Nella procedura successiva viene illustrato come aggiungere il supporto per una modalità aggiuntiva al fine di specificare le informazioni sul percorso. Dopo aver aggiunto questo codice, il provider sarà in grado di estrarre le informazioni sul percorso dalle espressioni della chiamata al metodo, ad esempio place.Name.StartsWith("Seat").

Per aggiungere il supporto per i predicati contenenti String.StartsWith

  1. Nel progetto LinqToTerraServerProvider, aggiungere il metodo VisitMethodCall alla definizione della classe LocationFinder.

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

  3. Per testare la nuova funzionalità del provider, aprire il file Program.cs (o Module1.vb in Visual Basic) nel progetto ClientApp. Sostituire il codice nel metodo Main con il codice seguente:

    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. Eseguire il programma e visualizzare i risultati. Dovrebbero essere presenti approssimativamente 29 risultati.

Nella procedura successiva viene illustrato come aggiungere la funzionalità al provider per consentire alla query client di specificare le informazioni sul percorso utilizzando due metodi aggiuntivi, in particolare Enumerable.Contains e List<T>.Contains. Dopo aver aggiunto questo codice, il provider sarà in grado di estrarre le informazioni sul percorso dalle espressioni della chiamata al metodo nella query client, ad esempio placeList.Contains(place.Name), dove l'insieme placeList è un elenco concreto fornito dal client. Il vantaggio di consentire ai client di utilizzare il metodo Contains è che possono specificare un numero qualsiasi di percorsi semplicemente aggiungendoli a placeList. Variando il numero di percorsi non si modifica la sintassi della query.

Per aggiungere il supporto per le query contenenti il metodo Contains nella relativa clausola 'where'

  1. Nella definizione della classe LocationFinder del progetto LinqToTerraServerProvider sostituire il metodo VisitMethodCall con il codice seguente:

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

    Questo metodo aggiunge ogni stringa dell'insieme, al quale viene applicato Contains, all'elenco di percorsi in base a cui eseguire una query sul servizio Web. Un metodo denominato Contains viene definito in Enumerable e List<T>. Pertanto, il metodo VisitMethodCall deve controllare entrambi questi tipi dichiaranti. Enumerable.Contains viene definito come metodo di estensione e pertanto l'insieme al quale è applicato è effettivamente il primo argomento per il metodo. List.Contains viene definito come metodo di istanza e pertanto l'insieme al quale è applicato è l'oggetto ricevente del metodo.

  2. Ricompilare il progetto LinqToTerraServerProvider.

  3. Per testare la nuova funzionalità del provider, aprire il file Program.cs (o Module1.vb in Visual Basic) nel progetto ClientApp. Sostituire il codice nel metodo Main con il codice seguente:

    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. Eseguire il programma e visualizzare i risultati. Dovrebbero essere presenti approssimativamente 5 risultati.

Passaggi successivi

In questo argomento della procedura dettagliata viene illustrato come creare un provider LINQ per un unico metodo di un servizio Web. Se si desidera procedere con lo sviluppo aggiuntivo di un provider LINQ, considerare queste possibilità:

  • Consentire al provider LINQ di gestire le altre modalità per specificare un percorso nella query client.

  • Esaminare gli altri metodi esposti dal servizio Web TerraServer-USA e creare un provider LINQ che si interfacci con uno di tali metodi.

  • Cercare un servizio Web diverso desiderato e crearvi un provider LINQ.

  • Creare un provider LINQ per un'origine dati diversa da un servizio Web.

Per ulteriori informazioni sulla creazione del provider LINQ, vedere LINQ: Building an IQueryable Provider (informazioni in lingua inglese) nei blog di MSDN.

Vedere anche

Attività

Esempi LINQ

Procedura: modificare strutture ad albero dell'espressione (C# e Visual Basic)

Riferimenti

IQueryable<T>

IOrderedQueryable<T>

Concetti

Abilitazione di un'origine dati per l'esecuzione di query LINQ

Servizi Windows Communication Foundation e dati WCF in Visual Studio