Get in touch
Thank you
We will get back to you as soon as possible

18.8.2021

12 min read

HATEOAS in ASP.NET Core

What is HATEOAS?

Almost every web developer knows what REST is (or at least heard about it), but much fewer of them know about HATEOAS, although it’s an essential part of the REST style.

HATEOAS stands for Hypermedia as the Engine of Application State. Formally, it’s a constraint of the REST application architecture, but in fact, it’s a structured format of responses that includes not only the requested data but also ways for working with it. It means that it’s enough for a client to know just some static URL, and everything else will be discovered in the process.

A REST-client that uses HATEOAS can be compared with a user of Wikipedia. The latter can start with an article about HATEOAS and learn about REST, without knowing what it is initially.

One important difference is that while consuming Wikipedia, users get read-only links that allow them only to read new articles. In the case of HATEOAS, the REST client is provided with links that are used additionally to change the state of the application. Thus, the endpoint that returns info on the current balance of the bank account also provides links to withdraw, transfer or deposit money. Moreover, it shouldn’t return links for the first two operations if the balance is negative or doesn’t meet the conditions. That’s why it’s the Engine of the Application State.

With this approach, the REST client still needs to know some URLs to start working with the application, and they can be treated as entry points. With a SPA-frontend, there may be a need for a bigger amount of such links, but it’s still much less than in the case of more common approaches.

HATEOAS in ASP.NET Core-1

HATEOAS is not a standard. It means that it can be implemented using different approaches. Still, the links cannot be in a random format. The REST client should always assume that all parts of the response are valid and have some basic structure. It means some standardized naming and JSON objects for storing them.

Currently, JSON Hypertext Application Language (hal+json) is one of the most common HATEOAS standards (used by Netflix, for example). For now, it’s an Internet-Draft. It was proposed in June 2012, and the latest version of the draft expired in 2016. There are also JSON-LD, Collection+JSON, and SIREN.

HATEOAS example in application/hal+json format:

{
  "_links": {
    "self": {
      "href": "/orders"
    },
    "next": {
      "href": "/orders?page=2"
    },
    "find": {
      "href": "/orders{?id}",
      "templated": true
    }
  },
  "_embedded": {
    "orders": [
      {
        "_links": {
          "self": {
            "href": "/orders/123"
          },
          "basket": {
            "href": "/baskets/98712"
          },
          "customer": {
            "href": "/customers/7809"
          }
        },
        "total": 30,
        "currency": "USD",
        "status": "shipped"
      },
      {
        "_links": {
          "self": {
            "href": "/orders/124"
          },
          "basket": {
            "href": "/baskets/97213"
          },
          "customer": {
            "href": "/customers/12369"
          }
        },
        "total": 20,
        "currency": "USD",
        "status": "processing"
      }
    ]
  },
  "currentlyProcessing": 14,
  "shippedToday": 20
}

Why use HATEOAS?

There are multiple reasons for going with HATEOAS:

Loose coupling

Usually, the REST client uses hard-coded URLs to get the resources it needs. HATEOAS solves it by providing those URLs. 

For example, after getting a collection of books, clients also can expect to have a link to retrieve details about each of them. Or there probably will be a link for making a POST request to create a new book. And after getting one book’s details, the client can expect to get a link for its editing or adding some nested entities. Of course, it all depends on the features of the application, but the main point is that the client doesn’t consume hard-coded URLs. It works with what was received.

The API becomes explorable and discoverable

HATEOAS also helps developers who are working with the frontend part of the application not to rely on some specifications or even directly communicate with API developers but just explore the possibilities of the API.

There are still some cases when one cannot work without some additional info, but even that can be included inside responses, providing a “place” where to look.

Less potential errors on the frontend side, that are related to invalid links

HATEOAS makes the REST client less error-prone since it doesn’t specify URLs manually. If the naming of the links doesn’t change, the client does not even care about their structure. What it needs is a base URL and the entry point (and the common sense of the API developers, who won’t change the names of the links or its set each sprint).

Security

The last advantage that is tightly connected to the previous one is security. A frontend application that uses HATEOAS links to traverse through the API doesn’t expose it to anyone. Thus potential intruders know only about an entry point that is much easier to secure than a set of links.

