Пошаговое руководство. Создание поставщика IQueryable LINQ

Этот дополнительный раздел содержит пошаговые инструкции по созданию пользовательского поставщика LINQ. Созданный поставщик можно будет применять для написания запросов LINQ к веб-службе TerraServer-USA.

Веб-служба TerraServer-USA предоставляет интерфейс к базе данных аэросъемки США. Она также предоставляет метод, возвращающий сведения о местах в США, по части или полному названию расположения. Этот метод с именем GetPlaceList будет вызываться поставщиком LINQ. Для взаимодействия с веб-службой поставщик будет использовать Windows Communication Foundation (WCF). Дополнительные сведения о веб-службе TerraServer-USA см. на веб-узле Обзор веб-служб TerraServer-USA.

Этот поставщик является относительно простым IQueryable поставщиком. Он ожидает определенные сведения в запросах, которые он обрабатывает, и имеет закрытую систему типов, предоставляя один тип для представления данных результата. Этот поставщик проверяет только один тип выражения самого внутреннего вызова метода Where в дереве выражения, которое представляет запрос. Он извлекает данные, необходимые для запроса к веб-службе из этого выражения. Затем он вызывает веб-службу и вставляет возвращаемые данные в дерево выражения вместо начального IQueryable источника данных. Остальная часть запроса обрабатывается Enumerable реализациями стандартных операторов запроса.

Примеры кода в этом разделе приводятся на C# и Visual Basic.

В данном пошаговом руководстве рассмотрены следующие задачи:

  • Создание проекта в Visual Studio.

  • Реализация интерфейсов IQueryable<T>IOrderedQueryable<T> и IQueryProvider, необходимых для IQueryable LINQ поставщика:

  • Добавление пользовательского типа .NET для представления данных из веб-службы.

  • Создание класса контекста запроса и класса, который получает данные из веб-службы.

  • Создание подкласса обхода дерева выражений для нахождения выражения, представляющего самый внутренний вызов метода Queryable.Where.

  • Создание подкласса обхода дерево выражений, который извлекает сведения из запроса LINQ для использования в запросе веб-службы.

  • Создание подкласса обхода дерева выражений, который изменяет дерево выражений, представляющее полный запрос LINQ.

  • Использование класса оценки для частичной оценки дерева выражения. Этот шаг необходим, так как он преобразует все локальные ссылки на переменные в запросе LINQ в значения.

  • Создание вспомогательного класса для дерева выражения и нового класса исключений.

  • Проверка поставщика LINQ из клиентского приложения, содержащего запрос LINQ.

  • Добавление более сложных возможностей запроса к поставщику LINQ.

    Примечание

    Создаваемый в этом пошаговом руководстве поставщик LINQ доступен в качестве образца.Дополнительные сведения см. в разделе Примеры LINQ.

Обязательные компоненты

Ниже приведены компоненты, необходимые для выполнения данного пошагового руководства.

  • Visual Studio 2008

Примечание

На вашем компьютере названия некоторых элементов интерфейса пользователя Visual Studio или их расположение могут отличаться от указанных в нижеследующих инструкциях. Это зависит от имеющегося выпуска Visual Studio и используемых параметров. Дополнительные сведения см. в разделе Параметры Visual Studio.

Создание проекта

Создание проекта в Visual Studio

  1. В Visual Studio создайте новое приложение Библиотека классов. Назовите проект LinqToTerraServerProvider.

  2. В обозревателе решений выберите файл Class1.cs (или Class1.vb) и переименуйте его в QueryableTerraServerData.cs (или QueryableTerraServerData.vb). Во всплывающем диалоговом окне выберите Да, чтобы переименовать все ссылки на элемент кода.

    Поставщик создается в виде проекта библиотеки классов в Visual Studio, поскольку исполняемые клиентские приложения будут добавлять сборку поставщика в качестве ссылки в их проект.

Добавление ссылки на службу для веб-службы

  1. В обозревателе решений щелкните правой клавишей мыши проект LinqToTerraServerProvider и выберите команду Добавить ссылку на службу.

    Откроется диалоговое окно Добавление ссылки на службу.

  2. В поле Адрес введите http://terraserver.microsoft.com/TerraService2.asmx.

  3. В поле Пространство имен введите TerraServerReference и нажмите кнопку ОК.

    Веб-служба TerraServer-USA добавлена как ссылка на службу, что позволяет приложению взаимодействовать с ней посредством Windows Communication Foundation (WCF). Добавляя ссылку на службу в проект, Visual Studio создает файл app.config, содержащий информацию о прокси и конечную точку для веб-службы. Дополнительные сведения см. в разделе Службы Windows Communication Foundation и службы данных WCF в Visual Studio.

Теперь имеется проект, в составе которого есть файл с именем app.config, файл с именем QueryableTerraServerData.cs (или QueryableTerraServerData.vb) и ссылка на службу TerraServerReference.

Реализация необходимых интерфейсов

Для создания поставщика LINQ необходимо, по крайней мере, реализовать интерфейсы IQueryable<T> и IQueryProvider. Интерфейсы IQueryable<T> и IQueryProvider являются производными от других требуемых интерфейсов, поэтому при их реализации происходит реализация и других интерфейсов, необходимых поставщику LINQ.

Если требуется поддерживать сортирующие операторы запроса, такие как OrderBy и ThenBy, необходимо также реализовывать интерфейс IOrderedQueryable<T>. Поскольку IOrderedQueryable<T> является производным от IQueryable<T>, можно реализовать оба этих интерфейса в одном типе, что и делает данный поставщик.

