Exemplarische Vorgehensweise: Erstellen eines IQueryable-LINQ-Anbieters

Aktualisiert: Juli 2008

Dieses weiterführende Thema bietet schrittweise Anleitungen zum Erstellen eines benutzerdefinierten LINQ-Anbieters. Nach Abschluss sind Sie in der Lage, den von Ihnen erstellten Anbieter zum Schreiben von LINQ-Abfragen für den TerraServer-USA-Webdienst zu verwenden.

Der TerraServer-USA-Webdienst bietet eine Benutzeroberfläche für den Zugriff auf eine Datenbank mit Luftaufnahmen der USA. Außerdem macht der Webdienst eine Methode verfügbar, die bei teilweiser oder vollständiger Angabe der Adressdaten Informationen zu Orten in den USA zurückgibt. Diese Methode mit der Bezeichnung GetPlaceList wird von Ihrem LINQ-Anbieter aufgerufen. Der Anbieter verwendet Windows Communication Foundation (WCF) zum Kommunizieren mit dem Webdienst. Weitere Informationen über den TerraServer-USA-Webdienst finden Sie unter Overview of the TerraServer-USA Web Services.

Dieser Anbieter ist ein relativ einfacher IQueryable-Anbieter. Er erwartet, dass in den von ihm verarbeiteten Abfragen spezifische Informationen enthalten sind und verfügt über ein geschlossenes Typsystem, wodurch ein einzelner Typ zur Darstellung der Ergebnisdaten verfügbar gemacht wird. Dieser Anbieter überprüft nur einen Ausdruckstyp des Methodenaufrufs in der Ausdrucksbaumstruktur, die die Abfrage darstellt, und zwar den innersten Aufruf von Where. Der Anbieter extrahiert die zum Abfragen des Webdiensts erforderlichen Daten aus diesem Ausdruck. Anschließend ruft er den Webdienst auf und fügt die zurückgegebenen Daten an der Stelle der ursprünglichen IQueryable-Datenquelle in die Ausdrucksbaumstruktur ein. Die restliche Abfrageverarbeitung wird von den Enumerable-Implementierungen der Standardabfrageoperatoren übernommen.

Die Codebeispiele in diesem Thema werden in C# und Visual Basic bereitgestellt.

In dieser exemplarischen Vorgehensweise werden die folgenden Aufgaben veranschaulicht:

  • Erstellen des Projekts in Visual Studio

  • Implementieren der für einen IQueryable LINQ-Anbieter erforderlichen Schnittstellen: IQueryable<T>IOrderedQueryable<T> und IQueryProvider.

  • Hinzufügen eines benutzerdefinierten .NET-Typs, um die Daten aus dem Webdienst darzustellen.

  • Erstellen einer Abfragekontextklasse und einer Klasse, die Daten aus dem Webdienst abruft.

  • Erstellen einer Besucherunterklasse für die Ausdrucksbaumstruktur, durch die der Ausdruck ermittelt wird, der den innersten Aufruf der Queryable.Where-Methode darstellt.

  • Erstellen einer Besucherunterklasse für die Ausdrucksbaumstruktur, durch die Informationen aus der LINQ-Abfrage extrahiert werden, die in der Webdienstanforderung verwendet werden soll.

  • Erstellen einer Besucherunterklasse für die Ausdrucksbaumstruktur, durch die die Ausdrucksbaumstruktur geändert wird, die die gesamte LINQ-Abfrage darstellt.

  • Verwenden einer Auswerterklasse, um eine Ausdrucksbaumstruktur teilweise auszuwerten. Dieser Schritt ist notwendig, da hierdurch alle lokalen Variablenverweise in der LINQ-Abfrage in Werte übersetzt werden.

  • Erstellen einer Hilfsklasse der Ausdrucksbaumstruktur und einer neuen Ausnahmeklasse.

  • Testen des LINQ-Anbieters von einer Clientanwendung aus, die eine LINQ-Abfrage enthält.

  • Erweitern des LINQ-Anbieters durch komplexere Abfragefunktionen.

    Hinweis:

    Der LINQ-Anbieter, der diese exemplarische Vorgehensweise erstellt, ist als Beispiel verfügbar. Weitere Informationen finden Sie unter Beispiel eines LINQ to TerraServer-Anbieters.

Vorbereitungsmaßnahmen

Zum Durchführen dieser exemplarischen Vorgehensweise benötigen Sie die folgenden Komponenten:

  • Visual Studio 2008
Hinweis:

Ihr Computer zeigt möglicherweise für einige der Elemente der Visual Studio-Benutzeroberfläche in der folgenden Anleitung andere Namen oder Standorte an. Diese Elemente sind von der jeweiligen Visual Studio-Version und den verwendeten Einstellungen abhängig. Weitere Informationen finden Sie unter Visual Studio-Einstellungen.

