In this article, we are going to learn about different return types that we can use with ASP.NET Core Web API controller actions.
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.
VIDEO: Different Web API Return Types You Can Use in Your Apps.
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.