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

Comments (40)

  1. Fshrking says:

    Aweosome Yao-

    Thanks so much for writing this. We're building out a new API now and have been hoping to use this feature.

    Cheers

  2. System.Web.Http.HttpConfiguration' does not contain a definition for 'Services' says:

    Created new ASP.NET MVC 4 WebAPI Project and resolved GlobalConfiguration…

  3. Thanigainathan says:

    Very nice article.

  4. Please note that ApiExplorer is currently available on our CodePlex bits as I mentioned in the previous post. You can try it out by installing our nightly NuGet packages: aspnetwebstack.codeplex.com/…/353867

    Henrik has a nice post on how to install these packages: blogs.msdn.com/…/using-nightly-nuget-packages-with-asp-net-web-stack.aspx

  5. Domlia says:

    This is great! I plan to use it on http://domlia.com

  6. roni says:

    why not to generate WADL like documentation?

  7. @roni

    The sample help page in this post is intended to be human-readable. But yeah, you can also use ApiExplorer to generate machine-readable description like WADL if you'd like.

  8. Steve Guidi says:

    For what it is worth, I maintain a library that programmatically retrieves XML documentation comments from System.Reflection method descriptors (i.e. MethodInfo).  You can use this library to simplify some of the parsing you have shown above with regular expressions.

    For more information, see http://jolt.codeplex.com.  The feature documentation is here: jolt.codeplex.com/wikipage

  9. Paige Cook says:

    Also, noticed that when you publish you site, you will need to make sure that you include the .xml comment file and set the "Copy to Output Directory" setting in the file properties to either copy always or copy if newer so that it will be published with the rest of your site.

  10. It's a great article indeed,but I noticed that the documentation for methods with Enumeration parameters isn't correct;

    * Generated Doc using the XmlCommentDocumentationProvider class in the article:

    GET api/Users?email={email}

    Parameters

       email: No Documentation Found.

    * Expected generated doc:

    GET api/Users?email={email}&type={type} <—type is an enumeration

    Parameters

       email: Email address the user registered with

       type: type of the search; 1:Profile , 2:Posts , 3: Pictures

  11. Martin says:

    When I try to run the same (at the point it should just say "Documentation for XYZ" I get the following error:

    Method 'LogError' in type 'System.Web.Http.Validation.ModelStateFormatterLogger' from assembly 'System.Web.Http, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' does not have an implementation.

    I have not installed any nighly packages but have Microsoft ASP.NET MVC 4 (RC) installed via NuGet.

  12. Martin says:

    I fixed the problem by installing the Web APi Help Page NuGet package from your other post: blogs.msdn.com/…/introducing-the-asp-net-web-api-help-page-preview.aspx

    After installing this package I also had to get WebAPi.OData as I use {Queryable] attribute.

    Thanks Martin

  13. @alaa9jo

    Looks like this issue is related to aspnetwebstack.codeplex.com/…/312, which we're planning to fix in the next release. Thanks for reporting it.

  14. Lobo Junior says:

    How do to use external assembly to create API documentation.

    I have a big API using webAPI and want separate the documentation site and the API implemented.

    Tks, good blog! Go ahead!

  15. @Lobo Junior

    There're couple options you could try:

    1. Generate the API documentation statically as HTML files and host them on a separate site. (I'll try to publish a sample on how to do that sometime soon)

    OR

    2. Starts with the service and help page generation on the same host, save the generated help page as HTML, turn off the help page and host the saved documentation on a separate site.

    Hope this helps,

    Yao

  16. Just a small note to say thank you for this – both informative and interesting!

  17. steven suing says:

    I have the same comment as @Lobo Junior. Would like to have the documentation on a different site.

  18. Andrej says:

    I am getting

    Could not find file 'D:AppsWebAPICRUDCRUDApp_DataCRUD.xml'.

  19. @Andrej

    Can you try including the xml file to the project and set the "Copy to Output Directory" in the file properties to copy always?

  20. DSNuts says:

    I am working in VB.net and I cant find that "XML Documentation file" option in the project properties and I guess for that reason the comments are not showing up all I see is "documentation for get…" any workaround?Thanks

  21. @DSNuts

    Can you manually copy the XML documentation file to App_Data folder? Make sure you include the file into the project. The "Copy to Output Directory" option should be under the file properties and not project properties.

    Another option would be to provide the full path to your XML documentation file when you register the XmlCommentDocumentationProvider.

    Hope this helps,

    Yao

  22. naresh says:

    @Yao – Can you share the best way to host api and help files are separately. its only available for dev infrastructure not in uat/production>

    Thanks

    naresh

  23. @naresh

    I'm planning to publish a sample for that this weekend so stay tuned :)

  24. ApiExplorer ignores actions with complex type parameters in URI. So I can't generate help for these actions.

  25. @Evgeny

    ApiExplorer currently doesn't support complex types in URI. But, if the types are simple enough you can post-process the ApiExplorer.ApiDescriptions by reflecting on the complex URI parameter, construct the URI query and append it to the RelativePath property (msdn.microsoft.com/…/system.web.http.description.apidescription.relativepath(v=vs.108).aspx).

    Hope this helps,

    Yao

  26. Cata says:

    I've started using PATCH with OData support for partial updates. While the Api Explorer sees the method, the documentation is not being pulled as long as I use the Delta type as a parameter. I can see the method in the help page but under documentation it says "No Documentation Found". As soon as I change the parameter type, the documentation starts showing. Any thoughts as to why this is happening?

    Thanks

  27. Steve Stokes says:

    As reported Enums in the RelativePath property of class ApiDescription do not display properly.  Any ETA on this being fixed soon?

  28. Hi Cata,

    Currently the Help Page doesn't support entitysetcontrollers by default because our OData implementation uses different routing mechanism. However, we might consider providing the support in the future.

  29. Jelle says:

    Hi Yao,

    This project is very helpful for creating documentation, thanks for this time saver! I got one question tho.

    I am currently working on a project with 30+ (and growing) plugins. Every plugin is a different assembly and brings in additional System.Web.Http.ApiController instances. I am using the XmlCommentDocumentationProvider to load the XML generated from the in-code documentation but I don't understand how I can use multiple XML files. I have one XML file for every assembly (30+) and currently I can load only one XML file into the documentation provider.

    Is there a way to load multiple XML files?

    Thanks,

    Jelle

  30. Hi Jelle,

    One way of doing this is by composing the XmlDocumentationProvider. Imagine having something like this:

    public class MultipleXmlDocumentationProvider : IDocumentationProvider {

      XmlDocumentationProvider[] providers;

      public MultipleXmlDocumentationProvider(params string[] paths) {

          // new up an XmlDocumentationProvider for each path and add it to providers.

      }

      public virtual string GetDocumentation(HttpActionDescriptor actionDescriptor) {

          // call GetDocumentation on each provider in the providers until one of them returns a valid documentation string.

      }

      // implement the other overload in a similar fashion

    }

    Please take a look at our Help Page package (nuget.org/…/Microsoft.AspNet.WebApi.HelpPage) which offers a more polished implementation of XmlCommentDocumentationProvider (It's now called XmlDocumentationProvider).

  31. Marty says:

    Hi,

    Today I've bumped into a small issue. Api descriptors "loose" the "returns" information from the method description. It does take the Summary and property descriptions, but the part " /// <returns>HttpStatusCode, OK when done, NotFound when doesn't exist</returns>" is nowhere to be found..

    Any chances of this being "changed"  in near future ?

  32. Noel Fernandes says:

    Hi,

    Just wondering….Can we hide a controller/action from ApiExplorer via settings in web.config rather than attribute usage. That way I can have documentation turned on in a dev environment and turned off (can be done by the operations team) in a production environment.

  33. Richard says:

    I made it work for single descriptions in <summary> tags but failed to put more formatted information between the <summary> tags. Is it possible that I have (html) formatted information such as href, highlighted text, new lines …

    Thanks!

    Richard

  34. Nilesh Ahir says:

    Dear Sir,

    How can I access the <returns>whatever</returns> and <remarks></remarks> from the xml comments?

    Thanks

    Nilesh Ahir

  35. JS says:

    Your API Explorer posts have helped me out quite a bit, thank you!

    Would you know of an easy way to separate the documentation of a web service onto its own domain? I'm thinking I will need to use separate solutions, one to host the API and the other to host documentation. This requires moving the .xml file, .DLL references, etc to the documentation solution though.

  36. James says:

    Very helpful. Can u shed some light on how to give custom css to the page?

  37. Srinvias says:

    HI,

    i want to implement same for OData Web api, there i could not enable api attribute, is there any way can i implement same help page functionality with OData WEB Api????

    revert me kondreddy1983@gmail.com

    Thanks

    SrinivasulaReddy

  38. Sameena says:

    Hi,

    can you please let me know how to implement a help page/api for a cloud service.

    Like I will have only services in my azure project.no UI/Web project.azure role project is a web api.

    Please share if you have any samples…

    Thanks

    Sameena