Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 25 de marzo de 2014
mvcComo seguro sabéis, una de las novedades que acompañan a MVC 5 es la inclusión de un nuevo mecanismo para la definición de rutas llamado attribute routing, que básicamente consiste en acercar el registro de rutas a las acciones y controladores que las utilizan en lugar de definirlas en el tradicional y lejano RouteConfig.cs.


A nivel de código la idea es permitir definir las rutas utilizando atributos de la siguiente forma:
public class ProductController: Controller
 {
    [Route("product/{category}/{product}")] 
    public ActionResult Show(string category, string product) 
    { 
        ... 
    } 
 }
Las ventajas que tiene este enfoque sobre el tradicional, llamado ahora rutado por convenciones, es que permite un control más detallado de las rutas y que hace el código bastante más legible, pues de un simple vistazo podemos ver tanto la acción que ejecutará una petición como la URL mediante la cual llegaremos a ella. Además, es más sencillo de utilizar y más previsible en muchos casos.

Como inconveniente, quizás citar que obviamente esto crea dispersión en el código. Usando rutado por convenciones en un único punto definíamos el formato de las peticiones de entrada a nuestra aplicación, lo cual permite ver muy rápidamente los distintos puntos de entrada de nuestro sistema. Si utilizamos atributos, esta misma información la tendremos repartida a lo largo de todos los controladores de nuestra aplicación.

En cualquier caso, lo mejor de todo es que ambos mecanismos no son excluyentes, por lo podemos utilizarlos al mismo tiempo y beneficiarnos de lo mejor de cada enfoque.

El concepto de rutado por atributos no es nuevo, ya existía desde hace años en ASP.NET MVC y Web API como proyecto open source (AttributeRouting), de mano de Tim McCall, que, de hecho, ha colaborado con el equipo de Microsoft para integrar el producto en ambos frameworks.

Attribute routing es una característica presente en MVC 5 y Web API 2, pero aunque se usan de la misma forma y con los mismos atributos, internamente se trata de componentes distintos. Por poner un ejemplo, existen dos atributos [Route], uno definido en System.Web.Mvc (MVC) y otro en System.Web.Http (Web API). Aseguraos de utilizar el correcto en cada caso para que todo funcione.

1. Activar attribute routing

Este mecanismo se introduce de forma muy natural en las aplicaciones MVC. Internamente, lo único que hace es recorrer los controladores de la aplicación y buscar en ellos los atributos que nos ayudan a definir las rutas, introduciendo en la tabla de rutas estándar la entrada equivalente por nosotros.

Por esta razón, para utilizar el rutado por atributos debemos invocar de forma manual el método que realiza este registro automático de rutas para que se ejecute durante el arranque de la aplicación. En el caso de ASP.NET MVC, lo haremos en App_Start/RouteConfig.cs, como sigue:
public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapMvcAttributeRoutes();

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home",
                            action = "Index", 
                            id = UrlParameter.Optional 
            }
        );
    }
}
Y con Web API es prácticamente igual, sólo cambiaremos el método utilizado para registrar las rutas y el archivo en el que se introduce la llamada, que en este caso normalmente será App_Start/WebApiConfig.cs:
public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}
En ambos casos es muy conveniente recordar que el orden de registro de las rutas importa. En los ejemplos anteriores se han introducido antes que la ruta por defecto para que se tengan en cuenta primero a la hora de hacer el matching entre las reglas y la ruta de la petición entrante.

2. Reglas básicas

Las rutas las definiremos utilizando el atributo Route sobre la acción a la que queremos facilitar el acceso. Es exactamente igual en MVC y Web API, exceptuando el espacio de nombres empleado en cada caso, que, recordemos, eran System.Web.Mvc y System.Web.Http respectivamente.

Tanto las secciones fijas de la URL como los parámetros se introducen usando la sintaxis a la que estamos acostumbrados:
using System.Web.Mvc;

public class ProductController: Controller
{
    [Route("product/{category}/{product}")] 
    public ActionResult Show(string category, string product) 
    { 
       ... 
    } 
}
Obviamente, el nombre que asignamos a los parámetros del patrón de ruta debe ser idéntico al utilizado por los parámetros del método de acción, para que el binder pueda detectar esta coincidencia y mapearlos sin problema.

En el momento de la definición podemos asignar un nombre a la regla, de forma que podremos referirnos a ella más adelante para generar URLs o enlaces hacia una acción. Su significado sería similar al del parámetro name cuando mapeamos utilizando MapRoute()en MVC o MapHttpRoute() en Web API:
[Route("product/{category}/{product}", Name = "CategoryProduct")] 
public ActionResult Show(string category, string product) 
{ 
   ... 
} 
Cuando todas las acciones en un mismo controlador son accesible a través de URLs que comienzan por un prefijo común, es posible "ascenderlo" a nivel de controlador para evitar duplicidades::
[RoutePrefix("calculator")] 
public class CalculatorController : Controller 
{ 
   [Route("sum/{a}/{b}")] 
   public ActionResult Sum(int a, int b) { ... } 

