Extreme ASP.NET

Roteamento com formulários da Web do ASP.NET

Scott Allen

Código disponível para download na MSDN Code Gallery
Navegue pelo código online

Sumário

O que é roteamento?
Um breve histórico de regravação de URL
Sobre rotas e manipuladores de rotas
Configurando o ASP.NET para roteamento
Configurando rotas
O manipulador de roteamento de receitas
Roteamento e segurança
Geração de URL
Conclusão sobre rotas

O Service Pack 1 do Microsoft .NET Framework 3.5 introduziu um mecanismo de roteamento ao runtime do ASP.NET. O mecanismo de roteamento pode desassociar a URL em uma solicitação de HTTP de entrada do formulário da Web físico que responde à solicitação, permitindo que você crie URLs amigáveis para seus aplicativos Web. Embora você possa usar URLs amigáveis em versões anteriores do ASP.NET, o mecanismo de roteamento fornece uma abordagem mais fácil, mais limpa e mais testável.

O mecanismo de roteamento começou como parte da estrutura MVC (Model View Controller) do ASP.NET, em estágio de visualização quando este artigo foi escrito. No entanto, a Microsoft reuniu a lógica de roteamento no assembly System.Web.Routing e lançou o assembly com o SP1. O assembly fornece atualmente o roteamento para sites usando os recursos de dados dinâmicos do ASP.NET (também lançados com o SP1), mas, nesta coluna, demonstrarei como usar a funcionalidade de roteamento com os formulários da Web do ASP.NET.

O que é roteamento?

Imagine que você possui um formulário da Web do ASP.NET chamado RecipeDisplay.aspx e ele reside em uma pasta chamada Web Forms. A abordagem clássica para exibir uma receita (recipe, em inglês) com esse formulário da Web é criar uma URL apontando para a localização física do formulário e codificar alguns dados na cadeia de caracteres de consulta para informar ao formulário da Web que receita exibir. O final de tal URL poder ter esta aparência: /WebForms/RecipeDisplay.aspx?id=5, onde o número 5 representa um valor de chave primário em uma tabela de banco de dados cheia de receitas.

O roteamento é fundamentalmente sobre decompor o ponto de extremidade de uma URL em parâmetros e, em seguida, usar esses parâmetros para conduzir o processamento de solicitação de HTTP a um componente específico. Como exemplo, vamos dar uma olhada na URL /recipe/5. Com a configuração adequada de roteamento, você ainda pode responder a essa URL com o formulário da Web RecipeDisplay.aspx.

A URL não representa mais um caminho físico. Em vez disso, a palavra recipe representa um parâmetro que o mecanismo de roteamento pode usar a fim de localizar um componente para processar solicitações de receitas. O número 5 representa um segundo parâmetro que você precisará durante o processamento para exibir uma receita específica. Em vez de codificar chaves de bancos de dados na URL, uma idéia melhor pode ser usar uma URL como /recipe/tacos. Essa URL não apenas inclui parâmetros suficientes para exibir uma receita específica, mas também pode ser lida por pessoas, revela sua intenção aos usuários finais e inclui palavras-chave importantes para os mecanismos de pesquisa exibirem.

Um breve histórico de regravação de URL

No ASP.NET, usar uma URL terminada com /recipe/tacos tradicionalmente exigia que se trabalhasse com um esquema de regravação de URL. Para obter informações detalhadas sobre a regravação de URL, consulte o artigo final de Scott Mitchell "Regravação de URL no ASP.NET." O artigo descreve a implementação comum de regravação de URL no ASP.NET usando um módulo HTTP e o método estático RewritePath da classe HttpContext. O artigo do Scott também detalha os benefícios de URLs amigáveis, que podem ser invadidas.

Aqueles de vocês que usaram a API RewritePath no passado provavelmente não estão familiarizados com algumas das sutilezas e problemas da abordagem de regravação. O principal problema com RewritePath é como o método altera o caminho virtual usado durante o processamento de uma solicitação. Com a regravação de URL, você precisava corrigir o destino do postback de cada formulário da Web (muitas vezes regravando a URL uma segunda vez durante a solicitação) para evitar postbacks na URL regravada interna.

