API Controllers
🎯 What You'll Learn
- [ApiController] attribute and its benefits
- Creating API controllers
- Action methods and return types
- Route attributes
- Returning data with ActionResult
- Complete CRUD implementation
What is an API Controller?
An API controller is a controller designed specifically for building Web APIs.
It uses the [ApiController] attribute for enhanced API-specific behaviors.
[ApiController] Attribute
The [ApiController] attribute enables several API-specific features:
- Automatic model validation: Returns 400 Bad Request for invalid models
- Binding source inference: Automatically infers [FromBody], [FromRoute], etc.
- Problem details: Returns RFC 7807 problem details for errors
- Attribute routing required: Conventional routing not supported
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
// Actions go here
}
API controllers inherit from ControllerBase (no view support).
MVC controllers inherit from Controller (includes view support).
Action Return Types
1. Specific Type
[HttpGet("{id}")]
public Product GetProduct(int id)
{
return new Product { Id = id, Name = "Laptop" };
}
// ✅ Simple and clean
// ❌ Can't return different status codes
2. IActionResult
[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
var product = /* find product */;
if (product == null)
return NotFound();
return Ok(product);
}
// ✅ Can return different status codes
// ❌ No compile-time type checking
3. ActionResult<T> (Recommended)
[HttpGet("{id}")]
public ActionResult<Product> GetProduct(int id)
{
var product = /* find product */;
if (product == null)
return NotFound();
return product; // Implicit conversion to ActionResult<Product>
}
// ✅ Can return different status codes
// ✅ Compile-time type checking
// ✅ Automatic OpenAPI documentation
Common Action Results
| Method | Status Code | When to Use |
|---|---|---|
Ok(value) |
200 | Successful GET, PUT, PATCH |
Created(uri, value) |
201 | Successful POST |
CreatedAtAction(...) |
201 | POST with generated Location header |
NoContent() |
204 | Successful DELETE, PUT (no body) |
BadRequest() |
400 | Invalid request |
NotFound() |
404 | Resource not found |
Conflict() |
409 | Resource conflict |
Complete CRUD Implementation
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
// GET: api/products
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
{
var products = await _productService.GetAllAsync();
return Ok(products);
}
// GET: api/products/5
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetProduct(int id)
{
var product = await _productService.GetByIdAsync(id);
if (product == null)
return NotFound();
return product;
}
// POST: api/products
[HttpPost]
public async Task<ActionResult<Product>> CreateProduct(CreateProductDto dto)
{
var product = await _productService.CreateAsync(dto);
return CreatedAtAction(
nameof(GetProduct),
new { id = product.Id },
product);
}
// PUT: api/products/5
[HttpPut("{id}")]
public async Task<IActionResult> UpdateProduct(int id, UpdateProductDto dto)
{
var exists = await _productService.ExistsAsync(id);
if (!exists)
return NotFound();
await _productService.UpdateAsync(id, dto);
return NoContent();
}
// DELETE: api/products/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProduct(int id)
{
var exists = await _productService.ExistsAsync(id);
if (!exists)
return NotFound();
await _productService.DeleteAsync(id);
return NoContent();
}
}
Query Parameters
// GET: api/products?category=electronics&page=1&pageSize=20
[HttpGet]
public async Task<ActionResult<PagedResult<Product>>> GetProducts(
[FromQuery] string? category = null,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
var result = await _productService.GetPagedAsync(category, page, pageSize);
return Ok(result);
}
Custom Routes
// GET: api/products/search?name=laptop
[HttpGet("search")]
public async Task<ActionResult<IEnumerable<Product>>> Search([FromQuery] string name)
{
var products = await _productService.SearchAsync(name);
return Ok(products);
}
// GET: api/products/low-stock
[HttpGet("low-stock")]
public async Task<ActionResult<IEnumerable<Product>>> GetLowStock()
{
var products = await _productService.GetLowStockAsync();
return Ok(products);
}
Async Best Practices
// ✅ Good: Async all the way
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetProduct(int id)
{
var product = await _productService.GetByIdAsync(id);
return product == null ? NotFound() : Ok(product);
}
// ❌ Bad: Blocking async call
[HttpGet("{id}")]
public ActionResult<Product> GetProduct(int id)
{
var product = _productService.GetByIdAsync(id).Result; // Deadlock risk!
return Ok(product);
}
Key Takeaways
- [ApiController]: Enables API-specific features
- ControllerBase: Base class for API controllers (no views)
- ActionResult<T>: Recommended return type (type safety + flexibility)
- HTTP attributes: [HttpGet], [HttpPost], [HttpPut], [HttpDelete]
- Status codes: Ok(200), Created(201), NoContent(204), NotFound(404)
- CreatedAtAction(): Returns 201 with Location header
- Async/await: Use async all the way
- Route attributes: Define custom routes
- Query parameters: Use [FromQuery] for filtering/pagination
You now understand API controllers! In the next section, we'll explore Model Binding—how ASP.NET Core automatically maps request data to action method parameters.