Demonstra Passo a passo: Criando um provedor IQueryable LINQ
Este tópico avançado fornece instruções passo a passo para criar um personalizado LINQ provedor. Quando tiver terminado, você poderá usar o provedor que você criar, gravar LINQ consultas ao serviço da Web do TerraServer EUA.
O serviço da Web do TerraServer EUA fornece uma interface para um banco de dados de imagens aéreas dos Estados Unidos. Ele também expõe um método que retorna informações sobre locais nos Estados Unidos, determinada parte ou todo o nome do local. Esse método, chamado GetPlaceList, é o método que seu LINQ provedor chamará. O provedor usará Windows Communication Foundation (WCF) para se comunicar com o serviço da Web. Para obter mais informações sobre o serviço da Web do TerraServer EUA, consulte Visão geral dos serviços da Web TerraServer EUA.
Este provedor é relativamente simples IQueryable provedor. Ele espera informações específicas nas consultas que ele manipula e tem um sistema de tipo fechado, expondo um único tipo para representar os dados do resultado. Este provedor examina apenas um tipo de expressão de chamada de método na árvore de expressão que representa a consulta, o que é a mais interna chamada para Where. Ele extrai os dados que ele deve ter para consultar o serviço da Web a partir dessa expressão. Em seguida, chama o Web service e insere os dados retornados a árvore de expressão no lugar de inicial IQueryable dados de origem. O restante da execução da consulta é tratado pelo Enumerable implementações de operadores de consulta padrão.
Os exemplos de código neste tópico são fornecidos em C# e Visual Basic.
Essa explicação passo a passo ilustra as seguintes tarefas:
Criando o projeto em Visual Studio.
Implementar as interfaces que são necessárias para uma IQueryable LINQ provedor: IQueryable<T>, IOrderedQueryable<T>, e IQueryProvider.
Adicionando um personalizado.NET tipo para representar os dados do serviço da Web.
Criando uma classe de contexto da consulta e uma classe que obtém dados do serviço da Web.
Criando uma subclasse de visitante de árvore de expressão que localiza a expressão que representa a chamada mais interna para o Queryable.Where método.
Criando uma subclasse de visitante de árvore de expressão que extrai informações a partir de LINQ consulta a ser usada na solicitação de serviço da Web.
Criando uma subclasse de visitante de árvore de expressão que modifica a árvore de expressão que representa o completo LINQ de consulta.
Usando uma classe do avaliador parcialmente avaliar uma árvore de expressão. Esta etapa é necessária porque ele converte todas as referências de variáveis locais na LINQ a consulta em valores.
Criando um auxiliar de árvore de expressão de classe e uma nova classe de exceção.
Testando o LINQ provedor de um aplicativo cliente que contém um LINQ de consulta.
Adicionando recursos de consulta mais complexos para o LINQ provedor.
Observação O provedor LINQ que este passo a passo cria está disponível como um exemplo. Para obter mais informações, Amostras do LINQ.
Pré-requisitos
Para completar este passo a passo, são necessários os seguintes componentes:
- Visual Studio 2008
Observação |
---|
Seu computador pode mostrar nomes ou locais diferentes para alguns dos elementos da interface do usuário do Visual Studio nas instruções a seguir. A edição do Visual Studio que você possui e as configurações que você usa determinam esses elementos. Para obter mais informações, consulte Configurações do Visual Studio. |
Criando o projeto
Para criar o projeto em Visual Studio
Em Visual Studio, crie uma nova Biblioteca de classe aplicativo. Nomeie o projeto LinqToTerraServerProvider.
Em Solution Explorer, selecione o Class1. cs (ou Class1. vb) de arquivos e renomeá-lo para QueryableTerraServerData.cs (ou QueryableTerraServerData.vb). Na caixa de diálogo pop-up, clique em Sim para renomear todas as referências ao elemento de código.
Você cria o provedor como um Biblioteca de classe projeto em Visual Studio porque os aplicativos cliente executável irá adicionar o assembly de provedor como uma referência ao seu projeto.
Para adicionar uma referência de serviço para o serviço da Web
Em Solution Explorer, com o botão direito do LinqToTerraServerProvider de projeto e clique em Add Service Reference.
O Add Service Reference abre a caixa de diálogo.
No endereço , digite http://terraserver.microsoft.com/TerraService2.asmx.
No Namespace , digite TerraServerReference e, em seguida, clique em OK.
O serviço da Web do TerraServer EUA é adicionado como uma referência de serviço para que o aplicativo possa se comunicar com o serviço da Web por meio de Windows Communication Foundation (WCF). Adicionando uma referência de serviço para o projeto, Visual Studio gera um App. config arquivo que contém um proxy e um ponto de extremidade do serviço da Web. Para obter mais informações, consulte Os serviços do Windows Communication Foundation e serviços de dados do WCF em Visual Studio.
Agora você tem um projeto de um arquivo chamado App. config, um arquivo chamado QueryableTerraServerData.cs (ou QueryableTerraServerData.vb) e uma referência de serviço chamado TerraServerReference.
Implementar as Interfaces necessárias
Para criar um LINQ provedor, no mínimo, você deve implementar a IQueryable<T> e IQueryProvider interfaces. IQueryable<T> e IQueryProvider são derivadas das outras interfaces necessárias; Portanto, ao implementar essas duas interfaces, você também está implementando as interfaces que são necessárias para uma LINQ provedor.
Se você deseja oferecer suporte a operadores de consulta de classificação, como OrderBy e ThenBy, você também deve implementar a IOrderedQueryable<T> interface. Porque IOrderedQueryable<T> deriva de IQueryable<T>, você pode implementar essas duas interfaces em um tipo, o que é que este provedor faz.
Para implementar System.Linq.IQueryable'1 e System.Linq.IOrderedQueryable'1
No arquivo QueryableTerraServerData.cs (ou QueryableTerraServerData.vb), adicione o seguinte código.
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 } }
O IOrderedQueryable<T> implementação pela QueryableTerraServerData classe implementa três propriedades declaradas no IQueryable e dois métodos de enumeração é declarada no IEnumerable e IEnumerable<T>.
Essa classe tem dois construtores. O primeiro construtor é chamado a partir do aplicativo de cliente para criar o objeto para escrever o LINQ compara. O segundo construtor é chamado interno para a biblioteca do provedor pelo código na IQueryProvider de implementação.
Quando o GetEnumerator método for chamado em um objeto do tipo QueryableTerraServerData, a consulta que ela representa é executada e os resultados da consulta são enumerados.
Esse código, exceto para o nome da classe, não é específico para esse provedor de serviços da Web do TerraServer EUA. Portanto, pode ser reutilizado por qualquer LINQ provedor.
Para implementar a System.Linq.IQueryProvider
Adicionar o TerraServerQueryProvider classe ao seu projeto.
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); } } }
O código do provedor de consulta nesta classe implementa os quatro métodos que são necessários para implementar a IQueryProvider interface. Os dois CreateQuery métodos criam consultas que estão associadas com a fonte de dados. Os dois Execute métodos enviam essas consultas a ser executado.
Não genérica CreateQuery método usa a reflexão para obter o tipo de elemento da seqüência de consulta, ele cria retornaria se ele foi executado. Depois ele usa o Activator classe para construir um novo QueryableTerraServerData instância que é construída com o tipo de elemento, como seu argumento de tipo genérico. O resultado da chamada não genérica CreateQuery método é o mesmo como se genérico CreateQuery método tinha sido chamado com o argumento do tipo correto.
Maior parte da lógica de execução da consulta é tratado em uma classe diferente, você irá adicionar posteriormente. Essa funcionalidade é tratada em outro lugar porque ele é específico para a fonte de dados que está sendo consultada, enquanto o código nessa classe é genérico para qualquer LINQ provedor. Para usar este código para um provedor diferente, talvez você precise alterar o nome da classe e o nome do tipo de contexto de consulta que é referenciado em dois dos métodos.
Adicionando um tipo personalizado para representar os dados de resultado
Você precisará de um.NET tipo para representar os dados que são obtidos do serviço da Web. Esse tipo será usado no cliente do LINQ consulta para definir os resultados ele desejos. O procedimento a seguir cria um tipo como esse. Esse tipo, chamado Place, contém informações sobre um único local geográfico, como uma cidade, um parque ou um lago.
Esse código também contém um tipo de enumeração denominado PlaceType, que define os vários tipos de localização geográfica e é usado na Place classe.
Para criar um tipo de resultado personalizado
Adicionar o Place classe e o PlaceType enumeração para o seu projeto.
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 } }
O construtor para o Place tipo simplifica a criação de um objeto de resultado do tipo que é retornado pelo serviço da Web. Enquanto o provedor pode retornar o tipo de resultado definido pela API de serviço da Web diretamente, ele exigiria a aplicativos de cliente para adicionar uma referência ao serviço da Web. Criando um novo tipo como parte da biblioteca do provedor, o cliente não tem que saber sobre os tipos e métodos que o Web service expõe.
A adição de funcionalidade para obter os dados da fonte de dados
A implementação de provedor pressupõe que a chamada mais interna para Queryable.Where contém as informações de local para usar para consultar o serviço da Web. Interno Queryable.Where chamada é a where cláusula (Where cláusula Visual Basic) ou Queryable.Where chamada de método que ocorre primeiro em um LINQ consulta, ou para a mais próxima ao "bottom" da árvore de expressão que representa a consulta.
Para criar uma classe de contexto da consulta
Adicionar o TerraServerQueryContext classe ao seu projeto.
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); } } }
Essa classe organiza o trabalho de execução de uma consulta. Após localizar a expressão que representa o interno Queryable.Where chamada, este código recupera a expressão lambda que representa o predicado que foi passado para Queryable.Where. Em seguida, ele passa a expressão de predicado para um método a ser avaliada parcialmente, para que todas as referências para as variáveis locais são convertidas em valores. Em seguida, ele chama um método para extrair os locais solicitados o predicado e chama outro método para obter os dados de resultado do serviço da Web.
Na próxima etapa, esse código copia a árvore de expressão que representa o LINQ de consulta e faz uma modificação para a árvore de expressão. O código usa uma subclasse de visitante de árvore de expressão para substituir a fonte de dados, a chamada de operador de consulta interna é aplicada com a lista de concreta de Place os objetos que foram obtidos a partir do serviço da Web.
Antes da lista de Place objetos é inserida na árvore de expressão, seu tipo é alterado de IEnumerable para IQueryable chamando AsQueryable. Essa alteração de tipo é necessária porque quando a árvore de expressão é regravado, o nó que representa a chamada de método para o método do operador de consulta interna é reconstruído. O nó é reconstruído porque um dos argumentos foi alterado (ou seja, a fonte de dados que ela é aplicada a). O Call(Expression, MethodInfo, IEnumerable<Expression>) método, que é usado para reconstruir o nó, lançará uma exceção se qualquer argumento não pode ser atribuído ao parâmetro correspondente do método que será passada para. Nesse caso, o IEnumerable lista de Place objetos não seriam pode ser atribuídos para o IQueryable parâmetro do Queryable.Where. Portanto, seu tipo é alterado para IQueryable.
Alterando o seu tipo para IQueryable, a coleção também obtém um IQueryProvider membro, acessado pela Provider propriedade, que pode criar ou executar consultas. O tipo dinâmico da IQueryable°Place é a coleção EnumerableQuery, que é um tipo interno para o System.Linq API. O provedor de consultas que está associado este tipo executa consultas substituindo Queryable chamadas de operador de consulta padrão com o equivalente a Enumerable operadores, portanto, que efetivamente a consulta se tornará um LINQ a consulta de objetos.
O código final o TerraServerQueryContext classe chama um dos dois métodos de IQueryable lista de Place objetos. Ele chama CreateQuery se a consulta de cliente retorna os resultados de enumerable, ou Execute se a consulta de cliente retornará um resultado-enumerable.
O código dessa classe é muito específico ao provedor TerraServer EUA. Portanto, ela é encapsulada na TerraServerQueryContext classe em vez de sendo inseridos diretamente nas mais genérica IQueryProvider de implementação.
O provedor que você está criando requer apenas as informações na Queryable.Where predicado para consultar o serviço da Web. Therefore, ele usa LINQ a objetos para fazer o trabalho de execução a LINQ consulta usando o interno EnumerableQuery tipo. Uma maneira alternativa de usar LINQ a objetos para executar a consulta deve ter a disposição do cliente a parte da consulta para ser executado por LINQ a objetos em um LINQ a consulta de objetos. Isso é feito chamando AsEnumerable<TSource> no restante da consulta, que é a parte da consulta que requer que o provedor para seus propósitos específicos. A vantagem desse tipo de implementação é que a divisão de trabalho entre o provedor personalizado e LINQ a objetos é mais transparente.
Observação |
---|
O provedor apresentado neste tópico é um provedor simples que tenha suporte de consulta mínima das suas próprias. Portanto, ele conta bastante com LINQ a objetos para executar consultas. Um complexo LINQ provedor como LINQ to SQL pode oferecer suporte a toda a consulta sem qualquer trabalho entregando para LINQ a objetos. |
Para criar uma classe para obter os dados do serviço da Web
Adicionar o WebServiceHelper classe (ou módulo em Visual Basic) para o seu projeto.
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; } } } }
Essa classe contém a funcionalidade que obtém dados do serviço da Web. Esse código usa um tipo chamado TerraServiceSoapClient, que é gerada automaticamente para o projeto por Windows Communication Foundation (WCF), para chamar o método de serviço Web GetPlaceList. Em seguida, cada resultado é traduzido do tipo de retorno do método de serviço Web para o.NET que o provedor define os dados.
Esse código contém duas verificações de aprimoram a usabilidade da biblioteca do provedor. A primeira verificação limita o tempo máximo que um aplicativo cliente irá aguardar uma resposta, limitando o número total de chamadas são feitas ao serviço da Web, por consulta, cinco. Para cada local é especificado na consulta de cliente, uma solicitação de serviço da Web é gerada. Portanto, o provedor lança uma exceção se a consulta contiver mais de cinco locais.
A segunda seleção determina se o número de resultados retornados pelo serviço da Web é igual ao número máximo de resultados pode retornar. Se o número de resultados é o número máximo, é provável que os resultados do serviço da Web são truncados. Em vez de retornar uma lista incompleta para o cliente, o provedor lança uma exceção.
Adicionando as Classes de visitante de árvore de expressão
Para criar o visitante que localiza o interno onde o método chamar expressão
Adicionar o InnermostWhereFinder classe, que herda de ExpressionVisitor classe, para o seu projeto.
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; } } }
Esta classe herda a classe de visitante de árvore de expressão base para executar a funcionalidade de encontrar uma expressão específica. A classe de visitante de árvore de expressão de base foi projetada para ser herdadas e especializadas para uma tarefa específica que envolve a percorrer uma árvore de expressão. As substituições de classe derivada de VisitMethodCall método para buscar a expressão que representa a mais interna chamada para Where na árvore de expressão que representa a consulta do cliente. Essa expressão interna é que o provedor extrai os locais de pesquisa de expressão.
Adicionar using diretivas (Imports as instruções em Visual Basic) o arquivo para os namespaces a seguir: System.Collections.Generic, System.Collections.ObjectModel and System.Linq.Expressions.
Para criar o visitante que extrai os dados para consultar o serviço da Web
Adicionar o LocationFinder classe ao seu projeto.
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); } } }
Essa classe é usada para extrair informações de localização do predicado que o cliente passa para Queryable.Where. Ele deriva do ExpressionVisitor classe e substitui apenas a VisitBinary método.
O ExpressionVisitor classe envia binárias expressões, como expressões de igualdade como place.Name == "Seattle" (place.Name = "Seattle" na Visual Basic), para o VisitBinary método. Neste substituindo VisitBinary método, se a expressão corresponde ao padrão da expressão de igualdade que pode fornecer informações sobre o local, que informações são extraídas e armazenadas em uma lista de locais.
Essa classe usa um visitante da árvore de expressão para localizar as informações de localização na árvore de expressão, como um visitante é projetado para percorrer e examinando a árvores de expressão. O código resultante é mais limpo e menos propensa a erros que se tinha sido implementado sem usar o visitante.
Nesse estágio da explicação, seu provedor suporta apenas uma forma limitada de fornecer informações de localização na consulta. Posteriormente no tópico, você irá adicionar funcionalidade para permitir mais maneiras de fornecer informações sobre o local.
Para criar o visitante que modifica a árvore de expressão
Adicionar o ExpressionTreeModifier classe ao seu projeto.
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; } } }
Essa classe deriva do ExpressionVisitor classe e substitui a VisitConstant método. Nesse método, ele substitui a chamada de operador de consulta interna de padrão aplicado ao objeto com uma lista de concreta de Place objetos.
Essa classe de modificador de árvore de expressão usa o visitante da árvore de expressão porque o visitante é projetado para desviar, examinar e copiar as árvores de expressão. Derivando da classe base expressão árvore visitante, essa classe requer código mínimo para executar sua função.
Adicionando o avaliador da expressão
O predicado que é passado para o Queryable.Where método na consulta de cliente pode conter subexpressões que não dependem do parâmetro da expressão lambda. Esses subexpressões isolados podem e devem ser avaliadas imediatamente. Eles poderiam ser referências a variáveis locais ou variáveis de membros que devem ser traduzidas em valores.
A próxima classe expõe um método, PartialEval(Expression), que determina que, se houver, das subárvores na expressão podem ser avaliadas imediatamente. Então, ele avalia as expressões Criando uma expressão lambda, compilar, e invocar o delegado retornado. Finalmente, ele substitui a subárvore com um novo nó que representa um valor constante. Isso é conhecido como avaliação parcial.
Para adicionar uma classe para realizar uma avaliação parcial de uma árvore de expressão
Adicionar o Evaluator classe ao seu projeto.
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; } } } }
Adicionando as Classes do auxiliar
Esta seção contém código para três classes do auxiliar para seu provedor.
Para adicionar a classe auxiliar usada pela implementação de System.Linq.IQueryProvider
Adicionar o TypeSystem classe (ou módulo em Visual Basic) para o seu projeto.
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; } } }
O IQueryProvider implementação que você adicionou anteriormente usa essa classe de auxiliar.
TypeSystem.GetElementTypeusa a reflexão para obter o argumento de tipo genérico de um IEnumerable<T> (IEnumerable(Of T) na Visual Basic) coleção. Este método é chamado de não-genéricas CreateQuery método na implementação de provedor de consulta para fornecer o tipo de elemento da coleção de resultado de consulta.
A classe auxiliar não é específica para esse provedor de serviços da Web do TerraServer EUA. Portanto, pode ser reutilizado por qualquer LINQ provedor.
Para criar uma classe de auxiliar de árvore de expressão
Adicionar o ExpressionTreeHelpers classe ao seu projeto.
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)); } } }
Essa classe contém métodos que podem ser usados para determinar as informações sobre e extrair dados de tipos específicos de árvores de expressão. Esse provedor, esses métodos são usados pela LocationFinder classe para extrair informações de localização da árvore de expressão que representa a consulta.
Para adicionar um tipo de exceção para consultas inválidos
Adicionar o InvalidQueryException classe ao seu projeto.
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; } } } }
Essa classe define um Exception tipo de seu provedor pode lançar quando ela não entende o LINQ consulta a partir do cliente. Definindo esse tipo de exceção de consulta inválido, o provedor pode lançar uma exceção mais específica que apenas Exception de vários lugares no código.
Agora você adicionou todas as peças necessárias para compilar o seu provedor. Construir o LinqToTerraServerProvider de projeto e verifique se não há nenhum erro de compilação.
Testando o provedor LINQ
Você pode testar seu LINQ provedor criando um aplicativo cliente que contém um LINQ consulta sua fonte de dados.
Para criar um aplicativo de cliente para testar o seu provedor.
Adicionar um novo Aplicativo de Console à sua solução de projeto e denomine ClientApp.
No novo projeto, adicione uma referência ao assembly do provedor.
Arraste o App. config arquivo de seu projeto de provedor para o projeto do cliente. (Esse arquivo é necessário para a comunicação com o serviço da Web.)
Observação Em Visual Basic, você talvez tenha que clicar a Mostrar todos os arquivos botão para ver o App. config de arquivo em Solution Explorer.
Adicione o seguinte using instruções (Imports instrução em Visual Basic) para o Program (ou Module1 na Visual Basic) arquivo:
using System; using System.Linq; using LinqToTerraServerProvider;
Imports LinqToTerraServerProvider
No Main método no arquivo Program (ou Module1 na Visual Basic), insira o seguinte código:
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
Esse código cria uma nova instância da IQueryable<T> Digite definido no seu provedor e a consulta o objeto usando LINQ. A consulta Especifica um local para obter dados usando uma expressão de igualdade. Porque a fonte de dados implementa IQueryable, o compilador traduz a sintaxe da expressão de consulta em chamadas para operadores de consulta padrão definidos no Queryable. Internamente, esses métodos operadores de consulta padrão construir uma árvore de expressão e a chamada a Execute ou CreateQuery métodos é implementada como parte de sua IQueryProvider de implementação.
Criar ClientApp.
Definir este aplicativo cliente como "Arranque" projeto para sua solução. Em Solution Explorer, com o botão direito do ClientApp de projeto e selecione Set as StartUp Project.
Execute o programa e exibir os resultados. Deve haver aproximadamente três resultados.
Adicionando recursos de consulta mais complexos
O provedor que você tem este ponto fornece uma maneira muito limitada de clientes especificar informações sobre o local no LINQ de consulta. Especificamente, o provedor só é capaz de obter informações sobre o local a partir de expressões de igualdade, como Place.Name == "Seattle" ou Place.State == "Alaska" (Place.Name = "Seattle" ou Place.State = "Alaska" em Visual Basic).
O procedimento a seguir mostra como adicionar suporte para uma outra maneira de especificar informações de localização. Quando você tiver adicionado esse código, seu provedor de ser capaz de extrair informações sobre o local das expressões de chamada de método, como place.Name.StartsWith("Seat").
Para adicionar suporte para predicados que contêm String. StartsWith
No LinqToTerraServerProvider de projeto, adicione a VisitMethodCall método para o LocationFinder definição de classe.
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); }
Recompilar o LinqToTerraServerProvider project.
Para testar o novo recurso do seu provedor, abra o arquivo Program (ou Module1 na Visual Basic) na ClientApp project. Substitua o código de Main método com o seguinte código:
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
Execute o programa e exibir os resultados. Deve haver aproximadamente 29 resultados.
O procedimento a seguir mostra como adicionar funcionalidade ao seu provedor para habilitar a consulta de cliente especificar informações de localização usando dois métodos adicionais, especificamente Enumerable.Contains e List<T>.Contains. Quando você tiver adicionado esse código, seu provedor de ser capaz de extrair informações sobre o local das expressões de chamada de método na consulta de cliente, como placeList.Contains(place.Name), onde o placeList coleção é uma lista de concreta fornecida pelo cliente. A vantagem de permitir que os clientes usam o Contains método é que eles podem especificar qualquer número de locais, basta adicioná-los para placeList. O número de locais de variação não altera a sintaxe da consulta.
Para adicionar suporte para consultas que tenham o método Contains em seu onde cláusula
No LinqToTerraServerProvider projeto, o LocationFinder definição de classe, substitua o VisitMethodCall método com o seguinte código:
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); }
Este método adiciona cada seqüência da coleção que Contains é aplicado, à lista de locais para consultar o Web service com. Um método chamado Contains é definido em ambas as Enumerable e List<T>. Portanto, o VisitMethodCall método precisa verificar a ambos esses tipos de declaração. Enumerable.Containsé definido como um método de extensão; Portanto, a coleção, a que ela é aplicada é realmente o primeiro argumento do método. List.Containsé definido como um método de instância; Portanto, a coleção, a que ela é aplicada é o objeto de recebimento do método.
Recompilar o LinqToTerraServerProvider project.
Para testar o novo recurso do seu provedor, abra o arquivo Program (ou Module1 na Visual Basic) na ClientApp project. Substitua o código de Main método com o seguinte código:
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
Execute o programa e exibir os resultados. Deve haver aproximadamente 5 resultados.
Próximas etapas
Este tópico de explicação passo a passo mostrou como criar um LINQ provedor para um método de um serviço da Web. Se você deseja buscar o desenvolvimento adicional de um LINQ provedor, considere essas possibilidades:
Habilitar o LINQ provedor para tratar de outras maneiras de especificar um local na consulta do cliente.
Investigue os outros métodos que EUA TerraServer Web service expõe e criar um LINQ provedor que interfaces com um desses métodos.
Localizar um serviço da Web diferente que você está interessado e cria um LINQ provedor de proprietário.
Criar um LINQ provedor para uma fonte de dados diferente de um serviço da Web.
Para obter mais informações sobre como criar seu próprio provedor LINQ, consulte LINQ: A criação de um provedor IQueryable nos Blogs do MSDN.
Consulte também
Tarefas
Como: Modificar as árvores de expressão (C# e Visual Basic)
Referência
Conceitos
A ativação de uma fonte de dados para a consulta do LINQ
Os serviços do Windows Communication Foundation e serviços de dados do WCF em Visual Studio