Enhancing API Documentation

Swashbuckle tooling for Swagger provides an easy way to document APIs in ASP.NET Core with out of the box functionality that creates a lot without additional customization. The OpenAPI spec allows for a lot of additional description to be included, and by annotating your code in appropriate places the Swagger generation can automatically fill these for you. Many of these additional descriptions also show up in the automatically generated Swagger UI.

There are multiple ways to add annotations, each of which covers a subset of the available types, with overlap in many places. The ones we’ll look at here include:

  1. Swagger configuration options set in your startup code
  2. MVC Produces attributes in your API code
  3. Swashbuckle Swagger attributes in your API code
  4. XML documentation comments in your API code

Swagger Configuration

Some top-level descriptions of your API can be added in your startup configuration. There are a lot of different options here depending on what kind of details you want to include. I’ve added a few examples that show up in the Swagger UI here, but see the documentation of OpenApiInfo for more.

builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "Swagger Demo API",
        Version = "v1",
        Description = "This is the Web API description",
        Contact = new OpenApiContact
        {
            Name = "John Bowen",
            Url = new Uri("https://codemindinterface.com"),
        },
        License = new OpenApiLicense
        {
            Name = "The API License",
            Url = new Uri("https://localhost/api-license"),
        }
    });
});

MVC Produces Attributes

ASP.NET Core includes a set of built in attributes that can be applied to API methods. A few of these, like Produces and Consumes, can also affect the behavior of your API (limiting allowed media types), but additional Produces… variations are available primarily as documentation.

  • Produces and Consumes specify valid media types for an API method. These are listed in OpenAPI docs and show up as a dropdown in the testing UI. [Produces("application/json")]
  • ProducesResponseType specifies a possible status code and optionally a data type that can be returned from the API method. [ProducesResponseType(typeof(User), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)]
  • ProducesErrorResponseType can be used to specify a common data type to return for any error responses rather than specifying the type individually on each response type. [ProducesErrorResponseType(typeof(void))] [ProducesErrorResponseType(typeof(MyCustomError))]
  • ProducesDefaultResponseType specifies a default data type to return for any response type that is not explicitly specified. If no type is specified, the default ProblemDetails data type is used. [ProducesDefaultResponseType] [ProducesDefaultResponseType(typeof(MyCustomError))]

Swashbuckle Swagger Attributes

These attributes require an additional NuGet package: Swashbuckle.AspNetCore.Annotations. Once this is referenced, additional configuration needs to be added to AddSwaggerGen at startup to read the attributes.

builder.Services.AddSwaggerGen(options =>
{
    options.EnableAnnotations();
});
  • SwaggerOperation [SwaggerOperation("Get a user", "Gets a user by id", OperationId = "get-user", Tags = new[] { "users" })]
  • SwaggerResponse [SwaggerResponse(StatusCodes.Status201Created, "The user was created", typeof(User), ContentTypes = new[] { "application/json" })]
  • SwaggerParameter is used on method parameters to specify descriptions or mark as required [SwaggerParameter("SwaggerParameter.Description", Required = false)] string organization
  • SwaggerRequestBody is used on method parameters like SwaggerParameter but specifically for request bodies (i.e. POST and PUT). [FromBody][SwaggerRequestBody("SwaggerRequestBody.Description", Required = true)] User? user
  • SwaggerSchema is used on model object classes and properties to provide descriptions and specify other attributes, like nullable and readonly. [SwaggerSchema("The email address of the user", Nullable = false)]
  • SwaggerTag is a class level attribute that will define the default tag grouping for all methods that don’t explicitly specify their own tags.

XML Documentation Comments

