Documenting Web APIs

Published on Author Michael

Prerequisite: This post assumes you understand what web APIs are, what they are designed for and how to write them. If not then take a look at this MSDN article first.

In order for web APIs to be useful they need documentation. Unlike traditional APIs there is no function signatures to look at. Even if you know the name of the “function” there is no easy way to get the parameters or return value. Hence documentation is important to anyone wanting to use such an API. If you are writing a REST API then the documentation is technically where the HATEOAS concept comes in. The reality is, for now at least, most people write web APIs using the REST API philosophy but without HATEOAS. Hence documentation is still needed.

The biggest issue with documentation is keeping it in sync with the code. We have XML doc tags for .NET languages and can use them with web API. But to be useful we have to ensure the documentation is refreshed whenever the API changes.  ASP.NET solves this problem by relying on the existing XML doc tags that you’re likely already using. At runtime the documentation is rendered on demand based upon the existing comments. This is one approach to solving the problem. In this article I will take a look at the “built in” functionality, identify when it might not be useful to you and provide an alternative approach that I use.

Web API Help Page

To get started go ahead and create a new web API project (steps are for Visual Studio 2015).

  1. Create a new project.
  2. Select a project type of Web and then ASP.NET Web Application.
  3. Give the project whatever name you want.
  4. For the template select Web API instead of MVC, notice that MVC is automatically included anyway.
  5. For this demo we do not need any authentication so set the authentication to none.

After the project is generated you’ll notice it has an area called HelpPage. This is where ASP.NET puts the documentation for your APIs. To make this easier to get to, and since we aren’t going to be hosting any visible UI, we’ll redirect the site to this page.  Go to HomeController.Index and redirect to the help page.

public ActionResult Index ()
{
    return RedirectToAction("Index", "Help", new { area = "HelpPage" });
}

The project template will have already created a default API controller for you but let’s set up a more complicated one to demonstrate the features of the documentation provider.

  1. Remove the existing ValueController.
  2. Now add a new controller called CountryController.  Traditionally API controllers are kept in a subfolder called Api so that is how I will set my project up.
  3. Now add the code below so we have some sample data to play with.  The repository type is a simple in-memory table of values and can be obtained in the sample code.
[RoutePrefix("api/countries")]
public class CountryController : ApiController
{
    [HttpGet]
    [Route("")]
    public IEnumerable GetAll ()
    {
        return CountryRepository.Default.GetAll();
    }

    [HttpGet]
    [Route("{id:int}")]
    public Country Get ( int id )
    {
        return CountryRepository.Default.Get(id);
    }
}

And the models.

public class Country
{
    public Country ()
    {
        Regions = new List();
    }

    public int Id { get; set; }

    [Required(AllowEmptyStrings = false)]
    public string Name { get; set; }

    [Required(AllowEmptyStrings = false)]
    [StringLength(2, MinimumLength = 2)]
    public string IsoCode { get; set; }

    public List Regions { get; private set; }
}

public class Region
{
    public int Id { get; set; }

    [Required(AllowEmptyStrings = false)]
    public string Name { get; set; }
}

When the site loads you should get a list of the available web API URLs. Clicking on a URL should take you to a detail page which provides a description of the action, the parameters, and any request/response body. For primitive types the JSON equivalent is given but for custom types a link to the type definition is given.

Now let’s take a closer look at the code involved. Firstly notice that there is a lot of code in the area. Ideally this code should reside in one of the web API assemblies but for now it is copied into each project. Note that there is a namespace called System.Web.Http.Description in the core framework for web API. The help page uses types in this namespace in combination with custom code to produce the output.

The core type is HelpPageConfigurationExtensions.  This extension class registers the various components needed to produce documentation. Some of the core registrations include:

  • IDocumententationProvider – Responsible for getting the documentation for controllers, actions and types
  • HelpPageSampleGenerator – Responsible for generating sample data and calls (part of the project code).
  • ModelDescriptionGenerator – Responsible for generating the model descriptions given the documentation.

