Procédure pas à pas : création d'un fournisseur LINQ IQueryable

Cette rubrique avancée fournit des instructions pas à pas pour créer un fournisseur LINQ personnalisé. Une fois que vous avez terminé, vous pouvez utiliser le fournisseur créé pour écrire des requêtes LINQ au service Web TerraServer-USA.

Le service Web TerraServer-USA fournit une interface à une base de données d'images aériennes des États-Unis. Il expose également une méthode qui retourne des informations sur des lieux situés aux États-Unis, à partir d'une partie ou de la totalité d'un nom de lieu. Cette méthode, appelée GetPlaceList, est la méthode qui est appelée par votre fournisseur LINQ. Le fournisseur utilise Windows Communication Foundation (WCF) pour communiquer avec le service Web. Pour plus d'informations sur le service Web TerraServer-USA, consultez Overview of the TerraServer-USA Web Services.

Ce fournisseur est un fournisseur IQueryable relativement simple. Il attend des informations spécifiques dans les requêtes qu'il gère et il a un système de type fermé, exposant un type unique pour représenter les données de résultat. Ce fournisseur examine un seul type d'expression d'appel de méthode dans l'arborescence de l'expression qui représente la requête, qui est l'appel le plus profond à Where. Il extrait les données dont il a besoin pour interroger le service Web à partir de cette expression. Il appelle alors le service Web et insère les données retournées dans l'arborescence de l'expression à l'emplacement de la source de données IQueryable initiale. Le reste de l'exécution de la requête est contrôlé par les implémentations Enumerable des opérateurs de requête standard.

Les exemples de code fournis dans cette rubrique sont fournis en langage C# et Visual Basic.

Cette procédure pas à pas décrit les tâches suivantes :

  • Création du projet dans Visual Studio.

  • Implémentation des interfaces requises pour un fournisseur IQueryableLINQ :IQueryable<T>,IOrderedQueryable<T> et IQueryProvider.

  • Ajout d'un type .NET personnalisé pour représenter les données du service Web.

  • Création d'une classe de contexte de requêtes et d'une classe qui obtient des données du service Web.

  • Création d'une sous-classe de visiteur d'arborescence d'expression qui recherche l'expression représentant l'appel le plus profond à la méthode Queryable.Where.

  • Création d'une sous-classe de visiteur d'arborescence d'expression qui extrait des informations de la requête LINQ à utiliser dans la requête de service Web.

  • Création d'une sous-classe de visiteur d'arborescence d'expression qui modifie l'arborescence de l'expression représentant la requête LINQ complète.

  • Utilisation d'une classe d'évaluateur pour évaluer partiellement une arborescence d'expression. Cette étape est nécessaire, car elle traduit toutes les références de variable locale contenues dans la requête LINQ en valeurs.

  • Création d'une classe d'assistance d'arborescence d'expression et d'une nouvelle classe d'exception.

  • Test du fournisseur LINQ à partir d'une application cliente qui contient une requête LINQ.

  • Ajout de fonctionnalités de requête plus complexes au fournisseur LINQ.

    Notes

    Le fournisseur LINQ que cette procédure pas à pas crée est disponible sous la forme d'un exemple. Pour plus d'informations, consultez Exemples LINQ.

Composants requis

Pour exécuter cette procédure pas à pas, vous devez disposer des composants suivants :

  • Visual Studio 2008

Notes

Il est possible que votre ordinateur affiche des noms ou des emplacements différents pour certains des éléments d'interface utilisateur de Visual Studio dans les instructions suivantes. L'édition de Visual Studio dont vous disposez et les paramètres que vous utilisez déterminent ces éléments. Pour plus d'informations, consultez Paramètres Visual Studio.

Création du projet

Pour créer le projet dans Visual Studio

  1. DansVisual Studio, créez une application Bibliothèque de classes. Nommez le projet LinqToTerraServerProvider.

  2. Dans l'Explorateur de solutions, sélectionnez le fichier Class1.cs (ou Class1.vb) ou renommez-le en QueryableTerraServerData.cs (ou QueryableTerraServerData.vb). Dans la boîte de dialogue qui apparaît, cliquez sur Oui pour renommer toutes les références à l'élément de code.

    Vous créez le fournisseur comme un projet Bibliothèque de classes dans Visual Studio, car les applications client exécutables ajouteront l'assembly fournisseur en tant que référence à leur projet.

