viernes, 19 de febrero de 2010

Nombres de algunos controladores distintos en ASP.NET MVC (ii)

Bueno… Este post es la continuación / aclaración del post anterior. En el post anterior configuramos la tabla de rutas junto con un RouteHandler propio y vimos que realmente las llamadas se enrutaban al controlador que tocaba: /api/Foo me enrutaba al controlador WarFoo y /Foo me enrutaba al controlador Foo.

Lo que no comenté es lo que deja de funcionar… :)

Supongo que teneis la tabla de rutas de la siguiente manera:

routes.MapRoute("Default","{controller}/{action}/{id}", 
new { controller = "Home", action = "Index", id = "" });
routes.Add(new Route("api/{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Home", action = "Index", id = "" }),
new WarRouteHandler())
);





Suponemos también que tenemos el controlador Foo (al que queremos acceder vía (/Foo) y WarFoo al cual queremos acceder via /api/Foo). Si uso Html.ActionLink para generar los enlaces obtengo lo siguiente:



























Llamada URL obtenida URL que querria yo
Html.ActionLink("Texto", "XX","Foo") /Foo/XX /Foo/XX
Html.ActionLink("Texto", "XX", "WarFoo") /WarFoo/XX /api/Foo/XX


Vamos a ver como podemos empezar a arreglar este desaguisado… Debemos recordar que el orden de las rutas en la tabla de rutas afecta: toda URL se empieza a comprobar en cada ruta, y se usa la primera que encaje. Dado que no hay nada que impida en la ruta Default que haya un controlador llamado WarFoo se usa esa ruta, y por eso obtenemos /WarFoo/XX como URL final.



Uno puede pensar que la solució es invertir el orden de las rutas en la tabla de rutas… Si lo haceis el reultado no es mucho mejor:



























Llamada URL obtenida URL que querria yo
Html.ActionLink("Texto", "XX","Foo") /api/Foo/XX /Foo/XX
Html.ActionLink("Texto", "XX", "WarFoo") /api/WarFoo/XX /api/Foo/XX


Que nos ocurre? Lo mismo de antes, salvo que ahora la primera ruta que se evalúa es la que tiene las URLs que empiezan por api. Pero esta ruta tampoco impone ninguna restricción sobre los controladores, así cualquier nombre de controlador también encaja en esta ruta, y es por eso que obtenemos siempre URLs que empiezan por api.



Cuando tenemos dos URLs que ambas aceptan cualquier controlador y acción, es dificil que ActionLink pueda distinguir entre una u otra, así que generalmente nos usará la primera definida para generar los enlaces. Dado que por norma general no queremos poner /api/ en todas las URLs podemos dejar la ruta “Default” como la primera. Ahora entra en acción RouteLink: podemos usar RouteLink para generar las URLs que deben empezar con /api y ActionLink para las que no. P.ej. puedo usar la siguiente llamada RouteLink, para onbtener la url /api/Foo/XX:




Html.RouteLink("Texto", "api", new RouteValueDictionary(
new { controller="Foo", action="XX"}))





Aquí estoy generando un link a la ruta “api” para generar el enlace. Debemos modificar la tabla de rutas para que la ruta que genera las urls con /api/ se llame api. Esto es tan simple como poner el nombre de la ruta como primer parámetro del método Add:




routes.Add("api", new Route("api/{controller}/{action}/{id}",
new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
new WarRouteHandler())
);





y todo solucionado no??? :)



Pues no… todavía nos queda un fleco… un fleco que también pase por alto en el post anterior.



Los enlaces estan bien generados, uno apunta a /Foo/XX y el otro a /api/Foo/XX. El primero funciona bien pero el segundo da un error… y eso?



Pensemos de nuevo en como ASP.NET MVC evalúa las rutas: por orden. Y la pregunta es la ruta /api/Foo/XX se puede procesar con la ruta {controller}/{action}/{id} (la Default)? Pues sí, suponiendo que controller es “api”, action es “Foo” e Id es “XX”. Es decir la ruta /api/Foo/XX me intenta invocar la acción Foo del controlador Api, pasándole XX como Id.



¿Cual es la solución entonces? Pues añadir una restricción a la ruta (Default) que impida que el nombre de los controladores sea “api”. De este modo si {controller} debe tomar el valor api para satisfacer la ruta, como tenemos la restricción la ruta no será satisfecha y ASP.NET MVC intentará usar la siguiente. Las restricciones se añaden como un nuevo parámetro en MapRoute:




routes.MapRoute("Default", "{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" },
new { controller = "restriccion" });






He añadido una restricción que afecta al parámetro controller. Y como se interpreta esta restricción. Pues bien, si es una cadena se interpreta como una expresión regular que debe cumplirse. Si la restricción no se puede (o no sabemos :p) expresar como una expresión regular podemos parar un objeto que implemente IRouteConstraint. Dado que yo soy muy torpe con las expresiones regulares, me he creado una clase que me comprueba que el valor no sea igual a un determinado valor:




public class NotEqualConstraint : IRouteConstraint
{
private string _match = String.Empty;
public NotEqualConstraint(string match)
{
_match = match;
}

public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
return String.Compare(values[parameterName].ToString(), _match, true) != 0;
}
}





Finalmente coloco la restricción en la ruta:




routes.MapRoute("Default", "{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" },
new { controller = new NotEqualConstraint("api") });





Ahora sí que sí. Los enlaces /api/Foo/XX no pueden ser procesados por la ruta Default, ya que no se cumple mi restricción (controller vale api), y entonces son procesados por la siguiente ruta (que es lo que queríamos). Ahora pues la url /Map/XX es procesada por la primera ruta y la URL /api/Map/XX es procesada por la segunda y enrutada al controlador WarMap.



Espero que estos dos posts os hayan ayudado a ver la potencia del sistema de rutas de ASP.NET MVC!



Un saludo a todos!



PD: Esto es un crosspost desde mi blog en geeks.ms

No hay comentarios: