In this article, we will learn about the ApiController attribute and the features it brings to our Web API controllers.

To download the source code for this article, you can visit our GitHub repository.

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.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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 AttributeInference 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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!