Além disso, a maioria dos desenvolvedores implementaria a regravação de URL como uma conversão unidirecional porque não havia um mecanismo fácil para permitir que a lógica da regravação de URL funcionasse em duas direções. Por exemplo, era fácil fornecer à lógica de regravação de URL uma URL voltada para o público e fazer com que a lógica retornasse a URL interna de um formulário da Web. Era difícil fornecer à lógica de regravação a URL interna de um formulário da Web e fazer com que ela retornasse a URL pública necessária para alcançar o formulário. A última opção é útil ao gerar hiperlinks a outros formulários da Web que se ocultam atrás de URLs regravadas. Como você verá no restante desta coluna, o mecanismo de roteamento de URL contorna esses problemas.

fig01.gif

Figura 1 Rotas, manipuladores de rotas e o módulo de roteamento

Sobre rotas e manipuladores de rotas

Existem três jogadores fundamentais no mecanismo de roteamento de URL: as rotas, os manipuladores de rotas e o módulo de roteamento. Uma rota associa uma URL a um manipulador de rotas. Uma instância da classe Route do namespace System.Web.Routing representa uma rota durante o runtime e descreve os parâmetros e as restrições da rota. Um manipulador de rotas é herdado da interface System.Web.Routing.IRouteHandler. Essa interface requer que o manipulador de rotas implemente um método GetHttpHandler que retorna um objeto que implementa a interface IHttpHandler. A interface IHttpHandler é uma parte do ASP.NET desde o início e um formulário da Web (System.Web.UI.Page) é um IHttpHandler. Ao usar o roteamento com formulários da Web, seus manipuladores de rotas precisam localizar, instanciar e retornar o formulário da Web adequado. Finalmente, o módulo de roteamento se vincula ao pipeline de processamento do ASP.NET. O módulo irá interceptar solicitações de entrada, examinar a URL e descobrir se existem rotas correspondentes definidas. O módulo irá recuperar o manipulador de rotas associado para uma rota correspondente e solicitar ao manipulador de roteamento o IHttpHandler que irá processar a solicitação.

Os três tipos principais que mencionei são mostrados na Figura 1. Na próxima seção, colocarei esses três jogadores para trabalhar.

Configurando o ASP.NET para roteamento

Para configurar um site do ASP.NET ou um aplicativo Web para roteamento, primeiro você precisa adicionar uma referência ao assembly System.Web.Routing. A instalação do SP1 para o .NET Framework 3.5 instalará esse assembly no cache global de assemblies e você poderá encontrar o assembly dentro da caixa de diálogo padrão "Add Reference" (Adicionar Referência).

Também será necessário configurar o módulo de roteamento no pipeline do ASP.NET. O módulo de roteamento é um módulo HTTP padrão. Para o IIS 6.0 e versões anteriores e para o servidor de desenvolvimento Web do Visual Studio, você instala o módulo usando a seção <httpModules> de web.config, como vê aqui:

<httpModules>      
    <add name="RoutingModule" 
         type="System.Web.Routing.UrlRoutingModule, 
               System.Web.Routing, 
               Version=3.5.0.0, Culture=neutral, 
               PublicKeyToken=31bf3856ad364e35"/>

    <!-- ... -->

</httpModules>

Para executar um site com o roteamento no IIS 7.0, você precisa de duas entradas em web.config. A primeira entrada é a configuração do módulo de roteamento de URL, encontrada na seção <modules> de <system.webServer>. Você também precisa de uma entrada para manipular solicitações para UrlRouting.axd na seção <handlers> de <system.webServer>. Ambas essas entradas são mostradas na Figura 2. Além disso, consulte o quadro "Entradas de configuração do IIS 7.0".

Figura 2 Configuração do módulo de roteamento de URL

<system.webServer>
   <modules runAllManagedModulesForAllRequests="true">

      <add name="UrlRoutingModule"
             type="System.Web.Routing.UrlRoutingModule, 
                   System.Web.Routing, Version=3.5.0.0, 
                   Culture=neutral, 
                   PublicKeyToken=31BF3856AD364E35" />
      <!-- ... -->

    </modules>
    <handlers>

      <add name="UrlRoutingHandler"
            preCondition="integratedMode"
            verb="*" path="UrlRouting.axd"
            type="System.Web.HttpForbiddenHandler, 
                  System.Web, Version=2.0.0.0, Culture=neutral, 
                  PublicKeyToken=b03f5f7f11d50a3a" />
      <!-- ... -->

    </handlers>
