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.