Реализация System.LINQ.IQueryable`1 и System.LINQ.IOrderedQueryable`1

  • В файл QueryableTerraServerData.cs (или QueryableTerraServerData.vb) добавьте следующий код.

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

    Реализация IOrderedQueryable<T> классом QueryableTerraServerData реализует три свойства, объявленных в IQueryable, и два метода перечисления, объявленных в IEnumerable и IEnumerable<T>.

    Этот класс имеет два конструктора. Первый конструктор вызывается из клиентского приложения для создания объекта для написания запроса LINQ. Второй конструктор вызывается для библиотеки поставщика в коде реализации IQueryProvider.

    При вызове метода GetEnumerator объекта типа QueryableTerraServerData запрос, который он представляет, выполняется и перечисляются результаты запроса.

    Этот код, за исключением имени класса, применим не только к этому поставщику веб-службы TerraServer-USA. Поэтому он может быть повторно использован для любого поставщика LINQ.

Реализация System.LINQ.IQueryProvider

  • Добавьте в проект класс TerraServerQueryProvider.

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

    Код поставщика запроса в этом классе реализует четыре метода, которые необходимы для реализации интерфейса IQueryProvider. Два метода CreateQuery создают запросы, связанные с источником данных. Два метода Execute отправляют эти запросы на выполнение.

    Не универсальный метод CreateQuery использует отражение для получения типа элемента последовательности, создаваемой запросом при выполнении. Затем он использует класс Activator для создания нового экземпляра QueryableTerraServerData, который формируется с типом элемента в качестве аргумента универсального типа. Результат вызова не универсального метода CreateQuery аналогичен результату, который был бы получен при использовании универсального метода CreateQuery с правильным типом аргумента.

    Большая часть логики выполнения запроса обрабатывается в другом классе, который будет добавлен позже. Эта функциональность обрабатывается в другом месте, поскольку она зависит от источника данных, к которому выполняется запрос, в то время как код в этом классе является общим для любых поставщиков LINQ. Использование этого кода для другого поставщика, возможно, потребует изменения имени класса и имени типа контекста запроса, на которые есть ссылки в двух методах.

Добавление нового типа для представления данных результата

Для представления данных, полученных из веб-службы, необходим тип .NET. Этот тип будет использоваться в запросе LINQ клиента для определения необходимых результатов. Такой тип создается в следующей процедуре. Этот тип с именем Place содержит сведения об одном географическом месте, например городе, парке или озере.

Этот код также содержит тип перечисления с именем PlaceType, который определяет различные типы географического положения и используется в классе Place.

Создание пользовательского типа результатов

  • Добавьте в проект класс Place и перечисление PlaceType.

    Public Class Place
        ' Properties.
        Public Property Name As String
        Public Property State As String
        Public Property PlaceType As PlaceType
    
        ' Constructor.
        Friend Sub New(ByVal name As String, 
                       ByVal state As String, 
                       ByVal placeType As TerraServerReference.PlaceType)
    
            Me.Name = name
            Me.State = state
            Me.PlaceType = CType(placeType, PlaceType)
        End Sub
    End Class
    
    Public Enum PlaceType
        Unknown
        AirRailStation
        BayGulf
        CapePeninsula
        CityTown
        HillMountain
        Island
        Lake
        OtherLandFeature
        OtherWaterFeature
        ParkBeach
        PointOfInterest
        River
    End Enum
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace LinqToTerraServerProvider
    {
        public class Place
        {
            // Properties.
            public string Name { get; private set; }
            public string State { get; private set; }
            public PlaceType PlaceType { get; private set; }
    
            // Constructor.
            internal Place(string name,
                            string state,
                            LinqToTerraServerProvider.TerraServerReference.PlaceType placeType)
            {
                Name = name;
                State = state;
                PlaceType = (PlaceType)placeType;
            }
        }
    
        public enum PlaceType
        {
            Unknown,
            AirRailStation,
            BayGulf,
            CapePeninsula,
            CityTown,
            HillMountain,
            Island,
            Lake,
            OtherLandFeature,
            OtherWaterFeature,
            ParkBeach,
            PointOfInterest,
            River
        }
    }
    

    Конструктор для типа Place упрощает создание результирующего объекта из типа, возвращаемого веб-службой. Несмотря на то, что поставщик может непосредственно возвращать тип результата, определенный API веб-службы, в клиентское приложение требуется добавить ссылку на веб-службу. Создание нового типа как части библиотеки поставщика освободит клиента от необходимости изучения типов и методов, предоставляемых веб-службой.

Добавление функциональных возможностей для получения данных из источника данных

Эта реализация поставщика предполагает, что самый внутренний вызов метода Queryable.Where содержит сведения о расположении для использования в запросе веб-службы. Самым внутренним является вызов метода Queryable.Where в предложении where (предложение Where в Visual Basic) или вызов метода Queryable.Where, который первым встречается в запросе LINQ или является ближайшим к основанию дерева выражения, которое представляет запрос.

