Documenting an API with Swagger

Josiah T Mahachi
9 min readMar 6, 2023

--

Documenting an API with Swagger
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.

Add New Scaffolded Item
Add New Scaffolded Item

Select API on the left, and “API Controller with actions, using Entity Framework”, and click “Next”.

Add API Controller with actions, using Entity Framework
Add API Controller with actions, using Entity Framework
Create a controller
Add New Scaffolded Item

Some NuGet packages may be added.

Adding Nuget Packages

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:

Swagger API Documentation
Swagger API Documentation

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:

Documentation of the API using Swagger and XML comments
Documentation of the API using Swagger and XML comments

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:

Model XML comments

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.

--

--

Josiah T Mahachi

Full-stack Developer, C#, ASP.Net, Laravel, React Native, PHP, Azure Dev Ops, MSSQL, MySQL