Skip to content

Hypermedia as the engine of application state HATEOAS

Diego Fernandez edited this page Mar 20, 2014 · 3 revisions

In other Wiki pages we looked at how Simple.Web can help us to build web applications, and more specifically those that are ReST-focused. Without wanting to involve myself in the 'What is ReST?' debate it is without doubt there is a Holy Grail - and that is Level 3 of the Richardson Maturity Model.

Hyperlinks

In Resource Handling I stated "a resource is state and methods in your applications’ domain that you have chosen to expose". The key element to HATEOAS is methods, which in the world of HTTP are usually represented as hyperlinks; it is this we will use to drive the client and in the process loosen the [coupling](http://en.wikipedia.org/wiki/Coupling_(computer_programming) of client to server.

Hyperlinks can be categorised as follows;

  • Root - Available without a predetermined path
  • Link - Indicates a path from the current state
  • Canonical - Denotes definitive location of the state

Let's take a contrived example path;

Root +
     |
     `- Customers List *
                       |
                       `- Customer Detail

Each of the above steps is a resource with one or more URIs. For each customer listed in the returned state there is a method to retrieve customer detail. If that method is presented as an hyperlink, together with it's relation to the resource, it can drive the client - act as the engine.

Example

Note In the examples below I've used custom vendor mime-types to demonstrate they are supported. I should state this is discouraged by the HTTP spec. If you are considering using custom mime-types in your application you might want to investigate using media parameters instead.

A simple example of HATEOAS could be an AngularJS SPA application consuming resources as follows;

  • Present root hyerlinks as application options to the user
  • Use hyperlink URI of rel="customers" to display customer list
  • Use hyperlink URI of rel="self" to display details

You will notice I am using the link "rel" (relation) to identify the hyperlink. This means that any custom "rel" is tightly coupled from client to server and thus changing it means breaking your API. Using a canonical rel of "self" where appropriate solves this and allows for usage with Put, Patch, or Delete.

Support

To help with the above scenarios Simple.Web offers the following class-level attributes;

  • [Root]

    • Rel - Relation to the resource state
    • Title - Display name/description
    • Type - Custom mime-type
  • [LinksFrom(Type, URI)]

    • Type - C# Type that this can be linked from
    • URI - URI, and optionally path segments, of the generated link
    • Rel - Relation to the resource state
    • Title - Display name/description
    • Type - Custom mime-type
  • [Canonical(Type, URI)]

    • Type - C# Type that this can be linked from
    • URI - URI (and optionally path segments) of the generated link

Example

[Bootstrap] Program.cs

namespace HATEOAS
{
    using System;
    using System.Collections.Generic;

    using HATEOAS.Customer;

    public static class TestData
    {
        public static readonly IEnumerable<CustomerModel> Customers = new[]
                              {
                                  new CustomerModel
                                      {
                                          CustomerUid = new Guid("fa63d776-9dab-4755-a27a-b0690e3f7be3"),
                                          Forename = "James",
                                          Surname = "Bond",
                                          DateOfBirth = new DateTime(1920, 11, 11),
                                          Gender = "Male"
                                      },
                                  new CustomerModel
                                      {
                                          CustomerUid = new Guid("d5afee73-1229-46d9-8fc4-a6e7ecd8f014"),
                                          Forename = "Pussy",
                                          Surname = "Galore",
                                          DateOfBirth = new DateTime(1925, 08, 22),
                                          Gender = "Female"
                                      }
                              };
    }

    internal class Program
    {
        public static Type[] EnforceTypesFor = { typeof(Simple.Web.JsonNet.JsonMediaTypeHandler) };

        private static void Main(string[] args)
        {
            new Simple.Web.Hosting.Self.OwinSelfHost().Run();
        }
    }
}

[Root Links] GetEndpoint.cs

namespace HATEOAS
{
    using System.Collections.Generic;

    using Simple.Web;
    using Simple.Web.Behaviors;
    using Simple.Web.Links;

    [UriTemplate("/")]
    public class GetEndpoint : IGet, IOutput<IEnumerable<Link>>
    {
        public Status Get()
        {
            this.Output = LinkHelper.GetRootLinks();

            return 200;
        }

        public IEnumerable<Link> Output { get; private set; }
    }
}

[Customer List Model] CustomersModel.cs

namespace HATEOAS.Customers
{
    using System;

    public class CustomersModel
    {
        public Guid CustomerUid { get; set; }

        public string Forename { get; set; }

        public string Surname { get; set; }
    }
}

[Customer List] GetEndpoint.cs

namespace HATEOAS.Customers
{
    using System.Collections.Generic;
    using System.Linq;

    using Simple.Web;
    using Simple.Web.Behaviors;
    using Simple.Web.Links;

    [UriTemplate("/customers")]
    [Root(Rel = "customers", Title = "Customer List", Type = "application/vnd.polygotadventures.list.customer")]
    public class GetEndpoint : IGet, IOutput<IEnumerable<CustomersModel>>
    {
        public Status Get()
        {
            this.Output = TestData.Customers.Select(c => new CustomersModel
                                                             {
                                                                 CustomerUid = c.CustomerUid,
                                                                 Forename = c.Forename,
                                                                 Surname = c.Surname
                                                             });

            return 200;
        }

        public IEnumerable<CustomersModel> Output { get; private set; }
    }
}

[Customer Detail Model] CustomerModel.cs

namespace HATEOAS.Customer
{
    using System;

    public class CustomerModel
    {
        public Guid CustomerUid { get; set; }

        public string Forename { get; set; }

        public string Surname { get; set; }

        public DateTime DateOfBirth { get; set; }

        public string Gender { get; set; }
    }
}

[Customer Detail] GetEndpoint.cs

namespace HATEOAS.Customer
{
    using System;
    using System.Linq;
    using Simple.Web;
    using Simple.Web.Behaviors;
    using Simple.Web.Links;

    [UriTemplate("/customer/{CustomerUid}")]
    [Canonical(typeof(Customers.CustomersModel), Title = "Customer Detail", Type = "application/vnd.polygotadventures.customer")]
    public class GetEndpoint : IGet, IOutput<CustomerModel>
    {
        public Status Get()
        {
            this.Output = TestData.Customers
                .Select(c => new CustomerModel
                    {
                        CustomerUid = c.CustomerUid,
                        Forename = c.Forename,
                        Surname = c.Surname,
                        DateOfBirth = c.DateOfBirth,
                        Gender = c.Gender
                    })
                .FirstOrDefault(c => c.CustomerUid == this.CustomerUid);

            return 200;
        }

        public Guid CustomerUid { get; set; }

        public CustomerModel Output { get; private set; }
    }
}

Canonical vs LinksFrom

I have used [Canonical] above which has a default rel of "self". You can also use [LinksFrom] should this better meet intent, such as supporting transitional commands via POST operations, for example;

[Customer Termination] PostEndpoint.cs

namespace HATEOAS.Customer
{
    using System;
    using Simple.Web;
    using Simple.Web.Links;

    [UriTemplate("/customer/{CustomerUid}/terminate")]
    [LinksFrom(typeof(Customers.CustomersModel), Rel = "customer.terminate")]
    public class PostEndpoint : IPost
    {
        public Status Post()
        {
           ...
        }
    }
}

Note Automatic link serialization is currently only supported in Simple.Web.JsonNet and Simple.Web.Xml, and not Simple.Web.JsonFx.

For more detail check out the Simple.Web API documentation.