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.
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.