Israel Aece

Software Developer

Introdução ao ASP.NET Web API - Segunda Edição

Este e-Book tem a finalidade de introduzir a tecnologia ASP.NET Web API, abordando desde a sua estrutura, arquitetura até as suas principais funcionalidades e utilizações. De uma forma bastante simples, tentei resumir os principais itens, com exemplos práticos de utilização, que podem ser transcritos para um projeto no Visual Studio e executá-los.

Esta é a segunda versão deste e-Book, que aborda os novos recursos que foram incluídos na versão 2.1 do ASP.NET Web API. Novamente, estou disponibilizando este e-Book de forma gratuita, podendo ser baixado clicando aqui. Use-o como quiser. Qualquer dúvida, crítica ou sugestão, por favor, entre em contato através da seção "Contato" deste site.

 

Tratamento e Log Global de Exceções - ASP.NET Web API

Para todo e qualquer tipo de aplicação, uma necessidade que temos é a captura dos erros não tratados pela aplicação e o log destes, para que seja possível a depuração e análise depois que o problema acontecesse, sendo necessário incluir o máximo de informações relevantes para essa apuração.

Apesar de existir algumas técnicas já bastante conhecidas no ASP.NET, a versão 2.1 do ASP.NET Web API, a Microsoft criou um novo namespace chamado System.Web.Http.ExceptionHandling e colocou ali alguns recursos específicos para a interceptação e log das exceções disparadas e não tratadas em qualquer nível da API, ou seja, tanto para as exceções que ocorrem no interior do código dos controllers, bem como os códigos de infraestrutura (formatadores, handlers, etc.).

Para utilizar este serviço de log, devemos recorrer à interface IExceptionLogger, que define um único método chamado LogAsync, que como o próprio nome sugere, já traz suporte para realizar esta tarefa de forma assíncrona, passando como parâmetro para ele uma instância da classe ExceptionLoggerContext, qual contém toda informação pertinente ao problema que ocorreu e onde ocorreu (ExceptionContext). Para facilitar as implementações, também já existe uma classe chamada ExceptionLogger, que implementa esta interface define um método chamado Log, que é onde customizeremos o log da exceção que ocorreu.

Para exemplificar, o código abaixo captura a exceção que ocorreu e armazena em um arquivo texto toda a stack trace. Só que criar a classe não é suficiente, pois precisamos acoplá-la à exceção, e para isso, devemos recorrer novamente ao arquivo Global.asax, que está logo no próximo trecho de código.

public class ExceptionTextLogger : ExceptionLogger
{
    private readonly string filePath;

    public ExceptionTextLogger(string filePath)
    {
        this.filePath = filePath;
    }

    public override void Log(ExceptionLoggerContext context)
    {
        File.AppendAllText(filePath, context.Exception.ToString());
    }
}

config.Services.Add(typeof(IExceptionLogger), 
    new ExceptionTextLogger(@"C:\Temp\Log.txt"));

Quando as exceções ocorrem, lembrando que não são somente aquelas exceções referentes ao código que escrevemos no interior da API, mas incluem também os erros que ocorrem dentro das partes referente à infraestrutura, todos eles são repassados para os loggers configurados na aplicação.

Depois que o log é realizado, entra em cena um novo recurso chamado de exception handler. Este recurso que a Microsoft criou nos permitirá avaliar o erro que ocorreu e determinar se a exceção deverá ou não ser disparada. Caso o handler opte por não tratar o erro que ocorreu, a exceção será lançada. Aqui é feito o uso da classe ExceptionDispatchInfo, que captura o contexto onde ocorreu a exceção e posterga o disparo até que o handler faça a análise da mesma.

Seguindo a mesma estrutura do logger, temos uma interface chamada IExceptionHandler que fornece um método assíncrono chamado HandleAsync, recebendo como parâmetro uma classe chamada ExceptionHandlerContext, que informa os detalhes sobre o problema ocorrido através da propriedade ExceptionContext. E, novamente, temos uma classe base chamada ExceptionHandler com a implementação básica da interface IExceptionHandler, qual já podemos utilizar em nosso código:

public class ExceptionMessageHandler : ExceptionHandler
{
    public override void Handle(ExceptionHandlerContext context)
    {
        if (context.Exception is BusinessException)
            context.Result = new BusinessBadRequest(context);
    }

    private class BusinessBadRequest : IHttpActionResult
    {
        private readonly ExceptionHandlerContext exceptionContext;

        public BusinessBadRequest(ExceptionHandlerContext exceptionContext)
        {
            this.exceptionContext = exceptionContext;
        }

