ASP.NET MVC Framework (2ª Parte): URL Routing

  1. El mes pasado escribí el primer post de una serie en la que veremos el nuevo ASP.NET MVC Framework. En aquel post vismo cómo crear un escenario simple en el que creamos un sitio para navegar por la lista de productos y librerías. Cubrimos los conceptos de alto nivel que hay detrás de MVC, y demostramos cómo crear un proyecto ASP.NET MVC de la nada hasta implementarlo y testear la funcionalidad de listado de productos.

En el post de hoy nos vamos a meter más en la arquitectura de rutado de ASP.NET MVC Framework, y veremos algunas cosas que podemos usar en escenarios más avanzados.

Recapitulando de la primera parte

En la primer post, creamos un sitio de e-comerce que exponía tres tipos de urls:

URL Format Behavior URL Example
/Products/Categories Browse all Product Categories /Products/Categories
/Products/List/Category List Products within a Category /Products/List/Beverages
/Products/Detail/ProductID Show Details about a Specific Product /Products

Manejábamos estas URLs creando la clase "ProductsController":

Una vez que añadimos esta clase a nuestra clase, ASP.NET MVC administra automáticamente las URLs entrantes y las ruta al método de acción al controlador correspondiente.

En el post de hoy vamos a meternos en cómo se hace este mapeo de URLs, y exploraremos escenarios más avanzados de rutado en los que nos aprovecharemos de las posibilidades de ASP.NET MVC. También demostraré lo fácil que es hacer test unitarios en estos escenarios.

¿Qué hace el sistema de rutado de URLs de ASP.NET MVC?

El framework ASP.NET incluye un sistema flexible de rutado de urls que nos permite definir reglas de mapeado en nuestras aplicaciones. El sistema de rutado tiene dos objetivos principales:

  1. Mapear urls entrantes a la aplicación y rutarlas al controlador y método de acción correctos.
  2. Crear urls de slaida que puedan ser usadas para volver a llamar a Controladores/Actiones (por ejemplo: desde un enlace <a href="" mce_href="">, y llamadas AJAX).

Poder usar reglas de mapeado de url para urls entrantes y salientes nos da una gran flexibilidad. Es decir, si queremos cambiar más adelante la estructura de urls de nuestra aplicacion (por ejemplo: cambiar el nombre /Products a /Catalog), podemos hacerlo modificando un conjunto de reglas de mapeo en el nivel de aplicación - sin tener que cambiar ninguna línea de código de los controladores o vistas.

Reglas de rutado por defecto de ASP.NET MVC

Cuando usamos Visual Studio para crear una aplicación con el template de "ASP.NET MVC Web Application" se añade una clase ASP.NET Application. Esto está implementado en el código trasero de Global.asax:

Esta clase permite a los programadores manejar el inicio/apagado de la aplicación y la administración de errores.

El template por defecto añade el método Application_Start a la clase y registra dos reglas de rutado asociadas:

La primera regla indica que ASP.NET MVC debería mapear las urls a los controladores usando el formato "[controlador]/[acción]/[id]" para determinar la clase controlador al que instanciar, y qué método de acción invocar (junto a los parámetros necesarios).

Esta regla por defecto es el motivo de que nuestra petición /Products/Detail/3 en el ejemplo del primer post invoca al método de detallles de la clase ProductsController y le pasa un 3 como argumento:

La segunda regla se añade para un caso especial: "Default.aspx", la raíz de nuestra aplicación (que normalemente pasan los servidores web en lugar de "/" cuando administran la raiz de las urls de una aplicación). Esta regla garantiza que las peticiones a "/Default.asx" ó "/" se manejan por la acción "Index()" de la clase "HomeController" (es un controlador que se añade automáticamente por Visual Studio cuando se crea un proyecto "ASP.NET MVC Web Application").

Comprendiendo las intancias de rutado