</system.webServer>

Depois de ter configurado o módulo de roteamento de URL no pipeline, ele irá se conectar aos eventos PostResolveRequestCache e PostMapRequestHandler. A Figura 3 mostra um subconjunto dos eventos do pipeline. Implementações de regravação de URL normalmente executam seu trabalho durante o evento BeginRequest, que é o primeiro evento a ser disparado durante uma solicitação. Com o roteamento de URL, a correspondência de rotas e a seleção de um manipulador de rotas ocorrem durante o estágio PostResolveRequestCache, após os estágios de processamento de autenticação, autorização e pesquisa de cache. Precisarei revisitar as implicações do tempo desse evento posteriormente nesta coluna.

fig03.gif

Figura 3 Solicitação HTTP

Configurando rotas

As rotas e os manipuladores de rotas andam de mãos dadas, mas eu examinarei o código para configurar as rotas primeiro. A classe RouteTable do mecanismo de roteamento expõe um RouteCollection por meio de sua propriedade estática Routes. Você precisa configurar todas as suas rotas personalizadas nessa coleção antes que o aplicativo comece a executar a primeira solicitação, o que significa que você precisa usar um arquivo global.asax e o evento Application_Start.

A Figura 4 mostra o código de registro da rota necessário para "/recipe/brownies" para chegar ao formulário da Web RecipeDisplay.aspx. Os parâmetros para o método Add na classe RouteCollection incluem um nome amigável para a rota, seguido pela rota em si. O primeiro parâmetro para o construtor Route é um padrão de URL. O padrão consiste em segmentos da URL que aparecerão no final de uma URL apontando para esse aplicativo (após qualquer segmento necessário para chegar à raiz do aplicativo). Para um aplicativo com raiz em localhost/food/, o padrão da rota da Figura 4 será correspondente a localhost/food/recipe/brownies.

Figura 4 Código de registro de rota para /recipe/brownies

protected void Application_Start(object sender, EventArgs e)
{
    RegisterRoutes();
}

private static void RegisterRoutes()
{
    RouteTable.Routes.Add(
        "Recipe",
        new Route("recipe/{name}", 
                  new RecipeRouteHandler(
                      "~/WebForms/RecipeDisplay.aspx")));
}

Entradas de configuração do IIS 7.0

O atributo runAllManagedModulesForAllRequests requer um valor de verdadeiro se você desejar usar as URLs sem extensões como fiz nesse exemplo. Além disso, pode parecer estranho configurar um manipulador HTTP para UrlRouting.axd. Essa é uma pequena solução temporária que o mecanismo de roteamento requer para o roteamento funcionar no IIS 7.0. O módulo UrlRouting realmente regrava a URL de entrada para /UrlRouting.axd, o que irá regravar a URL de volta à URL de entrada original. É provável que uma versão futura do IIS se integre perfeitamente com o mecanismo de roteamento e não precise dessa solução temporária.

Os segmentos entre chaves denotam parâmetros e o mecanismo de roteamento irá extrair automaticamente os valores deles e colocá-los em um dicionário name/value que existirá durante a solicitação. Usando o exemplo anterior de localhost/food/recipe/brownies, o mecanismo de roteamento irá extrair o valor "brownies" e armazená-lo no dicionário com uma chave "name". Você verá como usar o dicionário quando eu examinar o código do manipulador de rotas.

Você pode adicionar quantas rotas precisar em RouteTable, mas a ordem das rotas é importante. O mecanismo de roteamento testará todas as URLs de entrada em relação às rotas da coleção na ordem em que elas aparecem e o mecanismo irá selecionar a primeira rota com um padrão correspondente. Por essa razão, você deve adicionar as rotas mais específicas primeiro. Se você adicionou uma rota genérica com o padrão de URL "{category}/{subcategory}" antes da rota da receita, o mecanismo de roteamento nunca encontrará a rota da receita. Uma observação adicional: o mecanismo de roteamento executa a correspondência de padrão de uma maneira que não diferencia maiúsculas de minúsculas.