Erstellen des Projekts

So erstellen Sie das Projekt in Visual Studio

  1. Erstellen Sie in Visual Studio eine neue Anwendung in Form einer Klassenbibliothek. Nennen Sie das Projekt LinqToTerraServerProvider.

  2. Wählen Sie im Projektmappen-Explorer die Datei Class1.cs (oder Class1.vb) aus, und benennen Sie sie in QueryableTerraServerData.cs (oder QueryableTerraServerData.vb) um. Klicken Sie im daraufhin geöffneten Dialogfeld auf Ja, um alle Verweise auf das Codeelement umzubenennen.

    Sie erstellen den Anbieter in Visual Studio als Klassenbibliotheksprojekt, da die Anbieterassembly von ausführbaren Clientanwendungen als Verweis auf deren Projekt hinzugefügt wird.

So fügen Sie einen Dienstverweis auf den Webdienst hinzu

  1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Projekt LinqToTerraServerProvider, und klicken Sie auf Dienstverweis hinzufügen.

    Das Dialogfeld Dienstverweis hinzufügen wird geöffnet.

  2. Geben Sie im Feld Adressehttp://terraserver.microsoft.com/TerraService2.asmx ein.

  3. Geben Sie im Feld NamespaceTerraServerReference ein, und klicken Sie auf OK.

    Der TerraServer-USA-Webdienst wird als Dienstverweis hinzugefügt, sodass die Anwendung über Windows Communication Foundation (WCF) mit dem Webdienst kommunizieren kann. Durch das Hinzufügen eines Dienstverweises zum Projekt wird von Visual Studio eine Datei app.config generiert, die einen Proxy und einen Endpunkt für den Webdienst enthält. Weitere Informationen finden Sie unter Einführung in Windows Communication Foundation-Dienste in Visual Studio.

Sie verfügen jetzt über ein Projekt mit einer Datei namens app.config, einer Datei namens QueryableTerraServerData.cs (bzw. QueryableTerraServerData.vb) und einem Dienstverweis mit dem Namen TerraServerReference.

Implementieren der erforderlichen Schnittstellen

Zum Erstellen eines LINQ-Anbieters müssen mindestens die IQueryable<T>-Schnittstelle und die IQueryProvider-Schnittstelle implementiert werden. Da IQueryable<T> und IQueryProvider von den anderen erforderlichen Schnittstellen abgeleitet werden, implementieren Sie durch die beiden oben genannten Schnittstellen zusätzlich die anderen Schnittstellen, die für einen LINQ-Anbieter erforderlich sind.

Wenn die Sortierung von Abfrageoperatoren wie OrderBy und ThenBy unterstützt werden soll, müssen Sie außerdem die IOrderedQueryable<T>-Schnittstelle implementieren. Da IOrderedQueryable<T> von IQueryable<T> abgeleitet wird, können Sie beide Schnittstellen in einem Typ implementieren, was dieser Anbieter tut.

So implementieren Sie System.Linq.IQueryable'1 und System.Linq.IOrderedQueryable'1

  • Fügen Sie in der Datei QueryableTerraServerData.cs (oder QueryableTerraServerData.vb) den folgenden Code hinzu.

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

    Durch die IOrderedQueryable<T>-Implementierung der QueryableTerraServerData-Klasse werden drei in IQueryable deklarierte Eigenschaften und zwei in IEnumerable und IEnumerable<T> deklarierte Enumerationsmethoden implementiert.

    Diese Klasse verfügt über zwei Konstruktoren. Der erste Konstruktor wird von der Clientanwendung zum Erstellen des Objekts aufgerufen, für das die LINQ-Abfrage geschrieben werden soll. Der zweite Konstruktor wird durch den Code in der IQueryProvider-Implementierung intern für die Anbieterbibliothek aufgerufen.

    Wenn die GetEnumerator-Methode für ein Objekt mit dem Typ QueryableTerraServerData aufgerufen wird, wird die von ihm dargestellte Abfrage ausgeführt, und die Ergebnisse der Abfrage werden aufgelistet.

    Mit Ausnahme des Klassennamens ist dieser Code nicht spezifisch für diesen TerraServer-USA-Webdienstanbieter. Deshalb kann er für beliebige LINQ-Anbieter wiederverwendet werden.