Создание класса контекста запроса

  • Добавьте в проект класс TerraServerQueryContext.

    Imports System.Linq.Expressions
    
    Public Class TerraServerQueryContext
    
        ' Executes the expression tree that is passed to it.
        Friend Shared Function Execute(ByVal expr As Expression, 
                                       ByVal IsEnumerable As Boolean) As Object
    
            ' The expression must represent a query over the data source.
            If Not IsQueryOverDataSource(expr) Then
                Throw New InvalidProgramException("No query over the data source was specified.")
            End If
    
            ' Find the call to Where() and get the lambda expression predicate.
            Dim whereFinder As New InnermostWhereFinder()
            Dim whereExpression As MethodCallExpression = 
                whereFinder.GetInnermostWhere(expr)
            Dim lambdaExpr As LambdaExpression
            lambdaExpr = CType(CType(whereExpression.Arguments(1), UnaryExpression).Operand, LambdaExpression)
    
            ' Send the lambda expression through the partial evaluator.
            lambdaExpr = CType(Evaluator.PartialEval(lambdaExpr), LambdaExpression)
    
            ' Get the place name(s) to query the Web service with.
            Dim lf As New LocationFinder(lambdaExpr.Body)
            Dim locations As List(Of String) = lf.Locations
            If locations.Count = 0 Then
                Dim s = "You must specify at least one place name in your query."
                Throw New InvalidQueryException(s)
            End If
    
            ' Call the Web service and get the results.
            Dim places() = WebServiceHelper.GetPlacesFromTerraServer(locations)
    
            ' Copy the IEnumerable places to an IQueryable.
            Dim queryablePlaces = places.AsQueryable()
    
            ' Copy the expression tree that was passed in, changing only the first
            ' argument of the innermost MethodCallExpression.
            Dim treeCopier As New ExpressionTreeModifier(queryablePlaces)
            Dim newExpressionTree = treeCopier.Visit(expr)
    
            ' This step creates an IQueryable that executes by replacing 
            ' Queryable methods with Enumerable methods.
            If (IsEnumerable) Then
                Return queryablePlaces.Provider.CreateQuery(newExpressionTree)
            Else
                Return queryablePlaces.Provider.Execute(newExpressionTree)
            End If
        End Function
    
        Private Shared Function IsQueryOverDataSource(ByVal expression As Expression) As Boolean
            ' If expression represents an unqueried IQueryable data source instance,
            ' expression is of type ConstantExpression, not MethodCallExpression.
            Return (TypeOf expression Is MethodCallExpression)
        End Function
    End Class
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        class TerraServerQueryContext
        {
            // Executes the expression tree that is passed to it.
            internal static object Execute(Expression expression, bool IsEnumerable)
            {
                // The expression must represent a query over the data source.
                if (!IsQueryOverDataSource(expression))
                    throw new InvalidProgramException("No query over the data source was specified.");
    
                // Find the call to Where() and get the lambda expression predicate.
                InnermostWhereFinder whereFinder = new InnermostWhereFinder();
                MethodCallExpression whereExpression = whereFinder.GetInnermostWhere(expression);
                LambdaExpression lambdaExpression = (LambdaExpression)((UnaryExpression)(whereExpression.Arguments[1])).Operand;
    
                // Send the lambda expression through the partial evaluator.
                lambdaExpression = (LambdaExpression)Evaluator.PartialEval(lambdaExpression);
    
                // Get the place name(s) to query the Web service with.
                LocationFinder lf = new LocationFinder(lambdaExpression.Body);
                List<string> locations = lf.Locations;
                if (locations.Count == 0)
                    throw new InvalidQueryException("You must specify at least one place name in your query.");
    
                // Call the Web service and get the results.
                Place[] places = WebServiceHelper.GetPlacesFromTerraServer(locations);
    
                // Copy the IEnumerable places to an IQueryable.
                IQueryable<Place> queryablePlaces = places.AsQueryable<Place>();
    
                // Copy the expression tree that was passed in, changing only the first
                // argument of the innermost MethodCallExpression.
                ExpressionTreeModifier treeCopier = new ExpressionTreeModifier(queryablePlaces);
                Expression newExpressionTree = treeCopier.Visit(expression);
    
                // This step creates an IQueryable that executes by replacing Queryable methods with Enumerable methods.
                if (IsEnumerable)
                    return queryablePlaces.Provider.CreateQuery(newExpressionTree);
                else
                    return queryablePlaces.Provider.Execute(newExpressionTree);
            }
    
            private static bool IsQueryOverDataSource(Expression expression)
            {
                // If expression represents an unqueried IQueryable data source instance,
                // expression is of type ConstantExpression, not MethodCallExpression.
                return (expression is MethodCallExpression);
            }
        }
    }
    

    Этот класс организует работу по выполнению запроса. После поиска выражения, представляющего вызов самого внутреннего метода Queryable.Where, этот код извлекает лямбда-выражение, представляющее предикат, переданный Queryable.Where. Затем выражение предиката передается методу и частично вычисляется, чтобы все ссылки на локальные переменные преобразовались в значения. Затем вызывается метод для извлечения запрошенного расположения из предиката и вызывается другой метод для получения данных результата от веб-службы.

    На следующем шаге этот код копирует дерево выражения, которое представляет запрос LINQ и делает одно изменение в дереве выражения. Код использует подкласс обхода дерева выражения для замены источника данных, к которому адресован самый внутренний оператор запроса, на конкретный список объектов Place, полученных от веб-службы.

    Перед вставкой списка объектов Place в дерево выражения, его тип изменяется с IEnumerable на IQueryable путем вызова AsQueryable. Это изменение типа необходимо, поскольку при перезаписи дерева выражений перестраивается узел, представляющий вызов метода к самому внутреннему методу оператора запроса. Узел перестраивается, поскольку один из аргументов был изменен (то есть источник данных). Метод Call(Expression, MethodInfo, IEnumerable<Expression>), использующийся для перестроения узла, вызовет исключение, если какие-либо из аргументов не смогут быть присвоены соответствующему параметру для передачи методу. В этом случае список IEnumerable объектов Place не будет присвоен параметру IQueryable метода Queryable.Where. Поэтому его тип изменяется на IQueryable.

    Изменение его тип на IQueryable, добавит к коллекции член IQueryProvider, доступный через свойство Provider, которое может создавать или выполнять запросы. Динамический тип EnumerableQuery°коллекции IQueryable°Place является типом, внутренним для API System.Linq. Поставщик запроса, который связан с этим типом выполняет запросы, заменяя вызовы стандартных операторов запроса Queryable эквивалентными вызовами операторов Enumerable для эффективного преобразования запроса в LINQ запрос объектов.

    Последний код в классе TerraServerQueryContext вызывает один из двух методов для списка IQueryable объектов Place. Он вызывает метод CreateQuery, если клиентский запрос возвращает перечислимые результаты или метод Execute, если запрос клиента возвращает результат не перечислимый.

    Код в этом классе предназначен исключительно для поставщика TerraServer-USA. Поэтому он инкапсулируется в классе TerraServerQueryContext вместо непосредственного расположения в более универсальной реализации IQueryProvider.