Pour ajouter une référence de service au service Web

  1. Dans l'Explorateur de solutions, cliquez avec le bouton droit sur le projet LinqToTerraServerProvider, puis cliquez sur Ajouter une référence de service.

    La boîte de dialogue Ajouter une référence de service s'ouvre.

  2. Dans la zone Adresse, tapez http://terraserver.microsoft.com/TerraService2.asmx.

  3. Dans la zone Espace de noms, tapez TerraServerReference, puis cliquez sur OK.

    Le service Web de TerraServer-USA est ajouté comme une référence de service afin que l'application puisse communiquer avec le service Web par le biais de Windows Communication Foundation (WCF). En ajoutant une référence de service au projet, Visual Studio génère un fichier app.config qui contient un proxy et un point de terminaison pour le service Web. Pour plus d'informations, consultez Services Windows Communication Foundation et services de données WCF dans Visual Studio.

Vous avez maintenant un projet qui comporte un fichier nommé app.config, un fichier nommé QueryableTerraServerData.cs (ou QueryableTerraServerData.vb) et une référence de service nommée TerraServerReference.

Implémentation des interfaces nécessaires

Pour créer un fournisseur LINQ, vous devez, au minimum, implémenter les interfaces IQueryable<T> et IQueryProvider. IQueryable<T> et IQueryProvider sont dérivés des autres interfaces requises ; par conséquent, en implémentant ces deux interfaces, vous implémentez également les autres interfaces requises pour un fournisseur LINQ.

Si vous souhaitez prendre en charge le tri des opérateurs de requête tels que OrderBy et ThenBy, vous devez également implémenter l'interface IOrderedQueryable<T>. IOrderedQueryable<T> dérivant de IQueryable<T>, vous pouvez implémenter ces deux interfaces en un type unique, ce que fait ce fournisseur.

Pour implémenter System.Linq.IQueryable'1 et System.Linq.IOrderedQueryable'1

  • Dans le fichier QueryableTerraServerData.cs (ou QueryableTerraServerData.vb), ajoutez le code suivant.

    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'implémentation IOrderedQueryable<T> par la classe QueryableTerraServerData implémente trois propriétés déclarées dans IQueryable et deux méthodes d'énumération déclarées dans IEnumerable et IEnumerable<T>.

    Cette classe d'attributs possède deux constructeurs. Le premier constructeur est appelé à partir de l'application cliente pour créer l'objet sur lequel écrire la requête LINQ. Le second constructeur est appelé interne à la bibliothèque de fournisseur par le code dans l'implémentation IQueryProvider.

    Lorsque la méthode GetEnumerator est appelée sur un objet de type QueryableTerraServerData, la requête qu'il représente est exécutée et les résultats de la requête sont énumérés.

    Ce code, à l'exception du nom de la classe, n'est pas spécifique à ce fournisseur de services Web TerraServer-USA. Par conséquent, il peut être réutilisé pour tout fournisseur LINQ.

Pour implémenter System.Linq.IQueryProvider

  • Ajoutez la classe TerraServerQueryProvider à votre projet.

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

    Le code du fournisseur de requêtes dans cette classe implémente les quatre méthodes requises pour implémenter l'interface IQueryProvider. Les deux méthodes CreateQuery créent des requêtes associées à la source de données. Les deux méthodes Execute envoient ces requêtes pour exécution.

    La méthode CreateQuery non générique utilise la réflexion pour obtenir le type d'élément de la séquence que la requête qu'elle crée retournerait si elle était exécutée. Elle utilise alors la classe Activator pour générer une nouvelle instance QueryableTerraServerData générée avec le type d'élément comme son argument de type générique. Le résultat de l'appel de la méthode CreateQuery non générique est le même que si la méthode CreateQuery générique avait été appelée avec l'argument de type correct.

    La majeure partie de la logique d'exécution de la requête est contrôlée dans une classe différente que vous ajouterez ultérieurement. Cette fonctionnalité est contrôlée ailleurs car elle est spécifique à la source de données qui est interrogée, alors que le code dans cette classe est générique à tout fournisseur LINQ. Pour utiliser ce code pour un fournisseur différent, vous devrez éventuellement modifier le nom de la classe et le nom du type du contexte de la requête référencé dans deux de ces méthodes.

