How to generate links to other resources in a refactoring-friendly way.

I recently spent a couple of hours yak-shaving, and despite much Googling couldn't find any help on the internet. I'm surprised that the following problem turned out to be so difficult to figure out, so it may just be that I'm ignorant or that my web search skills failed me that day. On the other hand, if this really is as difficult as I found it, perhaps this article can save some other poor soul an hour or two.

I was developing a REST API and wanted to generate some links like these:

{
  "links": [
    {
      "rel": "urn:reservations",
      "href": "http://localhost:53568/reservations"
    },
    {
      "rel": "urn:year",
      "href": "http://localhost:53568/calendar/2020"
    },
    {
      "rel": "urn:month",
      "href": "http://localhost:53568/calendar/2020/7"
    },
    {
      "rel": "urn:day",
      "href": "http://localhost:53568/calendar/2020/7/7"
    }
  ]
}

Like previous incarnations of the framework, ASP.NET Core 3 has an API for generating links to a method on a Controller. I just couldn't get it to work.

Using nameof #

I wanted to generate a URL like http://localhost:53568/calendar/2020 in a refactoring-friendly way. While ASP.NET wants you to define HTTP resources as methods (actions) on Controllers, the various Action overloads want you to identify these actions and Controllers as strings. What happens if someone renames one of those methods or Controller classes?

That's what the C# nameof keyword is for.

I naively called the Action method like this:

var href = Url.Action(
    nameof(CalendarController.Get),
    nameof(CalendarController),
    new { year = DateTime.Now.Year },
    Url.ActionContext.HttpContext.Request.Scheme,
    Url.ActionContext.HttpContext.Request.Host.ToUriComponent());

Looks good, doesn't it?

I thought so, but it didn't work. In the time-honoured tradition of mainstream programming languages, the method just silently fails to return a value and instead returns null. That's not helpful. What might be the problem? No clue is provided. It just doesn't work.

Strip off the suffix #

It turns out that the Action method expects the controller argument to not contain the Controller suffix. Not surprisingly, nameof(CalendarController) becomes the string "CalendarController", which doesn't work.

It took me some time to figure out that I was supposed to pass a string like "Calendar". That works!

As a first pass at the problem, then, I changed my code to this:

var controllerName = nameof(CalendarController);
var controller = controllerName.Remove(
    controllerName.LastIndexOf(
        "Controller",
        StringComparison.Ordinal));
 
var href = Url.Action(
    nameof(CalendarController.Get),
    controller,
    new { year = DateTime.Now.Year },
    Url.ActionContext.HttpContext.Request.Scheme,
    Url.ActionContext.HttpContext.Request.Host.ToUriComponent());

That also works, and is more refactoring-friendly. You can rename both the Controller class and the method, and the link should still work.

Conclusion #

The UrlHelperExtensions.Action methods expect the controller to be the 'semantic' name of the Controller, if you will - not the actual class name. If you're calling it with values produced with the nameof keyword, you'll have to strip the Controller suffix away.



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 03 August 2020 10:01:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 03 August 2020 10:01:00 UTC