Documenting your ASP.Net Web API’s

I’ve picked something up where Yao Huang Lin of Microsoft left off. For preliminary material, check out his blog and check out his posts on generating documentation.

In one of his later posts, he suggested creating a help controller. This is where I’ve picked things up. In Yao’s solution, he’s rendering html-based views. While that works well and makes for a nice presentation, I wanted to remain within the mode of just returning data, whether it is JSON or XML. Before continuing on with this post, please be sure to read Yao’s posts on the topic as I will be picking up where he left off on this post where he talks about other implemenations.

The first thing we need is a help controller.  Here is the one I’ve created:

using System.Collections.Generic;
using System.Net;
using System.Web.Http;
using System.Web.Http.Description;

namespace WebAPI.Controllers
{
 [ApiExplorerSettings(IgnoreApi = true)]
 public class HelpController : ApiController
 {
  public List Get()
  {
   return APIDocumentationRepository.Get();
  }

  public APIEndPoint Get(string api)
  {
   return APIDocumentationRepository.Get(api);
  }
 }
}

Nothing all that complicated here. Like all good controllers, this one is thin – with just enough logic to expose and service the end points. I’ve created an APIDocumenationRepository Class to handle all of the data-related operations. One point to focus on is the attribute: [ApiExplorerSettings(IgnoreApi = true)]. We don’t want the help controller itself to appear in the documentation. No need to do that since in order to get to the help documentation, you need to know the help endpoint exists in the first place!

There are two endpoints: one to get all of the endpoints and another to get a specific endpoint. In my earlier posts, I was referencing a simple Products Controller. I’m continuing to use that same controller here. For review, here is the listing for that controller:

using System;
using System.Linq;
using System.Net.Http;
using System.Web.Http;
using WebApi.Models;

namespace WebApi.Controllers
{
 public class ProductsController : ApiController
 {
  /// <summary>
  /// Returns the Product Collection.
  /// </summary>
  /// <returns></returns>
  [Queryable]
  public IQueryable<Product> GetProducts()
  {
   return ProductsRepository.data.AsQueryable();
  }

  /// <summary>
  /// Returns an individual Product.
  /// </summary>
  /// <param name="id">The Product id.</param>
  /// <returns></returns>
  public Product GetProduct(int id)
  {
   try
   {
    return ProductsRepository.get(id);
   }
   catch (NotFoundException)
   {
    throw new HttpResponseException(new HttpResponseMessage()
    {
     StatusCode = System.Net.HttpStatusCode.NotFound
    });
   }
  }

  /// <summary>
  /// Deletes the Products Collection and reverts back to original state.
  /// </summary>
  /// <returns></returns>
  [HttpDelete]
  public void ResetProducts()
  {
   ProductsRepository.reset();
  }

  /// <summary>
  /// Deletes an individual Product.
  /// </summary>
  /// <param name="id">The Product id.</param>
  /// <returns></returns>
  public void DeleteProduct(int id)
  {
   try
   {
    ProductsRepository.delete(id);
   }
   catch (NotFoundException)
   {
    throw new HttpResponseException(new HttpResponseMessage()
    {
     StatusCode = System.Net.HttpStatusCode.NotFound
    });
   }
  }

  /// <summary>
  /// Updates an individual Product.
  /// </summary>
  /// <param name="product">The Product object.</param>
  /// <returns></returns>
  public void PutProduct(Product product)
  {
   ProductsRepository.update(product);
  }

  /// <summary>
  /// Creates a new Product.
  /// </summary>
  /// <param name="product">The Product object.</param>
  /// <returns></returns>
  public void PostProduct(Product product)
  {
   ProductsRepository.add(product);
  }
 }
}

There are a few changes from the earlier versions of this controller. As you can see, I’m using the XML Documentation features Yao talks about in his post. I’ve simply employed the technique he describes.

The next thing to cover is the APIDocumenationRepository Class. Here is the code for that class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Web;
using System.Web.Http;
using System.Web.Http.Description;

namespace WebAPI
{
 public class APIDocumentationRepository
 {
  public static APIEndPoint Get(string apiName) {
    return getAPIEndPoint(apiName);
  }