Las reglas de rutado se registran añadiendo instancias Route en la colección System.Web.Mvc.RouteTable.

La clase Route define unas propiedades que podemos usar para configurar las reglas de mapeo. Podemos configurar estas propiedades con las asiganciones tradicionales de .NET 2.0:

O aprovechandonos de la inicialización de objetos de los compiladores de C# y VB:

La propiedad Url de la clase Route define la regla que debe ser usada para comprobar si una regla de rutado se aplica a una petición entrante. También define cómo debe "tokenizarse" para los parámetros. Estos parámetros se definen con la sintaxis "[ParamName]". Como veremos más tarde, no estamos restrinjidos a usar un conjunto de parametros "bien conocidos" - podemos tener un número arbitrario de parámetros. Por ejemplo, podemos usar una regla "/Blogs/[Username]/Archive/[Year]/[Month]/[Day]/[Title]" para tokenizar las urls entrantes a los posts - y haremos que MVC Framework parsee y apase el Username, year, month, day y title como parámetros al método de acción del controlador.

La propiedad "Defaults" define un diccionario con valores por defecto a usar en el evento de la url entrante si no incluye alguno de los parámetros especificados. Por ejemplo, en los casos anteriores estamos definiendo dos valores por defecto - uno para "[action]" y otro para "[id]". Con esto si recibimos una url con /Products/ el sistema de rutado usará "Index" como nombre de la acción de ProductsController a ejecutar. Si se especificó /Products/List se pasará una cadena nula como parámetro ID.

La propiedad "RouteHandler" define la instancia IRouteHandler que debe usarse para procesar la petición después de que la URL es "tokenizada" y se determina cuál es la regla de rutado. En los ejemplos anteriores estamos indicando que queremos usar la clase System.Web.Mvc.MvcRounteHandler para procesar las URLs. El motivo de este paso extra es que queremos asegurarnos de que el sistema de rutado de URLs puede usarse tanto para peticiones MVC como NO-MVC. Con la interfaz IRouteHandler somos capazes de usar de forma limpia peticiones NO-MVC (como para los WebForms, soporte Astoria REST, etc).

También hay una propiedad "Validation" que veremos más tarde. Esta propiedad nos permite especificar precondiciones necesarias para encontrar la regla de rutado adecuada. Por ejemplo, podemos indicar que una regla de rutado sólo se aplicara a un verbo específico HTTP (pudiendo mapear así comandos REST), o podríamos usar expresiones regulares en los argumentos para ver qué regla es la adecuada.

Nota: En la primera preview pública de MVC la clase Route no es extensible . Estamos viendo si la haremos extensible en la próxima versión y permitir a los desarrolladores añadir reglas específicas (por ejemplo: una clase RestRoute) para añadir semántica adicional y funcionalidad.

Evaluacion de reglas de rutado

Cuando llega una peticion URL a una aplicacion ASP.NET MVC, el framework MVC evalua las reglas de la colección RouteTable.Routes para determinar el controlador apropiado para manejar la petición.

MVC elige el controlador a usar evaluando las reglas de RouteTable en el orden el que fueron registradas. La URL es comrobada con cada regla  y cuando encuentra una que sea adecuada nos dirá el RouteHandler que procesará la petición (y todas las demás reglas se ignoran). Con esto, la mejor forma de estructurar las reglas es de más específica a menos.

Escenario de rutado: Busqueda personalizada de urls

Vamos a ver ahora el uso de reglas de rutado personalizadas en un escenario real. Vamos a implementar la búsqueda en nuestro sitio de e-comerce.

Añadimos la clase SearchController al proyecto:

Ahora definiremos dos métodos de acción. Usaremos el método Index() para presentar una página de búsqueda que tenga un textbox para que se introduzca el texto a buscar. La acción Results() se usará para realizar la búsqueda contra la base de datos y mostrar los resultados al usuario:

Usando la regla de mapeado por defecto /[controller]/[action]/[id] , podríamos usar las urls siguientes para invocar las acciones del SearchController:

Scenario URL Action Method
Search Form: /Search/ Index
Search Results: /Search/Results?query=Beverages Results
  /Search/Results?query=ASP.NET Results

Fijáos que la razón por la que la raíz de /Search por defecto mapea la acción Index() ya que la definición /[controller]/[action]/[id] se crea por defecto cuando Visual Studio crea un nuevo proyecto:

Si bien URLs como Search/Results?query=Beverages son perfectamente funcionales, podemos querer URLs algo más "bonitas" para los resultados de búsqueda. Concretamente podríamos querer quitar el nombre de la acción de la URL "Results", y pasarla en la consulta como parte de la URL en lugar de usar un argumento QueryString. Por ejemplo:

Scenario URL Action Method
Search Form: /Search/ Index
Search Results: /Search/Beverages Results
  /Search/ASP.NET Results

Podemos permitir estas URLs "bonitas" añadiendo dos reglas de mapeado ántes de la de por defecto /[controller]/[action]/[id]:

Con estas dos reglas estamos especificando explícitamente los parámetros del Contorlador y de la Acción para /Search/URLs. Estamos indicando que "/Search" siempre será manejado por la acción "Index" en el SearchController. Cualquier URL con una jerarquía así (/Search/Foo, /Search/Bar, etc) siempre será manejada por la acción "Results" en el SearchController.

La segunda regla indica que cualquier prefijo de /Search/ debe tratarse como parámetro "[query]" que será pasado al método de acción:

Lo más común es que permitados unos resultados de búsquedas paginados (donde sólo mostraremos los 10 resultados de una vez). Esto lo podemos hacer a través de un argumento querystring (por ejemplo: /Search/Beverages?page=2) o podemos embeber el índice de la página como parte de la URL (Por ejemplo: /Search/Beverages/2). Para permitir esto lo que tenemos que añadir es un parámetro opcional extra a la segunda regla:

Fijáos cómo ahora la regla es "Search/[query]/[page]". También hemos configurado el índice por defecto a 1 en los casos en los que no se incluya en la url (esto se hace a través de los tipo anónimo como valores por defecto).

Ahora podemos actualizar el método de acción SearchControler.Results para que tome este parámetro de la página como un argumento:

Y con esto tenemos una URL "bonita" para las búsquedas de nuestro sitio (todo lo que queda por implementar es el algoritmo de búsqueda que dejaremos como ejercicio para el lectos <g>).

Validación de precondiciones para reglas de rutado

Como ya he mencionado ántes, la clase Route tiene la propiedad "Validation" que nos permite añadir precondiciones a validar para las reglas (a parte de los filtros de URL). El framework ASP.NET MVC nos permite usar expresiones regulares para validar cada parámetro  en la URL, así como evaluar las cabeceras HTTP (para rutar URLs vasándonos en verbos HTTP).

Aquí tenéis una validación personalizada que podríamos crear para las urls del tipo "/Products/Detail/43". Especifica que el argumento ID debe ser un número (no un string), y que debe tener entre 1 y 8 caracteres:

Si le pasamos la url /Products/Detail/12 a nuestra a plicación, la regla anterior será válida. Si le pasamos /Products/Detail/abc o /Products/Detail/23232323232323 no lo será.

Crear Urls de salida desde el sistema de rutado

Al principio del post dije que el sistema de rutado de urls de MVC era responsable de dos cosas:

  1. Mapear urls entrantes a Controladores/Acciones
  2. Ayudar en la construcción de URLs salientes que puedan ser usadas par ahacer call backs a los Controladores/Acciones (por ejemplo, desde posts, enlaces <a href="">,  llamadas de AJAX).

