56

I have seen this question a couple of times here in SO but none of them with any acceptable answer:

ASP.NET MVC @Url.Action includes current route data
ASP.NET MVC implicitly adds route values

Basically I have Controller with an action method called Group, it has an overload that receives no parameters and displays a list of elements and another one that receives an id and displays details for that group.

If I do something like this:

Url.Action("Group", "Groups");

From the main page of the site (/) it returns an url like this:

"mysite.com/Groups/Group"

which is alright Now, if the current address of the site is /Groups/Group/1 And I call the same method

Url.Action("Group", "Groups");

the returned url is this:

"mysite.com/Groups/Group/1"

It automatically adds the value of the route for the current page when generating the URL. Even if I generate the URL this way:

Url.Action("Group", "Groups", null);

Thus explicitly specifying that I don't want any route values, the generated URL is the same. To get the address I want I have to explicitly set the route value to an empty string, like so:

Url.Action("Group", "Groups", new {id=""});

This will generate the following url:

"mysite.com/Groups/Group"

My question is, why does this happen? If I don't set any route values it shouldn't add them to the generated URL.

Community
  • 1
  • 1
willvv
  • 8,439
  • 16
  • 66
  • 101
  • 1
    i have yet to see a decent explanation of why they did it like this, or any example of its use. the only time this crops up is in surprise when people first realize that their code is unexpectedly broken by this behavior. very bad design IMHO. – Spongman Nov 25 '14 at 21:03

7 Answers7

53

Url.Action will reuse the current request parameters, if you do not explicitly set them. It is by design in outbound url-matching algorithm. When looking for the route data parameters in a process of generating url, parameters are taken from:

1) explicitly provided values

2) values from the current request

3) defaults

In the order I specified above.

Outbound matching algorithm for routes is complicated, so it is good practice to explicitly set all parameters for request, as you did in your example

objectbox
  • 1,281
  • 1
  • 11
  • 13
  • 1
    And it's a good thing that it does so. Otherwise we'd always have to provide route values which would be far more frequent than the opposite. – Robert Koritnik Aug 20 '11 at 20:37
  • 28
    For me this causes more problems than it solves. I wish there was an easy way to disable this. – Xavier Poinas Mar 20 '13 at 05:34
  • @Xavier Poinas: I have added an answer. hope it helps you too. – Valamas Apr 02 '13 at 01:52
  • 2
    Robert could you explain an scenario in which this default behavior is helpful? Url.Action is taking default parameters from the page which in my case are irrelevant to the generated URLs. – tmorell Apr 28 '13 at 14:28
  • 1
    It also interferes when the client-side needs to provide some of the values by concatenation – Jason Kleban May 16 '14 at 19:30
  • So Url.RouteUrl doesn't appear to do this, at least in my limited testing. Navigating to a page with a parameter and generating a route URL to another route that reuses the same parameter does not generate a link passing the parameter through. – kamranicus May 31 '15 at 09:22
2

My application explicitly sets route values and does not want a magical value from the current request. I want to be in full control.

I have made an extension which coexists with my route library collection. Hence the single RouteValueDictionary param. (See my Route library comment at the bottom)

Here I remove any routevalues from the request prior to generating a url.

(note: for the array.contains ignorecase part, see: How can I make Array.Contains case-insensitive on a string array?)

public static string Action(this UrlHelper helper, 
                            RouteValueDictionary routeValues)
{
    RemoveRoutes(helper.RequestContext.RouteData.Values);

    string url = helper.Action(routeValues["Action"].ToString(), routeValues);
    return url;
}

public static void RemoveRoutes(RouteValueDictionary currentRouteData)
{
    List<string> keyList = new List<string>(currentRouteData.Keys);

    string[] ignore = new[] { "Area", "Controller", "Action" };
    foreach (string key in keyList)
    {
        if (!ignore.Contains(key, StringComparer.CurrentCultureIgnoreCase))
            currentRouteData.Remove(key);
    }
}

I have Form and ActionLink extension methods that uses the RemoveRoutes method. No helper in my mvc library uses a method that is not an extension method i have created. Thereby, all routedata is cleaned up before generating urls.

For reference I use AttributeRouting. Here is an example of one route from my route library.

public static RouteValueDictionary DisplayNews(int newsId)
{
    RouteValueDictionary route = new RouteValueDictionary();
    route["Area"] = _area;
    route["Controller"] = _controller;
    route["Action"] = "DisplayNews";
    route["newsId"] = newsId;
    return route;
}
Community
  • 1
  • 1