So implementieren Sie System.Linq.IQueryProvider

  • Fügen Sie dem Projekt die TerraServerQueryProvider-Klasse hinzu.

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

    Der Abfrageanbietercode in dieser Klasse implementiert die vier Methoden, die zum Implementieren der IQueryProvider-Schnittstelle erforderlich sind. Durch die beiden CreateQuery-Methoden werden Abfragen erstellt, die der Datenquelle zugeordnet sind. Die beiden Execute-Methoden senden diese Abfragen zur Ausführung weiter.

    Die nicht generische CreateQuery-Methode verwendet Reflektion, um den Elementtyp der Sequenz abzurufen, den die Abfrage, von der sie erstellt wird, bei deren Ausführung zurückgeben würde. Anschließend wird mithilfe der Activator-Klasse eine neue QueryableTerraServerData-Instanz mit dem Elementtyp als generischem Typargument erstellt. Das Ergebnis des Aufrufs der nicht generischen CreateQuery-Methode ist identisch mit dem Ergebnis, das durch den Aufruf der generischen CreateQuery-Methode mit dem richtigen Typargument erzielt würde.

    Die meiste Logik der Abfrageausführung wird in einer anderen Klasse behandelt, die Sie später hinzufügen. Diese Funktionalität wird an anderer Stelle behandelt, da sie sich spezifisch auf die abgefragte Datenquelle bezieht, während der Code in dieser Klasse generisch für jeden LINQ-Anbieter gilt. Um diesen Code für einen anderen Anbieter zu verwenden, müssten Sie den Namen der Klasse und den Namen des Abfragekontexttyps ändern, auf den in zwei der Methoden verwiesen wird.

Hinzufügen eines benutzerdefinierten Typs zur Darstellung der Ergebnisdaten

Sie benötigen einen .NET-Typ, um die Daten darzustellen, die aus dem Webdienst abgerufen werden. Dieser Typ wird in der LINQ-Clientabfrage verwendet, um die gewünschten Ergebnisse zu definieren. Im folgenden Verfahren wird ein solcher Typ erstellt. Dieser Typ mit dem Namen Place enthält Informationen über einen einzelnen geografischen Ort wie eine Stadt, einen Park oder einen See.

Außerdem enthält dieser Code einen Enumerationstyp mit der Bezeichnung PlaceType, der die verschiedenen Arten geografischer Standorte definiert und in der Place-Klasse verwendet wird.

So erstellen Sie einen benutzerdefinierten Ergebnistyp

  • Fügen Sie dem Projekt die Place-Klasse und die PlaceType-Enumeration hinzu.

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

    Das Erstellen eines Ergebnisobjekts aus dem vom Webdienst zurückgegebenen Typ wird durch den Konstruktor für den Place-Typ vereinfacht. Während der Anbieter den von der Webdienst-API definierten Ergebnistyp direkt zurückgeben kann, müssten Clientanwendungen dazu einen Verweis auf den Webdienst hinzufügen. Wenn ein neuer Typ als Teil der Anbieterbibliothek erstellt wird, ist es nicht erforderlich, dass dem Client die vom Webdienst verfügbar gemachten Typen und Methoden bekannt sind.

Hinzufügen von Funktionen zum Abrufen von Daten aus der Datenquelle

Bei dieser Anbieterimplementierung wird davon ausgegangen, dass der innerste Aufruf von Queryable.Where die Standortinformationen enthält, die zum Abfragen des Webdiensts verwendet werden sollen. Bei dem innersten Queryable.Where-Aufruf handelt es sich um die where-Klausel (Where-Klausel in Visual Basic) oder den Queryable.Where-Methodenaufruf, der zuerst in einer LINQ-Abfrage auftritt oder um denjenigen, der sich am nächsten zur Basis der Ausdrucksbaumstruktur befindet, die die Abfrage darstellt.

