Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Url.Action returning empty string in some environments

I have a Sitecore site and on 2 of my CD servers, Url.Action returns an empty string. This works locally and on 9 other servers ranging from dev to prod, CD and CM.

Deployment automation ensures that the exact same web.config is deployed to all environments; ditto for all other configs.

My controller inherits from SitecoreController properly. This is not isolated to a certain controller or action, this happens with all controllers and actions.

What would make Url.Action return an empty string in one environment and not others, with identical code?

like image 854
TheSoftwareJedi Avatar asked Jan 30 '18 18:01

TheSoftwareJedi


1 Answers

What would make Url.Action return an empty string sometimes?

Specifically, route values that are derived from the current request.

Explanation

The Url.Action method is driven by the UrlHelper, which in turn is driven by routes. It uses route values to determine which route to use to build the URL. The routing framework attempts to match each route against the route values in the order they are registered until a match is found. If the routing framework reaches the end of the routing table and there is still no match, it returns an empty string (because there is no other reasonable default behavior).

On the other hand, if you call Url.Action and pass a route name, this narrows the possible matches to only 1 specific route (the named one). But the route values still need to match that route or you get the default empty string.

In general, all route values must match, but there are a couple of things that may make the behavior quirky:

  1. Route values can be made optional. This means that the route value doesn't need to be present in order for the route to match.
  2. If a route value is not supplied in the call to Url.Action, it may be supplied automatically if it exists in the current request.

This second quirk means that if Url.Action is put on a shared view and one request contains a route value to make it match a route, and another request doesn't contain that route value, in the latter case the URL may match another route or it may be an empty string.

Example

Say the routing configuration is setup like this:

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute(
            name: "AboutWith4RouteValues",
            url: "test/home/about/{foo}/{bar}",
            defaults: new { controller = "Home", action = "About" });

        routes.MapRoute(
            name: "ContactWith4RouteValues",
            url: "test/home/contact/{foo}/{bar}",
            defaults: new { controller = "Home", action = "Contact", bar = UrlParameter.Optional });

        routes.MapRoute(
            name: "Home",
            url: "",
            defaults: new { controller = "Home", action = "Index" }
        );
    }
}

Also, let's say there is a link on the _Layout.cshtml page:

<a href='@Url.Action("About", "Home")'>About</a>

If you go to the home page in the browser (/), the resulting link URL will be:

<a>About</a>

This is because foo and bar are not present in the route values of the request so it doesn't match any of the registered routes.

Actually, Url.Action returns an empty string, but Razor optimizes away the empty href='' attribute.

On the other hand, if you put this URL in the browser:

/test/home/contact/arg1/arg2

The link URL is generated as:

<a href='/test/home/about/arg1/arg2'>About</a>

This is because both {foo} (with value arg1) and {bar} (with value arg2) are available in the current request. Note that the incoming request matches the ContactWith4RouteValues route, but when the link URL is generated it uses the AboutWith4RouteValues route. Since both foo and bar are present in the request, they are carried over to the generation of the URL.

Now, if the URL in the browser is changed to:

/test/home/contact/arg1

It still matches the ContactWith4RouteValues route because the last value is optional. However, the URL that is generated is now:

<a>About</a>

This is because foo has a value in the request, but bar has no value, the Url.Action generation request does not match AboutWith4RouteValues because bar is a required value in order to make it match. And since it also doesn't match the Home route, we have reached the end of the route table and the only logical thing to return is empty string.

Workaround

The simplest workaround to avoid these quirks of the current request is to manually specify the route values when calling Url.Action or other UrlHelper based methods (such as ActionLink, RedirectToRoute, etc).

<a href='@Url.Action("About", "Home", new { foo = Model.Foo, bar = Model.Bar })'>About</a>

This ensures those values are always present when building the URL even if they don't happen to be part of the current request.

like image 104
NightOwl888 Avatar answered Sep 29 '22 20:09

NightOwl888