        public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
        {
            return Task.FromResult(
                this.exceptionContext.Request.CreateErrorResponse(
                    HttpStatusCode.BadRequest, 
                    new HttpError(this.exceptionContext.Exception, true)));
        }
    }
}

O código acima implementa o método Handle e avalia se a exceção que ocorreu foi do tipo BusinessException. Caso tenha sido, ele cria uma mensagem de retorno específica, atribuindo à propriedade Result. Caso essa propriedade não seja abastecida (no nosso caso, se não for uma BusinessException), então o ASP.NET Web API irá disparar o erro. E, finalmente, para colocá-lo em execução, vamos recorrer ao arquivo Global.asax:

config.Services.Replace(typeof(IExceptionHandler), new BusinessExceptionHandler());

Suporte à BSON no ASP.NET Web API

Já há algum tempo trabalhamos com o formato JSON por ser mais leve e menos verboso quando comparado ao XML, e em tempos onde aplicativos Web estão cada vez mais em evidência, acaba sendo um formato mais simples de lidar quando a manipulação acontecerá através de uma linguagem como o Javascript.

Enquanto as informações retornadas são apenas dados simples (strings, inteiros, decimais, etc.), na maioria das vezes, tudo funciona como esperado, ou seja, com uma boa performance e um conteúdo bem enxuto é trafegado entre as partes (cliente e servidor). O problema começa a acontecer quando precisamos, de alguma forma, retornar informações mais complexas e pesadas, tais como uma informação binária, e aqui, podemos incluir qualquer tipo delas, como por exemplo, uma imagem.

Eis que entra em cena um formato chamado de BSON (Binary JSON), que é uma variação do JSON mas que tem como finalidade serializar as informações em formato binário, otimizando este tipo de conteúdo para trafegar de forma mais reduzida e, consequentemente, mais rápida. Pelo fato de tipos numéricos serem codificados como números e não como strings, a codificação e decodificação é mais eficiente, e além disso, conteúdos binários não precisam ser codificados em Base64, que torna o tamanho da mensagem muito maior do que o normal.

A partir da versão 2.1 do ASP.NET Web API, a Microsoft incluiu um novo formatador chamado BsonMediaTypeFormatter, que pode ser utilizado tanto do lado do servidor quanto do lado do cliente (através do HttpClient), que encapsula a serialização neste formato. Para fazer uso deste novo formatador do lado do servidor, basta acoplá-lo à coleção de formatadores do ASP.NET Web API, através da configuração que está no arquivo Global.asax:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        //...

        config.Formatters.Add(new BsonMediaTypeFormatter());
    }
}

Este formatador opera através do mime type application/bson, e através da negociação de conteúdo que o ASP.NET Web API já suporta, ele acaba sendo capaz de gerar o resultado no formato escolhido pelo cliente. Para que seja possível perceber a sua funcionalidade, vamos recorrer à um exemplo que retorna uma imagem (através de um array de bytes) e poderemos visualizar a diferença entre os resultados que são retornados para o cliente através das imagens extraídas do Fiddler. Note que a primeira imagem utiliza JSON e a segunda BSON, que por sua vez, tem uma melhora de cerca de 25%.

Particionando o Resultado no ASP.NET Web API

Via de regra, quando fazemos a requisição para algum recurso, ele retorna todo o conteúdo para cliente que solicitou. Enquanto estamos considerando simples porções de dados, não há muito problema em sempre retornar toda a informação sempre que solicitada. O problema pode ocorrer quando estamos retornando uma quantidade considerável de informação em resposta a requisição solicitada. Se durante a transmissão desta grande quantidade de informação a conexão, por algum motivo, for abortada, toda a informação se perde e será necessário recomeçar.

O protocolo HTTP contempla uma funcionalidade chamada de byte serving, que é um processo que faz o particionamento do resultado, enviando uma pequena porção de informação para o cliente, que por sua vez, pode controlar exatamente o que está sendo recebido, e com isso, poderá solicitar ao serviço a parte que ele ainda não tenha, evitando recomeçar do zero. Para formalizar esse tipo de comunicação, o protocolo HTTP faz uso de três headers: Range (requisições), Accept-Ranges e Content-Range (respostas).

