tag route parameter net mvc attribute asp c# .net asp.net-mvc asp.net-mvc-5 asp.net-mvc-routing

c# - route - ASP.NET MVC 5 cultura en ruta y url



route attribute asp net core (2)

He traducido mi sitio web de mvc, que funciona muy bien. Si selecciono otro idioma (holandés o inglés), el contenido se traduce. Esto funciona porque puse la cultura en la sesión.

Ahora quiero mostrar la cultura seleccionada (= cultura) en la url. Si es el idioma predeterminado, no debe mostrarse en la url, solo si no es el idioma predeterminado, debe mostrarse en la url.

p.ej:

Para cultura predeterminada (holandés):

site.com/foo site.com/foo/bar site.com/foo/bar/5

Para cultura no predeterminada (inglés):

site.com/en/foo site.com/en/foo/bar site.com/en/foo/bar/5

Mi problema es que siempre veo esto:

site.com/nl / foo / bar / 5 incluso si hice clic en inglés (ver _Layout.cs). Mi contenido está traducido al inglés, pero el parámetro de ruta en la URL permanece en "nl" en lugar de "en".

¿Cómo puedo resolver esto o qué estoy haciendo mal?

Intenté en global.asax configurar el RouteData pero no ayuda.

public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.IgnoreRoute("favicon.ico"); routes.LowercaseUrls = true; routes.MapRoute( name: "Errors", url: "Error/{action}/{code}", defaults: new { controller = "Error", action = "Other", code = RouteParameter.Optional } ); routes.MapRoute( name: "DefaultWithCulture", url: "{culture}/{controller}/{action}/{id}", defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional }, constraints: new { culture = "[a-z]{2}" } );// or maybe: "[a-z]{2}-[a-z]{2} routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional } ); }

Global.asax.cs:

protected void Application_Start() { MvcHandler.DisableMvcResponseHeader = true; AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); } protected void Application_AcquireRequestState(object sender, EventArgs e) { if (HttpContext.Current.Session != null) { CultureInfo ci = (CultureInfo)this.Session["Culture"]; if (ci == null) { string langName = "nl"; if (HttpContext.Current.Request.UserLanguages != null && HttpContext.Current.Request.UserLanguages.Length != 0) { langName = HttpContext.Current.Request.UserLanguages[0].Substring(0, 2); } ci = new CultureInfo(langName); this.Session["Culture"] = ci; } HttpContextBase currentContext = new HttpContextWrapper(HttpContext.Current); RouteData routeData = RouteTable.Routes.GetRouteData(currentContext); routeData.Values["culture"] = ci; Thread.CurrentThread.CurrentUICulture = ci; Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name); } }

_Layout.cs (donde dejo que el usuario cambie el idioma)

// ... <ul class="dropdown-menu" role="menu"> <li class="@isCurrentLang("nl")">@Html.ActionLink("Nederlands", "ChangeCulture", "Culture", new { lang = "nl", returnUrl = this.Request.RawUrl }, new { rel = "alternate", hreflang = "nl" })</li> <li class="@isCurrentLang("en")">@Html.ActionLink("English", "ChangeCulture", "Culture", new { lang = "en", returnUrl = this.Request.RawUrl }, new { rel = "alternate", hreflang = "en" })</li> </ul> // ...

CultureController: (= donde configuro la sesión que uso en GlobalAsax para cambiar CurrentCulture y CurrentUICulture)

