In this article, we are going to learn about different return types that we can use with ASP.NET Core Web API controller actions.

To download the source code for this article, you can visit our GitHub repository.

So let’s start

ASP.NET Core Web API allows us to return different types from the controller action methods:

  • Specific Types
  • IActionResult
  • ActionResult<T> 

We are going to discuss each of these in detail with some examples.

Specific Return Types

In its simplest form, an ASP.NET Core Web API controller action can just return a specific type like a string or a custom entity. Let’s consider a simple controller action method that returns the list of all employees:

[HttpGet]
public List<Employee> Get() =>
    _repository.GetEmployees();

This action will return a 200 Ok status code along with a collection of Employee objects when it runs successfully. Of course, in case of errors, it will return a 500 Error status code along with error details. Even while generating API documentation using tools like Swagger, it will generate this possible successful outcome in the Responses section.  Since the action doesn’t have any validations or multiple return paths, returning a specific type work well here.

However, if we want to add some validations into the action and return a validation failure with a 400 Bad Request response, this approach won’t work and we have to use either IActionResult or ActionResult<T> types. We will explain how to do that in later sections.

IEnumerable<T> vs IAsyncEnumerable<T> 

It is a common practice to return a collection from controller actions using the IEnumerable<T> type. However, there is an important behavior of ASP.NET Core that we need to consider before choosing this type.  ASP.NET Core buffers the result of the action endpoint that returns IEnumerable<T> before writing them into the response. This means even if we get the underlying data part by part asynchronously, ASP.NET Core will wait till it receives the complete data and then send the response at once. 

For instance, let’s inspect an action method that uses the yield return statement to return elements one at a time:

[HttpGet("active")]
public IEnumerable<Employee> GetActive()
{
    foreach (var employee in _repository.GetActiveEmployees())
    {
        yield return employee;
    }                         
}

Here, even if the repository supports returning data part by part asynchronously, our action endpoint will still wait till it receives all the data and then returns everything together.

So what if we want to support asynchronous iteration? Well, for that, we need to use the IAsynEnumerable<T> with the await foreach syntax:

[HttpGet("activeasync")]
public async IAsyncEnumerable<Employee> GetActiveAsync()
{
    await foreach (var employee in _repository.GetActiveEmployeesAsync())
    {
        yield return employee;
    }
}

With IAsyncEnumerable<T> and await foreach syntax, the action will return each element as it arrives. 

We have explained the concept of IAsyncEnumerable in detail in our IAsyncEnumerable with yield in C# article and it will be a good reference to learn more about this topic.

IActionResult Return Type

Whenever an action has multiple return paths and needs to support returning multiple ActionResult types, then  IActionResult is a great choice. Several types derive from the ActionResult type and each of them represents an HTTP status code. For instance, when we want to return a 400 Bad Request response, we can use the BadRequestResult type. Similarly, we can use NotFoundResult for returning a 404 Not Found response and OkObjectResult for returning a 200 Ok response.

On top of that, the ControllerBase class has defined some convenience methods which is equivalent to creating and returning an ActionResult type. For instance, if we want to return a 400 Bad Request response, instead of using the return new BadRequestResult();, we could just write return BadRequest(); . Similarly, we could use Ok() and NotFound() methods to return the 200 Ok and 404 Not Found responses respectively.

While using IActionResult type, it is important to provide the [ProducesResponseType] attribute for all possible scenarios since multiple response types and paths are possible. This attribute defines all the HTTP status codes and returns types that are possible in the action which will help in producing more descriptive response details while generating API documentation using tools like Swagger.

To see this in action, first, let’s create a synchronous action method for fetching an employee that returns the IActionResult return type:

[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Employee))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetById(int id)
{
    if (!_repository.TryGetEmployee(id, out Employee? employee))
    {
        return NotFound();
    }

    return Ok(employee);
}

Note that there are two possible return types in this action. If the employee with the specified id is not found, it returns a 404 Not Found response. On the other hand, once it finds the employee with the specified id, it returns a 200 Ok status code with the employee object.

Also, note how we have specified the [ProducesResponseType] attribute multiple times to indicate all possible response status codes and types.

Now let’s create an asynchronous action method for creating an employee record that returns the IActionResult type:

[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateAsync(Employee employee)
{
    if (employee.Name.Length < 3 || employee.Name.Length > 30)
    {
        return BadRequest("Name should be between 3 and 30 characters.");
    }

    await _repository.AddEmployeeAsync(employee);

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

We have two possible return types in this action as well. In case the validation rule fails on the employee name field, it returns a 400 Bad Request response. On the other hand, once the employee record is created successfully, it returns a 201 Created Success status code. Note that we are using the CreatedAtAction() shorthand method which will return the newly created employee record along with the response.

Automatic HTTP 400 Response

While using ASP.NET Core Web API, if we mark a controller with the [ApiController]  attribute, it will automatically trigger an HTTP 400 response if there is a model validation error.

For instance, let’s say we have marked some of the attributes of our Employee class with the [Required] attribute:

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

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

    public bool IsActive { get; set; }
}

Now if we do not provide a value for the Name property in the request, it will automatically return a 400 Bad request response provided the [ApiController]  attribute is applied to the EmployeeController class.

You can read more about this attribute in our ApiController attribute article.

ActionResult<T> Return Type

ASP.NET Core supports returning the ActionResult<T> type from Web API controller actions. While using the ActionResult<T>, we can either return an ActionResult type or a specific type.

One advantage of using this type is that we can skip the Type property of the [ProducesResponseType] attribute. This is because the expected return type can be inferred from the T in the ActionResult<T>.

Similarly, while returning the ActionResult<T> type, it supports implicit casting of both type T and ActionResult to ActionResult<T>. This means instead of writing return new OkObjectResult(employee) we could simply write return employee and it will give the same results.

However, remember that C# doesn’t support implicit casting on interfaces. For instance, if we are using ActionResult<IEnumerable<T>> as the return type on an action,  we cannot return an IEnumerable<T> type as the implicit casting will not work in that case. One solution for this is to convert the IEnumerable collection to a List using the .ToList() method before returning it.

Let’s convert the previous examples using IActionResult to return ActionResult<T>. First, let’s modify the synchronous method:

[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<Employee> GetById(int id)
{
    if (!_repository.TryGetEmployee(id, out var employee))
    {
        return NotFound();
    }
                        
    return employee;
}

Note that we have removed the Type property from the [ProducesResponseType] attribute. Apart from that, we have modified the return type to ActionResult<Employee> and we return the employee object directly. If the employee record is not found, it still returns the 404 ActionResult response. Here ASP.NET Core will cast both the ActionResult and the Employee type to the ActionResult<Employee> type.

Next, let’s see how to convert the asynchronous action using the IActionResult to ActionResult<T> type:

[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<Employee>> CreateAsync(Employee employee)
{
    if (employee.Name.Length < 3 || employee.Name.Length > 30)
    {
        return BadRequest("Name should be between 3 and 30 characters.");
    }

    await _repository.AddEmployeeAsync(employee);

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

Here, we just need to change the return type from IActionResult to ActionResult<Employee> . Since this action returns ActionResult type responses in both cases, ASP.NET Core will cast it to ActionResult<Employee> type.

Comparing the Different Web API Return Types

Now that we have learned about different return types of ASP.NET Core Web API actions, let’s do a quick comparison of them to check their pros and cons and determine when to use which.

Specific types are the simplest ones to use if we do not have multiple paths and return types in an action. While using specific types, we do not have to define the [ProducesResponseType] attribute as the return type is visible from the action signature and documentation tools like swagger can read it. However, specific types cannot support multiple return types. Hence we cannot use them if we want to return different status codes like NotFound, BadRequest, Redirect, etc., which are very common with Web API actions.

IActionResult type can solve the limitation of specific types by supporting returning multiple types along with status codes. However, as it returns multiple types, the documentation tools like swagger would not be able to infer its output type directly. Hence it is very important to use the [ProducesResponseType] attribute explicitly for every possible return type. This can be a limitation of using this type.

ActionResult<T>  is a combination of ActionResult and specific types and it allows us to return either an ActionResult or a specific type. While using this type, we can exclude the Type property of the [ProducesResponseType] attribute as the return type can be inferred from the T in the ActionResult<T> type. On top of that, C# supports implicit casting of both T and ActionResult to  ActionResult<T>

Conclusion

In this article, we explored the different return types that are possible with the ASP.NET Core Web API Controller actions. Additionally, we learned the benefits and limitations of each type and how to use each of them synchronously and asynchronously.