Paging with Range Headers using AngularJS and Web API 2 (Part 2)

In part one of this series, I talked about my need to add paging to a web page, how the HTTP range headers work, and how to use them on the client side in AngularJS. If you jumped in here, you can review that here. In this post, I’ll go over how to use the range headers in an ASP.NET Web API controller.

Getting the values

Getting the values for the range header in the Api controller is fairly simple. Inherited from ApiController is a property called Request, which represents the HttpRequestMessage. In the Request is a Headers collection, and in that collection is the Range. If the range header wasn’t there, I typically assume the user wants all the records so I default the from and to values to 0 and the maximum long value. It looks like this:

long fromCustomer = 0;
long toCustomer = long.MaxValue;
var range = Request.Headers.Range;
if (range != null)
{
    fromCustomer = range.Ranges.First().From.Value;
    toCustomer = range.Ranges.First().To.Value;
}

Returning the Content-Range

This is a little more complicated than getting the values, but still not that bad. The problem is that we don’t have a nice, convenient Response property on our controller. We have to create the response and return that from the method. We need to replace the return of the Get method from IEnumerable<Customer> to a Reponse object that implements IHttpActionResult. This gives us complete control over the Response.

Creating an implementation of IHttpActionResult is not that hard. You only have to implement ExecuteAsync, where you’ll create the response from the request. You’ll also need a constructor for passing in the needed values. Mine looks like this:

public class CustomerGetListResult : IHttpActionResult
{
    private readonly HttpRequestMessage _request;
    private readonly List<Customer> _customers;
    private readonly long _from;
    private readonly long _to;
    private readonly long? _length;

    public CustomerGetListResult(HttpRequestMessage request,
                                 List<Customer> customers,
                                 long from, long to, long? length)
    {
        // Save values for the execute later
        _request = request;
        _customers = customers;
        _from = from;
        _to = to;
        _length = length;
    }

    public Task<HttpResponseMessage> ExecuteAsync(
        CancellationToken cancellationToken)
    {
        HttpStatusCode code;
        if (_length.HasValue)
        {
            // status is 206 if there's more data
            // or 200 if it's at the end
            code = _length - 1 == _to
                ? HttpStatusCode.OK
                : HttpStatusCode.PartialContent;
        }
        else
        {
            // status is 200 if we don't know length
            code = HttpStatusCode.OK;
        }
        // create the response from the original request
        var response = _request.CreateResponse(code, _customers);
        // add the Content-Range header to the response
        response.Content.Headers.ContentRange = _length.HasValue
            ? new ContentRangeHeaderValue(_from, _to, _length.Value)
            : new ContentRangeHeaderValue(_from, _to);
        response.Content.Headers.ContentRange.Unit = "customers";

        return Task.FromResult(response);
    }
}

In ExecuteAsync, we have to take care of three things. First, determine the status code we want to return. Second, create the response with the status code and content. Finally, we add the Content-Range header. Notice the overloaded ContentRangeHeaderValue constructor. If you use the two parameter version, the header value will look like “customers 0-19/*” to indicate that the length is unknown.

Now all that we need to do is construct the result and return it from the controller, which I do in one step:

return new CustomerGetListResult(Request,
    customerList,
    fromCustomer,
    fromCustomer + customerList.Count() - 1,
    CustomerTable.Count());

Final Thoughts

So that’s pretty much it. Remember to check out Part 1 if you haven’t already seen it. Feel free to leave comments, or constructive criticism, or questions below.

Also, the sample code is in my github at https://github.com/qanwi1970/customer-paging.

Paging with Range Headers using AngularJS and Web API 2 (Part 1)

Recently, I needed to take a page with a list and add paging and sorting to it. We didn’t do it when we first wrote the page because the data set was small and we had higher priorities. However, the time was getting near when people would have to deal with a table of over 100 rows. It sounded simple, until the little trick of letting the client know when not to page. Somehow, I needed to tell the client how many total records there were so it could appropriately disable the Next arrow. I read about and debated lots of methods, but finally settled on using HTTP Range headers. I could write about that, but I can save time by saying that I pretty much ended up agreeing with this guy.

Before I get into how to use Range headers, let me briefly cover the what. The HTTP spec allows for partial downloading of content by having the client ask for the part it needs. One of the standard response headers is Accept-Ranges, and it tells the client whether asking for a range is allowed, and what unit to ask for. In the past, the range unit was typically bytes, which the client could use to download files in pieces. Once it knew that, it would ask for a range of bytes using the Range header. The bytes would be in the response content, and the Content-Range response header would tell the client the unit (again), the start and end of the range, and possibly the length of the file.

Fast forward to our RESTful way of doing things and the range unit becomes something like “customers” and the length is more like the total number of customers available. The client might ask for “customers=0-19” with the expectation of getting the first 20 customers. The server would respond with something like “customers 0-19/137”, meaning that it gave the first 20, and there are 137 total customers.

On the AngularJS side, we use the transformRequest field of the resource to add a transform function that will add the Range header. I tried to use the header field, but that just sets the default header for each getCustomers request and we need the header to change for each call. It looks like this:

var customerServices = angular.module('customerServices',
                                      ['ngResource']);

customerServices.factory('customerService', [
    '$resource',
    function($resource) {
        return $resource('/api/CustomerService', {}, {
            getCustomers: {
                method: 'GET',
                isArray: true,
                transformRequest: function(data, headersGetter) {
                    var headers = headersGetter();
                    headers['Range'] = 'customers='
                        + fromCustomer + '-' + toCustomer;
                }
            }
       });
    }
]);

I’ll skip over the Web API part of it, for now (wait for Part 2), and go over handling the response. In the success block of getCustomers, we get the Content-Range header and parse out the important pieces.

 
customerService.getCustomers({},
     function(value, headers) {
         var rangeFields = headers('Content-Range').split(/\s|-|\//);
         $scope.fromCustomer = parseInt(rangeFields[1]);
         $scope.toCustomer = parseInt(rangeFields[2]);
         $scope.totalCustomers = parseInt(rangeFields[3]);
         // and then do cool stuff...

Now that the client knows the from customer, the to customer, and the total number of customers, it can do all the neat paging stuff it wants. It can enable and disable the Previous or Next control. It could place page number links that let the user skip to whatever page they want. It could have some kind of button that goes all the way to the end or all the way to the beginning, and so on.

Don’t miss Part 2, where I’ll go over the Web API side of this puzzle.

Sample code for this series can be found at: https://github.com/qanwi1970/customer-paging

 

Special thanks to Mathieu Brun for the data ganerator used in the sample (https://github.com/mathieubrun/Cogimator.SampleDataGenerator)