Ajout d'un type personnalisé pour représenter les données de résultat

Vous aurez besoin d'un type .NET pour représenter les données obtenues à partir du service Web. Ce type sera utilisé dans la requête LINQ cliente pour définir les résultats voulus. La procédure suivante crée ce type. Ce type nommé Place contient des informations sur un emplacement géographique unique, tel qu'une ville, un parc ou un lac.

Ce code contient également un type énumération, nommé PlaceType, qui définit les divers types d'emplacement géographique et est utilisé dans la classe Place.

Pour créer un type de résultat personnalisé

  • Ajoutez la classe Place et l'énumération PlaceType à votre projet.

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

    Le constructeur pour le type Place simplifie la création d'un objet de résultat à partir du type retourné par le service Web. Alors que le fournisseur peut retourner directement le type de résultat défini par l'API de service Web, il aurait besoin des applications clientes pour ajouter une référence au service Web. En créant un nouveau type dans le cadre de la bibliothèque fournisseur, le client ne doit pas nécessairement connaître les types et les méthodes exposés par le service Web.

Ajout des fonctionnalités requises pour obtenir des données de la source de données

Cette implémentation de fournisseur suppose que l'appel le plus profond à Queryable.Where contient les informations d'emplacement à utiliser pour interroger le service Web. L'appel Queryable.Where le plus profond est la clause where (clause Where en Visual Basic) ou l'appel de méthode Queryable.Where qui se produit en premier dans une requête LINQ, ou celui le plus proche du "bas" de l'arborescence de l'expression qui représente la requête.