So erstellen Sie eine Abfragekontextklasse

  • Fügen Sie dem Projekt die TerraServerQueryContext-Klasse hinzu.

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

    In dieser Klasse sind die Aufgaben zum Ausführen einer Abfrage organisiert. Nachdem der Ausdruck ermittelt wurde, der den innersten Queryable.Where-Aufruf darstellt, ruft dieser Code den lambda-Ausdruck ab, der das an Queryable.Where übergebene Prädikat darstellt. Anschließend wird der Prädikatausdruck für eine Teilauswertung an eine Methode übergeben, damit alle Verweise auf lokale Variablen in Werte übersetzt werden. Dann wird eine Methode zum Extrahieren der angeforderten Standorte aus dem Prädikat und eine weitere Methode zum Abrufen der Ergebnisdaten aus dem Webdienst aufgerufen.

    Im nächsten Schritt wird die Ausdrucksbaumstruktur, die die LINQ-Abfrage darstellt, durch diesen Code kopiert und eine Änderung an der Ausdrucksbaumstruktur vorgenommen. Im Code wird eine Besucherunterklasse für die Ausdrucksbaumstruktur verwendet, um die Datenquelle, auf die der innerste Abfrageoperatoraufruf angewendet wird, durch die konkrete Liste von Place-Objekten zu ersetzen, die vom Webdienst abgerufen wurden.

    Bevor die Liste der Place-Objekte in die Ausdrucksbaumstruktur eingefügt wird, wird der Listentyp durch den Aufruf von AsQueryable von IEnumerable in IQueryable geändert. Diese Typänderung ist erforderlich, da der Knoten, der den Methodenaufruf für die innerste Abfrageoperatormethode darstellt, neu erstellt wird, wenn die Ausdrucksbaumstruktur neu geschrieben wird. Der Knoten wird neu erstellt, da eines der Argumente geändert wurde (und zwar die Datenquelle, auf die es angewendet wird). Die zum Neuerstellen des Knotens verwendete Call(Expression, MethodInfo, IEnumerable<Expression>)-Methode löst eine Ausnahme aus, wenn eines der Argumente keinem entsprechenden Parameter der Methode zugewiesen werden kann, an die es übergeben wird. In diesem Fall könnte die IEnumerable-Liste mit Place-Objekten nicht dem IQueryable-Parameter von Queryable.Where zugewiesen werden. Aus diesem Grund wird ihr Typ in IQueryable geändert.

    Durch die Änderung des Typs in IQueryable erhält die Auflistung zusätzlich einen IQueryProvider-Member, auf den durch die Provider-Eigenschaft zugegriffen wird, von der Abfragen erstellt oder ausgeführt werden können. Der dynamische Typ der IQueryable°Place-Auflistung entspricht EnumerableQuery, also einem für die System.Linq-API internen Typ. Der diesem Typ zugeordnete Abfrageanbieter führt Abfragen aus, indem Aufrufe von Queryable-Standardabfrageoperatoren durch die entsprechenden Enumerable-Operatoren ersetzt werden, wodurch die Abfrage tatsächlich zu einer LINQ to Objects-Abfrage wird.

    Durch den endgültigen Code in der TerraServerQueryContext-Klasse wird eine der beiden Methoden in der IQueryable-Liste mit Place-Objekten aufgerufen. So wird CreateQuery aufgerufen, wenn die Clientabfrage auflistbare Ergebnisse zurückgibt, oder Execute, wenn die Clientabfrage ein Ergebnis zurückgibt, das nicht aufgelistet werden kann.

    Der Code in dieser Klasse ist äußerst spezifisch für diesen TerraServer-USA-Anbieter. Daher wird er in der TerraServerQueryContext-Klasse gekapselt und nicht direkt in die generischere IQueryProvider-Implementierung eingefügt.

Der von Ihnen erstellte Anbieter benötigt zur Abfrage des Webdiensts lediglich die im Queryable.Where-Prädikat enthaltenen Informationen. Daher verwendet er LINQ to Objects, um die LINQ-Abfrage unter Verwendung des internen EnumerableQuery-Typs auszuführen. Eine andere Möglichkeit, LINQ to Objects zum Ausführen der Abfrage zu verwenden, besteht darin, den Teil der Abfrage, die von LINQ to Objects ausgeführt werden soll, vom Client von einer LINQ to Objects-Abfrage umschließen zu lassen. Dies wird durch den Aufruf von AsEnumerable<TSource> für den Rest der Abfrage erreicht, der den Teil der Abfrage darstellt, den der Anbieter für seine speziellen Zwecke benötigt. Der Vorteil dieser Art der Implementierung besteht darin, dass die Arbeitsteilung zwischen dem benutzerdefinierten Anbieter und LINQ to Objects transparenter ist.

Hinweis:

Der in diesem Thema vorgestellte Anbieter ist ein einfaches Modell, das selbst eine sehr geringe Abfrageunterstützung bietet. Daher ist der Anbieter bei der Ausführung von Abfragen in hohem Maße von LINQ to Objects abhängig. Ein komplexer LINQ-Anbieter wie LINQ to SQL unterstützt u. U. die gesamte Abfrage, ohne Aufgaben auf LINQ to Objects übertragen zu müssen.

