Ao efetuar uma requisição para algum recurso sobre o protocolo HTTP, o servidor identifica o mesmo, faz o processamento, gera o resultado e, finalmente, devolve o resultado para o cliente que fez a solicitação. Por mais que isso não fica explícito, o conteúdo que trafega do cliente para o servidor (requisição) e do servidor para o cliente (resposta), sempre possui um formato específico.
Em grande parte de todos os recursos fornecidos através do protocolo HTTP, uma das necessidades é justamente definir o formato deste conteúdo, que por sua vez, direciona a interpretação pelo navegador, por uma outra aplicação ou até mesmo de uma biblioteca, permitindo efetuar o parser do resultado e, consequentemente, materializar o mesmo em algo “palpável”/visível.
Os formatos são representados por uma simples string, onde você tem uma primeira parte para descrever qual o tipo de conteúdo, e depois o seu formato. Por exemplo, ao invocar uma página onde o retorno é um conteúdo HTML, o formato será definido como text/html; ao solicitar uma imagem, o seu formato será definido como image/jpeg. Uma lista contendo todos os formatos de conteúdos disponíveis na internet, é gerenciada e mantida por entidade chamada IANA, e pode ser acessada aqui.
Como o WCF Web API tem uma forte afinidade com as características do HTTP, ele permite receber ou gerar conteúdos em formatos popularmente conhecidos pelo mercado. A finalidade deste artigo é mostrar como podemos proceder para configurar, utilizar e customizar os media types para os serviços. Em primeiro lugar, precisamos analisar a estrutura da classe que representa um media type. Para isso, a Microsoft criou uma classe abstrata chamada de MediaTypeFormatter, e já existem algumas implementações definidas dentro da API, como por exemplo, as classes XmlMediaTypeFormatter e JsonMediaTypeFormatter. A imagem abaixo mostra essa herança:
Como podemos perceber, os serviços que criamos utilizando essa nova API nada sabem sobre o formato em que ele chegou ou o formato em que ele será devolvido para o cliente. Na verdade o que ocorre é que os media types já são acoplados à execução, e para isso, a Microsoft utilizou um recurso de estensibilidade desta API, que são os operation handlers. Existem duas classes públicas chamadas RequestContentHandler e ResponseContentHandler, e como os próprios nomes dizem, fazem parte da requisição e da resposta, respectivamente.
Essas duas classes possuem uma propriedade chamada Formatters, que é uma coleção do tipo MediaTypeFormatterCollection. Ambos handlers já possuem as classes XmlMediaTypeFormatter (application/xml) e JsonMediaTypeFormatter (application/json) adicionadas por padrão. Através da imagem abaixo podemos visualizar onde se encontram cada um destes objetos que vimos acima:
Olhando para a imagem acima, uma pergunta que aparece é como o WCF escolhe qual dos formatadores utilizar. A escolha se baseia no formato solicitado pelo cliente. O formato pode ser incluído como um item na requisição, através do header Accept ou do Content-Type. O WCF escolhe o formatador de acordo com o valor que ele encontra em um desses headers, e caso o formato definido não for encontrado, o padrão é sempre devolver o conteúdo em formato Xml.
Customização
Apesar do Xml e Json serem os principais formatos que temos atualmente, pode haver situações onde desejamos criar um formato próprio, para que esse possa gerar e/ou receber o conteúdo em um formato específico. Como já percebemos acima, se desejarmos fazer isso, temos que recorrer à implementação da classe abstrata MediaTypeFormatter, customizando basicamente dois métodos principais OnReadFromStream e OnWriteToStream. Vamos analisar cada um desses métodos abaixo:
-
OnCanReadType: Dado um objeto do tipo Type, este método retorna um valor boleano (onde o padrão é True), indicando se aquele tipo é ou não entendido por aquele formatador. O tipo identifica os eventuais parâmetros que existem no método do serviço. Aqui podemos fazer validações, como por exemplo, identificar se o tipo é ou não serializável, se possui um determinado atributo, etc.
-
OnReadFromStream: Se o método acima retornar True, então este método é executado. Como parâmetro ele recebe o tipo do objeto (o mesmo que foi passado para o método acima), e um objeto do tipo Stream, que é fonte das informações (estado) do objeto a ser criado. Este método retorna um object, que corresponde à um objeto criado dinamicamente e configurado com os valores provenientes do Stream.
-
OnCanWriteType: Dado um objeto do tipo Type, este método retorna um valor boleano (padrão é True), indicando se aquele tipo é ou não entendido pelo formatador. O tipo identifica o retorno do método do serviço. Aqui podemos fazer validações, como por exemplo, identificar se o tipo é ou não serializável, se possui um determinado atributo, etc.
-
OnWriteToStream: Se o método acima retornar True, então este método é executado. Como parâmetro, ele recebe o tipo do objeto a ser serializado e um object que corresponde a instância do objeto a ser gravado. Ainda recebemos um Stream, que é o destino do objeto serializado.
Para podemos exemplificar, vamos criar um formatador específico para o formato CSV, que é um padrão bem tradicional, onde cada valor é separado pelo caracter “;”. Abaixo temos a classe CsvMediaTypeFormatter, que herda de MediaTypeFormatter. O que vemos de interessante no trecho de código abaixo, é a definição do formato application/csv sendo adicionado à coleção de media types suportados por este formatador customizado, que está acessível através da propriedade SupportedMediaTypes.
public class CsvMediaTypeFormatter : MediaTypeFormatter
{
private const char SEPARATOR = ‘;’;
public CsvMediaTypeFormatter()
{
this.SupportedMediaTypes.Add(new MediaTypeHeaderValue(“application/csv”));
}
public override object OnReadFromStream(Type type, Stream stream, HttpContentHeaders contentHeaders)
{
//implementação
}
public override void OnWriteToStream(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext context)
{
//implementação
}
}
A implementação é bastante trivial, e optei por não colocar toda ela aqui por questões de espaço. A implementação na íntegra está no arquivo que está vinculado à este artigo. De qualquer forma, ao gerar um conteúdo, estou analisando se o objeto é ou não uma coleção; se for, fazemos um trabalho diferenciado para capturar o tipo concreto do elemento que temos. Depois de chegar no elemento concreto, percorremos os itens da coleção, extraindo via Reflection, os valores das propriedades de cada item. Caso seja um objeto único, apenas extraímos as suas respectivas propriedades. Para ambos os casos, depois de encontrar o tipo concreto, percorremos as propriedades capturando os respectivos nomes, para que seja possível gerar o header do conteúdo.
Já quando lemos o conteúdo que está chegando ao serviço, extraímos a primeira linha (header do conteúdo) para capturar as propriedades que temos dentro do corpo da mensagem. Em seguida, se for uma coleção, criamos dinamicamente a instância de um objeto que implementa a interface IList, e para cada linha subsequente, criamos a instância do tipo concreto, adicionando na coleção criada no passo anterior. Caso seja um objeto único, tudo o que fazemos é instânciá-lo, configurar as propriedades e retorná-lo ao WCF.
Como já era esperado, o que precisamos fazer a partir de agora é instalá-lo à execução. E para isso, recorremos à classe HttpHostConfiguration, que através do método AddFormatters, podemos incluir classes que herdam de MediaTypeFormatter. O que fazemos aqui é instanciar e adicionar a classe CsvMediaTypeFormatter:
protected void Application_Start(object sender, EventArgs e)
{
RouteTable.Routes.MapServiceRoute<ServicoDeTeste>(
“teste”,
HttpHostConfiguration.Create().AddFormatters(new CsvMediaTypeFormatter()));
}
Para exemplificar, temos alguns métodos definidos no serviço que manda e/ou recebe instância(s) da classe Informacao. Novamente estou omitindo parte da implmentação para poupar espaço. Abaixo temos as assinaturas de cada um desses métodos:
[ServiceContract]
public class ServicoDeTeste
{
[WebGet]
public List<Informacao> Exemplo1() { }
[WebInvoke]
public List<Informacao> Exemplo2(List<Informacao> info) { }
[WebGet]
public Informacao Exemplo3() { }
[WebInvoke]
public Informacao Exemplo4(Informacao info) { }
public class Informacao
{
public string Dado { get; set; }
public string Codigo { get; set; }
}
}
Finalmente, abaixo temos o log salvo pelo Fiddler das requisições efetuadas para cada um dos métodos de exemplo. Repare que quando enviamos uma requisição via GET, definimos o header Accept como application/csv, para que o WCF utilize o formatador customizado que criamos. Ao enviar uma requisição via POST, também definimos o header Content-Type como application/csv, para indicar ao WCF que o conteúdo está no formato CSV, e deverá utilizar o formatador customizado que entenda este conteúdo. É interessante notar também que no último log, onde omitimos o header Accept, e o resultado está sendo devolvido em Xml, que conforme foi comentado acima, é o formatador padrão.
Exemplo 1 – Requisição
GET http://localhost:2025/teste/Exemplo1 HTTP/1.1
User-Agent: Fiddler
Host: localhost:2025
Accept: application/csv
Content-Length: 0
Exemplo 1 – Resposta
HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Wed, 04 May 2011 01:59:48 GMT
X-AspNet-Version: 4.0.30319
Content-Length: 88
Cache-Control: private
Content-Type: application/csv
Connection: Close
Dado;Codigo
Alguma Info;0
Alguma Info;1
Alguma Info;2
Alguma Info;3
Alguma Info;4
————————————————————-
Exemplo 2 – Requisição
POST http://localhost:2025/teste/Exemplo2 HTTP/1.1
User-Agent: Fiddler
Host: localhost:2025
Content-Type: application/csv
Content-Length: 39
Dado;Codigo
Info1;111111
Info2;222222
Exemplo 2 – Resposta
HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Wed, 04 May 2011 02:04:42 GMT
X-AspNet-Version: 4.0.30319
Content-Length: 41
Cache-Control: private
Content-Type: application/csv
Connection: Close
Dado;Codigo
Info1;111111
Info2;222222
————————————————————-
Exemplo 3 – Requisição
GET http://localhost:2025/teste/Exemplo3 HTTP/1.1
User-Agent: Fiddler
Host: localhost:2025
Accept: application/csv
Exemplo 3 – Resposta
HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Wed, 04 May 2011 02:06:54 GMT
X-AspNet-Version: 4.0.30319
Content-Length: 30
Cache-Control: private
Content-Type: application/csv
Connection: Close
Dado;Codigo
Alguma Info;334
————————————————————-
Exemplo 4 – Requisição
POST http://localhost:2025/teste/Exemplo4 HTTP/1.1
User-Agent: Fiddler
Host: localhost:2025
Content-Type: application/csv
Content-Length: 29
Dado;Codigo
Algum Teste;1981
Exemplo 4 – Resposta
HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Wed, 04 May 2011 02:08:32 GMT
X-AspNet-Version: 4.0.30319
Content-Length: 41
Cache-Control: private
Content-Type: application/csv
Connection: Close
Dado;Codigo
Algum Teste ping;1981 ping
————————————————————-
Exemplo 1 (Xml) – Requisição
POST http://localhost:2025/teste/Exemplo4 HTTP/1.1
User-Agent: Fiddler
Host: localhost:2025
Exemplo 1 (Xml) – Resposta
HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Wed, 04 May 2011 02:13:36 GMT
X-AspNet-Version: 4.0.30319
Content-Length: 412
Cache-Control: private
Content-Type: application/xml; charset=utf-8
Connection: Close
<?xml version=”1.0″ encoding=”utf-8″?>
<ArrayOfInformacao>
<Informacao>
<Dado>Alguma Info</Dado>
<Codigo>0</Codigo>
</Informacao>
<Informacao>
<Dado>Alguma Info</Dado>
<Codigo>1</Codigo>
</Informacao>
<Informacao>
<Dado>Alguma Info</Dado>
<Codigo>2</Codigo>
</Informacao>
<Informacao>
<Dado>Alguma Info</Dado>
<Codigo>3</Codigo>
</Informacao>
<Informacao>
<Dado>Alguma Info</Dado>
<Codigo>4</Codigo>
</Informacao>
</ArrayOfInformacao>