Problem
How to implement RESTful API and custom media types in ASP.NET Core.
Solution
Using the CRUD and Paging samples, we’ll add links for clients to interact with our API. Download the code with this post to get a working sample and sample HTTP requests (Postman export).
Add a custom media type when configuring MVC services,
- public void ConfigureServices(
- IServiceCollection services)
- {
-
- services.AddMvc(options =>
- {
- options.ReturnHttpNotAcceptable = true;
- options.OutputFormatters
- .OfType<JsonOutputFormatter>()
- .FirstOrDefault()
- ?.SupportedMediaTypes
- .Add("application/vnd.fiver.hateoas+json");
- });
- }
Create types that would add links to output models being sent to the client,
- public class LinksWrapper<T>
- {
- public T Value { get; set; }
- public List<LinkInfo> Links { get; set; }
- }
-
- public class LinksWrapperList<T>
- {
- public List<LinksWrapper<T>> Values { get; set; }
- public List<LinkInfo> Links { get; set; }
- }
Add links for the collection resource,
- private List<LinkInfo> GetLinks_List(PagedList<Movie> model)
- {
- var links = new List<LinkInfo>();
-
-
-
- links.Add(new LinkInfo
- {
- Href = urlHelper.Link("CreateMovie", new { }),
- Rel = "create-movie",
- Method = "POST"
- });
-
- return links;
- }
Add links for single resource,
- private List<LinkInfo> GetLinks_Model(Movie model)
- {
- var links = new List<LinkInfo>();
-
- links.Add(new LinkInfo
- {
- Href = urlHelper.Link("GetMovie", new { id = model.Id }),
- Rel = "self",
- Method = "GET"
- });
-
- links.Add(new LinkInfo
- {
- Href = urlHelper.Link("UpdateMovie", new { id = model.Id }),
- Rel = "update-movie",
- Method = "PUT"
- });
-
- links.Add(new LinkInfo
- {
- Href = urlHelper.Link("DeleteMovie", new { id = model.Id }),
- Rel = "delete-movie",
- Method = "DELETE"
- });
-
- return links;
- }
The GET methods will return response based on media type,
- [HttpGet(Name = "GetMovies")]
- public IActionResult Get(
- PagingParams pagingParams,
- [FromHeader(Name = "Accept")]string acceptHeader)
- {
- var model = service.GetMovies(pagingParams);
-
- Response.Headers.Add("X-Pagination", model.GetHeader().ToJson());
-
- if (string.Equals(acceptHeader, "application/vnd.fiver.hateoas+json"))
- {
- var outputModel = ToOutputModel_Links(model);
- return Ok(outputModel);
- }
- else
- {
- var outputModel = ToOutputModel_Default(model);
- return Ok(outputModel);
- }
- }
-
- [HttpGet("{id}", Name = "GetMovie")]
- public IActionResult Get(int id,
- [FromHeader(Name = "Accept")]string acceptHeader)
- {
- var model = service.GetMovie(id);
- if (model == null)
- return NotFound();
-
- if (string.Equals(acceptHeader, "application/vnd.fiver.hateoas+json"))
- {
- var outputModel = ToOutputModel_Links(model);
- return Ok(outputModel);
- }
- else
- {
- var outputModel = ToOutputModel_Default(model);
- return Ok(outputModel);
- }
- }
Output for GET,
Discussion
The idea behind HATEOAS (Hypermedia As The Engine Of Application State) is to transfer links in the resource representations. The sample demonstrates how links can be provided for a collection and individual resources. The links represent the actions that can be performed on the resource at a given point in time.
Custom media types are used to more accurately specify the type of representation being exchanged. For instance it is true that we’re exchanging JSON, however, it is more accurate to specify that what we’re exchanging with clients is more than JSON, it is a special form of JSON i.e. our own representation / media type.
We could also have custom media types for input formatters,
- options.InputFormatters
- .OfType<JsonInputFormatter>()
- .FirstOrDefault()
- ?.SupportedMediaTypes
- .Add("application/vnd.fiver.movie.input+json");
And then use Action Constraints to restrict access to action methods,
- [ContentTypeOf("application/vnd.fiver.movie.input+json")]
- [HttpPost(Name = "CreateMovie")]
- public IActionResult Create(
- [FromBody]MovieInputModel inputModel,
- [FromHeader(Name = "Accept")]string acceptHeader)
- {
The custom action constraint is a class inheriting from Attribute and implementing IActionConstraint,
- public class ContentTypeOf : Attribute, IActionConstraint
- {
- private readonly string expectedContentType;
-
- public ContentTypeOf(string expectedContentType)
- {
- this.expectedContentType = expectedContentType;
- }
-
- public int Order => 0;
-
- public bool Accept(ActionConstraintContext context)
- {
- var request = context.RouteContext.HttpContext.Request;
-
- if (!request.Headers.ContainsKey("Content-Type"))
- return false;
-
- return string.Equals(request.Headers["Content-Type"],
- expectedContentType,
- StringComparison.OrdinalIgnoreCase);
- }
- }
Note
I didn’t go into theoretical details about REST and HATEOAS here because there are plenty of good resources available, my favorite is a book “REST in Practice” by Jim Webber.
Note
Postman file included with the sample contains HTTP requests for GET, POST, PUT, DELETE and PATCH.
Source Code
GitHub