  public static List<APIEndPoint> Get()
  {

   var Controllers = GlobalConfiguration
       .Configuration
       .Services
       .GetApiExplorer()
       .ApiDescriptions
       .GroupBy(x => x.ActionDescriptor.ControllerDescriptor.ControllerName)
       .Select(x => x.First().ActionDescriptor.ControllerDescriptor.ControllerName)
       .ToList();
       
   var apiEndPoints = new List<APIEndPoint>();

   foreach (var controller in Controllers) {
      apiEndPoints.Add(getAPIEndPoint(controller));
   }
   
   return apiEndPoints;
  }

  static APIEndPoint getAPIEndPoint(string controller) {

   var apis = GlobalConfiguration
    .Configuration
    .Services
    .GetApiExplorer()
    .ApiDescriptions
    .Where(x => x.ActionDescriptor.ControllerDescriptor.ControllerName == controller);

   List<APIEndPointDetail> apiEndPointDetails = null; 

   if (apis.ToList().Count > 0)
   {

   apiEndPointDetails = new List<APIEndPointDetail>();
   foreach (var api in apis)
   {
    apiEndPointDetails.Add(getAPIEndPointDetail(api));
   } 
   }
   else
   {
    controller = string.Format("The {0} api does not exist.",controller);
   }
   return new APIEndPoint(controller,apiEndPointDetails);
  }

  static APIEndPointDetail getAPIEndPointDetail(ApiDescription api) {

   if (api.ParameterDescriptions.Count > 0)
   {
    var parameters = new List<APIEndPointParameter>();
    foreach (var parameter in api.ParameterDescriptions)
    {
     parameters.
     Add(new APIEndPointParameter(parameter.Name, parameter.Documentation, parameter.Source.ToString()));
    }
    return new APIEndPointDetail(api.RelativePath, api.Documentation, api.HttpMethod.Method, parameters);
   }
   else
   {
    return new APIEndPointDetail(api.RelativePath, api.Documentation, api.HttpMethod.Method);
   }
  }
 }

 [DataContract]
 public class APIEndPoint {
  [DataMember] public string Name { get; private set; }
  [DataMember] public List<APIEndPointDetail> APIEndPointDetails { get; private set; }

  public APIEndPoint(string name, List<APIEndPointDetail> apiEndPointDetails)
  {
    Name = name;
    APIEndPointDetails = apiEndPointDetails;
  }

 }

 [DataContract]
 public class APIEndPointDetail
 {
  [DataMember]
  public string RelativePath { get; private set; }
  [DataMember]
  public string Documentation { get; private set; }
  [DataMember]
  public string Method { get; private set; }
  [DataMember]
  public List<APIEndPointParameter> Parameters { get; private set; }

  public APIEndPointDetail(string relativePath, string documentation, string method, 
   List<APIEndPointParameter> parameters) : this(relativePath, documentation, method)
  {
   Parameters = parameters;
  }

  public APIEndPointDetail(string relativePath, string documentation, string method)
  {
   RelativePath = relativePath;
   Documentation = documentation;
   Method = method;
  }
 }

 [DataContract]
 public class APIEndPointParameter
 {
  [DataMember]
  public string Name { get; set; }
  [DataMember]
  public string Documentation { get; private set; }
  [DataMember]
  public string Source { get; private set; }

  public APIEndPointParameter(string name, string documentation, string source)
  {
   Name = name;
   Documentation = documentation;
   Source = source;
  }
 }
}

With the everything in place, including all of the things outlined in Yao’s post, with this url:
http://localhost:18950/api/help?api=Products – the following is the api documenation for the Products API:

{
   "Name":"Products",
   "APIEndPointDetails":[
      {
         "RelativePath":"api/Products",
         "Documentation":"Returns the Product Collection.",
         "Method":"GET"
      },
      {
         "RelativePath":"api/Products/{id}",
         "Documentation":"Returns an individual Product.",
         "Method":"GET",
         "Parameters":[
            {
               "Name":"id",
               "Documentation":"The Product id.",
               "Source":"FromUri"
            }
         ]
      },
      {
         "RelativePath":"api/Products",
         "Documentation":"Deletes the Products Collection and reverts back to original state.",
         "Method":"DELETE"
      },
      {
         "RelativePath":"api/Products/{id}",
         "Documentation":"Deletes an individual Product.",
         "Method":"DELETE",
         "Parameters":[
            {
               "Name":"id",
               "Documentation":"The Product id.",
               "Source":"FromUri"
            }
         ]
      },
      {
         "RelativePath":"api/Products",
         "Documentation":"Updates an individual Product.",
         "Method":"PUT",
         "Parameters":[
            {
               "Name":"product",
               "Documentation":"The Product object.",
               "Source":"FromBody"
            }
         ]
      },
      {
         "RelativePath":"api/Products",
         "Documentation":"Creates a new Product.",
         "Method":"POST",
         "Parameters":[
            {
               "Name":"product",
               "Documentation":"The Product object.",
               "Source":"FromBody"
            }
         ]
      }
   ]
}

