ASP.NET Web API - Roteamento

Israel Aece

Julho 2013

No ínicio foi comentado que uma das característica dos serviços REST, é que todo recurso é endereçável através de uma URI (Universal Resource Identifier). É através da URI que sabemos onde ele está localizado e, consequentemente, conseguiremos chegar até ele recurso e executar uma determinada ação.

Cada URI identifica um recurso específico, e ela tem uma sintaxe que é bastante comum para os desenvolvedores: [scheme]:[port]://[host]/[path][?query]. Cada pedaço dela tem uma representação e funcionalidade, que estão detalhes na listagem abaixo:

  • Scheme: Identifica o protocolo que será utilizado para realizar a requisição à algum recurso, podendo ser: HTTP, HTTPS, FTP, etc.
  • Port: Opção para definir o número da porta (no destino) que será utilizada para receber a requisição. Se omitido, utilizará a porta padrão do protocolo, onde no HTTP é a porte 80, no HTTPS a porta 443, etc.
  • Host: O Host define o nome (fornecido pelo DNS) ou o endereço IP do servidor.
  • Path: Esta parte da URI é qual identifica o recurso em si. É através dele que apontamos qual dos recursos que desejamos acesso dentro daquele servidor.
  • Query: Trata-se de uma informação opcional que pode ser encaminhada para o serviço, fornecendo informações adicionais para o processamento da requisição. Um exemplo disso é incluir o identificador do recurso que está sendo solicitado.

Até pouco tempo atrás as URI eram algo indecifrável para o usuário final, sendo apenas um endereço para algum recurso (seja ele qual for) em um ponto da rede. Aos poucos as URIs foram ficando cada vez mais legível, ou seja, tendo a mesma funcionalidade mas sendo mais amigável, o que facilita o retorno do usuário, ou ainda, a própria URI refletir o que ela representa. Há também benefícios relacionados ao SEO (Search Engine Optimization), onde os buscadores consideram as palavras encontradas na URI durante a pesquisa.

Durante o desenvolvimento do ASP.NET MVC, a Microsoft criou dentro dele um mecanismo de roteamento, que baseado em uma tabela de regras, ele é capaz de interpretar a requisição (URI) que está chegando para o mesmo e identificar o local, o serviço, a classe e o método a ser executado. Antes mesmo do lançamento oficial do ASP.NET MVC, a Microsoft fez com que este recurso de roteamento fosse desacoplado e levado para o core do ASP.NET, e com isso, pode ser utilizado pelo ASP.NET MVC, Web Forms e também pelo ASP.NET Web API, que é o nosso foco aqui.

Tudo começa pela configuração dessa tabela de regras que está localizada no arquivo Global.asax. Como sabemos, ao rodar a aplicação pela primeira vez, o evento Application_Start é disparado, e é justamente dentro dele onde o código que faz toda a configuração das rotas é colocada.

O objeto de configuração fornece uma coleção de rotas, e que na template de projeto que estamos utilizando, a rota padrão é colocada no arquivo WebApiConfig. Notamos que a rota padrão possui o nome de “DefaultApi”, e existe um segmento na seção Path da URI chamado de “api”. Ele é utilizado para diferenciar e não causar conflito com outras eventuais rotas que já existam naquela aplicação, como por exemplo, aquelas que são criadas pelo projeto ASP.NET MVC. Essa necessidade se deve pelo fato de que podemos hospedar debaixo de um mesmo projeto, controllers que renderizam views (HTML) bem como controllers que retornam dados (Web API).

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

Toda rota pode ser composta de literais e de placeholders, que são substituídos durante a execução. No exemplo acima, “api” trata-se de um literal, enquanto o {controller} e {id} são os placeholders. Quando a requisição é realizada, o ASP.NET tenta mapear os valores extraídos da URL e preencher os placeholders colocados na rota. Caso nenhuma rota se enquadre com a requisição que está solicitada, o erro 404 (Not Found) é retornado ao cliente.

