Extending your ASP.NET Web API responses with useful metadata

Β· 2075 words Β· 10 minutes to read

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][1], so let’s use it again.

Now, by default, the response will look like this (for a single item):

{  
"UrlId": 1,  
"Address": "/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:

{  
"TotalResults": 3,  
"ReturnedResults": 2,  
"Results": [  
{  
"UrlId": 1,  
"Address": "/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": "/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 _HttpResponseMessage_s, extract the response object and wrap into our custom generic Metadata. Then it will be flushed to the client.
  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][1] - it’s really simple and perfect for testing. Let’s have a look at the Metadata instead.

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

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);<br /> } else {<br /> var list = new List
    
    <object>
      ();<br /> list.Add(responseObject);<br /> ProcessObject
      
      <object>
        (responseObject as IEnumerable
        
        <object>
          , task.Result, true);<br /> }<br /> return task.Result;<br /> }<br /> }<br /> );<br /> }<br /> }<br /> ```</p> 
          
          <p>
            As mentioned, we need to modify the response, and to do that we need to override <i>Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)</i> method and add a <i>ContinueWith</i> to it.
          </p>
          
          <p>
            In there, we extract the <i>object</i> from the response and pass it to a private method for altering.
          </p>
          
          <p>
            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 <i>DataContractSerializer</i>, it will not be able to serialize <i>object</i> types properly. You would need known types, so you'd need either reflection or typecasting:<br /> ```csharp
<br /> if (responseObject is IQueryable<Url>)<br /> {<br /> ProcessObject<Url>(responseObject as IQueryable<Url>, task.Result, true);<br /> }<br /> else if (responseObject is Url)<br /> {<br /> var blog = new List<Url>();<br /> blog.Add(responseObject as Blog);<br /> ProcessObject<Blog>(blog as IEnumerable<Blog>, task.Result, false);<br /> }<br /> ```
          </p>
          
          <p>
            Anyway, going back to our example, the <i>ProcessObject</i> method:<br /> ```csharp
