ASP.NET Web API Help Page Part 3: Advanced Help Page customizations

In this post, I’ll go over some advanced customization scenarios for ASP.NET Web API Help Page. First, I’ll demonstrate how you can enable new functionalities such as displaying the documentation in the <returns> tag by adding a new property to the HelpPageApiModel. Second, I’ll show you how to create a custom display template for samples that aren’t texts or images but videos. With that in mind, let’s get started.

1. Adding additional information to the HelpPageApiModel

Note that in this post I’ll only add the support for the documentation in the <returns> tag, but you can use the same pattern for the documentation in other XML comment tags.

Step 1: Adding a ResponseDocumentation property to the HelpPageApiModel

We are going to start by adding a new property to the HelpPageApiModel so that we can store the comments from the <returns> tag.

 public class HelpPageApiModel
 {
     public string ResponseDocumentation { get; set; }
  
     // existing code
 }

Step 2: Modifying the XmlDocumentationProvider

Next, we’re going to define a new interface called IResponseDocumentationProvider.

 public interface IResponseDocumentationProvider
 {
     string GetResponseDocumentation(HttpActionDescriptor actionDescriptor);
 }

After that, we’ll have the XmlDocumentationProvider implement the IResponseDocumentationProvider to read the value in the <retuns> tag.

 public virtual string GetResponseDocumentation(HttpActionDescriptor actionDescriptor)
 {
     XPathNavigator methodNode = GetMethodNode(actionDescriptor);
     if (methodNode != null)
     {
         XPathNavigator returnsNode = methodNode.SelectSingleNode("returns");
         if (returnsNode != null)
         {
             return returnsNode.Value.Trim();
         }
     }
  
     return null;
 }

Step 3: Populating the ResponseDocumentation

In HelpPageConfigurationExtensions.cs, we’re going to add some logic that will get back the XmlDocumentationProvider as IResponseDocumentationProvider and populate the ResponseDocumentation with the value in the <returns> tag.

 private static HelpPageApiModel GenerateApiModel(ApiDescription apiDescription, HelpPageSampleGenerator sampleGenerator, HttpConfiguration config)
 {
     HelpPageApiModel apiModel = new HelpPageApiModel();
     apiModel.ApiDescription = apiDescription;
  
     IResponseDocumentationProvider responseDocProvider = config.Services.GetDocumentationProvider() as IResponseDocumentationProvider;
     if (responseDocProvider != null)
     {
         apiModel.ResponseDocumentation = responseDocProvider.GetResponseDocumentation(apiDescription.ActionDescriptor);
     }
     
     // existing code
 }

Step 4: Displaying ResponseDocumentation on the View

As the latest step, we’re going to add something to the HelpPageApiModel.cshtml to display the ResponseDocumentation.

 @if (hasResponseSamples)
 {      
     <h2>Response Information</h2> 
     if (!String.IsNullOrEmpty(Model.ResponseDocumentation))
     {
         <p>@Model.ResponseDocumentation</p>
     }
     <h3>Response body formats</h3>
     @Html.DisplayFor(apiModel => apiModel.SampleResponses, "Samples", new { sampleClass = "response" })
 }

Result

And that’s it, your help page now supports XML comment’s <returns> tag!

image image

 

2. Creating new sample display templates

You can easily create reusable samples that aren’t text based using display templates. For illustration, I’ll create a new template to display video samples for APIs that return video/mp4 as the content type.

Step 1: Creating the template model

First, we start by creating a VideoSample class which we’ll use to represent a video sample.

 public class VideoSample
 {
     public VideoSample(string src)
     {
         if (src == null)
         {
             throw new ArgumentNullException("src");
         }
         Src = src;
     }
  
     public string Src { get; private set; }
 }

Step 2: Creating the template view

Next, we’ll create a view named VideoSample.cshtml under the DisplayTemplates folder.

image

 @model VideoSample
  
 <video width="320" height="240" controls="controls">
   <source src="@Model.Src" type="video/mp4">
   <object data="@Model.Src" width="320" height="240">
   </object>
 </video>

