ASP.NET Web API: Generating a Web API help page using ApiExplorer

In the previous post, I briefly introduced you to the IApiExplorer which can be used to generate documentation, machine-readable metadata, or a test client. In this blog post, we’re going to take a closer look at the ApiExplorer, which is the default implementation of IApiExplorer, and see how we can use it to generate a simple help page. A help page is nothing more than a web-based documentation for your web APIs. It can contain information like the resource URIs, the allowed HTTP methods and the expected parameters so that other developers who consume your APIs know how to call them. What’s great about this help page is that it will automatically update the content when you modify your APIs.

Now let’s get started by looking at the ApiExplorer.

ApiExplorer

The main goal of this class is to produce a collection of ApiDescription. It does so by statically inspecting the routes and the available actions inside your controllers. Each ApiDescription describes an API that is available on your service. As you can see from the simplified class diagram below, the ApiDescription contains basic information such as the HttpMethod, the RelativePath, the Documentation, etc. But it also points to an ActionDescriptor which is part of the core Web API component that knows everything about an action. You can use it to access a lot more information such as the action name, the return type, the custom attributes, etc. Similarly, you can access the ParameterDescriptor for the expected parameters.

image

Now, let’s see how we can use it to generate a help page.

Generating the help page

For the sake of simplicity, I’m going to assume your service is web-hosted side-by-side with MVC much like our default template. See “Other implementations” section below for ideas on how to generate a help page on self-hosted services.

Sample

Now, I’ll use the default “Web API” template as the starting point for the code sample.

projectTemplate

By default the template project comes with a MVC HomeController and a Web API ValuesController. Let’s modify the Index action of the HomeController to display the help page.

Step 1: Getting the ApiExplorer and passing it to the view

Let add the following two lines to the Index action in the HomeController.

    1: public ActionResult Index()
    2: {
    3:     var apiExplorer = GlobalConfiguration.Configuration.Services.GetApiExplorer();
    4:     return View(apiExplorer);
    5: }

Step 2: Customizing the view to display the APIs

In the Index.cshtml, we can specify the IApiExplorer as the type of the Model.

    1: @model System.Web.Http.Description.IApiExplorer

Then we can iterate through the Model.ApiDescriptions to display the supported HTTP method, the relative URL, the documentation and the expected parameters.

    1: @foreach (var api in Model.ApiDescriptions)
    2: {
    3:     <h5>@api.HttpMethod @api.RelativePath</h5>
    4:     <p>@api.Documentation</p>
    5:     @if (api.ParameterDescriptions.Count > 0)
    6:     {
    7:         <h6>Parameters</h6>
    8:         <ul>
    9:         @foreach (var parameter in api.ParameterDescriptions)
   10:         {
   11:             <li>@parameter.Name: @parameter.Documentation (@parameter.Source)</li>
   12:         }
   13:         </ul>
   14:     }
   15: }

And of course you can customize the HTML to make it slightly prettier. Here is the complete code snipped for the view.

    1: @model System.Web.Http.Description.IApiExplorer
    2: <div id="body">
    3:     <section class="featured">
    4:         <div class="content-wrapper">
    5:             <hgroup class="title">
    6:                 <h1>ASP.NET Web API Help Page</h1>
    7:             </hgroup>
    8:         </div>
    9:     </section>
   10:     <section class="content-wrapper main-content clear-fix">
   11:         <h3>APIs</h3>
   12:         <ul>
   13:         @foreach (var api in Model.ApiDescriptions)
   14:         {
   15:             <li>
   16:             <h5>@api.HttpMethod @api.RelativePath</h5>
   17:             <blockquote>
   18:             <p>@api.Documentation</p>
   19:             @if (api.ParameterDescriptions.Count > 0)
   20:             {
   21:                 <h6>Parameters</h6>
   22:                 <ul>
   23:                 @foreach (var parameter in api.ParameterDescriptions)
   24:                 {
   25:                     <li>@parameter.Name: @parameter.Documentation (@parameter.Source)</li>
   26:                 }
   27:                 </ul>
   28:             }
   29:             </blockquote>
   30:             </li>
   31:         }
   32:         </ul>
   33:     </section>
   34: </div>

Update 9/27/12: If you’re using .NET 4.5, you’ll need to add the reference to System.Net.Http in your web.config. Otherwise you might get an error like the following: “CS0012: The type 'System.Net.Http.HttpMethod' is defined in an assembly that is not referenced. You must add a reference to assembly 'System.Net.Http, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'”. This is because in 4.5, System.Net.Http.dll is coming from the GAC instead of a NuGet package and Razor doesn’t include the reference automatically like it does when the assembly is coming from a NuGet package.

 <system.web>
     <compilation debug="true">
         <assemblies>
             <add assembly="System.Net.Http, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
         </assemblies>
     </compilation>
 </system.web>

Now when you run the application, you should see the following help page instead.

image

If you look closer, the API documentation simply says “Documentation for XYZ” which is not very helpful. Let’s add some real documentation for our APIs.

Step 3: Providing the documentation

