ASP.NET MVC 5 culture in route and url
Asked Answered
D

2

59

I've translated my mvc website, which is working great. If I select another language (Dutch or English) the content gets translated. This works because I set the culture in the session.

Now I want to show the selected culture(=culture) in the url. If it is the default language it should not be showed in the url, only if it is not the default language it should show it in the url.

e.g.:

For default culture (dutch):

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

For non-default culture (english):

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

My problem is that I always see this:

site.com/nl/foo/bar/5 even if I clicked on English (see _Layout.cs). My content is translated in English but the route parameter in the url stays on "nl" instead of "en".

How can I solve this or what am I doing wrong?

I tried in the global.asax to set the RouteData but doesn't help.

  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 (where I let user change language)

// ...
                            <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: (=where I set the Session that I use in GlobalAsax to change the CurrentCulture and 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");
      }
    }
  }
Dore answered 24/9, 2015 at 15:14 Comment(0)
D
145

There are several issues with this approach, but it boils down to being a workflow issue.

  1. You have a CultureController whose only purpose is to redirect the user to another page on the site. Keep in mind RedirectToAction will send an HTTP 302 response to the user's browser, which will tell it to lookup the new location on your server. This is an unnecessary round-trip across the network.
  2. You are using session state to store the culture of the user when it is already available in the URL. Session state is totally unnecessary in this case.
  3. You are reading the HttpContext.Current.Request.UserLanguages from the user, which might be different from the culture they requested in the URL.

The third issue is primarily because of a fundamentally different view between Microsoft and Google about how to handle globalization.

Microsoft's (original) view was that the same URL should be used for every culture and that the UserLanguages of the browser should determine what language the website should display.

Google's view is that every culture should be hosted on a different URL. This makes more sense if you think about it. It is desirable for every person who finds your website in the search results (SERPs) to be able to search for the content in their native language.

Globalization of a web site should be viewed as content rather than personalization - you are broadcasting a culture to a group of people, not an individual person. Therefore, it typically doesn't make sense to use any personalization features of ASP.NET such as session state or cookies to implement globalization - these features prevent search engines from indexing the content of your localized pages.

If you can send the user to a different culture simply by routing them to a new URL, there is far less to worry about - you don't need a separate page for the user to select their culture, simply include a link in the header or footer to change the culture of the existing page and then all of the links will automatically switch to the culture the user has chosen (because MVC automatically reuses route values from the current request).

Fixing the Issues

First of all, get rid of the CultureController and the code in the Application_AcquireRequestState method.

CultureFilter

Now, since culture is a cross-cutting concern, setting the culture of the current thread should be done in an IAuthorizationFilter. This ensures the culture is set before the ModelBinder is used in 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);
    }
}

You can set the filter globally by registering it as a global filter.

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

Language Selection

You can simplify the language selection by linking to the same action and controller for the current page and including it as an option in the page header or footer in your _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>

As mentioned previously, all other links on the page will automatically be passed a culture from the current context, so they will automatically stay within the same culture. There is no reason to pass the culture explicitly in those cases.

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

With the above link, if the current URL is /Home/Contact, the link that is generated will be /Home/About. If the current URL is /en/Home/Contact, the link will be generated as /en/Home/About.

Default Culture

Finally, we get to the heart of your question. The reason your default culture is not being generated correctly is because routing is a 2-way map and regardless of whether you are matching an incoming request or generating an outgoing URL, the first match always wins. When building your URL, the first match is DefaultWithCulture.

Normally, you can fix this simply by reversing the order of the routes. However, in your case that would cause the incoming routes to fail.

So, the simplest option in your case is to build a custom route constraint to handle the special case of the default culture when generating the URL. You simply return false when the default culture is supplied and it will cause the .NET routing framework to skip the DefaultWithCulture route and move to the next registered route (in this case 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 + "$");
        }
    }
}

All that is left is to add the constraint to your routing configuration. You also should remove the default setting for culture in the DefaultWithCulture route since you only want it to match when there is a culture supplied in the URL anyway. The Default route on the other hand should have a culture because there is no way to pass it through the 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 }
);

AttributeRouting

NOTE: This section applies only if you are using MVC 5. You can skip this if you are using a previous version.

For AttributeRouting, you can simplify things by automating the creation of 2 different routes for each action. You need to tweak each route a little bit and add them to the same class structure that MapMvcAttributeRoutes uses. Unfortunately, Microsoft decided to make the types internal so it requires Reflection to instantiate and populate them.

RouteCollectionExtensions

Here we just use the built in functionality of MVC to scan our project and create a set of routes, then insert an additional route URL prefix for the culture and the CultureConstraint before adding the instances to our MVC RouteTable.

There is also a separate route that is created for resolving the URLs (the same way that AttributeRouting does it).

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);
    }
}

