I am working on a new asp.net web api service and have spent some time with some Pluralsight courses on this. One of the best goes deep into the design and implementation of hypermedia (HATEOAS).
I watched the implementation in the video as it was very direct and was new to the mvc / web api. It was really useful to see that it works from end to end.
However, as soon as I started digging a little deeper in my implementation, using UrlHelper () to compute the return link began to fall apart.
In the code below, I have a simple Get () that returns a collection of specific resources, and then Get (int id) that allows you to return a single resource.
All results go through ModelFactory, which converts my POCOs to return the results back to post, patch and puts.
I tried to do this in a more complex way, allowing ModelFactory to handle all the linking intelligence as it was created using the Request object.
Now I know that I could solve all this by simply processing the linking / inclusion links directly in my methods and maybe this is the answer, but I was curious how others handle it.
My goal:
1) In result sets (ie, collections of results returned by "Get ()") to include the total number of items, total number of pages, next and previous pages as needed. I have implemented a custom json converter to remove empty links to the ground. For example, I do not print "prevPage" when you are on the first page. It works today.
2) In separate results (that is, the result returned by "Get (id)"), to include links to self, include rel, the method that represents the link, and the template template. It works today.
What is broken:
As you will see in the following figure, two things are wrong. When you look at the "POST" link for a new single item, the URL is correct. This is because I am removing the last part of the URI (discarding the resource identifier). However, when returning the result set, the URI for "POST" is now incorrect. This is due to the fact that the route did not include the identifier of an individual resource, since "Get ()" was called rather than "Get (id)".
Again, the implementation can be changed to create different links depending on which method was hit, pulled them from the factory and into the controller, but I would like to believe that I will just miss something obvious.
Any pointers for this newbie for routing and web APIs?
Get () Controller
[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 } ); }
Get controller (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 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 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 started implementing the solution described here: http://benfoster.io/blog/generating-hypermedia-links-in-aspnet-web-api . Ben's solution is very well thought out and allows me to support my models (stored in a public library for use in other .NET solutions (e.g. RestSharp)) and allows me to use AutoMapper instead of implementing my own ModelFactory. Where AutoMapper was short, I had to work with contextual data (such as a query). Since my HATEOAS implementation has been pulled out in MessageHandler, AutoMapper will again become a viable option.