And if XML is your thing, no problem. Simply set the content-type header to application/xml:

<APIEndPoint xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/WebAPI">
  <APIEndPointDetails>
    <APIEndPointDetail>
      <Documentation>Returns the Product Collection.</Documentation>
      <Method>GET</Method>
      <Parameters i:nil="true" />
      <RelativePath>api/Products</RelativePath>
    </APIEndPointDetail>
    <APIEndPointDetail>
      <Documentation>Returns an individual Product.</Documentation>
      <Method>GET</Method>
      <Parameters>
        <APIEndPointParameter>
          <Documentation>The Product id.</Documentation>
          <Name>id</Name>
          <Source>FromUri</Source>
        </APIEndPointParameter>
      </Parameters>
      <RelativePath>api/Products/{id}</RelativePath>
    </APIEndPointDetail>
    <APIEndPointDetail>
      <Documentation>Deletes the Products Collection and reverts back to original state.</Documentation>
      <Method>DELETE</Method>
      <Parameters i:nil="true" />
      <RelativePath>api/Products</RelativePath>
    </APIEndPointDetail>
    <APIEndPointDetail>
      <Documentation>Deletes an individual Product.</Documentation>
      <Method>DELETE</Method>
      <Parameters>
        <APIEndPointParameter>
          <Documentation>The Product id.</Documentation>
          <Name>id</Name>
          <Source>FromUri</Source>
        </APIEndPointParameter>
      </Parameters>
      <RelativePath>api/Products/{id}</RelativePath>
    </APIEndPointDetail>
    <APIEndPointDetail>
      <Documentation>Updates an individual Product.</Documentation>
      <Method>PUT</Method>
      <Parameters>
        <APIEndPointParameter>
          <Documentation>The Product object.</Documentation>
          <Name>product</Name>
          <Source>FromBody</Source>
        </APIEndPointParameter>
      </Parameters>
      <RelativePath>api/Products</RelativePath>
    </APIEndPointDetail>
    <APIEndPointDetail>
      <Documentation>Creates a new Product.</Documentation>
      <Method>POST</Method>
      <Parameters>
        <APIEndPointParameter>
          <Documentation>The Product object.</Documentation>
          <Name>product</Name>
          <Source>FromBody</Source>
        </APIEndPointParameter>
      </Parameters>
      <RelativePath>api/Products</RelativePath>
    </APIEndPointDetail>
  </APIEndPointDetails>
  <Name>Products</Name>
</APIEndPoint>

Enjoy…

JVP

About johnvpetersen

I've been developing software for 20 years, starting with dBase, Clipper and FoxBase + thereafter, migrating to FoxPro and Visual FoxPro and Visual Basic. Other areas of concentration include Oracle and SQL Server - versions 6-2008. From 1995 to 2001, I was a Microsoft Visual FoxPro MVP. Today, my emphasis is on ASP MVC .NET applications. I am a current Microsoft ASP .NET MVP. Publishing In 1999, I wrote the definitive whitepaper on ADO for VFP Developers. In 2002, I wrote the Absolute Beginner’s Guide to Databases for Que Publishing. I was a co-author of Visual FoxPro Enterprise Development from Prima Publishing with Rod Paddock, Ron Talmadge and Eric Ranft. I was also a co-author of Visual Basic Web Development from Prima Publishing with Rod Paddock and Richard Campbell. Education - B.S Business Administration – Mansfield University - M.B.A. – Information Systems – Saint Joseph’s University - J.D. – Rutgers University School of Law (Camden) In 2004, I graduated from the Rutgers University School of Law with a Juris Doctor Degree. I passed the Pennsylvania and New Jersey Bar exams and was in private practice for several years – concentrating transactional and general business law (contracts, copyrights, trademarks, independent contractor agreements, NDA’s, intellectual property and mergers and acquisitions.).
This entry was posted in ASP.NET Web API, ASPNET MVC 4. Bookmark the permalink. Follow any comments here with the RSS feed for this post.