Para exemplificar, vamos considerar que queremos fazer uma requisição para uma API que retorna o conteúdo de um arquivo texto, que possui a seguinte informação: 11223344556677889900. Quando o cliente realizar a requisição, ele deverá informar através do header Range a quantidade (em bytes) que ele quer que seja retornado. Ao atender a requisição, o serviço deverá ser capaz de capturar o arquivo em questão e particionar o conteúdo, para servir ao cliente somente o trecho que ele está solicitando. O serviço deverá responder com o status 206 (Partial Content), incluindo o header Content-Range, onde teremos o intervalo (dos dados) que está sendo disponibilizado e também a quantidade total que o conteúdo (o arquivo) possui. Vale lembrar que a propriedade Content-Length sempre refletirá a quantidade de dados que está sendo devolvido no corpo da mensagem e nunca o tamanho total do recurso (do arquivo) que está sendo utilizado.

O header Range deve definir a unidade e o intervalo que será utilizada para calcular a porção de informação que será devolvida. Note nos logs abaixo que está a requisição e sua respectiva resposta. A requisição sempre define o intervalo, e a resposta ratifica o intervalo extraído, contemplando também o tamanho total, para que o cliente possa criar o mecanismo de extração enquanto não recepcionar o conteúdo na íntegra, podendo inclusive criar mecanismos para pausar o download. O que também chama atenção é na última requisição, que apenas colocamos 13- para identificar que queremos o intervalo entre o byte 13 até o final do conteúdo.

-----------------------------------------------------

GET http://localhost:4918/api/Files/Download HTTP/1.1
Host: localhost:4918
Range: bytes=0-7

HTTP/1.1 206 Partial Content
Content-Length: 8
Content-Type: text/txt
Content-Range: bytes 0-7/23

11223

-----------------------------------------------------

GET http://localhost:4918/api/Files/Download HTTP/1.1
Host: localhost:4918
Range: bytes=8-12

HTTP/1.1 206 Partial Content
Content-Length: 5
Content-Type: text/txt
Content-Range: bytes 8-12/23

34455

-----------------------------------------------------

GET http://localhost:4918/api/Files/Download HTTP/1.1
Host: localhost:4918
Range: bytes=13-

HTTP/1.1 206 Partial Content
Content-Length: 10
Content-Type: text/txt
Content-Range: bytes 13-22/23

6677889900

-----------------------------------------------------

A implementação por parte da API não é tão complexa. Basta recorrermos à classe ByteRangeStreamContent, que faz toda a mágica para particionar e devolver somente o que foi solicitado. Obviamente que esta classe recebe como parâmetro um Stream contendo o conteúdo (que pode ser uma imagem, um arquivo texto, etc.) e o intervalo solicitado que pode (e deve) ser extraído da requisição, mas não há necessidade de realizar qualquer cálculo manual, pois de posse do header Range, ela será capaz de realizar todo o procedimento.

public class FilesController : ApiController
{
    [HttpGet]
    public HttpResponseMessage Download()
    {
        return new HttpResponseMessage(HttpStatusCode.PartialContent)
        {
            Content = new ByteRangeStreamContent(
                new FileStream("Dados.txt", FileMode.Open, FileAccess.Read),
                    this.Request.Headers.Range,
                    "text/txt")
        };
    }
}

Processamento em Batch no ASP.NET Web API

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--

e-Book - Introdução ao ASP.NET Web API

Este e-Book tem a finalidade de introduzir a tecnologia ASP.NET Web API, abordando desde a sua estrutura, arquitetura até as suas principais funcionalidades e utilizações. De uma forma bastante simples, tentei resumir os principais itens, com exemplos práticos de utilização, que podem ser transcritos para um projeto no Visual Studio e executá-los.

Publicamente quero agradecer imensamente ao Elemar Junior (que dispensa apresentações), por prontamente atender ao meu pedido para escrever o prefácio deste livro. Fiquei extremamente honrado.

Estou disponibilizando este e-Book de forma gratuita, podendo ser baixado clicando aqui. Use-o como quiser. Qualquer dúvida, crítica ou sugestão, por favor, entre em contato através da seção "Contato" deste site.

HttpClient na Portable Class Library

É comum termos a necessidade de criar uma biblioteca com algum código que seja compartilhado entre diversos projetos. Para isso, recorremos à uma template de projeto chamada Class Library, em que seu output é um arquivo do tipo DLL, que pode ser referenciado e, consequentemente, utilizado por projetos que queiram utilizar a funcionalidade exportada por ela.

Tudo isso funcionará até o momento em que precisarmos expor uma determinada funcionalidade para diferentes plataformas, algo muito comum hoje no desenvolvimento de aplicações dentro do ambiente Microsoft. Possuimos diversos alvos que podemos atingir, como por exemplo: aplicações .NET, aplicações para telefones, aplicações para jogos (XBox), aplicações para tablets, etc. Apesar de existir certa simetria em alguns pontos, em outros já podem não ter tantas semelhanças assim, o que torna difícil o controle manual durante a escrita de uma biblioteca para atender qualquer uma estas plataformas.

