Everyone hates copy/pasting code, and Action Filters in ASP.NET Core MVC offer a great way to avoid filling your controllers with boilerplate.
Filters offer you entry points into the execution pipeline for an action where you can examine the incoming parameters or generated results and modify them to suit your needs.
Here are a couple of examples of how this can help.
Treat a null result as a 404 Not Found
By default an ASP.NET Core controller will return a 204 No Content
response if you return null
from an action:
[Route("api/example")]
public class ExampleApiController : Controller
{
[HttpGet("")]
public string GetExample()
{
return null;
}
}
In some cases, however, you might not want to treat null
as No Content
. If your API is looking up a resource by ID, for example, then a 404 Not Found
response would be more useful:
[HttpGet("{id}")]
public MyDtoObject GetById(int id)
{
if (!_store.ContainsId(id))
return null; //should return 404
//...
}
We can use an action filter to automatically replace the null
result with a NotFoundResult
:
//filter
public class NullAsNotFoundAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext context)
{
var objectResult = context.Result as ObjectResult;
if (objectResult?.Value == null)
context.Result = new NotFoundResult();
}
}
//controller
[HttpGet("{id}")]
[NullAsNotFound]
public MyDtoObject GetById(int id)
{
//...
}
Here we override OnActionExecuted
to invoke our code after the action method has generated a result but before that result is processed.
If the generated result is an ObjectResult
with a null value then we replace it with an empty NotFoundResult
and our controller will now return a 404 response.
Treat invalid models as a 400 Bad Request
It is very common to see the following pattern in MVC controllers:
[HttpPost("")]
public IActionResult Create(MyModel model)
{
if (!ModelState.IsValid)
return new BadRequestObjectResult(ModelState); //or a View, or other validation behaviour
//...eventually return created model
return Ok(model);
}
This has 2 downsides:
- Boilerplate code in every action that needs to validate
- Return type must be
IActionResult
to accomodate 2 result types
The second point is fairly minor but worth noting. By exposing IActionResult
instead of the concrete type we lose metadata about the action.
That metadata is useful for things like generating swagger docs, and losing it can mean you need to decorate the method with response types (though this is improved in ASP.NET Core 2.1 with IActionResult
).
In any case, we can make this behaviour generic by moving the validation check into another action filter:
public class InvalidModelAsBadRequestAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
This time we are overriding OnActionExecuting
instead of OnActionExecuted
so our code gets run before the controller action. We can tell if the model is invalid before hitting our controller so we can skip the action entirely if we know it should be replaced with a 400.
Other Possibilities
Wherever you find yourself writing duplicate code in many actions it is worth considering whether it can be pulled out into a filter (or middleware) to keep your controllers clean and focussed on their intent.