An Example of HATEOAS implementation in ASP.NET Core Web API application

The first idea would be to create a pretty simple service that is responsible for generating links based on the data from the application. But it means that it should be injected in each controller or at least in the base one. This approach will still generate valid links, but it creates room for errors, this time for API developers.

Instead, it can be done by creating custom middleware in ASP.NET Core, so that the whole process would be totally implicit.

For this example, I’ll provide code just for the API part of the application, mocking the data access layer and completely ignoring the business layer.

Custom result filter

To make the process implicit and not to include the call of the service responsible for adding links, we can use Filters from ASP.NET Core, in particular, Result filters.

Result filters run code immediately before and after the execution of action results. They run only when the action method has been executed successfully. They are useful for logic that must surround view or formatter execution.

A class that implements the IResultFIlter interface should have two methods: OnResultExecuted and OnResultExecuting. The former can be skipped here, and the latter is pretty simple:

public class HateoasResultFilter : IResultFilter
{
    private readonly IHateoasService _hateoasService;

    public HateoasResultFilter(IHateoasService hateoasService)
    {
        _hateoasService = hateoasService;
    }

    public void OnResultExecuting(ResultExecutingContext context)
    {
        if (context.Result is not ObjectResult objectResult) return;

        context.HttpContext.Response.ContentType = "application/hal+json";
        var result = _hateoasService.GenerateHateoasResponse(objectResult.Value, context);
        context.Result = new ObjectResult(result);
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
    }
}

Here we just change the content type of the response to “application/hal+json” and generate a response according to this format. And all this happens only in the case of IActionResult being an ObjectResult. After that, a new response is being overwritten. That’s the value that is being sent to the client.

Note that _hateoasService, which can be potentially used in each endpoint of each controller, is injected here just once.

And it’s also added as a scoped service in Startup:

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IHateoasService, HateoasService>();
}

Links configuration middleware

The next expected step is the service that is responsible for generating the response. But before going that far, it’s important to look into some helper classes.

Link options

Links are represented in the form of LinkOption class (constructors are omitted):


    public class LinkOptions<T> : ILinkOptions
    {
        private readonly Func<T, object> _getRouteValues;
        private readonly Func<T, string, string> _templateLinkGenerator;
        private readonly Func<T, bool> _canCreateLink;
        private readonly string _template;
        public string Relation { get; }
        public string RouteName { get; }
        public HttpMethod HttpMethod { get; }
        public bool IsTemplate => !string.IsNullOrEmpty(_template);
        public object GetRouteValues(object obj) => _getRouteValues((T)obj);
        public string GetLinkTemplate(object obj) => _templateLinkGenerator((T)obj, _template);
        public bool CanCreateLink(object obj) => _canCreateLink((T)obj);
    }
</T>

There are two types of links created by the two corresponding types of LinkOptions: regular and templated. The former can be consumed directly and the latter includes some templated parts that should be replaced to become valid.

Both of them have three identical options: 

  • Relation: the name of the link, that reflects its relation to the retrieved resource
  • HttpMethod: for the client to know how to use this link (GET, POST, etc.)
  • CanCreateLink: a method for specifying a condition under which this link should be added to the response

And there are some unique options:

  • For regular links, we need a way to get some data from the response to put in the link while it’s being generated (for example, an identifier of the parent entity) and the route name (that is assigned to one of the endpoints in the application)
  • For templated links, it’s the template itself and some of the values that can be replaced by the application

We’ll configure a couple of links in the last step, and the HateosService will use them to generate a response to send to the client.

Storing, adding new and getting links

There is also one class that is responsible for storing all links. We use it for getting link options each time when generating a new response.


