Existing entity constraints

Action methods of controllers usually perform constraints on incoming data. A common practice is to centralize that kind of logic in filters. Let's take, for example, OrderController, which we discussed in the previous chapter:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc;
using SampleAPI.Filters;
using SampleAPI.Models;
using SampleAPI.Repositories;
using SampleAPI.Requests;

namespace SampleAPI.Controllers
{
[Route("api/order")]
[ApiController]
public class OrderController : ControllerBase
{
private readonly IOrderRepository _orderRepository;

public OrderController(IOrderRepository ordersRepository)
{
_orderRepository = ordersRepository;
}

...

[HttpPut("{id:guid}")]
[OrderExists]
public IActionResult Put(Guid id, OrderRequest request)
{
if (request.ItemsIds == null)
{
return BadRequest();
}

var order = _orderRepository.Get(id);

if (order == null)
{
return NotFound(new { Message = $"Item with id {id}
not exist." });

}

order = Map(request, order);

_orderRepository.Update(id, order);
return Ok();
}

[HttpPatch("{id:guid}")]
[OrderExists]
public IActionResult Patch(Guid id, JsonPatchDocument<Order>
requestOp)
{
var order = _orderRepository.Get(id);

if (order == null)
{
return NotFound(new { Message = $"Item with id {id} not
exist." });

}

requestOp.ApplyTo(order);
_orderRepository.Update(id, order);

return Ok();
}

[HttpDelete("{id:guid}")]
[OrderExists]
public IActionResult Delete(Guid id)
{
var order = _orderRepository.Get(id);

if (order == null)
{
return NotFound(new { Message = $"Item with id {id} not
exist." });

}

_orderRepository.Delete(id);
return NoContent();
}

...
}
}

Three out of five action methods perform the same existing check by calling _orderRepository:

 var order = _orderRepository.Get(id);

if (order == null)
{
return NotFound(new { Message = $"Item with id {id} not exist." });
}

A recommended practice is to extract this logic and put it somewhere else, possibly an action filter, so that it can be used across action methods. It is specific enough to be used only when necessary. Let's start by setting up our filter and adding the dependency with IOrderRepository:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using SampleAPI.Repositories;

namespace SampleAPI.Filters
{
public class OrderExistsAttribute : TypeFilterAttribute
{
public OrderExistsAttribute() : base(typeof
(OrderExistsFilterImpl)) { }

private class OrderExistsFilterImpl : IAsyncActionFilter
{
private readonly IOrderRepository _orderRepository;

public OrderExistsFilterImpl(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}

public async Task OnActionExecutionAsync(ActionExecutingContext
context, ActionExecutionDelegate next)
{
...
}
}
}
}

The OrderExistsFilterImpl class provides the basic setup for an action filter. It accepts IOrderRepository as a dependency and implements OnActionExecutionAsync. This implementation class is contained in an attribute class that implements TypeFilterAttribute.

After declaring the attribute class, we can proceed by implementing the logic. OrderExistsAttribute has three purposes:

  • To check whether the incoming request contains an id
  • To check whether the requested id is a Guid
  • To query IOrderRepository to check whether the entity exists

Let's proceed by describing a possible implementation of the previous logic:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using SampleAPI.Repositories;

namespace SampleAPI.Filters
{
public class OrderExistsAttribute : TypeFilterAttribute
{
public OrderExistsAttribute() : base(typeof(OrderExistsFilterImpl))
{
}

private class OrderExistsFilterImpl : IAsyncActionFilter
{
private readonly IOrderRepository _orderRepository;

public OrderExistsFilterImpl(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}

public async Task OnActionExecutionAsync(ActionExecutingContext
context, ActionExecutionDelegate next)
{
if (!context.ActionArguments.ContainsKey("id"))
{
context.Result = new BadRequestResult();
return;
}

if (!(context.ActionArguments["id"] is Guid id))
{
context.Result = new BadRequestResult();
return;
}

var result = _orderRepository.Get(id);

if (result == null)
{
context.Result =
new NotFoundObjectResult(
new {Message = $"Item with id {id} not exist."});
return;
}

await next();
}
}
}
}

First of all, the code checks whether our action arguments, which are populated by the model binder, contain any key by using the !context.ActionArguments.ContainsKey("id") statement. If the check is not true, the action filters interrupt the pipeline by adding a BadRequestResult to the response and exiting from the method. Secondly, the code checks whether the requested id is a Guid using !(context.ActionArguments["id"] is Guid id). In this case, if the condition fails, it returns a BadRequestResult and interrupts the pipeline. Finally, the action filter calls IOrderRepository and checks whether the requested entity exists. If the test is positive, it continues the pipeline by calling the await next(); method; otherwise, it returns a BadRequestResult.

In conclusion, we can add our attribute on top of methods that perform the actual checks and remove the previously replicated code that's inside each action method:

[Route("api/order")]
[ApiController]
public class OrderController : ControllerBase
{
...

[HttpGet("{id:guid}")]
[OrderExists]
public IActionResult GetById(Guid id) { ... }

[HttpPut("{id:guid}")]
[OrderExists]
public IActionResult Put(Guid id, UpdateOrderRequest request) { ... }

[HttpPatch("{id:guid}")]
[OrderExists]
public IActionResult Patch(Guid id, JsonPatchDocument<Order> requestOp)
{ ... }

[HttpDelete("{id:guid}")]
[OrderExists]
public IActionResult Delete(Guid id) { ... }

...
}

This kind of approach is compliant with the DRY principle. Furthermore, we can reuse the filter and handle the logic in a unique entry point.

Before ASP.NET Core 2.1, the same approach was used to check whether a model was valid. Instead of replicating the Model.IsValid check-in in each action, the logic was centralized in an action filter. With the introduction of the built-in ApiController attribute, the constraint has now become implicit.

Next, let's have a look at altering exceptions.

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

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