So erstellen Sie eine Klasse zum Abrufen von Daten aus dem Webdienst

  • Fügen Sie dem Projekt die WebServiceHelper-Klasse (bzw. das entsprechende Modul in Visual Basic) hinzu.

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

    Diese Klasse enthält die Funktionen zum Abrufen von Daten aus dem Webdienst. In diesem Code wird ein Typ mit dem Namen TerraServiceSoapClient verwendet, der von Windows Communication Foundation (WCF) automatisch für das Projekt generiert, um die Webdienstmethode GetPlaceList aufzurufen. Anschließend wird jedes Ergebnis vom Rückgabetyp der Webdienstmethode in den vom Anbieter für Daten definierten .NET-Typ übersetzt.

    Dieser Code enthält zwei Überprüfungen, die die Verwendbarkeit der Anbieterbibliothek verbessern. Durch die erste Überprüfung wird die maximale Dauer beschränkt, die eine Clientanwendung auf eine Antwort wartet, indem die Gesamtanzahl der pro Abfrage an den Webdienst gesendeten Aufrufe auf fünf Aufrufe begrenzt wird. Für jeden in der Clientabfrage angegebenen Standort, wird eine Webdienstanforderung generiert. Deshalb löst der Anbieter eine Ausnahme aus, wenn die Abfrage mehr als fünf Standorte enthält.

    Mit der zweiten Überprüfung wird festgestellt, ob die Anzahl der vom Webdienst zurückgegebenen Ergebnisse der maximalen Anzahl von Ergebnissen entspricht, die der Dienst zurückgeben kann. Wenn die Anzahl der Ergebnisse der maximalen Anzahl entspricht, besteht die Wahrscheinlichkeit, dass die Ergebnisse des Webdiensts abgeschnitten sind. Statt eine unvollständige Liste an den Client zurückzugeben, löst der Anbieter eine Ausnahme aus.

Hinzufügen der Besucherklassen für die Ausdrucksbaumstruktur

So erstellen Sie den Besucher, mit dem der innerste Ausdruck des Where-Methodenaufrufs ermittelt wird

  1. Fügen Sie dem Projekt die ExpressionVisitor-Klasse hinzu. Dieser Code ist unter Gewusst wie: Implementieren eines Ausdrucksbaumstruktur-Besuchers verfügbar. Fügen Sie der Datei für die folgenden Namespaces using-Direktiven (Imports-Anweisungen in Visual Basic) hinzu: System.Collections.Generic, System.Collections.ObjectModel und System.Linq.Expressions.

  2. Fügen Sie dem Projekt die InnermostWhereFinder-Klasse hinzu, die die ExpressionVisitor-Klasse erbt.

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

    Diese Klasse erbt die Besucherbasisklasse der Ausdrucksbaumstruktur, damit die Funktionen zum Suchen eines bestimmten Ausdrucks ausgeführt werden können. Die Besucherbasisklasse der Ausdrucksbaumstruktur ist so konzipiert, dass sie geerbt und für eine bestimmte Aufgabe spezialisiert wird, die das Durchlaufen einer Ausdrucksbaumstruktur beinhaltet. Die abgeleitete Klasse überschreibt die VisitMethodCall-Methode, um den Ausdruck zu ermitteln, der in der Ausdrucksbaumstruktur, die die Clientabfrage darstellt, dem innersten Aufruf von Where entspricht. Dieser innerste Ausdruck ist der Ausdruck, aus dem der Anbieter die gesuchten Standorte extrahiert.

So erstellen Sie den Besucher, der Daten zum Abfragen des Webdiensts extrahiert

  • Fügen Sie dem Projekt die LocationFinder-Klasse hinzu.

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

    Diese Klasse wird verwendet, um Standortinformationen aus dem Prädikat zu extrahieren, das der Client an Queryable.Where übergibt. Sie wird von der Besucherbasisklasse der Ausdrucksbaumstruktur abgeleitet und überschreibt nur die VisitBinary-Methode.

    Die Besucherbasisklasse der Ausdrucksbaumstruktur sendet binäre Ausdrücke, z. B. Gleichheitsausdrücke wie place.Name == "Seattle" (place.Name = "Seattle" in Visual Basic), an die VisitBinary-Methode. Wenn der Ausdruck in dieser überschriebenen VisitBinary-Methode mit dem Muster des Gleichheitsausdrucks übereinstimmt, das Standortinformationen bereitstellen kann, werden diese Informationen extrahiert und in einer Standortliste gespeichert.

    Diese Klasse verwendet einen Ausdrucksbaumstruktur-Besucher, um die Standortinformationen in der Ausdrucksbaumstruktur zu suchen, da ein Besucher für das Durchlaufen und Überprüfen von Ausdrucksbaumstrukturen konzipiert ist. Der resultierende Code ist übersichtlicher und weniger fehleranfällig als Code, der ohne Verwendung des Besuchers implementiert worden wäre.

    An diesem Punkt der exemplarischen Vorgehensweise bietet der Anbieter nur eingeschränkte Möglichkeiten zum Bereitstellen von Standortinformationen in der Abfrage. Später in diesem Thema fügen Sie Funktionen hinzu, um weitere Möglichkeiten zum Bereitstellen von Standortinformationen zu schaffen.

