Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

REST Hypermedia URI Changes Based On Context in Web API (HATEOAS)

I am working on a new asp.net web api restful service and spent some time with some Pluralsight courses on the subject. One of the better ones dives deep into design and the implementation of hypermedia (HATEOAS).

I followed the implementation in the video as it was very straight forward and being new to mvc/web api it was really helpful to see it working end to end.

However as soon as I started to dig a bit deeper into my implementation, the use of a UrlHelper() to calculate the link to return began to fall apart.

In the code below, I have a simple Get() which returns a collection of a particular resources and then a Get(int id) which allows for the returning of a individual resource.

All of the results go through a ModelFactory which transforms my POCOs to return results and back again on post, patch and puts.

I was trying to do this in a more sophisticated way by allowing the ModelFactory to handle all of the intelligence of link creation since it is constructed using the Request object.

Now I know I could solve all of this by simply handling the link generation/inclusion right inside my methods and maybe that is the answer but I was curious how others are handling it.

My goal:

1) In result sets (i.e. collections of results returned by "Get()"), to include total item count, total page count, next and previous pages as necessary. I have implemented a custom json converter to drop empty links on the ground. For example, I do not print out "prevPage" when you are on the first page. This works today.

2) In individual results (i.e. result returned by "Get(id)"), to include links to self, include rel, method the link represents and whether or not it is templated. This works today.

What is broken:

As you will see in the output below, two things are "wrong". When you look at the "POST" link for a new individual item, the URL is correct. This is because I am stripping out the last portion of the URI (dropping the resource ID). When returning a result set however, the URI for a "POST" is now incorrect. This is because the route did not include the individual resource id since "Get()" was called, not "Get(id)".

Again, the implementation could be changed to produce different links depending on which method was hit, pulling them out of the factory and into controller but I would like to believe I am just missing something obvious.

Any pointers for this newbie to routing and Web API?

Controller Get()

[HttpGet]
    public IHttpActionResult Get(int pageSize = 50, int page = 0)
    {
        if (pageSize == 0)
        {
            pageSize = 50;
        }

        var links = new List<LinkModel>();

        var baseQuery = _deliverableService.Query().Select();
        var totalCount = baseQuery.Count();
        var totalPages = Math.Ceiling((double) totalCount / pageSize);

        var helper = new UrlHelper(Request);
        if (page > 0)
        {
            links.Add(TheModelFactory.CreateLink(helper.Link("Deliverables",
                new
                {
                    pageSize,
                    page = page - 1
                }),
                "prevPage"));
        }
        if (page < totalPages - 1)
        {
            links.Add(TheModelFactory.CreateLink(helper.Link("Deliverables",
                new
                {
                    pageSize,
                    page = page + 1
                }),
                "nextPage"));
        }

        var results = baseQuery
            .Skip(page * pageSize)
            .Take(pageSize)
            .Select(p => TheModelFactory.Create(p))
            .ToList();

        return Ok(new DeliverableResultSet
                  {
                      TotalCount = totalCount,
                      TotalPages = totalPages,
                      Links = links,
                      Results = results
                  }
            );
    }

Controller Get(id)

        [HttpGet]
    public IHttpActionResult GetById(int id)
    {
        var entity = _deliverableService.Find(id);

        if (entity == null)
        {
            return NotFound();
        }

        return Ok(TheModelFactory.Create(entity));
    }

ModelFactory Create()

 public DeliverableModel Create(Deliverable deliverable)
    {
        return new DeliverableModel
               {
                   Links = new List<LinkModel>
                           {
                               CreateLink(_urlHelper.Link("deliverables",
                                   new
                                   {
                                       id = deliverable.Id
                                   }),
                                   "self"),
                                   CreateLink(_urlHelper.Link("deliverables",
                                   new
                                   {
                                       id = deliverable.Id
                                   }),
                                   "update", "PUT"),
                                   CreateLink(_urlHelper.Link("deliverables",
                                   new
                                   {
                                       id = deliverable.Id
                                   }),
                                   "delete", "DELETE"),
                               CreateLink(GetParentUri() , "new", "POST")
                           },
                   Description = deliverable.Description,
                   Name = deliverable.Name,
                   Id = deliverable.Id
               };
    }

