Quando trabalhamos com serviços baseados em HTTP, ele são baseados em um modelo de requisição e resposta, ou seja, para cada solicitação que fazemos ao serviço, ele retorna uma resposta contendo o status ou o resultado da sua solicitação. E ainda falando especificamente sobre o protocolo HTTP, a cada mensagem trocada (na requisição ou na resposta), ela contém um cabeçalho e o corpo da mensagem. O cabeçalho é apenas um dicionário de chave e valor, com informações contextuais e o corpo da mensagem é opcional e pode trazer dados que estão sendo postados ou retornados, serializados em um formato específico.
Independentemente do que esteja dentro da requisição ou da resposta, tudo isso é computado como dados que estão sendo trafegados entre o cliente o serviço, e quando estamos em um ambiente onde o custo da rede é significativo (tanto em termos de performance quanto de custo (se estivermos falando em plano de dados de alguma operadora celular)) podemos ter surpresas durante o funcionamento da aplicação que o consome.
Para tentar reduzir isso, a Microsoft implementou no ASP.NET Web API 2 a possibilidade de realizar requisições em batch. A finalidade desta funcionalidade é basicamente permitir ao cliente empacotar múltiplas requisições em apenas uma, enviar ao serviço que o receberá e entregará as várias requisições para suas respectivas ações (métodos). Depois de processado, o resultado que é devolvido ao cliente também será um outro pacote, contendo o resultado para cada uma das requisições enviadas e processadas. Ambas bibliotecas do ASP.NET Web API (cliente e serviço) já estão preparadas para interagir com este tipo de serviço.
Para exemplificar, vamos utilizar uma API que já está disponível ao criar um projeto ASP.NET Web API: ValuesController. Vou omitir a implementação por questões de espaço, mas o que cada método faz é manipular a coleção estática chamada dados que está declarada no interior desta classe.
public class ValuesController : ApiController
{
private static List<string> dados = new List<string>();
public IEnumerable<string> Get() { }
public string Get(int id) { }
public void Post([FromBody]string value) { }
public void Put(int id, [FromBody]string value) { }
public void Delete(int id) { }
}
O primeiro passo é configurar o serviço para que ele aceite requisições em batch. Tudo começa com a inclusão de um rota que receberá este tipo especial de requisição. E para isso vamos recorrer ao método MapHttpBatchRoute, definindo como handler o DefaultHttpBatchHandler. É este handler, que recebe como parâmetro a instância do HttpServer que ele utilizará para tratar e processar as mensagens internamente, gerando os respectivos resultados e devolvendo para o cliente.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpBatchRoute(
“BatchApi”,
“api/batch”,
new DefaultHttpBatchHandler(GlobalConfiguration.DefaultServer));
config.Routes.MapHttpRoute(
name: “DefaultApi”,
routeTemplate: “api/{controller}/{id}”,
defaults: new { id = RouteParameter.Optional });
}
}
Uma preocupação que é sempre levantanda com tudo que é processado em batch, é a sequência em que isso é feito. Por padrão, o DefaultHttpBatchHandler utiliza o modelo sequencial, que como o prório nome sugere, executa as requisições na mesma ordem em que elas foram empacotadas, sendo que ele somente processará a requisições seguinte depois que a anterior finalizar. Caso a ordem não seja relevante para o processamento das requisições, então podemos configurar o handler para realizar o processamento de forma não sequencial e, consequentemente, de forma assíncrona. Para isso, basta recorrer à propriedade ExecutionOrder, escolhendo uma das opções expostas pelo enumerador BatchExecutionOrder:
config.Routes.MapHttpBatchRoute(
“BatchApi”,
“api/batch”,
new DefaultHttpBatchHandler(GlobalConfiguration.DefaultServer)
{
ExecutionOrder = BatchExecutionOrder.NonSequential
});
Com isso o serviço está pronto para receber requisições em batch. Agora compente aos clientes também se ajustarem para conseguir enviar e receber requisições neste formato. Como já foi mencionado acima, a biblioteca do ASP.NET Web API para consumo de serviços HTTP já possui suporte para isso. A implementação deste recurso foi baseada no tipo de conteúdo conhecido como multipart, que é quando um ou mais conjuntos de dados são combinados em um único body. Assim como uma mensagem HTTP é composta por um cabeçalho, uma linha em branco como separador e um corpo, cada conjunto de dados do multipart (denominado body parts) tem um cabeçalho, uma linha em branco e um corpo. Cada uma dessas partes também possui uma espécie de chave (boundary) que é utilizado para associar essas partes.
O uso por parte do cliente consiste instanciar a classe MultipartContent e adicionar nela as partes, que neste caso são requisições para um mesmo serviço/API. A classe HttpMessageContent recebe em seu construtor a instância da classe HttpRequestMessage, que como sabemos, representa uma requisição HTTP, e devemos realizar a configuração dela como se estivéssemos fazendo a configuração para submete-la diretamente para o HttpClient, mas não é o caso.
private static async Task Run()
{
using (var client = new HttpClient())
{
var requests =
new MultipartContent(“mixed”, “batch_” + Guid.NewGuid().ToString());
requests.Add(
new HttpMessageContent(
new HttpRequestMessage(HttpMethod.Post, “http://localhost:4467/api/values”)
{
Content = new ObjectContent<string>(“Israel”, new JsonMediaTypeFormatter())
}));
requests.Add(
new HttpMessageContent(
new HttpRequestMessage(HttpMethod.Get, “http://localhost:4467/api/values”)));
using (var br = await client.SendAsync(
new HttpRequestMessage(HttpMethod.Post, “http://localhost:4467/api/batch”)
{
Content = requests
}))
{
var responses = await br.Content.ReadAsMultipartAsync();
var postResponse = await responses.Contents[0].ReadAsHttpResponseMessageAsync();
var getResponse = await responses.Contents[1].ReadAsHttpResponseMessageAsync();
foreach (var item in await getResponse.Content.ReadAsAsync<IEnumerable<string>>())
Console.WriteLine(item);
}
}
}
É importante notar que uma das mensagens está postando o nome “Israel” para que seja adicionado à coleção, e logo na sequência, estamos empacotando também a chamada para o método Get, que deverá retornar todos os nomes adicionados. Finalmente, depois das requisições que desejamos realizar empacotadas no MultipartContent, então criamos uma terceira requisição (HttpRequestMessage) que levará em seu corpo as requisições POST e GET que configuramos e, como já era de se esperar, esta terceira requisição deve ser direcionada para o endpoint de batch que configuramos no serviço, que saberá como tratar esse tipo de mensagem.
Depois da requisição realizada pelo HttpClient e devolvida pelo serviço, podemos capturar através do método ReadAsMultipartAsync o conteúdo (resposta) da requisição que foi realizada para o endpoint de batch. Da mesma forma que a requisição, a resposta desta requisição também contém em seu interior a resposta para cada um métodos que foram empacotados, e justamente por isso, devemos recorrer à coleção chamada Contents, que através do índice podemos extrair cada uma das respostas que desejarmos.
Com toda essa configuração realizada, se interceptarmos a requisição e resposta podemos visualizar todo o trabalho sendo realizado pelo cliente e pelo serviço, e através das mensagens HTTP que foram trocadas, podemos confirmar todo o procedimento que está sendo realizado para garantir o envio e recebimento correto pelas partes.
[Requisição]
POST http://localhost:4467/api/batch HTTP/1.1
Content-Type: multipart/mixed; boundary=”batch_93565607-7bd6-4147-b2eb-27a6b35cd42a”
Host: localhost:4467
Content-Length: 402
Expect: 100-continue
Connection: Keep-Alive
–batch_93565607-7bd6-4147-b2eb-27a6b35cd42a
Content-Type: application/http; msgtype=request
POST /api/values HTTP/1.1
Host: localhost:4467
Content-Type: application/json; charset=utf-8
“Israel”
–batch_93565607-7bd6-4147-b2eb-27a6b35cd42a
Content-Type: application/http; msgtype=request
GET /api/values HTTP/1.1
Host: localhost:4467
–batch_93565607-7bd6-4147-b2eb-27a6b35cd42a–
[Resposta]
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 360
Content-Type: multipart/mixed; boundary=”70d3b7ff-6b4c-41ca-aa72-3f2225c19344″
Expires: -1
Server: Microsoft-IIS/8.5
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Wed, 20 Nov 2013 12:20:52 GMT
–70d3b7ff-6b4c-41ca-aa72-3f2225c19344
Content-Type: application/http; msgtype=response
HTTP/1.1 204 No Content
–70d3b7ff-6b4c-41ca-aa72-3f2225c19344
Content-Type: application/http; msgtype=response
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
[“Israel”,”Israel”,”Israel”,”Israel”]
–70d3b7ff-6b4c-41ca-aa72-3f2225c19344–