So erstellen Sie den Besucher, durch den die Ausdrucksbaumstruktur geändert wird

  • Fügen Sie dem Projekt die ExpressionTreeModifier-Klasse hinzu.

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

    Diese Klasse wird von der Besucherbasisklasse der Ausdrucksbaumstruktur abgeleitet und überschreibt die VisitConstant-Methode. In dieser Methode ersetzt sie das Objekt, auf das der Aufruf des innersten Standardabfrageoperators angewendet wird, durch eine konkrete Liste von Place-Objekten.

    Die CopyAndModify-Methode ruft die Basisklassenimplementierung der Visit-Methode auf. Diese CopyAndModify-Methode ist erforderlich, da die Visit-Methode, die mit protected (Protected in Visual Basic) gekennzeichnet ist, nicht direkt von der Abfragekontextklasse aufgerufen werden kann.

    Diese Modifiziererklasse der Ausdrucksbaumstruktur verwendet den Ausdrucksbaumstruktur-Besucher, da der Besucher für das Durchlaufen, Überprüfen und Kopieren von Ausdrucksbaumstrukturen konzipiert ist. Zum Ausführen der Funktionen dieser Klasse wird äußerst wenig Code benötigt, da von der Besucherbasisklasse der Ausdrucksbaumstruktur abgeleitet wird.

Hinzufügen der Ausdrucksauswertung

Das an die Queryable.Where-Methode in der Clientabfrage übergebene Prädikat kann Teilausdrücke enthalten, die nicht vom Parameter des lambda-Ausdrucks abhängig sind. Diese isolierten Teilausdrücke können und sollten unverzüglich ausgewertet werden. Sie könnten Verweise auf lokale Variablen oder Membervariablen darstellen, die in Werte übersetzt werden müssen.

Die nächste Klasse macht die Methode PartialEval(Expression) verfügbar, mit der bestimmt wird, welche (sofern vorhanden) Teilstrukturen im Ausdruck sofort ausgewertet werden können. Anschließend werden diese Ausdrücke ausgewertet, indem ein lambda-Ausdruck erstellt, kompiliert und der zurückgegebene Delegat aufgerufen wird. Schließlich wird die Teilstruktur durch einen neuen Knoten ersetzt, der einen konstanten Wert darstellt. Dies wird als teilweise Auswertung bezeichnet.

So fügen Sie eine Klasse hinzu, um eine teilweise Auswertung einer Ausdrucksbaumstruktur auszuführen

  • Fügen Sie dem Projekt die Evaluator-Klasse hinzu.

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

Hinzufügen der Hilfsklassen

Dieser Abschnitt enthält Code für drei Hilfsklassen für den Anbieter.

So fügen Sie die Hilfsklasse hinzu, die von der System.Linq.IQueryProvider-Implementierung verwendet wird

  • Fügen Sie dem Projekt die TypeSystem-Klasse (bzw. das entsprechende Modul in Visual Basic) hinzu.

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

    Die IQueryProvider-Implementierung, die Sie zuvor hinzugefügt haben, verwendet diese Hilfsklasse.

    TypeSystem.GetElementType verwendet Reflektion, um das generische Typargument von einer IEnumerable<T>-Auflistung (IEnumerable(Of T) in Visual Basic) abzurufen. Diese Methode wird von der nicht generischen CreateQuery-Methode in der Implementierung des Abfrageanbieters aufgerufen, um den Elementtyp der Abfrageergebnisauflistung bereitzustellen.

    Diese Hilfsklasse ist für diesen TerraServer-USA-Webdienstanbieter nicht spezifisch. Deshalb kann er für beliebige LINQ-Anbieter wiederverwendet werden.

So erstellen Sie eine Hilfsklasse für die Ausdrucksbaumstruktur

  • Fügen Sie dem Projekt die ExpressionTreeHelpers-Klasse hinzu.

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

    Diese Klasse enthält Methoden, mit deren Hilfe Informationen über bestimmte Typen von Ausdrucksbaumstrukturen ermittelt und Daten aus bestimmten Typen von Ausdrucksbaumstrukturen extrahiert werden können. In diesem Anbieter werden diese Methoden von der LocationFinder-Klasse verwendet, um Standortinformationen aus der Ausdrucksbaumstruktur zu extrahieren, die die Abfrage darstellt.

So fügen Sie einen Ausnahmetyp für ungültige Abfragen hinzu

  • Fügen Sie dem Projekt die InvalidQueryException-Klasse hinzu.

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

    Diese Klasse definiert einen Exception-Typ, den der Anbieter auslösen kann, wenn er eine LINQ-Abfrage vom Client nicht interpretieren kann. Durch die Definition dieses Ausnahmetyps für ungültige Abfragen kann der Anbieter eine spezifischere Ausnahme als Exception an verschiedenen Stellen im Code auslösen.

Sie haben jetzt alle Bestandteile hinzugefügt, die zum Kompilieren des Anbieters erforderlich sind. Erstellen Sie das LinqToTerraServerProvider-Projekt, und stellen Sie sicher, dass keine Compilerfehler auftreten.