El sistema de rutado de URLs tiene una serie de métodos de ayuda y clases que hacen fácil crear URLs en tiempo de ejecución (también podéis tener URLs trabajando con la colección RouteTable.Route directamente.

Html.ActionLink

En la primera parte de esta serie resumí un poco el método Html.ActionLink(). Podemos usarlo en las vistas y permitir la generación de enlaces <a href="">. Lo interesante es que estas URLs se generan a partir de las reglas de mapeo del sistema e rutado. Por ejemplo, las dos llamadas a Html.ActionLink siguientes:

cojen automáticamente los resultados de la regla de búsquedas que configuramos antes y el atributo "href" se genera automáticamente :

Fijáos en cómo la segunda llamada al método Html.ActionLink mapea el parámetro de la "pagina" como parte de la URL (y fijáos en que la primera omite el valor - ya que sabe cual es el valor por defecto en el lado del servidor).

Url.Action

Además de usar el método Html.ActionLink, ASP.NET MVC también tiene este segundo método. Genera un conjunto de strings de URLs - que podemos usar donde queramos. Por ejemplo, el code snippet siguiente:

puede usarlo el sistema de rutado para devolver una url.

Controller.RedirectToAction

ASP.NET MVC también tiene este tercer método que podemos usar en los controladores para crear redirecciones (donde las URLs son generadas a partir del sistema de rutado de URLs)

Por ejemplo cuando invocamos al método siguiente en un controlador:

internamente genera una llama a Response.Redirect("/Search/Beverages")

DRY

Lo bonito de estos métodos es que nos permiten no tener un código enrevasdo en los paths de las URL en la lógica de los controladores y vistas. Si más adelante decidimos cambiar el mapeo de la ruta URL de la búsqueda de "/Search/[query]/[page]" a "/Search/Results/[query]/[page]" o /Search/Results?query=[query]&page=[page]"  podemos hacerlo fácilmente editándolo sólo en un lugar (en el código de registro de rutas). No tenemos que cambiar ningún código de nuestras vistas ni controladores (esto sigue  el principo DRY

Creando Urls de salida desde el sistema de rutado (con expresiones lambda)

Los ejemplos anteriores usan el nuevo soporte de tipos anónimos de VB y C# de VS 2008. En los ejemplos estamos usando los tipos anónimos para pasar la secuencia de nombres/valores para usarlos para mapar URLs(podéis pensar en esto como una forma limpia de generar diccionarios).

Además de para parámetros de forma dinámica usando tipos anónimos, el framework ASP.NET MVC también tiene la habilidad de crear acciones de rutas usando un mecanismo fuertemente tipado que nos da en tiempo de compilación intellisense para ayudarnos con las URLs. Esto lo hace con tipos genéricos y el soporte de VB y C# para expresiones Lambda.

Por ejemplo, el tipo anónimo de la llamada ActionLink:

También puede escribirse así:

Además de ser fácil de escribir, la segunda opción tiene el beneficio de que es type-safe, lo que quiere decir que tenemos checkeo en tiempo de compilacion e intellisense en Visual Studio (también podemos usar herramientas de refactoring):

Fijáos cómo podemos usar el intellisense para seleccionar el método Action del SearchController que queramos usar - y cómo los parámetros son fuertemente tipados. Las urls generadas son todas del sistema de rutado de ASP.NET MVC.

Os estaréis preguntando - ¿Como nos aseguramos de que esto funciona? Si recordáis, hace ocho meses escribí sobre las expresiones Lambda. Hablaba sobre cómo se compilan las expresiones lambda como un delegado, así como con un árbol de objetos de expresión que pueden ser usadas en tiempo de ejcución para analizar expresiones lambda. Con el método Html.ActionLink<T> usamos la opción de este árbol de expresiones y se analizan las lambda en tiempo de ejecución buscando el método de acción que invoca así como los tipos de los parámetros, nombres y valores que se especifican en la expresión. Podemos usar esto en el sistema de rutado de MVC para devolver la url adecuada y asociarle el HTML.

Importante: Cuando usamos estas expresiones lambda nunca ejecutaremos la acción del controlador. Por ejemplo, el siguiente código NO invoca el método de acción "Results" en el SearchController:

Si no que devuelve este link html:

Cuando se hace clic en este link mandará una petición http al servidor que invocará el método "Results" del SearchController.

Tests unitarios de rutas

Uno de los principios del diseño del framework ASP.NET MVC es permitir un soporte para test. Como todo el framework MVC, podemos hacer test unitarios sobre rutas y reglas de rutado. El sistema de rutado de MVC puede ser instanciado y ejecutado independientemente de ASP.NET - con esto podemos cargar y testear patrones de rutas con cualquier librería de test (no hay que ejecutar ningún servidor web) y usar cualquier framework (NUnit, MBUnit, MSTest, etc).

Aunque podemos hacer test unitarios con la colección de RouteTable de una aplicación ASP.NET MVC directamente, en general no es buena idea tener test unitarios que se basen en estados globales. Un mejor patrón que podemos usar es estructurar la lógica de registro de rutas en un método RegisterRoutes() como el siguiente que trabaja contra el RouteCollection que se pasa como argumenteo (nota: probablemente crearemos un template para VS en la próxima actualización):

Ahora podemos escribir test unitarios con la instacia de nuestra RouteCollection y llamar al método Registerroutes() de nuestra aplicación. Y podremos simular peticiones a la aplicación y comprobar que se registran los controladores y vistas adecuados - sin tener que preocuparnos en ningún efecto colateral:

Resumen

Este post nos enseña unos cuantos detalles sobre cómo funciona la arquitectura de rutado de ASP.NET MVC, y cómo podemos usarla para personalizar la estructura y aspecto de las urls que publicaremos en nuestras aplicaciones ASP.NET MVC.

Por defecto cuando creamos una aplicación web ASP.NET MVC se predefinirá la regla de rutado  /[controller]/[action]/[id] que podemos usar (sin tener que configurar nada). ESto nos permite crear muchas aplicaciones sin tener que registrar reglas de rutado personalizadas. Pero hemos demostrado que si queremos podemos crear nuestros propios formatos de URL y que no es difícil - y que el MVC framework nos permite una gran flexibilidad para hacer esto.

Espero que sirva.

Scott.

Traducido por: Juan María Laó Ramos.

ARtículo original: http://weblogs.asp.net/scottgu/archive/2007/12/03/asp-net-mvc-framework-part-2-url-routing.aspx

4 pensamientos en “ASP.NET MVC Framework (2ª Parte): URL Routing

  1. Pingback: Actualización del Road-Map del ASP.NET MVC Framework « Thinking in .NET

  2. lobo

    El articulo esta muy bueno y lo pude comprender mejor gracias a la traduccion! Gracias!
    Recien me estoy iniciando en MVC y no se si es posible o no lo que quiero hacer, pero pregunto aca por si alguien sabe la respuesta.

    Supongamos que en vez de buscar beverages (/Search/Beverages) quiero buscar el siguiente string “7*/%” (siete asterisco barra porciento), la URL que me quedaria seria /search/7*/% pero eso no es valido… y si uso la funcion HttpUtility.UrlEncode en ese texto para ponerlo en un link, obtengo: “7*%2f%25″, pero tambien es invalido…

    Se puede usar este tipo de enrutamiento por URL teniendo que buscar ese tipo de caracteres?

    Responder
  3. Rey

    Necesito tener mis controladores dentro de directorios dentro del directorio controller y lo mismo con las vistas.
    digamos cuando use la url: /Administracion/Usuario/Insertar que vaya al directorio Controller, la carpeta Administracion, el controlador usuario y el metodo Insertar, y que la vista que muestre tenga esa misma direccion pero en la carpeta View. Como podria ser la ruta que tengo que ponerle para que reconozca eso?

    Responder

Deja un comentario