Pour créer une classe de contexte de requête

  • Ajoutez la classe TerraServerQueryContext à votre projet.

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

    Cette classe organise la tâche d'exécution d'une requête. Après avoir trouvé l'expression qui représente l'appel Queryable.Where le plus profond, ce code extrait l'expression lambda qui représente le prédicat qui a été passé à Queryable.Where. Il passe alors l'expression de prédicat à une méthode pour son évaluation partielle, afin que toutes les références aux variables locales soient traduites en valeurs. Il appelle ensuite une méthode pour extraire du prédicat les emplacements demandés et appelle une autre méthode pour obtenir les données de résultat à partir du service Web.

    Dans l'étape suivante, ce code copie l'arborescence de l'expression qui représente la requête LINQ et apporte une modification à l'arborescence de l'expression. Le code utilise une sous-classe de visiteur d'arborescence d'expression pour remplacer la source de données à laquelle est appliqué l'appel d'opérateur de requête le plus profond par la liste concrète des objets Place obtenus à partir du service Web.

    Avant l'insertion de la liste d'objets Place dans l'arborescence de l'expression, son type est modifié de IEnumerable en IQueryable par l'appel de AsQueryable. Cette modification de type est nécessaire, car l'arborescence de l'expression est réécrite, le nœud qui représente l'appel de méthode à la méthode de l'opérateur de la requête la plus profonde est régénéré. Ce nœud est recréé, car l'un de ces arguments a été modifié (c'est-à-dire, la source de données à laquelle il est appliqué). La méthode Call(Expression, MethodInfo, IEnumerable<Expression>), utilisée pour régénérer le nœud, lèvera une exception si un argument n'est pas assignable au paramètre correspondant de la méthode à laquelle il sera passé. Dans ce cas, la liste IEnumerable d'objets Place ne serait pas assignable au paramètre IQueryable de Queryable.Where. Par conséquent, son type est modifié en IQueryable.

    En modifiant son type en IQueryable, la collection obtient également un membre IQueryProvider, auquel accède la propriété Provider, qui peut créer ou exécuter des requêtes. Le type dynamique de la collection  IQueryable Place est EnumerableQuery, qui est un type interne à l'API System.Linq. Le fournisseur de requêtes associé à ce type exécute des requêtes en remplaçant des appels d'opérateur de requête standard Queryable par les opérateurs Enumerable équivalents, de sorte que la requête devient effectivement une requête LINQ to Objects.

    Le dernier code de la classe TerraServerQueryContext appelle l'une de ces deux méthodes sur la liste IQueryable des objets Place. Il appelle CreateQuery si la requête cliente retourne des résultats enumérables ou Execute si la requête cliente retourne un résultat non enumérable.

    Le code de cette classe est très spécifique à ce fournisseur TerraServer-USA. Il est donc encapsulé dans la classe TerraServerQueryContext au lieu d'être inséré directement dans l'implémentation IQueryProvider plus générique.

Le fournisseur que vous créez requiert uniquement les informations contenues dans le prédicat Queryable.Where pour interroger le service Web. Il utilise donc LINQ to Objects pour l'exécution de la requête LINQ à l'aide du type EnumerableQuery interne. Une autre manière d'utiliser LINQ to Objects pour exécuter la requête consiste à donner au client l'instruction d'encapsuler la partie de la requête à exécuter par LINQ to Objects dans une requête LINQ to Objects. Pour ce faire, vous devez appeler AsEnumerable<TSource> sur le reste de la requête, qui est la partie de la requête dont le fournisseur a besoin pour ces objectifs spécifiques. L'avantage de ce type d'implémentation est que la division du travail entre le fournisseur personnalisé et LINQ to Objects est plus transparente.

Notes

Le fournisseur présenté dans cette rubrique est un fournisseur simple qui comporte en lui-même une prise en charge des requêtes minimale. Par conséquent, il dépend pour une grande part de LINQ to Objects pour l'exécution des requêtes. Un fournisseur LINQ complexe tel que LINQ to SQL peut prendre en charge la requête entière sans recourir à LINQ to Objects.

Pour créer une classe pour obtenir des données du service Web

  • Ajoutez la classe WebServiceHelper (ou module en Visual Basic) à votre projet.

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

    Cette classe contient les fonctionnalités qui obtiennent des données du service Web. Ce code utilise un type nommé TerraServiceSoapClient, qui est généré automatiquement pour le projet par Windows Communication Foundation (WCF), pour appeler la méthode de service Web GetPlaceList. Puis, chaque résultat est traduit du type de retour de la méthode de service Web Service en type .NET que le fournisseur définit pour les données.

    Ce code contient deux contrôles qui améliorent la facilité d'utilisation de la bibliothèque du fournisseur. Le premier contrôle limite le délai maximum durant lequel une application cliente attendra une réponse en limitant à cinq le nombre total des appels faits au service Web, par requête. Pour chaque emplacement spécifié dans la requête cliente, une requête de service Web est générée. Le fournisseur lève donc une exception si la requête contient plus de cinq emplacements.

    Le second contrôle détermine si le nombre de résultats retournés par le service Web est égal au nombre maximal des résultats qu'il peut retourner. Si le nombre de résultats est le nombre maximal, les résultats du service Web risquent d'être tronqués. Au lieu de retourner une liste incomplète au client, le fournisseur lève une exception.

Ajout des classes de visiteur d'arborescence d'expression

Pour créer le visiteur qui recherche l'expression d'appel de méthode Where la plus profonde

  1. Ajoutez à votre projet la classe InnermostWhereFinder, qui hérite de 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;
            }
        }
    }
    

    Cette classe hérite de la classe de visiteur d'arborescence d'expression de base pour exécuter les fonctionnalités de recherche d'une expression spécifique. La classe de visiteur d'arborescence d'expression de base est conçue pour être héritée et spécialisée pour une tâche spécifique qui implique le parcours de l'arborescence de l'expression. La classe dérivée substitue la méthode VisitMethodCall pour rechercher l'expression qui représente l'appel le plus profond à Where dans l'arborescence de l'expression qui représente la requête cliente. Cette expression la plus profonde est l'expression à partir de laquelle le fournisseur extrait les emplacements de recherche.

  2. Ajoutez des directives using (instructions Imports en Visual Basic) au fichier pour les espaces de noms suivants : System.Collections.Generic, System.Collections.ObjectModel et System.Linq.Expressions.