Testen des LINQ-Anbieters

Sie können den LINQ-Anbieter testen, indem Sie eine Clientanwendung erstellen, die eine LINQ-Abfrage für Ihre Datenquelle enthält.

So erstellen Sie eine Clientanwendung zum Testen des Anbieters

  1. Fügen Sie der Projektmappe ein neues Projekt vom Typ Konsolenanwendung hinzu, und nennen Sie es ClientApp.

  2. Fügen Sie im neuen Projekt einen Verweis auf die Anbieterassembly hinzu.

  3. Ziehen Sie die Datei app.config vom Anbieterprojekt in das Clientprojekt. (Diese Datei ist für die Kommunikation mit dem Webdienst erforderlich.)

    Hinweis:

    Möglicherweise müssen Sie in Visual Basic auf die Schaltfläche Alle Dateien anzeigen klicken, damit die Datei app.config im Projektmappen-Explorer angezeigt wird.

  4. Fügen Sie die folgenden using-Anweisungen (bzw. die Imports-Anweisung in Visual Basic) zur Datei Program.cs (oder Module1.vb in Visual Basic) hinzu:

    using System;
    using System.Linq;
    using LinqToTerraServerProvider;
    
    Imports LinqToTerraServerProvider
    
  5. Fügen Sie in der Main-Methode in der Datei Program.cs (oder Module1.vb in Visual Basic) folgenden Code ein:

    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
    

    Durch diesen Code wird eine neue Instanz des im Anbieter definierten IQueryable<T>-Typs erstellt und anschließend das Objekt mithilfe von LINQ abgefragt. In der Abfrage wird unter Verwendung eines Gleichheitsausdrucks ein Standort angegeben, zu dem Daten abgerufen werden sollen. Da die Datenquelle IQueryable implementiert, übersetzt der Compiler die Abfrageausdruckssyntax in Aufrufen der Standardabfrageoperatoren, die in Queryable definiert sind. Intern bilden diese Standardabfrageoperator-Methoden eine Ausdrucksbaumstruktur und rufen die Execute-Methode oder die CreateQuery-Methode auf, die Sie als Teil der IQueryProvider-Implementierung implementiert haben.

  6. Erstellen Sie ClientApp.

  7. Legen Sie diese Clientanwendung als "Start"-Projekt für die Projektmappe fest. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Projekt ClientApp, und wählen Sie Als Startprojekt festlegen aus.

  8. Führen Sie das Programm aus, und lassen Sie die Ergebnisse anzeigen. Ungefähr drei Ergebnisse sollten ausgegeben werden.

Hinzufügen komplexerer Abfragefunktionen

Der bisher erstellte Anbieter bietet Clients sehr eingeschränkte Möglichkeiten, um Standortinformationen in der LINQ-Abfrage anzugeben. Außerdem ist der Anbieter nur in der Lage, Standortinformationen mithilfe von Gleichheitsausdrücken wie Place.Name == "Seattle" oder Place.State == "Alaska" (Place.Name = "Seattle" oder Place.State = "Alaska" in Visual Basic) abzurufen.

Das nächste Verfahren zeigt, wie ein zusätzlicher Weg zum Festlegen von Standortinformationen unterstützt werden kann. Nachdem Sie diesen Code hinzugefügt haben, ist der Anbieter in der Lage, Standortinformationen aus Methodenaufrufausdrücken wie place.Name.StartsWith("Seat") zu extrahieren.

So fügen Sie Unterstützung für Prädikate hinzu, die String.StartsWith enthalten

  1. Fügen Sie im LinqToTerraServerProvider-Projekt der LocationFinder-Klassendefinition die VisitMethodCall-Methode hinzu.

    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);
    }
    
  2. Kompilieren Sie das LinqToTerraServerProvider-Projekt neu.

  3. Um die neue Fähigkeit des Anbieters zu testen, öffnen Sie die Datei Program.cs (bzw. Module1.vb in Visual Basic) im ClientApp-Projekt. Ersetzen Sie den Code in der Main-Methode durch den folgenden Code:

    QueryableTerraServerData<Place> terraPlaces = new QueryableTerraServerData<Place>();
    
    var query = from place in terraPlaces
                where place.Name.StartsWith("Lond")
                select new { place.Name, place.State };
    
    foreach (var obj in query)
        Console.WriteLine(obj);
    
    Dim terraPlaces As New QueryableTerraServerData(Of Place)
    
    Dim query = From place In terraPlaces _
                Where place.Name.StartsWith("Lond") _
                Select place.Name, place.State
    
    For Each obj In query
        Console.WriteLine(obj)
    Next
    
  4. Führen Sie das Programm aus, und lassen Sie die Ergebnisse anzeigen. Ungefähr 29 Ergebnisse sollten ausgegeben werden.

