Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Web API Thoughts 3 of 3 - Extending Web API Documentation

0.00/5 (No votes)
1 Dec 2014 1  
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.

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 and HttpResponseMessage for request/response type
  • A response that returns a Success Response and an Error Response 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

  1. Add/Reference WebAPIDocumentationExtenderLibrary to your Web API project
  2. Reference the library under Global.asax.cs or WebApiConfig.cs
    using WebAPIDocumentationExtenderLibrary;
  3. 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();
       }
    }
        
  4. Once the library is referenced (Step 2) to the desired service controller class, add RequestTypeAttribute and ResponseTypeAttribute 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:

  1. Generic response object
  2. Same like the first one except user expected to pass two types namely, SuccessResponse and ErrorResponse. 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:

  1. Load the Web API project assembly
  2. 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 define SetSampleRequest and SetSampleResponse methods with proper input parameters.
    • ObjectGenerator class should define GenerateObject method with proper input parameter.

  3. Collect methods applicable for a request and response API documentation.
  4. Filter out any duplicate request and response methods per ApiController.
  5. 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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here