É importante mencionar que os placeholders podem ter valor padrão definido. É isso que define o último parâmetro do método MapHttpRoute, chamado defaults. Podemos determinar se ele é opcional, e definir um valor padrão se ele for omitido durante a requisição através de um dicionário, onde cada item corresponde ao nome do placeholder e seu respectivo valor. Abaixo temos algumas URLs indicando se ela está ou não enquadrada na rota padrão:

URL

Método

api/Artistas/MaxPezzali

ArtistasController.Get(“MaxPezzali”)

api/Artistas/34

ArtistasController.Get(“34”)

Artistas/Eros

Como não há o sufixo “api/”, ele não se enquadrará na rota padrão, rejeitando-a.

api/Artistas

ArtistasController.Get() e o parâmetro id será nulo.

Para o exemplo acima, estamos utilizando o seguinte método:

public Artista Get(string id = "") { }

Por convenção, se o método for nomeado com o verbo do HTTP, automaticamente o ASP.NET Web API entrega a requisição naquela verbo paa ele, sem qualquer trabalho adicional para que isso ocorra.

Durante a execução o processo de roteamento possui alguns estágios de processamento que são executados até que a ação seja de fato executada. O primeiro passo consiste ao enquadramento da URI à uma rota configurada na tabela de roteamento. Se encontrada, as informações colocadas na URI são extraídas e adicionadas no dicionário de rotas, que poderão ser utilizadas por toda a requisição, sendo pelo ASP.NET Web API (runtime) ou pelo código customizado.

Depois de alguma rota encontrada, começa o processo para identificar qual é a ação, ou melhor, o método que o cliente está solicitando. Só que antes de disso, é necessário identificar a classe (controller) onde ele está. Como a rota padrão já vem configurada no projeto, os dois placeholders determinam indiretamente a classe (controller) e o método (ação), competindo ao ASP.NET realizar o parser dos fragmentos da URI, encontrar a classe e o método, e depois disso, abastecer os parâmetros (caso eles existam) e, finalmente, invocar o método, retornando o resultado (sucesso ou falha) ao cliente solicitante.

Apesar do compilador não indicar, toda a classe que será considerada uma API, é necessário que ela não seja abstrata e tenha o seu modificador de acesso definido como público, caso contrário, o ASP.NET Web API não será capaz de instanciá-la. E, finalmente, apesar de termos a necessidade do sufixo Controller nas classes, essa palavra não está presente na URI. Isso é apenas uma exigência do ASP.NET Web API, para identificar quais classes dentro do projeto correspondem à uma API.

Depois de encontrada a classe, é o momento de escolher o método a ser executado. Da mesma forma que a classe, o método a ser executado também é escolhido baseado na URI da requisição. Pela afinidade ao HTTP, a busca do método dentro da classe considera os verbos do HTTP (GET, POST, PUT, DELETE, etc.) durante a busca. O algoritmo de pesquisa do método dentro do controller segue os passos descritos abaixo:

  1. Seleciona todos os métodos do controller que se enquadram com o verbo do HTTP da requisição. O método pode ser nomeado ou prefixado apenas com o nome do verbo.
  2. Remove aquelas ações com o nome diferente do valor que está no atributo action do dicionário de rotas.
  3. Tentar realizar o mapeamento dos parâmetros, onde os tipos simples são extraídos da URI, enquanto tipos complexos são extraídos do corpo da mensagem.
    1. Para cada ação, extrai uma lista de parâmetros de tipos simples, e exceto os parâmetros opcionais, tenta encontrar o valor de cada um deles da URI.
    2. Dessa lista tenta encontrar o respectivo valor no dicionário de rotas ou na querystring da URI, independente de case ou de ordem dos parâmetros.
    3. Seleciona aquela ação onde cada um dos parâmetros é encontrado na URI.
  4. Ignora as ações que estão decoradas com o atributo NonActionAttribute.

Como notamos no algoritmo acima, ele não considera a busca de ações quando há tipos complexos. A ideia é tentar encontrar uma ação através da descrição estática da mensagem, ou seja, sem a necessidade de recorrer a qualquer mecanismo extra. Caso isso não seja suficiente, o ASP.NET Web API recorrerá à um binder ou à um formatador, baseado na descrição do parâmetro e que na maioria das vezes, é oriundo a partir do corpo da mensagem.

