ApiController (Attribute)
What is [ApiController]?
[ApiController] is an attribute you apply to controller classes to enable API-specific behaviors and conventions in ASP.NET Core. Think of it as “API mode” for your controllers - it automatically handles many common API tasks that you’d otherwise have to code manually.
Here’s a typical use:
[Route("api/[controller]")]
[ApiController] // This attribute right here
public class ProductsController : ControllerBase
{
// Your API actions
}
That single attribute gives you a collection of automatic behaviors that make building APIs easier, cleaner, and more consistent.
What Does It Do?
When you decorate a controller with [ApiController], you get these automatic behaviors:
1. Automatic HTTP 400 Responses
Without [ApiController]:
You have to manually check if the model state is valid and return appropriate errors:
public class ProductsController : ControllerBase
{
[HttpPost]
public ActionResult<Product> CreateProduct(Product product)
{
// You must manually check this
if (!ModelState.IsValid)
{
return BadRequest(ModelState); // Manual error handling
}
// Your actual logic
return Ok(product);
}
}
With [ApiController]:
Model validation happens automatically - no manual checks needed:
[ApiController]
public class ProductsController : ControllerBase
{
[HttpPost]
public ActionResult<Product> CreateProduct(Product product)
{
// ModelState is automatically checked
// If invalid, 400 BadRequest is returned automatically
// You never get here with invalid data
// Your actual logic - assume product is valid
return Ok(product);
}
}
If a client sends invalid data, they automatically get a 400 response with details about what went wrong:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Name": [
"The Name field is required."
],
"Price": [
"The field Price must be between 0 and 10000."
]
}
}
2. Binding Source Parameter Inference
Without [ApiController]:
You must explicitly tell ASP.NET Core where each parameter comes from:
public class ProductsController : ControllerBase
{
[HttpGet("{id}")]
public ActionResult<Product> GetProduct([FromRoute] Guid id) // Must specify
{
// ...
}
[HttpPost]
public ActionResult<Product> CreateProduct([FromBody] Product product) // Must specify
{
// ...
}
[HttpGet]
public ActionResult<IEnumerable<Product>> Search([FromQuery] string name) // Must specify
{
// ...
}
}
With [ApiController]:
The attribute infers where parameters come from based on their type and position:
[ApiController]
public class ProductsController : ControllerBase
{
[HttpGet("{id}")]
public ActionResult<Product> GetProduct(Guid id) // Automatically [FromRoute]
{
// ...
}
[HttpPost]
public ActionResult<Product> CreateProduct(Product product) // Automatically [FromBody]
{
// ...
}
[HttpGet]
public ActionResult<IEnumerable<Product>> Search(string name) // Automatically [FromQuery]
{
// ...
}
}
The inference rules:
- Complex types (classes, objects) →
[FromBody] - Simple types (string, int, Guid, etc.) in route template →
[FromRoute] - Simple types not in route template →
[FromQuery] - IFormFile and IFormFileCollection →
[FromForm]
3. Multipart/Form-Data Request Inference
When you use IFormFile or IFormFileCollection, the attribute automatically infers [FromForm]:
[ApiController]
public class FilesController : ControllerBase
{
[HttpPost("upload")]
public ActionResult UploadFile(IFormFile file) // Automatically [FromForm]
{
// Handle file upload
return Ok();
}
}
4. Problem Details for Error Status Codes
When you return error status codes (4xx and 5xx), the response is automatically wrapped in a standardized problem details format (RFC 7807):
[ApiController]
public class ProductsController : ControllerBase
{
[HttpGet("{id}")]
public ActionResult<Product> GetProduct(Guid id)
{
var product = _repository.GetById(id);
if (product == null)
{
return NotFound(); // Returns standardized problem details
}
return Ok(product);
}
}
Response for NotFound():
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
"title": "Not Found",
"status": 404,
"traceId": "00-abc123..."
}
Why Use [ApiController]?
1. Less Boilerplate Code
Compare these two approaches for a simple POST action:
Without [ApiController] (manual everything):
[Route("api/products")]
public class ProductsController : ControllerBase
{
[HttpPost]
public ActionResult<Product> CreateProduct([FromBody] Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (product == null)
{
return BadRequest("Product cannot be null");
}
// Actual logic
_repository.Add(product);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
}
With [ApiController] (automatic behaviors):
[Route("api/products")]
[ApiController]
public class ProductsController : ControllerBase
{
[HttpPost]
public ActionResult<Product> CreateProduct(Product product)
{
// Validation already happened, product is guaranteed valid
// Just write your business logic
_repository.Add(product);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
}
2. Consistent Error Responses
All validation errors follow the same format across your entire API - no need to manually format error responses.
3. Convention Over Configuration
The attribute embraces conventions - simple types in routes come from the route, complex types come from the body. You write less code and the behavior is predictable.
Requirements for [ApiController]
To use [ApiController], your controller must:
1. Inherit from ControllerBase
// ✅ Good - inherits from ControllerBase
[ApiController]
public class ProductsController : ControllerBase
{
// ...
}
// ❌ Bad - doesn't inherit from ControllerBase
[ApiController]
public class ProductsController
{
// Won't work
}
2. Have a Route Attribute
You must specify a route on the controller or action:
// ✅ Good - has route attribute
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
// ...
}
// ❌ Bad - no route attribute
[ApiController]
public class ProductsController : ControllerBase
{
// Won't work properly
}
Applying to Multiple Controllers
You can apply [ApiController] to individual controllers, or to a base class that all your API controllers inherit from:
Individual Controllers
[Route("api/products")]
[ApiController]
public class ProductsController : ControllerBase { }
[Route("api/orders")]
[ApiController]
public class OrdersController : ControllerBase { }
[Route("api/customers")]
[ApiController]
public class CustomersController : ControllerBase { }
Base Controller
[ApiController]
public class ApiControllerBase : ControllerBase
{
// Common functionality for all API controllers
}
[Route("api/products")]
public class ProductsController : ApiControllerBase { }
[Route("api/orders")]
public class OrdersController : ApiControllerBase { }
[Route("api/customers")]
public class CustomersController : ApiControllerBase { }
Assembly-Level
Apply to all controllers in the assembly:
// In Program.cs or a separate file
[assembly: ApiController]
// Now all controllers automatically get the behavior
[Route("api/products")]
public class ProductsController : ControllerBase { }
Customizing Behavior
You can customize how [ApiController] behaves:
Custom Validation Response
In Program.cs, configure how validation errors are formatted:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var errors = context.ModelState
.Where(e => e.Value.Errors.Count > 0)
.Select(e => new
{
Name = e.Key,
Message = e.Value.Errors.First().ErrorMessage
})
.ToArray();
return new BadRequestObjectResult(new
{
Message = "Validation failed",
Errors = errors
});
};
});
Now your validation errors look like:
{
"message": "Validation failed",
"errors": [
{
"name": "Name",
"message": "The Name field is required."
},
{
"name": "Price",
"message": "The field Price must be between 0 and 10000."
}
]
}
Disable Automatic 400 Responses
If you want to manually handle validation:
builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
Now you’re back to manually checking ModelState.IsValid.
Disable Binding Source Inference
If you want to explicitly specify [FromBody], [FromRoute], etc.:
builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressInferBindingSourcesForParameters = true;
});
Real-World Example
Here’s a complete controller showing [ApiController] in action:
using Microsoft.AspNetCore.Mvc;
namespace ProductAPI.Controllers;
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
private readonly IProductRepository _repository;
private readonly ILogger<ProductsController> _logger;
public ProductsController(
IProductRepository repository,
ILogger<ProductsController> logger)
{
_repository = repository;
_logger = logger;
}
// GET: api/products
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetAll()
{
var products = await _repository.GetAllAsync();
return Ok(products);
}
// GET: api/products/{id}
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetById(Guid id) // id automatically from route
{
var product = await _repository.GetByIdAsync(id);
if (product == null)
{
return NotFound(); // Automatic problem details response
}
return Ok(product);
}
// POST: api/products
[HttpPost]
public async Task<ActionResult<Product>> Create(Product product) // product automatically from body
{
// No need to check ModelState - already validated automatically
// If validation failed, this code never runs
await _repository.AddAsync(product);
return CreatedAtAction(
nameof(GetById),
new { id = product.Id },
product);
}
// PUT: api/products/{id}
[HttpPut("{id}")]
public async Task<ActionResult> Update(Guid id, Product product)
{
// id from route, product from body - both automatic
if (id != product.Id)
{
return BadRequest("ID mismatch");
}
var exists = await _repository.ExistsAsync(id);
if (!exists)
{
return NotFound();
}
await _repository.UpdateAsync(product);
return NoContent();
}
// DELETE: api/products/{id}
[HttpDelete("{id}")]
public async Task<ActionResult> Delete(Guid id)
{
var product = await _repository.GetByIdAsync(id);
if (product == null)
{
return NotFound();
}
await _repository.DeleteAsync(id);
return NoContent();
}
// GET: api/products/search?name=laptop&minPrice=100
[HttpGet("search")]
public async Task<ActionResult<IEnumerable<Product>>> Search(
string name, // Automatically from query string
decimal? minPrice // Automatically from query string
)
{
var products = await _repository.SearchAsync(name, minPrice);
return Ok(products);
}
}
Notice how clean this is:
- No manual
ModelState.IsValidchecks - No
[FromRoute],[FromBody],[FromQuery]attributes needed - Automatic problem details for errors
- All validation happens before your code runs
When NOT to Use [ApiController]
There are rare cases where you might not want it:
1. MVC Controllers (Views)
If you’re returning views (HTML), don’t use [ApiController]:
// This is an MVC controller, not an API controller
public class HomeController : Controller
{
public IActionResult Index()
{
return View(); // Returns HTML
}
}
2. Need Manual Control
If you need complete control over validation and error responses:
public class CustomController : ControllerBase
{
[HttpPost]
public ActionResult Create(Product product)
{
// Custom validation logic
if (!IsValid(product))
{
return BadRequest(new CustomErrorFormat
{
// Your specific error format
});
}
// ...
}
}
But honestly, these cases are rare. 99% of the time, [ApiController] is exactly what you want.
Wrap Up
The [ApiController] attribute is one of those features that just makes sense - it automates the boring, repetitive parts of building APIs while giving you a consistent, predictable behavior.
Key benefits:
- Automatic model validation - no manual
ModelState.IsValidchecks - Automatic parameter binding - infers
[FromBody],[FromRoute],[FromQuery] - Standardized error responses - consistent problem details format
- Less boilerplate - more time writing business logic, less time on plumbing
When building ASP.NET Core APIs, just use [ApiController] on all your controllers. It’s a modern best practice and makes your code cleaner and more maintainable. The behaviors it provides are exactly what you’d want anyway - it just does them automatically.