ModelFactory CreateLink()

public LinkModel CreateLink(string href, string rel, string method = "GET", bool isTemplated = false)
    {
        return new LinkModel
               {
                   Href = href,
                   Rel = rel,
                   Method = method,
                   IsTemplated = isTemplated
               };
    }

Result of Get()

{
totalCount: 10,
totalPages: 4,
links: [{
    href: "https://localhost/Test.API/api/deliverables?pageSize=2&page=1",
    rel: "nextPage"
}],
results: [{
    links: [{
        href: "https://localhost/Test.API/api/deliverables/2",
        rel: "self"
    },
    {
        href: "https://localhost/Test.API/api/deliverables/2",
        rel: "update",
        method: "PUT"
    },
    {
        href: "https://localhost/Test.API/api/deliverables/2",
        rel: "delete",
        method: "DELETE"
    },
    {
        href: "https://localhost/Test.API/api/",
        rel: "new",
        method: "POST"
    }],
    name: "Deliverable1",
    description: "",
    id: 2
},
{
    links: [{
        href: "https://localhost/Test.API/api/deliverables/3",
        rel: "self"
    },
    {
        href: "https://localhost/Test.API/api/deliverables/3",
        rel: "update",
        method: "PUT"
    },
    {
        href: "https://localhost/Test.API/api/deliverables/3",
        rel: "delete",
        method: "DELETE"
    },
    {
        href: "https://localhost/Test.API/api/",
        rel: "new",
        method: "POST"
    }],
    name: "Deliverable2",
    description: "",
    id: 3
}]

}

Result of Get(id)

{
links: [{
    href: "https://localhost/Test.API/api/deliverables/2",
    rel: "self"
},
{
    href: "https://localhost/Test.API/api/deliverables/2",
    rel: "update",
    method: "PUT"
},
{
    href: "https://localhost/Test.API/api/deliverables/2",
    rel: "delete",
    method: "DELETE"
},
{
    href: "https://localhost/Test.API/api/deliverables/",
    rel: "new",
    method: "POST"
}],
name: "Deliverable2",
description: "",
id: 2

}

Update 1

On Friday I found and began to implement the solution outlined here: http://benfoster.io/blog/generating-hypermedia-links-in-aspnet-web-api. Ben's solution is very well thought out and allows me to maintain my models (stored in a publicly available library for use in other .NET (i.e. RestSharp)) solutions and allows me to use AutoMapper instead of implementing my own ModelFactory. Where AutoMapper fell short was when I needed to work with contextual data (such as the Request). Since my HATEOAS implementation has been pulled out and into a MessageHandler, AutoMapper again becomes a viable option.

like image 576
James Legan Avatar asked Apr 18 '14 15:04

James Legan


People also ask

What's true about a hypermedia as the engine of application state HATEOAS select all that apply?

Hypermedia as the Engine of Application State (HATEOAS) is a constraint of the REST application architecture that distinguishes it from other network application architectures. With HATEOAS, a client interacts with a network application whose application servers provide information dynamically through hypermedia.

What is hypermedia driven REST Web services?

Hypermedia is an important aspect of REST. It lets you build services that decouple client and server to a large extent and let them evolve independently. The representations returned for REST resources contain not only data but also links to related resources.

What is HATEOAS format?

HATEOAS stands for Hypermedia as the Engine of Application State and it is a component of RESTful API architecture and design. With the use of HATEOAS, the client-side needs minimal knowledge about how to interact with a server.


1 Answers

I extended Ben's solution (link below) and it has met every requirement I have placed on it. I believe that "enriching" the return result in the handlers with the required HATEOAS data is the way to go. The only time I need to set links directly outside of the handler is when I get into things like paging where only the controller has the necessary information to make the decision on what the links should look like. At that point, I simply add the link to the collection on my model which carries through to the handler where even more links might be added.

http://benfoster.io/blog/generating-hypermedia-links-in-aspnet-web-api

like image 144
James Legan Avatar answered Nov 08 '22 23:11

James Legan