Extending Web API Help Page with information from attributes

Β· 969 words Β· 5 minutes to read

Web API help page, available for your Web API via the Nuget package or built into the Web API template if you used the ASP.NET Fall Preview installer, is an extremely useful tool for documenting your API. That is, both for the external users that will consume the API, but also for your own development team, especially if the API is part of a larger internal ecosystem of applications and you need an automated, quick and easy reference for everyone to be able to look at.

Recently, I have been asked how you could automate the exposure of some additional information about the API, such as whether an action requires authorization or what are the potential response status codes.

And sure enough, it is very easy to do.

More after the jump.

Adding the help page πŸ”—

Before we can start doing anything, we need to add the Help Page to our application. You can do that by installing the Microsoft.AspNet.WebApi.HelpPage package from Nuget or simply by creating a new Web API project if you are using the aforementioned ASP.NET Fall Update.

By the way, if you are not familiar with the Help Page, there are many great resources that can introduce you into the basics, like this article by Mark Berryman.

Adding support for additional information hrough attributes πŸ”—

We can provide additional info about our controllers’ actions by decorating it with attributes. These attributes can be either informative only (in this case, we will put them on an action to describe some behavior for the API Help Page to be able to read) or can be part of your application logic (i.e. authorization logic).

When the help page gets generated, it is very easy to read all the attributes applied to an action. Let’s have a look at an action that requires authorization:

[Authorize]  
public void Post([FromBody]string value)  

We could extend the Help Page Model (HelpPageApiModel.cs, to include this type of authorization information, by adding a simple property to it:

public bool RequiresAuthorization { get; set; }  

Now, we have to make sure, that whenever our API model gets hydrated with data from IApiExplorer, we force it to read the Authorize attribute as well. This happens in the HelpPageConfigurationExtensions.cs class, inside of the GenerateApiModel method.

private static HelpPageApiModel GenerateApiModel(ApiDescription apiDescription, HelpPageSampleGenerator sampleGenerator)  
{  
HelpPageApiModel apiModel = new HelpPageApiModel();  
apiModel.ApiDescription = apiDescription;

//Add this:  
var isAnonymous = apiDescription.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any()  
|| apiDescription.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any();

if (!isAnonymous && apiDescription.ActionDescriptor.GetFilterPipeline().Where(f => f.Instance is IAuthorizationFilter).Any())  
apiModel.RequiresAuthorization = true;

//(&#8230;) rest of code omitted for brevity  
}  

So effectively, what we did, is we read the attribute pipeline (action/controller/global levels - thanks Imran!) and if an auhtorization filter is in place, and is not overriden by AllowAnonymouseAttribute), we set the flag to true - and we didn’t even have to mess with reflection directly, as we have a very handy method on the ActionDescriptor, GetCustomAttributes.

Now, in the Web API help page view, in the HelpPageApiModel.cshtml, you need to simply read the data from the new property and render wherever you want it:

@if (Model.RequiresAuthorization)  
{

### Requires Authorization!

}  

We can now go to the API Help Page, to our controller & action, and we will see:

Adding information about different response status codes πŸ”—

As mentioned earlier, I have been recently asked about providing info about response status codes. And to be honest, this is the question that was already asked long time ago in one the meetings I’ve been a part of, “what status codes should I expect?”. And at that time, Darrel Miller had the most brilliant answer to it - “it’s HTTP. All of them”.

While in principle I very much agree with him, you might not be HTTP purists, and perhaps you might find it useful to provide such information. Again, we could do it really simply with an attribute:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]  
public class ResponseCodesAttribute : Attribute  
{  
public ResponseCodesAttribute(params HttpStatusCode[] statusCodes)  
{  
ResponseCodes = statusCodes;  
}

public IEnumerable<HttpStatusCode> ResponseCodes { get; private set; }  
}  

We could now apply it to some of the actions we have, depending on what we are returning from the actions and what kind of HTTP exceptions we might be throwing. Notice that this attribute is purely informative, and does not participate in any runtime logic:

[ResponseCodes(HttpStatusCode.OK, HttpStatusCode.NotFound)]  
public IEnumerable<string> Get()  
{  
//do something  
}

[ResponseCodes(HttpStatusCode.OK, HttpStatusCode.NotFound)]  
public string Get(int id)  
{  
//do something  
}

[Authorize]  
[ResponseCodes(HttpStatusCode.Created, HttpStatusCode.BadRequest, HttpStatusCode.Unauthorized)]  
public void Post([FromBody]string value)  
{  
//do something  
}

[Authorize]  
[ResponseCodes(HttpStatusCode.NoContent, HttpStatusCode.BadRequest, HttpStatusCode.Unauthorized)]  
public void Put(int id, [FromBody]string value)  
{  
//do something  
}

[Authorize]  
[ResponseCodes(HttpStatusCode.NoContent, HttpStatusCode.Unauthorized)]  
public void Delete(int id)  
{  
//do something  
}  

Now we need to modify the API Help Page Model, the model creation and the model’s Razor view in the same way we did it before.

HelpPageApiModel.cs:

public IEnumerable<HttpStatusCode> ResponseCodes { get; set; }  

HelpPageConfigurationExtensions.cs:

private static HelpPageApiModel GenerateApiModel(ApiDescription apiDescription, HelpPageSampleGenerator sampleGenerator)  
{  
HelpPageApiModel apiModel = new HelpPageApiModel();  
apiModel.ApiDescription = apiDescription;

var statusCodesAttrib = apiDescription.ActionDescriptor.GetCustomAttributes<ResponseCodesAttribute>();

var isAnonymous = apiDescription.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any()  
|| apiDescription.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any();

if (!isAnonymous && apiDescription.ActionDescriptor.GetFilterPipeline().Where(f => f.Instance is IAuthorizationFilter).Any())  
apiModel.RequiresAuthorization = true;

if (statusCodesAttrib.Any())  
apiModel.ResponseCodes = statusCodesAttrib.FirstOrDefault().ResponseCodes;

//omitted  
}  

HelpPageApiModel.cshtml:

@if (Model.ResponseCodes != null)  
{

### Response Codes

@foreach (var code in Model.ResponseCodes) {</p> 

  * @code (@((int)code))
} </ul> 

}  

Once that’s done, we can run our API Help Page and see it enriched with the information about the potential response status codes.

Summary πŸ”—

As mentioned, in my personal opinion Web API Help Page is a very useful little tool - and even if you don’t plan to make the API public or think that why would you even care about such type of information, the development team can still greatly benefit from it, as it provides a freebie up-to-date documentation.

Hopefully some of the techniques shown here will help you better document your API and improve the productivity by a tiny fraction!

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP