Documenting an API with Swagger
Introduction
When it comes to building a robust and scalable web API, documentation is key. Not only does it help developers understand how to use your API, but it also makes it easier for them to troubleshoot issues that arise. Swagger is a powerful tool that can help you generate interactive API documentation that’s easy to use and understand. In this (not so) short blog post, I’ll show you how to use Swagger to document an ASP.NET API.
Please note that this article assumes you are new at creating APIs in ASP.NET. I therefore make the same ‘mistakes’ a lot of newbies make (myself included haha, when I didn’t know any better). Then, the next article will correct those mistakes by moving the data access layer to repositories.
What is Swagger?
Swagger is an open-source software framework that helps developers design, build, document, and consume RESTful web services. It provides a powerful set of tools for creating interactive API documentation, including automatic documentation generation, API testing, and client SDK generation. Swagger comes already added to an ASP.NET API project so we will not be manually adding the nuget package.
Creating the Controllers
Now, we’ll create a few controllers to handle requests to the API. In this example, we’ll inject the LibraryContext class directly into each controller (terrible idea by the way). We will refactor these controllers in the next article as we discuss why this is a bad idea.
To create a controller, right click on the Controller folder on the project under Solution Explorer, and select Add > Controller.
Select API on the left, and “API Controller with actions, using Entity Framework”, and click “Next”.
Some NuGet packages may be added.
Repeat the same process to create the other controllers.
The BookController
:
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using EightApp.Demo.EfCoreCodeFirst01.Models;
namespace EightApp.Demo.EfCoreCodeFirst01.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class BookController : ControllerBase
{
private readonly LibraryContext _context;
public BookController(LibraryContext context)
{
_context = context;
}
// GET: api/Book
[HttpGet]
public async Task<ActionResult<IEnumerable<Book>>> GetBooks()
{
return await _context.Books.ToListAsync();
}
// GET: api/Book/5
[HttpGet("{id}")]
public async Task<ActionResult<Book>> GetBook(int id)
{
var book = await _context.Books.FindAsync(id);
if (book == null)
{
return NotFound();
}
return book;
}
// PUT: api/Book/5
[HttpPut("{id}")]
public async Task<IActionResult> PutBook(int id, Book book)
{
if (id != book.Id)
{
return BadRequest();
}
_context.Entry(book).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!BookExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/Book
[HttpPost]
public async Task<ActionResult<Book>> PostBook(Book book)
{
_context.Books.Add(book);
await _context.SaveChangesAsync();
return CreatedAtAction("GetBook", new { id = book.Id }, book);
}
// DELETE: api/Book/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteBook(int id)
{
var book = await _context.Books.FindAsync(id);
if (book == null)
{
return NotFound();
}
_context.Books.Remove(book);
await _context.SaveChangesAsync();
return NoContent();
}
private bool BookExists(int id)
{
return _context.Books.Any(e => e.Id == id);
}
}
}
The BookController
injects the LibraryContext
instance into its constructor using dependency injection and uses it to perform CRUD operations on the Book entity. The controller contains methods for retrieving all books, retrieving a single book by its ID, updating a book, creating a new book, and deleting a book. Let’s break it down.
[Route("api/[controller]")]
[ApiController]
[Route("api/[controller]")]
is an attribute that defines the route for the controller. It specifies that the controller should be accessed using the base route api
, followed by the name of the controller in square brackets. The name of the controller is automatically generated based on the name of the controller class, without the word "Controller" at the end.
For example, if the name of the controller class is BookController
, the route will be api/book
.
[ApiController]
is an attribute that indicates that the controller should use certain behaviors that are specific to web APIs. This includes automatic model validation, binding source parameter inference, and error handling. By using this attribute, the controller will be configured to return a 400 Bad Request response if the model state is invalid, and will also handle exceptions thrown during request processing.
private readonly LibraryContext _context;
public BookController(LibraryContext context)
{
_context = context;
}
In the above code, LibraryContext
is the database context that is injected into the constructor of the BookController
. This is done so that the controller can access the database to perform CRUD operations on the Book
entity.
The private readonly
keyword declares a private class-level variable called _context
, which is initialized by the context instance passed in via the constructor. This allows the variable to be accessed by any method within the controller.
[HttpGet]
public async Task<ActionResult<IEnumerable<Book>>> GetBooks()
{
return await _context.Books.ToListAsync();
}
In the above code, we have an action method with the [HttpGet]
attribute. This attribute indicates that this action method should be called when an HTTP GET request is made to the specified endpoint.
The GetBooks
method is asynchronous, meaning that it will not block the thread while waiting for the ToListAsync()
method to complete. The ToListAsync()
method is called on the _context.Books
property which returns a DbSet<Book>
. ToListAsync()
is an extension method provided by Entity Framework Core that returns a List<T>
of the items in the DbSet<T>
.
The ActionResult<IEnumerable<Book>>
return type indicates that this action method returns a collection of Book
objects wrapped in an ActionResult
. The ActionResult
provides a way to return a response with a specific HTTP status code and content type.
The await
keyword is used to asynchronously wait for the ToListAsync()
method to complete before returning the results. The ToListAsync()
method retrieves all the books from the database and converts them to a list. Finally, the ActionResult
with the list of books is returned.
The rest of the methods follow the same logic. I’m sure you get the idea, and for brevity, we will not show the AuthorController
and the PublisherController
as they follow the same patterns with the BookController
. Now, let us document the BookController
with Swagger.
Documenting the API Endpoints
The last few steps have been building up to this moment. Let’s see how the BookController
looks like without Swagger documentation first so that we have a point of reference. Run the project:
Now, stop the app and let us start updating the documentation.
Add the [ProducesResponseType]
attribute to each action in the BookController
to specify the types of responses that the action can return. For example:
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<Book>>> GetBooks()
{
return await _context.Books.ToListAsync();
}
This code specifies that the GetBooks
action can return a 200 OK
response with an array of Book
objects.
Add the [SwaggerOperation]
attribute to each action in the BookController
to specify additional metadata about the action, such as a description and any parameters. For example:
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SwaggerOperation("GetBookById")]
public async Task<ActionResult<Book>> GetBook(int id)
{
var book = await _context.Books.FindAsync(id);
if (book == null)
{
return NotFound();
}
return book;
}
This code specifies that the GetBook
action can return a 200 OK
response with a Book
object, or a 404 Not Found
response if the book with the specified ID is not found. It also sets the OperationId
property to "GetBookById" to give the action a more descriptive name in the Swagger UI.
Fnally, add the [Produces]
and [Consumes]
attributes to each action in the BookController
to specify the types of content that the action can produce or consume. For example:
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Produces("application/json")]
[Consumes("application/json")]
public async Task<ActionResult<Book>> PostBook(Book book)
{
_context.Books.Add(book);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetBook), new { id = book.Id }, book);
}
This code specifies that the PostBook
action can produce a 201 Created
response with a Book
Again, we repeat the same for the rest of the other controllers. This is what the BookController
now looks like:
using EightApp.Demo.EfCoreCodeFirst01.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Swashbuckle.AspNetCore.Annotations;
namespace EightApp.Demo.EfCoreCodeFirst01.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class BookController : ControllerBase
{
private readonly LibraryContext _context;
public BookController(LibraryContext context)
{
_context = context;
}
/// <summary>
/// Retrieves all books.
/// </summary>
[HttpGet]
[SwaggerOperation("GetAllBooks")]
[ProducesResponseType(typeof(IEnumerable<Book>), 200)]
public async Task<ActionResult<IEnumerable<Book>>> GetBooks()
{
return await _context.Books.ToListAsync();
}
/// <summary>
/// Retrieves a specific book by ID.
/// </summary>
/// <param name="id">The ID of the book to retrieve.</param>
[HttpGet("{id}")]
[SwaggerOperation("GetBookById")]
[ProducesResponseType(typeof(Book), 200)]
[ProducesResponseType(404)]
public async Task<ActionResult<Book>> GetBook(int id)
{
var book = await _context.Books.FindAsync(id);
if (book == null)
{
return NotFound();
}
return book;
}
/// <summary>
/// Adds a new book to the database.
/// </summary>
/// <param name="book">The book to add.</param>
[HttpPost]
[SwaggerOperation("AddBook")]
[ProducesResponseType(typeof(Book), 201)]
public async Task<ActionResult<Book>> AddBook(Book book)
{
_context.Books.Add(book);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetBook), new { id = book.Id }, book);
}
/// <summary>
/// Updates an existing book in the database.
/// </summary>
/// <param name="id">The ID of the book to update.</param>
/// <param name="book">The updated book information.</param>
[HttpPut("{id}")]
[SwaggerOperation("UpdateBook")]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public async Task<IActionResult> UpdateBook(int id, Book book)
{
if (id != book.Id)
{
return BadRequest();
}
_context.Entry(book).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!BookExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
/// <summary>
/// Deletes a book from the database.
/// </summary>
/// <param name="id">The ID of the book to delete.</param>
[HttpDelete("{id}")]
[SwaggerOperation("DeleteBook")]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public async Task<IActionResult> DeleteBook(int id)
{
var book = await _context.Books.FindAsync(id);
if (book == null)
{
return NotFound();
}
_context.Books.Remove(book);
await _context.SaveChangesAsync();
return NoContent();
}
private bool BookExists(int id)
{
return _context.Books.Any(e => e.Id == id);
}
}
}
We will also need to add XML comments to all our models:
using System.ComponentModel.DataAnnotations;
namespace EightApp.Demo.EfCoreCodeFirst01.Models
{
/// <summary>
/// The book model
/// </summary>
public class Book : ModelBase
{
/// <summary>
/// Title of the book
/// </summary>
[Required]
[StringLength(100)]
public string Title { get; set; } = string.Empty;
/// <summary>
/// Foreign key to the publisher
/// </summary>
public int PublisherId { get; set; }
/// <summary>
/// Relationship to the publisher model
/// </summary>
public Publisher Publisher { get; set; } = new();
/// <summary>
/// List of authors for the book
/// </summary>
public ICollection<Author> Authors { get; set; } = new List<Author>();
}
}
Edit the Program.cs file:
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "EF Core Demo V1",
Description = "A demo of a number of concepts for developing an API using ASP.NET Core and Entity Framework",
TermsOfService = new Uri("https://www.example.com/terms"),
Contact = new OpenApiContact
{
Name = "Demo Contact",
Url = new Uri("https://www.example.com/contact"),
},
License = new OpenApiLicense
{
Name = "Demo Contact",
Url = new Uri("https://www.example.com/contact"),
}
});
var xmlFileName = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFileName));
});
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "EF Core Demo"));
Make sure that you have enabled XML documentation generation in your project’s properties. In Visual Studio, go to Project Properties > Build > Output > XML documentation file and make sure that the checkbox is checked:
…and by pressing F5
on the keyboard, we now have this:
You will notice the there is now more information about our API in the top left corner. Also, there is a description of what each endpoint does, the types of parameters it accepts, and so on.
If you scroll down to the end of the page, you will notice the models documented there as well:
The highlighted documentation is coming from the XML comments we added to each property of the model.
That is it for this article. It ended up being longer than I anticipated. In the next article, we will explore DTOs and Auto Mapping as a way of fixing some issues we have with our controllers.
And in conclusion
Documenting endpoints with Swagger is a crucial aspect of building a robust and functional ASP.NET API. Swagger simplifies the documentation process by generating an interactive and user-friendly interface that allows developers to explore and test API endpoints without using external tools or writing any additional code. It also provides a way to define request and response types, status codes, and other relevant metadata that can help improve the overall quality of your API documentation.
By following the steps outlined in this blog post, you can easily integrate Swagger into your ASP.NET API project and generate comprehensive and accurate documentation for your API endpoints. Remember to keep your documentation up-to-date and provide clear and concise descriptions of each endpoint’s purpose, input parameters, and expected responses. With the right approach and attention to detail, you can create a well-documented API that developers will love to work with.