Section 2 of 7

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
Basic API Controller C#
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    // Actions go here
}
ℹ️ ControllerBase vs Controller

API controllers inherit from ControllerBase (no view support). MVC controllers inherit from Controller (includes view support).

Action Return Types

1. Specific Type

Return Specific Type C#
[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

Return IActionResult C#
[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)

Return ActionResult<T> C#
[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

Controllers/ProductsController.cs C#
[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

Filtering and Pagination C#
// 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

Custom Action Routes C#
// 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

Async/Await Pattern C#
// ✅ 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
🎯 Next Steps

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.