E o método MapHttpRoute ainda fornece um outro parâmetro que nos permite especificar constraints. Através delas podemos determinar um validador para um parâmetro específico, ou seja, se conseguimos antecipar qual o formato que a informação deve ser colocada, isso evitará que ela seja rejeitada logo nos primeiros estágios do processamento, retornando o erro 404 (Not Found).

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{periodo}",
            defaults: new { periodo = DateTime.Now.ToString("yyyy-MM") },
            constraints: new { periodo = @"[0-9]{4}\-[0-9]{2}" }
        );
    }
}

Da mesma forma que fazemos com os valores padrão para cada placeholder, criamos um novo dicionário onde a chave é o nome do parâmetro, e o valor é uma expressão regular que determina o formato em que o parâmetro deve ter. O fato de adicionar isso, o próprio ASP.NET Web API se encarrega de avaliar se o valor dos parâmetros se enquadram com a regra determinada na expressão regular. Isso nos dá a chance de separar as validações do método que executa a ação, ficando responsável apenas por lidar com a regra de negócio.

Depurando Rotas

Acima vimos como podemos configurar as rotas no projeto, mas não estamos limitados a incluir apenas uma opção de roteamento. Na medida em que as APIs vão sendo construídas, você está livre para criar rotas específicas para recepcionar as requisições.

A medida em que as rotas vão sendo adicionadas, podemos enfrentar alguma dificuldade no desenvolvimento, onde a requisição não chega ao destino esperado, não encontrando o controller ou a ação, ou até mesmo, em casos mais extremos, se enquadrando em uma rota diferente daquela que estávamos imaginando.

Para facilitar a depuração disso, existe uma ferramenta chamada ASP.NET Web API Route Debugger, que pode ser adicionado à aplicação através do Nuget. Para adicioná-la ao projeto, basta executar o comando abaixo na Package Manager Console, conforme é mostrado abaixo:

    PM> Install-Package WebApiRouteDebugger

Ao rodar este comando, alguns elementos são incluídos no projeto. Basicamente trata-se de uma nova área, que possui a interface gráfica para exibir o resultado da requisição com os parâmetros e resultados da interceptação que foi realizada pelo debugger.

Depois de instalar este depurador, basta rodarmos a aplicação e quando chegarmos no navegador, basta acrescentarmos na URL o sufixo /rd. Isso fará com que uma tela seja exibida para que você digite o endereço completo para a API que deseja invocar.

Dn376303.1393C74CD74D871277816D08CFF0B0F0(pt-br,MSDN.10).png

Figura 9 - Route Debbuger.

A partir desta tela podemos ir testando as URIs que temos para validar se alguma rota dentro deste projeto consegue ser encontrada e, consequentemente, direcionada para o método que tratará a requisição. Quando pressionamos o botão Send, um resumo como o resultado da avaliação é exibido, e nele você conseguirá analisar de forma simples as informações que foram extraídas e facilmente identificará onde está o problema.

Entre as informações, ele exibe o dicionário de dados que foi extraído da URI. Ele elenca as informação (chave e valor), quais você poderá avaliar se o valor informado na URI está, corretamente, abastacendo o parâmetro desejado.

A próxima tabela aponta as rotas que estão configuradas no projeto, incluindo as informações extras, tais como: valores padrão, constraints, etc. Com as informações que compõem a configuração da rota sendo colocada aqui, fica fácil determinar o porque os parâmetros entraram ou não, porque eles os parâmetros assumiram o valor padrão ao invés de receber aquele que está na URI, e assim por diante.

Entre as duas últimas tabelas temos a primeira que lista todos os controllers que estão adicionados ao projeto, bem como o seu tipo, namespace e assembly. Já a segunda, e não menos importante, exibe todas as ações que foram localizadas, descrevendo-as complementos, incluindo o verbo HTTP que ela atende, o seu nome e o(s) parâmetro(s). Logo na sequência, ele nos aponta se ação (método) foi encontrado pelo nome (By Action Name) ou pelo verbo (By HTTP Verb). A imagem abaixo ilustra essa tela com todos os detalhes descritos aqui.

