Controllers are the absolute core of your Web API routing logic. They act as the "air traffic controllers" for incoming HTTP requests. They intercept the raw network data, bind it to C# objects, invoke business logic via dependency injection, and format the results back into HTTP status codes and JSON.
If you've previously built ASP.NET MVC applications, your classes inherited from Controller. In Web API, we inherit from ControllerBase.
class : Controller (MVC)Contains all the logic needed to return UI View() results, interact with Razor pages, and render HTML. Highly bloated for pure data APIs.
class : ControllerBase (Web API)A lightweight base class specifically optimized for REST APIs. It provides only the helper methods required to return raw data and HTTP status codes (e.g., Ok(), NotFound(), BadRequest()).
Placing the [ApiController] attribute at the top of a controller is not strictly required, but it is highly recommended as it activates several essential quality-of-life developer features automatically.
using Microsoft.AspNetCore.Mvc;
namespace MyApp.Api.Controllers
{
[ApiController] // Extraframework magic starts here
[Route("api/[controller]")]
public class EmployeesController : ControllerBase
{
// action methods...
}
}
if (!ModelState.IsValid) return BadRequest(); at the top of every method anymore. It automatically validates incoming data annotations and instantly returns a 400 JSON response if validation fails.[FromBody] without you needing to explicitly type the attribute.A controller's only job is handling HTTP semantics. It should never contain complex business logic or EF Core DbContext database queries directly. Instead, it relies entirely on Constructor Dependency Injection.
[ApiController]
[Route("api/v1/billing")]
public class BillingController : ControllerBase
{
// The controller only knows the interface, not the Implementation!
private readonly IInvoiceService _invoiceService;
private readonly ILogger<BillingController> _logger;
// ASP.NET Core's DI system automatically supplies these instances
public BillingController(IInvoiceService invoiceService, ILogger<BillingController> logger)
{
_invoiceService = invoiceService;
_logger = logger;
}
[HttpPost("generate/{customerId}")]
public async Task<IActionResult> GenerateInvoice(int customerId)
{
_logger.LogInformation($"Begin generating invoice for Client ID {customerId}");
// Delegate heavy lifting to the service layer completely.
var invoiceDto = await _invoiceService.ComputeMonthlyInvoiceAsync(customerId);
if (invoiceDto == null) {
return NotFound(new { error = "Customer not found in database." });
}
// Return a 200 HTTP Status Code attached to the JSON payload
return Ok(invoiceDto);
}
}
Q: "Why should we use Dependency Injection to pass Service interfaces into a Controller instead of just instantiating them with new InvoiceService()?"
Architect Answer: "Tight coupling and testability. If we use the keyword new inside a controller, that controller is permanently glued to a specific infrastructure implementation. If InvoiceService talks to a SQL Database, our Controller now implicitly requires a physical SQL database to be tested. By passing an interface via Constructor Injection, we invert the dependency. During Unit Testing, we can pass a mock framework (like Moq) FakeInvoiceService into the controller, allowing us to perfectly test the Controller's HTTP logic in isolated memory without touching a single database or network socket."