Pour créer le visiteur qui extrait des données pour interroger le service Web

  • Ajoutez la classe LocationFinder à votre projet.

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

    Cette classe est utilisée pour extraire des informations d'emplacement à partir du prédicat que le client passe à Queryable.Where. Elle dérive de la classe ExpressionVisitor et se substitue uniquement à la méthode VisitBinary.

    La classe ExpressionVisitor envoie des expressions binaires, telles que des expressions d'égalité place.Name == "Seattle" (place.Name = "Seattle" en Visual Basic), à la méthode VisitBinary. Dans cette méthode VisitBinary prioritaire, si l'expression correspond au modèle d'expression d'égalité qui peut fournir des informations d'emplacement, cette information est extraite et stockée dans une liste d'emplacements.

    Cette classe utilise un visiteur d'arborescence d'expression pour rechercher les informations d'emplacement dans l'arborescence de l'expression, car un visiteur est conçu pour parcourir et examiner des arborescences d'expression. Le code résultant est plus net et moins sujet à erreur que s'il avait été implémenté sans utiliser le visiteur.

    À ce stade de la procédure pas à pas, votre fournisseur prend en charge uniquement des manières limitées de fournir des informations d'emplacement dans la requête. Ultérieurement dans la rubrique, vous ajouterez des fonctionnalités offrant des manières supplémentaires de fournir des informations d'emplacement.

Pour créer le visiteur qui modifie l'arborescence de l'expression

  • Ajoutez la classe ExpressionTreeModifier à votre projet.

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

    Cette classe dérive de la classe ExpressionVisitor et se substitue à la méthode VisitConstant. Dans cette méthode, elle remplace l'objet auquel est appliqué l'appel d'opérateur de requête standard le plus profond avec une liste concrète d'objets Place.

    Cette classe de modificateur de l'arborescence de l'expression utilise le visiteur d'arborescence d'expression, car le visiteur est conçu pour parcourir, examiner et copier des arborescences d'expression. Dérivant de la classe de visiteur d'arborescence d'expression de base, cette classe requiert un code minimal pour exécuter cette fonction.

Ajout de l'évaluateur d'expression

Le prédicat passé à la méthode Queryable.Where dans la requête cliente peut contenir des sous-expressions qui ne dépendent pas du paramètre de l'expression lambda. Ces sous-expressions isolées peuvent et doivent être évaluées immédiatement. Il peut s'agir de références à des variables locales ou des variables membres qui doivent être traduites en valeurs.

La classe suivante expose une méthode, PartialEval(Expression), qui détermine parmi les sous-arbres éventuels de l'expression celui qui peut être évalué immédiatement. Elle évalue ensuite ces expressions en créant une expression lambda, en la compilant et en appelant le délégué retourné. Enfin, elle remplace le sous-arbre par un nouveau nœud qui représente une valeur de constante. Cette opération est appelée "évaluation partielle".

Pour ajouter une classe pour exécuter l'évaluation partielle d'une arborescence d'expression

  • Ajoutez la classe Evaluator à votre projet.

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

Ajout des classes d'assistance

Cette section contient le code pour trois classes d'assistance pour votre fournisseur.

Pour ajouter la classe d'assistance utilisée par l'implémentation System.Linq.IQueryProvider

  • Ajoutez la classe TypeSystem (ou module en Visual Basic) à votre projet.

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

    L'implémentation IQueryProvider que vous avez ajoutée précédemment utilise cette classe d'assistance.

    TypeSystem.GetElementType utilise la réflexion pour obtenir l'argument de type générique d'une collection IEnumerable<T> (IEnumerable(Of T) en Visual Basic). Cette méthode est appelée à partir de la méthode CreateQuery non générique dans l'implémentation du fournisseur de la requête pour fournir le type d'élément de la collection de résultats de la requête.

    Cette classe d'assistance n'est pas spécifique à ce fournisseur de services Web TerraServer-USA. Par conséquent, il peut être réutilisé pour tout fournisseur LINQ.