Dn376303.0CC2624EFFE33A8DBEAA5F0205B26519(pt-br,MSDN.10).png

Figura 10 - Route Debugger exibindo as informações.

Para a grande maioria das tarefas discutidas neste capítulo, existem objetos responsáveis por executar cada uma delas, e que podemos customizar cada um deles para que seja possível mudarmos algumas regras ou interceptarmos os estágios do processamento e incluir um código para efetuar logs, mensurar performance, etc. A estensibilidade é abordada em detalhes em um capítulo mais adiante.

Rotas Declarativas

O modelo de roteamento que vimos até aqui é baseado em uma convenção que é definida através de templates e estão centralizadas arquivo Global.asax. Como falamos acima, durante a execução os placeholders são substituídos pelos parâmetros que são extraídos, na maioria das vezes, da URI e, consequentemente, encaminhada para a ação dentro do controller. Por ser uma coleção, podemos predefinir várias rotas para atender todas as URIs que são criadas para as APIs que rodam dentro daquela aplicação.

Apesar da centralização ser um ponto positivo, começa a ficar complicado quando o número de rotas criadas começa a crescer. Haverá um trabalho em olhar para essa tabela e analisar qual das rotas a ação se enquadra. Nem sempre haverá uma rota genérica para atender todas as requisições, já que para expressar melhor a estrutura e hirarquia dos recursos, teremos que definir a rota para cada ação contida no controller.

É para este fim que a Microsoft incluiu no ASP.NET Web API uma funcionalidade chamada de attribute routing, ou roteamento declativo. A ideia aqui é configurar o roteamento para uma ação em um local mais próximo dela, para além de permitir a configuração específica, facilitar a manutenção.

A implementação é bastante simples mas muito poderosa. Aqueles atributos que vimos acima que determinam através de que verbo do HTTP a ação estará acessível (HttpGetAttribute, HttpPutAttribute, HttpPostAttribute, HttpDeleteAttribute, etc.), passam a ter um overload no construtor que aceita uma string com a template da rota que será atendida por aquela ação (método).

public class ArtistasController : ApiController
{
    [HttpGet("artistas/{nomeDoArtista}/albuns")]
    public IEnumerable<Album> RecuperarAlbuns(string nomeDoArtista)
    {
        return new[] 
        { 
            new Album() { Titulo = "Max 20" },
            new Album() { Titulo = "Terraferma" }
        };
    }
}

No exemplo acima vemos que a rota foi configurada in-place, ou seja, no próprio local onde a ação foi criada. Da mesma forma que fazemos na definição da rota no arquivo Global.asax, aqui também podemos utilizar placeholders, para que eles sejam substituídos pelos parâmetros da própria ação que, novamente, são extraídos da requisição.

Mas aplicar os atributos com as rotas não é suficiente. Há a necessidade de instruir o ASP.NET a coletar as informações sobre o roteamento diretamente nas ações. Para isso há um método de estensão que é aplicado à classe HttpConfiguration chamado MapHttpAttributeRoutes no arquivo Global.asax:

    config.MapHttpAttributeRoutes();

O resultado final para a chamada desta ação fica:

    https://localhost:49170/artistas/MaxPezzali/albuns

É claro que o controller pode conter diversas outras ações, onde cada uma delas retorna outras informações, neste caso, de um determinado artista. Suponhamos que também teremos uma ação que retorne as notícias do artista. Bastaria adicionar o método à classe, configurar o roteamento apenas alterando a última parte , trocando albuns por noticias.

Há também uma opção para elevar o prefixo das rotas para o nível da classe (controller). Para isso, utilizaremos um atributo chamado RoutePrefixAttribute que nos permite definir um valor que será combinado com as rotas configuradas ação por ação para compor a rota final de acesso para cada uma delas.

[RoutePrefix("artistas")]
public class ArtistasController : ApiController
{
    [HttpGet("{nomeDoArtista}/albuns")]
    public IEnumerable<Album> RecuperarAlbuns(string nomeDoArtista)
    {
        return new[] 
        { 
            new Album() { Titulo = "Max 20" },
            new Album() { Titulo = "Terraferma" }
        };
    }

