Procédure pas à pas : création d'un fournisseur LINQ IQueryable
Mise à jour : Juillet 2008
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, dénommé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 illustre 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 demande 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.
Remarque : Le fournisseur LINQ que cette procédure pas à pas crée est disponible sous la forme d'un exemple. Consultez Fournisseur LINQ to TerraServer, exemple pour plus d'informations.
Composants requis
Vous avez besoin des composants suivants pour exécuter cette procédure pas à pas :
- Visual Studio 2008
Remarque : |
---|
Il est possible que votre ordinateur affiche des noms ou des emplacements différents pour certains des éléments d'interface utilisateur 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
Dans Visual Studio, créez une nouvelle application Bibliothèque de classes. Nommez le projet LinqToTerraServerProvider.
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
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.
Dans la zone Adresse, tapez http://terraserver.microsoft.com/TerraService2.asmx.
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 Introduction aux services Windows Communication Foundation 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. Private _Name As String Private _State As String Private _PlaceType As PlaceType Public Property Name() As String Get Return _Name End Get Private Set(ByVal value As String) _Name = value End Set End Property Public Property State() As String Get Return _State End Get Private Set(ByVal value As String) _State = value End Set End Property Public Property PlaceType() As PlaceType Get Return _PlaceType End Get Private Set(ByVal value As PlaceType) _PlaceType = value End Set End Property ' Constructor. Friend Sub New(ByVal name As String, _ ByVal state As String, _ ByVal placeType As TerraServerReference.PlaceType) Me.Name = name Me.State = state Me.PlaceType = CType(placeType, PlaceType) End Sub End Class Public Enum PlaceType Unknown AirRailStation BayGulf CapePeninsula CityTown HillMountain Island Lake OtherLandFeature OtherWaterFeature ParkBeach PointOfInterest River End Enum
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace LinqToTerraServerProvider { public class Place { // Properties. public string Name { get; private set; } public string State { get; private set; } public PlaceType PlaceType { get; private set; } // Constructor. internal Place(string name, string state, LinqToTerraServerProvider.TerraServerReference.PlaceType placeType) { Name = name; State = state; PlaceType = (PlaceType)placeType; } } public enum PlaceType { Unknown, AirRailStation, BayGulf, CapePeninsula, CityTown, HillMountain, Island, Lake, OtherLandFeature, OtherWaterFeature, ParkBeach, PointOfInterest, River } }
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.CopyAndModify(expr) ' This step creates an IQueryable that executes by replacing ' Queryable methods with Enumerable methods. If (IsEnumerable) Then Return queryablePlaces.Provider.CreateQuery(newExpressionTree) Else Return queryablePlaces.Provider.Execute(newExpressionTree) End If End Function Private Shared Function IsQueryOverDataSource(ByVal expression As Expression) As Boolean ' If expression represents an unqueried IQueryable data source instance, ' expression is of type ConstantExpression, not MethodCallExpression. Return (TypeOf expression Is MethodCallExpression) End Function End Class
using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; namespace LinqToTerraServerProvider { class TerraServerQueryContext { // Executes the expression tree that is passed to it. internal static object Execute(Expression expression, bool IsEnumerable) { // The expression must represent a query over the data source. if (!IsQueryOverDataSource(expression)) throw new InvalidProgramException("No query over the data source was specified."); // Find the call to Where() and get the lambda expression predicate. InnermostWhereFinder whereFinder = new InnermostWhereFinder(); MethodCallExpression whereExpression = whereFinder.GetInnermostWhere(expression); LambdaExpression lambdaExpression = (LambdaExpression)((UnaryExpression)(whereExpression.Arguments[1])).Operand; // Send the lambda expression through the partial evaluator. lambdaExpression = (LambdaExpression)Evaluator.PartialEval(lambdaExpression); // Get the place name(s) to query the Web service with. LocationFinder lf = new LocationFinder(lambdaExpression.Body); List<string> locations = lf.Locations; if (locations.Count == 0) throw new InvalidQueryException("You must specify at least one place name in your query."); // Call the Web service and get the results. Place[] places = WebServiceHelper.GetPlacesFromTerraServer(locations); // Copy the IEnumerable places to an IQueryable. IQueryable<Place> queryablePlaces = places.AsQueryable<Place>(); // Copy the expression tree that was passed in, changing only the first // argument of the innermost MethodCallExpression. ExpressionTreeModifier treeCopier = new ExpressionTreeModifier(queryablePlaces); Expression newExpressionTree = treeCopier.CopyAndModify(expression); // This step creates an IQueryable that executes by replacing Queryable methods with Enumerable methods. if (IsEnumerable) return queryablePlaces.Provider.CreateQuery(newExpressionTree); else return queryablePlaces.Provider.Execute(newExpressionTree); } private static bool IsQueryOverDataSource(Expression expression) { // If expression represents an unqueried IQueryable data source instance, // expression is of type ConstantExpression, not MethodCallExpression. return (expression is MethodCallExpression); } } }
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.
Remarque : |
---|
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 demande 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
Ajoutez la classe ExpressionVisitor à votre projet. Ce code est disponible dans Comment : implémenter un visiteur de l'arborescence de l'expression. Ajoutez des directives using (ou des instructions Imports en Visual Basic) au fichier pour les espaces de noms suivants : System.Collections.Generic, System.Collections.ObjectModel et System.Linq.Expressions.
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.
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 est dérivée de la classe de visiteur d'arborescence d'expression de base et substitue uniquement la méthode VisitBinary.
La classe de base du visiteur d'arborescence d'expression envoie des expressions binaires, telles que les expressions d'égalité comme 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 Friend Function CopyAndModify(ByVal expression As Expression) As Expression Return Me.Visit(expression) End Function Protected Overrides Function VisitConstant(ByVal c As ConstantExpression) As Expression ' Replace the constant QueryableTerraServerData arg with the queryable Place collection. If c.Type Is GetType(QueryableTerraServerData(Of Place)) Then Return Expression.Constant(Me.queryablePlaces) Else Return c End If End Function End Class
using System; using System.Linq; using System.Linq.Expressions; namespace LinqToTerraServerProvider { internal class ExpressionTreeModifier : ExpressionVisitor { private IQueryable<Place> queryablePlaces; internal ExpressionTreeModifier(IQueryable<Place> places) { this.queryablePlaces = places; } internal Expression CopyAndModify(Expression expression) { return this.Visit(expression); } protected override Expression VisitConstant(ConstantExpression c) { // Replace the constant QueryableTerraServerData arg with the queryable Place collection. if (c.Type == typeof(QueryableTerraServerData<Place>)) return Expression.Constant(this.queryablePlaces); else return c; } } }
Cette classe dérive de la classe de visiteur d'arborescence d'expression de base et 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.
La méthode CopyAndModify appelle l'implémentation de la classe de base de la méthode Visit. Cette méthode CopyAndModify est nécessaire, car la méthode Visit, qui est protected (Protected en Visual Basic), ne peut pas être appelée directement à partir de la classe de contexte de requête.
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 Protected Overrides Function Visit(ByVal exp As Expression) As Expression If exp Is Nothing Then Return Nothing ElseIf Me.candidates.Contains(exp) Then Return Me.Evaluate(exp) End If Return MyBase.Visit(exp) End Function Private Function Evaluate(ByVal e As Expression) As Expression If e.NodeType = ExpressionType.Constant Then Return e End If Dim lambda = Expression.Lambda(e) Dim fn As [Delegate] = lambda.Compile() Return Expression.Constant(fn.DynamicInvoke(Nothing), e.Type) End Function End Class ''' <summary> ''' Performs bottom-up analysis to determine which nodes can possibly ''' be part of an evaluated sub-tree. ''' </summary> Class Nominator Inherits ExpressionVisitor Private fnCanBeEvaluated As Func(Of Expression, Boolean) Private candidates As HashSet(Of Expression) Private cannotBeEvaluated As Boolean Friend Sub New(ByVal fnCanBeEvaluated As Func(Of Expression, Boolean)) Me.fnCanBeEvaluated = fnCanBeEvaluated End Sub Friend Function Nominate(ByVal expr As Expression) As HashSet(Of Expression) Me.candidates = New HashSet(Of Expression)() Me.Visit(expr) Return Me.candidates End Function Protected Overrides Function Visit(ByVal expr As Expression) As Expression If expr IsNot Nothing Then Dim saveCannotBeEvaluated = Me.cannotBeEvaluated Me.cannotBeEvaluated = False MyBase.Visit(expr) If Not Me.cannotBeEvaluated Then If Me.fnCanBeEvaluated(expr) Then Me.candidates.Add(expr) Else Me.cannotBeEvaluated = True End If End If Me.cannotBeEvaluated = Me.cannotBeEvaluated Or _ saveCannotBeEvaluated End If Return expr End Function End Class End Module
using System; using System.Collections.Generic; using System.Linq.Expressions; namespace LinqToTerraServerProvider { public static class Evaluator { /// <summary> /// Performs evaluation & replacement of independent sub-trees /// </summary> /// <param name="expression">The root of the expression tree.</param> /// <param name="fnCanBeEvaluated">A function that decides whether a given expression node can be part of the local function.</param> /// <returns>A new tree with sub-trees evaluated and replaced.</returns> public static Expression PartialEval(Expression expression, Func<Expression, bool> fnCanBeEvaluated) { return new SubtreeEvaluator(new Nominator(fnCanBeEvaluated).Nominate(expression)).Eval(expression); } /// <summary> /// Performs evaluation & replacement of independent sub-trees /// </summary> /// <param name="expression">The root of the expression tree.</param> /// <returns>A new tree with sub-trees evaluated and replaced.</returns> public static Expression PartialEval(Expression expression) { return PartialEval(expression, Evaluator.CanBeEvaluatedLocally); } private static bool CanBeEvaluatedLocally(Expression expression) { return expression.NodeType != ExpressionType.Parameter; } /// <summary> /// Evaluates & replaces sub-trees when first candidate is reached (top-down) /// </summary> class SubtreeEvaluator : ExpressionVisitor { HashSet<Expression> candidates; internal SubtreeEvaluator(HashSet<Expression> candidates) { this.candidates = candidates; } internal Expression Eval(Expression exp) { return this.Visit(exp); } protected override Expression Visit(Expression exp) { if (exp == null) { return null; } if (this.candidates.Contains(exp)) { return this.Evaluate(exp); } return base.Visit(exp); } private Expression Evaluate(Expression e) { if (e.NodeType == ExpressionType.Constant) { return e; } LambdaExpression lambda = Expression.Lambda(e); Delegate fn = lambda.Compile(); return Expression.Constant(fn.DynamicInvoke(null), e.Type); } } /// <summary> /// Performs bottom-up analysis to determine which nodes can possibly /// be part of an evaluated sub-tree. /// </summary> class Nominator : ExpressionVisitor { Func<Expression, bool> fnCanBeEvaluated; HashSet<Expression> candidates; bool cannotBeEvaluated; internal Nominator(Func<Expression, bool> fnCanBeEvaluated) { this.fnCanBeEvaluated = fnCanBeEvaluated; } internal HashSet<Expression> Nominate(Expression expression) { this.candidates = new HashSet<Expression>(); this.Visit(expression); return this.candidates; } protected override Expression Visit(Expression expression) { if (expression != null) { bool saveCannotBeEvaluated = this.cannotBeEvaluated; this.cannotBeEvaluated = false; base.Visit(expression); if (!this.cannotBeEvaluated) { if (this.fnCanBeEvaluated(expression)) { this.candidates.Add(expression); } else { this.cannotBeEvaluated = true; } } this.cannotBeEvaluated |= saveCannotBeEvaluated; } return expression; } } } }
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 And _ seqType.BaseType IsNot GetType(Object) Then Return FindIEnumerable(seqType.BaseType) End If Return Nothing End Function End Module
using System; using System.Collections.Generic; namespace LinqToTerraServerProvider { internal static class TypeSystem { internal static Type GetElementType(Type seqType) { Type ienum = FindIEnumerable(seqType); if (ienum == null) return seqType; return ienum.GetGenericArguments()[0]; } private static Type FindIEnumerable(Type seqType) { if (seqType == null || seqType == typeof(string)) return null; if (seqType.IsArray) return typeof(IEnumerable<>).MakeGenericType(seqType.GetElementType()); if (seqType.IsGenericType) { foreach (Type arg in seqType.GetGenericArguments()) { Type ienum = typeof(IEnumerable<>).MakeGenericType(arg); if (ienum.IsAssignableFrom(seqType)) { return ienum; } } } Type[] ifaces = seqType.GetInterfaces(); if (ifaces != null && ifaces.Length > 0) { foreach (Type iface in ifaces) { Type ienum = FindIEnumerable(iface); if (ienum != null) return ienum; } } if (seqType.BaseType != null && seqType.BaseType != typeof(object)) { return FindIEnumerable(seqType.BaseType); } return null; } } }
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
Ajoutez un nouveau projet Application console à votre solution et nommez-le ClientApp.
Dans le nouveau projet, ajoutez une référence à l'assembly fournisseur.
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.)
Remarque : 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.
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
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.
Générez ClientApp.
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.
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
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") Or _ ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "State") Then _locations.Add(ETH.GetValueFromExpression(m.Arguments(0))) Return m End If End If Return MyBase.VisitMethodCall(m) End Function
protected override Expression VisitMethodCall(MethodCallExpression m) { if (m.Method.DeclaringType == typeof(String) && m.Method.Name == "StartsWith") { if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "Name") || ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "State")) { locations.Add(ExpressionTreeHelpers.GetValueFromExpression(m.Arguments[0])); return m; } } return base.VisitMethodCall(m); }
Recompilez le projet LinqToTerraServerProvider.
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
Exécutez le programme et consultez les résultats. Il doit y avoir 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
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") Or _ ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "State") Then _locations.Add(ETH.GetValueFromExpression(m.Arguments(0))) Return m End If ElseIf m.Method.Name = "Contains" Then Dim valuesExpression As Expression = Nothing If m.Method.DeclaringType Is GetType(Enumerable) Then If ETH.IsSpecificMemberExpression(m.Arguments(1), GetType(Place), "Name") Or _ ETH.IsSpecificMemberExpression(m.Arguments(1), GetType(Place), "State") Then valuesExpression = m.Arguments(0) End If ElseIf m.Method.DeclaringType Is GetType(List(Of String)) Then If ETH.IsSpecificMemberExpression(m.Arguments(0), GetType(Place), "Name") Or _ ETH.IsSpecificMemberExpression(m.Arguments(0), GetType(Place), "State") Then valuesExpression = m.Object End If End If If valuesExpression Is Nothing OrElse valuesExpression.NodeType <> ExpressionType.Constant Then Throw New Exception("Could not find the location values.") End If Dim ce = CType(valuesExpression, ConstantExpression) Dim placeStrings = CType(ce.Value, IEnumerable(Of String)) ' Add each string in the collection to the list of locations to obtain data about. For Each place In placeStrings _locations.Add(place) Next Return m End If Return MyBase.VisitMethodCall(m) End Function
protected override Expression VisitMethodCall(MethodCallExpression m) { if (m.Method.DeclaringType == typeof(String) && m.Method.Name == "StartsWith") { if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "Name") || ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "State")) { locations.Add(ExpressionTreeHelpers.GetValueFromExpression(m.Arguments[0])); return m; } } else if (m.Method.Name == "Contains") { Expression valuesExpression = null; if (m.Method.DeclaringType == typeof(Enumerable)) { if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[1], typeof(Place), "Name") || ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[1], typeof(Place), "State")) { valuesExpression = m.Arguments[0]; } } else if (m.Method.DeclaringType == typeof(List<string>)) { if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[0], typeof(Place), "Name") || ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[0], typeof(Place), "State")) { valuesExpression = m.Object; } } if (valuesExpression == null || valuesExpression.NodeType != ExpressionType.Constant) throw new Exception("Could not find the location values."); ConstantExpression ce = (ConstantExpression)valuesExpression; IEnumerable<string> placeStrings = (IEnumerable<string>)ce.Value; // Add each string in the collection to the list of locations to obtain data about. foreach (string place in placeStrings) locations.Add(place); return m; } return base.VisitMethodCall(m); }
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 exécuter une vérification pour ces deux types de déclaration. Enumerable.Contains est défini comme une méthode d'extension ; par conséquent, la collection à laquelle il s'applique est en fait le premier argument de la méthode. List.Contains est défini comme une méthode d'instance ; par conséquent, la collection à laquelle il s'applique est l'objet de réception de la méthode.
Recompilez le projet LinqToTerraServerProvider.
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
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.
Voir aussi
Tâches
Fournisseur LINQ to TerraServer, exemple
Comment : implémenter un visiteur de l'arborescence de l'expression
Comment : modifier des arborescences d'expression
Concepts
Activation d'une source de données pour l'interrogation LINQ
Référence
Autres ressources
Services Windows Communication Foundation et services de données ADO.NET
Historique des modifications
Date |
Historique |
Raison |
---|---|---|
Juillet 2008 |
Ajout d'un lien vers l'exemple TerraServer |
Résolution des bogues de contenu. |