public class CultureController : Controller { // GET: Culture public ActionResult Index() { return RedirectToAction("Index", "Home"); } public ActionResult ChangeCulture(string lang, string returnUrl) { Session["Culture"] = new CultureInfo(lang); if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction("Index", "Home"); } } }


Corrección de cultura predeterminada

Increíble publicación de NightOwl888. Sin embargo, falta algo: las rutas de atributos de generación de URL normales (no localizadas), que se agregan a través de la reflexión, también necesitan un parámetro de cultura predeterminado; de lo contrario, se obtiene un parámetro de consulta en la URL.

? cultura = nl

Para evitar esto, estos cambios deben hacerse:

using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Web; using System.Web.Mvc; using System.Web.Mvc.Routing; using System.Web.Routing; namespace Endpoints.WebPublic.Infrastructure.Routing { public static class RouteCollectionExtensions { public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string urlPrefix, object defaults, object constraints) { MapLocalizedMvcAttributeRoutes(routes, urlPrefix, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints)); } public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string urlPrefix, RouteValueDictionary defaults, RouteValueDictionary constraints) { var routeCollectionRouteType = Type.GetType("System.Web.Mvc.Routing.RouteCollectionRoute, System.Web.Mvc"); var subRouteCollectionType = Type.GetType("System.Web.Mvc.Routing.SubRouteCollection, System.Web.Mvc"); FieldInfo subRoutesInfo = routeCollectionRouteType.GetField("_subRoutes", BindingFlags.NonPublic | BindingFlags.Instance); var subRoutes = Activator.CreateInstance(subRouteCollectionType); var routeEntries = Activator.CreateInstance(routeCollectionRouteType, subRoutes); // Add the route entries collection first to the route collection routes.Add((RouteBase)routeEntries); var localizedRouteTable = new RouteCollection(); // Get a copy of the attribute routes localizedRouteTable.MapMvcAttributeRoutes(); foreach (var routeBase in localizedRouteTable) { if (routeBase.GetType().Equals(routeCollectionRouteType)) { // Get the value of the _subRoutes field var tempSubRoutes = subRoutesInfo.GetValue(routeBase); // Get the PropertyInfo for the Entries property PropertyInfo entriesInfo = subRouteCollectionType.GetProperty("Entries"); if (entriesInfo.PropertyType.GetInterfaces().Contains(typeof(IEnumerable))) { foreach (RouteEntry routeEntry in (IEnumerable)entriesInfo.GetValue(tempSubRoutes)) { var route = routeEntry.Route; // Create the localized route var localizedRoute = CreateLocalizedRoute(route, urlPrefix, constraints); // Add the localized route entry var localizedRouteEntry = CreateLocalizedRouteEntry(routeEntry.Name, localizedRoute); AddRouteEntry(subRouteCollectionType, subRoutes, localizedRouteEntry); // Add the default route entry AddRouteEntry(subRouteCollectionType, subRoutes, routeEntry); // Add the localized link generation route var localizedLinkGenerationRoute = CreateLinkGenerationRoute(localizedRoute); routes.Add(localizedLinkGenerationRoute); // Add the default link generation route //FIX: needed for default culture on normal attribute route var newDefaults = new RouteValueDictionary(defaults); route.Defaults.ToList().ForEach(x => newDefaults.Add(x.Key, x.Value)); var routeWithNewDefaults = new Route(route.Url, newDefaults, route.Constraints, route.DataTokens, route.RouteHandler); var linkGenerationRoute = CreateLinkGenerationRoute(routeWithNewDefaults); routes.Add(linkGenerationRoute); } } } } } private static Route CreateLocalizedRoute(Route route, string urlPrefix, RouteValueDictionary constraints) { // Add the URL prefix var routeUrl = urlPrefix + route.Url; // Combine the constraints var routeConstraints = new RouteValueDictionary(constraints); foreach (var constraint in route.Constraints) { routeConstraints.Add(constraint.Key, constraint.Value); } return new Route(routeUrl, route.Defaults, routeConstraints, route.DataTokens, route.RouteHandler); } private static RouteEntry CreateLocalizedRouteEntry(string name, Route route) { var localizedRouteEntryName = string.IsNullOrEmpty(name) ? null : name + "_Localized"; return new RouteEntry(localizedRouteEntryName, route); } private static void AddRouteEntry(Type subRouteCollectionType, object subRoutes, RouteEntry newEntry) { var addMethodInfo = subRouteCollectionType.GetMethod("Add"); addMethodInfo.Invoke(subRoutes, new[] { newEntry }); } private static RouteBase CreateLinkGenerationRoute(Route innerRoute) { var linkGenerationRouteType = Type.GetType("System.Web.Mvc.Routing.LinkGenerationRoute, System.Web.Mvc"); return (RouteBase)Activator.CreateInstance(linkGenerationRouteType, innerRoute); } } }

Y para atribuir el registro de rutas:

RouteTable.Routes.MapLocalizedMvcAttributeRoutes( urlPrefix: "{culture}/", defaults: new { culture = "nl" }, constraints: new { culture = new CultureConstraint(defaultCulture: "nl", pattern: "[a-z]{2}") } );

Mejor solución

Y, en realidad, después de un tiempo, necesitaba agregar la traducción de URL, así que busqué más, y parece que no hay necesidad de hacer el hackeo de reflexión descrito. Los chicos de ASP.NET lo pensaron, hay una solución mucho más limpia; en cambio, puede extender un DefaultDirectRouteProvider como este:

public static class RouteCollectionExtensions { public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string defaultCulture) { var routeProvider = new LocalizeDirectRouteProvider( "{culture}/", defaultCulture ); routes.MapMvcAttributeRoutes(routeProvider); } } class LocalizeDirectRouteProvider : DefaultDirectRouteProvider { ILogger _log = LogManager.GetCurrentClassLogger(); string _urlPrefix; string _defaultCulture; RouteValueDictionary _constraints; public LocalizeDirectRouteProvider(string urlPrefix, string defaultCulture) { _urlPrefix = urlPrefix; _defaultCulture = defaultCulture; _constraints = new RouteValueDictionary() { { "culture", new CultureConstraint(defaultCulture: defaultCulture) } }; } protected override IReadOnlyList<RouteEntry> GetActionDirectRoutes( ActionDescriptor actionDescriptor, IReadOnlyList<IDirectRouteFactory> factories, IInlineConstraintResolver constraintResolver) { var originalEntries = base.GetActionDirectRoutes(actionDescriptor, factories, constraintResolver); var finalEntries = new List<RouteEntry>(); foreach (RouteEntry originalEntry in originalEntries) { var localizedRoute = CreateLocalizedRoute(originalEntry.Route, _urlPrefix, _constraints); var localizedRouteEntry = CreateLocalizedRouteEntry(originalEntry.Name, localizedRoute); finalEntries.Add(localizedRouteEntry); originalEntry.Route.Defaults.Add("culture", _defaultCulture); finalEntries.Add(originalEntry); } return finalEntries; } private Route CreateLocalizedRoute(Route route, string urlPrefix, RouteValueDictionary constraints) { // Add the URL prefix var routeUrl = urlPrefix + route.Url; // Combine the constraints var routeConstraints = new RouteValueDictionary(constraints); foreach (var constraint in route.Constraints) { routeConstraints.Add(constraint.Key, constraint.Value); } return new Route(routeUrl, route.Defaults, routeConstraints, route.DataTokens, route.RouteHandler); } private RouteEntry CreateLocalizedRouteEntry(string name, Route route) { var localizedRouteEntryName = string.IsNullOrEmpty(name) ? null : name + "_Localized"; return new RouteEntry(localizedRouteEntryName, route); } }

Hay una solución basada en esto, incluida la traducción de URL aquí: https://github.com/boudinov/mvc-5-routing-localization


Hay varios problemas con este enfoque, pero se reduce a ser un problema de flujo de trabajo.

