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 (38)

  1. 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

  2. @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

    }

  3. Josh Crawford says:

    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?

  4. 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}}

               });

  5. Greg Estes says:

    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.

  6. 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

    });

  7. KP says:

    Nice article!!  All my post methods return a HttpResopnseMessage with a dto in the body.  The out-of-the-box documentation engine will not pick up that return type.  Is there something that I can do to output the body of the HttpResponseMessage?

  8. Sally says:

    I need specific help with this in vb any resources available?

  9. 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

  10. 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?

  11. Sally says:

    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"

       }

     ]

    }

  12. 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: http://www.asp.net/…/knockoutjs-template

    Thanks,

    Yao

  13. Steve says:

    Using your example closely, why might this be null?

    IRemarksDocumentationProvider remarksDocProvider = config.Services.GetDocumentationProvider() as IRemarksDocumentationProvider;

  14. Steve says:

    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.

  15. 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

  16. Steve says:

    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>

       <p>Returns: @Model.ResponseDocumentation</p>

       …

    ————————————————————————-

    HelpPageConfig.cs:

           public static void Register(HttpConfiguration config)

           {

               config.SetDocumentationProvider(new XmlDocumentationProvider(HttpContext.Current.Server.MapPath("~/App_Data/XmlDocument.xml")));

               …

           }

    ————————————————————————-

  17. Steve says:

    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);

       }

    ————————————————————————-

  18. 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

  19. Jamie says:

    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.

  20. Hi Jamie,

    Here are the ways how you can contribute to the project: aspnetwebstack.codeplex.com/wikipage

    Thanks,

    Yao

  21. Steve says:

    That was it. Thanks Yao.

  22. Peter says:

    How do I sort the api list that is displayed?  I'd like it to be sorted by controller name.

  23. Hi Peter,

    You can open the AreasHelpPageViewsHelpIndex.cshtml and replace the following line:

    ILookup<string, ApiDescription> apiGroups = Model.ToLookup(api => api.ActionDescriptor.ControllerDescriptor.ControllerName);

    with something like:

    ILookup<string, ApiDescription> apiGroups = Model.OrderBy(d => d.ActionDescriptor.ControllerDescriptor.ControllerName).ToLookup(api => api.ActionDescriptor.ControllerDescriptor.ControllerName);

    Hope this helps,

    Yao

  24. Jamie says:

    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 😛

  25. Adam says:

    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.  

  26. 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.

  27. Thanks for the wonderful insight! These overviews have really gotten me through the hassle of API Documentation.

  28. 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. 🙂

  29. Lucian N says:

    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

  30. Jason Hollister says:

    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?

  31. Ilya Krinchiyan says:

    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.

  32. Ilya Krinchiyan says:

    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?

  33. Nazar Harasym says:

    Lucian N, just add this to controller that you want to hide

       [ApiExplorerSettings(IgnoreApi = true)]

  34. Ram says:

    I am getting below error when try to generate using command line WebApiHelpPageGenerator

    "Error: Cannot bind to the target method because its signature or security transparency is not compatible with that of the delegate type."

    Can you please help me here ?  Thanks in advance

  35. Ram Naresh Talluri says:

    I am getting below error when try to generate using command line WebApiHelpPageGenerator

    "Error: Cannot bind to the target method because its signature or security transparency is not compatible with that of the delegate type."

    Can you please help me here ?  Thanks in advance

  36. Lalosoft says:

    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?

  37. Aging Hippie says:

    Step 4 uses hasResponseSamples.  It's not declared anywhere in the preceding steps and did not already exist.

  38. Sorting says:

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