   [Route("product/{a}/{b}")] 
   public ActionResult Product(int a, int b) { ... }
}
Si en alguna acción concreta queremos sobrescribir el prefijo asignado a nivel controlador, podemos hacerlo fácilmente precediendo su URL de acceso por el gusanillo “~”:
[RoutePrefix("calculator")] 
public class CalculatorController : Controller 
{ 
   [Route("~/sum/{a}/{b}")] 
   public ActionResult Sum(int a, int b) { ... } 

   [Route("product/{a}/{b}")] 
   public ActionResult Product(int a, int b) { ... }
}
En el ejemplo anterior, accederemos a la primera acción usando la dirección “/sum/3/4”, mientras que a la segunda tendremos que utilizar el prefijo “/calculator/product/3/4”.

Por defecto todos los parámetros de ruta son obligatorios. Si queremos indicar en alguno de ellos que es opcional, podemos hacerlo añadiendo el signo de interrogación tras su nombre, de la siguiente forma:
[Route("calculator/sum/{a}/{b?}")]
public ActionResult Sum(int a, int? b) { ... }
Y si nos interesa, incluso podemos especificar valores por defecto para ellos:
[Route("product/{a}/{b=1}")] 
public ActionResult Product(int a, int b) { ... }
Podemos combinar [Route] con otros atributos que ayuden al framework a determinar si la acción debe ser ejecutada o no. Por ejemplo, en Web API es muy frecuente encontrar atributos que indiquen el verbo HTTP permitido para cada acción (HttpGet, HttpPost, HttpPut, HttpPatch, HttpHead, HttpOptions, o HttpDelete):
[RoutePrefix("calculator")] 
public class CalculatorController : ApiController 
{ 
   [HttpGet, Route("sum/{a}/{b}")] 
   public int Sum(int a, int b) { ... } 

   [HttpPost, Route("product/{a}/{b}")] 
   public int Product(int a, int b) { ... }
}
En el ejemplo anterior, las acciones Web API se invocarían mediante peticiones del tipo “GET /calculator/sum/1/3” y “POST /calculator/product/3/4”  respectivamente. Si no indicamos el verbo, Web API utilizará su convención habitual, consistente en determinar el verbo permitido desde el nombre del método.

También es posible indicar a nivel de controlador la acción a ejecutar por defecto si ésta no es especifica en la URL entrante. En el siguiente ejemplo indicamos que las acciones del controlador son accesibles utilizando como prefijo la dirección “/product”, y, si no se indica nada más, se ejecutará la acción ShowAll:
[RoutePrefix("product")]
[Route("{action=ShowAll}")]
public class ProductController: Controller 
{
    public ActionResult ShowAll()
    {
       // returns a view showing all products
    }

    [...] // Other actions
}
Por último, en el caso de ASP.NET MVC disponemos de un atributo adicional llamado [RouteArea] que, como su nombre sugiere, permite indicar el área en el que se encuentra un controlador:
[RouteArea("Blog")] 
[RoutePrefix("Post")] 
public class ContentsController : Controller 
{ 
     // GET /blog/post/hello-world 
     [Route("{slug}")] 
     public ActionResult Show(string slug) 
     { 
         return Content("Post: "+slug); 
     } 
}
Un detalle interesante a añadir es que si utilizamos rutado por atributos en los controladores pertenecientes a un área ya no será necesario mantener el tradicional archivo AreaRegistration.cs donde se definían las rutas.

Y para no hacer muy extenso este post, lo dejaremos aquí. En entregas posteriores veremos cómo introducir restricciones o constraints en los parámetros, algo bastante útil pero incómodo de realizar usando el rutado tradicional y que con attribute routing se convierte en trivial.

Publicado en Variable not found.

2 Comentarios:

Sergio León dijo...

Hola Jose,

Muy buen post, la verdad es que hace unos días el Attribute Routing me salvo!! :-)

Lo cierto es que nunca había necesitado más rutas que la de por defecto, pero cuando necesité más el problema no fue agregarla sino los efectos colaterales... Me explico: En la aplicación utilizo siempre Html.ActionLink y Html.BeginForm y al agregar la nueva ruta todos estos enlaces pasaron a navegar a la primera ruta coincidente, la recién agregada :( Si lo hubiera sabido antes quizás hubiera utilizado Html.RouteLink y BeginRouteForm, pero con el proyecto a medias... pues no!! Sin embargo, ahí apareció al rescate Attribute Routing ;-) Me permite definir rutas y el código existente no cambiará nada, genial!
Un saludo y con ganas de leer más posts sobre este tema.

josé M. Aguilar dijo...

Gracias, Sergio!

Pues sí, el escenario que comentas es otra de las ventajas de attribute routing. A veces es realmente difícil prever los "daños colaterales" de introducir una nueva regla en la tabla de rutas, y usando este mecanismo lo tenemos mucho más fácil.

Un saludo!