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<(Of <(T>)>), IOrderedQueryable<(Of <(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.
Per completare questa procedura dettagliata, è necessario disporre dei seguenti componenti:
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. |
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<(Of <(T>)>) e IQueryProvider. IQueryable<(Of <(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<(Of <(T>)>). Poiché IOrderedQueryable<(Of <(T>)>) deriva da IQueryable<(Of <(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<(Of <(T>)>) mediante la classe QueryableTerraServerData implementa tre proprietà dichiarate in IQueryable e due metodi di enumerazione dichiarati in IEnumerable e IEnumerable<(Of <(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<(Of <(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<(Of <(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
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.
È 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<(Of <(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<(Of <(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<(Of <(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.
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.
Attività
Concetti
Riferimenti
Altre risorse
Cronologia delle modifiche
Date | History | Motivo |
|---|
Luglio 2008 | Aggiunto collegamento all'esempio TerraServer. | Correzione di errori nel contenuto. |