Versões sobrecarregadas do construtor Route permitem que você crie valores de parâmetros padrão e aplique restrições. Os padrões permitem que você especifique valores padrão para o mecanismo de roteamento colocar no dicionário de parâmetros name/value quando nenhum valor existir para o parâmetro em uma URL de entrada. Por exemplo, você poderia fazer de "brownies" o nome de receita padrão quando o mecanismo de roteamento exibir uma URL de receita sem um valor de nome (como localhost/food/recipe).

As restrições permitem que você especifique expressões regulares para validar parâmetros e ajustar a correspondência de padrão de rota em URLs de entrada. Se você estava usando valores de chaves primários para identificar receitas em uma URL (como localhost/food/recipe/5), você poderia usar uma expressão regular para garantir que o valor de chave primário na URL é um inteiro. Você também pode aplicar restrições usando um objeto que implementa a interface IRouteConstraint.

O segundo parâmetro para o construtor Route é uma nova instância do meu manipulador de rotas, que examinarei na Figura 5.

Figura 5 RecipeRouteHandler

public class RecipeRouteHandler : IRouteHandler
 {
     public RecipeRouteHandler(string virtualPath)
     {
         _virtualPath = virtualPath;
     }

     public IHttpHandler GetHttpHandler(RequestContext requestContext)
     {
         var display = BuildManager.CreateInstanceFromVirtualPath(
                         _virtualPath, typeof(Page)) as IRecipeDisplay;

         display.RecipeName = requestContext.RouteData.Values["name"] as string;
         return display;
     }

     string _virtualPath;
 }

O manipulador de roteamento de receitas

O trecho do código a seguir mostra uma implementação básica de um manipulador de rotas para solicitações de receitas. Como o manipulador de rotas precisa, por último, criar uma instância de um IHttpHandler (neste caso, RecipeDisplay.aspx), o construtor requer um caminho virtual que aponte para o formulário da Web que o manipulador de rotas criará. O método GetHttpHandler transmite esse caminho virtual ao BuildManager do ASP.NET para recuperar o formulário da Web instanciado:

interface IRecipeDisplay : IHttpHandler
{
    string RecipeName { get;  set; }
}

Observe como o manipulador de rotas também pode extrair dados do dicionário de parâmetros do mecanismo de roteamento, que é a propriedade RouteData da classe RequestContext. O mecanismo de roteamento configura o RequestContext e transmite uma instância quando invoca esse método. Existem várias opções disponíveis para colocar os dados da rota no formulário da Web. Você poderia transmitir os dados da rota na coleção HttpContext Items, por exemplo. Nesse exemplo, você definiu uma interface para seu formulário da Web implementar (IRecipeDisplay). O manipulador de rotas pode definir propriedades com rigidez de tipos no formulário da Web para transmitir qualquer informação que o formulário da Web exija e essa abordagem funcionará com o site do ASP.NET e modelos de compilação do aplicativo ASP.NET.

Roteamento e segurança

Quando vocês está usando o roteamento do ASP.NET, ainda pode usar todos os recursos do ASP.NET que adora — páginas mestras, cache de saída, temas, controles do usuário e mais. Contudo, existe uma exceção notável. O módulo de roteamento faz sua mágica usando eventos no pipeline que ocorrem após os estágios do processamento de autenticação e autorização, o que significa que o ASP.NET irá autorizar seus usuários usando a URL pública, visível, e não o caminho virtual para o formulário da Web do ASP.NET que o manipulador de rotas seleciona para processar a solicitação. Você precisa prestar bastante atenção à estratégia de autorização para um aplicativo que usa o roteamento.

Digamos que você queria apenas permitir que usuários autenticados exibissem receitas. Uma abordagem seria modificar o web.config raiz para usar as configurações de autorização aqui:

<location path="recipe">
  <system.web>
    <authorization>
      <deny users="?"/>
    </authorization>
  </system.web>
</location>

Embora essa abordagem impeça que usuários anônimos exibam /recipe/tacos, ela possui dois problemas fundamentais. Primeiro, a configuração não impede que um usuário solicite diretamente /WebForms/RecipeDisplay.aspx (embora você possa adicionar outra regra de autorização que impeça que todos os usuários solicitem diretamente recursos da pasta Web Forms). Segundo, é fácil alterar a configuração da rota em global.asax.cs sem alterar as regras de autorização e deixar suas receitas secretas abertas a usuários anônimos.

