Question

I need to add the culture to the url to support localization in my asp.net mvc application with url's like: sample.com/en/about
sample.com/en/product/2342

I recently upgraded my app from MVC 5.0 to 5.1 but the routing did not work as expected so I created a fresh asp.net mvc 5.0 test application and got the culture to show up in the url in a matter of minutes. However as soon as I upgrade this test application to MVC 5.1 the culture is no longer generated in links and if you manually type it into the url you get a 404 error.

I zipped up my 5.0 and 5.1 test applications here. I need help understanding why this doesn't work in MVC 5.1 and how to correct it. Perhaps my understanding of routing is flawed or this is a legitimate bug with 5.1?

In this test application the Home/About action has a routing attribute applied to it [Route("about")] and it's expected that when the link for that route is generated it should be localhost/en/about but instead it's just localhost/about. If you type localhost/en/about into the address bar you'll get a 404 error in the Mvc 5.1 test application.

Here is the relevant code that does work in MVC 5.0:

public class RouteConfig
{
    private const string STR_Culture = "culture";
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        routes.LowercaseUrls = true;
        routes.MapMvcAttributeRoutes();
        routes.MapRoute(
            name: "Default",
            url: "{culture}/{controller}/{action}/{id}",
            defaults: new { culture = "en", controller = "Home", action = "Index", id = UrlParameter.Optional }
        );

        foreach (var item in routes)
        {
            // this works in MVC 5.0
            if (item is Route)
            {
                var route = item as Route;
                if (route.Url.IndexOf("{" + STR_Culture + "}") == -1)
                    route.Url = String.Format("{{{0}}}/{1}", STR_Culture, route.Url);

                //AddCulture(route.Defaults);
            }
        }
    }
    private static void AddCulture(RouteValueDictionary dictionary)
    {
        if (dictionary == null)
            dictionary = new RouteValueDictionary();

        if (dictionary.ContainsKey(STR_Culture) == false)
            dictionary.Add(STR_Culture, "en");
    }
}
Was it helpful?

Solution

Ok, figured this out. MVC 5.1 has introduced breaking changes. In the code above there is a foreach loop that dynamically changes all routing urls to append the "{culture}/" placeholder. e.g the route about becomes {culture}/about and so on.

This works in 5.0 because routes are of type System.Web.Routing.Route. In 5.1 they have introduced a bunch of additional classes. One of which is called LinkGenerationRoute that is used for all routes applied through attribute routing. This class holds on to a private readonly reference of the original Route that was made during the initial call to routes.MapMvcAttributeRoutes(); that registers attribute based routes. Then this class clones that Route by sending its individual properties to the base class that it inherits from: Route.

In the foreach loop I'm effectively modifying the base classe's Url but NOT the internally referenced Route object that LinkGenerationRoute is holding on to. The effect is that there are now two instances of the Route inside the framework and we only have the ability to modify the base one after its created. Unfortunately the internal Route (_innerRoute) is used for getting the virtual path thus causing links to be generated incorrectly because it cannot be modified after its created.

Looks like the only way is to manually add this placeholder in every route definition. e.g. [Route("{culture}/about")], [Route("{culture}/contact")], [Route("{culture}/product/{productId:int}")] and so on.

At the end of the day I see no point to holding an internal reference to the Route in this class. The current instance should be used. e.g. this.GetVirtualPath(requestContext, values);

internal class LinkGenerationRoute : Route
{
    private readonly Route _innerRoute; // original route cannot be modified

    public LinkGenerationRoute(Route innerRoute)
        : base(innerRoute.Url, innerRoute.Defaults, innerRoute.Constraints, innerRoute.DataTokens,
        innerRoute.RouteHandler) // original route is effectively cloned by sending individual properties to base class
    {
        if (innerRoute == null)
        {
            throw Error.ArgumentNull("innerRoute");
        }

        _innerRoute = innerRoute;
    }

    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        // Claims no routes
        return null;
    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        // internal route is used for getting the virtual path. fail..
        return _innerRoute.GetVirtualPath(requestContext, values);
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top