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)

Why I Changed Jobs

The word is out. I’m changing employers. For the sake of all parties involved, let me take a minute to explain the move. I realize some people are making assumptions, and I want everyone to be clear.

First, no ill will towards the old employer

I love what they’re doing. The whole idea behind CommonWell is genius and I hope it becomes a larger reality. They took a WinForms developer and gave him the chance to become an experienced, certified web developer. I got to fly out to San Francisco enough that I was able to learn the sitar. I got to work on the latest technologies. I had a great set of coworkers that were very capable, with an unusually low amount of egos.

There were frustrations, and occasional drama. But what workplace doesn’t have that? And, even in retrospect, those issues were less frequent than most jobs I’ve had and definitely less than a lot of people I talk to at other companies.

So, to be clear, I am not leaving because I’m overly frustrated with something or someone.

It came down to career direction

About eight months ago, a few months before this WordPress account was created, I sat down and tried to figure out what I want to do with the rest of my career. I realized I had two choices. I could stay where I was and use my developer talents to create software in the medical vertical, or I could harness some of my other talents and be something more.

So what are those other talents? In spite of being really smart, I have pretty good social skills (that was a paraphrase of real compliments). I can take relatively complex concepts and break them down so that people can understand them. And, based on the grades of my papers in college, I’m at least a decent writer. All this adds up to the fact that, in addition to coding, I could be facing customers. I could be blogging (is blogging about blogging redundant?). I could speak at conferences or teach.

All those things sound fun, and I wanted to morph my career path to use all those. And that’s where the slight disconnect happened. My employer did not have such a position, and it could be argued that they shouldn’t have that kind of position. They’re selling a set of web services, or a platform, for their customers to use. The realization of that platform did not require the other activities I wished to pursue. In fact, time spent on those other activities would take away from time spent making the platform awesome.

Enter the new employer

The new company has a different model. They’re a consulting firm that sells consulting services. Those services are only as good as the consultant performing them. Therefore, it is in their best interest to have well known hot shots who are part of the development scene. That means they want, or even need, people that are blogging, speaking, and teaching, in addition to developing solutions.

And that is how we came together. My career path exactly matches their employee needs, and together we benefit from some kind of symbiosis. They give me time and budget for blogging, speaking, and going to conferences. With that, I gain stronger skills and notoriety. In return, they are able to sell more consulting because they have a demonstrably better staff than the competition. Imagine how easy it is for the salesperson after they show blogs proving that the company has thought leadership in the area the customer needs.

Final Thoughts

All jobs have hiccups. Your coworkers aren’t perfect. Some of the processes are inefficient. You shouldn’t let those things get to you, or you end up jumping from job to job, letting your emotions dictate your career path by accident. Instead, figure out how to handle those things. Listen to your coworkers before judging, and give them the benefit of the doubt sometimes because technical people are known for having bad communication skills. Remember the qualified compliment I mentioned earlier? I only have pretty good social skills, in light of how smart I am. We all need to be tolerated sometimes.