Создаваемому поставщику требуются только сведения из предиката Queryable.Where для запроса веб-службы. Поэтому он использует LINQ to Objects для выполнения запроса LINQ и внутренний тип EnumerableQuery. Альтернативным способом использования LINQ to Objects для выполнения запроса является перенос клиентом части запроса, выполняемой LINQ to Objects, в запрос LINQ to Objects. Это достигается путем вызова метода AsEnumerable<TSource> для остальной части запроса, которая необходима поставщику для своих определенных целей. Преимуществом такого вида реализации является то, что разделение работы между пользовательским поставщиком и LINQ to Objects является более прозрачным.

Примечание

Поставщик, представленный в этом разделе, является простым поставщиком с минимальной самостоятельной поддержкой запросов.Поэтому при выполнении запросов он в большей степени полагается на LINQ to Objects.Сложные поставщики LINQ, такие как LINQ to SQL, могут поддерживать весь запрос целиком без передачи работы LINQ to Objects.

Создание класса для получения данных из веб-службы

  • Добавьте в проект класс WebServiceHelper (или модуль в 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;
                }
            }
        }
    }
    

    Этот класс содержит функциональность для получения данных из веб-службы. В этом коде используется тип с именем TerraServiceSoapClient, автоматически сгенерированный для проекта Windows Communication Foundation (WCF), для вызова метода веб-службы GetPlaceList. Затем, каждый результат преобразуется из возвращаемого типа метода веб-службы к типу .NET, определенному поставщиком для данных.

    Этот код содержит две проверки для повышения удобства использования библиотеки поставщика. Первая проверка ограничивает максимальное время, которое клиентское приложение будет ожидать ответа, ограничивая общее число вызовов веб-службы за один запрос пятью. Для каждого местоположения, указанного в клиентском запросе создается один запрос к веб-службе. Поэтому поставщик вызывает исключение, если запрос содержит более пяти местоположений.

    Вторая проверка определяет равно ли число результатов, возвращаемых веб-службой максимальному числу результатов, которые она может возвращать. Если число результатов совпадает с максимальным числом, вполне вероятно, что результаты веб-службы, усекаются. Вместо возвращения неполного списка клиенту, поставщик вызывает исключение.

Добавление классов обхода дерева выражения

Создание класса для нахождения самого внутреннего вызова метода выражения Where

  1. Добавьте к проекту класс InnermostWhereFinder, наследуемый от класса 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;
            }
        }
    }
    

    Данный класс наследует функциональность базового класса обхода дерева выражения для поиска определенного выражения. Базовый класс обхода дерева выражения предназначен для наследования и использования для конкретной задачи по перемещению в дереве выражений. Производный класс переопределяет метод VisitMethodCall для поиска выражения, представляющего самый внутренний вызов Where в дереве выражений, представляющее собой запрос клиента. Это внутреннее выражение является выражением, из которого поставщик извлекает расположения для поиска.

  2. Добавьте директивы using (или инструкции Imports в Visual Basic) в файл для следующих пространств имен: System.Collections.Generic, System.Collections.ObjectModel и System.Linq.Expressions.

Создание класса обхода для извлечения данных для запроса к веб-службе

  • Добавьте в проект класс LocationFinder.

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

    Этот класс используется для извлечения сведений о местоположении из предиката, который клиент передает Queryable.Where. Он наследует классу ExpressionVisitor и переопределяет только метод VisitBinary.

    Класс ExpressionVisitor отправляет бинарные выражения, например выражения равенства, как place.Name == "Seattle" (place.Name = "Seattle" в Visual Basic) в метод VisitBinary. В этом переопределенном методе VisitBinary если выражение совпадает с выражением равенства шаблона, который может предоставить сведения о местоположении, эти сведения извлекается и хранятся в списке местоположений.

    Этот класс использует обход дерева выражений для поиска сведений о местоположении, он разработан для перемещения и изучения деревьев выражений. Результирующий код является более аккуратным и содержит меньше ошибок, по сравнению с его реализацией без использования класса обхода.

    На этом этапе пошагового руководства поставщик поддерживает только ограниченные способы предоставления сведений о местоположении в запросе. Ниже в разделе будет добавлена возможность включения дополнительных способов передачи сведений о местоположении.

