In this article, we will learn about the ApiController attribute and the features it brings to our Web API controllers.
Let’s get started.
Why We Should Use ApiController Attribute
By annotating our API controllers with ApiController
, we get features and behaviors that are focused on improving developer experience by reducing the amount of boilerplate code in our controllers.
These features include the requirement for attribute routing, automatic model validation, and new binding parameter inference rules.
How to Use ApiController Attribute?
We can apply the ApiController
attribute directly to individual controllers:
[ApiController] [Route("[controller]")] public class CustomersController : ControllerBase
We can also apply it to a custom controller base class. Then, all its subclasses will inherit ApiController
behavior.
Lastly, we can apply the ApiController
attribute at the assembly level by adding it to any project file, usually in Program.cs
:
using Microsoft.AspNetCore.Mvc; [assembly: ApiController]
Attribute Routing Requirement
Once we add the ApiController
attribute to our Web API controllers, routes defined as conventional routes won’t be able to reach our actions. We must use attribute routing instead.
Automatic Model Validation and HTTP 400 Responses
ApiController
automatically checks the model state and returns HTTP 400
responses in case of model validation errors. Therefore, we don’t have to check ModelState.IsValid
explicitly in our actions:
if (!ModelState.IsValid) // We don't need to do this check { return BadRequest(ModelState); }
Furthermore, ApiController
introduces the ValidationProblem
format for HTTP 400
responses. It is a machine-readable RFC 7807 compliant format:
{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "|7fb5e16a-4c8f23bbfc974667.", "errors": { "Name": [ "The Name field is required." ] } }
When we perform custom validations, we can use ValidationProblem()
instead of BadRequest()
to keep all our HTTP 400
responses consistent:
[HttpGet("{id}")] public IActionResult GetCustomersById(int id) { var customer = new Customer(); if (!customer.IsActive) return ValidationProblem(); // Do not use BadRequest() return Ok(customer); }
Invalid Model Response Customization
We can fully customize our validation problem responses as well. To do that, we provide our own implementation of the InvalidModelStateResponseFactory
delegate in the ConfigureApiBehaviorOptions()
extension method:
builder.Services.AddControllers() .ConfigureApiBehaviorOptions(options => { options.InvalidModelStateResponseFactory = context => { // Build your custom bad request response here }; });
Let’s imagine that we want to replace the default validation problem response format. Specifically, we want our API users to be able to see the request path and method, and exactly which controller action handled it:
builder.Services.AddControllers() .ConfigureApiBehaviorOptions(opt => { opt.InvalidModelStateResponseFactory = context => { var responseObj = new { path = context.HttpContext.Request.Path.ToString(), method = context.HttpContext.Request.Method, controller = (context.ActionDescriptor as ControllerActionDescriptor)?.ControllerName, action = (context.ActionDescriptor as ControllerActionDescriptor)?.ActionName, errors = context.ModelState.Keys.Select(k => { return new { field = k, Messages = context.ModelState[k]?.Errors.Select(e => e.ErrorMessage) }; }) }; return new BadRequestObjectResult(responseObj); }; });
Here, we create a new delegate to replace the default InvalidModelStateResponseFactory
. It receives an ActionContext
(context) parameter from which we get the data we need to include in the response.
The new delegate must return an implementation of IActionResult
. In our case, an instance of BadRequestObjectResult
:
{ "path": "/customers", "method": "POST", "controller": "Customers", "action": "PostCustomer", "errors": [{ "field": "Name", "messages": ["The Name field is required."] }] }
We can even disable the automatic responses altogether by setting the SuppressModelStateInvalidFilter
property of the ApiBehaviorOptions
object to false
:
builder.Services.AddControllers() .ConfigureApiBehaviorOptions(options => { options.SuppressModelStateInvalidFilter = true; });
Source Binding Inference Rules
ApiController
will try to infer the source of some action parameters without us having to use binding attributes like [FromBody]
or [FromQuery]
. For this purpose, ApiController
applies a set of inference rules:
Binding Attribute | Inference Rule |
---|---|
[FromBody] | Inferred for any complex type |
[FromForm] | Inferred for any parameters of type IFormFile or IFormFileCollection |
[FromRoute] | Inferred for any action parameter whose name matches a parameter in the route template |
[FromQuery] | Inferred for all the other action parameters |
[FromHeader] | No rule for header binding |
Additionally, we can disable all the rules by setting the SuppressInferBindingSourcesForParameters
option to true
.
builder.Services.AddControllers() .ConfigureApiBehaviorOptions(options => { options.SuppressInferBindingSourcesForParameters = true; });
Conclusion
In this article, we’ve learned what the ApiController
attribute does for us how to use it to add common behavior to our Web API controllers.
We’ve learned about the automatic HTTP 400
responses generated by the ModelStateInvalidFilter
and how to customize them.
Finally, we’ve reviewed ApiController
‘s source binding inference rules and how they save us from explicitly using binding attributes.