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


Samples in ASP.NET Web API Help Page are automatically generated based on your action parameters and return types. They represent the kind of contents that could go into the request or response body. For instance, if you have the Delete action inside ValuesController like below: 

public class ValuesController : ApiController
{
    public void Delete(CompositeValue param);
}
 
public class CompositeValue
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime Date { get; set; }
}

The help page would give you the following samples:

image

Understanding how samples are generated

Before we get into customization, it’s worth understanding how these samples are generated. For starters, it uses ApiExplorer to figure out the body parameter type and the return type of you action. Then it will try to create an instance of these types with dummy data. Finally, it ask ApiExplorer for the appropriate formatters to write the created instance into the sample string.

Now that you understand how the samples are generated, there shouldn’t be any surprise when you see something like “Failed to generate the sample for media type ‘application/x-www-form-urlencoded’” on the help page:

image

Yes, that’s because the default JQueryMvcFormUrlEncodedFormatter doesn’t support writing. However, you can still make it work by setting custom samples.

Setting custom samples

Most of the help page customizations can be done in HelpPageConfig.cs. If you open the file, you’ll see a bunch of commented codes that show you different ways of providing custom samples to the help page.

image

One way of solving the problem described above is by using SetSampleForType, which allow you to set the sample string directly. For instance, setting the following will have the help page display “Id=123&Name=Foo&Date=2012-10-13” as the application/x-www-form-urlencoded sample for actions with CompositeValue.

config.SetSampleForType(
    "Id=123&Name=Foo&Date=2012-10-13", 
    new MediaTypeHeaderValue("application/x-www-form-urlencoded"), 
    typeof(CompositeValue));

And there’re other ways of customizing the samples that you can explore on your own:

// Uncomment the following to use "sample string" as the sample for all actions that have string as the body parameter or return type.
// Also, the string arrays will be used for IEnumerable<string>. The sample objects will be serialized into different media type 
// formats by the available formatters.
config.SetSampleObjects(new Dictionary<Type, object>
{
    {typeof(string), "sample string"},
    {typeof(IEnumerable<string>), new string[]{"sample 1", "sample 2"}}
});
 
// Uncomment the following to use "1234" directly as the request sample for media type "text/plain" on the controller named "Values" 
// and action named "Put".
config.SetSampleRequest("1234", new MediaTypeHeaderValue("text/plain"), "Values", "Put");
 
// Uncomment the following to use the image on "../images/aspNetHome.png" directly as the response sample for media type "image/png"
// on the controller named "Values" and action named "Get" with parameter "id".
config.SetSampleResponse(new ImageSample("../images/aspNetHome.png"), new MediaTypeHeaderValue("image/png"), "Values", "Get", "id");

 

Setting the samples when the action returns an HttpResponseMessage

When the action returns an HttpResponseMessage, the content of the response body becomes unpredictable based on the signature. Therefore, by default the help page simply doesn’t know what sample to display. However, you easily customize it to show the right sample.

Consider the following two scenarios where you return an HttpResponseMessage:

HttpResponseMessage with content negotiation

This is when your action supports content negotiation. Something like below:

public HttpResponseMessage Post()
{
    var returnValue = new CompositeValue { Id = 1, Name = "Foo", Date = DateTime.Now };
    var response = Request.CreateResponse<CompositeValue>(HttpStatusCode.Created, returnValue);
    response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = returnValue.Id }));
    return response;
}

Here you simply need to use SetActualResponseType to indicate the actual type that goes into the body of the HttpResponseMessage. You can set this in the HelpPageConfig.cs:

config.SetActualResponseType(typeof(CompositeValue), "Values", "Post");

As a result, the help page would now display the right samples for the Post action above.

image

 

HttpResponseMessage without content negotiation

This is when you’re returning the HttpResponseMessage without the content negotiation. Something like the following where the content is simply a StringContent.

[AcceptVerbs("Custom")]
public HttpResponseMessage Custom()
{
    return new HttpResponseMessage
    {
        Content = new StringContent("action completed.")
    };
}

Here you can simply control what sample to display for the action by using SetSampleResponse:

config.SetSampleResponse("action completed.", new MediaTypeHeaderValue("text/plain"), "Values", "Custom");

This will have the help page display the following sample for the Custom action above.

image 

 

Related blog posts

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

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

