Documenting Web APIs with Swagger

Published on Author Michael

A while back I posted an article on how to extend the existing help pages generated by Visual Studio for Web API projects to use reflection instead of XML documentation. One of the limitations of that approach was that you could not test the APIs directly. You had to use SoapUI or equivalent. Since then I have started using Swagger for documentation. As a documentation/testing tool it fills the need. In this article I will demonstrate how to integrate Swagger into a Web API project. Additionally I will continue to use the reflection provider from the previous article.

What is Swagger

Swagger is a popular, open source specification for REST APIs. I first ran across it when I noticed the Add REST Client option in Visual Studio 2015. Clicking that button gives you the option of reading a REST specification from Azure or Swagger and generating a REST client for it. By itself Swagger simply defines an API using a JSON format. Starting this year it is formally known as the OpenAPI Specification. You can read it if you like but really there is no reason to get into the gory details. There are plenty of editors available that allow you to import an existing API and generate the OpenAPI specification for it. There is a web-based one here. I prefer one that allows me to import .NET assemblies so I use NSwagStudio. But for purposes of this article I want to focus on generating the specification from the code on demand.

Swagger is language agnostic (it is just a specification) so to use it you must acquire the tools for your language of choice. For .NET and Typescript (the most likely targets of Windows hosts) NSwag is the core library you will need. Additionally you will likely want some sort of tool to generate the OpenAPI specification file. For this article we be doing it programmatically but having an editor like one of the tools mentioned earlier is useful for seeing the specification. Another tool that you will likely want is a client generator. Visual Studio 2015 has an option to generate a REST client from an OpenAPI specification. NSwagStudio has similar options to generate a .NET or Typescript client for you as well.

Integrating Swagger Into a Project

To get started create a new Web API project. For this article we only need Web API but it will automatically add an MVC area for help. This is the page we talked about previously. Go ahead and delete the area as we won’t need it. We now need to add in the NuGet packages to support Swagger. Since Swagger is evolving it is confusing to determine which implementation to get. For this article I’ll use Swashbuckle. Use Package Manager to download it. It comes with both the libraries need to generate documentation but also a UI we’ll talk about later. The package does a couple of things. Firstly it creates a new file called SwaggerConfig.cs in your App_Start folder. This is the file you’ll use to configure Swagger later. The second thing it does is set up a route so that you can access the specification. To make this a little more meaningful
we’ll swap out the simple controller provided in the sample with a more complex one.

public class ProductController : ApiController
{
    static ProductController()
    {
        s_products.AddRange(new[]
        {
            new Product() { Id = 1, Name = "Product A", Price = 100.0M },
            new Product() { Id = 2, Name = "Product B", Price = 50.0M },
            new Product() { Id = 3, Name = "Product C", Price = 25.0M },
        });
        s_id = 3;
    }
        
    public IEnumerable Get ()
    {
        return s_products;
    }
    
    public Product Get ( int id )
    {
        return s_products.FirstOrDefault(p => p.Id == id);
    }
    
    public Product Post ( [FromBody]Product value )
    {
        var newProduct = new Product()
        {
            Id = ++s_id,
            Name = value.Name,
            Price = value.Price
        };
        s_products.Add(newProduct);
        return newProduct;
    }
    
    public void Put ( int id, [FromBody]Product value )
    {
        var product = Get(id);
        if (product != null)
        {
            product.Name = value.Name;
            product.Price = value.Price;
        } else
        {
            Post(value);
        };
    }
    
    public void Delete ( int id )
    {
        s_products.RemoveAll(p => p.Id == id);
    }
    private static List s_products = new List();
    private static int s_id;
}

Now that Swagger is installed go ahead and run the application and go to the URL ~/swagger/docs/v1 (by default). This returns back the JSON for the API and is how a UI or client generator will get the specification. Navigate to ~/swagger/ui/index and you should be able to see a UI that renders the JSON. More importantly all the API actions should be available and they can be executed. This is useful for learning the API and for testing.

Configuring Swagger

We have some control over the specification using the EnableSwagger method. The first thing I want to change is the URL for the specification. The default URL is not intuitive to me so I’ll change it to ~/help/docs/v1. The version is important if you will have multiple versions of your API. Everything else is personal opinion. Swagger supports multiple versions but you need to cnfigure the URL such that Swagger can specify the version correctly.