Создание класс обхода для изменения дерева выражений

  • Добавьте в проект класс ExpressionTreeModifier.

    Imports System.Linq.Expressions
    
    Friend Class ExpressionTreeModifier
        Inherits ExpressionVisitor
    
        Private queryablePlaces As IQueryable(Of Place)
    
        Friend Sub New(ByVal places As IQueryable(Of Place))
            Me.queryablePlaces = places
        End Sub
    
        Protected Overrides Function VisitConstant(ByVal c As ConstantExpression) As Expression
            ' Replace the constant QueryableTerraServerData arg with the queryable Place collection.
            If c.Type Is GetType(QueryableTerraServerData(Of Place)) Then
                Return Expression.Constant(Me.queryablePlaces)
            Else
                Return c
            End If
        End Function
    End Class
    
    using System;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        internal class ExpressionTreeModifier : ExpressionVisitor
        {
            private IQueryable<Place> queryablePlaces;
    
            internal ExpressionTreeModifier(IQueryable<Place> places)
            {
                this.queryablePlaces = places;
            }
    
            protected override Expression VisitConstant(ConstantExpression c)
            {
                // Replace the constant QueryableTerraServerData arg with the queryable Place collection.
                if (c.Type == typeof(QueryableTerraServerData<Place>))
                    return Expression.Constant(this.queryablePlaces);
                else
                    return c;
            }
        }
    }
    

    Этот класс наследует классу ExpressionVisitor и переопределяет только метод VisitConstant. В этом методе, он заменяет объект, к которому применяется вызов самого внутреннего стандартного оператора запроса, на список конкретных объектов Place.

    Этот класс модификатора дерева выражений использует класс обхода дерева выражений, поскольку класс обхода разработан для просмотра, копирования и перемещения по дереву выражений. Будучи производным от базового класса обхода дерева выражения, этот класс требует минимального кода для выполнения своих функций.

Добавление вычислителя выражения

Предикат, передаваемый методу Queryable.Where в запросе клиента, может содержать подвыражения, которые не зависят от параметра лямбда выражения. Эти изолированные подвыражения могут и должны быть вычислены немедленно. Они могут быть ссылками на локальные переменные или переменные-члены, которые должны быть преобразованы в значения.

Следующий класс предоставляет метод PartialEval(Expression), определяющий можно ли немедленно вычислить какое-либо поддерево выражений. Затем он вычисляет эти выражения, создает лямбда выражение, компилирует его и вызывает возвращаемый делегат. Наконец, он заменяет поддерево новым узлом, представляющим постоянное значение. Это называется частичным вычислением.