Pour créer une classe d'assistance d'arborescence d'expression

  • Ajoutez la classe ExpressionTreeHelpers à votre projet.

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

    Cette classe contient des méthodes qui peuvent être utilisées pour déterminer des informations et extraire des données à partir de types spécifiques d'arborescences d'expression. Dans ce fournisseur, ces méthodes sont utilisées par la classe LocationFinder pour extraire des informations d'emplacement à partir de l'arborescence de l'expression qui représente la requête.

Pour ajouter un type d'exception pour les requêtes non valides

  • Ajoutez la classe InvalidQueryException à votre projet.

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

    Cette classe définit un type Exception que votre fournisseur peut lever lorsqu'il ne comprend pas la requête LINQ du client. En définissant ce type d'exception de la requête non valide, le fournisseur peut lever une exception plus spécifique que seulement Exception à partir de différents emplacements dans le code.

Vous avez maintenant ajouté tous les composants requis pour compiler votre fournisseur. Générez le projet LinqToTerraServerProvider et vérifiez qu'il n'y a pas d'erreurs de compilation.

Test du fournisseur LINQ

Vous pouvez tester votre fournisseur LINQ en créant une application cliente qui contient une requête LINQ sur votre source de données.

Pour créer une application cliente pour tester votre fournisseur

  1. Ajoutez un nouveau projet Application console à votre solution et nommez-le ClientApp.

  2. Dans le nouveau projet, ajoutez une référence à l'assembly fournisseur.

  3. Faites glisser le fichier app.config de votre projet fournisseur vers le projet client. (Ce fichier est nécessaire pour communiquer avec le service Web.)

    Notes

    En Visual Basic, vous devrez peut-être cliquer sur le bouton Afficher tous les fichiers pour consulter le fichier app.config dans l'Explorateur de solutions.

  4. Ajoutez les instructions using suivantes (instructionImports en Visual Basic) au fichier Program.cs(ou Module1.vb en Visual Basic) :

    using System;
    using System.Linq;
    using LinqToTerraServerProvider;
    
    Imports LinqToTerraServerProvider
    
  5. Dans la méthode Main dans le fichier Program.cs (ou Module1.vb en Visual Basic), insérez le code suivant :

    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
    

    Ce code crée une nouvelle instance du type IQueryable<T> que vous avez défini dans votre fournisseur, puis interroge cet objet à l'aide de LINQ. La requête spécifie un emplacement sur lequel obtenir des données à l'aide d'une expression d'égalité. La source de données implémentant IQueryable, le compilateur traduit la syntaxe d'expression de requête en appels aux opérateurs de requête standard définis dans Queryable. En interne, ces méthodes d'opérateur de requête standard génèrent une arborescence d'expression et appellent les méthodes Execute ou CreateQuery que vous avez implémentées dans le cadre de votre implémentation IQueryProvider.

  6. Générez ClientApp.

  7. Définissez cette application cliente comme projet de "démarrage" pour votre solution. Dans l'Explorateur de solutions, cliquez avec le bouton droit sur le projet ClientApp, puis sélectionnez Définir comme projet de démarrage.

  8. Exécutez le programme et consultez les résultats. Il doit y avoir environ trois résultats.

Ajout de fonctions de requête plus complexes

Le fournisseur dont vous disposez à ce stade fournit une manière très limitée pour les clients de spécifier des informations vous avez à ce point offre un moyen très limité aux clients de spécifier des informations d'emplacement dans la requête LINQ. Plus précisément, le fournisseur peut seulement obtenir des informations d'emplacement à partir d'expressions d'égalité telles que Place.Name == "Seattle" ou Place.State == "Alaska" (Place.Name = "Seattle" ou Place.State = "Alaska" en Visual Basic).

