3

Adding REST links as a cross-cutting concern

 3 years ago
source link: https://blog.ploeh.dk/2020/08/24/adding-rest-links-as-a-cross-cutting-concern/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Use a piece of middleware to enrich a Data Transfer Object. An ASP.NET Core example.

When developing true REST APIs, you should use hypermedia controls (i.e. links) to guide clients to the resources they need. I've always felt that the code that generates these links tends to make otherwise readable Controller methods unreadable.

I'm currently experimenting with generating links as a cross-cutting concern. So far, I like it very much.

Links from home #

Consider an online restaurant reservation system. When you make a GET request against the home resource (which is the only published URL for the API), you should receive a representation like this:

{
  "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/8"
    },
    {
      "rel": "urn:day",
      "href": "http://localhost:53568/calendar/2020/8/13"
    }
  ]
}

As you can tell, my example just runs on my local development machine, but I'm sure that you can see past that. There's three calendar links that clients can use to GET the restaurant's calendar for the current day, month, or year. Clients can use these resources to present a user with a date picker or a similar user interface so that it's possible to pick a date for a reservation.

When a client wants to make a reservation, it can use the URL identified by the rel (relationship type) "urn:reservations" to make a POST request.

Link generation as a Controller responsibility #

I first wrote the code that generates these links directly in the Controller class that serves the home resource. It looked like this:

public IActionResult Get()
{
    var links = new List<LinkDto>();
    links.Add(Url.LinkToReservations());
    if (enableCalendar)
    {
        var now = DateTime.Now;
        links.Add(Url.LinkToYear(now.Year));
        links.Add(Url.LinkToMonth(now.Year, now.Month));
        links.Add(Url.LinkToDay(now.Year, now.Month, now.Day));
    }
    return Ok(new HomeDto { Links = links.ToArray() });
}

That doesn't look too bad, but 90% of the code is exclusively concerned with generating links. (enableCalendar, by the way, is a feature flag.) That seems acceptable in this special case, because there's really nothing else the home resource has to do. For other resources, the Controller code might contain some composition code as well, and then all the link code starts to look like noise that makes it harder to understand the actual purpose of the Controller method. You'll see an example of a non-trivial Controller method later in this article.

It seemed to me that enriching a Data Transfer Object (DTO) with links ought to be a cross-cutting concern.

LinksFilter #

In ASP.NET Core, you can implement cross-cutting concerns with a type of middleware called IAsyncActionFilter. I added one called LinksFilter:

internal class LinksFilter : IAsyncActionFilter
{
    private readonly bool enableCalendar;
 
    public IUrlHelperFactory UrlHelperFactory { get; }
 
    public LinksFilter(
        IUrlHelperFactory urlHelperFactory,
        CalendarFlag calendarFlag)
    {
        UrlHelperFactory = urlHelperFactory;
        enableCalendar = calendarFlag.Enabled;
    }
 
    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        var ctxAfter = await next().ConfigureAwait(false);
        if (!(ctxAfter.Result is OkObjectResult ok))
            return;
 
        var url = UrlHelperFactory.GetUrlHelper(ctxAfter);
        switch (ok.Value)
        {
            case HomeDto homeDto:
                AddLinks(homeDto, url);
                break;
            case CalendarDto calendarDto:
                AddLinks(calendarDto, url);
                break;
            default:
                break;
        }
    }
 
    // ...

There's only one method to implement. If you want to run some code after the Controllers have had their chance, you invoke the next delegate to get the resulting context. It should contain the response to be returned. If Result isn't an OkObjectResult there's no content to enrich with links, so the method just returns.

Otherwise, it switches on the type of the ok.Value and passes the DTO to an appropriate helper method. Here's the AddLinks overload for HomeDto:

private void AddLinks(HomeDto dto, IUrlHelper url)
{
    if (enableCalendar)
    {
        var now = DateTime.Now;
        dto.Links = new[]
        {
            url.LinkToReservations(),
            url.LinkToYear(now.Year),
            url.LinkToMonth(now.Year, now.Month),
            url.LinkToDay(now.Year, now.Month, now.Day)
        };
    }
    else
    {
        dto.Links = new[] { url.LinkToReservations() };
    }
}

You can probably recognise the implemented behaviour from before, where it was implemented in the Get method. That method now looks like this:

public ActionResult Get()
{
    return new OkObjectResult(new HomeDto());
}

That's clearly much simpler, but you probably think that little has been achieved. After all, doesn't this just move some code from one place to another?

Yes, that's the case in this particular example, but I wanted to start with an example that was so simple that it highlights how to move the code to a filter. Consider, then, the following example.

A calendar resource #

The online reservation system enables clients to navigate its calendar to look up dates and time slots. A representation might look like this:

{
  "links": [
    {
      "rel": "previous",
      "href": "http://localhost:53568/calendar/2020/8/12"
    },
    {
      "rel": "next",
      "href": "http://localhost:53568/calendar/2020/8/14"
    }
  ],
  "year": 2020,
  "month": 8,
  "day": 13,
  "days": [
    {
      "links": [
        {
          "rel": "urn:year",
          "href": "http://localhost:53568/calendar/2020"
        },
        {
          "rel": "urn:month",
          "href": "http://localhost:53568/calendar/2020/8"
        },
        {
          "rel": "urn:day",
          "href": "http://localhost:53568/calendar/2020/8/13"
        }
      ],
      "date": "2020-08-13",
      "entries": [
        {
          "time": "18:00:00",
          "maximumPartySize": 10
        },
        {
          "time": "18:15:00",
          "maximumPartySize": 10
        },
        {
          "time": "18:30:00",
          "maximumPartySize": 10
        },
        {
          "time": "18:45:00",
          "maximumPartySize": 10
        },
        {
          "time": "19:00:00",
          "maximumPartySize": 10
        },
        {
          "time": "19:15:00",
          "maximumPartySize": 10
        },
        {
          "time": "19:30:00",
          "maximumPartySize": 10
        },
        {
          "time": "19:45:00",
          "maximumPartySize": 10
        },
        {
          "time": "20:00:00",
          "maximumPartySize": 10
        },
        {
          "time": "20:15:00",
          "maximumPartySize": 10
        },
        {
          "time": "20:30:00",
          "maximumPartySize": 10
        },
        {
          "time": "20:45:00",
          "maximumPartySize": 10
        },
        {
          "time": "21:00:00",
          "maximumPartySize": 10
        }
      ]
    }
  ]
}

This is a JSON representation of the calendar for August 13, 2020. The data it contains is the identification of the date, as well as a series of entries that lists the largest reservation the restaurant can accept for each time slot.

Apart from the data, the representation also contains links. There's a general collection of links that currently holds only next and previous. In addition to that, each day has its own array of links. In the above example, only a single day is represented, so the days array contains only a single object. For a month calendar (navigatable via the urn:month link), there'd be between 28 and 31 days, each with its own links array.

Generating all these links is a complex undertaking all by itself, so separation of concerns is a boon.

Calendar links #

As you can see in the above LinksFilter, it branches on the type of value wrapped in an OkObjectResult. If the type is CalendarDto, it calls the appropriate AddLinks overload:

private static void AddLinks(CalendarDto dto, IUrlHelper url)
{
    var period = dto.ToPeriod();
    var previous = period.Accept(new PreviousPeriodVisitor());
    var next = period.Accept(new NextPeriodVisitor());
 
    dto.Links = new[]
    {
        url.LinkToPeriod(previous, "previous"),
        url.LinkToPeriod(next, "next")
    };
 
    if (dto.Days is { })
        foreach (var day in dto.Days)
            AddLinks(day, url);
}

It both generates the previous and next links on the dto, as well as the links for each day. While I'm not going to bore you with more of that code, you can tell, I hope, that the AddLinks method calls other helper methods and classes. The point is that link generation involves more than just a few lines of code.

You already saw that in the first example (related to HomeDto). The question is whether there's still some significant code left in the Controller class?

Calendar resource #

The CalendarController class defines three overloads of Get - one for a single day, one for a month, and one for an entire year. Each of them looks like this:

public async Task<ActionResult> Get(int year, int month)
{
    var period = Period.Month(year, month);
    var days = await MakeDays(period).ConfigureAwait(false);
    return new OkObjectResult(
        new CalendarDto
        {
            Year = year,
            Month = month,
            Days = days
        });
}

It doesn't look as though much is going on, but at least you can see that it returns a CalendarDto object.

While the method looks simple, it's not. Significant work happens in the MakeDays helper method:

private async Task<DayDto[]> MakeDays(IPeriod period)
{
    var firstTick = period.Accept(new FirstTickVisitor());
    var lastTick = period.Accept(new LastTickVisitor());
    var reservations = await Repository
        .ReadReservations(firstTick, lastTick).ConfigureAwait(false);
 
    var days = period.Accept(new DaysVisitor())
        .Select(d => MakeDay(d, reservations))
        .ToArray();
    return days;
}

After having read relevant reservations from the database, it applies complex business logic to allocate them and thereby being able to report on remaining capacity for each time slot.

Not having to worry about link generation while doing all that work seems like a benefit.

Filter registration #

You must tell the ASP.NET Core framework about any filters that you add. You can do that in the Startup class' ConfigureServices method:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(opts => opts.Filters.Add<LinksFilter>());
 
    // ...

When registered, the filter executes for each HTTP request. When the object represents a 200 OK result, the filter populates the DTOs with links.

Conclusion #

By treating RESTful link generation as a cross-cutting concern, you can separate if from the logic of generating the data structure that represents the resource. That's not the only way to do it. You could also write a simple function that populates DTOs, and call it directly from each Controller action.

What I like about using a filter is that I don't have to remember to do that. Once the filter is registered, it'll populate all the DTOs it knows about, regardless of which Controller generated them.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK