When working on a bigger API for the first time, I wanted to get it right. I added Swashbuckle to automatically generate Swagger documentation so that the frontend could easily consume the API. I specified types of the objects that were returned by the endpoints so that they would be visible in the documentation.
Problem
The API looked like this:
[HttpGet("{id}")]
public async Task<ActionResult<Book>> Get(int id)
{
var book = await dbContext.Books.FirstOrDefaultAsync(b => b.Id == id);
if (book is null)
return NotFound();
return Ok(book);
}
But then the frontend guys came and told me that some of the endpoints are actually returning object types different from what they promise. I looked closer and realized that I can return basically anything and the code will happily compile and run.
[HttpGet("{id}")]
public async Task<ActionResult<Book>> Get(int id)
{
var book = await dbContext.Books.FirstOrDefaultAsync(b => b.Id == id);
if (book is null)
return NotFound();
// We don't return Book here, but the code still compiles!
return Ok(new WhateverObjectItIs());
}
Of course I believe that there must be a reason and a rational explanation for this behaviour (although I am not aware of it), but I have to admit that it just caught me by surprise.
What doesn’t work
Naturally, the next step was to find out how to enforce such type control. It’s much easier to spot build error than to manually inspect the methods.
I searched the documentation and found ApiController.Ok
method with a generic overload Ok<T>(T)
which, at first sight, seemed exactly like what I needed, except 1. it didn’t compile if called explicitly and 2. even if it did, it wouldn’t work.
Fortunately, before I started banging my head against the wall, I realized that of course it couldn’t work – it’s ApiController
’s method, but I am using ASP.NET Core 3.1 and therefore my controllers are derived from ControllerBase
, not from ApiController
. (I also made a mental note to search the documentation in a better way.)
Therefore, I was left with ControllerBase.Ok
method that doesn’t provide a generic overload so I cannot just write return Ok<Book>(book)
. Even if I could, the problem wouldn’t disappear, as nothing would prevent me from putting something else than the return type between <
and >
.
What does work
Let’s work with the fact that:
- although
OkObjectResult
(the return type ofControllerBase.Ok
method) doesn’t have a generic version, - its parent (and the return type of our controller’s actions)
ActionResult
does.
Therefore:
- Instead of
ControllerBase.Ok
method, we will create our own method that returnsActionResult<T>
. - We will use it instead of
ControllerBase.Ok
(or any similar method, e.g.ControllerBase.CreatedAtAction
). - The compiler will protest if we try to return anything else than what we promised.
And we do it by creating an AbstractControllerBase
class that:
- will inherit from
ControllerBase
and - will become a parent class of all our controllers.
public abstract class AbstractControllerBase : ControllerBase
{
protected ActionResult<T> Ok<T>(T resultValue)
{
return base.Ok(resultValue);
}
protected ActionResult<T> CreatedAtAction<T>(string actionName, object routeValue, T resultValue)
{
return base.CreatedAtAction(actionName, routeValue, resultValue);
}
}
The best thing about this solution is that we don’t need to change anything in our controllers, except the parent class:
public class BooksController : AbstractControllerBase
And we’re done!
Hopefully this will save you some time and a couple of bugs.
Last modified on 2020-11-20