Desde 2010 a Microsoft trabalha em um projeto para a criação de bibliotecas que possam ser compartilhadas entre diferentes plataformas, e com o Visual Studio 2012, já temos uma template de projeto exclusiva para isso: Portable Class Library. Ao criar um projeto deste tipo, você será obrigado a informar com quais tipos de projetos deseja que ela seja compatível. Isso garantirá com que você somente utilize membros que sejam comuns entre todas as plataformas selecionadas.

Geralmente algumas informações e funcionalidades estão expostas para serem consumidas remotamente, entre os mais diversos dispositivos. Sendo assim, é muito comum termos a necessidade de consumir os serviços expostos pela própria empresa, parceiros de negócios, etc., afim de reutilizar recursos que já estejam construídos e disponibilizados por terceiros. E como já era de se esperar, APIs REST estão cada vez mais populares, e ter um mecanismo para o consumo de forma simples destes tipos de serviços é essencial em qualquer plataforma.

Sabendo disso, a Microsoft criou uma classe chamada HttpClient, que é utilizada para abstrarir toda a complexidade na interação com APIs REST, e mais recentemente, a Microsoft criou a versão portável desta mesma classe. Com isso, poderemos reutilizar o consumo de APIs REST (HTTP) através de diferentes plataformas, sem a necessidade de reconstruir a cada novo projeto uma ponte de comunicação com estes tipos de serviços, e ainda, sem a necessidade de recorrer de classes de mais baixo nível para atingir o mesmo objetivo.

Em princípio a classe HttpClient fornece as mesmas funcionalidades expostas desde a sua criação, acrescentando alguns poucos métodos que expõem alguma funcionalidade específica para uma determinada plataforma. Para utilizar essa DLL em algum dos seus projetos (incluindo bibliotecas portáveis customizadas (Portable Class Libraries)), basta você adicionar o pacote "Http Client Libraries", conforme mostrado na imagem abaixo:

Geração de Documentação para ASP.NET WebApi

Há algum tempo eu comentei aqui sobre REST e WSDL, onde a ideia era apontar a maneira de se trabalhar com serviços sem a necessidade de ter um schema que define o que serviço disponibiliza e, principalmente, toda a estrutura das mensagens (SOAP) que são trocadas entre as partes.

Mas é importante dizer que mesmo serviços baseados em REST, também precisam, de alguma forma, expor alguma espécie de documentação, para descrever as ações que as APIs estão disponibilizando aos consumidores, apontando o caminho (URI) até aquele ponto, método/verbo (HTTP), informações que precisam ser passadas, formatos suportados, etc.

A ideia é apenas ser informativo, ou seja, isso não será utilizado pelo cliente para a criação automática de uma proxy. Pensando nisso, a Microsoft incluiu no ASP.NET Web API a opção para gerar e customizar as documentações de uma API.

Mas a documentação é sempre exibida, na maioria das vezes, de forma amigável ao consumidor, para que ele possa entender cada uma das ações, suas exigências, para que ele possa construir as requisições da forma correta. Sendo assim, podemos na própria aplicação onde nós temos as APIs, criar um controller que retorna uma view (HTML), contendo a descrição das APIs que estão sendo hospedadas naquela mesma aplicação.

public class DeveloperController : Controller
{
    public ActionResult Apis()
    {
        var explorer = GlobalConfiguration.Configuration.Services.GetApiExplorer();

        return View(explorer.ApiDescriptions);
    }
}

Note que estamos recorrendo ao - novo - método GetApiExplorer, disponibilizado através da configuração global das APIs. Este método retorna um objeto que implementa a interface IApiExplorer, que como o próprio nome sugere, define a estrutura que permite obter a descrição das APIs. Nativamente já temos uma implementação chamada ApiExplorer, que materializa todoas as APIs em instâncias da classe ApiDescription, e uma coleção deste objeto é retornada através da propriedade ApiDescriptions, e que repassamos para que a view possa renderizar isso.

Na view, tudo o que precisamos fazer é iterar pelo modelo, e cada elemento dentro deste laço representa uma ação específica que está dentro da API. A classe que representa a ação, possui várias propriedades, fornecendo tudo o que é necessário para que os clientes possam consumir qualquer ums destas ações. Abaixo temos o código que percorre e exibe cada uma delas:

