Search code examples
asp.net-mvcroutesasp.net-mvc-routing

UrlHelper.RouteUrl( routeName, RouteValueDictionary ) returns null


I have an ASP.NET MVC 5 Web Application running on .NET 4.6.1.

I have an Area in my application called "Foo" and in my Area registration I add a named route like so:

RegisterArea(AreaRegistrationContext context) {

    context.MapRoute("Foo_Bar", "{tenantName}/foo/{fooId}/{fooRevision}", new { controller = "Foo", action = "Index" } );

}

From my Razor view code I want to generate a link using this route, so I do:

RouteValueDictionary rd = new RouteValueDictionary( this.Url.RequestContext.RouteData.Values );
rd["fooId"      ] = "123";
rd["fooRevision"] = "4";

this.Url.RouteUrl("Foo_Bar", rd );

However this returns null. No exception. No other side-effects.

The debugger shows that rd contains 5 named values: tenantName, controller, action, fooId, and fooRevision. I also added area during Debugging just to make sure, however it still returns null.

I verified that the route exists because this.Url.RouteCollection["Foo_Bar"] returns the System.Web.Routing.Route object corresponding to that route.

I tried .NET Source Stepping and I was able to step 1 level into UrlHelper, but stepping into the GenerateUrl function fails and Visual Studio 2015 U2 always steps over it.


Solution

  • I had to step-into the .NET Framework source code to figure this one out - first into System.Web.Mvc.dll to get into UrlHelper.RouteUrl which then called into System.Web.Routing.dll (which is now a stub for System.Web.dll in .NET 4.5).

    The answer lies in ParsedRoute::Bind ( http://referencesource.microsoft.com/#System.Web/Routing/ParsedRoute.cs )

    So the problem was in this line:

    RouteValueDictionary rd = new RouteValueDictionary( this.Url.RequestContext.RouteData.Values );
    

    This causes rd to copy (inherit) the values from the current request-context, I thought this was desirable. I thought that because the route Foo_Bar provides a default controller and action parameter values, that it would always use those in preference to whatever was in rd, but I was wrong.

    The rd passed into UrlHelper.RouteUrl contained values for controller, and action but they were for the current request, and so were different than the defaults.

    The definition of ParsedRoute::Bind is such that it aborts if a provided RouteValueDictionary provides a parameter value different to the defaults (see lines 133 through 149 in ParsedRoute.cs.

    I feel the framework should throw an exception in that case, rather than returning null - because this behavior doesn't seem to be well documented - especially as all of the search results say the problem is simply that the route doesn't exist or insufficient parameters were passed - in my case, the problem was that in a way too many parameters were being passed.