Saturday, August 11, 2012

Documenting your ASP.Net Web API’s

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










DIGITAL JUICE

No comments:

Post a Comment

Thank's!