In this demo, we'll have a look at OpenAPI analyzers.
They were introduced with ASP.NET Core 2.2,
quite some time ago, and they help with analyzing our API and suggesting changes,
for example, for correct generation of the OpenAPI Specification.
In the past,
the way to add these was to look for them via the NuGet
dialog and add the necessary package.
Back then it was called the Microsoft.AspNetCore.Mvc.Api.Analyzers package,
which included a set of API analyzers.
But in more recent SDK versions for .NET Core 3 right up to .NET 5 and .NET 6,
the analyzers we are looking for are included in the SDK itself.
All we need to do is enable them, and we can do that in the project file,
which you see on screen now.
We set IncludeOpenAPIAnalyzers to true.
Let's save this, and let's wait a few seconds,
and we see some additional new warnings show up in our error list.
These are exactly the warnings we are looking for.
These tell us which potential status codes can be returned by our API actions,
but haven't been declared.
Let's click one.
A 404 seems to be missing in our BooksController,
we're looking at the GetBooks action here,
and indeed we see a return NotFound statement.
If we hover over it, we see the warning we also see in our error list.
If we click the light bulb, we see a potential resolution for our issue.
It proposes to add a set of ProducersResponseType attributes.
So let's do that, and there we go.
It added a ProducersResponseType attribute for 200 OK and for 404 Not Found.
So far, so good.
But, it also added a ProducersDefaultResponse type attribute.
What's that about?
Well, let's run our application and let's have a look.
Let's scroll down to the Books resource.
We see our 200 status code and our 404 Not Found.
And if we scroll down a bit more, we also see a default section.
That is the effect of the ProducersDefaultResponse type attribute.
It adds a default response with a Problem Details body for
all codes we haven't explicitly specified.
But, is this a good idea?
Providing a default in case we forget or overlook a specific
status code that causes a problem is a good idea,
but it is more important to be explicit about
response types when you know about them.
The default response isn't necessary, correct or complete.
As an example,
let's have a look at the UpdateAuthor method on our AuthorsController.
Here, too, we see that green squiggly line that is the result of the analyzer.
Click light bulb, and we choose to add ProducersResponseType attributes.
We see 200 OK, 404 Not Found, and a default response type.
Let's give that a try, and let's try sending a request to update an author,
but with an invalid request body.
So let me paste in an author ID,
this is an ID of an existing author, and as request body, let me
just pass through abc. Definitely not valid.
We want to select application/json here as the content type for
the body as that is what the content type is.
Then we click Execute. We get back a 406 Not Acceptable.
That means I made a small mistake. I forgot to set the value of the accept
header to something we accept. Text/plain is not acceptable for us;
application/json is. We'll improve our documentation later on so this cannot
happen anymore. For now, let me just set it to application/json and click
Execute again to send the request again. And there we go, we get back a 400
Bad Request. And that is correct.
This status code wasn't explicitly defined, so we could
argue that it would fall under the default.
The body fits the Problem Details object description.
Problem Details is fairly permissive.
In fact, with that definition, almost all types of problems can be
serialized for that class. But we could be more specific.
Our documentation could explicitly state that the API can return a
400 Bad Request, so the consumer of the API knows that that is a code
that can be expected. And let me try sending that faulty request
again I previously sent. As an accept header, I'm again going to pass
through text/plain instead of application/json. We get back a 406 Not
Acceptable.
That, too, is a code that isn't covered by our documentation.
In other words,
API analyzers are a good starting point, but it's
by no means a complete solution.
They only analyze the code in controllers that have the ApiController
attribute applied to them. But as we know by now,
other parts of our code, be it framework code or custom code of
ourselves, can also influence potential responses.
So, what could we do to fix this?
Well,
we as developers know our API best, so it's our responsibility to correctly add
the ProducersResponseType attributes that are missing to our actions, like a
400 Bad Request. We already know how to do that.
We could simply add another ProducersResponseType
attribute to our UpdateAuthor action.
The thing is, though, a lot of these attributes would be repetitive.
For example,
all actions in our BooksController can potentially return a 400 Bad Request.
They can also return a 406 Unacceptable or a 500 Internal
Server error. Luckily, we can also apply these attributes at
the controller level, so let's do that.
There we go.
This also means we can get rid of the ProducersResponseType attribute
for a bad request at the level of the GetBook action.
There we go.
Let's give this a try.
Let's scroll down to Books, and there we go.
We see our 400, 406, and 500 appear, and we see those appear for
all subsections That's already a lot better.
But, our BooksController isn't the only one from which the actions
can return responses with these status codes.
We've got another one, the AuthorsController.
And if we scroll back up, we see that the status codes don't appear here yet.
We could just copy/paste the attributes to the
AuthorsController and apply them at that level, but there's a
way to globally apply them to all controllers.
Let's open the Program file.
When we call into AddControllers,
we can configure the underlying services we're adding.
One of the things we can do is add filters. On configure we can find
the Filters collection. Items in this collection are applied to all
controllers in our code base, so to this collection we can add our
ProducersResponseType attributes.
We can now also get rid of these attributes at the level of the BooksController.
There we go.
Let's save this and let's give this a try.
Let's have a look at a resource from the Authors collection.
Here's our status codes.
And if we scroll down to one from the Books collection,
we see those same status codes, and more appear.
So, that's much better.
We're not there yet, though.
Up until now, we've explicitly used attributes for the different response types.
There's another way of achieving this through conventions. Let's have a look.