Skip to main content
CodeOath
← All posts

.NET Core / Web API71 min total · 19 parts

Building REST APIs with ASP.NET Core: Routing, Middleware, and Dependency Injection

Part 5 of 19 · ~1 min

Controllers, [ApiController], and ControllerBase

ReservationsController above inherits from ControllerBase — not Controller, which pulls in view support for server-rendered pages that Bench, a pure JSON API, has no use for. ControllerBase is where Ok(), NotFound(), CreatedAtAction(), and access to HttpContext, User, and ModelState all come from.

[ApiController] is the other line doing real work on that class, and it's why ReservationsController above is shorter than it looks like it should be. It switches on four things at once:

  • Automatic model validation — a CreateReservationRequest that fails a data-annotation rule (below) never reaches Create's body at all; the framework hands back a 400 before your code runs.
  • Inferred binding sources — toolId and id come from the route, request from the body, with no [FromRoute]/[FromBody] spelled out anywhere (the exact inference rule is next).
  • Attribute routing is mandatory — Bench has no convention-based routing hiding in the background; every endpoint carries an attribute you can actually search for in the codebase.
  • Automatic ProblemDetails responses — a validation failure comes back as structured application/problem+json, not a bare 400 with nothing explaining what went wrong.

Drop [ApiController] from a controller and none of the above is a compile error — it's just gone, silently, and you're back to writing the boilerplate it was doing for you. Theo actually did this once, copy-pasting a controller into a quick internal admin tool and forgetting the attribute; the giveaway wasn't a crash, it was validation that stopped firing and a [FromBody] that suddenly needed to be spelled out by hand.