Добавление класса для выполнения частичного вычисления дерева выражения

  • Добавьте в проект класс Evaluator.

    Imports System.Linq.Expressions
    
    Public Module Evaluator
        ''' <summary>Performs evaluation and replacement of independent sub-trees</summary>
        ''' <param name="expr">The root of the expression tree.</param>
        ''' <param name="fnCanBeEvaluated">A function that decides whether a given expression node can be part of the local function.</param>
        ''' <returns>A new tree with sub-trees evaluated and replaced.</returns>
        Public Function PartialEval(
            ByVal expr As Expression, 
            ByVal fnCanBeEvaluated As Func(Of Expression, Boolean)
            )  As Expression
    
            Return New SubtreeEvaluator(New Nominator(fnCanBeEvaluated).Nominate(expr)).Eval(expr)
        End Function
    
        ''' <summary>
        ''' Performs evaluation and replacement of independent sub-trees
        ''' </summary>
        ''' <param name="expression">The root of the expression tree.</param>
        ''' <returns>A new tree with sub-trees evaluated and replaced.</returns>
        Public Function PartialEval(ByVal expression As Expression) As Expression
            Return PartialEval(expression, AddressOf Evaluator.CanBeEvaluatedLocally)
        End Function
    
        Private Function CanBeEvaluatedLocally(ByVal expression As Expression) As Boolean
            Return expression.NodeType <> ExpressionType.Parameter
        End Function
    
        ''' <summary>
        ''' Evaluates and replaces sub-trees when first candidate is reached (top-down)
        ''' </summary>
        Class SubtreeEvaluator
            Inherits ExpressionVisitor
    
            Private candidates As HashSet(Of Expression)
    
            Friend Sub New(ByVal candidates As HashSet(Of Expression))
                Me.candidates = candidates
            End Sub
    
            Friend Function Eval(ByVal exp As Expression) As Expression
                Return Me.Visit(exp)
            End Function
    
            Public Overrides Function Visit(ByVal exp As Expression) As Expression
                If exp Is Nothing Then
                    Return Nothing
                ElseIf Me.candidates.Contains(exp) Then
                    Return Me.Evaluate(exp)
                End If
    
                Return MyBase.Visit(exp)
            End Function
    
            Private Function Evaluate(ByVal e As Expression) As Expression
                If e.NodeType = ExpressionType.Constant Then
                    Return e
                End If
    
                Dim lambda = Expression.Lambda(e)
                Dim fn As [Delegate] = lambda.Compile()
    
                Return Expression.Constant(fn.DynamicInvoke(Nothing), e.Type)
            End Function
        End Class
    
    
        ''' <summary>
        ''' Performs bottom-up analysis to determine which nodes can possibly
        ''' be part of an evaluated sub-tree.
        ''' </summary>
        Class Nominator
            Inherits ExpressionVisitor
    
            Private fnCanBeEvaluated As Func(Of Expression, Boolean)
            Private candidates As HashSet(Of Expression)
            Private cannotBeEvaluated As Boolean
    
            Friend Sub New(ByVal fnCanBeEvaluated As Func(Of Expression, Boolean))
                Me.fnCanBeEvaluated = fnCanBeEvaluated
            End Sub
    
            Friend Function Nominate(ByVal expr As Expression) As HashSet(Of Expression)
                Me.candidates = New HashSet(Of Expression)()
                Me.Visit(expr)
    
                Return Me.candidates
            End Function
    
            Public Overrides Function Visit(ByVal expr As Expression) As Expression
                If expr IsNot Nothing Then
    
                    Dim saveCannotBeEvaluated = Me.cannotBeEvaluated
                    Me.cannotBeEvaluated = False
    
                    MyBase.Visit(expr)
    
                    If Not Me.cannotBeEvaluated Then
                        If Me.fnCanBeEvaluated(expr) Then
                            Me.candidates.Add(expr)
                        Else
                            Me.cannotBeEvaluated = True
                        End If
                    End If
    
                    Me.cannotBeEvaluated = Me.cannotBeEvaluated Or 
                                           saveCannotBeEvaluated
                End If
    
                Return expr
            End Function
        End Class
    End Module
    
    using System;
    using System.Collections.Generic;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        public static class Evaluator
        {
            /// <summary>
            /// Performs evaluation & replacement of independent sub-trees
            /// </summary>
            /// <param name="expression">The root of the expression tree.</param>
            /// <param name="fnCanBeEvaluated">A function that decides whether a given expression node can be part of the local function.</param>
            /// <returns>A new tree with sub-trees evaluated and replaced.</returns>
            public static Expression PartialEval(Expression expression, Func<Expression, bool> fnCanBeEvaluated)
            {
                return new SubtreeEvaluator(new Nominator(fnCanBeEvaluated).Nominate(expression)).Eval(expression);
            }
    
            /// <summary>
            /// Performs evaluation & replacement of independent sub-trees
            /// </summary>
            /// <param name="expression">The root of the expression tree.</param>
            /// <returns>A new tree with sub-trees evaluated and replaced.</returns>
            public static Expression PartialEval(Expression expression)
            {
                return PartialEval(expression, Evaluator.CanBeEvaluatedLocally);
            }
    
            private static bool CanBeEvaluatedLocally(Expression expression)
            {
                return expression.NodeType != ExpressionType.Parameter;
            }
    
            /// <summary>
            /// Evaluates & replaces sub-trees when first candidate is reached (top-down)
            /// </summary>
            class SubtreeEvaluator : ExpressionVisitor
            {
                HashSet<Expression> candidates;
    
                internal SubtreeEvaluator(HashSet<Expression> candidates)
                {
                    this.candidates = candidates;
                }
    
                internal Expression Eval(Expression exp)
                {
                    return this.Visit(exp);
                }
    
                public override Expression Visit(Expression exp)
                {
                    if (exp == null)
                    {
                        return null;
                    }
                    if (this.candidates.Contains(exp))
                    {
                        return this.Evaluate(exp);
                    }
                    return base.Visit(exp);
                }
    
                private Expression Evaluate(Expression e)
                {
                    if (e.NodeType == ExpressionType.Constant)
                    {
                        return e;
                    }
                    LambdaExpression lambda = Expression.Lambda(e);
                    Delegate fn = lambda.Compile();
                    return Expression.Constant(fn.DynamicInvoke(null), e.Type);
                }
            }
    
            /// <summary>
            /// Performs bottom-up analysis to determine which nodes can possibly
            /// be part of an evaluated sub-tree.
            /// </summary>
            class Nominator : ExpressionVisitor
            {
                Func<Expression, bool> fnCanBeEvaluated;
                HashSet<Expression> candidates;
                bool cannotBeEvaluated;
    
                internal Nominator(Func<Expression, bool> fnCanBeEvaluated)
                {
                    this.fnCanBeEvaluated = fnCanBeEvaluated;
                }
    
                internal HashSet<Expression> Nominate(Expression expression)
                {
                    this.candidates = new HashSet<Expression>();
                    this.Visit(expression);
                    return this.candidates;
                }
    
                public override Expression Visit(Expression expression)
                {
                    if (expression != null)
                    {
                        bool saveCannotBeEvaluated = this.cannotBeEvaluated;
                        this.cannotBeEvaluated = false;
                        base.Visit(expression);
                        if (!this.cannotBeEvaluated)
                        {
                            if (this.fnCanBeEvaluated(expression))
                            {
                                this.candidates.Add(expression);
                            }
                            else
                            {
                                this.cannotBeEvaluated = true;
                            }
                        }
                        this.cannotBeEvaluated |= saveCannotBeEvaluated;
                    }
                    return expression;
                }
            }
        }
    }
    

Добавление вспомогательных классов

В этом разделе содержится код для трех вспомогательных классов для поставщика.