Any and all these types can be replaced to customize the behavior of the help UI as we’ll see shortly.

Default Documentation Provider

The default documentation provider is called XmlDocumentationProvider. It is part of the project and relies on the XML doc tags that are optionally associated with the controller and action. To demonstrate this go ahead and add some summary, parameter and return tags to the controller, actions and models. In order for the provider to work you have to follow a few setup steps.

  1. Go to the project’s Build settings and enable the generation of the documentation XML.
  2. In the HelpPageConfig.Register method uncomment the line to enable the documentation provider and set the path to the XML file (default will be the output directory).
  3. Make any other adjustments that are desired.

When running the site the comments should now appear in the documentation. For custom types like Country notice there is a link to the type definition. Also notice that any data annotations are also listed.

The advantages of the default provider are many.

  • No code to write.
  • Relies on the standard documentation that you are used to writing.

But there are some cons to this approach as well.

  • Have to enable comment generation and ensure the XML file is available.
  • Turning on comment generation means that either all code must be commented or you have to ignore compiler warnings about missing documentation.
  • Have to add comments to code that traditionally doesn’t use doc comments (i.e. controllers, action methods)

Overall the default provider is a solid choice. There are probably few reasons not to use it. But that is the nice thing about the implementation, we can change it. As part of my deployment process I remove all the documentation XML as it is not really needed. I also tend not to have documentation turned on for sites because nobody needs them. For these reasons I have created a new documentation provider that can be used instead.

Reflection Documentation Provider

The new provider is reflection based. We can argue whether XPath queries or reflection is faster but since this is a documentation site and the data is cached it shouldn’t really matter. The advantages of reflection are

  • Don’t need documentation generation turned on.
  • Do not need any external file.

A problem with reflection is that documentation is not generally part of the metadata. But we can use the existing documentation metadata attributes to accomplish this. There are any number of approaches we could take but I decided to go with the simple route of using DescriptionAttribute as the documentation indicator. It works with everything, is built into the framework and describes what we are trying to do.

To start with we need to create a new type that implements IDocumententationProvider. There are a number of methods to implement but none of them are very hard. The GetDocumentation method is called to get the documentation for the various elements including the controller, action, types and parameters. In each case we will use reflection to get the DescriptionAttribute, if any. If we get the attribute then we return the description otherwise we return null which displays a default message.

public class ReflectionDocumentationProvider : IDocumentationProvider
                                                , IModelDocumentationProvider
{        
    public string GetDocumentation(HttpControllerDescriptor controllerDescriptor)
    {
        return controllerDescriptor.ControllerType.GetDescription();
    }

    public virtual string GetDocumentation(HttpActionDescriptor actionDescriptor)
    {
        return GetMethod(actionDescriptor).GetDescription();
    }

    public virtual string GetDocumentation(HttpParameterDescriptor parameterDescriptor)
    {
        var reflectedParameterDescriptor = parameterDescriptor 
                                                as ReflectedHttpParameterDescriptor;

        return reflectedParameterDescriptor.ParameterInfo.GetDescription();            
    }

    public string GetResponseDocumentation(HttpActionDescriptor actionDescriptor)
    {
        var returnInfo = GetMethod(actionDescriptor);
        if (returnInfo == null)
            return null;

        return returnInfo.ReturnTypeCustomAttributes.GetDescription();
    }

    public string GetDocumentation(MemberInfo member)
    {
        return member.GetDescription();
    }

    public string GetDocumentation(Type type)
    {
        return type.GetDescription();            
    }

    #region Private Members
        
    private MethodInfo GetMethod(HttpActionDescriptor actionDescriptor)
    {
        var reflectedActionDescriptor = actionDescriptor as ReflectedHttpActionDescriptor;
        if (reflectedActionDescriptor != null)
            return reflectedActionDescriptor.MethodInfo;
            
        return null;
    }
    #endregion
}

