¿Por qué asignar rutas especiales primero antes de rutas comunes en asp.net mvc?
asp.net-mvc asp.net-mvc-5 (1)
El motor de enrutamiento tomará la primera ruta que coincida con la URL proporcionada e intentará usar los valores de ruta en esa ruta.
La razón por la que esto sucede es porque
RouteTable
se usa como una declaración de cambio de mayúsculas y minúsculas.
Imagen de lo siguiente:
int caseSwitch = 1;
switch (caseSwitch)
{
case 1:
Console.WriteLine("Case 1");
break;
case 1:
Console.WriteLine("Second Case 1");
break;
default:
Console.WriteLine("Default case");
break;
}
Si
caseSwitch
es
1
, el segundo bloque nunca se alcanza porque el primer bloque lo atrapa.
Route
clases de
Route
siguen un patrón similar (en los métodos
GetRouteData
y
GetVirtualPath
).
Pueden devolver 2 estados:
-
Un conjunto de valores de ruta (o un objeto
VirtualPath
en el caso deGetVirtualPath
). Esto indica que la ruta coincide con la solicitud. -
null
Esto indica que la ruta no coincide con la solicitud.
En el primer caso, MVC usa los valores de ruta producidos por la ruta para buscar el método
Action
.
En este caso, la
RouteTable
ya no se analiza.
En el segundo caso, MVC verificará la próxima
Route
en la
RouteTable
para ver si coincide con la solicitud (el comportamiento integrado coincide con la URL y las restricciones, pero técnicamente puede hacer coincidir cualquier cosa en la solicitud HTTP).
Y una vez más, esa ruta puede devolver un conjunto de
RouteValues
de
RouteValues
o
null
dependiendo del resultado.
Si intenta usar una declaración de cambio de mayúsculas y minúsculas como se indicó anteriormente, el programa no se compilará.
Sin embargo, si configura una ruta que
nunca
devuelve
null
o devuelve un objeto
RouteValues
en más casos de lo que debería, el programa se compilará, pero se comportará mal.
Ejemplo de configuración incorrecta
Aquí está el ejemplo clásico que veo frecuentemente publicado en StackOverflow (o alguna variante de este):
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "CustomRoute",
url: "{segment1}/{action}/{id}",
defaults: new { controller = "MyController", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
En este ejemplo:
-
CustomRoute
coincidirá con cualquier URL que tenga 1, 2 o 3 segmentos de longitud (tenga en cuenta que elsegment1
es obligatorio porque no tiene un valor predeterminado). -
Default
coincidirá con cualquier URL que tenga 0, 1, 2 o 3 segmentos de longitud.
Por lo tanto, si la aplicación pasa la URL
/Home/About
,
CustomRoute
coincidirá y proporcionará los siguientes
RouteValues
de
RouteValues
a MVC:
-
segment1 = "Home"
-
controller = "MyController"
-
action = "About"
-
id = {}
Esto hará que MVC busque una acción llamada
About
en un controlador llamado
MyControllerController
, que fallará si no existe.
La ruta
Default
es una ruta de ejecución inalcanzable en este caso porque, aunque coincida con una URL de 2 segmentos, el marco no le dará la oportunidad porque la primera coincidencia gana.
Arreglando la configuración
Hay varias opciones sobre cómo proceder para arreglar la configuración. Pero todos dependen del comportamiento de que gane el primer partido y luego el enrutamiento no buscará más.
Opción 1: agregar uno o más segmentos literales
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "CustomRoute",
url: "Custom/{action}/{id}",
// Note, leaving `action` and `id` out of the defaults
// makes them required, so the URL will only match if 3
// segments are supplied begining with Custom or custom.
// Example: Custom/Details/343
defaults: new { controller = "MyController" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
Opción 2: Agregar 1 o más restricciones RegEx
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "CustomRoute",
url: "{segment1}/{action}/{id}",
defaults: new { controller = "MyController", action = "Index", id = UrlParameter.Optional },
constraints: new { segment1 = @"house|car|bus" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
Opción 3: Agregar 1 o más restricciones personalizadas
public class CorrectDateConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var year = values["year"] as string;
var month = values["month"] as string;
var day = values["day"] as string;
DateTime theDate;
return DateTime.TryParse(year + "-" + month + "-" + day, System.Globalization.CultureInfo.InvariantCulture, DateTimeStyles.None, out theDate);
}
}
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "CustomRoute",
url: "{year}/{month}/{day}/{article}",
defaults: new { controller = "News", action = "ArticleDetails" },
constraints: new { year = new CorrectDateConstraint() }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
Opción 4: Hacer segmentos requeridos + Hacer que el número de segmentos no coincida con las rutas existentes
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "CustomRoute",
url: "{segment1}/{segment2}/{action}/{id}",
defaults: new { controller = "MyController" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
En el caso anterior,
CustomRoute
solo coincidirá con una URL con 4 segmentos (tenga en cuenta que estos pueden ser cualquier valor).
La ruta
Default
como antes solo coincide con las URL con 0, 1, 2 o 3 segmentos.
Por lo tanto, no hay una ruta de ejecución inalcanzable.
Opción 5: Implementar RouteBase (o Ruta) para comportamiento personalizado
Cualquier cosa que el enrutamiento no admita desde el primer momento (como la coincidencia en un dominio o subdominio específico) se puede hacer
implementando su propia subclase
RouteBase
o subclase Route.
También es la mejor manera de entender cómo / por qué el enrutamiento funciona de la manera en que lo hace.
public class SubdomainRoute : Route
{
public SubdomainRoute(string url) : base(url, new MvcRouteHandler()) {}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var routeData = base.GetRouteData(httpContext);
if (routeData == null) return null; // Only look at the subdomain if this route matches in the first place.
string subdomain = httpContext.Request.Params["subdomain"]; // A subdomain specified as a query parameter takes precedence over the hostname.
if (subdomain == null) {
string host = httpContext.Request.Headers["Host"];
int index = host.IndexOf(''.'');
if (index >= 0)
subdomain = host.Substring(0, index);
}
if (subdomain != null)
routeData.Values["subdomain"] = subdomain;
return routeData;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
object subdomainParam = requestContext.HttpContext.Request.Params["subdomain"];
if (subdomainParam != null)
values["subdomain"] = subdomainParam;
return base.GetVirtualPath(requestContext, values);
}
}
Esta clase fue tomada de: ¿Es posible hacer una ruta ASP.NET MVC basada en un subdominio?
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new SubdomainRoute(url: "somewhere/unique"));
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
NOTA: El verdadero problema aquí es que la mayoría de las personas asumen que todas sus rutas deberían parecerse a la ruta
Default
. Copiar, pegar, listo, ¿verdad? Incorrecto.Hay 2 problemas que comúnmente surgen con este enfoque:
- Casi todas las demás rutas deben tener al menos un segmento literal (o una restricción si te gusta ese tipo de cosas).
- El comportamiento más lógico suele ser hacer que el resto de las rutas tengan segmentos requeridos .
Otro concepto erróneo común es que los segmentos opcionales significan que puede omitir cualquier segmento, pero en realidad solo puede omitir el segmento o segmentos más a la derecha.
Microsoft logró hacer un enrutamiento basado en convenciones, extensible y potente. No lograron hacerlo intuitivo de entender. Prácticamente todos fallan la primera vez que lo prueban (¡sé que lo hice!). Afortunadamente, una vez que entiendes cómo funciona, no es muy difícil.
Desde el www:
... El motor de enrutamiento tomará la primera ruta que coincida con la URL proporcionada e intentará usar los valores de ruta en esa ruta. Por lo tanto, las rutas menos comunes o más especializadas deben agregarse primero a la tabla, mientras que las rutas más generales deben agregarse más adelante ...
¿Por qué debo mapear rutas especializadas primero? ¿Alguien puede darme un ejemplo, por favor, donde puedo ver la falla de "mapa de ruta común primero"?