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

public class LinkDescriptor
{
    public string Rel { get; set; }
    public string Href { get; set; }
    public string Resource { get; set; }
}

Letting the MVC controller classes have their resource's links definitions but not in action methods.

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

public static void RegisterLinks(LinkSubscriber subscriber)
{
    subscriber
        .Add(
            resource: "/Home",
            rel: "urn:reservations",
            href: "/reservations")
        .Add(
            resource: "/Home",
            rel: "urn:year",
            href: "/calendar/2020")
        .Add(
            resource: "/Home",
            rel: "urn:month",
            href: "/calendar/2020/8")
        .Add(
            resource: "/Home",
            rel: "urn:day",
            href: "/calendar/2020/8/13");
}

Registering resources links by convention

public static class MvcBuilderExtensions
{
    public static IMvcBuilder AddRestLinks(this IMvcBuilder mvcBuilder)
    {
        var subscriber = new LinkSubscriber();

        var linkRegistrationMethods = GetLinkRegistrationMethods(mvcBuilder.Services);

        PopulateLinks(subscriber, linkRegistrationMethods);

        mvcBuilder.Services.AddSingleton<IEnumerable<LinkDescriptor>>(subscriber.LinkDescriptors);

        return mvcBuilder;
    }

    private static List<MethodInfo> GetLinkRegistrationMethods(IServiceCollection services)
    {
        return typeof(MvcBuilderExtensions).Assembly.ExportedTypes
            .Where(tp => typeof(ControllerBase).IsAssignableFrom(tp))
            .Select(tp => tp.GetMethod("RegisterLinks", new[] { typeof(LinkSubscriber) }))
            .Where(mi => mi != null)
            .ToList();
    }

    private static void PopulateLinks(LinkSubscriber subscriber, List<MethodInfo> linkRegistrationMethods)
    {
        foreach (var method in linkRegistrationMethods)
        {
            method.Invoke(null, new[] { subscriber });
        }
    }
}

Add dependencies and execute procedures for adding links to responses

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(conf => conf.Filters.Add<LinksFilter>())
            .AddRestLinks();
}

And Last letting the LinksFilter to dynamicaly add resources links by utilizing ExpandoObject

public class LinksFilter : IAsyncActionFilter
{
    private readonly IEnumerable<LinkDescriptor> links;

    public LinksFilter(IEnumerable<LinkDescriptor> links)
    {
        this.links = links;
    }

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var relatedLinks = links
            .Where(lk => context.HttpContext.Request.Path.Value.ToLower() == lk.Resource);

        if (relatedLinks.Any())
            await ManipulateResponseAsync(context, next, relatedLinks);
    }

    private async Task ManipulateResponseAsync(ActionExecutingContext context, ActionExecutionDelegate next, IEnumerable<LinkDescriptor> relatedLinks)
    {
        var ctxAfter = await next().ConfigureAwait(false);

        if (!(ctxAfter.Result is ObjectResult objRes))
            return;

        var expandoResult = new ExpandoObject();

        FillExpandoWithResultProperties(expandoResult, objRes.Value);

        FillExpandoWithLinks(expandoResult, relatedLinks);

        objRes.Value = expandoResult;
    }

    private void FillExpandoWithResultProperties(ExpandoObject resultExpando, object value)
    {
        var properties = value.GetType().GetProperties();

        foreach (var property in properties)
        {
            resultExpando.TryAdd(property.Name, property.GetValue(value));
        }
    }

    private void FillExpandoWithLinks(ExpandoObject resultExpando, IEnumerable<LinkDescriptor> relatedLinks)
    {
        var linksToAdd = relatedLinks.Select(lk => new { Rel = lk.Rel, Href = lk.Href });

        resultExpando.TryAdd("Links", linksToAdd);
    }
}

If absoulute URI in href field is prefered, IUriHelper can be injected in LinksFilter to create URI paths.

2020-08-24 23:39 UTC

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?

2020-08-27 07:45 UTC

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?

2020-08-27 08:07 UTC

No, your presentation was fine. Looking forward to see the completed project!

2020-08-27 08:57 UTC

Mehdi, thank you for writing. It's true that the LinksFilter implementation code contains a switch 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, the switch statement just calls some private helper methods (all called AddLinks), 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 a RegisterLinks 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 on CalendarController that produce CalendarDto representations, I also have a ScheduleController 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...

2020-09-01 07:56 UTC


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, 24 August 2020 06:47:00 UTC

Tags



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