ASP.NET MVC Url.Action adds current route values to generated url
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.
Solution 1:
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
Solution 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;
}
Solution 3:
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+" }
);
Solution 4:
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();
...
}