Share via


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

  • Anonymous
    May 21, 2012
    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

  • Anonymous
    May 24, 2012
    Created new ASP.NET MVC 4 WebAPI Project and resolved GlobalConfiguration...

  • Anonymous
    May 24, 2012
    Very nice article.

  • Anonymous
    May 25, 2012
    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

  • Anonymous
    May 26, 2012
    This is great! I plan to use it on http://domlia.com

  • Anonymous
    May 29, 2012
    why not to generate WADL like documentation?

  • Anonymous
    May 29, 2012
    @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.

  • Anonymous
    June 12, 2012
    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

  • Anonymous
    June 17, 2012
    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.

  • Anonymous
    August 27, 2012
    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

  • Anonymous
    August 28, 2012
    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.

  • Anonymous
    August 28, 2012
    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

  • Anonymous
    August 29, 2012
    @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.

  • Anonymous
    October 01, 2012
    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!

  • Anonymous
    October 02, 2012
    @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
  • Anonymous
    October 09, 2012
    Just a small note to say thank you for this - both informative and interesting!

  • Anonymous
    October 15, 2012
    I have the same comment as @Lobo Junior. Would like to have the documentation on a different site.

  • Anonymous
    November 02, 2012
    I am getting Could not find file 'D:AppsWebAPICRUDCRUDApp_DataCRUD.xml'.

  • Anonymous
    November 03, 2012
    @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?

  • Anonymous
    November 21, 2012
    The comment has been removed

  • Anonymous
    December 02, 2012
    @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

  • Anonymous
    January 16, 2013
    @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

  • Anonymous
    January 16, 2013
    @naresh I'm planning to publish a sample for that this weekend so stay tuned :)

  • Anonymous
    January 20, 2013
    @naresh I just published the sample. See blogs.msdn.com/.../design-time-generation-of-help-page-or-proxy-for-asp-net-web-api.aspx for more information. Thanks, Yao

  • Anonymous
    February 02, 2013
    ApiExplorer ignores actions with complex type parameters in URI. So I can't generate help for these actions.

  • Anonymous
    February 05, 2013
    @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

  • Anonymous
    March 19, 2013
    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

  • Anonymous
    May 07, 2013
    As reported Enums in the RelativePath property of class ApiDescription do not display properly.  Any ETA on this being fixed soon?

  • Anonymous
    May 08, 2013
    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.

  • Anonymous
    May 08, 2013
    Hi Steve, This is fixed in our CodePlex branch aspnetwebstack.codeplex.com/.../60200c1b936ee888fe1d7cf9d4a182c771f90e5b You can try it out using our nightly build: aspnetwebstack.codeplex.com/wikipage

  • Anonymous
    May 30, 2013
    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

  • Anonymous
    June 07, 2013
    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).

  • Anonymous
    August 11, 2013
    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 ?

  • Anonymous
    August 14, 2013
    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.

  • Anonymous
    November 04, 2013
    The comment has been removed

  • Anonymous
    February 10, 2014
    Dear Sir, How can I access the <returns>whatever</returns> and <remarks></remarks> from the xml comments? Thanks Nilesh Ahir

  • Anonymous
    May 27, 2014
    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.

  • Anonymous
    June 24, 2014
    Very helpful. Can u shed some light on how to give custom css to the page?

  • Anonymous
    December 04, 2014
    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

  • Anonymous
    February 16, 2015
    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