IActionResult, and returning a status without inventing a shape

A PHP controller returns whatever the framework will serialise and the status code is set separately, which is why every codebase grows its own response helper. The return type here carries both.

[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
    var order = await _orders.FindAsync(id);

    if (order == null)
    {
        return NotFound();                    // 404, no body
    }

    if (!await _auth.CanRead(User, order))
    {
        return Forbid();                      // 403
    }

    return Ok(new OrderResource(order));      // 200 + serialised
}

NotFound(), BadRequest(), Created() and the rest are methods on the base controller, so the status and the body are one expression and the compiler knows the return type. Model validation is separate again: [ApiController] in 2.1 makes a failed binding return a 400 with a problem-details body automatically, which removes the guard clause everybody writes. The trade is that the automatic response has a shape you did not choose, and overriding it is a configuration lambda rather than a filter.