Adding REST links as a cross-cutting concern by Mark Seemann
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.
The code shown here is part of the sample code base that accompanies my book Code That Fits in Your Head.
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.
Comments
Thanks for your good and insightful posts.
Separation of REST concerns from MVC controller's concerns is a great idea, But in my opinion this solution has two problems:
Distance between related REST implementations #
When implementing REST by MVC pattern often REST archetypes are the reasons for a MVC Controller class to be created. As long as the MVC Controller class describes the archetype, And links of a resource is a part of the response when implementing hypermedia controls, having the archetype and its related links in the place where the resource described is a big advantage in easiness and readability of the design. pulling out link implementations and putting them in separate classes causes higher readability and uniformity of the code abstranction levels in the action method at the expense of making a distance between related REST implementation.
Implementation scalability #
There is a switch statement on the ActionExecutingContext's result in the LinksFilter to decide what links must be represented to the client in the response.The DTOs thre are the results of the clients's requests for URIs. If this solution generalised for every resources the API must represent there will be several cases for the switch statement to handle. Beside that every resources may have different implemetations for generating their links. Putting all this in one place leads the LinksFilter to be coupled with too many helper classes and this coupling process never stops.
Solution #
LinkDescriptor and LinkSubscriber for resources links definition
Letting the MVC controller classes have their resource's links definitions but not in action methods.
Registering resources links by convention
Add dependencies and execute procedures for adding links to responses
And Last letting the LinksFilter to dynamicaly add resources links by utilizing ExpandoObject
If absoulute URI in href field is prefered, IUriHelper can be injected in LinksFilter to create URI paths.
Mark, thanks for figuring out the tricky parts so we don't have to. :-)
I did not see a link to a repo with the completed code from this article, and a cursory look around your Github profile didn't give away any obvious clues. Is the example code in the article part of a repo we can clone? If so, could you please provide a link?
Jes, it's part of a larger project that I'm currently working on. Eventually, I hope to publish it, but it's not yet in a state where I wish to do that.
Did I leave out essential details that makes it hard to reproduce the idea?
No, your presentation was fine. Looking forward to see the completed project!
Mehdi, thank you for writing. It's true that the
LinksFilter
implementation code contains aswitch
statement, and that this is one of multiple possible designs. I do believe that this is a trade-off rather than a problem per se.That
switch
statement is an implementation detail of the filter, and something I might decide to change in the future. I did choose that implementation, though, because I it was the simplest design that came to my mind. As presented, theswitch
statement just calls some private helper methods (all calledAddLinks
), but if one wanted that code close to the rest of the Controller code, one could just move those helper methods to the relevant Controller classes.While you wouldn't need to resort to Reflection to do that, it's true that this would leave that
switch
statement as a central place where developers would have to go if they add a new resource. It's true that your proposed solution addresses that problem, but doesn't it just shift the burden somewhere else? Now, developers will have to know that they ought to add aRegisterLinks
method with a specific signature to their Controller classes. This replaces a design with compile-time checking with something that may fail at run time. How is that an improvement?I think that I understand your other point about the distance of code, but it assumes a particular notion of REST that I find parochial. Most (.NET) developers I've met design REST APIs in a code-centric (if not a database-centric) way. They tend to conflate representations with resources and translate both to Controller classes.
The idea behind Representational State Transfer, however, is to decouple state from representation. Resources have state, but can have multiple representations. Vice versa, many resources may share the same representation. In the code base I used for this article, not only do I have three overloaded
Get
methods onCalendarController
that produceCalendarDto
representations, I also have aScheduleController
class that does the same.Granted, not all REST API code bases are designed like this. I admit that what actually motivated me to do things like this was to avoid having to inherit from
ControllerBase
. Moving all the code that relies heavily on the ASP.NET infrastructure keeps the Controller classes lighter, and thus easier to test. I should probably write an article about that...