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.
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 theT
inActionResult<T>
. - Implicit cast operators support the conversion of both
T
andActionResult
toActionResult<T>
.T
converts to ObjectResult, which meansreturn new ObjectResult(T);
is simplified toreturn 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.