XML documentation comments (using /// followed by defined tags) can be added on classes and methods to produce a doc file for your entire project which can then be read in by the Swagger generator. To enable XML documentation generation in your project, in your .csproj file add the following lines:

<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

This will generate an XML file containing the documentation comments for your code. By default it names the file using the format <AssemblyName>.xml. In order for Swagger to use these comments, additional configuration needs to again be added to AddSwaggerGen at startup.

builder.Services.AddSwaggerGen(options =>
{
    options.IncludeXmlComments(Path.Combine(
        AppContext.BaseDirectory,
        $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"), true);
});

Although the XML doc spec includes a lot of additional valid tags, not all of them will come through to the OpenAPI doc. The key tags that can show up:

  • <summary> on API classes, methods, model classes, and properties. These turn into summary or description in multiple places so are probably the primary thing you’ll want to add.
    /// <summary>
    /// Create a new user
    /// </summary>
    
  • <remarks> on API methods show up as additional descriptions and can be useful for adding things like examples or links
    /// <remarks>
    /// Creating a new user requires a valid email address object. <![CDATA[<br/>]]>
    /// See https://link.local/ for more details.
    /// </remarks>
    
  • <param> on API methods and model classes. On methods, these work like the SwaggerParameter attribute but also allow for specifying an example value. For model classes they work like the property level SwaggerSchema attribute. /// <param name="id" example="A12345" required="true">The user ID</param> /// <param name="Zip">The Zip Code</param>

Example

Because there are so many options it can be difficult to tell where various descriptions will actually show up in your OpenAPI doc and the Swagger UI. The following example code uses a mix of all of the techniques listed to demonstrate what the output will look like. The complete output can be found in the example OpenAPI doc. Here are some examples of where various descriptions show up in the Swagger UI:

Top level API and the POST endpoint

Startup configuration in Program.Main

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.IncludeXmlComments(Path.Combine(
        AppContext.BaseDirectory,
        $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"), true);
    options.EnableAnnotations();

    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "This is OpenApiInfo.Title",
        Version = "v1",
        Description = "This is OpenApiInfo.Description",
        Contact = new OpenApiContact
        {
            Name = "OpenApiContact.Name",
            Url = new Uri("https://codemindinterface.com"),
        },
        License = new OpenApiLicense
        {
            Name = "OpenApiLicense.Name",
            Url = new Uri("https://localhost/api-license"),
        }
    });
}); 

Model objects

[SwaggerSchema("SwaggerSchema.Description on class", ReadOnly = true, Nullable = false)]
public record User(string Id, string First, string Last, string Email, string Username, Address HomeAddress)
{
    [SwaggerSchema("SwaggerSchema.Description on property", ReadOnly = true, Nullable = false)]
    public string Id { get; init; } = Id;
    [SwaggerSchema("The first name of the user", Title = "User First Name")]
    public string First { get; init; } = First;
    [SwaggerSchema("The last name of the user")]
    public string Last { get; init; } = Last;
    [SwaggerSchema("The email address of the user")]
    public string Email { get; init; } = Email;
    [SwaggerSchema("The username of the user")]
    public string Username { get; init; } = Username;
    [SwaggerSchema("The home address of the user")]
    public Address HomeAddress { get; init; } = HomeAddress;
}

/// <summary>
/// XML doc comment summary
/// </summary>
/// <param name="Street">XML doc comment param</param>
/// <param name="City">The City</param>
/// <param name="State">The US State Abbreviation</param>
/// <param name="Zip">The Zip Code</param>
public record Address(string Street, string City, string State, string Zip);

public class CustomErrorResponse
{
    public int ErrorCode { get; set; }
    public string? Message { get; set; }
}

API Controller with class and method level attributes

Get User API endpoint

Get All API endpoint

/// <summary>
/// XML doc comment class summary
/// </summary>
[Route("api/[controller]")]
[ApiController]
[SwaggerTag("SwaggerTag on class")]
public class UserController : ControllerBase
{
    [HttpGet]
    [ProducesResponseType(typeof(List<User>), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesErrorResponseType(typeof(void))]
    [SwaggerOperation("SwaggerOperation.Summary", 
         "SwaggerOperation.Description", 
         OperationId = "SwaggerOperation.OperationId", 
         Tags = new[] { "SwaggerOperation.Tags1", "users" })]
    [Produces("application/json")]
    public IActionResult Get()
    {
        //...
    }

    /// <param name="id" example="A12345" required="true">XML doc comment param</param>
    [HttpGet("{id}")]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesDefaultResponseType]
    [Produces("application/json")]
    [SwaggerOperation("Get a user", 
         "Gets a user by id", 
         OperationId = "get-user", 
         Tags = new[] { "users" })]
    [SwaggerResponse(StatusCodes.Status200OK, 
         "The user was found", 
         typeof(User))]
    public IActionResult Get(string id)
    {
        //...
    }

    /// <summary>
    /// XML doc comment summary
    /// </summary>
    /// <remarks>
    /// XML doc comment remarks.
    /// </remarks>
    [HttpPost]
    [SwaggerOperation(OperationId = "create-user")]
    [SwaggerResponse(StatusCodes.Status201Created, 
         "The user was created", 
         typeof(User), 
         ContentTypes = new[] { "application/json" })]
    [SwaggerResponse(StatusCodes.Status400BadRequest, 
         "The user was invalid", 
         typeof(void))]
    [SwaggerResponse(StatusCodes.Status401Unauthorized, 
         "The user was not authorized", 
         typeof(void))]
    public IActionResult Post(
        [FromBody][SwaggerRequestBody("SwaggerRequestBody.Description", Required = true)] User? user,
        [SwaggerParameter("SwaggerParameter.Description", Required = false)] string organization)
    {
        //...
    }

    [HttpDelete("{id}")]
    [SwaggerOperation(OperationId = "delete-user")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesDefaultResponseType(typeof(CustomErrorResponse))]
    public IActionResult Delete(string id)
    {
        //...
    }
}

Code Versions

Example code is using .NET 7.0 with these library versions:

  • Microsoft.AspNetCore.OpenApi 7.0.11
  • Swashbuckle.AspNetCore 6.5.0
  • Swashbuckle.AspNetCore.Annotations 6.5.0