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 aCategory, answered with200 OK, or a status result such asNotFound().- Every action that returns one item should answer
404 Not Foundwhen there's no such item. ControllerBasehas a helper for each common answer:Ok,CreatedAtAction,NoContent,BadRequest,NotFoundandConflict, plusStatusCodefor any other code.- Use
ActionResult<T>when the action sends data back, andIActionResultwhen it only ever sends a status. - Don't wrap every action in
tryandcatchto return500. 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 answers200 OKwith 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
- ActionResult<T>
- IActionResult
- 409 Conflict