What is a controller?

A controller is a class that handles HTTP requests. While minimal APIs (the paradigm we've used up to this point) are great, they have limitations.

  • They're less flexible than their controller counterparts.
  • As the project grows in size, minimal APIs tend to be a bit more "boilerplate"-y and harder to organize. (Controllers, on the other hand, are intended to be an organizational tool by their nature.)
  • In the wild, they're far less common than controllers, which have been around longer.
  • Dependency management on controllers is cleaner (IMO)

You don't want minimal APIs that look like this:

root.MapPut("/minimal/customers/{id}", 
            async (int id, 
                   UpdateCustomerRequest request, 
                   MyAppContext db,
                   HttpContext context,
                   IEmailService emailService,
                   ISmsService smsService,
                   IPaymentService paymentService,
                   IWhateverService whateverService,
                   IDontKnowIfThisIsEnoughServicesService ughService,
                   [FromQuery] string someOtherData) =>
            {
            //that's a lot of services...

🌶️🌶️🌶️ Minimal APIs make it very easy to get started in ASP.NET Core, and that's great! That said, I vastly prefer controllers for apps of any decent size. My rule of thumb is usually: if I think a solution will be bigger than 10 or so endpoints, I skip minimal APIs and jump straight to controllers.

What does a controller look like?

A controller is a class that inherits from Controller. (You can also inherit from ControllerBase, but Controller gives you some extra goodies.)

[ApiController]
[Route("[controller]")]
public class CustomersController : Controller
{
    [HttpGet]
    public IActionResult GetCustomers()
    {
        return Ok(_repo.GetAll());
    }

    [HttpGet("{id}")]
    public IActionResult GetCustomerById(int id)
    {
        var customer = _repo.GetById(id);
        if (customer == null)
        {
            return NotFound();
        }
        return Ok(customer);
    }
}

Let's break it down one line at a time.

[ApiController]

While optional, this attribute adds special behaviors to the API controllers:

  • Requires all endpoints to use attribute-based routing. Basically that means [HttpGet], [HttpPost], etc are required.
  • Returns a ProblemDetails object for all 4xx/5xx response codes.
  • Automatically returns ValidationProblemDetails for 400 Bad Request responses.
  • Automatic validation for invalid objects. (DOES NOT APPLY when using FluentValidation - more on that later.)
  • Does some basic inference on parameters to your controller methods, which means you don't have to use [FromQuery], [FromRoute], [FromBody], etc. as much. (🌶️🌶️🌶️ Spencer always recommends using binding attributes anyways, as it makes things clearer to you and your team.)

You can read more in depth at the Microsoft docs.

[Route("[controller]")]

This attribute is how ASP.NET knows how to route requests to your controller. The [controller] token is replaced with the controller name (without the word Controller). In this case, the controller name is Customers, so the route will be /customers.

public class CustomersController : Controller

This is the base class for your controller. It gives your controller access to the Controller class, which has a bunch of useful properties and methods.

🌶️🌶️🌶️ I always always always define my own controller base class and inherit from that. It gives you the ability to add a bunch of useful and reusable methods/properties to each of your controllers.

[HttpGet]
public IActionResult GetCustomers()

The HttpGet attribute marks the GetCustomers method as an HTTP GET endpoint.

This is the method that will be called when a GET request is made to /customers. The IActionResult return type is akin to the Results object we've been using in minimal APIs. Note however that we don't need to qualify our Ok calls with Results - it's because the Controller base class has that as a method.

🌶️🌶️🌶️ IActionResult is your friend. It's a very flexible return type that can be used to return a wide variety of responses.

[HttpGet("{id}")]

This attribute marks the GetCustomerById method as an HTTP GET endpoint. The {id} token is a route parameter.

public IActionResult GetCustomerById(int id)

This is the method that will be called when a GET request is made to /customers/1. The int id parameter is inferred to come from the route.

How do you make ASP.NET Core use your controller?

Controllers are read from your assembly by default. You still need to add the middleware call to get the app to use them and add controllers to the DI container:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();

var app = builder.Build();

app.MapControllers();

app.Run();

MapControllers() adds the necessary middleware to the pipeline to use the controllers.