Controller action return types in ASP.NET Core Web API

By Scott Addie

View or download sample code (how to download)

ASP.NET Core offers the following options for Web API controller action return types:

This document explains when it's most appropriate to use each return type.

Specific type

The simplest action returns a primitive or complex data type (for example, string or a custom object type). Consider the following action, which returns a collection of custom Product objects:

[HttpGet]
public IEnumerable<Product> Get()
{
    return _repository.GetProducts();
}

Without known conditions to safeguard against during action execution, returning a specific type could suffice. The preceding action accepts no parameters, so parameter constraints validation isn't needed.

When known conditions need to be accounted for in an action, multiple return paths are introduced. In such a case, it's common to mix an ActionResult return type with the primitive or complex return type. Either IActionResult or ActionResult<T> are necessary to accommodate this type of action.

IActionResult type

The IActionResult return type is appropriate when multiple ActionResult return types are possible in an action. The ActionResult types represent various HTTP status codes. Some common return types falling into this category are BadRequestResult (400), NotFoundResult (404), and OkObjectResult (200).

Because there are multiple return types and paths in the action, liberal use of the [ProducesResponseType] attribute is necessary. This attribute produces more descriptive response details for API help pages generated by tools like Swagger. [ProducesResponseType] indicates the known types and HTTP status codes to be returned by the action.

Synchronous action

Consider the following synchronous action in which there are two possible return types:

[HttpGet("{id}")]
[ProducesResponseType(200, Type = typeof(Product))]
[ProducesResponseType(404)]
public IActionResult GetById(int id)
{
    if (!_repository.TryGetProduct(id, out var product))
    {
        return NotFound();
    }

    return Ok(product);
}

In the preceding action, a 404 status code is returned when the product represented by id doesn't exist in the underlying data store. The NotFound helper method is invoked as a shortcut to return new NotFoundResult();. If the product does exist, a Product object representing the payload is returned with a 200 status code. The Ok helper method is invoked as the shorthand form of return new OkObjectResult(product);.

Asynchronous action

Consider the following asynchronous action in which there are two possible return types:

[HttpPost]
[ProducesResponseType(201, Type = typeof(Product))]
[ProducesResponseType(400)]
public async Task<IActionResult> CreateAsync([FromBody] Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    await _repository.AddProductAsync(product);

    return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}

In the preceding action, a 400 status code is returned when model validation fails and the BadRequest helper method is invoked. For example, the following model indicates that requests must provide the Name property and a value. Therefore, failure to provide a proper Name in the request causes model validation to fail.

public class Product
{
    public int Id { get; set; }

    [Required]
    public string Name { get; set; }

    public string Description { get; set; }
}

The preceding action's other known return code is a 201, which is generated by the CreatedAtAction helper method. In this path, the Product object is returned.

ActionResult<T> type

ASP.NET Core 2.1 introduces the ActionResult<T> return type for Web API controller actions. It enables you to return a type deriving from ActionResult or return a specific type. ActionResult<T> offers the following benefits over the IActionResult type:

Most actions have a specific return type. Unexpected conditions can occur during action execution, in which case the specific type isn't returned. For example, an action's input parameter may fail model validation. In such a case, it's common to return the appropriate ActionResult type instead of the specific type.

Synchronous action

Consider a synchronous action in which there are two possible return types:

[HttpGet("{id}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public ActionResult<Product> GetById(int id)
{
    if (!_repository.TryGetProduct(id, out var product))
    {
        return NotFound();
    }

    return product;
}

In the preceding code, a 404 status code is returned when the product doesn't exist in the database. If the product does exist, the corresponding Product object is returned. Before ASP.NET Core 2.1, the return product; line would have been return Ok(product);.

Tip

As of ASP.NET Core 2.1, action parameter binding source inference is enabled when a controller class is decorated with the [ApiController] attribute. A parameter name matching a name in the route template is automatically bound using the request route data. Consequently, the preceding action's id parameter isn't explicitly annotated with the [FromRoute] attribute.

Asynchronous action

Consider an asynchronous action in which there are two possible return types:

[HttpPost]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
public async Task<ActionResult<Product>> CreateAsync(Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    await _repository.AddProductAsync(product);

    return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}

If model validation fails, the BadRequest method is invoked to return a 400 status code. The ModelState property containing the specific validation errors is passed to it. If model validation succeeds, the product is created in the database. A 201 status code is returned.

Tip

As of ASP.NET Core 2.1, action parameter binding source inference is enabled when a controller class is decorated with the [ApiController] attribute. Complex type parameters are automatically bound using the request body. Consequently, the preceding action's product parameter isn't explicitly annotated with the [FromBody] attribute.

Additional resources