  1. Tiene un CultureController cuyo único propósito es redirigir al usuario a otra página del sitio. Tenga en cuenta que RedirectToAction enviará una respuesta HTTP 302 al navegador del usuario, que le indicará que busque la nueva ubicación en su servidor. Este es un viaje de ida y vuelta innecesario a través de la red.
  2. Está utilizando el estado de sesión para almacenar la cultura del usuario cuando ya está disponible en la URL. El estado de la sesión es totalmente innecesario en este caso.
  3. Está leyendo el HttpContext.Current.Request.UserLanguages del usuario, que puede ser diferente de la cultura que solicitó en la URL.

El tercer problema se debe principalmente a una visión fundamentalmente diferente entre Microsoft y Google sobre cómo manejar la globalización.

La opinión (original) de Microsoft era que la misma URL debería usarse para cada cultura y que los UserLanguages de UserLanguages del navegador deberían determinar qué idioma debería mostrar el sitio web.

La opinión de Google es que cada cultura debe estar alojada en una URL diferente . Esto tiene más sentido si lo piensas. Es deseable que cada persona que encuentre su sitio web en los resultados de búsqueda (SERP) pueda buscar el contenido en su idioma nativo.

La globalización de un sitio web debe verse como contenido en lugar de personalización: está transmitiendo una cultura a un grupo de personas, no a una persona individual. Por lo tanto, generalmente no tiene sentido usar ninguna función de personalización de ASP.NET, como el estado de la sesión o las cookies para implementar la globalización; estas funciones evitan que los motores de búsqueda indexen el contenido de sus páginas localizadas.

Si puede enviar al usuario a una cultura diferente simplemente enrutando a una nueva URL, hay mucho menos de qué preocuparse: no necesita una página separada para que el usuario seleccione su cultura, simplemente incluya un enlace en el encabezado o pie de página para cambiar la cultura de la página existente y luego todos los enlaces cambiarán automáticamente a la cultura que el usuario ha elegido (porque MVC reutiliza automáticamente los valores de ruta de la solicitud actual ).

Solucionando los problemas

En primer lugar, elimine el CultureController y el código en el método Application_AcquireRequestState .

CultureFilter

Ahora, dado que la cultura es una preocupación transversal, establecer la cultura del hilo actual debe hacerse en un IAuthorizationFilter . Esto garantiza que la cultura se establezca antes de que ModelBinder se use en MVC.

using System.Globalization; using System.Threading; using System.Web.Mvc; public class CultureFilter : IAuthorizationFilter { private readonly string defaultCulture; public CultureFilter(string defaultCulture) { this.defaultCulture = defaultCulture; } public void OnAuthorization(AuthorizationContext filterContext) { var values = filterContext.RouteData.Values; string culture = (string)values["culture"] ?? this.defaultCulture; CultureInfo ci = new CultureInfo(culture); Thread.CurrentThread.CurrentCulture = ci; Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(ci.Name); } }

Puede configurar el filtro globalmente registrándolo como un filtro global.

public class FilterConfig { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new CultureFilter(defaultCulture: "nl")); filters.Add(new HandleErrorAttribute()); } }