public class TypeLinks
    {
        private readonly Dictionary<Type, Dictionary<string, ILinkOptions>> _links;
        internal IEnumerable<ILinkOptions> GetLinks(Type type) => _links.ContainsKey(type) ? _links[type].Values : null;
        internal bool HasLinks(Type type) => _links.ContainsKey(type);
        public TypeLinks()
        {
            _links = new Dictionary<Type, Dictionary<string, ILinkOptions>>();
        }
        internal void AddLinkTemplate<T>(string relation, HttpMethod httpMethod, string template, Func<T, string, string> templateGenerator, Func<T, bool> canCreate)
        {
            var type = typeof(T);
            if (!_links.ContainsKey(type))
            {
                _links[type] = new Dictionary<string, ILinkOptions>();
            }
            _links[type][relation] = new LinkOptions</T>(relation, httpMethod, template, templateGenerator, canCreate);
        }
        internal void AddLink<T>(string relation, HttpMethod httpMethod, string routeName, Func<T, object> routeValues, Func<T, bool> canCreate)
        {
            var type = typeof(T);
            if (!_links.ContainsKey(type))
            {
                _links[type] = new Dictionary<string, ILinkOptions>();
            }
            _links[type][relation] = new LinkOptions</T>(relation, httpMethod, routeName, routeValues, canCreate);
        }
</ILinkOptions>

It has three main methods. Two of them are for adding links: a regular one and a templated option. And a GetLinks method is used when we need to get those links and generate a link.

Service for generating links

It is the class where all the magic is happening. Let’s take it apart:


public object GenerateHateoasResponse(object value, ActionContext context)
{
    if (value == null)
    {
        return null;
    }
    IDictionary<string, object> result = AddPropertiesAndEmbedded(value, context);
    IDictionary<string, object> links = AddResourceLinks(value, context);
    if (links is not null)
    {
        result["_links"] = AddResourceLinks(value, context);
    }
    return result;
}

A response can have two objects (excluding the data itself): _links and _embedded.

In the first step, we are adding all the data and then the links if only they are present.


private IDictionary<string, object> AddPropertiesAndEmbedded(object obj, ActionContext context)
{
    IDictionary<string, object> properties = new ExpandoObject();
    IDictionary<string, object> embedded = new ExpandoObject();
    foreach (PropertyInfo property in obj.GetType().GetProperties())
    {
        var propertyValue = property.GetValue(obj);
        var propertyName = property.Name;
        var propertyType = property.PropertyType;
        if (propertyValue is null)
        {
            continue;
        }
        if (!_typeLinks.Value.HasLinks(propertyType) && propertyValue is not IEnumerable<object>)
        {
            properties.Add(propertyName, propertyValue);
            continue;
        }
        var content = propertyValue is IEnumerable</object> list
            ? list.ToList().Select(x => GenerateHateoasResponse(x, context)).ToList()
            : GenerateHateoasResponse(propertyValue, context);
        embedded.Add(propertyName, content);
    }
    if (embedded.Count > 0)
    {
        properties["_embedded"] = embedded;
    }
    return properties;
}

The main points here are:

  • Properties are being added as they are if there are no links for them and if the property is not a collection
  • If the property is a collection, we use the initial method recursively for each item of the collection
  • If there are links for a single property, we also use the initial method recursively

private IDictionary<string, object> AddResourceLinks(object obj, ActionContext context)
{
    var linkOptions = _typeLinks.Value.GetLinks(obj.GetType()); // getting all configured links for the specified type
    return linkOptions?
        .Where(x => x.CanCreateLink(obj)) // add links only if specified conditions were met
        .ToDictionary(link => link.Relation, link => new
    {
        href = link.IsTemplate 
            ? link.GetLinkTemplate(obj) // create a link using a template if any
            : new Uri(_linkGenerator.GetUriByRouteValues(context.HttpContext, link.RouteName, link.GetRouteValues(obj)) ?? string.Empty).PathAndQuery, // or create a link using route name
        method = link.HttpMethod.Method, // specify HTTP method
        templated = link.IsTemplate // specify if a template or not
    } as object);
}

The only thing left is to see how to configure a set of links for the type.

Links Configuration

Lastly, we need to configure links for each class that should be extended with the links. To do this we’re going to use the AddLink or AddLinkTemplate method of the TypeLinks class. It has multiple overloads and we’re going to use some of them.

The simplest one:


links.AddLink<ThoughtDto>(
    "self:collection",
    HttpMethod.Get,
    nameof(ThoughtsController.GetThoughts));
</ThoughtDto>

Here we add a “self:collection” link that’ll be returned with ThoughtDto. With this link, clients will be able to retrieve a collection of thoughts. And here’s the example of the response:

{
  "Id": 1,
  "Name": "Have a rest",
  "Description": "Don't overtime",
  "OccurredOn": "2021-01-01T00:00:00",
  "_links": {
    "collection": {
      "href": "/Thoughts",
      "method": "GET",
      "templated": false
    }
  }
}

The client that got one thought by id is also aware of the endpoint “/thoughts”, which it can use to retrieve all the thoughts.


links.AddLinkTemplate<ThoughtListDto>(
    "self",
    HttpMethod.Get,
    (ThoughtTemplates.Get),
    (_, template) => template,
    x => x.Total > 0);
</ThoughtListDto>

While retrieving a list of thoughts, we don’t just return a generic collection, but a DTO with a collection as one of the properties. Another property here is the total value. For it, we have a link named “self”. It tells the client how to get details on each of the included items of the collection.

Here we can’t use the route name since there is a template value there that we cannot replace now.

For that reason we’re using a manually created link:

"thoughts/{thoughtId}"

And the last parameter of the AddLinkTemplate method is CanCreate. It specifies the conditions under which a link can be added to the response. In this case, it’s the total value. Logically, we don’t need a link for getting details of one thought if they just don’t exist.

So, for the collection we’re getting this response:

{
  "Total": 2,
  "_embedded": {
    "Thoughts": [
      {
        "Id": 1,
        "Name": "Have a rest",
        "Description": "Don't overtime",
        "OccurredOn": "2021-01-01T00:00:00",
        "_links": {
          "collection": {
            "href": "/Thoughts",
            "method": "GET",
            "templated": false
          }
        }
      },
      {
        "Id": 2,
        "Name": "Tell about HATEAOS",
        "Description": "Prepare a tutorial and provide a demo code",
        "OccurredOn": "2021-04-15T00:00:00",
        "_links": {
          "collection": {
            "href": "/Thoughts",
            "method": "GET",
            "templated": false
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "thoughts/{thoughtId}",
      "method": "GET",
      "templated": true
    }
  }
}

You can see that the “_links” value is not only in the main object but also in the nested ones. In this case, they create duplication, but it’s just for demonstration purposes. Also, the “self” link is templated, and the client knows that it should replace some parts to use it.

There is also an example endpoint with an empty collection, that returns another response:

{
  "Total": 0,
  "_embedded": {
    "Thoughts": []
  },
  "_links": {}
}

While the “thoughts” collection inside _embedded is empty we don’t need any links here.

These are the basic examples, but you can create all kinds of links for all objects you have in the application.

Disadvantages and problems that occurred in the process

Some disadvantages should be considered while working on the HATEOAS constraint.

  • A client can be unaware of all the API endpoints, but it still should know the format for POST and PUT requests. It means that you still need some documentation. One of the elegant solutions is to include a link for each specific body of the endpoint inside the HATEOAS link
  • All links still need to be configured. Some parts of the process can be factorized, but you need to manually specify the HTTP method, endpoint name, or template and the conditions under which it should be added to the response
  • Imagine an endpoint “/mind/{mindId}/thoughts/{thoughtId}”. You can send it as a template but there won’t be too much sense in it for the client, as it has to replace a lot of values. So, you need to at least replace the first part. And to do that you need to return the mind identifier, which is not needed by the client. It’s a small downside that sometimes your response will consist of redundant data, that is needed only by the middleware for building links.
  • Almost certainly you’ll need some kind of a link builder for generating links that include more than one template value. Just do it at the beginning to be ready to create any kind of links

Summary

HATEOAS is a widely known but not popular constraint for building API the way it was conceived. The main reason for it not being very popular is the difficulty of implementing it and the lack of specialized tools for working with it. This plan for creating HATEOAS middleware in the ASP.NET Core application should help you create a solution for working with any kind of links. There are some ways for improving, that can mostly ease the process of configuration, so don’t hesitate to improve it by yourself.

0 Comments
name *
email *