.EnableSwagger("help/docs/{apiVersion}", c => 
{
    //Do not include actions marked obsolete
    c.IgnoreObsoleteActions();
    //Include any XML documentation
    c.IncludeXmlComments(GetDocumentationPath());
    //Ignore properties marked obsolete
    c.IgnoreObsoleteProperties();	
}

The GetDocumentationPath method returns back the path to the site bin directory where the XML files are generated by default.

private static string GetDocumentationPath ()
{
    return Path.Combine(HttpRuntime.BinDirectory, "SwaggerDemo.xml");
}  

There are other options available including security, injecting additional styling and changing how operations and types are generated.

UI

Swashbuckle includes Swagger.UI which is a simple UI for allowing you to browse the API and, more importantly, test it. Browse to ~/swagger and it will redirect you to the UI where you can view and test the exposed APIs. Under the hood it is using the specification from the API controller to expose the information. I personally don’t like the defaults so we’re going to change them. Firstly I would prefer that the URL doesn’t mention Swagger and instead is more meaningful so open the SwaggerConfig file and find the call to EnableSwaggerUI. The code is pretty well commented to allow you to configure the UI settings. For our purposes we’ll modify the call to pass a string as the first parameter. The string is the URL we want to use.

.EnableSwaggerUi("help/ui/{*assetPath}", c =>

This will change the URL to simply ~/help/ui. For projects that have no MVC controllers (like this one) it makes sense to simply send the user to the documentation so also modify the home controller to redirect to the help instead of showing a default view.

public ActionResult Index ()
{
    return RedirectPermanent("~/help/ui/index");
}

Notice the index at the end. That is the asset path that was in the earlier URL. We can configure other aspects of the UI as well if we want. For my purposes I don’t want the validators to run and I want the operation list to start expanded.

.EnableSwaggerUi("help/ui/{*assetPath}", c => 
{
    //Do not use online Swagger validation
    c.DisableValidator();
    //Expand the list of operations by default
    c.DocExpansion(DocExpansion.List);
}

Swashbuckle provides us a limited set of options to change. If you want to change other options in the UI then you either need to hack the code or use a different UI altogether.

Documenting the APIs

Out of the box Swagger will use the XML documentation that can be generated by your assemblies. Basically all you need to do is add summary, param, returns and optionally remarks comments to the API controllers, actions and models and you have instant documentation. The downside to this approach is the same as discussed in the earlier article on documenting Web API calls. Therefore I want to port the ReflectionDocumentationProvider from the earlier code over to Swagger. This is actually pretty easy since all the hard work is already done. All we have to do is implement 2 methods. The hard part is knowing what OpenAPI expects. The first thing we need to do is update the interface list for the type. IDocumentationProvider is still valid but IModelDocumentationProvider is replaced by IModelFilte and we add IOperationFilter. The new interfaces each have a single method that simply asks for the documentation for the operation or model, accordingly.

public class ReflectionDocumentationProvider : IDocumentationProvider, IOperationFilter, IModelFilter
{
    public void Apply ( Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription )
    {
        operation.summary = GetDocumentation(apiDescription.ActionDescriptor);
        
	//Parameters
        if (operation.parameters != null)
        {
            foreach (var parameter in apiDescription.ParameterDescriptions)
            {
                var opParam = operation.parameters.FirstOrDefault(p => String.Compare(p.name, parameter.Name, true) == 0);
                if (opParam != null)
                    opParam.description = GetDocumentation(parameter.ParameterDescriptor);
            };
        };
        
	//Return value
        if (apiDescription.ActionDescriptor.ReturnType != null)
        {
            operation.responses.Clear();
            operation.responses["200"] = new Response()
            {
	            description = GetResponseDocumentation(apiDescription.ActionDescriptor),
                schema = new Schema()
                {
                    @ref = apiDescription.ActionDescriptor.ReturnType.FullName
                }
            };
        };
    }
    
    public void Apply ( Schema model, ModelFilterContext context )
    {
        model.description = GetDocumentation(context.SystemType);
    
	//Properties
	model.properties.Clear();
        foreach (var property in context.SystemType.GetProperties())
        {
            model.properties[property.Name] = new Schema()
            {
                description = GetDocumentation(property)
            };
        };
    }
}

For operations we use reflection to get the description associated with the action. For each parameter and the return value we do the same thing. For models (types) we follow a similar approach except we look at the properties. In the previous article I used the standard ComponentModel types but now it probably makes sense to use custom attributes instead. So we introduce a couple of new attributes that can be used to apply documentation to the parameters and return value of an action without the more verbose use of per-parameter attribute. I find it cleaner.

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class RestParameterAttribute : Attribute
{
    public RestParameterAttribute ( string name, string description )
    {
        Name = name ?? "";
        Description = description ?? "";
    }

    public string Description { get; private set; }
    public string Name { get; private set; }        
}

[AttributeUsage(AttributeTargets.Method)]
public class RestResponseAttribute : Attribute
{
    public RestResponseAttribute ( string description ) : this(description, null)
    {
    }

    public RestResponseAttribute ( string description, Type responseType )
    {
        Description = description ?? "";
        Type = responseType;
    }

    public string Description { get; private set; } 
    public Type Type { get; private set; }
}

Here’s how we might apply it.

[RestParameter("id", "The product identifier.")]
[RestResponse("The product, if any.")]
public Product Get ( int id )
{
}

The reflection provider needs to be updated to look for these new attributes. To make it easier to use we will use the attributes if available or fall back to the standard ComponentModel attributes otherwise.

public virtual string GetDocumentation(HttpParameterDescriptor parameterDescriptor)
{
    var attr = parameterDescriptor.ActionDescriptor?.GetCustomAttributes()
						.FirstOrDefault(a => String.Compare(a.Name, parameterDescriptor.ParameterName, true) == 0);
    if (attr != null)
        return attr.Description;
    var reflectedParameterDescriptor = parameterDescriptor 
                                            as ReflectedHttpParameterDescriptor;
    return reflectedParameterDescriptor.ParameterInfo.GetDescription();            
}
public string GetResponseDocumentation(HttpActionDescriptor actionDescriptor)
{
    var attr = actionDescriptor.GetCustomAttributes().FirstOrDefault();
    if (attr != null)
        return attr.Description;
    var returnInfo = GetMethod(actionDescriptor);
    if (returnInfo == null)
        return null;            
    return returnInfo.ReturnTypeCustomAttributes.GetDescription();
}

Now we need to modify Swagger to use it. Normally Swagger uses the standard IDocumentationProvider registered for Web API. However Swashbuckle stomps over that, unfortunately. This is the one area that I find Swashbuckle to be sorely lacking. I want to use it for its combination of Swagger and Swagger UI but I don’t like how it limits access to the core stuff. This actually causes us a problem when it comes to models. To change the documentation provider we need to add an operation filter to Swagger. We also need to hook into the model filters but unfortunately Swashbuckle marks them internal. If we were using Swagger directly we could handle this all directly but instead we’ll have to resort to some reflection trickery to register our provider. To make it simple we’ll wrap it in an extension method.

internal static class SwaggerConfigExtensions
{
    public static SwaggerDocsConfig UseReflectionDocumentation ( this SwaggerDocsConfig source )
    {
        //Register documentation provider
        source.OperationFilter(() => ReflectionDocumentationProvider.Default);
        
        //HACK: This is a hack to get access to the model filters because it isn't public
        //source.ModelFilters();
        var modelFilters = source.GetType().GetField("_modelFilters", BindingFlags.NonPublic | BindingFlags.IgnoreCase | BindingFlags.Instance);
        var filters = modelFilters?.GetValue(source) as System.Collections.Generic.IList<Func>;
        if (filters != null)
        {
            filters.Add(() => ReflectionDocumentationProvider.Default);
        } else
            Debug.WriteLine("Unable to get model filters from SwaggerDocsConfig, model documentation will be unavailable.");

        return source;
    }
}

Finally we can update the configuration to use the new provider.

    //c.IncludeXmlComments(GetDocumentationPath());
    c.UseReflectionDocumentation();

Caching

The final area to look at is performance. Reflection isn’t fast and the documentation is really static once compilation occurs. To speed up documentation generation we could cache the generated Swagger information. Swashbuckle exposes this option as a custom provider. We simply need to implement a simple caching provider to generate the documentation once.

internal class CachingSwaggerProvider : ISwaggerProvider
{
    public CachingSwaggerProvider ( ISwaggerProvider defaultProvider )
    {
        m_defaultProvider = defaultProvider;
    }
    public SwaggerDocument GetSwagger ( string rootUrl, string apiVersion )
    {
        var cacheKey = $"SwaggerDoc-{apiVersion}";
        //Check cache first
        var doc = HttpRuntime.Cache.Get(cacheKey) as SwaggerDocument;
        if (doc == null)
        {
            doc = m_defaultProvider.GetSwagger(rootUrl, apiVersion);
            HttpRuntime.Cache[cacheKey] = doc;
        };
        return doc;
    }
    private readonly ISwaggerProvider m_defaultProvider;        
}

And the changes to the Swagger config.

c.CustomProvider((defaultProvider) => new CachingSwaggerProvider(defaultProvider));

Final Thoughts

I’m still figuring my way through Swagger so there may be issues with the code when using it with larger code blocks and for some situations. Things that come to mind include

  • Documenting non-success responses
  • Providing error details
  • Custom model types that map to standard JSON types
  • Enumerable lists (currently generates an error in the documentation
  • Providing a custom RestDescription for type, member descriptions
  • More Swagger documentation including examples

Code