Selección de idioma

Puede simplificar la selección de idioma al vincular a la misma acción y controlador para la página actual e incluirla como una opción en el encabezado o pie de página en su _Layout.cshtml .

@{ var routeValues = this.ViewContext.RouteData.Values; var controller = routeValues["controller"] as string; var action = routeValues["action"] as string; } <ul> <li>@Html.ActionLink("Nederlands", @action, @controller, new { culture = "nl" }, new { rel = "alternate", hreflang = "nl" })</li> <li>@Html.ActionLink("English", @action, @controller, new { culture = "en" }, new { rel = "alternate", hreflang = "en" })</li> </ul>

Como se mencionó anteriormente, todos los demás enlaces de la página pasarán automáticamente a una cultura del contexto actual, por lo que permanecerán automáticamente dentro de la misma cultura. No hay razón para pasar la cultura explícitamente en esos casos.

@ActionLink("About", "About", "Home")

Con el enlace anterior, si la URL actual es /Home/Contact , el enlace que se genera será /Home/About . Si la URL actual es /en/Home/Contact , el enlace se generará como /en/Home/About .

Cultura predeterminada

Finalmente, llegamos al corazón de su pregunta. La razón por la cual su cultura predeterminada no se genera correctamente es porque el enrutamiento es un mapa bidireccional e independientemente de si está haciendo coincidir una solicitud entrante o generando una URL saliente, la primera coincidencia siempre gana. Al crear su URL, la primera coincidencia es DefaultWithCulture .

Normalmente, puede solucionar esto simplemente invirtiendo el orden de las rutas. Sin embargo, en su caso, las rutas entrantes fallarían.

Por lo tanto, la opción más simple en su caso es crear una restricción de ruta personalizada para manejar el caso especial de la cultura predeterminada al generar la URL. Simplemente devuelve falso cuando se proporciona la cultura predeterminada y hará que el marco de enrutamiento .NET omita la ruta DefaultWithCulture y se mueva a la siguiente ruta registrada (en este caso, Default ).

