How to prevent Url.RouteUrl(...) from inheriting route values from the current request

Simon_Weaver picture Simon_Weaver · Jul 18, 2009 · Viewed 7.9k times · Source

Lets say you have an action method to display products in a shopping cart

 // ProductsController.cs
 public ActionMethod Index(string gender) {

      // get all products for the gender
 }

Elsewhere, in a masthead that is displayed on every page you are using Url.RouteUrl to create HREF links to other pages on the site :

 <a href="<%= Url.RouteUrl("testimonials-route", new { }) %>" All Testimonials </a>

This testimonials-route is defined in global.ascx by the first route below. Notice that the above call to RouteUrl does not provide a gender, but the route is defined with a default of 'neutral' so we'd expect Testimonials.Index("neutral") to be called.

 routes.MapRoute(
  "testimonials-route",
  "testimonials/{gender}",
  new { controller = "Testimonials", action = "Index", gender = "neutral" },
  new { gender = "(men|women|neutral)" }
 );

routes.MapRoute(
  "products-route",
  "products/{gender}",
  new { controller = "Products", action = "Index", gender = (string)null },
  new { gender = "(men|women|neutral)" }
 );

If someone visits the page /products/women we get an HREF to /testimonials/women If someone visits the page /products then we get an empty HREF (the call to RouteUrl returns null).

But that doesn't make sense does it? testimonials-route is supposed to default to 'neutral' for us if we don't provide a route value for it?

What turns out is happening is that Url.RouteUrl(routeName, routeValues) helper extension will first look in its routeValues parameter for a gender route value and if it doesn't find it in that dictionary it will look at the current URL that we're on (remember that Url is a UrlHelper object which has the context of the current request available to it).

This has a possibly nice effect of giving us a link to men's testimonials if we're on a mens product page, but that probably isnt what we want if we haven't passed a value in the RouteUrl call, and explicitly specified 'neutral' as a default in the global.asax.cs file.

In the case where we visited /products/ we triggered the 'products-route' route and the Products(null) method was called. The call to Url.RouteUrl() actually inherits THIS null value for gender when we're creating a URL using testimonials-route. Even though we have specified a default for gender in 'testimionials-route' it still uses this null value which causes the route to fail and RouteUrl returns null. [note: the route fails because we have a constraint on (men|women|neutral) and null doesn't fit that]

It actually gets more scary - in that 'controller' and 'action' can be inherited in the same way. This can lead to URLs being generated to completely the wrong controller even when calling RouteUrl(...) with an explicit route name that has a default controller.

In this case once you've figured it out you can fix it quite easily in numerous ways, but it could in other cases cause some dangerous behavior. This may be by design, but its definitely scary.

Answer

Simon_Weaver picture Simon_Weaver · Jul 18, 2009

My solution was this :

An HtmlExtension helper method :

    public static string RouteUrl(this UrlHelper urlHelper, string routeName, object routeValues, bool inheritRouteParams)
    {
        if (inheritRouteParams)
        {
            // call standard method
            return urlHelper.RouteUrl(routeName, routeValues);
        }
        else
        {
            // replace urlhelper with a new one that has no inherited route data
            urlHelper = new UrlHelper(new RequestContext(urlHelper.RequestContext.HttpContext, new RouteData()));
            return urlHelper.RouteUrl(routeName, routeValues);
        }
    }

I can now do :

Url.RouteUrl('testimonials-route', new { }, false)

and know for sure it will always behave the same way no matter what the context.

The way it works is to take the existing UrlHelper and create a new one with blank 'RouteData'. This means there is nothing to inherit from (even a null value).