public static class CustomAttributeProviderExtensions
{
    public static T GetAttribute ( this ICustomAttributeProvider source ) 
                                        where T : Attribute
    {
        if (source == null)
            return null;

        var attrs = source.GetCustomAttributes(typeof(T), true);

        return (attrs != null) ? attrs.OfType().FirstOrDefault() : null;
    }

    public static string GetDescription ( this ICustomAttributeProvider source )
    {
        if (source == null)
            return null;

        var attr = source.GetAttribute();

        return (attr != null) ? attr.Description : null;
    }
}

To use the new provider we need to do the following.

  1. (Optional) Go to the project’s Build settings and turn off documentation generation.
  2. In the HelpPageConfig.Register method replace the existing provider with our new provider.
  3. Replace the existing XML doc comments with a DescriptionAttribute on each controller, action, model and property.

Note that the return value and parameters must each have a DescriptionAttribute as well. The syntax is as follows.

[return: Description("The country, if found.")]
public Country Get ( [Description("The ID of the country.")] int id )
{
    return CountryRepository.Default.Get(id);
}

When we run the site we should see the same behavior as before but it is using reflection now.

Changing Type Names

If you look at the project code you’ll find the ModelNameAttribute type. This attribute changes the type name that is displayed. It is very common to use a model as the parameter or return type rather than the underlying data. In these situations we often append –Model to the end of the type. But that doesn’t look good in an API call so you can use this attribute to change the displayed name.

[ModelName("Region")]
public class RegionModel
{
}

For JSON this is fine but note that XML will continue using the original type name. If you don’t support XML then this is a good approach.

An alternative approach to changing the type name is to use ModelDescriptionGenerator. This type has a field called DefaultTypeDocumentation that maps the primitives to API-friendly names. You can extend it to change the name of any type given. There are some restrictions to this however.

  1. The field is private so you have to modify the existing code.
  2. If you change the type name then it will no longer link to the underlying type.

The second restriction may be a benefit or not. There are some cases where you might want to use a type but have the field exposed as, say a string. A good example might be the ISO code. We might have a struct that backs the ISO code and has some validation. But for purposes of the API we treat it as a string. By modifying the private field we can tell the generator to treat our ISO code type as a string.

Ideally the generator should be an interface and we could override the implementation but the generated code does not work that. That would be a useful enhancement unto itself but it would require changes to the other generated code.

Data Annotations

Another enhancement you might want to make is around the displayed annotations. As with the type information, this is done in the ModelDescriptionGenerator type. As with type information it is also done through a hidden field and not extensible. To add additional annotations you need to either modify the type itself or create a new type and change the generated code.

Enhancements

There are many enhancements that can be done to the given code, and the generated project template code. The first enhancement that comes to mind is to move all the core logic into a separate assembly. This would simplify the site and allow for better code sharing.

An enhancement for the reflection provider is to allow support for other attributes. In this article I used DescriptionAttribute because it was readily available and it works but you might consider using something else like perhaps the data contract attributes.

Yet another enhancement that would really make the help pages more useful would be to allow the actions to actually be called. Currently clicking on the various links simply navigate you to the documentation but it would be nice if you could click a link, fill in the parameter and body elements and trigger a call to the API. This is especially useful for non-GET requests since browsers don’t support that without client side code. Sure you can use a separate tool like SoapUI but it would be nice to be able to do it all from within the help pages themselves. .

Conclusion

Overall I think the inclusion of automated documentation generation is useful for web API projects. I wish the existing logic was more extensible, didn’t rely on documentation comments and was self contained. Fortunately we can fix this ourselves through a little effort. Perhaps I’ll blog about my work in that area later. Until then feel free to use the reflection provider if you want. If the documentation provider is already working for you then there is no great benefit in switching over but it is nice to have an alternative.

WebAPIDemo.zip