Добавление вспомогательного класса, используемого реализацией System.Linq.IQueryProvider

  • Добавьте в проект класс TypeSystem (или модуль в 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 AndAlso
               seqType.BaseType IsNot GetType(Object) Then
    
                Return FindIEnumerable(seqType.BaseType)
            End If
    
            Return Nothing
        End Function
    End Module
    
    using System;
    using System.Collections.Generic;
    
    namespace LinqToTerraServerProvider
    {
        internal static class TypeSystem
        {
            internal static Type GetElementType(Type seqType)
            {
                Type ienum = FindIEnumerable(seqType);
                if (ienum == null) return seqType;
                return ienum.GetGenericArguments()[0];
            }
    
            private static Type FindIEnumerable(Type seqType)
            {
                if (seqType == null || seqType == typeof(string))
                    return null;
    
                if (seqType.IsArray)
                    return typeof(IEnumerable<>).MakeGenericType(seqType.GetElementType());
    
                if (seqType.IsGenericType)
                {
                    foreach (Type arg in seqType.GetGenericArguments())
                    {
                        Type ienum = typeof(IEnumerable<>).MakeGenericType(arg);
                        if (ienum.IsAssignableFrom(seqType))
                        {
                            return ienum;
                        }
                    }
                }
    
                Type[] ifaces = seqType.GetInterfaces();
                if (ifaces != null && ifaces.Length > 0)
                {
                    foreach (Type iface in ifaces)
                    {
                        Type ienum = FindIEnumerable(iface);
                        if (ienum != null) return ienum;
                    }
                }
    
                if (seqType.BaseType != null && seqType.BaseType != typeof(object))
                {
                    return FindIEnumerable(seqType.BaseType);
                }
    
                return null;
            }
        }
    }
    

    Реализация IQueryProvider, добавленная ранее использует этот вспомогательный класс.

    Метод TypeSystem.GetElementType использует отражение для получения аргумента универсального типа коллекции IEnumerable<T> (IEnumerable(Of T) в Visual Basic). Этот метод вызывается из не универсального метода CreateQuery в реализации поставщика запроса для предоставления типа элемента в результирующей коллекции запроса.

    Этот вспомогательный класс не привязан к данному поставщику веб-службы TerraServer-USA. Поэтому он может быть повторно использован для любого поставщика LINQ.

Создание вспомогательного класса для дерева выражения

  • Добавьте в проект класс ExpressionTreeHelpers.

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

    Этот класс содержит методы, используемые для определения сведений и извлечения данных из определенных типов деревьев выражений. В этом поставщике эти методы используются классом LocationFinder для извлечения сведений из дерева выражения, представляющего запрос.

Добавление нового типа исключения для недопустимых запросов

  • Добавьте в проект класс InvalidQueryException.

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

    Этот класс определяет тип Exception, вызываемый поставщиком, когда он не понимает запрос LINQ клиента. С помощью определения этого типа исключения недопустимого запроса поставщик может вызывать более конкретное исключение, чем просто Exception из различных участков кода.

Теперь вы добавили все части, которые необходимы для компиляции поставщика. Выполните построение проекта LinqToTerraServerProvider и убедитесь в отсутствии ошибок компиляции.

Тестирование поставщика LINQ

Создав клиентское приложения, содержащего запрос LINQ к источнику данных, можно проверить поставщик LINQ.

Создание клиентского приложения для проверки поставщика

  1. Добавьте новый проект Консольное приложение к решению и назовите его ClientApp.

  2. В новом проекте добавьте ссылку на сборку поставщика.

  3. Перетащите файл app.config из проекта поставщика в клиентский проект. (Этот файл необходим для взаимодействия с веб-службой.)

    Примечание

    В Visual Basic может потребоваться нажать кнопку Показать все файлы, чтобы увидеть файл app.config в обозревателе решений.

  4. Добавьте следующие операторы using (Imports в Visual Basic) к файлу Program.cs (или Module1.vb в Visual Basic):

    using System;
    using System.Linq;
    using LinqToTerraServerProvider;
    
    Imports LinqToTerraServerProvider
    
  5. Вставьте следующий код в метод Main в файле Program.cs (или Module1.vb в Visual Basic):

    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
    

    Этот код создает новый экземпляр типа IQueryable<T>, определенного в поставщике, а затем создает запросы к этим объектам с помощью LINQ. Этот запрос указывает местоположение для получения данных с помощью выражения равенства. Поскольку источник данных реализует IQueryable, компилятор преобразует синтаксис выражений запроса в вызовы стандартных операторов запроса, определенных в Queryable. Внутренне эти методы стандартных операторов запросов строят дерево выражений и вызывают метод Execute или CreateQuery, реализованные как часть реализации IQueryProvider.

  6. Выполните построение приложения ClientApp.

  7. Установите это клиентское приложение "автозагружаемым" проектом решения. В обозревателе решений щелкните правой клавишей мыши проект ClientApp и выберите команду Назначить автозагружаемым проектом.

  8. Выполните программу и просмотрите результаты. Их должно быть приблизительно три.

Добавление более сложных возможностей запроса

Поставщик, доступный в данный момент, обеспечивает клиенту очень ограниченный способ указания сведений о расположении в запросе LINQ. В частности, поставщик способен получать сведения о расположении только из выражений равенства, таких как Place.Name == "Seattle" или Place.State == "Alaska" (Place.Name = "Seattle" или Place.State = "Alaska" в Visual Basic).

В следующей процедуре показано добавление поддержки дополнительных способов указания сведений о расположении. После добавления этого кода, ваш поставщик сможет извлечь сведения о расположении из выражений вызова метода, таких как place.Name.StartsWith("Seat").

Добавление поддержки для предикатов, содержащих String.StartsWith

  1. В проекте LinqToTerraServerProvider добавьте метод VisitMethodCall к определению класса LocationFinder.

    Protected Overrides Function VisitMethodCall(ByVal m As MethodCallExpression) As Expression
        If m.Method.DeclaringType Is GetType(String) And m.Method.Name = "StartsWith" Then
            If ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "Name") OrElse
               ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "State") Then
                _locations.Add(ETH.GetValueFromExpression(m.Arguments(0)))
                Return m
            End If
        End If
    
        Return MyBase.VisitMethodCall(m)
    End Function
    
    protected override Expression VisitMethodCall(MethodCallExpression m)
    {
        if (m.Method.DeclaringType == typeof(String) && m.Method.Name == "StartsWith")
        {
            if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "Name") ||
            ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "State"))
            {
                locations.Add(ExpressionTreeHelpers.GetValueFromExpression(m.Arguments[0]));
                return m;
            }
        }
    
        return base.VisitMethodCall(m);
    }
    
  2. Перекомпилируйте проект LinqToTerraServerProvider.

  3. Для проверки новых возможностей поставщика, откройте файл Program.cs (или Module1.vb в Visual Basic) в проекте ClientApp. Замените код в методе Main следующим кодом:

    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. Выполните программу и просмотрите результаты. Должно быть приблизительно 29 результатов.

