In ASP.NET Core Web API, deciding how your Controller Actions return data strongly impacts how standard HTTP Status codes are sent to your frontend. Furthermore, choosing the correct return type allows Swagger (OpenAPI) to automatically map your API schema for other developers.
The simplest way to return data is to simply return the raw C# object. ASP.NET Core will automatically serialize the object into JSON and attach an HTTP 200 (OK) status code.
[HttpGet("{id}")]
public Product GetProduct(int id)
{
var product = _db.Products.Find(id);
if (product == null) {
// ERROR: We cannot return "NotFound(404)".
// We are strictly bound to returning a 'Product' object!
return null; // Returns HTTP 204 (No Content), which is semantically incorrect.
}
return product;
}
Using IActionResult allows you to return varying HTTP status codes depending on the business logic execution (e.g., returning a 404 Not Found, 400 Bad Request, or 200 OK).
[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
var product = _db.Products.Find(id);
if (product == null) {
return NotFound(); // Returns HTTP 404
}
return Ok(product); // Returns HTTP 200 populated with JSON
}
However, IActionResult has a downside: Swagger cannot inspect this method at compile-time to know what type of data is wrapped inside the Ok(). Swagger will just document this endpoint as "Returns void", which breaks frontend code generators.
Introduced in .NET Core 2.1, ActionResult<T> gives us the best of both worlds. We retain the ability to return multiple HTTP status codes, AND Swagger can automatically infer the T type to generate perfect OpenAPI documentation.
[HttpGet("{id}")]
public ActionResult<Product> GetProductSafe(int id)
{
var product = _db.Products.Find(id);
if (product == null) {
return NotFound(); // Implicitly converted to ActionResult<T>
}
// We don't even need to wrap it in Ok().
// Just returning the generic type implicitly wraps it in an Ok() !
return product;
}
| Method | Status Code | Typical Use Case |
|---|---|---|
Ok(data) | 200 OK | A GET request successfully found the data. |
CreatedAtRoute() | 201 Created | A POST request successfully created a new resource (must include a Location URI). |
NoContent() | 204 No Content | A PUT/DELETE request succeeded, but there is no payload to send back. |
BadRequest(modelState) | 400 Bad Request | The user sent invalid JSON payload or failed model validation. |
Unauthorized() | 401 Unauthorized | The user lacks a valid JWT Token. |
Forbid() | 403 Forbidden | The user has a token, but lacks the specific 'Admin' Role to perform the action. |
NotFound() | 404 Not Found | The requested Database ID does not exist. |
Q: "When would you explicitly use [ProducesResponseType] attributes instead of relying purely on ActionResult<T>?"
Architect Answer: "While ActionResult<T> handles the Happy Path (HTTP 200) documentation for Swagger perfectly, Swagger still does not know what other HTTP status codes the endpoint might throw under failure conditions. By adding [ProducesResponseType(StatusCodes.Status200OK)] alongside [ProducesResponseType(StatusCodes.Status404NotFound)], you explicitly document for frontend developers exactly what errors to handle. This is the cornerstone of generating robust TypeScript proxy clients from OpenAPI specifications."