Step 3: Setting the sample

Now all we need to do is to set the sample on the API that returns video/mp4. For instance, you can use SetSampleResponse to set the response sample.

 public static class HelpPageConfig
 {
     public static void Register(HttpConfiguration config)
     {
         config.SetSampleResponse(new VideoSample("/Videos/movie.mp4"), new MediaTypeHeaderValue("video/mp4"), "Values", "Post");
     }
 }

Result

That’s it, now you can use videos as samples!

image

 

Related blog posts

ASP.NET Web API Help Page Part 1: Basic Help Page customizations

ASP.NET Web API Help Page Part 2: Providing custom samples on the Help Page

Comments

  • Anonymous
    December 17, 2012
    Great work, I have one small observation with regards help pages; when using controllers located in a different assembly these will not produce the automated help as only a single .xml is reference as the document source. It be worth setting allowing a base documentation directory and then using reflection to generate the recommended filename/path that can be used by the ApiExplorer. Regards Grahame Horner

  • Anonymous
    December 18, 2012
    @GrahameHorner Thanks for the suggestion! Meanwhile it should be pretty easy to add the support for multiple xml by composing the XmlDocumentationProvider. Imagine having something like this: public class MultipleXmlDocumentationProvider : IDocumentationProvider {    XmlDocumentationProvider[] providers;    public MultipleXmlDocumentationProvider(params string[] paths) {        // new up an XmlDocumentationProvider for each path and add it to providers.    }    public virtual string GetDocumentation(HttpActionDescriptor actionDescriptor) {        // call GetDocumentation on each provider in the providers until one of them returns a not null string.    }    // implement the other overload in a similar fashion }

  • Anonymous
    January 25, 2013
    The Response Sample returns "Sample is not available" when a controller returns an interface. The ObjectGenerator returns null since there is not a default constructor. If I change the return to a concrete type the sample is generated. What is the proper method to generate the sample in this situation? Do I just need to modify the ObjectGenerator to use my DI container or am I missing something?

  • Anonymous
    January 26, 2013
    Hi Josh, Yes, modifying the ObjectGenerator would be the way to go. However, if you don't have too many interfaces you can consider using the config.SetSampleObjects in HelpPageConfig.cs to set the sample objects manually:            config.SetSampleObjects(new Dictionary<Type, object>            {                {typeof(IWork), new MyWork{ MyProperty=4}}            });

  • Anonymous
    February 14, 2013
    I'm fairly new to MVC so this may be something simple I'm missing.  How would one go about extending the request/response parameters documentation?  For example I have an POST API to charge a credit card.   The Help Page currently shows for Request Name Description Additional information request Generic docs  Define this parameter in the request body. What I would like it to show is the following.  These are the public properties of the request parameter object.   Name Description Additional information OrderID   Amount ccInfo - sub class with more properties.  ccAccount  ccExp  ccName  ccAddress  ccCity I'm sure I can add all this information to the POST API summary, but I use the object in multiple APIs and would like it to generate the documentation from the classes so it only has be changed in one place.  The Sample generator obviously is able to read these properties because they show up in the request sample.  The ApiDescription has the parameter type when building the APIModel.  From there I'm not sure where to go.

  • Anonymous
    February 17, 2013
    Hi Greg Estes, You can do something similar to "Step 3: Populating the ResponseDocumentation". Open the HelpPageConfigurationExtensions.cs, inside GenerateApiModel method, use reflection to get the list of properties for your parameter type and add them to the apiDescription.ParameterDescriptions: apiDescription.ParameterDescriptions.Add(new ApiParameterDescription {    Name = "property name",    Documentation = "property documentation",    Source = ApiParameterSource.FromBody,    ParameterDescriptor = propertyType });

  • Anonymous
    March 05, 2013
    The comment has been removed

  • Anonymous
    March 09, 2013
    I need specific help with this in vb any resources available?

  • Anonymous
    March 12, 2013
    Hi KP, Please take a look at the Part 2 of this series where I explain how to set the samples when the action returns an HttpResponseMessage: blogs.msdn.com/.../asp-net-web-api-help-page-part-2-providing-custom-samples-on-the-help-page.aspx Thanks, Yao

  • Anonymous
    March 12, 2013
    Hi Sally, Are you using the help page package for VB? (nuget.org/.../Microsoft.AspNet.WebApi.HelpPage.VB). It should be in theory equivalent to the C# counterpart. Is there a specific issue that you're running into?

  • Anonymous
    March 13, 2013
    I am getting this error application/xml Sample: An exception has occurred while using the formatter 'XmlMediaTypeFormatter' to generate sample for media type 'application/xml'. Exception message: One or more errors occurred. Got the error for JsonFormater but found this work around. config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Serialize            config.Formatters.JsonFormatter.SerializerSettings.PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.Objects now my output is: Sample:{  "ProductID": 1,  "ProductName": "sample string 2",  "ProductDescription": "sample string 3",  "ProductPrice": 4.0,  "ProductCost": 5.0,  "ProductOwerID": 6,  "ProductDeliveryID": 7,  "OwnersProductCode": "sample string 8",  "ProductImageURL": "sample string 9",  "ProductImageThumbnailURL": "sample string 10",  "ProductActive": true,  "XACTN_TS": "2013-03-13T18:47:00.4257629-04:00",  "ProductCategories": [    {      "$id": "2",      "ProductCategoryID": 1,      "ProductID": 2,      "ProductCategoryTypeID": 3,      "CategoryLevel": 4,      "XACTN_TS": "2013-03-13T18:47:00.426763-04:00",      "Products": {        "$ref": "1"      }    },    {      "$ref": "2"    },    {      "$ref": "2"    }  ] }

  • Anonymous
    March 14, 2013
    Hi Sally, To solve the circular reference issue in XML have you tried using [DataContract(IsReference = true)](msdn.microsoft.com/.../system.runtime.serialization.datacontractattribute.isreference.aspx)? Another approach I'd suggest is using DTOs like we do in our SPA template: www.asp.net/.../knockoutjs-template Thanks, Yao

  • Anonymous
    March 14, 2013
    Using your example closely, why might this be null? IRemarksDocumentationProvider remarksDocProvider = config.Services.GetDocumentationProvider() as IRemarksDocumentationProvider;

  • Anonymous
    March 14, 2013
    I meant to say: IResponseDocumentationProvider responseDocProvider = config.Services.GetDocumentationProvider() as IResponseDocumentationProvider; I'm trying to get at the <remarks> tag too, but I am having the same problem with the <returns> tag. Of note, I had to add a HttpConfiguration config parameter to the GenerateApiModel() method because the template that was installed for the Web API Help Page nuget package did not have it.

  • Anonymous
    March 15, 2013
    Hi Steve, Maybe the documentation provider is not wired up? Can you go to HelpPageConfig.cs and make sure config.SetDocumentationProvider is uncommented? You can find more information under "Providing API documentations" in Part 1 of this series: blogs.msdn.com/.../asp-net-web-api-help-page-part-1-basic-help-page-customizations.aspx

  • Anonymous
    March 15, 2013
    HelpPageConfigurationExtensions.cs:    private static HelpPageApiModel GenerateApiModel(ApiDescription apiDescription, HelpPageSampleGenerator sampleGenerator, HttpConfiguration config)    {        ...        IRemarksDocumentationProvider remarksDocProvider = config.Services.GetDocumentationProvider() as IRemarksDocumentationProvider;        if (remarksDocProvider != null)        {            apiModel.RemarksDocumentation = remarksDocProvider.GetRemarksDocumentation(apiDescription.ActionDescriptor);        }        else        {            apiModel.RemarksDocumentation = "No <remarks>";        }        IResponseDocumentationProvider responseDocProvider = config.Services.GetDocumentationProvider() as IResponseDocumentationProvider;        if (responseDocProvider != null)        {            apiModel.ResponseDocumentation = responseDocProvider.GetResponseDocumentation(apiDescription.ActionDescriptor);        }        else        {            apiModel.ResponseDocumentation = "No <returns>";        }        ...    }


HelpPageApiModel.cshtml:    ...    <p>Remarks: @Model.RemarksDocumentation</p&gt;    <p>Returns: @Model.ResponseDocumentation</p&gt;    ...

HelpPageConfig.cs:        public static void Register(HttpConfiguration config)        {            config.SetDocumentationProvider(new XmlDocumentationProvider(HttpContext.Current.Server.MapPath("~/App_Data/XmlDocument.xml")));            ...        }

  • Anonymous
    March 15, 2013
    Thank you for your response. Yes, I had uncommented out config.SetDocumentationProvider. Here is more details about what I implemented. The XML documentation for <summary> shows up just fine, so it's consuming the document successfully. (I'll have to break it across multiple posts.)

HelpPageApiModel.cs:    public class HelpPageApiModel    {        ...        public string RemarksDocumentation { get; set; }        public string ResponseDocumentation { get; set; }        ...    }

XmlDocumentationProvider.cs    public class XmlDocumentationProvider : IDocumentationProvider    {        ...        public virtual string GetRemarksDocumentation(HttpActionDescriptor actionDescriptor)        {            XPathNavigator methodNode = GetMethodNode(actionDescriptor);            if (methodNode != null)            {                XPathNavigator returnsNode = methodNode.SelectSingleNode("remarks");                if (returnsNode != null)                {                    return returnsNode.Value.Trim();                }            }            return null;        }        public virtual string GetResponseDocumentation(HttpActionDescriptor actionDescriptor)        {            XPathNavigator methodNode = GetMethodNode(actionDescriptor);            if (methodNode != null)            {                XPathNavigator returnsNode = methodNode.SelectSingleNode("returns");                if (returnsNode != null)                {                    return returnsNode.Value.Trim();                }            }            return null;        }        ...    }    public interface IRemarksDocumentationProvider    {        string GetRemarksDocumentation(HttpActionDescriptor actionDescriptor);    }    public interface IResponseDocumentationProvider    {        string GetResponseDocumentation(HttpActionDescriptor actionDescriptor);    }

  • Anonymous
    March 15, 2013
    Hi Steve, Looks like you forgot to have the XmlDocumentationProvider implement the IResponseDocumentationProvider and IRemarksDocumentationProvider. BTW you can merge these interfaces into one Called IDocumentationProviderExtension or something and have both methods there. Hope this helps, Yao

  • Anonymous
    March 15, 2013
    This really is a fantastic project, is there any way we can make suggestions/submissions to the library? I'd be happy to submit my changes for the API's I've been working on.

  • Anonymous
    March 17, 2013
    Hi Jamie, Here are the ways how you can contribute to the project: aspnetwebstack.codeplex.com/wikipage Thanks, Yao

  • Anonymous
    March 18, 2013
    That was it. Thanks Yao.

  • Anonymous
    April 03, 2013
    How do I sort the api list that is displayed?  I'd like it to be sorted by controller name.

  • Anonymous
    April 05, 2013
    The comment has been removed

  • Anonymous
    April 28, 2013
    Thanks Yao, finally got around to sending that code through via codeplex. Let me know what you think. I also worked out the issue with the web api not working, I happened to put [IgnoreApi] on a base class I inherited :P

  • Anonymous
    May 28, 2013
    Yao, Is there a way to remove a parameter from the sample?  In my situation, I have a GUID field that is auto-generated, so I don't want users to think they need to specify, even though it will accept it.  

  • Anonymous
    June 07, 2013
    Hi Adam, The easiest way I can think of is by using a DTO that hides the GUID field. E.g. public class ValueDto {    public string Name { get; set; } } public class Value {    public Guid Id { get; set; }    public string Name { get; set; } } public Value Put(ValueDto input) { } Alternatively, you can just put a comment in the description saying that the GUID field is optional even though it's shown in the sample.

  • Anonymous
    June 12, 2013
    Thanks for the wonderful insight! These overviews have really gotten me through the hassle of API Documentation.

  • Anonymous
    June 12, 2013
    There is one thing, however, that I'd appreciate your input on. I have the documentation being grabbed from my standard API calls (including the return tags, thanks to your previous articles) just wonderfully. I am running into a problem with inheritance, however. Some of my API calls directly inherit, rather than override, the behavior of some base calls that I have. These base calls are commented, but the methods that inherit them are not. Do you have any ideas as to how I would go about displaying the comments for an overridden method, as normal, but extrapolating the comments from a base method if the method in question simply inherits without overriding? I appreciate any help you can give. :)

  • Anonymous
    September 06, 2013
    Is there any support to turn off some of the controllers from the help pages? I hope for some specific attribute that should be applied... Thx

  • Anonymous
    October 09, 2013
    I've run into a strange problem. I've got several documented functions in a class. When I build and bring up the documentation page, The first two functions documentation shows. So does the documentation for the remaining 6 functions, but it shows up twice. The repeated documentation doesn't appear repeated in the documentation file, and I've been unable to track down where it is getting duplicated. Moreover, if I duplicate one of the duplicating functions in the code, change it enough to make it distinct both in code and documentation, that new function will show up in the documentation, and the original will not repeat. I know this sounds very weird, but I'm at my wits end. Do you have any idea how this could happen? Any suggestions about how to track down where the duplication is occurring?

  • Anonymous
    November 06, 2013
    Hello. I have 2 roots, avaliable for WebApi controller: config.Routes.MapHttpRoute(               name: "DefaultApi",               routeTemplate: "api/{controller}/{action}/{id}",               defaults: new { id = RouteParameter.Optional },               constraints: new { action = @"^[a-zA-Z]+$" }               );            config.Routes.MapHttpRoute(              name: "RestFull",              routeTemplate: "api/{controller}/{id}",              defaults: new { id = RouteParameter.Optional }); This is done to be able to do, for example, GET api/controller/{id} for some actions and GET api/controller/action/{id} for other actions In this case, if we have action GetItem(), api help generates description for both ways of calling the this action: GET api/controller and GET api/controller/GetItem Is there a way to explictly choos, which way will be used as a way of callin function in api help? Thanks in advance.

  • Anonymous
    November 06, 2013
    I hav a web api project where I need to be able to call actions both, using template api/{controller}/{action}/{id} and api/{controller}/{id}. To do this, I've added 2 routes for api controller: config.Routes.MapHttpRoute(               name: "DefaultApi",               routeTemplate: "api/{controller}/{action}/{id}",               defaults: new { id = RouteParameter.Optional },               constraints: new { action = @"^[a-zA-Z]+$" }               );            config.Routes.MapHttpRoute(              name: "RestFull",              routeTemplate: "api/{controller}/{id}",              defaults: new { id = RouteParameter.Optional }); So now if I have controller MyController and method Delete, I can call it both DELETE api/MyController/Delete and DELETE api/MyController; Also I have auto-generated api help and after I've added second variant of the routing - some methods are now displayed two times in the help. What I want is to have only one reference in the help, for each action. Is it possible? Or maybe something is wrong with my routing and I can have multiple GET/POST methods, just using "api/{controller}/{id}" template?

  • Anonymous
    April 22, 2014
    Lucian N, just add this to controller that you want to hide    [ApiExplorerSettings(IgnoreApi = true)]

  • Anonymous
    August 12, 2014
    The comment has been removed

  • Anonymous
    August 12, 2014
    The comment has been removed

  • Anonymous
    June 06, 2015
    I have my user interface posted on a separate site and would like to have the API documentation there instead of on the API site itself. Is there any why to get this help provider to work on on a site that is separate from the API itself?

  • Anonymous
    June 11, 2015
    Step 4 uses hasResponseSamples.  It's not declared anywhere in the preceding steps and did not already exist.

  • Anonymous
    August 04, 2015
    ILookup<HttpControllerDescriptor, ApiDescription> apiGroups = Model.OrderBy(d=>d.ActionDescriptor.ControllerDescriptor.ControllerName).ToLookup(api => api.ActionDescriptor.ControllerDescriptor);