When generating the documentation for an API, ApiExplorer asks the IDocumentationProvider to supply the content. IDocumentationProvider is an abstraction that let you define your own source of documentation (e.g. database, custom attributes, files, etc). For instance, you can implement a custom IDocumentationProvider that let you specify the documentation through attributes. Here, I’ll show you something better, I’ve implemented a simple IDocumentationProvider (XmlCommentDocumentationProvider) that will grab the documentation from C# XML Documentation Comments.

    1: using System.Linq;
    2: using System.Reflection;
    3: using System.Text.RegularExpressions;
    4: using System.Web.Http.Controllers;
    5: using System.Web.Http.Description;
    6: using System.Xml.XPath;
    7:  
    8: namespace System.Web.Http
    9: {
   10:     public class XmlCommentDocumentationProvider : IDocumentationProvider
   11:     {
   12:         XPathNavigator _documentNavigator;
   13:         private const string _methodExpression = "/doc/members/member[@name='M:{0}']";
   14:         private static Regex nullableTypeNameRegex = new Regex(@"(.*\.Nullable)" + Regex.Escape("`1[[") + "([^,]*),.*");
   15:  
   16:         public XmlCommentDocumentationProvider(string documentPath)
   17:         {
   18:             XPathDocument xpath = new XPathDocument(documentPath);
   19:             _documentNavigator = xpath.CreateNavigator();
   20:         }
   21:  
   22:         public virtual string GetDocumentation(HttpParameterDescriptor parameterDescriptor)
   23:         {
   24:             ReflectedHttpParameterDescriptor reflectedParameterDescriptor = parameterDescriptor as ReflectedHttpParameterDescriptor;
   25:             if (reflectedParameterDescriptor != null)
   26:             {
   27:                 XPathNavigator memberNode = GetMemberNode(reflectedParameterDescriptor.ActionDescriptor);
   28:                 if (memberNode != null)
   29:                 {
   30:                     string parameterName = reflectedParameterDescriptor.ParameterInfo.Name;
   31:                     XPathNavigator parameterNode = memberNode.SelectSingleNode(string.Format("param[@name='{0}']", parameterName));
   32:                     if (parameterNode != null)
   33:                     {
   34:                         return parameterNode.Value.Trim();
   35:                     }
   36:                 }
   37:             }
   38:  
   39:             return "No Documentation Found.";
   40:         }
   41:  
   42:         public virtual string GetDocumentation(HttpActionDescriptor actionDescriptor)
   43:         {
   44:             XPathNavigator memberNode = GetMemberNode(actionDescriptor);
   45:             if (memberNode != null)
   46:             {
   47:                 XPathNavigator summaryNode = memberNode.SelectSingleNode("summary");
   48:                 if (summaryNode != null)
   49:                 {
   50:                     return summaryNode.Value.Trim();
   51:                 }
   52:             }
   53:  
   54:             return "No Documentation Found.";
   55:         }
   56:  
   57:         private XPathNavigator GetMemberNode(HttpActionDescriptor actionDescriptor)
   58:         {
   59:             ReflectedHttpActionDescriptor reflectedActionDescriptor = actionDescriptor as ReflectedHttpActionDescriptor;
   60:             if (reflectedActionDescriptor != null)
   61:             {
   62:                 string selectExpression = string.Format(_methodExpression, GetMemberName(reflectedActionDescriptor.MethodInfo));
   63:                 XPathNavigator node = _documentNavigator.SelectSingleNode(selectExpression);
   64:                 if (node != null)
   65:                 {
   66:                     return node;
   67:                 }
   68:             }
   69:  
   70:             return null;
   71:         }
   72:  
   73:         private static string GetMemberName(MethodInfo method)
   74:         {
   75:             string name = string.Format("{0}.{1}", method.DeclaringType.FullName, method.Name);
   76:             var parameters = method.GetParameters();
   77:             if (parameters.Length != 0)
   78:             {
   79:                 string[] parameterTypeNames = parameters.Select(param => ProcessTypeName(param.ParameterType.FullName)).ToArray();
   80:                 name += string.Format("({0})", string.Join(",", parameterTypeNames));
   81:             }
   82:  
   83:             return name;
   84:         }
   85:  
   86:         private static string ProcessTypeName(string typeName)
   87:         {
   88:             //handle nullable
   89:             var result = nullableTypeNameRegex.Match(typeName);
   90:             if (result.Success)
   91:             {
   92:                 return string.Format("{0}{{{1}}}", result.Groups[1].Value, result.Groups[2].Value);
   93:             }
   94:             return typeName;
   95:         }
   96:     }
   97: }

First, you’ll need to wire-up the custom IDocumentationProvider. A simple way of doing that is through HttpConfiguration. Notice that XmlCommentDocumentationProvider needs to know the path of your XML documentation file.

    1: var config = GlobalConfiguration.Configuration;
    2: config.Services.Replace(typeof(IDocumentationProvider), 
    3:     new XmlCommentDocumentationProvider(HttpContext.Current.Server.MapPath("~/App_Data/MyApp.xml")));

You can make sure that the XML documentation file is enabled by going to the project properties.

image

Update 11/03/12: When deploying the service, make sure you include the XML documentation file as part of the project and set the “Copy to Output Directory” in the file properties to “Copy always” or “Copy if newer”.

After that, make sure your APIs are documented using XML comments.

image

Finally, run the sample again, and voila, the documentation from your XML comment shows up on your help page.

image

 

Hiding a controller/action from ApiExplorer

If for any reason, you don’t want an API to show up on the help page, you can exclude it by using the ApiExplorerSettingsAttribute instead of going through the ApiDescription collection and deleting it.

    1: public class ValuesController : ApiController
    2: {
    3:     [ApiExplorerSettings(IgnoreApi = true)]
    4:     public void MySpecialAction()
    5:     {
    6:     }

Similarly you can declare the attribute on a controller and all the actions in that controller will be hidden from ApiExplorer.

    1: [ApiExplorerSettings(IgnoreApi = true)]
    2: public class MySpecialController : ApiController
    3: {

Other implementations

What I showed you above is just one way of implementing the help page. There’re other ways you can implement it. Here is another idea:

  • Create a custom ApiController, you can call it HelpController. Within the controller, have a GET action that returns the API information (you can choose to have it in different formats, including HTML). Internally, the HelpController can just use ApiExplorer to get all the information it needs. The advantage of this approach is that it would work for both self-hosted and web-hosted services.

 

Hope this helps,

Yao