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…