Desenvolvimento - Web Services
WCF - Serviços RESTFul
A versão 3.5 do Windows Communication Foundation introduziu uma nova forma de expor e consumir serviços. Esse novo modelo, também conhecido como Web Programming Model, permite o consumo destes serviços através dos mais variados clientes, como é o caso dos navegadores.
por Israel AéceA versão 3.5 do Windows Communication Foundation introduziu uma nova forma de expor e consumir serviços. Esse novo modelo, também conhecido como Web Programming Model, permite o consumo destes serviços através dos mais variados clientes, como é o caso dos navegadores.
Como sabemos, o WCF implementa vários protocolos (padrões) WS-* sob o SOAP, como é o caso de transações, mensagens confiáveis, etc.. Desta forma, sempre é necessário a construção deste envelope SOAP para realizar a requisição e, também através de um envelope SOAP, o serviço nos envia a resposta do processo executado e isso tudo é abstraído pelo runtime do WCF ou qualquer outro framework.
Assim como os antigos Web Services (ASMX), essa nova funcionalidade permite ao WCF expor um serviço e ser acessado a partir do protocolo HTTP diretamente, sem a necessidade da criação de um envelope SOAP para isso. Este modelo tem algumas características, como o suporte as operações do serviço via métodos GET e POST do HTTP; as URIs identificam o endpoint e que operação deve invocar. Os possíveis parâmetros que o método exige são recuperados da QueryString ou do corpo da mensagem (isso depende do método utilizado). Além disso, este modelo ainda permite a serialização da requisição/resposta em XML ou JSON.
Para fazer o uso desta nova funcionalidade, além de precisa referenciar o Assembly System.ServiceModel.dll, é preciso também adicionar a referência para o Assembly System.ServiceModel.Web.dll que foi introduzido a partir da versão 3.5 do .NET Framework. Este novo Assembly trará uma série de tipos (classes, interfaces e atributos) que serão utilizados para a configuração e execução de um serviço a ser exposto desta forma. Inicialmente analisaremos os tipos relacionados à configuração do serviço e, em seguida, os tipos relacionados a execução do serviço.
O primeiro detalhe que precisamos nos atentar é com relação a dois atributos de configuração que devem ser aplicados a interface do contrato de serviço: WebGetAttribute e WebInvokeAttribute. Esse atributos devem ser utilizados em conjunto com o atributo OperationContractAttribute ao invés de substituí-lo.
Os serviços que são expostos no formato Web fazem o uso de verbos como o HTTP GET para recuperação de dados e, uso de verbos como o HTTP POST para efetuar a invocação de um determinado método, assim como já comentamos acima. Os atributos WebGetAttribute e WebInvokeAttribute permite ao desenvolvedor controlar (de forma individual) o formato da URI a ser exposta e como associar um determinado verbo com as operações que o serviço (contrato) fornece.
Quando decoramos um método com o atributo WebGetAttribute, possibilitamos que ele seja invocado via HTTP GET. Com esse atributo e mais algumas configurações que veremos mais adiante, é possível invocarmos esse método a partir de uma URI, sem a necessidade do envelope SOAP e, automaticamente, o runtime do WCF faz o mapeamento do método e possíveis parâmetros e, em seguida, encaminha a requisição para o respectivo método/serviço. Além disso, esse atributo possui várias propriedades que merecem destaque e, estão abordadas na tabela abaixo:
|
Para finalizarmos os atributos de configuração, ainda temos o atributo WebInvokeAttribute. Este atributo deve ser aplicado a operações que devem estar disponíveis via operações HTTP, como POST, PUT ou DELETE. Este atributo fornece as mesmas propriedades do atributo WebGetAttribute, incluindo mais uma propriedade chamada Method, descrita detalhadamente através da tabela abaixo:
|
Com o conhecimento destes dois principais atributos, neste momento devemos criar a Interface do contrato de serviço que será exposto. O contrato de exemplo terá apenas dois métodos: Recuperar e Adicionar que, já são auto-explicativos. A idéia é que no primeiro deles, Recuperar, devemos permitir o acesso via HTTP GET e, na segunda opção, utilizar o padrão HTTP POST, para submeter um novo registro. O trecho de código abaixo exibe na integra o contrato a ser utilizado como exemplo, já com os devidos atributos configurados:
using System; using System.ServiceModel; using System.ServiceModel.Web; [ServiceContract] public interface IUsuario { [OperationContract] [WebGet(UriTemplate = "RecuperarUsuarios/{quantidade}")] string[] Recuperar(string quantidade); [OperationContract] [WebInvoke(UriTemplate = "AdicionarNovoUsuario/{nome}/{email}")] bool Adicionar(string nome, string email); } Imports System Imports System.ServiceModel Imports System.ServiceModel.Web <ServiceContract()> Public Interface IUsuario <OperationContract(), WebGet(UriTemplate:="RecuperarUsuarios/{quantidade}")> _ Function Recuperar(ByVal quantidade As String) As String() <OperationContract(), WebInvoke(UriTemplate:="AdicionarNovoUsuario/{nome}/{email}")> _ Function Adicionar(ByVal nome As String, ByVal email As String) As Boolean End Interface |
|||
C# | VB.NET |
Neste momento devemos nos atentar na propriedade UriTemplate. Como comentamos acima, ela defina a template da URI a ser invocada. É a partir dela que podemos especificar o nome da operação que o cliente deverá invocar - podendo ou não ser o mesmo nome do método; além disso, podemos também estruturar os parâmetros que esta operação recebe, organizando-os em QueryStrings ou até mesmo com barras "/", como é o caso do exemplo. Há apenas um detalhe com relação aos parâmetros que devemos nos atentar: os parâmetros da operação deverão estar dentro da URI, envolvidos entre chaves. Automaticamente, quando isso for executado, o runtime do WCF se encarregará de extrair os valores do HTTP (do corpo ou da URL) e encaminhar para a respectiva operação, populando cada um desses parâmetros.
As mudanças no contrato acabam neste momento. A implementação do mesmo na classe concreta não tem nenhuma diferença. Contextualmente, temos uma classe chamada WebOperationContext que expõe uma propriedade chamada Current que retorna o mesmo tipo, representando o contexto da chamada atual. Basicamente essa classe disponibiliza quatro propriedades: IncomingRequest, IncomingResponse, OutgoingRequest e OutgoingResponse que, como os próprios nomes dizem, trazem informações a respeito do contexto da requisição e da resposta referente a execução da operação corrente.
Em nível de execução, temos algumas pequenas mudanças que precisamos nos atentar. A primeira e mais notável é com relação a criação do hosting que será utilizado para expor o serviço. Ao contrário de outros serviços que são expostos para diversos outros bindings, aqui utilizaremos o WebServiceHost ao invés do ServiceHost. Basicamente esse host mais especializado herda diretamente da classe ServiceHost, customizando o método OnOpening, desabilitando qualquer acesso aos metadados ou página de suporte; cria e configura endpoints para todos os tipos do contrato com o binding WebHttpBinding e adiciona o WebHttpBehavior para todos os endpoints para que as operações Get e Invoke trabalhem sem qualquer configuração adicional.
Outro detalhe importante é com relação ao tipo de binding. Dentro do Assembly System.ServiceModel.Web.dll é disponibilizado um novo binding, chamado de WebHttpBinding. Este binding permite expor os serviços através de requisições HTTP ao invés da criação do envelope SOAP. A utilização deste binding é implícita quando se utiliza o WebServiceHost, pois ele internamente o cria. É importante dizer que nada impede de criar uma instância da classe ServiceHost e configurá-la para expor o serviço via HTTP e, para isso, será necessário que explicitamente você crie um endpoint definindo como binding uma instância da classe WebHttpBinding.
Com essas considerações, as configurações do host ficam muito simples. Basta instanciar a classe WebServiceHost, especificando o nome da classe que implementa o contrato e também o endereço HTTP em que o serviço estará disponível. O código abaixo ilustra essa configuração:
using System; using System.ServiceModel; using System.ServiceModel.Web; using (WebServiceHost host = new WebServiceHost(typeof(Usuarios), new Uri[] { new Uri("http://localhost:8892") })) { host.Open(); Console.ReadLine(); } Imports System Imports System.ServiceModel Imports System.ServiceModel.Web Using host As New WebServiceHost(GetType(Usuarios), _ New Uri() {New Uri("http://localhost:8892")}) host.Open() Console.ReadLine() End Using |
|||
C# | VB.NET |
Já que estamos com o host devidamente configurado, podemos iniciá-lo e invocar o endereço a partir do navegador para que possamos visualizar o resultado. De acordo com a configuração que efetuamos no contrato (Interface), podemos invocar a operação RecuperarUsuarios através do seguinte endereço: http://localhost:8892/RecuperarUsuarios/10. Podemos notar de que acordo com a especificação colocada na propriedade UriTemplate do atributo WebGetAttribute, depois do nome do método devemos especificar a quantidade. O resultado é mostrado abaixo com as duas possibilidades de saída (Xml e JSON):
ResponseFormat = WebMessageFormat.Json [ "Usuario 1","Usuario 2","Usuario 3","Usuario 4","Usuario 5", "Usuario 6","Usuario 7","Usuario 8","Usuario 9","Usuario 10", ] ResponseFormat = WebMessageFormat.Xml <ArrayOfstring> <string>Usuario 1</string> <string>Usuario 2</string> <string>Usuario 3</string> <string>Usuario 4</string> <string>Usuario 5</string> <string>Usuario 6</string> <string>Usuario 7</string> <string>Usuario 8</string> <string>Usuario 9</string> <string>Usuario 10</string> </ArrayOfstring> |
|||
Resultado |
A partir do navegador podemos nos certificar de que o retorno está conforme o esperado. Podemos agora, através do próprio .NET criarmos código para efetuar a requisição para este mesmo método e recuperar o retorno a partir de uma aplicação .NET (ou qualquer outra, desde que faça o acesso via HTTP para o serviço). Basicamente temos que recorrer a classes já bastante conhecidas, como é o caso da WebRequest e WebResponse que formula a requisição e trata a resposta, respectivamente. O trecho de código abaixo como podemos proceder para invocar o método RecuperarUsuarios a partir da aplicação .NET:
using System; using System.IO; using System.Net; WebRequest request = WebRequest.Create("http://localhost:8892/RecuperarUsuarios/10"); using (WebResponse response = request.GetResponse()) { using (StreamReader sr = new StreamReader(response.GetResponseStream())) { Console.WriteLine(sr.ReadToEnd()); } } Imports System Imports System.ServiceModel Imports System.ServiceModel.Web Dim request As WebRequest = _ WebRequest.Create("http://localhost:8892/RecuperarUsuarios/10") Using response As WebResponse = request.GetResponse() Using sr As New StreamReader(response.GetResponseStream()) Console.WriteLine(sr.ReadLine()) End Using End Using |
|||
C# | VB.NET |
Em nenhum dos testes acima invocamos o segundo método, AdicionarNovoUsuario. Isso porque, pelo fato desta operação estar decorada com o atributo WebInvokeAttribute ele obrigatoriamente precisa ser invocada através do método POST do HTTP. Para isso, precisamos mudar o código acima para que atenda a essa necessidade.
Basicamente o que precisamos fazer é definir as propriedades Method, ContentLength e ContentType da classe WebRequest. Na primeira propriedade devemos especificar o tipo da requisição que, neste caso, será "POST"; já a propriedade ContentLength precisamos especificar o comprimento dos dados que farão parte da respectiva requisição e, finalmente, a propriedade ContentType especificamos o tipo de conteúdo que está sendo enviado. O código que temos acima muda ligeiramente para:
using System; using System.IO; using System.Net; WebRequest request = WebRequest.Create( "http://localhost:8892/AdicionarNovoUsuario/Israel/israel@projetando.net" ); request.Method = "POST"; request.ContentLength = 0; using (WebResponse response = request.GetResponse()) { using (StreamReader sr = new StreamReader(response.GetResponseStream())) { Console.WriteLine(sr.ReadToEnd()); } } Imports System Imports System.ServiceModel Imports System.ServiceModel.Web Dim request As WebRequest = _ WebRequest.Create("http://localhost:8892/AdicionarNovoUsuario/Israel/israel@projetando.net") request.Method = "POST" request.ContentLength = 0 Using response As WebResponse = request.GetResponse() Using sr As New StreamReader(response.GetResponseStream()) Console.WriteLine(sr.ReadLine()) End Using End Using |
|||
C# | VB.NET |
RAW do HTTP POST
Este código funciona bem, mas quando desejamos enviar o conteúdo através do RAW do HTTP POST (que envia a requisição com o content-type definido como "application/x-www-form-urlencoded"), demandará algumas mudanças, inclusive em nível de contrato do serviço. Para que isso seja possível, primeiramente precisamos mudar a assinatura do método da operação; ao invés de aceitar os parâmetros tradicionais, teremos que fazê-la receber um objeto do tipo Stream. A mesma interface que utilizamos acima, para suportar esse novo modelo, deve ficar da seguinte forma:
using System; using System.IO; using System.ServiceModel; using System.ServiceModel.Web; [ServiceContract] public interface IUsuario { [OperationContract] [WebGet(UriTemplate = "RecuperarUsuarios/{quantidade}")] string[] Recuperar(string quantidade); [OperationContract] [WebInvoke(UriTemplate = "AdicionarNovoUsuario")] bool Adicionar(Stream s); } Imports System Imports System.IO Imports System.ServiceModel Imports System.ServiceModel.Web <ServiceContract()> Public Interface IUsuario <OperationContract(), WebGet(UriTemplate:="RecuperarUsuarios/{quantidade}")> _ Function Recuperar(ByVal quantidade As String) As String() <OperationContract(), WebInvoke(UriTemplate:="AdicionarNovoUsuario")> _ Function Adicionar(ByVal s As Stream) As Boolean End Interface |
|||
C# | VB.NET |
Além dessa mudança, a sua implementação também é um pouco diferente. O Stream que é passado retorna todos os dados contidos no corpo da requisição HTTP. Isso nos obrigará a fazer um parser deste conteúdo e, felizmente, há uma classe dentro do .NET que, dado uma string, retorna uma coleção de chave-valor com essas informações. Abaixo é exibido apenas a implementação do método que é alvo da discussão por questões de espaço:
using System; using System.IO; using System.Web; using System.Collections.Generic; using System.Collections.Specialized; public class Usuarios : IUsuario { public bool Adicionar(Stream s) { NameValueCollection coll = HttpUtility.ParseQueryString(new StreamReader(s).ReadToEnd()); string nome = coll["nome"]; string email = coll["email"]; //adicionar em algum repositório return true; } } Imports System Imports System.IO Imports System.Web Imports System.Collections.Generic Imports System.Collections.Specialized Public Class Usuarios Implements IUsuario Public Function Adicionar(ByVal s As stream) As Boolean Implements IUsuario.Adicionar Dim coll As NameValueCollection = _ HttpUtility.ParseQueryString(New StreamReader(s).ReadToEnd()) Dim nome As String = coll("nome") Dim email As String = coll("email") Return True End Function End Class |
|||
C# | VB.NET |
Infelizmente neste momento, a referência para o Assembly System.Web.dll se faz necessário porque a classe HttpUtility está definida dentro dele. A sua utilidade é justamente fazer o parser em uma string, retornando a coleção com chave-valor e, conseqüentemente, podendo utilizá-la da forma que acharmos conveniente.
Finalmente, para que seja possível executar o serviço a partir de uma aplicação, precisamos modificar a forma como passamos os parâmetros para a mesma. É necessário escrevermos o conteúdo que será enviado no corpo da requisição. Primeiramente precisamos definir em um formato de chave-valor todos os parâmetros que o método exige. Depois disso, com a string totalmente formada, utilizando & para separar cada um dos parâmetros, precisamos convertê-la em um array de bytes e atribuirmos na requisição. O código abaixo exibe tal configuração:
using System; using System.IO; using System.Net; using System.Text; string parameters = "nome=Israel Aece&email=israel@projetando.net"; byte[] body = Encoding.Default.GetBytes(parameters); WebRequest request = HttpWebRequest.Create("http://localhost:8892/AdicionarNovoUsuario"); request.Method = "POST"; request.ContentType = "application/x-www-form-urlencoded"; request.ContentLength = body.Length; request.GetRequestStream().Write(body, 0, body.Length); using (WebResponse response = request.GetResponse()) { using (StreamReader sr = new StreamReader(response.GetResponseStream())) { Console.WriteLine(sr.ReadToEnd()); } } Imports System Imports System.IO Imports System.Net Imports System.Text Dim parameters As String = "nome=Israel Aece&email=israel@projetando.net" Dim body() As Byte = Encoding.Default.GetBytes(parameters) Dim request As WebRequest = _ WebRequest.Create("http://localhost:8892/AdicionarNovoUsuario") request.Method = "POST" request.ContentType = "application/x-www-form-urlencoded" request.ContentLength = body.Length request.GetRequestStream().Write(body, 0, body.Length) Using response As WebResponse = request.GetResponse() Using sr As StreamReader = New StreamReader(response.GetResponseStream()) Console.WriteLine(sr.ReadToEnd()) End Using End Using |
|||
C# | VB.NET |
Além da exigência de definir a propriedade ContentType com "application/x-www-form-urlencoded", a diferença mais considerável no código acima é com relação ao método GetRequestStream. Este método retorna uma instância de um Stream que representa o corpo da requisição e é dentro dele que devemos escrever o conteúdo (os parâmetros) que a operação irá necessitar. Automaticamente o WCF é capaz de enviar este Stream para o serviço e, internamente, extraímos o conteúdo do mesmo como vimos mais acima.
Conclusão: Este artigo explica detalhadamente como podemos expor nossos serviços para serem invocados a partir de uma URL, sem a necessidade do overhead do protocolo SOAP. Muitas ferramentas que a Microsoft está criando, fazem uso deste recurso que, em um primeiro momento, apesar de parecer pouco útil, é extremamente válido e flexível quando precisamos expor os serviços e/ou operações a partir do HTTP, como é o caso de Blogs, RSS, etc..
- Verificando disponibilidade de um serviço WCF ou WebServiceC#
- Criando um WebService com ASP.NET Razor e WebMatrixWeb Services
- Construindo um List Suggest com ASP.NET Web Services e JQueryASP. NET
- Implementando um Processo de Negócio com BPELWeb Services
- Criando e consumindo um Serviço Web usando o Visual Studio 2008Web Services