Valamas
  • 24,169
  • 25
  • 107
  • 177
  • I like this solution best. however it would make sense to add this to the ignorelist: "MS_DirectRouteMatches" used for attribute based routing information – Dbl Apr 15 '15 at 15:04
  • 3
    I don't think this is a very good solution. It seems that it removes the values from `RequestContext` completely once `Url.Action` is called. There might be some other code that still wants to use those values. I don't think that calling `Url.Action` should modify anything in the request, that seems like a nasty side-effect. – Tom Pažourek May 15 '17 at 08:59
2

So when I read objectbox's answer I thought I would have to modify several links in my code. I then tried adding a default route omitting the default parameters which solved the problem:

routes.MapRoute(
    "ArtistArtworkDefPage",
    "Artist/{username}/Artwork",
    new
    {
        controller = "Artist",
        action = "Artwork",
        page = 1
    }
);

routes.MapRoute(
    "ArtistArtwork",
    "Artist/{username}/Artwork/{page}",
    new
    {
        controller = "Artist",
        action = "Artwork",
        page = 1
    },
    new { page = @"\d+" }
);
tmorell
  • 549
  • 5
  • 14
  • Specifically (1) go to a page, (2) go to the page with id=fred, (3) go to the page again, the url has id=fred but shouldn't – tony Apr 17 '15 at 10:32
2

Simple example:

public class ProductController : Controller
{
  public ActionResult Edit(int id)
  {
    return View();
  }

  [Route("Product/Detail/{id:int}")]
  public ActionResult Detail(int id)
  {
    return View();
  }
}

Edit view contains only this:

@{ Layout = null;}
@Url.Action("Detail", "Cmr")

So when you run your site e.g. localhost:randomPort/Product/Edit/123 you get next response: /Product/Detail/123

Why? Because Route attribute has required parameter id. Id parameter is read from url, although we wrote only Url.Action(methodName, controller) - without specifying parameter. Also it doesn't make sense to have a method detail without id.

In order for attributes to work next line must be added to RouteConfig.cs:

public static void RegisterRoutes(RouteCollection routes)
{
  ...
  routes.MapMvcAttributeRoutes();
  ...
}
broadband
  • 3,266
  • 6
  • 43
  • 73
1

A silly workaround I found that works to handle the current page routing data, including changing the page parameter upon your preferences

@{
    var current_route_1 = new RouteValueDictionary(Url.RequestContext.RouteData.Values);
    var current_route_2 = new RouteValueDictionary(Url.RequestContext.RouteData.Values);

    //If you want to customize the routing values
    current_route_1["controller"] = "Controller1";
    current_route_2["controller"] = "Controller2";
}

@Url.RouteUrl(current_route_1);
@Url.RouteUrl(current_route_2);
  • this work like a bomb just now trying to figure out how you would limit the forward path-ing and then redirect, if no access.. without having to do this on very permutation but i dont think there is a way. but thanks for the above. – Seabizkit Nov 26 '19 at 14:32
1

Here's a workaround that doesn't require attribute routing, nor does it alter the current route values within your request. This idea comes from http://notherdev.blogspot.ca/2013/10/aspnet-mvc-current-values-used-by-url-action.html, which I altered slightly to work without MvcFutures.

I built my own Action extension on UrlHelper with an additional bool that allows you to optionally ignore the current route values (as to not clash with the existing Action methods on UrlHelper.)

What it does is builds a completely new RequestContext from the current one, by using the current route but not the current route values. Then we pass that along to the base helpers. That way, when the base helpers go to look at what the route values are from within the request context, it'll find none, and thus not use them when generating the URL.

public static string Action(this UrlHelper urlHelper, string actionName, string controllerName, object routeValues, bool ignoreCurrentRouteValues=false) {
    var routeValueDictionary = new RouteValueDictionary(routeValues);
    var requestContext = urlHelper.RequestContext;
    if (ignoreCurrentRouteValues) {
        var currentRouteData = requestContext.RouteData;
        var newRouteData = new RouteData(currentRouteData.Route, currentRouteData.RouteHandler);
        requestContext = new RequestContext(requestContext.HttpContext, newRouteData);
    }

    return UrlHelper.GenerateUrl(null, actionName, controllerName, routeValueDictionary,
        urlHelper.RouteCollection, requestContext, includeImplicitMvcValues: false);
}
Ber'Zophus
  • 7,497
  • 3
  • 22
  • 22
0

A simple workaround would be first to call

Url.Action("dummy", new { ... }) 

and then rename dummy in the resulting string to the correct action name.

Martin Staufcik
  • 8,295
  • 4
  • 44
  • 63