UrlHelper.Action includes undesired additional parameters
Asked Answered
P

4

12

I have a method in the controller ApplicationsController, in which I need to get the base URL for an action method:

public ActionResult MyAction(string id)
{
    var url = Url.Action("MyAction", "Applications");
    ...
}

The problem is that this includes the string id from the current route data, when I need the URL without (the URL is used to fetch content from a CMS on a URL-based lookup).

I have tried passing null and new { } as the routeValues parameter to no avail.

The matching route is as follows (above all other routes):

routes.MapLowercaseRoute(
    name: "Applications",
    url: "applications/{action}/{id}",
    defaults: new { controller = "Applications",
                    action = "Index", id = UrlParameter.Optional });

I've seen a couple of other questions touch on this but none of them seem to have a viable solution. At present, I am resorting to hardcoding the path in the controller; however, I'd like to be able to abstract this into an action filter, so I need to be able to generate the URL.

Is there a clean/conventional way to prevent this behaviour?

Parabolize answered 3/12, 2013 at 11:18 Comment(2)
Are you sure it's being resolved by this route ? (if it's the only one).Pecksniffian
@ThomasJaskula - Positive - I have a catchall route directly beneath it that redirects directly to static CMS content. Commenting the Applications route out causes that second route to catch it instead. Tested that as a first port of call and this is the only route that resolves against that URL at all.Parabolize
P
7

Ended up getting around this with a different approach. The only way I could come up with to prevent arbitrarily-named route values from being inserted into the generated URL was to temporarily remove them from RouteData when calling Url.Action. I've written a couple of extension methods to facilitate this:

public static string NonContextualAction(this UrlHelper helper, string action)
{
    return helper.NonContextualAction(action,
        helper.RequestContext.RouteData.Values["controller"].ToString());
}

public static string NonContextualAction(this UrlHelper helper, string action,
                                         string controller)
{
    var routeValues = helper.RequestContext.RouteData.Values;
    var routeValueKeys = routeValues.Keys.Where(o => o != "controller"
                         && o != "action").ToList();

    // Temporarily remove routevalues
    var oldRouteValues = new Dictionary<string, object>();
    foreach (var key in routeValueKeys)
    {
        oldRouteValues[key] = routeValues[key];
        routeValues.Remove(key);
    }

    // Generate URL
    string url = helper.Action(routeValues["Action"].ToString(),
                               routeValues["Controller"].ToString());

    // Reinsert routevalues
    foreach (var kvp in oldRouteValues)
    {
        routeValues.Add(kvp.Key, kvp.Value);
    }

    return url;
}

This allows me to do this in an action filter where I won't necessarily know what the parameter names for the action are (and therefore can't just pass an anonymous object as in the other answers).

Still very much interested to know if someone has a more elegant solution, however.

Parabolize answered 5/12, 2013 at 10:50 Comment(2)
Very good approach. In fact the Url class itself should have an alternative to those behaviors (like an optional bool parameter on the Action method).Moitoso
@MarceloMyara That was my thought too - in fact, I'm quite surprised this hasn't been considered.Parabolize
F
14

Ok, I see the problem. It's something called "Segment variable reuse". When generating the routes for outbound URLs, and trying to find values for each of the segment variables in a route’s URL pattern, the routing system will look at the values from the current request. This is a behavior that confuses many programmers and can lead to a lengthy debugging session. The routing system is keen to make a match against a route, to the extent that it will reuse segment variable values from the incoming URL. So I think you have to override the value like Julien suggested :

var url = Url.Action("MyAction", "Applications", new { id = "" })
Forlorn answered 3/12, 2013 at 13:59 Comment(3)
Interesting. Will give this a whirl, but is there any way to force this to happen without explicitly specifying the individual parameter names? I'd like to abstract this into an action filter where the URL segment will not necessarily always be called idParabolize
I don't think there is an easy way to do it as it's baked to the MVC Framework. It would require to customize the routing system. This behaviour is always that suprise me a lot and that's why I think the routing is the worst part of MVC. This should not happen very often and I'm dealing with it case by case.Pecksniffian
I'm getting this behavior with UrlHelper.Link and this does not solve the issuePistachio
P
7

Ended up getting around this with a different approach. The only way I could come up with to prevent arbitrarily-named route values from being inserted into the generated URL was to temporarily remove them from RouteData when calling Url.Action. I've written a couple of extension methods to facilitate this:

public static string NonContextualAction(this UrlHelper helper, string action)
{
    return helper.NonContextualAction(action,
        helper.RequestContext.RouteData.Values["controller"].ToString());
}

public static string NonContextualAction(this UrlHelper helper, string action,
                                         string controller)
{
    var routeValues = helper.RequestContext.RouteData.Values;
    var routeValueKeys = routeValues.Keys.Where(o => o != "controller"
                         && o != "action").ToList();

    // Temporarily remove routevalues
    var oldRouteValues = new Dictionary<string, object>();
    foreach (var key in routeValueKeys)
    {
        oldRouteValues[key] = routeValues[key];
        routeValues.Remove(key);
    }

    // Generate URL
    string url = helper.Action(routeValues["Action"].ToString(),
                               routeValues["Controller"].ToString());

    // Reinsert routevalues
    foreach (var kvp in oldRouteValues)
    {
        routeValues.Add(kvp.Key, kvp.Value);
    }

    return url;
}

This allows me to do this in an action filter where I won't necessarily know what the parameter names for the action are (and therefore can't just pass an anonymous object as in the other answers).

Still very much interested to know if someone has a more elegant solution, however.

Parabolize answered 5/12, 2013 at 10:50 Comment(2)
Very good approach. In fact the Url class itself should have an alternative to those behaviors (like an optional bool parameter on the Action method).Moitoso
@MarceloMyara That was my thought too - in fact, I'm quite surprised this hasn't been considered.Parabolize
A
6

Use a null or empty value for id to prevent Url.Action from using the current one:

var url = Url.Action("MyAction", "Applications", new { id = "" })
Adrenalin answered 3/12, 2013 at 11:57 Comment(0)
L
0

I was not entirely comfortable with the altering, transient or otherwise, of the RouterData in @AntP's otherwise fine solution. Since my code for creating the links was already centralized, I borrowed @Tomasz Jaskuλa and @AntP to augment the ExpandoObject, I was already using.

IDictionary<string,object> p = new ExpandoObject();

// Add the values I want in the route
foreach (var (key, value) in linkAttribute.ParamMap)
{
    var v = GetPropertyValue(origin, value);                    
    p.Add(key, v); 
}

// Ideas borrowed from https://mcmap.net/q/900752/-urlhelper-action-includes-undesired-additional-parameters
// Null out values that I don't want, but are already in the RouteData
foreach (var key in _urlHelper.ActionContext.RouteData.Values.Keys)
{
    if (p.ContainsKey(key))
        continue;

    p.Add(key, null); 
}

var href = _urlHelper.Action("Get", linkAttribute.HRefControllerName, p);
Loar answered 3/8, 2018 at 13:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.