Im nächsten Verfahren erfahren Sie, wie Sie dem Anbieter Funktionen hinzufügen, die es ermöglichen, dass in der Clientabfrage Standortinformationen unter Verwendung der beiden zusätzlichen Methoden Enumerable.Contains und List<T>.Contains festgelegt werden. Nachdem Sie diesen Code hinzugefügt haben, ist der Anbieter in der Lage, Standortinformationen aus Methodenaufrufausdrücken in der Clientabfrage wie placeList.Contains(place.Name) zu extrahieren, wobei die placeList-Auflistung eine konkrete, vom Client bereitgestellte Liste ist. Der Vorteil, dass Clients die Contains-Methode verwenden können, besteht darin, dass sie eine beliebige Anzahl von Standorten angeben können, indem diese einfach placeList hinzugefügt werden. Die Syntax der Abfrage wird durch eine variierenden Anzahl der Standorte nicht geändert.

So fügen Sie Unterstützung für Abfragen hinzu, in deren 'where'-Klausel die Contains-Methode enthalten ist

  1. Ersetzen Sie im LinqToTerraServerProvider-Projekt in der LocationFinder-Klassendefinition die VisitMethodCall-Methode durch den folgenden Code:

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

    Durch diese Methode wird der Liste der Standorte, die für die Webdienstabfrage verwendet werden sollen, die einzelnen Zeichenfolgen der Auflistung hinzugefügt, auf die Contains angewendet wurde. Eine Methode mit dem Namen Contains wird sowohl in Enumerable als auch in List<T> definiert. Daher muss die VisitMethodCall-Methode eine Prüfung auf beide dieser Deklarationstypen ausführen. Da Enumerable.Contains als Erweiterungsmethode definiert wird, handelt es sich bei der Auflistung, auf die sie angewendet wird, tatsächlich um das erste Argument für die Methode. Da List.Contains als Instanzenmethode definiert wird, handelt es sich bei der Auflistung, auf die sie angewendet wird, um das empfangende Objekt der Methode.

  2. Kompilieren Sie das LinqToTerraServerProvider-Projekt neu.

  3. Um die neue Fähigkeit des Anbieters zu testen, öffnen Sie die Datei Program.cs (bzw. Module1.vb in Visual Basic) im ClientApp-Projekt. Ersetzen Sie den Code in der Main-Methode durch den folgenden Code:

    QueryableTerraServerData<Place> terraPlaces = new QueryableTerraServerData<Place>();
    
    string[] places = { "Johannesburg", "Yachats", "Seattle" };
    
    var query = from place in terraPlaces
                where places.Contains(place.Name)
                orderby place.State
                select new { place.Name, place.State };
    
    foreach (var obj in query)
        Console.WriteLine(obj);
    
    Dim terraPlaces As New QueryableTerraServerData(Of Place)
    
    Dim places = New String() {"Johannesburg", "Yachats", "Seattle"}
    
    Dim query = From place In terraPlaces _
                Where places.Contains(place.Name) _
                Order By place.State _
                Select place.Name, place.State
    
    For Each obj In query
        Console.WriteLine(obj)
    Next
    
  4. Führen Sie das Programm aus, und lassen Sie die Ergebnisse anzeigen. Ungefähr fünf Ergebnisse sollten ausgegeben werden.

Nächste Schritte

In dieser exemplarischen Vorgehensweise wurde erläutert, wie ein LINQ-Anbieter für eine Methode eines Webdiensts erstellt wird. Wenn Sie einen LINQ-Anbieter noch weiter entwickeln möchten, sollten Sie folgende Möglichkeiten in Erwägung ziehen:

  • Sie können es dem LINQ-Anbieter ermöglichen, andere Verfahren zum Festlegen eines Standorts in der Clientabfrage zu verwenden.

  • Informieren Sie sich über die anderen vom TerraServer-USA-Webdienst verfügbar gemachten Methoden, und erstellen Sie einen LINQ-Anbieter, der mit einer dieser Methoden verbunden werden kann.

  • Suchen Sie einen anderen für Sie interessanten Webdienst, und erstellen Sie einen LINQ-Anbieter dafür.

  • Erstellen Sie einen LINQ-Anbieter für eine andere Datenquelle als einen Webdienst.

Siehe auch

Aufgaben

Beispiel eines LINQ to TerraServer-Anbieters

Gewusst wie: Implementieren eines Ausdrucksbaumstruktur-Besuchers

Gewusst wie: Ändern von Ausdrucksbaumstrukturen

Konzepte

Aktivieren einer Datenquelle für LINQ-Abfragen

Referenz

IQueryable<T>

IOrderedQueryable<T>

Weitere Ressourcen

Windows Communication Foundation-Dienste und ADO.NET Data Services