В следующей процедуре демонстрируется добавление поставщику возможности, позволяющей запросам клиента указывать расположение при помощи двух дополнительных методов, а именно Enumerable.Contains и List<T>.Contains. После добавления этого кода, ваш поставщик сможет извлекать сведения о расположении из выражений вызовов метода в запросе клиента, таких как placeList.Contains(place.Name), где коллекция placeList является конкретным списком, предоставляемым клиентом. Для клиентов преимуществом использования метода Contains является то, что можно задавать любое количество расположений путем их простого добавления в список placeList. Изменение числа расположений не приводит к изменению синтаксиса запроса.

Добавление поддержки для запросов с методом Contains в предложениях "where"

  1. В проекте LinqToTerraServerProvider в определении класса LocationFinder замените метод VisitMethodCall на следующий код:

    Protected Overrides Function VisitMethodCall(ByVal m As MethodCallExpression) As Expression
        If m.Method.DeclaringType Is GetType(String) And m.Method.Name = "StartsWith" Then
            If ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "Name") OrElse
               ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "State") Then
                _locations.Add(ETH.GetValueFromExpression(m.Arguments(0)))
                Return m
            End If
        ElseIf m.Method.Name = "Contains" Then
            Dim valuesExpression As Expression = Nothing
    
            If m.Method.DeclaringType Is GetType(Enumerable) Then
                If ETH.IsSpecificMemberExpression(m.Arguments(1), GetType(Place), "Name") OrElse
                   ETH.IsSpecificMemberExpression(m.Arguments(1), GetType(Place), "State") Then
                    valuesExpression = m.Arguments(0)
                End If
    
            ElseIf m.Method.DeclaringType Is GetType(List(Of String)) Then
                If ETH.IsSpecificMemberExpression(m.Arguments(0), GetType(Place), "Name") OrElse
                   ETH.IsSpecificMemberExpression(m.Arguments(0), GetType(Place), "State") Then
                    valuesExpression = m.Object
                End If
            End If
    
            If valuesExpression Is Nothing OrElse valuesExpression.NodeType <> ExpressionType.Constant Then
                Throw New Exception("Could not find the location values.")
            End If
    
            Dim ce = CType(valuesExpression, ConstantExpression)
    
            Dim placeStrings = CType(ce.Value, IEnumerable(Of String))
            ' Add each string in the collection to the list of locations to obtain data about.
            For Each place In placeStrings
                _locations.Add(place)
            Next
    
            Return m
        End If
    
        Return MyBase.VisitMethodCall(m)
    End Function
    
    protected override Expression VisitMethodCall(MethodCallExpression m)
    {
        if (m.Method.DeclaringType == typeof(String) && m.Method.Name == "StartsWith")
        {
            if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "Name") ||
            ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "State"))
            {
                locations.Add(ExpressionTreeHelpers.GetValueFromExpression(m.Arguments[0]));
                return m;
            }
    
        }
        else if (m.Method.Name == "Contains")
        {
            Expression valuesExpression = null;
    
            if (m.Method.DeclaringType == typeof(Enumerable))
            {
                if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[1], typeof(Place), "Name") ||
                ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[1], typeof(Place), "State"))
                {
                    valuesExpression = m.Arguments[0];
                }
            }
            else if (m.Method.DeclaringType == typeof(List<string>))
            {
                if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[0], typeof(Place), "Name") ||
                ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[0], typeof(Place), "State"))
                {
                    valuesExpression = m.Object;
                }
            }
    
            if (valuesExpression == null || valuesExpression.NodeType != ExpressionType.Constant)
                throw new Exception("Could not find the location values.");
    
            ConstantExpression ce = (ConstantExpression)valuesExpression;
    
            IEnumerable<string> placeStrings = (IEnumerable<string>)ce.Value;
            // Add each string in the collection to the list of locations to obtain data about.
            foreach (string place in placeStrings)
                locations.Add(place);
    
            return m;
        }
    
        return base.VisitMethodCall(m);
    }
    

    Этот метод добавляет каждую строку в коллекцию, к которой применяется Contains, в список расположений для запроса веб-службы. Метод с именем Contains определен и в Enumerable, и в List<T>. Поэтому метод VisitMethodCall должен проверять оба объявляемых типа. Enumerable.Contains объявляется как метод расширения, поэтому коллекция, к которой он применяется, фактически является первым аргументом данного метода. List.Contains объявляется как метод экземпляра, поэтому коллекция, к которой он применяется, является принимающим объектом метода.

  2. Перекомпилируйте проект LinqToTerraServerProvider.

  3. Для проверки новых возможностей поставщика, откройте файл Program.cs (или Module1.vb в Visual Basic) в проекте ClientApp. Замените код в методе Main следующим кодом:

    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. Выполните программу и просмотрите результаты. Должно быть приблизительно 5 результатов.

Следующие действия

В этом пошаговом руководстве показано создание поставщика LINQ для одного метода веб-службы. Если вы хотите продолжить разработку поставщика LINQ, рассмотрите следующие возможности:

  • Дайте возможность поставщику LINQ обрабатывать другие способы указания расположения в запросе клиента.

  • Изучите другие методы, предоставляемые веб-службой TerraServer-USA, и создайте поставщик LINQ для работы с одним из этих методов.

  • Найдите другую веб-службу и создайте для нее поставщик LINQ.

  • Создайте поставщик LINQ для источника данных, отличного от веб-службы.

Дополнительные сведения о создании собственного поставщика LINQ см. на веб-странице LINQ: Building an IQueryable Provider в блогах MSDN.

См. также

Задачи

Примеры LINQ

Практическое руководство. Изменение деревьев выражений (C# и Visual Basic)

Ссылки

IQueryable<T>

IOrderedQueryable<T>

Основные понятия

Включение источника данных для запросов LINQ

Службы Windows Communication Foundation и службы данных WCF в Visual Studio