If you ever worked with any API, which, in this day of age, you must have, you surely noticed that in most situations the API response isn’t just the result (requested data), but also a set of helpful metadata, like “total Results”, “timestamp”, “status” and so on.
In Web API, by default, you just serialize your models (or DTO) and such information are not present. Let’s build something which will solve this problem and help you decorate your response with hepful information. This would make it very easy for the client to implement paging, auto-loading scenarios, caching (if you return last modified information) and a lot more.
What are we going to build
First let’s go through a plan of what we are going to build.
Assume you have some sample repository. In the recent samples, I have always been using a dummy repository from this post, so let’s use it again.
Now, by default, the response will look like this (for a single item):
1 2 3 4 5 6 7 8 |
{ "UrlId": 1, "Address": "http://www.strathweb.com/2012/03/build-facebook-style-infinite-scroll-with-knockout-js-and-last-fm-api/", "Title": "Build Facebook style infinite scroll with knockout.js and Last.fm API", "Description": "Since knockout.js is one of the most amazing and innovative pieces of front-end code I have seen in recent years, I hope this is going to help you a bit in your everday battles. In conjuction with Last.FM API, we are going to create an infinitely scrollable history of your music records – just like the infinite scroll used on Facebook or on Twitter.", "CreatedAt": "2012-03-20T00:00:00", "CreatedBy": "Filip" } |
If we request for IQueryable, we obivously get an Array.
Now, what we’d like to have is something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
{ "TotalResults": 3, "ReturnedResults": 2, "Results": [ { "UrlId": 1, "Address": "http://www.strathweb.com/2012/03/build-facebook-style-infinite-scroll-with-knockout-js-and-last-fm-api/", "Title": "Build Facebook style infinite scroll with knockout.js and Last.fm API", "Description": "Since knockout.js is one of the most amazing and innovative pieces of front-end code I have seen in recent years, I hope this is going to help you a bit in your everday battles. In conjuction with Last.FM API, we are going to create an infinitely scrollable history of your music records – just like the infinite scroll used on Facebook or on Twitter.", "CreatedAt": "2012-03-20T00:00:00", "CreatedBy": "Filip" }, { "UrlId": 2, "Address": "http://www.strathweb.com/2012/04/your-own-sports-news-site-with-espn-api-and-knockout-js/", "Title": "Your own sports news site with ESPN API and Knockout.js", "Description": "You will be able to browse the latest news from ESPN from all sports categories, as well as filter them by tags. The UI will be powered by KnockoutJS and Twitter bootstrap, and yes, will be a single page. We have already done two projects together using knockout.js – last.fm API infinite scroll and ASP.NET WebAPI file upload. Hopefully we will continue our knockout.js adventures in an exciting, and interesting for you, way.", "CreatedAt": "2012-04-08T00:00:00", "CreatedBy": "Filip" } ], "Timestamp": "2012-06-03T09:57:58.6265082+02:00", "Status": "Success" } |
So much nicer isn’t it?
Designing the service
So how are we going to accomplish this? Pretty simple:
1. We will implement a DelegatingHandler which will capture all HttpResponseMessages, extract the response object and wrap into our custom generic Metadata
2. Additionally, we’ll have a CustomQueryableAttribute, which we will use on IQueryable Actions, to keep the information about the size (count) of the IQueryable. This way we will be able to provide information about what is the total size of the collection and thus support OData filtering. This way the client can request i.e. $top=3 results, but still have information about the total size of the IQueryable.
Models
As mentioned, we will use a repository from here – it’s really simple and perfect for testing. Let’s have a look at the Metadata
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
[DataContract] public class Metadata<T> where T : class { [DataMember] public int TotalResults { get; set; } [DataMember] public int ReturnedResults { get; set; } [DataMember] public T[] Results { get; set; } [DataMember] public DateTime Timestamp { get; set; } [DataMember] public string Status { get; set; } public Metadata(HttpResponseMessage httpResponse, bool isIQueryable) { //extract info from response } } |
It is a generic class, which will provide information such as:
– total results (since the data may be filtered)
– returned results (since the data may be filtered)
– results object (which is equal to the default WebAPI response)
– timestamp of the response
– status – to indicate a successful or unsuccessful response
I will leave the constructor empty for now, it will be more clear to what’s happening there, once we go through the handler and filter.
The Data annotations are there for compatibility with DataContractSerializer. Content negotation is supported, but in principle, this functionality is better suited for JSON.NET, but more on that later.
MetadataHandler
MetadataHandler will inherit from a DelegatingHandler, which means it will have access to the HttpResponseMessage before it is flushed to the client and before applying MediaFormatting.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class MetadataHandler : DelegatingHandler { protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { return base.SendAsync(request, cancellationToken).ContinueWith( task => { if (ResponseIsValid(task.Result)) { object responseObject; task.Result.TryGetContentValue(out responseObject); if (responseObject is IQueryable) { ProcessObject<object>(responseObject as IQueryable<object>, task.Result, true); } else { var list = new List<object>(); list.Add(responseObject); ProcessObject<object>(responseObject as IEnumerable<object>, task.Result, true); } return task.Result; } } ); } } |
As mentioned, we need to modify the response, and to do that we need to override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) method and add a ContinueWith to it.
In there, we extract the object from the response and pass it to a private method for altering.
Now, here is a note. If you serialize to JSON, using JSON.NET you can do just that, it can handle that just fine. On the other hand, if you are willing to use DataContractSerializer, it will not be able to serialize object types properly. You would need known types, so you’d need either reflection or typecasting:
1 2 3 4 5 6 7 8 9 10 |
if (responseObject is IQueryable<Url>) { ProcessObject<Url>(responseObject as IQueryable<Url>, task.Result, true); } else if (responseObject is Url) { var blog = new List<Url>(); blog.Add(responseObject as Blog); ProcessObject<Blog>(blog as IEnumerable<Blog>, task.Result, false); } |
Anyway, going back to our example, the ProcessObject method:
1 2 3 4 5 6 7 8 |
private void ProcessObject<T>(IEnumerable<T> responseObject, HttpResponseMessage response, bool isIQueryable) where T : class { var metadata = new Metadata<T>(response, isIQueryable); //uncomment this to preserve content negotation, but remember about typecasting for DataContractSerliaizer //var formatter = GlobalConfiguration.Configuration.Formatters.First(t => t.SupportedMediaTypes.Contains(new MediaTypeHeaderValue(response.Content.Headers.ContentType.MediaType))); //response.Content = new ObjectContent<Metadata<T>>(metadata, formatter); response.Content = new ObjectContent<Metadata<T>>(metadata, GlobalConfiguration.Configuration.Formatters[0]); } |
In this case, we instantiate a new Metadata and set it as the content of the response. As you see, I arbitrairly set the formatter to JSON.NET. The commented out code preservers content negotiation, but if you use that you’d need to type cast the objects as mentioned before.
We also used a simple helper method to check if the response is even worth processing:
1 2 3 4 5 |
private bool ResponseIsValid(HttpResponseMessage response) { if (response == null || response.StatusCode != HttpStatusCode.OK || !(response.Content is ObjectContent)) return false; return true; } |
Let’s now revisit the constructor we omitted earlier.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public Metadata(HttpResponseMessage httpResponse, bool isIQueryable) { this.Timestamp = DateTime.Now; if (httpResponse.Content != null && httpResponse.IsSuccessStatusCode) { this.TotalResults = 1; this.ReturnedResults = 1; this.Status = "Success"; if (isIQueryable) { IEnumerable<T> enumResponseObject; httpResponse.TryGetContentValue<IEnumerable<T>>(out enumResponseObject); this.Results = enumResponseObject.ToArray(); this.ReturnedResults = enumResponseObject.Count(); } else { T responseObject; httpResponse.TryGetContentValue<T>(out responseObject); this.Results = new T[] {responseObject}; } } else { this.Status = "Error"; this.ReturnedResults = 0; } } |
The constructor takes the HttpResponseMessage and builds up useful information based on it – i.e. setting the “sucess” or “error”, and also calculating the number of returned results.
Note, if single item is requested (via regular Get(int id) type of action, not OData) it will show 1 total result, because that’s how many matches there were. Total results is greater than returned results only if you filter data with OData.
Adding support for OData and IQueryable
All of this wouldn’t be very useful if we didn’t add our key functionality, which is “total results”. To do that, we need to introduce a new attribute filter, CustomQueryableAttribute, inheriting from QueryableAttribute.
We’d then decorate all IQueryable actions with it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class CustomQueryableAttribute : QueryableAttribute { protected override IQueryable ApplyResultLimit(HttpActionExecutedContext actionExecutedContext, IQueryable query) { object responseObject; actionExecutedContext.Response.TryGetContentValue(out responseObject); var originalquery = responseObject as IQueryable<object>; if (originalquery != null) { var originalSize = new string[] { originalquery.Count().ToString() }; actionExecutedContext.Response.Headers.Add("originalSize", originalSize as IEnumerable<string>); } return base.ApplyResultLimit(actionExecutedContext, query); } } |
We override the method IQueryable ApplyResultLimit(HttpActionExecutedContext actionExecutedContext, IQueryable query), which gives us access to IQueryable prior to applying the filtering. What it means, is that we can easily save aside the total number of results of the IQueryable and then use it later in our Metadata object.
We use a bit of a hack here, since I save that in a custom header field “originalSize”. In the MessageHandler later on, we will read that value from the headers and remove it so that they don’t get sent to the client – so it only saves the purpose of transporting a variable from the ActionFilter to the MessageHandler. If you want to pass data between two ActionFilters you could use ControllerContext.RouteData.Values, but for our scenario I couldn’t find a better way.
Now we need to update the MessageHandler, to make it aware of the “originalSize”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
private void ProcessObject<T>(IEnumerable<T> responseObject, HttpResponseMessage response, bool isIQueryable) where T : class { var metadata = new Metadata<T>(response, isIQueryable); var originalSize = new string[1] as IEnumerable<string>; response.Headers.TryGetValues("originalSize", out originalSize); response.Headers.Remove("originalSize"); if (originalSize != null) { metadata.TotalResults = Convert.ToInt32(originalSize.FirstOrDefault()); } //uncomment this to preserve content negotation, but remember about typecasting for DataContractSerliaizer //var formatter = GlobalConfiguration.Configuration.Formatters.First(t => t.SupportedMediaTypes.Contains(new MediaTypeHeaderValue(response.Content.Headers.ContentType.MediaType))); //response.Content = new ObjectContent<Metadata<T>>(metadata, formatter); response.Content = new ObjectContent<Metadata<T>>(metadata, GlobalConfiguration.Configuration.Formatters[0]); } |
So we read the header value and get rid of the redundant header key.
Registering handler, decorating methods
The final thing to do is to register the handler, so in App_Start we add:
1 2 |
GlobalConfiguration.Configuration.MessageHandlers.Add(new MetadataHandler()); GlobalConfiguration.Configuration.Formatters.RemoveAt(1); |
For testing, I get rid of the XmlFormatter as well.
We also add our custom queryable filter to all Action’s returning IQueryable:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class UrlController : ApiController { private readonly IUrlRepository urlRepo = new UrlRepository(); [CustomQueryableAttribute] public IQueryable<Url> Get() { return urlRepo.GetAll(); } public Url Get(int id) { return urlRepo.Get(id); } } |
Running the application
Everything is ready, so let’s roll.
Now, just to show that this doesn’t really on any strong types, let’s add a new repository with Blog objects.
Summary & source code
Hopefully someone will find the functionality described here useful. It can be obviously extended further, and optimized (perhaps someone can figure out a better way to handle DataContractSerializer).
Anyway, I had fun writing this, hope you had fun reading. Till next time!