Routes created through Attributes Routing have null route data

Topics: ASP.NET MVC
Mar 21, 2014 at 11:20 PM
Edited Mar 21, 2014 at 11:21 PM
There is some sort of inconsistency between "normal" routes and routes created through Attribute Routing. I have created a specialized Route that allows for the routes to be localized and it works just fine with normal routes but as soon as I attempt to do the same for attribute routes it fails with 404 responses because the base.GetRouteData() method allways returns null.

This code allows for localized urls like this:
http://localhost:61673/Demarrer/Infos/ -French
http://localhost:61673/Thuis/Over/ - Dutch
http://localhost:61673/Home/About/ - English

How can I get my code to work with Attribute Routing? (MVC 5.1)
Why do attribute based routes return null RouteData?


Global.asax.cs
// attribute routes don't work they return 404 errors
routes.MapMvcAttributeRoutes();

// normal routes work just fine
routes.Add(new LocalizedRoute("{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "" }));

// this works too if I remove the attribute route from the controller
routes.Add(new LocalizedRoute("Account/Login/{id}", new { controller = "Account", action = "Login", id = "" }));
Code:
public class LocalizedRoute : Route
{
    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        /*
         * This translates from the destination language back to the default language.
         * This executes even if the destination language is the same as the default language.
         * French           |  English
         * -----------------|-----------------
         * /produits/2342/  | /products/2342/
         */

        RouteData routeData = base.GetRouteData(httpContext);
        if (routeData == null) // returns null when attribute routing is used.
            return null;

        var keys = routeData.Values.Keys;

        for (int i = 0; i < keys.Count; i++)
        {
            var key = routeData.Values.Keys.ElementAt(i);
            var routeSegment = routeData.Values[key] as string;

            if (string.IsNullOrWhiteSpace(routeSegment))
                continue;

            var map = LocalizedRouteManager.Routes.GetMappingForRouteSegment(routeSegment);
            if (map == null)
                continue;

            string value = map.Items.Where(a => a.IsDefault == true).FirstOrDefault().TranslatedValue;
            routeData.Values[key] = value;
        }

        return routeData;
    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        /*
         * This does the translation from the default language to the destination language. This executes
         * even if the destination language is the same as the default language.
         */
        if (values.Keys.Contains("IsRenderAction") == false) // Prevent RenderAction() calls from exploding by skipping the conversion
        {
            var keys = values.Keys;
            for (int i = 0; i < keys.Count; i++)
            {
                var key = values.Keys.ElementAt(i);
                var routeSegment = values[key] as string;

                if (string.IsNullOrWhiteSpace(routeSegment))
                    continue;

                var map = LocalizedRouteManager.Routes.GetMappingForRouteSegment(routeSegment);
                if (map == null)
                    continue;

                string currentCultureName = CultureInfo.CurrentUICulture.Name;
                var localizedEntry = map.Items.Where(a => a.CultureName.Equals(currentCultureName, StringComparison.InvariantCultureIgnoreCase) ||
                    a.CultureName.Equals(currentCultureName.Substring(0, 2), StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();

                if (localizedEntry == null)
                    continue;

                values[key] = localizedEntry.TranslatedValue;
            }
        }
        VirtualPathData virtualPath = base.GetVirtualPath(requestContext, values);
        return virtualPath;
    }

    private static RouteValueDictionary CreateRouteValueDictionary(object values)
    {
        IDictionary<string, object> dictionary = values as IDictionary<string, object>;
        if (dictionary != null)
        {
            return new RouteValueDictionary(dictionary);
        }
        return new RouteValueDictionary(values);
    }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class LocalizedRouteAttribute : Attribute, IDirectRouteFactory, IRouteInfoProvider
{
    public LocalizedRouteAttribute() { }
    public LocalizedRouteAttribute(string template) { Template = template; }

    public string Name { get; set; }
    public string Template { get; private set; }
    public int Order { get; set; }

    public RouteEntry CreateRoute(DirectRouteFactoryContext context)
    {
        var builder = context.CreateBuilder(Template);

        builder.Name = Name;
        builder.Order = Order;
        var tempEntry = builder.Build();
        // convert the route into a LocalizedRoute
        var entry = new RouteEntry(Name, new LocalizedRoute(tempEntry.Route.Url, tempEntry.Route.RouteHandler)
        {
            Defaults = tempEntry.Route.Defaults,
            Constraints = tempEntry.Route.Constraints,
            DataTokens = tempEntry.Route.DataTokens,
            RouteExistingFiles = tempEntry.Route.RouteExistingFiles
        });

        return entry;
    }
}
Developer
Mar 22, 2014 at 1:16 AM
Are you sure that the following route works?...like did you try out without the "{controller}/{action}/{id}" route before the following one?
// this works too if I remove the attribute route from the controller
routes.Add(new LocalizedRoute("Account/Login/{id}", new { controller = "Account", action = "Login", id = "" }));
Overall my guess is that either for attribute routing or the above "Account/Login/{id}" route, the matching algorithm is trying to match 'literal' segments (ex: 'produits/index' is being tried to matched with 'products/index') and hence you are seeing null.

Where as in the case of "{controller}/{action}/{id}", controller and action are variables and they it gets matched.
Mar 24, 2014 at 12:43 AM
I did not get matched. After closer inspection none of the routes added through attribute routing were matching. The concept this code was original based on is flawed and will only ever match the default route that is tokenized. I've gone on to fix this in another way and solved this problem.