Implementing HATEOAS in a controller

The final step is to implement a controller that can handle the ItemHateoasResponse response model implemented previously. More specifically, we can proceed by creating a new ItemHateoasController class in our Catalog.API project. Note that we are building a new controller for demonstration purposes. An alternative would be to edit the already defined ItemController to return HATEOAS-compliant responses. 

The ItemHateoasController class will use the ILinksService interface provided by the RiskFirst.Hateoas namespace to enrich the Links attribute of the ItemHateoasResponse model and return it to our client. Let's proceed by implementing the controller:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Catalog.API.Filters;
using Catalog.API.ResponseModels;
using Catalog.Domain.Requests.Item;
using Catalog.Domain.Services;
using Microsoft.AspNetCore.Mvc;
using RiskFirst.Hateoas;

namespace Catalog.API.Controllers
{
[Route("api/hateoas/items")]
[ApiController]
[JsonException]
public class ItemsHateoasController : ControllerBase
{
private readonly IItemService _itemService;
private readonly ILinksService _linksService;

public ItemsHateoasController(ILinksService linkService, IItemService itemService)
{
_linksService = linkService;
_itemService = itemService;
}

[HttpGet(Name = nameof(Get))]
public async Task<IActionResult> Get([FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0)
{
var result = await _itemService.GetItemsAsync();

var totalItems = result.Count();

var itemsOnPage = result.OrderBy(c => c.Name)
.Skip(pageSize * pageIndex)
.Take(pageSize);

var hateoasResults = new List<ItemHateoasResponse>();

foreach (var itemResponse in itemsOnPage)
{
var hateoasResult = new ItemHateoasResponse { Data = itemResponse };
await _linksService.AddLinksAsync(hateoasResult);

hateoasResults.Add(hateoasResult);
}

var model = new PaginatedItemResponseModel<ItemHateoasResponse>(
pageIndex, pageSize, totalItems, hateoasResults);

return Ok(model);
}


[HttpGet("{id:guid}", Name = nameof(GetById))]
[ItemExists]
public async Task<IActionResult> GetById(Guid id)
{
var result = await _itemService.GetItemAsync(new GetItemRequest { Id = id });
var hateoasResult = new ItemHateoasResponse { Data = result };
await _linksService.AddLinksAsync(hateoasResult);

return Ok(hateoasResult);
}

The Get and GetById action methods are quite similar to the one that is present in ItemController. The only difference is that they return a different response type, which is represented by the ItemHateoasResponse class. Furthermore, the action methods assign the response object to the Data field. Each action method also calls the AddLinksAsync method provided by the ILinksService interface to populate the link attribute. In the same way, we can extend the behavior of the other action methods present in the controller class:

...

[HttpPost(Name = nameof(Post))]
public async Task<IActionResult> Post(AddItemRequest request)
{
var result = await _itemService.AddItemAsync(request);
return CreatedAtAction(nameof(GetById), new { id = result.Id }, null);
}

[HttpPut("{id:guid}", Name = nameof(Put))]
[ItemExists]
public async Task<IActionResult> Put(Guid id, EditItemRequest request)
{
request.Id = id;
var result = await _itemService.EditItemAsync(request);

var hateoasResult = new ItemHateoasResponse { Data = result };
await _linksService.AddLinksAsync(hateoasResult);

return Ok(hateoasResult);
}

[HttpDelete("{id:guid}", Name = nameof(Delete))]
[ItemExists]
public async Task<IActionResult> Delete(Guid id)
{
var request = new DeleteItemRequest { Id = id };
await _itemService.DeleteItemAsync(request);
return NoContent();
}
..

This is the declaration of the implementation of the create, update, and delete action methods. Also, in this case, we are using the ItemHateoasResponse model class to retrieve the response of the action method. We should notice that the action methods declare Name in the [HttpVerb] attribute decorator, such as  [HttpDelete("{id:guid}", Name = nameof(Delete))]. Indeed, we will use the Name declared by the attribute in the Startup class to refer to each route and include it in the Link property:

namespace Catalog.API
{
public class Startup
{
...

public void ConfigureServices(IServiceCollection services)
{
...

services.AddLinks(config =>
{
config.AddPolicy<ItemHateoasResponse>(policy =>
{
policy
.RequireRoutedLink(nameof(ItemsHateoasController.Get),
nameof(ItemsHateoasController.Get))
.RequireRoutedLink(nameof(ItemsHateoasController.GetById),
nameof(ItemsHateoasController.GetById), _ => new {id = _.Data.Id})
.RequireRoutedLink(nameof(ItemsHateoasController.Post),
nameof(ItemsHateoasController.Post))
.RequireRoutedLink(nameof(ItemsHateoasController.Put),
nameof(ItemsHateoasController.Put), x => new {id = x.Data.Id})
.RequireRoutedLink(nameof(ItemsHateoasController.Delete),
nameof(ItemsHateoasController.Delete), x => new {id = x.Data.Id});
});
});
}
...
}
}

The AddLinks extension method provided by the RiskFirst.Hateoas package allows us to define the policies related to a response model. This is the case for the config.AddPolicy<ItemHateoasResponse> method, which calls RequireRoutedLink for each action method name declared in ItemsHateoasControllerNote that, similar to the previous cases, we can extract this snippet of code in an external extension method to keep the Startup class as clean as possible. Finally, this kind of approach allows us to define different groups of links for different response models. Moreover, it is possible to establish a policy related to a specific response model.

Consequently, we can now run and verify the result by executing the dotnet run command in the Catalog.API folder. Please note, to run the Docker SQL Server, specify the connection string in appsetting.json file. After that, we can run the following curl request to verify ItemsHateoasController. The resulting JSON response will look as follows:

{
"_links": {
"get": {
"rel": "ItemsHateoas/Get",
"href": "https://localhost:5001/api/hateoas/items",
"method": "GET"
},
"get_by_id": {
"rel": "ItemsHateoas/GetById",
"href": "https://localhost:5001/api/hateoas/items/8ff0fe8f-9dbc-451f-7a57-08d652340f56",
"method": "GET"
},
"create": {
"rel": "ItemsHateoas/Post",
"href": "https://localhost:5001/api/hateoas/items",
"method": "POST"
},
"update": {
"rel": "ItemsHateoas/Put",
"href": "https://localhost:5001/api/hateoas/items/8ff0fe8f-9dbc-451f-7a57-08d652340f56",
"method": "PUT"
},
"delete": {
"rel": "ItemsHateoas/Delete",
"href": "https://localhost:5001/api/hateoas/items/8ff0fe8f-9dbc-451f-7a57-08d652340f56",
"method": "DELETE"
}
},
"id": "8ff0fe8f-9dbc-451f-7a57-08d652340f56",
"name": "Malibu",

...

As you can see, in addition to the list of items, ItemHateoasController also retrieves an envelope that provides additional information to the client, such as the other routes needed for the get, add, update, and delete operations. Furthermore, this approach gives all the URIs needed by the client, in order to navigate through the information exposed by the web service.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.217.144.32