Comments (31)

  1. Thanks for this Yao, this and the ApiExplorer are fantastic projects.  

    Is there a way I could limit Help Page to only document ApiDescriptors against certain routes?  In my current Web Api project I have a few HttpRoutes mapped, and Help Page seems to list each ApiDescriptor against each route even when the route would not be successful.

    When using the IApiExplorer manually I am able to do this by filtering the ApiDescriptors down to only the ones whose Route is the "DefaultApi" route.  However I can't find a way to do this from Help Page.

    Thanks,

    ~Derek

  2. Hi Derek, you can plug in a custom ApiExplorer to do that. Help Page will automatically pick it up since it's using ApiExplorer under the hood. Here is a simple code snippet showing how this can be done: gist.github.com/a82f539d7344b4a48fcd

    Hope this helps,

    Yao

  3. Jamie says:

    Have to say this is absolutely fantastic having played about with this with my latest REST API.

    I have a few questions if you could take the time to answer that would be greatly appreciated.

    1) How is performance affected by generating this from the docs each time, can we apply cache headers on the calls to help the area (I'm using the pre-release alpha 2 I believe)

    2) is there a method of finding out/recommended method of determining what exceptions or HTTP status codes are returned from a particular web api call? whether this be functional via description or from documentation is fine, I just couldn't work it out and thought it might have already been thought about.

    I'm looking at finding a pretty much self documented service and I feel that is the only thing missing at the moment.

    I can't vote this library highly enough and look forward to the full release version.

    Cheers,

    J

  4. Hi Jamie, thanks for trying this out. To anwser your questions:

    1. The underlying models are generated on the first time only and are cached for subsequent requests. If you want even better perf, you can cache the entire view by using OutputCacheAttribute, assuming your APIs won't be changing much at that point.

    2. You can create a custom attribute to specify the status codes (see below). Then modify the code in HelpPageConfigurationExtensions.cs to retrieve the information and add them to the HelpPageApiModel. Here is a code snippet to give you an idea: gist.github.com/6ed0d6f74a6889ec0c3b

       [ResponseStatus(HttpStatusCode.NotFound, HttpStatusCode.Unauthorized)]

       public IQueryable<string> Get()

    Hope this helps,

    Yao

  5. Jamie says:

    Thanks, I had that idea but wasn't sure that would fit in. Cheers for the snippet I'll try that tomorrow morning when I get in to work.

    I think if it's cached first time then that should be fine.

    I should I could do the same thing with Http Headers as well, say I require an extra header named X-Token, I could modify the code to output that as well?

    I'm really excited about this, it's an evolution of sandcastle which will hopefully make our documentation team examining the content rather than worrying about the format.

  6. Jamie says:

    Thanks, very much. I've managed to heavily customize the code so that it can be tailored to our needs (Headers/Status Codes etc). I'd be happy to send over the code if your thinking of adding such functionality.

    One question I have is that on some machines it doesn't generate through the API, is there some kind of issue with using reflection (trust I'm thinking) or does the Help API require some form of extra permissions?

  7. Hi Jamie, the Help API doesn't require any extra permission more than what's needed for your application to work properly. Internally it uses public APIs such as IHttpControllerSelector.GetControllerMapping and IHttpActionSelector.GetActionMapping to get a list of controllers/actions that's available in your application. Let me know if you figureout what the issue was. If you think it's a legitimate bug, feel free to log an entry on aspnetwebstack.codeplex.com/…/basic

    Hope this helps,

    Yao

  8. Shawn says:

    Is it possible to create one's own 'dummy object' in HelpPageConfig.cs so that the actual string values make some sense?  For example;

    Location.Address = "123 Main Street",

    Location.Phone = "123-456-7890"

    I've tried passing dummy objects into SetSampleForType, but it does not format correctly to JSON (which is the type I am trying to customize).   Any suggestions?

  9. Hi Shawn, try using SetSampleObjects:

    config.SetSampleObjects(new Dictionary<Type, object>

    {

       {typeof(Location), new Location{ … } }

    });

  10. JTech says:

    Hi Yeo,

    Is it possible to have the sample generator ignore certain properties of a model?

    For example, we use the same DTO for object Request and Response messages. When user is POSTing a model (creating a new record) they don't need to provide the ID field.

    But once its created and we return the new record serialized in the response body, the ID field is included.

    So in the POST request sample, I don't want the ID field to be displayed because for post request it doesn't make sense.

    But the POST response sample, I do want the ID field displayed…

  11. Hi JTech,

    I wonder if you can just use a different DTO for the request with no ID (see sample below). That way, the help page can just show what it takes as parameter instead of hiding something. I think it would be a cleaner solution because the deserializer then doesn't need to populate the property that you don't want for the request. It's also a better security practice to prevent any unexpected behaviors when someone do try to set the ID in the request.

    public MyDto Post(MyRequestDto request);

    public class MyRequestDto

    {

       // some properties

    }

    public class MyDto : MyRequestDto

    {

       public int Id { get; set; }

    }

  12. Yao:

    I came across the Web API Help pages.  I have tried getting my Web API calls to work with the Web API Help pages but I am not geting the Response object formatted correctly.

    Below, I have broken down how my apis calls are constructed and the format of the response.

    Here is the JSON response sent back to client.  The "data" fields can take on many different formats.

    {

    "ok" : "1",

    "data" : { "firstname" : "steve", ….. }

    }

    I use a factory method to build the response result.  I use "data" as object bucket to return any data.

    public abstract class ResultBase

       {

           public const int Result_Ok = 1;

           public const int Result_Error = 0;

           [JsonProperty(Order = 0)]

           public string ok { get; set; }

    }

    public class ResultOk : ResultBase

    {

    [JsonProperty(Order = 1)]

    public Object data { get; set; }

    public ResultOk()

    {

    // Initialize value

    base.ok = "1";

    }

    }

    Here is the method that is invoked by the api call.  The persons variable contains a DTO object for Person.

    [HttpPost]

    public ResultBase PostUsers(JObject jsonData)

    {

    result = ResultFactory.Get(ResultBase.Result_Ok);

    ((ResultOk)result).data = persons;

    result = ResultFactory.Get(ResultBase.Result_Error);

    ((ResultWithError)result).message = "User Token is not valid.";

    return result;

    }

    In the HelpPageConfig.cs file, I have added the following:

    ResultBase resultPersonDTO = ResultFactory.Get(ResultBase.Result_Ok);

    ((ResultOk)resultPersonDTO).data = new resultPersonDTO() { FirstName = "Steve" };

    config.SetSampleObjects(new Dictionary<Type, object>

    {

    {typeof(string), "sample string"},

    {typeof(IEnumerable<string>), new string[]{"sample 1", "sample 2"}},

    {typeof(ResultBase), resultVendorDTO}

    });

    However, I don't this approach is correct.  I have many different DTOs that get assigned to "data".

    Can you provide an approach for using the Web API Help pages with my api calls?

    Thanks.

  13. urban says:

    Hi Yao

    How can I use the:

    config.SetSampleObjects(new Dictionary<Type, object>

    {

      {typeof(object), new object{ … } }

    });

    When my services return an IQueryable<object> ??

    It seems to hook up OK when I use:

    config.SetSampleObjects(new Dictionary<Type, object>

    {

      {typeof(IQueryable<object>), new ??}

    });

    But I cannot figure out what object to create then?

    Any ideas?

  14. Hi Gotdablues,

    You can try the following to inject a sample object based on the action:

    1. Open HelpPageSampleGenerator.cs and add the following property:

    public Func<ApiDescription, Type, object> InjectSampleObject { get; set; }

    2. The replace the following line (in HelpPageSampleGenerator.cs):

    object sampleObject = GetSampleObject(type);

    with:

    object sampleObject = InjectSampleObject != null ? InjectSampleObject(api, type) : GetSampleObject(type);

    3. In HelpPageConfig.cs, inject the sample object as follows:

    config.GetHelpPageSampleGenerator().InjectSampleObject = (api, type) =>

    {

       if (type == typeof(ResultBase) && api.ActionDescriptor.ActionName == "PostUsers")

       {

           // return ResultOk with users

       }

       // handle other cases

       return null;

    };

  15. Hi urban,

    Is your question about how to create an IQueryable<object>? If so, then you can try something like: new List<object> { new object{…} }.AsQueryable(). Make sure to add the using statement for System.Linq.

  16. Great post Yao. A simple question regarding "HttpResponseMessage with content negotiation", will there be an attribute over action like [Returns(MyOddClass)]? Then we can avoid the SetActualResponseType method (which is a bit unsafe). Almost all of our web apis returns HttpResponseMessage. Thanks.

  17. Correct: [Returns(MyOddClass)] should be [Returns(typeof(MyOddClass))]

  18. Hi Karl,

    Thanks for the suggestion. We're considering doing something similar in a future iteration.

  19. Cam says:

    In MVC 5 you can now use the IHttpActionResult instead of HttpResponseMessage.  However, now that I'm using this no samples are being generated (json/xml) for my models.  How would I go about implementing IHttpActionResult so samples are generated?

    Thank you for any help you can give me.

  20. Cam says:

    It appears to be some issues with MVC 5 and HttpResponseMessage not working with your example provided.  Using your example, I added to my HelppageConfig.cs file:

    config.SetActualResponseType(typeof(CompositeValue), "Values", "Post");

    In one of my controllers, I added Post:

    public HttpResponseMessage Post()

           {

               var returnValue = new CompositeValue { Id = 1, Name = "Foo", Date = DateTime.Now };

               var response = Request.CreateResponse<CompositeValue>(HttpStatusCode.Created, returnValue);

               response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = returnValue.Id }));

               return response;

           }

    When I go to the api help page, it shows only the following:

    Help Page Home

    POST api/Schedule

    No documentation available.

    Where did I go wrong?

    The only way I can get the API Help page to work is if I use a controller like this:

    public CompositeValue Post()

           {

               var returnValue = new CompositeValue { Id = 1, Name = "Foo", Date = DateTime.Now };

               return returnValue;

           }

    Of course, it then ignores what is in the helppageconfig.cs file.  

    Completely lost at this point in time <arg>.  Any help would be appreciated.  Thank you.

  21. Joao Grassi says:

    Hi Cam. I'm having the same issue. I did the same in this post, and the controllers which returns the HttpResponseMessage are not showing the samples. When I return the actual response type in the controller, the samples does show up. Any help?

  22. Nilesh Ahir says:

    Dear Yao,

    How can I get <remarks>This is remark</remarks> value on api help page?

    Thanks

    Nilesh Ahir

  23. trond says:

    I second the question from Nilesh Ahir, and I also want to know if it possible to do the same with <code> and <example> in the documentation. I'd rather do that than what's been described here and there.

  24. Igor says:

    Cam & Joao Grassi, i have the same issue, here is the mistake at example, you should provide full name of the action method, not only "Post" literal. F.e.

    i have a ProductsController which have PostAddProduct action method,

    here is my working sample:

    config.SetActualResponseType(typeof(Product), "Products", "PostAddProduct");

  25. Igor says:

    Also i have a question myself, what xml comment tag i should use to fill "Additional information" column at "Body Parameters" section?

    Default value is "none".

    Thanks

  26. Scott says:

    Is there any way to NOT show the "application/xml", "text/xml", or "application/x-www-form-urlencoded"  Request Formats? My service only handles json data and having those others on the page could be quite misleading.

    Also, my application/json, text/json string is displayed as:

    "{"SwitchName":"3048-AD202-1", "Port":"Ethernet1/1", "ConnectedVLAN":70, …

    Perhaps because I'm trying to hard-code the json string and should be doing it some other way?

    {typeof(string), "{"SwitchName":"3048-AD202-1", "Port":"Ethernet1/1",

    Any help appreciated!

    Thanks,

    Scott F.

  27. DaveJ says:

    Like Cam reported last year I'm also using IHttpActionResult and no samples are being generated (json/xml) for my models. You said there might be a fix sometime in the future in your reply. Has there been a fix for this since you post?

  28. Anonymous says:

    Igor, thanks a lot! i would never figured out how to use is just seeing the tutotial, a pratical exaqmple helps a lot! Fully Thanks ^^

  29. _Smooth_Criminal says:

    Thanks for this,yao

    but I have some issue in my project.

    why my ResponseSample Appear  "$id" like

    {

     "status": "sample string 1",

     "securityTypeName": "sample string 2",

     "codes": {

       "$id": "2",

       "$values": [

         "sample string 1",

         "sample string 2"

       ]

     }

    }

    I did not declared it in my class,and how to get rid of it.

  30. Spencer Sullivan says:

    This is not exactly what I was looking for, but it led me to where I needed to go.

    Thank you!