Web API Thoughts 3 of 3 - Extending Web API Documentation






4.95/5 (114 votes)
ASP.NET Web API related projects. It touches most parts of the technology through the articles on Data Streaming, Working with HTTPS and Extending Web API Documentation.
- Download source code Web API Thoughts
- Nuget package
- Github repository link available in History section
This is the continuing section on the subject matter. For review, refer to WebAPI Thoughts - Introduction article.
PM > Install-Package WebAPI-DocumentX |
Extending Web API Documentation
The other coolest feature of Web API is its ability to document Web API service methods. The feature helps to document the service methods by listing them, describing and generating samples to their corresponding request/response values. Though the existing feature provides good API documentation, it's not capable of documenting certain request/response data types and it doesn't inform the user what exactly happens to the documentation.
Problem
Suppose you define a service method called AddInternalPhysician
that accepts Physician
class types. But this Physician
class is an abstract
class. Then, you tried to view the documentation of the method through the API documentation help section, but you couldn't view it even if the method is defined in your ApiContoller
. You don't even know what happened to the method API documentation. Same is true for the method that returns HttpResponseMessage
object and method that accepts HttpRequestMessage
object.
Visual Studio 2012 update 1(SP1) tries to address this issue by adding a ResponseType
attribute (located System.Web.Http.Description
namespace) that will be used to define a custom response type. Still, the issues persists as is for various reasons which I already explained previously.
Solution
One possible solution is to extend the API documentation provider so that Web API methods will be attributed with actual request/response classes. The concept is similar to what is being tried by VS team but more generic one and it resolves the problem described above.
The library will document Web API method that has:
- Multiple request parameters.
- An Abstract/Interface for request/response type.
HttpRequestMessage
andHttpResponseMessage
for request/response type.- A response that returns a
SuccessResponseType
and anErrorResponseType
depending on the service method logic. - Provides an explanation what happened regarding to the request/response type of the method.
Participating Projects
WebAPICommonLibrary
WebAPIDocumentationDemo
WebAPIDocumentationExtenderLibrary
POCOLibrary
How To Use
- Add/Reference
WebAPIDocumentationExtenderLibrary
to your Web API project - Reference the library under Global.asax.cs or WebApiConfig.cs
using WebAPIDocumentationExtenderLibrary;
- For Global.asax.cs
protected void Application_Start() { // ......................................... // Full code is available in the source code // ......................................... GlobalConfiguration.Configuration.RegisterRequestResponseHelp(); }
For WebApiConfig.cs
public static class WebApiConfig { public static void Register(HttpConfiguration config) { // ......................................... // Full code is available in the source code // ......................................... config.RegisterRequestResponseHelp(); } }
- Once the library is referenced (Step 2) to the desired service controller class, add
RequestTypeAttribute
andResponseTypeAttribute
to the operation with the appropriate request/response types. Example:[RequestType(typeof(InternalPhysicianBase), "physicianRequest")] [ResponseType(typeof(bool), typeof(Message))] // for success return true/false, for error/exception return message type with appropriate content. [HttpPost] public HttpResponseMessage AddInternalPhysician(HttpRequestMessage physicianRequest) { InternalPhysician internalPhysician = physicianRequest.Content.ReadAsAsync<InternalPhysician>().Result; return base.AddPhysician(internalPhysician); }
Here is what AddInternalPhysician
method documentation output looks like:
How the Web API Documentation Extender Library Works
The basic idea behind the solution is to instrument the Web API project assembly through reflection, then build samples for each method defined on the Web API project and finally register the custom message and sample to the Web API documentation section. The library is composed of many classes and interfaces that facilitate the solution. It also uses existing Web API documentation classes through .NET Reflection to generate the desired documentation sample and to register the Web API methods to the appropriate documentation places. These classes/interfaces are shown in the following class diagram:
As you can see from the diagram, there are three core interfaces that facilitate the generation of the documentation sample. These interfaces are defined as follows:
/// <summary>
/// API Documentation Builder interface
/// </summary>
internal interface IFluentBuilder
{
/// <summary>
/// Get Sample
/// </summary>
string Sample { get; }
/// <summary>
/// Build sample API Documentation
/// </summary>
/// <param name="input">Input value</param>
/// <returns>IFluentBuilder object</returns>
IFluentBuilder BuildSample(string input);
}
/// <summary>
/// Request sample builder interface
/// </summary>
internal interface IFluentRequestBuilder : IFluentBuilder
{
/// <summary>
/// Build request sample API Documentation
/// </summary>
/// <param name="type">Type value</param>
/// <param name="parameterName">ParameterName value</param>
/// <returns>IFluentRequestBuilder object</returns>
IFluentRequestBuilder BuildSample(Type type, string parameterName);
}
/// <summary>
/// Response sample builder interface
/// </summary>
internal interface IFluentResponseBuilder : IFluentBuilder
{
/// <summary>
/// Build response sample API Documentation
/// </summary>
/// <param name="type">Type value</param>
/// <returns>IFluentResponseBuilder object</returns>
IFluentResponseBuilder BuildSample(Type type);
/// <summary>
/// Build response sample API Documentation
/// </summary>
/// <param name="successResponseType">Success response type</param>
/// <param name="errorResponseType">Error response type</param>
/// <returns>IFluentResponseBuilder object</returns>
IFluentResponseBuilder BuildSample(Type successResponseType, Type errorResponseType);
}
Base abstract
class called APISampleBuilder
implements two interfaces namely IFluentRequestBuilder
and IFluentResponseBuilder
for building the required sample output for the request and response of a method. It also contains private
methods that facilitate the sample output. Two derived classes namely JSONSampleBuilder
and XMLSampleBuilder
are defined to facilitate JSON and XML sample output respectively. Each of these classes implements an abstract
method called BuildSample
that accepts an object instance input.
/// <summary>
/// API Documentation builder abstract class
/// </summary>
internal abstract class APISampleBuilder : IFluentRequestBuilder, IFluentResponseBuilder
{
// --------------------------------------------
// Full Code is available in the source control
// --------------------------------------------
/// <summary>
/// Build sample API Documentation
/// </summary>
/// <param name="instance">Instance value</param>
/// <returns>IFluentBuilder object</returns>
public abstract IFluentBuilder BuildSample(object instance);
/// <summary>
/// Build sample API Documentation
/// </summary>
/// <param name="input">Input value</param>
/// <returns>IFluentBuilder object</returns>
public IFluentBuilder BuildSample(string input)
{
if (!string.IsNullOrWhiteSpace(input))
sampleStringBuilder.AppendLine(input);
return this;
}
/// <summary>
/// Build request sample
/// </summary>
/// <param name="type">Type value</param>
/// <param name="parameterName">ParameterName value</param>
/// <returns>IFluentRequestBuilder object</returns>
public IFluentRequestBuilder BuildSample(Type type, string parameterName)
{
string header = "Request";
string messageHeader = string.Empty;
if (!string.IsNullOrWhiteSpace(parameterName))
{
messageHeader = string.Format("{0} sample for {1} ", header, parameterName);
BuildSample(messageHeader)
.BuildSample(messageLiner);
}
else
BuildSample(messageLiner);
return BuilderSample(type, header) as IFluentRequestBuilder;
}
/// <summary>
/// Build response sample
/// </summary>
/// <param name="type">Type value</param>
/// <returns>IFluentResponseBuilder object</returns>
public IFluentResponseBuilder BuildSample(Type type)
{
string header = "Response";
BuildSample(messageLiner);
return BuilderSample(type, header) as IFluentResponseBuilder;
}
/// <summary>
/// Build response sample API Documentation
/// </summary>
/// <param name="successResponseType">Success response type</param>
/// <param name="errorResponseType">Error response type</param>
/// <returns>IFluentResponseBuilder object</returns>
public IFluentResponseBuilder BuildSample(Type successResponseType, Type errorResponseType)
{
return ((BuildSample("Success response message sample") as IFluentResponseBuilder)
.BuildSample(successResponseType)
.BuildSample("Error response message sample") as IFluentResponseBuilder)
.BuildSample(errorResponseType);
}
}
/// <summary>
/// JSON sample builder class
/// </summary>
sealed class JSONSampleBuilder : APISampleBuilder
{
// --------------------------------------------
// Full code is available in the source control
// --------------------------------------------
/// <summary>
/// BuildSample API Documentation sample
/// </summary>
/// <param name="instance">Instance value</param>
/// <returns>IFluentBuilder object</returns>
public override IFluentBuilder BuildSample(object instance)
{
string json = string.Empty;
try
{
// Helps to serialzied the exact type of the object. i.e Base vs Derived classes
JsonSerializerSettings jss = new JsonSerializerSettings();
if (_jsonFormatter != null && _jsonFormatter.SerializerSettings != null &&
_jsonFormatter.SerializerSettings.TypeNameHandling != TypeNameHandling.None)
{
jss = _jsonFormatter.SerializerSettings;
}
else
{
jss.TypeNameHandling = TypeNameHandling.Auto;
};
json = JsonConvert.SerializeObject(instance, Formatting.Indented, jss);
}
catch (Exception)
{
}
return base.BuildSample(json);
}
}
/// <summary>
/// XML Sample builder class
/// </summary>
sealed class XMLSampleBuilder : APISampleBuilder
{
// --------------------------------------------
// Full code is available in the source control
// --------------------------------------------
/// <summary>
/// BuildSample API Documentation sample
/// </summary>
/// <param name="instance">Instance value</param>
/// <returns>IFluentBuilder object</returns>
public override IFluentBuilder BuildSample(object instance)
{
string xml = string.Empty;
try
{
using (Stream streamWriter = new MemoryStream())
using (StreamReader streamReader = new StreamReader(streamWriter))
{
DataContractSerializer xmlSerializer = new DataContractSerializer(instance.GetType());
xmlSerializer.WriteObject(streamWriter, instance);
streamWriter.Position = 0;
xml = streamReader.ReadToEnd();
xml = XElement.Parse(xml).ToString(); // Helps for proper indentation
}
}
catch (Exception)
{
}
return base.BuildSample(xml);
}
}
RequestTypeAttribute
and ResponseTypeAttibute
will help to decorate the desired request/response type for the Web API controller methods. RequestTypeAttribute
accepts two parameters through its constructor. One for the actual types to be documented and the other to show the type is defined to which parameter among the operation parameters.
/// <summary>
/// RequestType attribute class used to decorate Web API request objects
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class RequestTypeAttribute : Attribute
{
// --------------------------------------------
// Full code is available in the source control
// --------------------------------------------
/// <summary>
/// RequestType attribute class used to decorate Web API request objects
/// </summary>
/// <param name="type">Type value that represents the any request value</param>
/// <param name="parameterName">ParameterName value that represents the request value</param>
public RequestTypeAttribute(Type type, string parameterName)
{
if (type == null)
throw new ArgumentNullException("Request type value is null !");
Type = type;
_parameterName = parameterName;
}
}
ResponseTypeAttribute
has two overloaded constructor definitions which are used for:
- Generic response object
- Same like the first one except user expected to pass two types namely,
SuccessResponse
andErrorResponse
. for a success and error response respectively
/// <summary>
/// Response attribute class used to decorate Web API response objects
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ResponseTypeAttribute : Attribute
{
// --------------------------------------------
// Full Code is available in the source control
// --------------------------------------------
/// <summary>
/// ResponseType attribute class used to decorate Web API response objects
/// </summary>
/// <param name="type">Type value that represents the any response object</param>
public ResponseTypeAttribute(Type type)
{
if (type == null)
throw new ArgumentNullException("Response type value is null !");
Type = type;
}
/// <summary>
/// ResponseType attribute class used to decorate Web API response objects
/// </summary>
/// <param name="successResponseType">
/// Type value that represents success response</param>
/// <param name="errorResponseType"> Type value that represents error response </param>
public ResponseTypeAttribute(Type successResponseType, Type errorResponseType)
{
if (successResponseType == null)
throw new ArgumentNullException("Success Response type value is null !");
if (errorResponseType == null)
throw new ArgumentNullException("Error Response type value is null !");
SuccessResponseType = successResponseType;
ErrorResponseType = errorResponseType;
}
}
For clarity, refer How to use section.
Once all these classes are defined, the sample document will be generated in the following manner:
- Load the Web API project assembly
- Validate the assembly
- It should contain at least one
ApiController
type. - It must contain a
HelpPageConfigurationExtensions
class. - It must contain an
ObjectGenerator
class. HelpPageConfigurationExtensions
class should defineSetSampleRequest
andSetSampleResponse
methods with proper input parameters.ObjectGenerator
class should defineGenerateObject
method with proper input parameter.
- It should contain at least one
- Collect methods applicable for a request and response API documentation.
- Filter out any duplicate request and response methods per
ApiController
. - Finally register these methods to generate a sample for API documentation.
So to perform the above procedures, three classes participate, namely RegisterRequestTypes
, RegisterResponseTypes
and RegisterAPIHelp
. As the name indicates, the first two are responsible for request and response sample documentation and the remaining one is responsible for validating and registering the request/response sample API documentation. The operation to perform their duty are shown in the following code:
/// <summary>
/// Register Request Type class
/// </summary>
internal sealed class RegisterRequestTypes
{
/// <summary>
/// Register Request types
/// </summary>
/// <param name="httpConfiguration">HttpConfiguration value</param>
/// <param name="setSampleRequest">SampleRequest MethodInfo value</param>
/// <param name="controllerName">ControllerName value</param>
/// <param name="requestActions">RequestActions value</param>
internal static void Register(HttpConfiguration httpConfiguration, MethodInfo setSampleRequest,
string controllerName, IEnumerable<methodinfo> requestActions, MethodInfo generateObject)
{
// --------------------------------------------
// Full code is available in the source control
// --------------------------------------------
}
}
/// <summary>
/// Register Response Type class
/// </summary>
internal sealed class RegisterResponseTypes
{
/// <summary>
/// Register Response types
/// </summary>
/// <param name="httpConfiguration">HttpConfiguration value</param>
/// <param name="setSampleResponse">SampleReponse MethodInfo value</param>
/// <param name="controllerName">ControllerName value</param>
/// <param name="responseActions">ResponseActions value</param>
internal static void Register(HttpConfiguration httpConfiguration, MethodInfo setSampleResponse,
string controllerName, IEnumerable<methodinfo> responseActions,
MethodInfo generateObject)
{
// --------------------------------------------
// Full code is available in the source control
// --------------------------------------------
}
}
/// <summary>
/// Register API Help class
/// </summary>
public static class RegisterAPIHelp
{
/// <summary>
/// Register Response/Request sample API Documentation
/// </summary>
/// <param name="httpConfiguration">HttpConfiguration object</param>
/// <param name="assembly"/>Assembly object</param>
public static void RegisterRequestResponseHelp
(this HttpConfiguration httpConfiguration, Assembly assembly = null)
{
// --------------------------------------------
// Full code is available in the source control
// --------------------------------------------
}
}
Point of interest
Even though the main point of the article is to show how to extend the Web API documentation, there are lot of things to learn from the code along the way such as:
- Method invocation through reflection
- Attribute usage
- Serialization
- Different design principles and patterns. Like Fluent Interface
History and GitHub Version
- WebAPI Thoughts @ Github
- First Version 1.0, Nov 04, 2014
- First Version 1.0, Dec 01, 2014