La procédure suivante vous indique comment ajouter la prise en charge d'une manière supplémentaire de spécifier des informations d'emplacement. Une fois que vous avez ajouté ce code, votre fournisseur peut extraire des informations d'emplacement à partir d'expressions d'appel de méthode telles que place.Name.StartsWith("Seat").

Pour ajouter la prise en charge pour les prédicats qui contiennent String.StartsWith

  1. Dans le projet LinqToTerraServerProvider, ajoutez la méthode VisitMethodCall à la définition de 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. Recompilez le projet LinqToTerraServerProvider.

  3. Pour tester la nouvelle fonction de votre fournisseur, ouvrez le fichier Program.cs (ou Module1.vb en Visual Basic) dans le projet ClientApp. Remplacez le code dans la méthode Main par le code suivant :

    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. Exécutez le programme et consultez les résultats. Vous devez obtenir environ 29 résultats.

La procédure suivante vous indique comment ajouter à votre fournisseur les fonctionnalités requises pour permettre à la requête cliente de spécifier des informations d'emplacement à l'aide de deux méthodes supplémentaires, précisément Enumerable.Contains et List<T>.Contains. Une fois que vous avez ajouté ce code, votre fournisseur peut extraire des informations d'emplacement à partir d'expressions d'appel de méthode dans la requête cliente telles que placeList.Contains(place.Name), où la collection placeList est une liste concrète fournie par le client. L'avantage de permettre aux clients d'utiliser la méthode Contains est qu'ils peuvent spécifier le nombre voulu d'emplacements simplement en les ajoutant à placeList. Varier le nombre d'emplacements ne modifie pas la syntaxe de la requête.

Pour ajouter la prise en charge pour les requêtes dont la clause "where" comporte la méthode Contain

  1. Dans le projet LinqToTerraServerProvider, dans la définition de classe LocationFinder, remplacez la méthode VisitMethodCall par le code suivant :

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

    Cette méthode ajoute chaque chaîne de la collection à laquelle Contains est appliqué, à la liste d'emplacements sur laquelle interroger le service Web. Une méthode nommée Contains est définie à la fois dans Enumerable et List<T>. Par conséquent, la méthode VisitMethodCall doit rechercher ces deux types déclarants. La méthode Enumerable.Contains est définie comme une méthode d'extension ; par conséquent, la collection à laquelle elle s'applique est en fait le premier argument passé à la méthode. La méthode List.Contains est définie comme une méthode d'instance ; par conséquent, la collection à laquelle elle s'applique est l'objet de réception de la méthode.

  2. Recompilez le projet LinqToTerraServerProvider.

  3. Pour tester la nouvelle fonction de votre fournisseur, ouvrez le fichier Program.cs (ou Module1.vb en Visual Basic) dans le projet ClientApp. Remplacez le code dans la méthode Main par le code suivant :

    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. Exécutez le programme et consultez les résultats. Il doit y avoir environ 5 résultats.

Étapes suivantes

Cette rubrique sous forme de procédure pas à pas vous a indiqué comment créer un fournisseur LINQ pour une méthode d'un service Web. Si vous souhaitez poursuivre le développement d'un fournisseur LINQ, vous avez le choix entre plusieurs options :

  • Autorisez le fournisseur LINQ à gérer d'autres façons de spécifier un emplacement dans la requête client.

  • Étudiez les autres méthodes exposées par le service Web TerraServer-USA et créez un fournisseur LINQ qui interagit avec l'une de ces méthodes.

  • Recherchez un autre service Web qui vous intéresse et créez un fournisseur LINQ pour ce service.

  • Créez un fournisseur LINQ pour une source de données autre qu'un service Web.

Pour plus d'informations sur la création de votre propre fournisseur LINQ, consultez LINQ : création d'un fournisseur IQueryable (page éventuellement en anglais) sur les blogs MSDN.

Voir aussi

Tâches

Exemples LINQ

Comment : modifier des arborescences d'expression (C# et Visual Basic)

Référence

IQueryable<T>

IOrderedQueryable<T>

Concepts

Activation d'une source de données pour l'interrogation LINQ

Services Windows Communication Foundation et services de données WCF dans Visual Studio