<br /> private void ProcessObject<T>(IEnumerable<T> responseObject, HttpResponseMessage response, bool isIQueryable) where T : class<br /> {<br /> var metadata = new Metadata<T>(response, isIQueryable);<br /> //uncomment this to preserve content negotation, but remember about typecasting for DataContractSerliaizer<br /> //var formatter = GlobalConfiguration.Configuration.Formatters.First(t => t.SupportedMediaTypes.Contains(new MediaTypeHeaderValue(response.Content.Headers.ContentType.MediaType)));<br /> //response.Content = new ObjectContent<Metadata<T>>(metadata, formatter);<br /> response.Content = new ObjectContent<Metadata<T>>(metadata, GlobalConfiguration.Configuration.Formatters[0]);<br /> }<br /> ```
          </p>
          
          <p>
            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.
          </p>
          
          <p>
            We also used a simple helper method to check if the response is even worth processing:<br /> ```csharp
<br /> private bool ResponseIsValid(HttpResponseMessage response)<br /> {<br /> if (response == null || response.StatusCode != HttpStatusCode.OK || !(response.Content is ObjectContent)) return false;<br /> return true;<br /> }<br /> ```
          </p>
          
          <p>
            Let's now revisit the constructor we omitted earlier.
          </p>
          
          <p>
            ```csharp
<br /> public Metadata(HttpResponseMessage httpResponse, bool isIQueryable)<br /> {<br /> this.Timestamp = DateTime.Now;<br /> if (httpResponse.Content != null && httpResponse.IsSuccessStatusCode)<br /> {<br /> this.TotalResults = 1;<br /> this.ReturnedResults = 1;<br /> this.Status = "Success";
          </p>
          
          <p>
            if (isIQueryable)<br /> {<br /> IEnumerable<T> enumResponseObject;<br /> httpResponse.TryGetContentValue<IEnumerable<T>>(out enumResponseObject);<br /> this.Results = enumResponseObject.ToArray();<br /> this.ReturnedResults = enumResponseObject.Count();<br /> }<br /> else<br /> {<br /> T responseObject;<br /> httpResponse.TryGetContentValue<T>(out responseObject);<br /> this.Results = new T[] {responseObject};<br /> }<br /> }<br /> else<br /> {<br /> this.Status = "Error";<br /> this.ReturnedResults = 0;<br /> }<br /> }<br /> ```
          </p>
          
          <p>
            The constructor takes the <i>HttpResponseMessage</i> and builds up useful information based on it - i.e. setting the "sucess" or "error", and also calculating the number of returned results.
          </p>
          
          <p>
            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.
          </p>
          
          <h3>
            Adding support for OData and IQueryable
          </h3>
          
          <p>
            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, <strong>CustomQueryableAttribute</strong>, inheriting from <i>QueryableAttribute</i>.
          </p>
          
          <p>
            We'd then decorate all <i>IQueryable</i> actions with it.<br /> ```csharp
<br /> public class CustomQueryableAttribute : QueryableAttribute<br /> {<br /> protected override IQueryable ApplyResultLimit(HttpActionExecutedContext actionExecutedContext, IQueryable query)<br /> {<br /> object responseObject;<br /> actionExecutedContext.Response.TryGetContentValue(out responseObject);<br /> var originalquery = responseObject as IQueryable
            
            <object>
              ;</p> 
              
              <p>
                if (originalquery != null)<br /> {<br /> var originalSize = new string[] { originalquery.Count().ToString() };<br /> actionExecutedContext.Response.Headers.Add("originalSize", originalSize as IEnumerable<string>);<br /> }<br /> return base.ApplyResultLimit(actionExecutedContext, query);<br /> }<br /> }<br /> ```
              </p>
              
              <p>
                We override the method <i>IQueryable ApplyResultLimit(HttpActionExecutedContext actionExecutedContext, IQueryable query)</i>, which gives us access to <i>IQueryable</i> <strong>prior to</strong> applying the filtering. What it means, is that we can easily save aside the total number of results of the <i>IQueryable</i> and then use it later in our <i>Metadata</i> object.
              </p>
              
              <p>
                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 <i>ControllerContext.RouteData.Values</i>, but for our scenario I couldn't find a better way.
              </p>
              
              <p>
                Now we need to update the MessageHandler, to make it aware of the "originalSize":<br /> ```csharp
<br /> private void ProcessObject<T>(IEnumerable<T> responseObject, HttpResponseMessage response, bool isIQueryable) where T : class<br /> {<br /> var metadata = new Metadata<T>(response, isIQueryable);<br /> var originalSize = new string[1] as IEnumerable<string>;
              </p>
              
              <p>
                response.Headers.TryGetValues("originalSize", out originalSize);<br /> response.Headers.Remove("originalSize");<br /> if (originalSize != null)<br /> {<br /> metadata.TotalResults = Convert.ToInt32(originalSize.FirstOrDefault());<br /> }<br /> //uncomment this to preserve content negotation, but remember about typecasting for DataContractSerliaizer<br /> //var formatter = GlobalConfiguration.Configuration.Formatters.First(t => t.SupportedMediaTypes.Contains(new MediaTypeHeaderValue(response.Content.Headers.ContentType.MediaType)));<br /> //response.Content = new ObjectContent<Metadata<T>>(metadata, formatter);<br /> response.Content = new ObjectContent<Metadata<T>>(metadata, GlobalConfiguration.Configuration.Formatters[0]);<br /> }<br /> ```
              </p>
              
              <p>
                So we read the header value and get rid of the redundant header key.
              </p>
              
              <h3>
                Registering handler, decorating methods
              </h3>
              
              <p>
                The final thing to do is to register the handler, so in <i>App_Start</i> we add:<br /> ```csharp
<br /> GlobalConfiguration.Configuration.MessageHandlers.Add(new MetadataHandler());<br /> GlobalConfiguration.Configuration.Formatters.RemoveAt(1);<br /> ```
              </p>
              
              <p>
                For testing, I get rid of the XmlFormatter as well.
              </p>
              
              <p>
                We also add our custom queryable filter to all Action's returning <i>IQueryable</i>:<br /> ```csharp
<br /> public class UrlController : ApiController<br /> {<br /> private readonly IUrlRepository urlRepo = new UrlRepository();<br /> [CustomQueryableAttribute]<br /> public IQueryable<Url> Get()<br /> {<br /> return urlRepo.GetAll();<br /> }<br /> public Url Get(int id)<br /> {<br /> return urlRepo.Get(id);<br /> }<br /> }<br /> ```
              </p>
              
              <h3>
                Running the application
              </h3>
              
              <p>
                Everything is ready, so let's roll.
              </p>
              
              <p>
                First let's get all Urls:<br /> <a href="/images/2012/06/metadata-getall.png"><img src="/images/2012/06/metadata-getall-1024x672.png" alt="" title="metadata-getall" width="584" height="383" class="aligncenter size-large wp-image-279" /></a>
              </p>
              
              <p>
                Let's test OData:<br /> <a href="/images/2012/06/metadata-getall-top1.png"><img src="/images/2012/06/metadata-getall-top1-1024x547.png" alt="" title="metadata-getall-top" width="584" height="311" class="aligncenter size-large wp-image-284" srcset="/images/2012/06/metadata-getall-top1-1024x547.png 1024w, /images/2012/06/metadata-getall-top1-300x160.png 300w, /images/2012/06/metadata-getall-top1.png 1031w" sizes="(max-width: 584px) 100vw, 584px" /></a>
              </p>
              
              <p>
                Now let's get a single Url:<br /> <a href="/images/2012/06/metadata-getsingle.png"><img src="/images/2012/06/metadata-getsingle-1024x381.png" alt="" title="metadata-getsingle" width="584" height="217" class="aligncenter size-large wp-image-283" srcset="/images/2012/06/metadata-getsingle-1024x381.png 1024w, /images/2012/06/metadata-getsingle-300x112.png 300w, /images/2012/06/metadata-getsingle.png 1045w" sizes="(max-width: 584px) 100vw, 584px" /></a>
              </p>
              
              <p>
                Now, just to show that this doesn't really on any strong types, let's add a new repository with <i>Blog</i> objects.<br /> <a href="/images/2012/06/metadata-getall-top2.png"><img src="/images/2012/06/metadata-getall-top2-300x210.png" alt="" title="metadata-getall-top2" width="300" height="210" class="aligncenter size-medium wp-image-281" /></a> <a href="/images/2012/06/metadata-getall-top2-skip.png"><img src="/images/2012/06/metadata-getall-top2-skip-300x247.png" alt="" title="metadata-getall-top2-skip" width="300" height="247" class="aligncenter size-medium wp-image-282" srcset="/images/2012/06/metadata-getall-top2-skip-300x247.png 300w, /images/2012/06/metadata-getall-top2-skip.png 478w" sizes="(max-width: 300px) 100vw, 300px" /></a>
              </p>
              
              <h3>
                Summary & source code
              </h3>
              
              <p>
                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 <i>DataContractSerializer</i>).
              </p>
              
              <p>
                Anyway, I had fun writing this, hope you had fun reading. Till next time!
              </p>
              
              <p>
                - <a href="https://github.com/filipw/Metadata.WebApi">source code on Github</a>
              </p>

 [1]: /2012/04/rss-atom-mediatypeformatter-for-asp-net-webapi/

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