Procedura dettagliata: creazione di un provider LINQ IQueryable
Aggiornamento: Luglio 2008
In questo argomento avanzato vengono fornite le istruzioni dettagliate per la creazione di un provider LINQ personalizzato. Al termine, sarà possibile utilizzare il provider creato per scrivere le query LINQ sul servizio Web TerraServer-USA.
Il servizio Web TerraServer-USA fornisce un'interfaccia a un database di immagini aeree degli Stati Uniti. Espone anche un metodo che restituisce informazioni sui luoghi degli Stati Uniti, fornendo una parte o tutto il nome di un luogo. Questo metodo, denominato GetPlaceList, rappresenta il metodo che verrà chiamato dal provider LINQ. Il provider utilizzerà Windows Communication Foundation (WCF) per comunicare con il servizio Web. Per ulteriori informazioni sul servizio Web TerraServer-USA, vedere Overview of the TerraServer-USA Web Services (informazioni in lingua inglese).
Si tratta di un provider IQueryable relativamente semplice. Prevede informazioni specifiche nelle query che gestisce e ha un sistema del tipo chiuso, esponendo un solo tipo per rappresentare i dati del risultato. Questo provider esamina solo uno tipo di espressione della chiamata al metodo nella struttura ad albero dell'espressione che rappresenta la query, ovvero la chiamata più interna a Where. Estrae i dati necessari per eseguire una query sul servizio Web da questa espressione. Chiama quindi il servizio Web e inserisce i dati restituiti nella struttura ad albero dell'espressione al posto dell'origine dati IQueryable iniziale. L'esecuzione rimanente della query viene gestita dalle implementazioni Enumerable degli operatori di query standard.
Gli esempi di codice presenti in questo argomento sono forniti in C# e Visual Basic.
In questa procedura dettagliata vengono illustrate le attività seguenti:
Creazione del progetto in Visual Studio.
Implementazione delle interfacce richieste per un provider IQueryableLINQ: IQueryable<T>, IOrderedQueryable<T> e IQueryProvider.
Aggiunta di un tipo .NET personalizzato per rappresentare i dati dal servizio Web.
Creazione di una classe di contesto della query e una classe che ottiene i dati dal servizio Web.
Creazione di una sottoclasse del visitatore della struttura ad albero dell'espressione che cerca l'espressione che rappresenta la chiamata più interna al metodo Queryable.Where.
Creazione di una sottoclasse del visitatore della struttura ad albero dell'espressione che estrae le informazioni dalla query LINQ da utilizzare nella richiesta del servizio Web.
Creazione di una sottoclasse del visitatore della struttura ad albero dell'espressione che modifica la struttura ad albero dell'espressione che rappresenta la query LINQ completa.
Utilizzo di una classe dell'analizzatore per valutare parzialmente una struttura ad albero dell'espressione. Questo passaggio è necessario poiché converte tutti i riferimenti alle variabili locali nella query LINQ in valori.
Creazione di una classe di supporto della struttura ad albero dell'espressione e di una nuova classe di eccezioni.
Test del provider LINQ da un'applicazione client che contiene una query LINQ.
Aggiunta di funzionalità di query più complesse al provider LINQ.
Nota: Il provider LINQ creato con questa procedura dettagliata è disponibile come esempio. Per ulteriori informazioni, vedere Esempio di provider LINQ to TerraServer.
Prerequisiti
Per completare questa procedura dettagliata, è necessario disporre dei seguenti componenti:
- Visual Studio 2008
Nota: |
---|
Nel computer in uso è possibile che vengano visualizzati nomi o percorsi diversi per alcuni elementi dell'interfaccia utente di Visual Studio nelle istruzioni seguenti. La versione di Visual Studio in uso e le impostazioni configurate determinano questi elementi. Per ulteriori informazioni vedere Impostazioni di Visual Studio. |
Creazione del progetto
Per creare il progetto in Visual Studio
In Visual Studio creare una nuova applicazione Libreria di classi. Denominare il progetto LinqToTerraServerProvider.
In Esplora soluzioni selezionare il file Class1.cs o Class1.vb e rinominarlo come QueryableTerraServerData.cs o QueryableTerraServerData.vb. Nella finestra di dialogo visualizzata fare clic su Sì per rinominare tutti i riferimenti all'elemento di codice.
Il provider viene creato come progetto Libreria di classi in Visual Studio poiché le applicazioni client eseguibili aggiungeranno l'assembly di provider come riferimento al progetto.
Per aggiungere un riferimento al servizio Web
In Esplora soluzioni fare clic con il pulsante destro del mouse sul progetto LinqToTerraServerProvider e scegliere Aggiungi riferimento a servizio.
Verrà visualizzata la finestra di dialogo Aggiungi riferimento a servizio.
Nella casella Indirizzo digitare http://terraserver.microsoft.com/TerraService2.asmx.
Nella casella Spazio dei nomi digitare TerraServerReference, quindi scegliere OK.
Il servizio Web TerraServer-USA viene aggiunto come riferimento al servizio in modo che l'applicazione possa comunicare con il servizio Web tramite Windows Communication Foundation (WCF). Aggiungendo un riferimento al servizio nel progetto, Visual Studio genera un file app.config che contiene un proxy e un endpoint per il servizio Web. Per ulteriori informazioni, vedere Introduzione ai servizi di Windows Communication Foundation in Visual Studio.
È stato così creato un progetto contenente un file denominato app.config, un file denominato QueryableTerraServerData.cs o QueryableTerraServerData.vb e un riferimento al servizio denominato TerraServerReference.
Implementazione delle interfacce necessarie
Per creare un provider LINQ, è necessario implementare almeno le interfacce IQueryable<T> e IQueryProvider. IQueryable<T> e IQueryProvider vengono derivate dalle altre interfacce necessarie; pertanto, implementando queste due interfacce, si implementano anche le altre interfacce necessarie per un provider LINQ.
Se si desidera supportare operatori di query di ordinamento, ad esempio OrderBy e ThenBy, è necessario implementare anche l'interfaccia IOrderedQueryable<T>. Poiché IOrderedQueryable<T> deriva da IQueryable<T>, è possibile implementare entrambe queste interfacce in un unico tipo. Tale operazione viene in pratica effettuata dal provider.
Per implementare System.Linq.IQueryable'1 e System.Linq.IOrderedQueryable'1
Nel file QueryableTerraServerData.cs o QueryableTerraServerData.vb, aggiungere il codice seguente.
Imports System.Linq.Expressions Public Class QueryableTerraServerData(Of TData) Implements IOrderedQueryable(Of TData) #Region "Private members" Private _provider As TerraServerQueryProvider Private _expression As Expression #End Region #Region "Constructors" ''' <summary> ''' This constructor is called by the client to create the data source. ''' </summary> Public Sub New() Me._provider = New TerraServerQueryProvider() Me._expression = Expression.Constant(Me) End Sub ''' <summary> ''' This constructor is called by Provider.CreateQuery(). ''' </summary> ''' <param name="_expression"></param> Public Sub New(ByVal _provider As TerraServerQueryProvider, ByVal _expression As Expression) If _provider Is Nothing Then Throw New ArgumentNullException("provider") End If If _expression Is Nothing Then Throw New ArgumentNullException("expression") End If If Not GetType(IQueryable(Of TData)).IsAssignableFrom(_expression.Type) Then Throw New ArgumentOutOfRangeException("expression") End If Me._provider = _provider Me._expression = _expression End Sub #End Region #Region "Properties" Public ReadOnly Property ElementType() As Type _ Implements IQueryable(Of TData).ElementType Get Return GetType(TData) End Get End Property Public ReadOnly Property Expression() As Expression _ Implements IQueryable(Of TData).Expression Get Return _expression End Get End Property Public ReadOnly Property Provider() As IQueryProvider _ Implements IQueryable(Of TData).Provider Get Return _provider End Get End Property #End Region #Region "Enumerators" Public Function GetGenericEnumerator() As IEnumerator(Of TData) _ Implements IEnumerable(Of TData).GetEnumerator Return (Me.Provider.Execute(Of IEnumerable(Of TData))(Me._expression)).GetEnumerator() End Function Public Function GetEnumerator() As IEnumerator _ Implements IEnumerable.GetEnumerator Return (Me.Provider.Execute(Of IEnumerable)(Me._expression)).GetEnumerator() End Function #End Region End Class
using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; namespace LinqToTerraServerProvider { public class QueryableTerraServerData<TData> : IOrderedQueryable<TData> { #region Constructors /// <summary> /// This constructor is called by the client to create the data source. /// </summary> public QueryableTerraServerData() { Provider = new TerraServerQueryProvider(); Expression = Expression.Constant(this); } /// <summary> /// This constructor is called by Provider.CreateQuery(). /// </summary> /// <param name="expression"></param> public QueryableTerraServerData(TerraServerQueryProvider provider, Expression expression) { if (provider == null) { throw new ArgumentNullException("provider"); } if (expression == null) { throw new ArgumentNullException("expression"); } if (!typeof(IQueryable<TData>).IsAssignableFrom(expression.Type)) { throw new ArgumentOutOfRangeException("expression"); } Provider = provider; Expression = expression; } #endregion #region Properties public IQueryProvider Provider { get; private set; } public Expression Expression { get; private set; } public Type ElementType { get { return typeof(TData); } } #endregion #region Enumerators public IEnumerator<TData> GetEnumerator() { return (Provider.Execute<IEnumerable<TData>>(Expression)).GetEnumerator(); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return (Provider.Execute<System.Collections.IEnumerable>(Expression)).GetEnumerator(); } #endregion } }
L'implementazione IOrderedQueryable<T> mediante la classe QueryableTerraServerData implementa tre proprietà dichiarate in IQueryable e due metodi di enumerazione dichiarati in IEnumerable e IEnumerable<T>.
Questa classe dispone di due costruttori. Il primo costruttore viene chiamato dall'applicazione client per creare l'oggetto su cui scrivere la query LINQ. Il secondo costruttore viene chiamato all'interno della libreria del provider dal codice nell'implementazione IQueryProvider.
Quando il metodo GetEnumerator viene chiamato su un oggetto di tipo QueryableTerraServerData, viene eseguita la relativa query e vengono enumerati i risultati della query.
Questo codice, ad eccezione del nome della classe, non è specifico di questo provider di servizi TerraServer-USA. Pertanto, può essere riutilizzato per qualsiasi provider LINQ.
Per implementare System.Linq.IQueryProvider
Aggiungere la classe TerraServerQueryProvider al progetto.
Imports System.Linq.Expressions Imports System.Reflection Public Class TerraServerQueryProvider Implements IQueryProvider Public Function CreateQuery(ByVal expression As Expression) As IQueryable _ Implements IQueryProvider.CreateQuery Dim elementType As Type = TypeSystem.GetElementType(expression.Type) Try Dim qType = GetType(QueryableTerraServerData(Of )).MakeGenericType(elementType) Dim args = New Object() {Me, expression} Dim instance = Activator.CreateInstance(qType, args) Return CType(instance, IQueryable) Catch tie As TargetInvocationException Throw tie.InnerException End Try End Function ' Queryable's collection-returning standard query operators call this method. Public Function CreateQuery(Of TResult)(ByVal expression As Expression) As IQueryable(Of TResult) _ Implements IQueryProvider.CreateQuery Return New QueryableTerraServerData(Of TResult)(Me, expression) End Function Public Function Execute(ByVal expression As Expression) As Object _ Implements IQueryProvider.Execute Return TerraServerQueryContext.Execute(expression, False) End Function ' Queryable's "single value" standard query operators call this method. ' It is also called from QueryableTerraServerData.GetEnumerator(). Public Function Execute(Of TResult)(ByVal expression As Expression) As TResult _ Implements IQueryProvider.Execute Dim IsEnumerable As Boolean = (GetType(TResult).Name = "IEnumerable`1") Dim result = TerraServerQueryContext.Execute(expression, IsEnumerable) Return CType(result, TResult) End Function End Class
using System; using System.Linq; using System.Linq.Expressions; namespace LinqToTerraServerProvider { public class TerraServerQueryProvider : IQueryProvider { public IQueryable CreateQuery(Expression expression) { Type elementType = TypeSystem.GetElementType(expression.Type); try { return (IQueryable)Activator.CreateInstance(typeof(QueryableTerraServerData<>).MakeGenericType(elementType), new object[] { this, expression }); } catch (System.Reflection.TargetInvocationException tie) { throw tie.InnerException; } } // Queryable's collection-returning standard query operators call this method. public IQueryable<TResult> CreateQuery<TResult>(Expression expression) { return new QueryableTerraServerData<TResult>(this, expression); } public object Execute(Expression expression) { return TerraServerQueryContext.Execute(expression, false); } // Queryable's "single value" standard query operators call this method. // It is also called from QueryableTerraServerData.GetEnumerator(). public TResult Execute<TResult>(Expression expression) { bool IsEnumerable = (typeof(TResult).Name == "IEnumerable`1"); return (TResult)TerraServerQueryContext.Execute(expression, IsEnumerable); } } }
Il codice del provider della query in questa classe implementa i quattro metodi richiesti per implementare l'interfaccia IQueryProvider. I due metodi CreateQuery creano query associate all'origine dati. I due metodi Execute inviano tali query da eseguire.
Il metodo CreateQuery non generico utilizza la riflessione per ottenere il tipo di elemento della sequenza che la query creata restituisce quando viene eseguita. Utilizza quindi la classe Activator per costruire una nuova istanza QueryableTerraServerData costruita con il tipo di elemento come argomento di tipo generico. Il risultato della chiamata al metodo CreateQuery non generico corrisponde al metodo CreateQuery generico chiamato con l'argomento di tipo corretto.
La maggior parte della logica di esecuzione della query viene gestita in una classe diversa che verrà aggiunta successivamente. Questa funzionalità viene gestita altrove poiché è specifica dell'origine dati su cui eseguire una query, mentre il codice in questa classe è generico per qualsiasi provider LINQ. Per utilizzare questo codice per un provider diverso, è possibile dovere modificare il nome della classe e il nome del tipo di contesto della query a cui viene fatto riferimento nei due metodi.
Aggiunta di un tipo personalizzato per rappresentare i dati dei risultati
È necessario un tipo .NET per rappresentare i dati ottenuti dal servizio Web. Questo tipo verrà utilizzato nella query LINQ client per definire i risultati desiderati. Nella procedura descritta di seguito viene creato tale tipo. Questo tipo, denominatoPlace, contiene le informazioni su una singola località geografica, ad esempio una città, un parco o un lago.
Questo codice contiene anche un tipo di enumerazione, denominato PlaceType, che definisce i vari tipi di località geografica e che viene utilizzato nella classe Place.
Per creare un tipo di risultati personalizzato
Aggiungere la classe Place e l'enumerazione PlaceType al progetto.
Public Class Place ' Properties. 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 } }
Il costruttore per il tipo Place semplifica la creazione di un oggetto risultato dal tipo restituito dal servizio Web. Sebbene il provider possa restituire il tipo di risultati definito direttamente dall'API del servizio Web, le applicazioni client devono aggiungere un riferimento al servizio Web. Creando un nuovo tipo come parte della libreria del provider, il client non deve necessariamente conoscere i tipi e i metodi esposti dal servizio Web.
Aggiunta della funzionalità per ottenere i dati dall'origine dati
Questa implementazione del provider presuppone che la chiamata più interna a Queryable.Where contenga le informazioni sul percorso da utilizzare per eseguire una query sul servizio Web. La chiamata Queryable.Where più interna è la clausola where (clausola Where in Visual Basic) o la chiamata al metodo Queryable.Where che si verifica prima in una query LINQ o in quella più vicino alla fine della struttura ad albero dell'espressione che rappresenta la query.
Per creare una classe di contesto della query
Aggiungere la classe TerraServerQueryContext al progetto.
Imports System.Linq.Expressions Public Class TerraServerQueryContext ' Executes the expression tree that is passed to it. Friend Shared Function Execute(ByVal expr As Expression, _ ByVal IsEnumerable As Boolean) As Object ' The expression must represent a query over the data source. If Not IsQueryOverDataSource(expr) Then Throw New InvalidProgramException("No query over the data source was specified.") End If ' Find the call to Where() and get the lambda expression predicate. Dim whereFinder As New InnermostWhereFinder() Dim whereExpression As MethodCallExpression = _ whereFinder.GetInnermostWhere(expr) Dim lambdaExpr As LambdaExpression lambdaExpr = CType(CType(whereExpression.Arguments(1), UnaryExpression).Operand, LambdaExpression) ' Send the lambda expression through the partial evaluator. lambdaExpr = CType(Evaluator.PartialEval(lambdaExpr), LambdaExpression) ' Get the place name(s) to query the Web service with. Dim lf As New LocationFinder(lambdaExpr.Body) Dim locations As List(Of String) = lf.Locations If locations.Count = 0 Then Dim s = "You must specify at least one place name in your query." Throw New InvalidQueryException(s) End If ' Call the Web service and get the results. Dim places() = WebServiceHelper.GetPlacesFromTerraServer(locations) ' Copy the IEnumerable places to an IQueryable. Dim queryablePlaces = places.AsQueryable() ' Copy the expression tree that was passed in, changing only the first ' argument of the innermost MethodCallExpression. Dim treeCopier As New ExpressionTreeModifier(queryablePlaces) Dim newExpressionTree = treeCopier.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); } } }
Questa classe organizza l'esecuzione di una query. Dopo avere trovato l'espressione che rappresenta la chiamata Queryable.Where più interna, questo codice recupera l'espressione lambda che rappresenta il predicato passato a Queryable.Where. Passa quindi l'espressione di predicato a un metodo da valutare parzialmente, in modo che tutti i riferimenti alle variabili locali vengano convertiti in valori. Chiama quindi un metodo per estrarre i percorsi richiesti dal predicato e chiama un altro metodo per ottenere i dati dei risultati dal servizio Web.
Nel passaggio successivo questo codice copia la struttura ad albero dell'espressione che rappresenta la query LINQ ed effettua una modifica alla struttura ad albero dell'espressione. Il codice utilizza una sottoclasse del visitatore della struttura ad albero dell'espressione per sostituire l'origine dati a cui viene applicata la chiamata dell'operatore di query più interna con l'elenco concreto di oggetti Place ottenuto dal servizio Web.
Prima di inserire l'elenco di oggetti Place nella struttura ad albero dell'espressione, il tipo viene impostato da IEnumerable su IQueryable chiamando AsQueryable. Questa modifica al tipo è necessaria poiché quando viene riscritta la struttura ad albero dell'espressione, viene ricostruito il nodo che rappresenta la chiamata al metodo dell'operatore di query più interno. Il nodo viene ricostruito poiché è stato modificato uno degli argomenti, ovvero l'origine dati a cui è applicato. Il metodo Call(Expression, MethodInfo, IEnumerable<Expression>), utilizzato per ricostruire il nodo, genererà un'eccezione se un argomento non può essere assegnato al parametro corrispondente del metodo al quale verrà passato. In questo caso, l'elenco IEnumerable di oggetti Place non potrà essere assegnato al parametro IQueryable di Queryable.Where. Pertanto, il tipo viene impostato su IQueryable.
Impostando il tipo su IQueryable, l'insieme ottiene anche un membro IQueryProvider, a cui è possibile accedere dalla proprietà Provider che può creare o eseguire query. Il tipo dinamico dell'insieme IQueryablePlace è EnumerableQuery, ovvero un tipo all'interno dell'API System.Linq. Il provider della query associato a questo tipo esegue query sostituendo le chiamate degli operatori di query standard Queryable con gli operatori Enumerable equivalenti, in modo che la query diventi effettivamente una query LINQ to Objects.
Il codice finale nella classe TerraServerQueryContext chiama uno dei due metodi nell'elenco IQueryable di oggetti Place. Chiama CreateQuery se la query client restituisce risultati enumerabili o Execute se la query client restituisce un risultato non enumerabile.
Il codice in questa classe è specifico di questo provider TerraServer-USA. Pertanto, è incapsulato nella classe TerraServerQueryContext anziché inserito direttamente nell'implementazione IQueryProvider più generica.
Il provider che si sta creando richiede solo le informazioni nel predicato Queryable.Where per eseguire una query sul servizio Web. Pertanto, utilizza LINQ to Objects per eseguire la query LINQ utilizzando il tipo EnumerableQuery interno. Un modo alternativo per utilizzare LINQ to Objects per eseguire la query è che il client esegua il wrapping della parte della query da eseguire mediante LINQ to Objects in una query LINQ to Objects. Ciò è possibile chiamando AsEnumerable<TSource> sul resto della query, ovvero la parte della query che il provider richiede per scopi specifici. Il vantaggio di questo tipo di implementazione è che la divisione del lavoro tra il provider personalizzato e LINQ to Objects è più trasparente.
Nota: |
---|
Il provider illustrato in questo argomento è un semplice provider con un supporto specifico minimo delle query. Pertanto, si basa in modo rilevante su LINQ to Objects per eseguire query. Un provider LINQ complesso come LINQ to SQL può supportare l'intera query senza passare il lavoro a LINQ to Objects. |
Per creare una classe per ottenere i dati dal servizio Web
Aggiungere al progetto la classe WebServiceHelper o il modulo in Visual Basic.
Imports System.Collections.Generic Imports LinqToTerraServerProvider.TerraServerReference Friend Module WebServiceHelper Private numResults As Integer = 200 Private mustHaveImage As Boolean = False Friend Function GetPlacesFromTerraServer(ByVal locations As List(Of String)) As Place() ' Limit the total number of Web service calls. If locations.Count > 5 Then Dim s = "This query requires more than five separate calls to the Web service. Please decrease the number of places." Throw New InvalidQueryException(s) End If Dim allPlaces As New List(Of Place) ' For each location, call the Web service method to get data. For Each location In locations Dim places = CallGetPlaceListMethod(location) allPlaces.AddRange(places) Next Return allPlaces.ToArray() End Function Private Function CallGetPlaceListMethod(ByVal location As String) As Place() Dim client As New TerraServiceSoapClient() Dim placeFacts() As PlaceFacts Try ' Call the Web service method "GetPlaceList". placeFacts = client.GetPlaceList(location, numResults, mustHaveImage) ' If we get exactly 'numResults' results, they are probably truncated. If (placeFacts.Length = numResults) Then Dim s = "The results have been truncated by the Web service and would not be complete. Please try a different query." Throw New Exception(s) End If ' Create Place objects from the PlaceFacts objects returned by the Web service. Dim places(placeFacts.Length - 1) As Place For i = 0 To placeFacts.Length - 1 places(i) = New Place(placeFacts(i).Place.City, _ placeFacts(i).Place.State, _ placeFacts(i).PlaceTypeId) Next ' Close the WCF client. client.Close() Return places Catch timeoutException As TimeoutException client.Abort() Throw Catch communicationException As System.ServiceModel.CommunicationException client.Abort() Throw End Try End Function End Module
using System; using System.Collections.Generic; using LinqToTerraServerProvider.TerraServerReference; namespace LinqToTerraServerProvider { internal static class WebServiceHelper { private static int numResults = 200; private static bool mustHaveImage = false; internal static Place[] GetPlacesFromTerraServer(List<string> locations) { // Limit the total number of Web service calls. if (locations.Count > 5) throw new InvalidQueryException("This query requires more than five separate calls to the Web service. Please decrease the number of locations in your query."); List<Place> allPlaces = new List<Place>(); // For each location, call the Web service method to get data. foreach (string location in locations) { Place[] places = CallGetPlaceListMethod(location); allPlaces.AddRange(places); } return allPlaces.ToArray(); } private static Place[] CallGetPlaceListMethod(string location) { TerraServiceSoapClient client = new TerraServiceSoapClient(); PlaceFacts[] placeFacts = null; try { // Call the Web service method "GetPlaceList". placeFacts = client.GetPlaceList(location, numResults, mustHaveImage); // If there are exactly 'numResults' results, they are probably truncated. if (placeFacts.Length == numResults) throw new Exception("The results have been truncated by the Web service and would not be complete. Please try a different query."); // Create Place objects from the PlaceFacts objects returned by the Web service. Place[] places = new Place[placeFacts.Length]; for (int i = 0; i < placeFacts.Length; i++) { places[i] = new Place( placeFacts[i].Place.City, placeFacts[i].Place.State, placeFacts[i].PlaceTypeId); } // Close the WCF client. client.Close(); return places; } catch (TimeoutException timeoutException) { client.Abort(); throw; } catch (System.ServiceModel.CommunicationException communicationException) { client.Abort(); throw; } } } }
Questa classe contiene la funzionalità che ottiene i dati dal servizio Web. Questo codice utilizza un tipo denominato TerraServiceSoapClient, generato automaticamente per il progetto da Windows Communication Foundation (WCF), per chiamare il metodo del servizio Web GetPlaceList. Quindi, ogni risultato viene convertito dal tipo restituito del metodo del servizio Web nel tipo .NET che il provider definisce per i dati.
Questo codice contiene due controlli che migliorano l'utilizzabilità della libreria del provider. Il primo controllo limita il tempo massimo di attesa di una risposta da parte di un'applicazione client limitando a cinque il numero totale di chiamate effettuate al servizio Web per query. Per ogni percorso specificato nella query client, viene generata una richiesta del servizio Web. Pertanto, il provider genera un'eccezione se la query contiene più di cinque percorsi.
Il secondo controllo determina se il numero di risultati restituiti dal servizio Web è uguale al numero massimo di risultati che può restituire. Se il numero di risultati è il numero massimo, è probabile che i risultati dal servizio Web vengano troncati. Anziché restituire un elenco incompleto al client, il provider genera un'eccezione.
Aggiunta di classi del visitatore della struttura ad albero dell'espressione
Per creare il visitatore che cerca l'espressione della chiamata al metodo Where più interna
Aggiungere la classe ExpressionVisitor al progetto. Questo codice è disponibile in Procedura: implementare un visitatore della struttura ad albero dell'espressione. Aggiungere le direttive using (istruzioni Imports in Visual Basic) al file per i seguenti spazi dei nomi: System.Collections.Generic, System.Collections.ObjectModel e System.Linq.Expressions.
Aggiungere al progetto la classe InnermostWhereFinder, che eredita la classe ExpressionVisitor.
Imports System.Linq.Expressions Class InnermostWhereFinder Inherits ExpressionVisitor Private innermostWhereExpression As MethodCallExpression Public Function GetInnermostWhere(ByVal expr As Expression) As MethodCallExpression Me.Visit(expr) Return innermostWhereExpression End Function Protected Overrides Function VisitMethodCall(ByVal expr As MethodCallExpression) As Expression If expr.Method.Name = "Where" Then innermostWhereExpression = expr End If Me.Visit(expr.Arguments(0)) Return expr End Function End Class
using System; using System.Linq.Expressions; namespace LinqToTerraServerProvider { internal class InnermostWhereFinder : ExpressionVisitor { private MethodCallExpression innermostWhereExpression; public MethodCallExpression GetInnermostWhere(Expression expression) { Visit(expression); return innermostWhereExpression; } protected override Expression VisitMethodCall(MethodCallExpression expression) { if (expression.Method.Name == "Where") innermostWhereExpression = expression; Visit(expression.Arguments[0]); return expression; } } }
Questa classe eredita la classe del visitatore della struttura ad albero dell'espressione di base per eseguire la funzionalità di ricerca di un'espressione specifica. La classe del visitatore della struttura ad albero dell'espressione di base è progettata per essere ereditata e specializzata per un'attività specifica che implica il passaggio di una struttura ad albero dell'espressione. La classe derivata esegue l'override del metodo VisitMethodCall per cercare l'espressione che rappresenta la chiamata più interna a Where nella struttura ad albero dell'espressione che rappresenta la query client. Questa espressione più interna è l'espressione da cui il provider estrae i percorsi di ricerca.
Per creare il visitatore che estrae i dati per eseguire una query sul servizio Web
Aggiungere la classe LocationFinder al progetto.
Imports System.Linq.Expressions Imports ETH = LinqToTerraServerProvider.ExpressionTreeHelpers Friend Class LocationFinder Inherits ExpressionVisitor Private _expression As Expression Private _locations As List(Of String) Public Sub New(ByVal exp As Expression) Me._expression = exp End Sub Public ReadOnly Property Locations() As List(Of String) Get If _locations Is Nothing Then _locations = New List(Of String)() Me.Visit(Me._expression) End If Return Me._locations End Get End Property Protected Overrides Function VisitBinary(ByVal be As BinaryExpression) As Expression ' Handles Visual Basic String semantics. be = ETH.ConvertVBStringCompare(be) If be.NodeType = ExpressionType.Equal Then If (ETH.IsMemberEqualsValueExpression(be, GetType(Place), "Name")) Then _locations.Add(ETH.GetValueFromEqualsExpression(be, GetType(Place), "Name")) Return be ElseIf (ETH.IsMemberEqualsValueExpression(be, GetType(Place), "State")) Then _locations.Add(ETH.GetValueFromEqualsExpression(be, GetType(Place), "State")) Return be Else Return MyBase.VisitBinary(be) End If Else Return MyBase.VisitBinary(be) End If End Function End Class
using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; namespace LinqToTerraServerProvider { internal class LocationFinder : ExpressionVisitor { private Expression expression; private List<string> locations; public LocationFinder(Expression exp) { this.expression = exp; } public List<string> Locations { get { if (locations == null) { locations = new List<string>(); this.Visit(this.expression); } return this.locations; } } protected override Expression VisitBinary(BinaryExpression be) { if (be.NodeType == ExpressionType.Equal) { if (ExpressionTreeHelpers.IsMemberEqualsValueExpression(be, typeof(Place), "Name")) { locations.Add(ExpressionTreeHelpers.GetValueFromEqualsExpression(be, typeof(Place), "Name")); return be; } else if (ExpressionTreeHelpers.IsMemberEqualsValueExpression(be, typeof(Place), "State")) { locations.Add(ExpressionTreeHelpers.GetValueFromEqualsExpression(be, typeof(Place), "State")); return be; } else return base.VisitBinary(be); } else return base.VisitBinary(be); } } }
Questa classe viene utilizzata per estrarre le informazioni sul percorso dal predicato che il client passa a Queryable.Where. Deriva dalla classe del visitatore della struttura ad albero dell'espressione di base ed esegue l'override solo del metodo VisitBinary.
La classe di base del visitatore della struttura ad albero dell'espressione invia le espressioni binarie, ad esempio le espressioni di uguaglianza come place.Name == "Seattle" (place.Name = "Seattle" in Visual Basic), al metodo VisitBinary. In questo metodo di overriding VisitBinary, se l'espressione corrisponde al modello dell'espressione di uguaglianza che può fornire informazioni sul percorso, tali informazioni vengono estratte e archiviate in un elenco di percorsi.
Questa classe utilizza un visitatore della struttura ad albero dell'espressione per cercare le informazioni sul percorso nella struttura ad albero dell'espressione poiché un visitatore è progettato per trasferire ed esaminare le strutture ad albero dell'espressione. Il codice risultante è più preciso e meno soggetto a errori rispetto all'implementazione senza utilizzare il visitatore.
In questa fase della procedura dettagliata, il provider supporta solo alcune modalità per fornire le informazioni sul percorso nella query. Più avanti in questo argomento verrà aggiunta la funzionalità per consentire ulteriori modalità per fornire informazioni sul percorso.
Per creare il visitatore che modifica la struttura ad albero dell'espressione
Aggiungere la classe ExpressionTreeModifier al progetto.
Imports System.Linq.Expressions Friend Class ExpressionTreeModifier Inherits ExpressionVisitor Private queryablePlaces As IQueryable(Of Place) Friend Sub New(ByVal places As IQueryable(Of Place)) Me.queryablePlaces = places End Sub 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; } } }
Questa classe deriva dalla classe del visitatore della struttura ad albero dell'espressione di base ed esegue l'override del metodo VisitConstant. In questo metodo, sostituisce l'oggetto a cui viene applicata la chiamata dell'operatore di query standard più interna con un elenco concreto di oggetti Place.
Il metodo CopyAndModify chiama l'implementazione della classe di base del metodo Visit. Questo metodo CopyAndModify è necessario poiché il metodo Visit che è protected (Protected in Visual Basic) non può essere chiamato direttamente dalla classe di contesto della query.
Questa classe di modificatori della struttura ad albero dell'espressione utilizza il visitatore della struttura ad albero dell'espressione poiché il visitatore è progettato per trasferire, esaminare e copiare le strutture ad albero dell'espressione. Derivando dalla classe del visitatore della struttura ad albero dell'espressione di base, questa classe richiede una quantità minima di codice per eseguire la relativa funzione.
Aggiunta dell'analizzatore di espressioni
Il predicato passato al metodo Queryable.Where nella query client può contenere sottoespressioni che non dipendono dal parametro dell'espressione lambda. Queste sottoespressioni isolate possono e devono essere valutate immediatamente. Possono essere riferimenti a variabili locali o variabili membro che devono essere convertite in valori.
La classe successiva espone un metodo, PartialEval(Expression), che determina la sottostruttura ad albero nell'espressione, se presente, da valutare immediatamente. Valuta quindi tali espressioni creando un'espressione lambda, compilandola e richiamando il delegato restituito. Infine, sostituisce la sottostruttura ad albero con un nuovo nodo che rappresenta un valore costante. Questa operazione è nota come valutazione parziale.
Per aggiungere una classe per eseguire la valutazione parziale di una struttura ad albero dell'espressione
Aggiungere la classe Evaluator al progetto.
Imports System.Linq.Expressions Public Module Evaluator ''' <summary>Performs evaluation and replacement of independent sub-trees</summary> ''' <param name="expr">The root of the expression tree.</param> ''' <param name="fnCanBeEvaluated">A function that decides whether a given expression node can be part of the local function.</param> ''' <returns>A new tree with sub-trees evaluated and replaced.</returns> Public Function PartialEval(ByVal expr As Expression, _ ByVal fnCanBeEvaluated As Func(Of Expression, Boolean)) _ As Expression Return New SubtreeEvaluator(New Nominator(fnCanBeEvaluated).Nominate(expr)).Eval(expr) End Function ''' <summary> ''' Performs evaluation and replacement of independent sub-trees ''' </summary> ''' <param name="expression">The root of the expression tree.</param> ''' <returns>A new tree with sub-trees evaluated and replaced.</returns> Public Function PartialEval(ByVal expression As Expression) As Expression Return PartialEval(expression, AddressOf Evaluator.CanBeEvaluatedLocally) End Function Private Function CanBeEvaluatedLocally(ByVal expression As Expression) As Boolean Return expression.NodeType <> ExpressionType.Parameter End Function ''' <summary> ''' Evaluates and replaces sub-trees when first candidate is reached (top-down) ''' </summary> Class SubtreeEvaluator Inherits ExpressionVisitor Private candidates As HashSet(Of Expression) Friend Sub New(ByVal candidates As HashSet(Of Expression)) Me.candidates = candidates End Sub Friend Function Eval(ByVal exp As Expression) As Expression Return Me.Visit(exp) End Function 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; } } } }
Aggiunta delle classi di supporto
In questa sezione è contenuto il codice per tre classi di supporto del provider.
Per aggiungere la classe di supporto utilizzata dall'implementazione System.Linq.IQueryProvider
Aggiungere al progetto la classe TypeSystem o il modulo in Visual Basic.
Imports System.Collections.Generic Friend Module TypeSystem Friend Function GetElementType(ByVal seqType As Type) As Type Dim ienum As Type = FindIEnumerable(seqType) If ienum Is Nothing Then Return seqType End If Return ienum.GetGenericArguments()(0) End Function Private Function FindIEnumerable(ByVal seqType As Type) As Type If seqType Is Nothing Or seqType Is GetType(String) Then Return Nothing End If If (seqType.IsArray) Then Return GetType(IEnumerable(Of )).MakeGenericType(seqType.GetElementType()) End If If (seqType.IsGenericType) Then For Each arg As Type In seqType.GetGenericArguments() Dim ienum As Type = GetType(IEnumerable(Of )).MakeGenericType(arg) If (ienum.IsAssignableFrom(seqType)) Then Return ienum End If Next End If Dim ifaces As Type() = seqType.GetInterfaces() If ifaces IsNot Nothing And ifaces.Length > 0 Then For Each iface As Type In ifaces Dim ienum As Type = FindIEnumerable(iface) If (ienum IsNot Nothing) Then Return ienum End If Next End If If seqType.BaseType IsNot Nothing 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; } } }
Questa classe di supporto viene utilizzata dall'implementazione IQueryProvider aggiunta precedentemente.
TypeSystem.GetElementType utilizza la riflessione per ottenere l'argomento di tipo generico di un insieme IEnumerable<T> (IEnumerable(Of T) in Visual Basic). Questo metodo viene chiamato dal metodo CreateQuery non generico nell'implementazione del provider della query per fornire il tipo di elemento dell'insieme di risultati della query.
Questa classe di supporto non è specifica di questo provider del servizio Web TerraServer-USA. Pertanto, può essere riutilizzato per qualsiasi provider LINQ.
Per creare una classe di supporto della struttura ad albero dell'espressione
Aggiungere la classe ExpressionTreeHelpers al progetto.
Imports System.Linq.Expressions Friend Class ExpressionTreeHelpers ' Visual Basic encodes string comparisons as a method call to ' Microsoft.VisualBasic.CompilerServices.Operators.CompareString. ' This method will convert the method call into a binary operation instead. ' Note that this makes the string comparison case sensitive. Friend Shared Function ConvertVBStringCompare(ByVal exp As BinaryExpression) As BinaryExpression If exp.Left.NodeType = ExpressionType.Call Then Dim compareStringCall = CType(exp.Left, MethodCallExpression) If compareStringCall.Method.DeclaringType.FullName = _ "Microsoft.VisualBasic.CompilerServices.Operators" AndAlso _ compareStringCall.Method.Name = "CompareString" Then Dim arg1 = compareStringCall.Arguments(0) Dim arg2 = compareStringCall.Arguments(1) Select Case exp.NodeType Case ExpressionType.LessThan Return Expression.LessThan(arg1, arg2) Case ExpressionType.LessThanOrEqual Return Expression.GreaterThan(arg1, arg2) Case ExpressionType.GreaterThan Return Expression.GreaterThan(arg1, arg2) Case ExpressionType.GreaterThanOrEqual Return Expression.GreaterThanOrEqual(arg1, arg2) Case Else Return Expression.Equal(arg1, arg2) End Select End If End If Return exp End Function Friend Shared Function IsMemberEqualsValueExpression(ByVal exp As Expression, _ ByVal declaringType As Type, _ ByVal memberName As String) As Boolean If exp.NodeType <> ExpressionType.Equal Then Return False End If Dim be = CType(exp, BinaryExpression) ' Assert. If IsSpecificMemberExpression(be.Left, declaringType, memberName) AndAlso _ IsSpecificMemberExpression(be.Right, declaringType, memberName) Then Throw New Exception("Cannot have 'member' = 'member' in an expression!") End If Return IsSpecificMemberExpression(be.Left, declaringType, memberName) OrElse _ IsSpecificMemberExpression(be.Right, declaringType, memberName) End Function Friend Shared Function IsSpecificMemberExpression(ByVal exp As Expression, _ ByVal declaringType As Type, _ ByVal memberName As String) As Boolean Return (TypeOf exp Is MemberExpression) AndAlso _ (CType(exp, MemberExpression).Member.DeclaringType Is declaringType) AndAlso _ (CType(exp, MemberExpression).Member.Name = memberName) End Function Friend Shared Function GetValueFromEqualsExpression(ByVal be As BinaryExpression, _ ByVal memberDeclaringType As Type, _ ByVal memberName As String) As String If be.NodeType <> ExpressionType.Equal Then Throw New Exception("There is a bug in this program.") End If If be.Left.NodeType = ExpressionType.MemberAccess Then Dim mEx = CType(be.Left, MemberExpression) If mEx.Member.DeclaringType Is memberDeclaringType AndAlso _ mEx.Member.Name = memberName Then Return GetValueFromExpression(be.Right) End If ElseIf be.Right.NodeType = ExpressionType.MemberAccess Then Dim mEx = CType(be.Right, MemberExpression) If mEx.Member.DeclaringType Is memberDeclaringType AndAlso _ mEx.Member.Name = memberName Then Return GetValueFromExpression(be.Left) End If End If ' We should have returned by now. Throw New Exception("There is a bug in this program.") End Function Friend Shared Function GetValueFromExpression(ByVal expr As expression) As String If expr.NodeType = ExpressionType.Constant Then Return CStr(CType(expr, ConstantExpression).Value) Else Dim s = "The expression type {0} is not supported to obtain a value." Throw New InvalidQueryException(String.Format(s, expr.NodeType)) End If End Function End Class
using System; using System.Linq.Expressions; namespace LinqToTerraServerProvider { internal class ExpressionTreeHelpers { internal static bool IsMemberEqualsValueExpression(Expression exp, Type declaringType, string memberName) { if (exp.NodeType != ExpressionType.Equal) return false; BinaryExpression be = (BinaryExpression)exp; // Assert. if (ExpressionTreeHelpers.IsSpecificMemberExpression(be.Left, declaringType, memberName) && ExpressionTreeHelpers.IsSpecificMemberExpression(be.Right, declaringType, memberName)) throw new Exception("Cannot have 'member' == 'member' in an expression!"); return (ExpressionTreeHelpers.IsSpecificMemberExpression(be.Left, declaringType, memberName) || ExpressionTreeHelpers.IsSpecificMemberExpression(be.Right, declaringType, memberName)); } internal static bool IsSpecificMemberExpression(Expression exp, Type declaringType, string memberName) { return ((exp is MemberExpression) && (((MemberExpression)exp).Member.DeclaringType == declaringType) && (((MemberExpression)exp).Member.Name == memberName)); } internal static string GetValueFromEqualsExpression(BinaryExpression be, Type memberDeclaringType, string memberName) { if (be.NodeType != ExpressionType.Equal) throw new Exception("There is a bug in this program."); if (be.Left.NodeType == ExpressionType.MemberAccess) { MemberExpression me = (MemberExpression)be.Left; if (me.Member.DeclaringType == memberDeclaringType && me.Member.Name == memberName) { return GetValueFromExpression(be.Right); } } else if (be.Right.NodeType == ExpressionType.MemberAccess) { MemberExpression me = (MemberExpression)be.Right; if (me.Member.DeclaringType == memberDeclaringType && me.Member.Name == memberName) { return GetValueFromExpression(be.Left); } } // We should have returned by now. throw new Exception("There is a bug in this program."); } internal static string GetValueFromExpression(Expression expression) { if (expression.NodeType == ExpressionType.Constant) return (string)(((ConstantExpression)expression).Value); else throw new InvalidQueryException( String.Format("The expression type {0} is not supported to obtain a value.", expression.NodeType)); } } }
Questa classe contiene i metodi che possono essere utilizzati per determinarne le informazioni ed estrarre i dati dai tipi specifici di strutture ad albero dell'espressione. In questo provider tali metodi vengono utilizzati dalla classe LocationFinder per estrarre le informazioni sul percorso dalla struttura ad albero dell'espressione che rappresenta la query.
Per aggiungere un tipo di eccezione per le query non valide
Aggiungere la classe InvalidQueryException al progetto.
Public Class InvalidQueryException Inherits Exception Private _message As String Public Sub New(ByVal message As String) Me._message = message & " " End Sub Public Overrides ReadOnly Property Message() As String Get Return "The client query is invalid: " & _message End Get End Property End Class
using System; namespace LinqToTerraServerProvider { class InvalidQueryException : System.Exception { private string message; public InvalidQueryException(string message) { this.message = message + " "; } public override string Message { get { return "The client query is invalid: " + message; } } } }
Questa classe definisce un tipo Exception che il provider può generare quando non riconosce la query LINQ dal client. Definendo questo tipo di eccezione per le query non valide, il provider può generare un'eccezione più specifica rispetto a Exception da vari punti del codice.
A questo punto sono stati aggiunti tutti i componenti necessari per compilare il provider. Compilare il progetto LinqToTerraServerProvider e assicurarsi che non si verifichino errori di compilazione.
Test del provider LINQ
È possibile testare il provider LINQ creando un'applicazione client contenente una query LINQ sull'origine dati.
Per creare un'applicazione client per testare il provider
Aggiungere un nuovo progetto Applicazione console alla soluzione e denominarlo ClientApp.
Nel nuovo progetto aggiungere un riferimento all'assembly di provider.
Trascinare il file app.config dal progetto del provider al progetto client. Questo file è necessario per comunicare con il servizio Web.
Nota: In Visual Basic è necessario fare clic sul pulsante Mostra tutti i file per visualizzare il file app.config in Esplora soluzioni.
Aggiungere le istruzioni using seguenti (istruzioneImports in Visual Basic) al file Program.cs (o Module1.vb in Visual Basic):
using System; using System.Linq; using LinqToTerraServerProvider;
Imports LinqToTerraServerProvider
Nel metodo Main del file Program.cs (o Module1.vb in Visual Basic) inserire il codice seguente:
QueryableTerraServerData<Place> terraPlaces = new QueryableTerraServerData<Place>(); var query = from place in terraPlaces where place.Name == "Johannesburg" select place.PlaceType; foreach (PlaceType placeType in query) Console.WriteLine(placeType);
Dim terraPlaces As New QueryableTerraServerData(Of Place) Dim query = From place In terraPlaces _ Where place.Name = "Johannesburg" _ Select place.PlaceType For Each placeType In query Console.WriteLine(placeType.ToString()) Next
Questo codice crea una nuova istanza del tipo IQueryable<T> definito nel provider e quindi esegue una query sull'oggetto utilizzando LINQ. La query specifica un percorso per ottenere i dati utilizzando un'espressione di uguaglianza. Poiché l'origine dati implementa IQueryable, il compilatore converte la sintassi di espressione della query in chiamate agli operatori di query standard definiti in Queryable. Internamente, questi metodi degli operatori di query standard compilano una struttura ad albero dell'espressione e chiamano i metodi Execute o CreateQuery implementati come parte dell'implementazione IQueryProvider.
Compilare ClientApp.
Impostare questa applicazione client come progetto di avvio per la soluzione. In Esplora soluzioni fare clic con il pulsante destro del mouse sul progetto ClientApp e scegliere Imposta come progetto di avvio.
Eseguire il programma e visualizzare i risultati. Dovrebbero essere presenti approssimativamente tre risultati.
Aggiunta di funzionalità di query più complesse
Il provider disponibile a questo punto fornisce una modalità molto limitata per consentire ai client di specificare le informazioni sul percorso nella query LINQ. In particolare il provider è solo in grado di ottenere le informazioni sul percorso da espressioni di uguaglianza come Place.Name == "Seattle" o Place.State == "Alaska" (Place.Name = "Seattle" o Place.State = "Alaska" in Visual Basic).
Nella procedura successiva viene illustrato come aggiungere il supporto per una modalità aggiuntiva al fine di specificare le informazioni sul percorso. Dopo aver aggiunto questo codice, il provider sarà in grado di estrarre le informazioni sul percorso dalle espressioni della chiamata al metodo, ad esempio place.Name.StartsWith("Seat").
Per aggiungere il supporto per i predicati contenenti String.StartsWith
Nel progetto LinqToTerraServerProvider, aggiungere il metodo VisitMethodCall alla definizione della classe LocationFinder.
Protected Overrides Function VisitMethodCall(ByVal m As MethodCallExpression) As Expression If m.Method.DeclaringType Is GetType(String) And m.Method.Name = "StartsWith" Then If ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "Name") 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); }
Ricompilare il progetto LinqToTerraServerProvider.
Per testare la nuova funzionalità del provider, aprire il file Program.cs (o Module1.vb in Visual Basic) nel progetto ClientApp. Sostituire il codice nel metodo Main con il codice seguente:
QueryableTerraServerData<Place> terraPlaces = new QueryableTerraServerData<Place>(); var query = from place in terraPlaces where place.Name.StartsWith("Lond") select new { place.Name, place.State }; foreach (var obj in query) Console.WriteLine(obj);
Dim terraPlaces As New QueryableTerraServerData(Of Place) Dim query = From place In terraPlaces _ Where place.Name.StartsWith("Lond") _ Select place.Name, place.State For Each obj In query Console.WriteLine(obj) Next
Eseguire il programma e visualizzare i risultati. Dovrebbero essere presenti approssimativamente 29 risultati.
Nella procedura successiva viene illustrato come aggiungere la funzionalità al provider per consentire alla query client di specificare le informazioni sul percorso utilizzando due metodi aggiuntivi, in particolare Enumerable.Contains e List<T>.Contains. Dopo aver aggiunto questo codice, il provider sarà in grado di estrarre le informazioni sul percorso dalle espressioni della chiamata al metodo nella query client, ad esempio placeList.Contains(place.Name), dove l'insieme placeList è un elenco concreto fornito dal client. Il vantaggio di consentire ai client di utilizzare il metodo Contains è che possono specificare un numero qualsiasi di percorsi semplicemente aggiungendoli a placeList. Variando il numero di percorsi non si modifica la sintassi della query.
Per aggiungere il supporto per le query contenenti il metodo Contains nella relativa clausola 'where'
Nella definizione della classe LocationFinder del progetto LinqToTerraServerProvider sostituire il metodo VisitMethodCall con il codice seguente:
Protected Overrides Function VisitMethodCall(ByVal m As MethodCallExpression) As Expression If m.Method.DeclaringType Is GetType(String) And m.Method.Name = "StartsWith" Then If ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "Name") 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); }
Questo metodo aggiunge ogni stringa dell'insieme, al quale viene applicato Contains, all'elenco di percorsi in base a cui eseguire una query sul servizio Web. Un metodo denominato Contains viene definito in Enumerable e List<T>. Pertanto, il metodo VisitMethodCall deve verificare entrambi questi tipi dichiaranti. Enumerable.Contains viene definito come metodo di estensione, per cui l'insieme al quale viene applicato rappresenta in realtà il primo argomento al metodo. List.Contains viene definito come metodo di istanza, per cui l'insieme al quale viene applicato rappresenta l'oggetto ricevente del metodo.
Ricompilare il progetto LinqToTerraServerProvider.
Per testare la nuova funzionalità del provider, aprire il file Program.cs (o Module1.vb in Visual Basic) nel progetto ClientApp. Sostituire il codice nel metodo Main con il codice seguente:
QueryableTerraServerData<Place> terraPlaces = new QueryableTerraServerData<Place>(); string[] places = { "Johannesburg", "Yachats", "Seattle" }; var query = from place in terraPlaces where places.Contains(place.Name) orderby place.State select new { place.Name, place.State }; foreach (var obj in query) Console.WriteLine(obj);
Dim terraPlaces As New QueryableTerraServerData(Of Place) Dim places = New String() {"Johannesburg", "Yachats", "Seattle"} Dim query = From place In terraPlaces _ Where places.Contains(place.Name) _ Order By place.State _ Select place.Name, place.State For Each obj In query Console.WriteLine(obj) Next
Eseguire il programma e visualizzare i risultati. Dovrebbero essere presenti approssimativamente 5 risultati.
Passaggi successivi
In questo argomento della procedura dettagliata viene illustrato come creare un provider LINQ per un unico metodo di un servizio Web. Se si desidera procedere con lo sviluppo aggiuntivo di un provider LINQ, considerare queste possibilità:
Consentire al provider LINQ di gestire le altre modalità per specificare un percorso nella query client.
Esaminare gli altri metodi esposti dal servizio Web TerraServer-USA e creare un provider LINQ che si interfacci con uno di tali metodi.
Cercare un servizio Web diverso desiderato e crearvi un provider LINQ.
Creare un provider LINQ per un'origine dati diversa da un servizio Web.
Vedere anche
Attività
Esempio di provider LINQ to TerraServer
Procedura: implementare un visitatore della struttura ad albero dell'espressione
Procedura: modificare strutture ad albero dell'espressione
Concetti
Attivazione di un'origine dati per l'esecuzione di query LINQ
Riferimenti
Altre risorse
Servizi Windows Communication Foundation e ADO.NET Data Services
Cronologia delle modifiche
Date |
History |
Motivo |
---|---|---|
Luglio 2008 |
Aggiunto collegamento all'esempio TerraServer. |
Correzione di errori nel contenuto. |