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