George Kosmidis

Microsoft MVP | Speaks of Azure, AI & .NET | Founder of Munich .NET
Building tomorrow @
slalom
slalom

Action results in ASP.NET CORE APIs

by George Kosmidis / Published 5 years ago, modified 4 years ago
Action results in ASP.NET CORE APIs

ASP.NET Core supports creating RESTful services, also known as web APIs. To handle requests, a web API uses controllers with actions (in essence methods) that return an ActionResult. Since an ActionResult can be almost anything (it’s like returning an object from your methods), it makes sense to know how to use it and when to choose to return a specific type instead.

Let’s go through the each different return type, starting from the simplest one and work our why towards the most generic ActionResult.

Controller action return types

There are three controller action return types in ASP.NET Core:

  • Specific type
  • IActionResult
  • ActionResult<T>

The first one is the simplest one. If there are no validation checks, no special conditions to safeguard and an action only returns a primitive or a complex type, there is no reason to worry about multiple action results. Check this gist for example:

//Primitive Type
[HttpGet]
public int GetOne()
{
    return 1;
}

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

This is absolutely correct, assuming of course the above: Only a list of products -maybe empty- will be returned and nothing.

When known conditions need to be accounted for an action, for example parameter constrains validation, multiple return paths are introduced: BadRequestResult (400), NotFoundResult (404), OkObjectResult (200) etc. 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.

ActionResult Inheritance Tree.
ActionResult Inheritance Tree.
Check a bigger graph or read the details on Microsoft Docs

In difference with the IActionResult, the ActionResult was introduced in ASP.NET 2.1 and it could be considered just syntactic sugar, as -according to Microsoft Docs– the benefits of using it instead of IActionResult type are syntactic:

  • The [ProducesResponseType] attribute’s Type property can be excluded. For example, [ProducesResponseType(200, Type = typeof(Product))] is simplified to [ProducesResponseType(200)]. The action’s expected return type is instead inferred from the T in ActionResult<T>.
  • Implicit cast operators support the conversion of both T and ActionResult to ActionResult<T>. T converts to ObjectResult, which means return new ObjectResult(T); is simplified to return T;.

The following two gists illustrate the differences between IActionResult and ActionResult<T>.
First the earlier IActionResult return type:

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

And as comparison, the same with the ActionResult<T> which makes code a bit cleaner:

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

    return product;
}

Polymorphic APIs, what to avoid

Polymorphism in an API is not bad by definition! Supporting for example both JSON and XML, or any other type by writing a custom OutputFormatter is a common practice. It is unprofessional and sloppy though, when polymorphic API means that an action can “return different surprise types based on internal conditions“:

Take these responses for example:

{
    "title": "Book 1",
    "author": "George Kosmidis",
}
{
    "title": "Book 2",
    "author": 
    {
        "name": "George Kosmidis",
        "user_id": 1
    }
}

Both responses are obviously books, but the author can be either a string or a complex type. This is more than enough to break the consumer’s code and should definitely be avoided. A different return type should mean different API version.

Unfortunately the code that follows compiles and works “perfectly”. This shows the freedom that ObjectResult return type allows and how easily it can be abused.

[HttpGet]
[ProducesResponseType(200, Type = typeof(int))]
[ProducesResponseType(404)]
public ActionResult<IEnumerable<bool>> Get()
{
    if (DateTime.Now.Ticks % 2 == 0)
    {
        return Ok("Just a stirng when ticks are even!");
    }
    
    return Ok(new {Id = 1, Name = "George" };);
}

Conclusion

Compiler vendors expect exceptions to be thrown rarely because they are -as the name suggests- exceptions of the normal flow. They focus more on collecting information about why this rare event happened and much less about optimizing the throw code. This makes the use of exceptions very expensive, and thus HttpResponseException was completely removed in ASP.NET Core. The alternative and suggested way is now ActionResult<T>, as it is also contributes towards readability and the principle of least astonishment. Nevertheless, your actions should always return one specific type wrapped in an ActionResult<T> that allows multiple ActionResult return types, and not the other way around.

Changing a little bit what Damian Conway wrote in his book Perl Best Practices:

[…] Always code as if the person who ends up using your API will be a violent psychopath who knows where your IP.

This page is open source. Noticed a typo? Or something unclear?
Edit Page Create Issue Discuss
Microsoft MVP - George Kosmidis
Azure Architecture Icons - SVGs, PNGs and draw.io libraries