    [HttpGet("{nomeDoArtista}/noticias")]
    public IEnumerable<Noticia> RecuperarNoticias(string nomeDoArtista)
    {
        return new[] 
        { 
            new Noticia() { Titulo = "Novo Album em 2013" }
        };
    }
}

O fato de configurarmos as rotas nas ações, não quer dizer que perderemos certas funcionalidades. Mesmo utilizando esta técnica, teremos a possibilidade de configurar valores padrão para os parâmetros, bem como constraints para garantir que o cliente informe os dados seguindo uma regra já estabelecida.

Para ambos os casos, teremos uma sintaxe ligeiramente diferente comparada ao modelo tradicional (centralizado, de forma imperativa). O próprio placeholder irá contemplar os valores padrão e as constraints. Para casos onde teremos um parâmetro opcional e desejarmos definir um valor padrão, teremos das formas para representar, onde a primeira delas basta utilizar o recurso do próprio C# para isso, e no placeholder sufixar o parâmetro com o caracter “?”.

[HttpGet("artistas/noticias/{cultura?}")]
public IEnumerable<Noticia> RecuperarNoticias(string cultura = "pt-BR") { }

[HttpGet("artistas/noticias/{cultura=pt-BR}")]
public IEnumerable<Noticia> RecuperarNoticias(string cultura) { }

Já a segunda forma, definimos o valor padrão do parâmetro no próprio placeholder, que ponto de vista daquele que está acessando a cultura de dentro da ação, não muda em nada. A diferença é que no segundo caso, o valor será encaminhado para o model binder, onde você poderá fazer algo diferente se assim desejar.

No caso das constraints temos um conjunto de opções e funções que garantem que os parâmetros informados pelo cliente se enquadrem com a exigência da API. A ideia é definir na configuração da rota a constraint usando a seguinte sintaxe: {parametro:contraint}. Na primeira parte definiremos o nome do parâmetro que está declarado na assinatura do método (ação), e na opção constraint iremos escolher uma das várias opções predefinidas que temos a nossa disposição:

Constraint

Descrição

Exemplo

alpha, datetime, bool, guid

Garante com que o parâmetro seja alfanumérico (a-z, A-Z). Há também opções para tipos de dados especiais.

{cultura:alpha}

int, decimal, double, float, long

Opções para números de diferentes tipos.

{pagina:int}

length, maxlength, minlength

Garante que uma determinada string tenha um tamanho específico.

{razaoSocial:maxlength(100)}

max, min, range

Opções para determinar intervalos.

{idade:range(18, 65)}

regex

Assegura que o parâmetro se encaixe em uma expressão regular.

