asp.net-mvc asp.net-mvc-5 url-routing asp.net-mvc-routing asp.net-mvc-5.2

¿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:

  1. Un conjunto de valores de ruta (o un objeto VirtualPath en el caso de GetVirtualPath ). Esto indica que la ruta coincide con la solicitud.
  2. 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:

  1. CustomRoute coincidirá con cualquier URL que tenga 1, 2 o 3 segmentos de longitud (tenga en cuenta que el segment1 es obligatorio porque no tiene un valor predeterminado).
  2. 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:

  1. segment1 = "Home"
  2. controller = "MyController"
  3. action = "About"
  4. 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:

  1. Casi todas las demás rutas deben tener al menos un segmento literal (o una restricción si te gusta ese tipo de cosas).
  2. 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"?