Returning ActionResult<T>

ActionResult<T> lets an action return either its data or a status code result, such as NotFound(). Return the item to answer 200 with it, or a helper such as NotFound(), BadRequest() or Conflict() to answer with that status instead.

Journeyman · ASP.NET Core · REST API · Updated

After this lesson you can

  • Return ActionResult<T> from an action, choosing between the item and a status such as NotFound()
  • Choose the ControllerBase helper for a status code, such as Ok, CreatedAtAction, NoContent, BadRequest, NotFound or Conflict

Key takeaways

  • ActionResult<Category> lets an action return either a Category, answered with 200 OK, or a status result such as NotFound().
  • Every action that returns one item should answer 404 Not Found when there's no such item.
  • ControllerBase has a helper for each common answer: Ok, CreatedAtAction, NoContent, BadRequest, NotFound and Conflict, plus StatusCode for any other code.
  • Use ActionResult<T> when the action sends data back, and IActionResult when it only ever sends a status.
  • Don't wrap every action in try and catch to return 500. An action handles only what it expects, such as a missing item.

GetById still has the problem from HTTP Status Codes: an overview: a missing category answers 204 No Content, when it should be 404 Not Found. The action needs to return either a category or a "not found". That's what ActionResult<T> is for.

Either the item, or a status

[HttpGet("{id}")]
public ActionResult<Category> GetById(int id)
{
    Category? category = categoryDao.GetById(id);
    if (category == null)
    {
        return NotFound();
    }
    return category;
}

ActionResult<Category> accepts two kinds of return:

  • A Category: ASP.NET Core answers 200 OK with it as JSON, exactly as before.
  • A status code result, such as NotFound(): ASP.NET Core answers with that status.
GET {{HostAddress}}/api/categories/300
HTTP/1.1 404 Not Found
Content-Type: application/problem+json; charset=utf-8

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404
}

A proper 404, with a problem details body the client can read. Every action that returns one item should work this way.

The status helpers

ControllerBase has a method for each common answer:

Helper Status Use it for
Ok(item) (or just return item;) 200 OK The data the client asked for, or the item a PUT just updated
CreatedAtAction(nameof(GetById), new { id }, item) 201 Created A new item, with its address in Location
NoContent() 204 No Content Success with nothing to send back: a DELETE
BadRequest("message") 400 Bad Request A request your own code finds wrong, beyond the annotations
NotFound() 404 Not Found No item at that address
Conflict("message") 409 Conflict The request clashes with the data, such as a name that's already taken
StatusCode(503) Any code Anything without its own helper

For example, Northwind's category names should be unique. No annotation can check that, so the action does, and answers 409 Conflict:

[HttpPost]
public ActionResult<Category> Add(Category category)
{
    if (categoryDao.GetAll().Any(c => c.CategoryName == category.CategoryName))
    {
        return Conflict(quot;There's already a category named {category.CategoryName}.");
    }

    Category added = categoryDao.Add(category);
    return CreatedAtAction(nameof(GetById), new { id = added.CategoryId }, added);
}

ActionResult<T> or IActionResult?

You've now seen both:

Return type When Example
ActionResult<Category> The action sends data back, but sometimes a status instead GetById, Add, Update
IActionResult The action only ever sends a status Delete
List<Category>, Category The action always succeeds with data GetAll

Prefer ActionResult<T> whenever there's data: the T tells readers, and tools that read your API's description at /openapi/v1.json, exactly what a successful answer contains.

What about 500?

You could wrap every action in a try and catch (Exception) and return StatusCode(500). Don't: that's the same few lines in every action of every controller, and it's easy to forget one. Errors you don't expect are better handled once, for the whole API, as Handling Global Exceptions in ASP.NET Core shows. An action should only handle what it expects, such as a missing item.

Check yourself

2 questions to try on the lesson page.

So what

ActionResult<T> lets each action tell the whole truth: the data when there is some, and the right status when there isn't. Pick the helper that says what happened, and clients can trust your status codes.

Key terms

Cheat sheets