Пошаговое руководство. Создание поставщика IQueryable LINQ
Эта расширенная раздел содержит пошаговые инструкции по созданию пользовательского поставщика LINQ. Когда закончите, вы будете использовать поставщика создается для создания запросов LINQ к веб-службе TerraServer- США.
Веб-служба 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
в Visual Studio, создайте новое приложение Библиотека классов. Назовите проект LinqToTerraServerProvider.
В обозревателе решений выберите файл Class1.cs (или Class1.vb) и переименуйте его в QueryableTerraServerData.cs (или QueryableTerraServerData.vb).Во всплывающем диалоговом окне выберите Да, чтобы переименовать все ссылки на элемент кода.
Поставщик создается в виде проекта библиотеки классов в Visual Studio, поскольку исполняемые клиентские приложения будут добавлять сборку поставщика в качестве ссылки в их проект.
Добавление ссылки на службу для веб-службы
В обозревателе решений щелкните правой клавишей мыши проект LinqToTerraServerProvider и выберите команду Добавить ссылку на службу.
Откроется диалоговое окно Добавление ссылки на службу.
В поле Адрес введите http://terraserver.microsoft.com/TerraService2.asmx.
В поле Пространство имен введите 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 содержит сведения об одном типа geometry позиции как город, парк или озеро.
Этот код также содержит тип перечисления с именем 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
Добавьте к проекту класс 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 в дереве выражений, представляющее собой запрос клиента.Это внутреннее выражение является выражением, из которого поставщик извлекает расположения для поиска.
Добавьте директивы 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.
Создание клиентского приложения для проверки поставщика
Добавьте новый проект Консольное приложение к решению и назовите его ClientApp.
В новом проекте добавьте ссылку на сборку поставщика.
Перетащите файл app.config из проекта поставщика в клиентский проект.(Этот файл необходим для взаимодействия с веб-службой.)
Примечание В Visual Basic может потребоваться нажать кнопку Показать все файлы, чтобы увидеть файл app.config в обозревателе решений.
Добавьте следующие операторы using (Imports в Visual Basic) к файлу Program.cs (или Module1.vb в Visual Basic):
using System; using System.Linq; using LinqToTerraServerProvider;
Imports LinqToTerraServerProvider
Вставьте следующий код в метод 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.
Выполните построение приложения ClientApp.
Установите это клиентское приложение "автозагружаемым" проектом решения.В обозревателе решений щелкните правой клавишей мыши проект ClientApp и выберите команду Назначить автозагружаемым проектом.
Выполните программу и просмотрите результаты.Их должно быть приблизительно три.
Добавление более сложных возможностей запроса
Поставщик, доступный в данный момент, обеспечивает клиенту очень ограниченный способ указания сведений о расположении в запросе LINQ.В частности, поставщик способен получать сведения о расположении только из выражений равенства, таких как Place.Name == "Seattle" или Place.State == "Alaska" (Place.Name = "Seattle" или Place.State = "Alaska" в Visual Basic).
В следующей процедуре показано добавление поддержки дополнительных способов указания сведений о расположении.После добавления этого кода, ваш поставщик сможет извлечь сведения о расположении из выражений вызова метода, таких как place.Name.StartsWith("Seat").
Добавление поддержки для предикатов, содержащих String.StartsWith
В проекте 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); }
Перекомпилируйте проект LinqToTerraServerProvider.
Для проверки новых возможностей поставщика, откройте файл 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
Выполните программу и просмотрите результаты.Должно быть приблизительно 29 результатов.
В следующей процедуре демонстрируется добавление поставщику возможности, позволяющей запросам клиента указывать расположение при помощи двух дополнительных методов, а именно Enumerable.Contains и List<T>.Contains.После добавления этого кода, ваш поставщик сможет извлекать сведения о расположении из выражений вызовов метода в запросе клиента, таких как placeList.Contains(place.Name), где коллекция placeList является конкретным списком, предоставляемым клиентом.Для клиентов преимуществом использования метода Contains является то, что можно задавать любое количество расположений путем их простого добавления в список placeList.Изменение числа расположений не приводит к изменению синтаксиса запроса.
Добавление поддержки для запросов с методом Contains в предложениях "where"
В проекте 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 объявляется как метод экземпляра, поэтому коллекция, к которой он применяется, является принимающим объектом метода.
Перекомпилируйте проект LinqToTerraServerProvider.
Для проверки новых возможностей поставщика, откройте файл 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
Выполните программу и просмотрите результаты.Должно быть приблизительно 5 результатов.
Следующие действия
В этом пошаговом руководстве показано создание поставщика LINQ для одного метода веб-службы.Если вы хотите продолжить разработку поставщика LINQ, рассмотрите следующие возможности:
Дайте возможность поставщику LINQ обрабатывать другие способы указания расположения в запросе клиента.
Изучите другие методы, предоставляемые веб-службой TerraServer-USA, и создайте поставщик LINQ для работы с одним из этих методов.
Найдите другую веб-службу и создайте для нее поставщик LINQ.
Создайте поставщик LINQ для источника данных, отличного от веб-службы.
Дополнительные сведения о создании собственного поставщика LINQ см. на веб-странице LINQ: Building an IQueryable Provider в блогах MSDN.
См. также
Задачи
Практическое руководство. Изменение деревьев выражений (C# и Visual Basic)
Ссылки
Основные понятия
Включение источника данных для запросов LINQ
Службы Windows Communication Foundation и службы данных WCF в Visual Studio