using System.Text.RegularExpressions; using System.Web; using System.Web.Routing; public class CultureConstraint : IRouteConstraint { private readonly string defaultCulture; private readonly string pattern; public CultureConstraint(string defaultCulture, string pattern) { this.defaultCulture = defaultCulture; this.pattern = pattern; } public bool Match( HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { if (routeDirection == RouteDirection.UrlGeneration && this.defaultCulture.Equals(values[parameterName])) { return false; } else { return Regex.IsMatch((string)values[parameterName], "^" + pattern + "$"); } } }

Todo lo que queda es agregar la restricción a su configuración de enrutamiento. También debe eliminar la configuración predeterminada para cultura en la ruta DefaultWithCulture ya que solo desea que coincida cuando haya una cultura proporcionada en la URL de todos modos. La ruta Default , por otro lado, debe tener una cultura porque no hay forma de pasarla a través de la URL.

routes.LowercaseUrls = true; routes.MapRoute( name: "Errors", url: "Error/{action}/{code}", defaults: new { controller = "Error", action = "Other", code = UrlParameter.Optional } ); routes.MapRoute( name: "DefaultWithCulture", url: "{culture}/{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, constraints: new { culture = new CultureConstraint(defaultCulture: "nl", pattern: "[a-z]{2}") } ); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional } );

Enrutamiento de atributos

NOTA: Esta sección se aplica solo si está utilizando MVC 5. Puede omitir esto si está utilizando una versión anterior.

Para AttributeRouting, puede simplificar las cosas automatizando la creación de 2 rutas diferentes para cada acción. MapMvcAttributeRoutes ajustar cada ruta un poco y agregarlas a la misma estructura de clase que utiliza MapMvcAttributeRoutes . Desafortunadamente, Microsoft decidió hacer los tipos internos, por lo que requiere Reflection para instanciarlos y poblarlos.

RouteCollectionExtensions

Aquí solo usamos la funcionalidad integrada de MVC para escanear nuestro proyecto y crear un conjunto de rutas, luego insertamos un prefijo de URL de ruta adicional para la cultura y la CultureConstraint antes de agregar las instancias a nuestra MVC RouteTable.

También hay una ruta separada que se crea para resolver las URL (de la misma manera que lo hace AttributeRouting).

using System; using System.Collections; using System.Linq; using System.Reflection; using System.Web.Mvc; using System.Web.Mvc.Routing; using System.Web.Routing; public static class RouteCollectionExtensions { public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string urlPrefix, object constraints) { MapLocalizedMvcAttributeRoutes(routes, urlPrefix, new RouteValueDictionary(constraints)); } public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string urlPrefix, RouteValueDictionary constraints) { var routeCollectionRouteType = Type.GetType("System.Web.Mvc.Routing.RouteCollectionRoute, System.Web.Mvc"); var subRouteCollectionType = Type.GetType("System.Web.Mvc.Routing.SubRouteCollection, System.Web.Mvc"); FieldInfo subRoutesInfo = routeCollectionRouteType.GetField("_subRoutes", BindingFlags.NonPublic | BindingFlags.Instance); var subRoutes = Activator.CreateInstance(subRouteCollectionType); var routeEntries = Activator.CreateInstance(routeCollectionRouteType, subRoutes); // Add the route entries collection first to the route collection routes.Add((RouteBase)routeEntries); var localizedRouteTable = new RouteCollection(); // Get a copy of the attribute routes localizedRouteTable.MapMvcAttributeRoutes(); foreach (var routeBase in localizedRouteTable) { if (routeBase.GetType().Equals(routeCollectionRouteType)) { // Get the value of the _subRoutes field var tempSubRoutes = subRoutesInfo.GetValue(routeBase); // Get the PropertyInfo for the Entries property PropertyInfo entriesInfo = subRouteCollectionType.GetProperty("Entries"); if (entriesInfo.PropertyType.GetInterfaces().Contains(typeof(IEnumerable))) { foreach (RouteEntry routeEntry in (IEnumerable)entriesInfo.GetValue(tempSubRoutes)) { var route = routeEntry.Route; // Create the localized route var localizedRoute = CreateLocalizedRoute(route, urlPrefix, constraints); // Add the localized route entry var localizedRouteEntry = CreateLocalizedRouteEntry(routeEntry.Name, localizedRoute); AddRouteEntry(subRouteCollectionType, subRoutes, localizedRouteEntry); // Add the default route entry AddRouteEntry(subRouteCollectionType, subRoutes, routeEntry); // Add the localized link generation route var localizedLinkGenerationRoute = CreateLinkGenerationRoute(localizedRoute); routes.Add(localizedLinkGenerationRoute); // Add the default link generation route var linkGenerationRoute = CreateLinkGenerationRoute(route); routes.Add(linkGenerationRoute); } } } } } private static Route CreateLocalizedRoute(Route route, string urlPrefix, RouteValueDictionary constraints) { // Add the URL prefix var routeUrl = urlPrefix + route.Url; // Combine the constraints var routeConstraints = new RouteValueDictionary(constraints); foreach (var constraint in route.Constraints) { routeConstraints.Add(constraint.Key, constraint.Value); } return new Route(routeUrl, route.Defaults, routeConstraints, route.DataTokens, route.RouteHandler); } private static RouteEntry CreateLocalizedRouteEntry(string name, Route route) { var localizedRouteEntryName = string.IsNullOrEmpty(name) ? null : name + "_Localized"; return new RouteEntry(localizedRouteEntryName, route); } private static void AddRouteEntry(Type subRouteCollectionType, object subRoutes, RouteEntry newEntry) { var addMethodInfo = subRouteCollectionType.GetMethod("Add"); addMethodInfo.Invoke(subRoutes, new[] { newEntry }); } private static RouteBase CreateLinkGenerationRoute(Route innerRoute) { var linkGenerationRouteType = Type.GetType("System.Web.Mvc.Routing.LinkGenerationRoute, System.Web.Mvc"); return (RouteBase)Activator.CreateInstance(linkGenerationRouteType, innerRoute); } }

Entonces solo es cuestión de llamar a este método en lugar de MapMvcAttributeRoutes .

public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); // Call to register your localized and default attribute routes routes.MapLocalizedMvcAttributeRoutes( urlPrefix: "{culture}/", constraints: new { culture = new CultureConstraint(defaultCulture: "nl", pattern: "[a-z]{2}") } ); routes.MapRoute( name: "DefaultWithCulture", url: "{culture}/{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, constraints: new { culture = new CultureConstraint(defaultCulture: "nl", pattern: "[a-z]{2}") } ); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional } ); } }