Uma abordagem alternativa para a autorização seria proteger o formulário da Web RecipeDisplay.aspx com base em sua localização física, que é colocar arquivos web.config com configurações <authorization> diretamente na pasta protegida. No entanto, como o ASP.NET está autorizando usuários com base na URL pública, você precisará fazer as verificações de autorização manualmente no caminho virtual que seu manipulador de rotas usa.

Você precisará adicionar o seguinte código no início do método GetHttpHandler do seu manipulador de rotas. Esse código usa o método estático CheckUrlAccessForPrincipal da classe UrlAuthorizationModule (o mesmo módulo que executa verificações de autorização no pipeline do ASP.NET):

if (!UrlAuthorizationModule.CheckUrlAccessForPrincipal(
    _virtualPath, requestContext.HttpContext.User,
                  requestContext.HttpContext.Request.HttpMethod))
{
    requestContext.HttpContext.Response.StatusCode = 
        (int)HttpStatusCode.Unauthorized;
    requestContext.HttpContext.Response.End(); 
}

Para acessar os membros de HttpContext por RequestContext, será necessário adicionar uma referência ao assembly System.Web.Abstractions.

Com um manipulador de roteamento seguro no lugar, você pode agora voltar sua atenção à página que precisa gerar hiperlinks para cada receita do seu banco de dados. Acontece que a lógica do roteamento pode ajudá-lo a criar essa página também.

Geração de URL

Para gerar o hiperlink para qualquer receita fornecida, mais uma vez, irei me voltar à coleção de rotas configuradas durante uma inicialização do aplicativo. Como mostrado aqui, a classe RouteCollection possui um método GetVirtualPath para esse fim:

VirtualPathData pathData = 
    RouteTable.Routes.GetVirtualPath(
                  null,
                  "Recipe",
                  new RouteValueDictionary { { "Name", recipeName } });

return pathData.VirtualPath;

Você precisa transmitir o nome de rota desejado ("Recipe") com um dicionário dos parâmetros necessários e seus valores associados. Esse método usará o padrão de URL criado anteriormente (/recipe/{name}) para construir a URL adequada.

O código a seguir usa esse método para gerar uma coleção de objetos de tipos anônimos. Os objetos possuem propriedades Name e Url que você pode usar com a vinculação de dados para gerar uma lista ou tabela de receitas disponíveis:

var recipes = 
    new RecipeRepository()
        .GetAllRecipeNames()
        .OrderBy(recipeName => recipeName)
        .Select(recipeName =>
          new
          {
            Name = recipeName,
            Url = GetVirtualPathForRecipe(recipeName)
          });

A capacidade de gerar URLs a partir da sua configuração de roteamento significa que você pode alterar a configuração sem o receio de criar links quebrados dentro do seu aplicativo. É claro que você ainda pode quebrar os links e marcadores favoritos do seu usuário, mas ter a capacidade de alterar é uma tremenda vantagem quando você ainda está projetando a estrutura de URL do aplicativo.

Conclusão sobre rotas

O mecanismo de roteamento de URL faz todo o trabalho sujo de correspondência de padrão de URL e geração de URL. Tudo o que você precisa fazer é configurar suas rotas e implementar seus manipuladores de rotas. Com o roteamento, você está verdadeiramente isolado de extensões de arquivos e do layout físico do seu sistema de arquivos e não precisa lidar com as sutilezas do uso de um regravador de URL. Em vez disso, você pode se concentrar no design ideal de URL para seus usuários finais e para os mecanismos de pesquisa. Além disso, a Microsoft está trabalhando para tornar o roteamento de URL com formulários da Web algo ainda mais fácil e mais configurável no futuro ASP.NET 4.0.

Envie perguntas e comentários para xtrmasp@microsoft.com.

Scott Allen é fundados da OdeToCode e membro da equipe técnica da Pluralsight. Entre em contato com o Scott pelo email scott@odetocode.com ou leia seu blog em OdeToCode.com/blogs/scott.