Becoming HATEOAS with ASP.NET Web API 2.2
The context
Hypermedia is an important aspect of REST. It allows you to build services that decouple client and server and allow them to evolve independently. This is achieved by returning representations of resources having links that indicate associated resources and way to interact with them.
For example, lets assume a package delivery service, which lists available deliveries and allows changing status of particular item to delivered. In other words, we expect client of delivery service to follow this flow:
GET /api/deliveries
PUT /api/deliveries/1234/status?status=delivered
The problem
To achieve HATEOAS constraint, the client should not construct status change request on its own. Instead, the information required for request should be returned as part of response to previous request.
The solution
Exact representation of available operations in responses can vary as neither plain json nor xml contain hypermedia support. The most popular solution is to borrow href standard from HTML or use other standardized format which covers this, like HAL for example, and use it instead pure json or xml. Lets assume requesting GET /api/deliveries
should response the following:
[
{
"Id": "dev1",
"Origin": "origin1",
"Desination": "destination1",
"Status": "Good news, everyone!",
"HandlerId": "fry",
"Links": [
{
"Href": "http://localhost:57009/api/deliveries/dev1",
"Rel": "self",
"Method": "GET"
},
{
"Href": "http://localhost:57009/api/deliveries/dev1/status?status=delivered",
"Rel": "status-delivered",
"Method": "PUT"
}
]
},
{
"Id": "dev2",
"Origin": "origin2",
"Desination": "destination2",
"Status": "Good news, everyone!",
"HandlerId": "leela",
"Links": [
{
"Href": "http://localhost:57009/api/deliveries/dev2",
"Rel": "self",
"Method": "GET"
},
{
"Href": "http://localhost:57009/api/deliveries/dev2/status?status=delivered",
"Rel": "status-delivered",
"Method": "PUT"
}
]
}
]
Here’s how this can be achieved using WebApi.
Step 1 - Define Deliveries
controller using attribute routing and provide GET /api/deliveries
[RoutePrefix("api/deliveries")]
public class DeliveriesController : ApiController
{
[HttpGet, Route("")]
public IHttpActionResult Get()
{
var deliveries = repository.GetDeliveries();
return Ok(deliveries);
}
}
The reponse at this point should look like:
[
{
"Id": "dev1",
"Origin": "origin1",
"Desination": "destination1",
"Status": "Good news, everyone!",
"HandlerId": "fry"
},
{
"Id": "dev2",
"Origin": "origin2",
"Desination": "destination2",
"Status": "Good news, everyone!",
"HandlerId": "leela"
}
]
Step 2 - Define method to change delivery status PUT /api/deliveries/1234/status?status=delivered
[HttpPut, Route("{id}/status", Name = "ChangeStatusById")]
public IHttpActionResult ChangeStatus(string id, [FromUri]string status)
{
var delivery = repository.GetDelivery(id);
if (delivery == default(Delivery))
{
return NotFound();
}
delivery.ChangeStatus(status);
return Ok();
}
The method is quite straightforward, nothing fancy. The only thing, that stands out is that the route has a name assigned (which is important).
Step 3 - Add a method to retrieve a delivery by id
[HttpGet, Route("{id}", Name = "GetDeliveryById")]
public IHttpActionResult Get(string id)
{
var delivery = repository.GetDelivery(id);
if (delivery == default(Delivery))
{
return NotFound();
}
return Ok(delivery);
}
Again, the only thing worth noticing is named routing definition.
Step 4 - Create a class which represents a hyperlink to available action
public class Link
{
public string Href { get; set; }
public string Rel { get; set; }
public string Method { get; set; }
}
That’s probably the simplest implementation possible. We might want to make this immutable, but that’s not the point of this example anyway.
Step 5 - Introduce a method which returns available hyperlinks associated with particular delivery.
private IEnumerable<Link> CreateLinks(Delivery delivery)
{
var links = new[]
{
new Link
{
Method = "GET",
Rel = "self",
Href = Url.Link("GetDeliveryById", new {id = delivery.Id})
},
new Link
{
Method = "PUT",
Rel = "status-delivered",
Href = Url.Link("ChangeStatusById", new {id = delivery.Id, status = "delivered"})
}
};
return links;
}
Now things get interesting. We’re creating available actions basing on a state of particular delivery. Method
property defines which HTTP methods are available for an action, Rel
identifies type of action (should be used by clients to find exact hyperlink among others) and Href
contains the hyperlink to be invoked to execute this action, with all the parameters already in place if possible. This allows the client to just use what’s provided without figuring out what goes where and what are the names of request params. But instead of constructing entire Href by ourselves, we’re letting WebApi create it for us basing on defined routings. To achieve this, we’re invoking Link
method defined on UrlHelper class which is accessed by Url
property of ApiController class. Of course WebApi needs to know which route should be used - that’s where names assigned to routes come in. The second parameter is an anonymous type containing names (and it’s important!) that match parameters of invoked methods.
In this example, for each delivery listed we’re generating two available operations - GET
to self as a way of retrieving single delivery (Rel
set to "self"
) and PUT
to change status of delivery to "delivered"
("status-delivered"
Rel
).
Step 6 - Enrich GET /api/deliveries
with generated links
To make use of generated links, we need to add them to the result returned by Get
methods. For example:
[HttpGet, Route("")]
public IHttpActionResult Get()
{
var deliveries = repository.GetDeliveries().Select(d =>
{
d.AddLinks(CreateLinks(d));
return d;
});
return Ok(deliveries);
}
Type of delivery, its properties and methods is not important for the sake of this example - we just need to make sure it can serialize provided links to json (or any other required format). The easiest way it just add:
public IEnumerable<Link> Links;
as part of contract.
Wrap-up
In this example we’ve achieved two important things. We’ve made our API fulfill HATEOAS constraint thus enabling clients to base state transitions only through dynamically generated by server links to actions. This means that the actions can be altered or even changed entirely on server-side without the need of changing the client code. Client just needs to be aware of type of action (identified by Rel
property of response) which it wants to invoke and look for it in the collection of returned links.
The other benefit is that actual Href
s to actions don’t need to be manually concatenated - WebApi generates them basing on Routes
, so that’s the only place that needs to be altered when introducing changes.