Then it is just a matter of calling this method instead of 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 }
        );
    }
}
Determination answered 29/9, 2015 at 8:54 Comment(17)
Thank you! This does not only give the answer but also gives me a better insight in the internal workings. I've learned a few extra thinks and using new classes like RouteConstraint! Wish every answer was so good!Dore
Works like a charm @NightOwl888! I've only changed the constraint to: $"^{pattern}$" (.net 4.6).Dore
one question: is it possible that it doesn't work so good with "routes.MapMvcAttributeRoutes();" ?Dore
You just need to ensure you create both a default and localized route for each action, and you typically need to register routes.MapMvcAttributeRoutes() before mapping any MapRoute routes. In this case you could probably call it right after routes.LowercaseUrls = true;. You need to use IInlineConstraintResolver to add custom constraints as shown here. You should also use the Order property of the attribute routes to ensure they are registered in the correct order.Determination
what about routes.IgnoreRoute("downloads/*"); this doesn't work anymore as it adds /nl/downloads to the url and this is not found anymore.Dore
I am not sure what you are trying to achieve with IgnoreRoute, but check this: #30802.Determination
A wellwritten and thorough answer! Thank you.Nuts
Very well written. A very clever solution I have to admit. Congratz.. It helped me a lot. I just have one question - What is the best way to show the user the site on his preferred language without making him change the lang explicitly and should I do it / is is correct / For example if I enter somesite.com to redirect me automatically to somesite.com/en . The option for the explicit language change should remain alsoMarpet
@Marpet - Using the above solution, search engines will index the site in each language it supports. Many (if not most) users will locate the site through a search in their native language, so they won't need to change the language when they reach the site. All users who bookmark the site/store the URL will return in their own language. The only traffic that may see the site in a different language than their own are those who type somesite.com into the browser manually, which is probably a very small percentage of the total traffic on the site.Determination
@Marpet - If you also pick a reasonable default language (for the majority of the site's traffic), the number of users that need to manually change to their own language is negligible. Keep in mind some "power users" will know to add /en at the end of the URL. You could alternatively use a subdomain (en.somesite.com) rather than an alternate path for each language, which might make it more intuitive. But I wouldn't worry about those who have to manually change - just make it easy to identify the selection control by using flag icons instead of text and put it in a visible location.Determination
@Determination if you are using custom constraints using DefaultInlineConstraintResolver then the code pukes here in the RouteCollectionExtensions class var localizedRouteTable = new RouteCollection(); I am trying to fix it ... any insight you can give would be greatly appreciated. To be clear doing this: ` var constraints = new DefaultInlineConstraintResolver(); constraints.ConstraintMap.Add("CustomName", typeof(CustomConstraint)); routes.MapMvcAttributeRoutes(constraints);` prior to calling routes.MapLocalizedMvcAttributeRoutes(... causes an erMadalena
@Madalena - You should not be calling MapMvcAttributeRoutes - MapLocalizedMvcAttributeRoutes is intended to replace it. I didn't set it up to use IInlineConstraintResolver (this isn't something I have worked with), but if you go to the MVC source you can probably get some ideas how it is supposed to be used - you just need to ensure the additional constraint for culture is applied along with whatever constraints are resolved. Do note there is another way to extend attribute routing (that I didn't explore) that is used in MvcRouteLocalizationDetermination
@Determination .... you are a genius bro. Your comment helped me fix it. I didn't realize that you intended the MapLocalizedMvcAttributeRoutes as a replacement. When I moved my DefaultInlineConstraintResolver into the RouteCollectionExtensions.MapLocalizedMvcAttributeRoutes function ... voila ... perfection. Thanks for the tip :)Madalena
Both ActionLink and Url.Action use the UrlHelper class, which in turn uses routes to generate the URL. So, it really doesn't matter which of these 2 methods you call to generate a URL in MVC, it works the same way. For example: <a href='@Url.Action(@action, @controller, new { culture = "en" })' rel="alternate" hreflang="en">English</a> is equivalent to @Html.ActionLink("English", @action, @controller, new { culture = "en" }, new { rel = "alternate", hreflang = "en" }).Determination
@Determination This is one of the most cogent, thorough, well written answers I have EVER had the pleasure of reading on SO. It should be held as an example of what this site is all about. Congrats. I wish I could upvote a thousand more times.Rave
What happens if page has another route data expect controller,action,culture like following localhost:64831/de/Products/Detail/xxx ,remaning route values should be added to language action linkDishevel
Regarding your note: "because MVC automatically reuses route values from the current request". As of .NET Core 2.2, ambient route values aren't reused when the linked destination is a different action or page. Here's an issue discussing it and the possible (not ideal) work arounds: github.com/dotnet/aspnetcore/issues/16960Mcwherter
A
14

Default culture fix

Incredible post by NightOwl888. There is something missing though - the normal(not localized) URL-generation attribute routes, which get added through reflection, also need a default culture parameter, otherwise you get a query parameter in the URL.

?culture=nl

In order to avoid this, these changes must be made:

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);
        }
    }
}

And to attribute routes registration:

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

Better solution

And actually, after some time, I needed to add url translation, so i digged in more, and it appears there is no need to do the reflection hacking described. The ASP.NET guys thought about it, there is much cleaner solution - instead you can extend a DefaultDirectRouteProvider like this:

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);
    }
}

There is a solution based on this, including url translation here: https://github.com/boudinov/mvc-5-routing-localization

Alterable answered 2/6, 2016 at 11:51 Comment(1)
Your Route.Config example is missing , pattern: "[a-z]{2}")Marchant

© 2022 - 2024 — McMap. All rights reserved.