2
ROTEAMENTO
1
O roteamento desempenha um papel importante na interceção e redirecionamento de pedidos HTTP para a plataforma ASP.NET MVC. Ao longo deste capítulo, vamos analisar algumas das particularidades associadas ao roteamento de pedidos em ASP.NET MVC.
2.1
URL
Um URL (Uniform Resource Locator1) representa um endereço que identifica univocamente um recurso numa determinada rede. Na prática, um URL segue sempre uma estrutura do tipo protocolo://máquina/caminho/recurso. No caso das aplicações Web, os URL foram usados durante anos para identificar recursos existentes num servidor (por exemplo, o índice da especificação do HTML5 pode ser obtida a partir de http://dev.w3.org/html5/spec/Overview.html). O leitor com experiência no desenvolvimento de aplicações Web no “universo” Microsoft recorda, com toda a certeza, o uso de URL do tipo http://www.minhaempresa.com/filmes/lista.aspx?categoria=1. Nestes casos, era quase certa a existência de uma página ASPX, designada por lista.aspx, que seria responsável por tratar pedidos desse género, tendo em atenção o parâmetro passado na query string. Ou seja, no mundo ASP.NET pré-MVC, era normal existir um relacionamento direto entre um URL e um recurso físico (geralmente, uma página ASPX ou uma handler) existente no disco. Com a introdução da plataforma ASP.NET MVC, este relacionamento entre URL e recurso físico já não existe, uma vez que o mapeamento passa a ser estabelecido entre um URL e um
Se quisermos ser precisos, um URL é um tipo concreto de URI (Uniform Resource Identifier). Contudo, e na prática, ambos são muitas vezes usados como sinónimos. O leitor interessado pode obter mais detalhes em http://bit.ly/TzQ7w. 1
© FCA – Editora de Informática
26
ASP.NET MVC
método de ação de um controlador (os métodos públicos de um controlador que são responsáveis por tratar pedidos HTTP são designados por métodos de ação). Por exemplo, no caso de aplicações ASP.NET MVC, é normal encontrarmos URL do tipo http://www.minhaempresa.com/filmes/categoria/1. Estes URL são intercetados pela infraestrutura de roteamento, que se encarrega de os redirecionar (indiretamente) para um método de ação de um controlador, que, por sua vez, é responsável por gerar a resposta devolvida ao browser. Note-se que esta forma de definir URL permite-nos ainda melhorar a usabilidade2 associada aos URL de uma aplicação (se compararmos os dois URL anteriores, rapidamente concluimos que o último é mais fácil de memorizar e transmite mais informação acerca do recurso devolvido – estas são apenas duas boas caraterísticas de um URL).
2.2
INTRODUÇÃO AO ROTEAMENTO A plataforma ASP.NET recorre ao roteamento para atingir dois objetivos: Mapear URL de pedidos HTTP em métodos de ação disponibilizados por um controlador; Gerar URL que, no futuro, produzam pedidos HTTP que sejam mapeados num método de ação de um controlador. Roteamento versus URL rewriting
O leitor com experiência no uso de URL rewriting pode ser tentado a ver o roteamento como uma nova forma de efetuar a reescrita de URL (a reescrita de URL permite-nos criar URL amigáveis que melhorem os resultados devolvidos pelos motores de pesquisa – esta é uma das técnicas mais usadas nas otimizações SEO 3). Existe, contudo, uma diferença importante entre ambas as técnicas. O URL rewriting concentra-se apenas em transformar um URL num outro URL. Por sua vez, o roteamento é responsável por mapear um URL
2 Jakob Nielsen (um guru do estudo da usabilidade) estudou a influência de URL na acessibilidade de aplicações. O leitor interessado pode obter algumas recomendações sobre a geração destes identificadores em http://bit.ly/9x0mm.
SEO (Search Engine Optimizations) descreve um processo para melhorar a visibilidade de links de sites nas pesquisas efetuadas por motores de busca. O link http://bit.ly/1oIpqX efetua uma boa introdução a este tópico. 3
© FCA – Editora de Informática
ROTEAMENTO
27
num determinado recurso existente na aplicação (como veremos, o módulo 4 de roteamento encarrega-se de associar um URL a uma handler5, que, por sua vez, instancia um controlador e executa um dos seus métodos, de forma a obter a vista que gerará o HTML devolvido ao browser). Para além dessa diferença, existe ainda outra: ao contrário do URL rewriting, o roteamento usado em ASP.NET MVC permite a geração de URL compatíveis com métodos expostos pelos controladores. Por outras palavras, podemos interagir com a infraestrutura de roteamento e pedir-lhe para gerar um URL que “represente” um determinado método de ação de um controlador.
Em ASP.NET MVC, precisamos de ter pelo menos uma rota ativa para que um URL seja redirecionado para um método de ação de um controlador. Uma rota é sempre caraterizada por um padrão, que é usado para verificar a sua compatibilidade com o URL de um pedido HTTP. Este padrão pode, ou não, conter parâmetros cujos valores serão apenas obtidos em runtime, depois de uma rota ter sido considerada compatível com o URL de um pedido HTTP (na prática, podemos pensar nos parâmetros como placeholders que serão substituídos pelos valores indicados num URL compatível com o padrão). Quando o padrão contém parâmetros, então a rota pode ainda indicar valores predefinidos e restrições aplicáveis aos mesmos. Finalmente, importa ainda referir que uma rota dever ser sempre identificada univocamente através de nome (definido durante o seu registo na tabela de roteamento). Processamento de várias rotas Numa aplicação ASP.NET MVC, é normal definirmos várias rotas. Neste caso, a pesquisa de uma rota termina quando a infraestrutura de roteamento encontra a primeira rota compatível na tabela de rotas (coleção que contém todas as rotas previamente registadas). Portanto, devemos ter algum cuidado na definição de rotas e garantir sempre que as rotas mais específicas são efetivamente registadas na tabela de roteamento antes das rotas generalistas.
Todos os pedidos HTTP tratados pela plataforma ASP.NET passam por várias fases. Estas fases constituem a chamada pipeline ASP.NET. Os módulos são classes que podem intercetar uma destas fases para modificar a informação do pedido atual. O leitor interessado pode obter mais detalhes sobre a construção destes elementos em http://bit.ly/c96Wfw. 4
5 As handlers são classes responsáveis por devolver a resposta a um pedido HTTP. Em ASP.NET Web Forms, todas as páginas ASPX são handlers. Em ASP.NET MVC, existe uma handler predefinida que trata todos os pedidos efetuados através da interação com um método de ação de um controlador. O artigo existente em http://bit.ly/rY7ExF é um bom ponto de partida para os interessados em obter mais informações sobre estes elementos.
© FCA – Editora de Informática
28
ASP.NET MVC
2.2.1
CRIAÇÃO DE UMA ROTA
As rotas devem ser sempre criadas e registadas no início de uma aplicação Web. É por isso que um projeto ASP.NET MVC gerado através do template de Internet (ou de Intranet) invoca o método estático RegisterRoutes a partir do método Application_Start definido no ficheiro global.asax.cs (por seu lado, este é executado apenas uma vez, quando o primeiro recurso ASP.NET é pedido). Por predefinição, o método RegisterRoutes contém código semelhante ao seguinte: public static void RegisterRoutes(RouteCollection routes){ routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "Default", // nome da rota "{controller}/{action}/{id}", //padrão URL com 3 parâmetros new { controller = "Home", //valores predefinidos parâmetros action = "Index", id = UrlParameter.Optional } ); }
A variável routes identifica a tabela de roteamento (representada programaticamente por um objeto do tipo RouteCollection) que contém todas as rotas, que, depois, são consumidas pelo módulo de roteamento. Concentremo-nos, para já, no método MapRoute. Na sua forma mais simples, este método obriga-nos apenas a definir o nome da rota e o respetivo padrão. O excerto seguinte ilustra o uso desta forma: Routes.MapRoute( "teste", "{parametro1}/{parametro2}/{parametro3}");
O padrão apresentado no excerto anterior define três parâmetros (parametro1, parametro2 e parametro3). Na prática, um parâmetro é sempre delimitado por { } e serve para identificar um segmento do URL cujo valor será obtido em runtime. Não existem grandes restrições quanto aos nomes que podem ser usados para identificar os parâmetros definidos num padrão. Na prática, contudo, convém apenas utilizarmos nomes que possam ser usados como identificadores em C#, se quisermos que os seus valores alimentem automaticamente
© FCA – Editora de Informática
ROTEAMENTO
29
eventuais parâmetros disponibilizados por um método de ação (a secção 2.2.2 apresenta mais pormenores sobre estas operações). Ao intercetar um pedido, o módulo de roteamento é responsável por verificar se o URL desse pedido é, ou não, compatível com alguma das rotas previamente registadas na tabela de roteamento. Quando uma rota é considerada compatível, então o módulo tem de obter os valores dos eventuais parâmetros do padrão da rota a partir desse URL. A Tabela 2.1 procura ilustrar o processo de obtenção de valores de parâmetros definidos num URL padrão. URL
VALORES DE PARÂMETROS
/filmes/cat/1
parametro1="filmes" parametro2="cat" parametro3="1"
/filmes/pt-PT/1
parametro1="filmes" parametro2="pt-PT" parametro3="1"
TABELA 2.1 – Obtenção de valores de parâmetros a partir de um URL
Como a rota teste (apresentada no excerto de código anterior) define três parâmetros, então é compatível com qualquer URL definido à custa de três segmentos (um segmento é uma porção de texto delimitado por uma barra /). Como é possível observar através do segundo exemplo apresentado na Tabela 2.1, o caráter / é o único que, efetivamente, delimita um segmento (repare-se como pt-PT é atribuído ao segundo parâmetro, tendo o módulo simplesmente ignorado o uso do caráter –). Depois de obter os valores dos parâmetros a partir do URL do pedido atual, o módulo de roteamento encarrega-se ainda de os guardar num dicionário do tipo RouteValueDictionary (o nome de cada parâmetro é usado como chave de cada entrada efetuada no dicionário). Como é óbvio, este dicionário pode ser recuperado mais tarde através do contexto de roteamento representado por uma instância do tipo RequestContext6 (voltaremos a este contexto mais tarde).
6
Como veremos, esta informação pode ser recuperada a partir de código usado num controlador ou
numa vista. © FCA – Editora de Informática
30
2.2.2
ASP.NET MVC
PARÂMETROS ESPERADOS PELO ASP.NET MVC
Como referimos, a plataforma ASP.NET MVC espera o uso de parâmetros com determinados nomes. Todas as rotas reencaminhadas para plataforma devem, pelo menos, definir dois parâmetros, designados por controller e action. O valor de controller identifica o nome da classe (controlador) que disponibiliza o método (de ação) identificado pelo valor do segundo parâmetro (action). Por convenção, a plataforma encarrega-se de adicionar o prefixo Controller ao valor do parâmetro e usa esse nome para procurar um tipo que implementa a interface IController (note-se que a capitulação não é tida em conta nesta pesquisa). O parâmetro action e a interface IController O parâmetro action é apenas usado para identificar o método de ação quando o controlador em causa reutiliza a classe base Controller. Controladores que implementem diretamente a interface IController são responsáveis por definir as suas próprias convenções e mapeamentos entre rotas e controladores. Como na maior parte das vezes a criação de um novo controlador implica a extensão da classe Controller, então é normal dizermos que temos de garantir sempre a existência de um parâmetro de roteamento designado por action.
Portanto, a rota que usámos na secção 2.1 acaba por não ser uma rota MVC válida. Para a usarmos numa aplicação ASP.NET MVC, temos de alterá-la por forma a garantir que um pedido inicializa sempre os parâmetros controller e action. O excerto seguinte ilustra uma forma de a tornamos numa rota que pode ser interpretada pela plataforma ASP.NET MVC: Routes.MapRoute( "teste", "{controller}/{action}/{parametro3}");
Se reutilizamos o primeiro URL apresentado na Tabela 2.1, então é possível afirmarmos que o URL /filmes/cat/1 será tratada pelo método Cat exposto pela classe FilmesController. Apesar de o segundo URL apresentado pela tabela ser compatível com o padrão da regra anterior, a verdade é que a rota obtida por ele não conduzirá à execução de um método de ação, uma vez que não é possível termos um método designado por pt-PT em C# (no Capítulo 3, veremos como é que podemos associar um nome C# inválido a um método de ação). Ou seja, neste segundo exemplo, o problema não reside na obtenção de uma rota compatível, mas sim no facto de o valor do parâmetro action não corresponder a um nome válido em C#. © FCA – Editora de Informática
ROTEAMENTO
2.2.3
31
MAPEAMENTO DE PARÂMETROS DE ROTEAMENTO EM PARÂMETROS DO MÉTODO DE AÇÃO
A plataforma ASP.NET MVC consegue ainda transformar os restantes parâmetros de roteamento indicados por um padrão da rota em parâmetros do método de ação que será invocado. Para que isto aconteça, os nomes dos parâmetros do método de ação têm de ser os mesmos que os especificados pelo padrão da rota e a plataforma tem de conseguir converter as strings obtidas a partir do URL atual nos tipos dos parâmetros usados nos métodos de ação. Por exemplo, suponhamos que o método Cat é definido da seguinte forma: public class FilmesController:Controller { public ActionResult Cat(Int32 parametro3) { //obtem filmes da categoria return View(); } }
Ao encontrar a informação processada pelo módulo de roteamento a partir do URL /filmes/cat/1, a plataforma ASP.NET MVC acaba por atribuir o valor 1 ao parâmetro parametro3 durante a invocação do método Cat. Isto só é possível porque ambos os parâmetros (o de roteamento e o definido pelo método de ação) possuem o mesmo nome e porque a plataforma consegue converter o valor extraído do URL (uma string) no tipo esperado pelo parâmetro do método de ação (voltaremos a esta conversão noutra secção). Antes de avançarmos, importa ainda referir que todo o trabalho de mapeamento de parâmetros de roteamento em parâmetros de métodos é feito pela plataforma ASP.NET MVC a partir do dicionário de valores preenchido pela infraestrutura de roteamento. Ou seja, apesar de estarmos a falar acerca destes mapeamentos neste capítulo, a verdade é que o trabalho desempenhado pela infraestrutura de roteamento limita-se apenas a preencher o dicionário de dados com informação obtida a partir do URL do pedido HTTP. Este dicionário é depois usado por um componente especial, designado por model binder, para inicializar os parâmetros passados ao método de ação responsável pelo tratamento do pedido HTTP atual (voltaremos a este tópico no Capítulo 3).
© FCA – Editora de Informática
32
2.2.4
ASP.NET MVC
USO DE VALORES LITERAIS
Apesar de todos os exemplos apresentados até agora terem usado parâmetros nos segmentos, a verdade é que o padrão de uma rota também pode conter valores literais7. O excerto seguinte ilustra este cenário: /historico/{controller}/{action}/{parametro3}
Para que um URL seja compatível com a regra anterior, tem de começar sempre pela string /historico/. Por exemplo, o URL /historico/Filmes /Cat/1 é compatível com este padrão, mas o URL /Filmes/Cat/1 já não é. Importa ainda referir que um segmento pode ter um ou mais parâmetros, que podem, ou não, ser misturados com valores literais. A única restrição prende-se com o facto de dois parâmetros não poderem ser definidos consecutivamente. O excerto seguinte ilustra estes cenários: /historico/{lingua}-{pais}/{controller}/{action} //válido {controller}/{action}.{outroValor} //válido: . separa parâmetros {controller}{action}/{id} //inválido: dois parâmetros seguidos
2.2.5
PARÂMETRO CATCH ALL
A infraestrutura de roteamento permite ainda o mapeamento de vários segmentos de um URL num único parâmetro de roteamento, através do uso de um parâmetro catch all. O parâmetro catch all é um parâmetro especial, cujo nome é prefixado obrigatoriamente pelo caráter *. O padrão de uma rota só pode ter um parâmetro deste tipo e este parâmetro, quando definido, deve ocupar sempre o seu último segmento. O exemplo seguinte ilustra o uso deste tipo de parâmetros no padrão de uma rota: Routes.MapRoute( "teste", "{controller}/{action}/{*extra}");
O excerto seguinte apresenta vários URL e explica a forma como os parâmetros são inicializados a partir do parsing do URL (supondo que a regra apresentada no excerto anterior é a única presente na tabela de roteamento): URL: /Filmes/Cat/Opcao/Outra
7 Em computação, a expressão “valor literal” é usada para representar um valor constante no código de uma aplicação. Por exemplo, a instrução int a = 10; inicializa a variável a com o valor literal 10.
© FCA – Editora de Informática
ROTEAMENTO
33
//controller = "Filmes" //action = "Cat" //extra = "Opcao/Outra" URL: /Filmes/Cat //controller = "Filmes" //action = "Cat" //extra = ""
Note-se como o segundo URL apresentado no exemplo é compatível com a regra de roteamento "{controller}/{action}/{*extra}", sendo o parâmetro extra inicializado com uma string vazia.
2.2.6
VALORES PREDEFINIDOS
O algoritmo de mapeamento de um URL numa rota depende ainda da atribuição de valores predefinidos aos parâmetros. Para ilustrar este ponto, vamos começar por alterar a rota que temos usado até aqui: Routes.MapRoute( "teste", "{controller}/{action}/{parametro3}", new {parametro3 = UrlParameter.Optional});
A atribuição de um valor predefinido ao parâmetro parametro3 aumenta o âmbito de compatibilidade dos URL. Como vimos nas secções anteriores, a inexistência de um valor predefinido fazia com que um URL só fosse compatível se definisse explicitamente o valor de todos os parâmetros definidos pelo padrão. A partir da altura em que atribuímos um valor predefinido a um parâmetro, a definição desse parâmetro num URL deixa de ser obrigatória para que ele seja considerado compatível com um padrão de uma rota. Portanto, a partir de agora, todos os URL apresentados no excerto seguinte são compatíveis com a nossa regra teste: /Filmes/Cat/1 //parametro3 = 1 /Filmes/Cat //parametro3 não definido no dicionário
A atribuição do valor UrlParameter.Optional pode causar alguma estranheza à primeira vista. Porque não atribuir uma string vazia ("")? Apesar de o resultado final ser muito semelhante, existe uma diferença importante entre ambas as opções: o uso do valor UrlParameter.Optional faz com que o © FCA – Editora de Informática
34
ASP.NET MVC
dicionário preenchido pelo módulo de roteamento não contenha uma entrada para o parâmetro parametro3. Se tivéssemos optado pela string vazia, o dicionário acabaria por ter uma entrada para o parâmetro inicializada com a string (vazia). Como é óbvio, não estamos limitados a definir apenas um valor por predefinição. No excerto seguinte, modificamos a nossa rota, por forma a garantir que todos os parâmetros usados possuem valores predefinidos: Routes.MapRoute( "teste", "{controller}/{action}/{parametro3}", new { controller = "Filme", action = "Cat", parametro3 = UrlParameter.Optional});
A partir desta altura, qualquer um dos URL apresentados no excerto seguinte é compatível com a nossa rota teste: URL: /Filme/Cat/1 //controller="Filme" //action="Cat" //parametro3=1 /Filme //controller="Filme" //action="Cat" //parametro3 inexistente no dicionário / //controller="Filme" //action="Cat" //parametro3 inexistente no dicionário
Qualquer um dos URL apresentados na lista anterior acaba por ser “redirecionado” para o método Cat exposto pela classe FilmeController. Nesta altura, importa salientar que a atribuição de um valor predefinido a um parâmetro obriga-nos a atribuir valores predefinidos a todos os parâmetros seguintes definidos no padrão. Por exemplo, e recuperando a nossa rota teste (que usa o padrão {controller}/{action}/{parametro3}), se atribuímos um © FCA – Editora de Informática
ROTEAMENTO
35
valor predefinido ao parâmetro action, então temos também de atribuir um ao parâmetro parametro3. A rota seguinte não respeita esta regra, já que atribui apenas um valor predefinido ao parâmetro action: Routes.MapRoute( "teste", "{controller}/{action}/{parametro3}", new { action = "Cat" });
Se não respeitarmos esta regra, a plataforma acaba por ignorar os valores predefinidos quando testa a compatibilidade de um URL. Tenho mesmo de definir os parâmetros controller e action no URL? A atribuição de valores predefinidos aos parâmetros faz com que isso deixe de ser obrigatório. O excerto seguinte ilustra esta estratégia: Routes.MapRoute( "outroTeste", "historico/{action}/{id}", new {controller = "Filmes", action = "Cat", id = UrlParameter.Optional } ); Apesar de o padrão do URL não usar o parâmetro controller, a verdade é que o dicionário de parâmetros gerado pelo módulo de roteamento contém entradas identificadas pelos nomes controller e action. A partir desta altura, a plataforma ASP.NET MVC tem toda a informação necessária para reencaminhar o pedido atual para o controlador adequado.
2.2.7
APLICAÇÃO DE RESTRIÇÕES
Se quisermos, podemos ainda definir restrições extra numa rota, que influenciam a sua compatibilidade com o URL de um pedido. Por exemplo, suponhamos que registamos uma rota semelhante à seguinte: Routes.MapRoute( "teste", "{controller}/{action}/{parametro3}", new { controller = "Filme", © FCA – Editora de Informática
36
ASP.NET MVC
action = "Cat", parametro3 = UrlParameter.Optional });
A informação definida no registo da rota anterior não é suficiente para, por exemplo, especificar que o parâmetro parametro3 só pode receber um número inteiro. Esse requisito pode ser explicitado através do uso de uma expressão regular8 aplicada a esse parâmetro, conforme ilustrado através do excerto seguinte: Routes.MapRoute( "teste", "{controller}/{action}/{parametro3}", new { controller = "Filme", action = "Cat", parametro3 = UrlParameter.Optional }, new{parametro3 = @"\d+"} );
O overload do método MapRoute apresentado no excerto anterior recorre a uma expressão regular para garantir que a rota é considerada compatível apenas com URL onde o parâmetro de roteamento parametro3 é definido à custa de um número inteiro (note-se ainda como a restrição é associada ao parâmetro através da introdução de um novo objeto anónimo, onde cada propriedade identifica o nome do parâmetro de roteamento ao qual a restrição indicada deve ser aplicada). O leitor com experiência em expressões regulares não deixará de observar que a regra \d+ não é compatível só com valores numéricos, já que, por exemplo, aceita strings do tipo asd123aa (obviamente, não era isso que pretendíamos quando aplicámos a regra). A solução para este problema seria simples e passaria pela adição dos carateres de início e final de linha à expressão anterior. Contudo, isso não é necessário, uma vez que a infraestrutura de roteamento encarrega-se
As expressões regulares podem ser vistas como uma minilinguagem que permite efetuar operações de pesquisa e substituição sobre texto. O leitor interessado pode obter mais detalhes sobre estas expressões em http://bit.ly/bPCm7j. 8
© FCA – Editora de Informática