@model IEnumerable<System.Web.Http.Description.ApiDescription>

<body>
    @foreach (var descriptor in this.Model)
    {
        <ul>
            <li><b>@descriptor.HttpMethod - @descriptor.RelativePath</b></li>
            <li>Documentation: @descriptor.Documentation</li>

            @if (descriptor.SupportedResponseFormatters.Count > 0)
            {
              <li>Media Types
                <ul>
                  @foreach (var mediaType in descriptor.SupportedResponseFormatters.Select(
                     mt => mt.SupportedMediaTypes.First().MediaType))
                  {
                    <li>@mediaType</li>
                  }
                </ul>
              </li>
            }

            @if (descriptor.ParameterDescriptions.Count > 0)
            {
              <li>Parameters
                  <ul>
                    @foreach (var parameter in descriptor.ParameterDescriptions)
                    {
                      <li>Name: @parameter.Name</li>
                      <li>Type: @parameter.ParameterDescriptor.ParameterType</li>
                      <li>Source: @parameter.Source</li>
                    }
                  </ul>
              </li>
            }
        </ul>
    }
</body>

Ao acessar essa view no navegador, temos a relação de todas as ações que estão expostas pelas APIs. A visibilidade das ações é controlada a partir do atributo ApiExplorerSettingsAttribute, que possui uma propriedade boleana chamada IgnoreApi, que quando definida como True, omite a extração e, consequentemente, a sua visualização.

É importante notar que na imagem acima, estamos apresentando a propriedade Documentation. A mensagem que aparece ali é uma customização que podemos fazer para prover essa informação, extraindo-a de algum lugar. Para definir a descrição da ação, vamos criar um atributo customizado para que quando decorado no método, ele será extraído por parte da infraestrutura do ASP.NET, alimentando a propriedade Documentation. O primeiro passo, consiste na criação de um atributo para definir a mensagem:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ApiDocumentationAttribute : Attribute
{
    public ApiDocumentationAttribute(string message)
    {
        this.Message = message;
    }

    public string Message { get; private set; }
}

O próximo passo é decorá-lo em cada uma das ações que quisermos apresentar uma informação/descrição. A classe abaixo representa a nossa API, e o atributo recentemente criado foi decorado em todas as ações, descrevendo suas respectivas funcionalidades:

public class ClientesController : ApiController
{
    [ApiDocumentation("Retorna todos os clientes.")]
    public IEnumerable<Cliente> Get()
    {
        //...
    }

    [ApiDocumentation("Retorna um cliente pelo seu Id.")]
    public Cliente Get(int id)
    {
        //...
    }

    [ApiDocumentation("Inclui um novo cliente.")]
    public void Post(Cliente cliente)
    {
        //...
    }

    [ApiDocumentation("Exclui um cliente existente.")]
    public void Delete(int id)
    {
        //...
    }
}

Só que o atributo por si só não funciona. Precisamos de algum elemento para extrair essa customização que fizemos, e para isso, a temos uma segunda interface, chamada IDocumentationProvider, que fornece dois métodos com o mesmo nome: GetDocumentation. A diferença entre eles é o parâmetro que cada um deles recebe. O primeiro recebe um parâmetro do tipo HttpParameterDescriptor, o que permitirá descrever, também, cada um dos parâmetros de uma determinada ação. Já o segundo método, recebe um parâmetro do tipo HttpActionDescriptor, qual utilizaremos para extrair as informações pertinentes à uma ação específica.

public class ApiDocumentationAttributeProvider : IDocumentationProvider
{
    public string GetDocumentation(HttpParameterDescriptor parameterDescriptor)
    {
        return null;
    }

    public string GetDocumentation(HttpActionDescriptor actionDescriptor)
    {
        var attributes =
            actionDescriptor.GetCustomAttributes<ApiDocumentationAttribute>();

        if (attributes.Count > 0)
            return attributes.First().Message;

        return null;
    }
}

Aqui extraímos o atributo que criamos, e se ele for encontrado, retornamos o valor definido na propriedade Message. A ausência deste atributo, faz com que um valor nulo seja retornado, fazendo com que nenhuma informação extra seja incluída para a ação.

E, finalmente, para incluir o provedor de documentação ao runtime do ASP.NET, recorremos à configuração das APIs, substituindo qualquer implementação existente para este serviço, para o nosso provedor que extraí a documentação do atributo customizado.

GlobalConfiguration.Configuration.Services.Replace(
    typeof(IDocumentationProvider),
    new ApiDocumentationAttributeProvider());