{email:( \b[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b)}

Nada impede definirmos mais que uma constraint para um mesmo parâmetro. Para isso basta utilizarmos uma vírgula para separar as constraints que desejamos aplicar a este parâmetro.

[HttpGet("artistas/detalhes/{artistaId:int}")]
public Artista RecuperarDetalhes(int artistaId) { }

Nomenclatura das Rotas

Tanto para as rotas que definimos em nível centralizado quanto estar em nível de método (ação), podemos opcionalmente definir um nome que a identifique. A finalidade deste nome é facilitar a geração dos links para recursos específicos que já existem ou que são criados sob demanda, evitando assim a definição em hard-code de links, o que dificulta a manutenção futura.

Como facilitador para a geração destes links, temos uma classe chamada UrlHelper, que utiliza a tabela de roteamento para compor as URLs necessárias para a API. A classe ApiController possui uma propriedade chamada Url, que retorna a instância da classe UrlHelper já configurada para a utilização.

[RoutePrefix("artistas")]
public class ArtistasController : ApiController
{
    private static IList<Artista> artistas = new List<Artista>();
    private static int Ids = 0;

    [HttpPost("adicionar")]
    public HttpResponseMessage AdicionarArtista(HttpRequestMessage request)
    {
        var novoArtista = request.Content.ReadAsAsync<Artista>().Result;
        novoArtista.Id = ++Ids;
        artistas.Add(novoArtista);

        var resposta = new HttpResponseMessage(HttpStatusCode.Created);
        resposta.Headers.Location =
            new Uri(Url.Link("Detalhamento", new { artistaId = novoArtista.Id }));

        return resposta;
    }

    [HttpGet("detalhes/{artistaId:int}", RouteName = "Detalhamento")]
    public Artista BuscarDetalhes(int artistaId)
    {
        return artistas.SingleOrDefault(a => a.Id == artistaId);
    }
}

Ao incluir o novo artista na coleção, o método cria e retorna um objeto do tipo HttpResponseMessage, definindo o header Location com o endereço para detalhar a entidade que foi recentemente criada. O método Link da classe UrlHelper já combina o endereço geral (host) da API com o path até a ação que é especificada, retornando assim o endereço absoluto para um determinado local. O resultado da resposta é mostrado abaixo:

    HTTP/1.1 201 Created
    Location: https://localhost:49170/artistas/detalhes/1
    Date: Mon, 01 Jul 2013 23:32:58 GMT
    Content-Length: 0

Veja também:

ASP.NET Web API – HTTP, REST e o ASP.NET: Para basear todas as funcionalidades expostas pela tecnologia, precisamos ter um conhecimento básico em relação ao que motivou tudo isso, contando um pouco da história e evolução, passando pela estrutura do protocolo HTTP e a relação que tudo isso tem com o ASP.NET.

ASP.NET Web API – Estrutura da API: Entenderemos aqui a template de projeto que o Visual Studio fornece para a construção das APIs, bem como sua estrutura e como ela se relaciona ao protocolo.

ASP.NET Web API – Roteamento: Como o próprio nome diz, o capítulo irá abordar a configuração necessária para que a requisição seja direcionada corretamente para o destino solicitado, preenchendo e validando os parâmetros que são por ele solicitado.

ASP.NET Web API – Hosting: Um capítulo de extrema relevância para a API. É o hosting que dá vida à API, disponibilizando para o consumo por parte dos clientes, e a sua escolha interfere diretamente em escalabilidade, distribuição e gerenciamento. Existem diversas formas de se expor as APIs, e aqui vamos abordar as principais delas.

ASP.NET Web API – Consumo: Como a proposta é ter uma API sendo consumido por qualquer cliente, podem haver os mais diversos meios (bibliotecas) de consumir estas APIs. Este capítulo tem a finalidade de exibir algumas opções que temos para este consumo, incluindo as opções que a Microsoft criou para que seja possível efetuar o consumo por aplicações .NET.

ASP.NET Web API – Formatadores: Os formatadores desempenham um papel importante na API. São eles os responsáveis por avaliar a requisição, extrair o seu conteúdo, e quando a resposta é devolvida ao cliente, ele entra em ação novamente para formatar o conteúdo no formato em que o cliente possa entender. Aqui vamos explorar os formatadores padrões que já estão embuitdos, bem como a criação de um novo.

ASP.NET Web API – Segurança: Como a grande maioria das aplicações, temos também que nos preocupar com a segurança das APIs. E quando falamos de aplicações distribuídas, além da autenticação e autorização, é necessário nos preocuparmos com a segurança das mensagens que são trocadas entre o cliente e o serviço. Este capítulo irá abordar algumas opções que temos disponíveis para tornar as APIs mais seguras.

ASP.NET Web API – Testes e Tracing: Para toda e qualquer aplicação, temos a necessidade de escrever testes para garantir que a mesma se comporte conforme o esperado. Isso não é diferentes com APIs Web. Aqui iremos abordar os recursos, incluindo a própria IDE, para a escrita, gerenciamento e execução dos testes.

ASP.NET Web API – Estensibilidade e Arquitetura: Mesmo que já tenhamos tudo o que precisamos para criar e consumir uma API no ASP.NET Web API, a customização de algum ponto sempre acaba sendo necessária, pois podemos criar mecanismos reutilizáveis, “externalizando-os” do processo de negócio em si. O ASP.NET Web API foi concebido com a estensibilidade em mente, e justamente por isso que existe um capítulo exclusivo para abordar esse assunto.

| Home | Artigos Técnicos | Comunidade