Introduction

Ever wondered why the Azure Portal has scroll forward pagination and next/previous pagination but no page size + page number pagination? Go on take a wild guess. If you said "Because it runs on CosmosDB" then you're right. The Azure portal is getting it's results from CosmosDB that's why you don't see any form of server side skip and take pagination.

If you ever see a page size and page number combination in the portal then the pagination is happening on the client side. An example of that would be the "Logs" section of Application Insights, which is able to return paged results with page size and page number but those results are pre-fetched and they are just rendered on the spot. We know that because you can find all the results returned in pages of 100 in the network traffic tool of our browser of choice. The client side code will do the rest of the paging job.

CosmosDB pagination support

CosmosDB doesn't support your traditional type of skip/take pagination. Instead results are always paged with a maximum page size.

Skip take pagination is however, the most requested CosmosDB feature since August of 2014. The official ComosDB Team account replied in March of 2018 that they are planning to implement it, so we're good.

Judging based on the pace features are being delivered currently, I would not expect it to be delivered anytime soon so we need an alternative, at least for now.

So what does CosmosDB support as of today in terms of pagination?

Well, you have two values related to how results are being returned.

  • MaxItemCount
  • RequestContinuation

The MaxItemCount is the maximum nnumber of items to be returned in the enumeration operation. They can be less, but they won't exceed that number.
The RequestContinuation is the continuation token which identifies which was the last result set for this operation and points CosmosDB to the next set of data it needs to return.

This is enough functionality for someone to workaround the lack of skip take pagination. This is where Cosmonaut kicks in and does the heavy lifting for you.

Cosmonaut's implementation

Since I started working on Cosmonaut I knew that, eventually, I'd have to add fluent pagination support.

I did not want to mess with the Skip and Take LINQ methods because I know that eventually CosmosDB will support both of them (only supports Take for now).

Cosmonaut supports an IQueryable extension called WithPagination(). This method has two overloads.

  • WithPagination(int pageNumber, int pageSize)
  • WithPagination(string continuationToken, int pageSize)

As you can see, the first signature looks very much like a clear skip/take type implementation of the pagination. The second one is the direct CosmosDB approach.

How do they work?

The first signature, that has both page number and page size, can get inefficient and expensive as the page number value increases. This is because the only way this method can work is by going through all the pages from the start until it hits that requested page number and page size.

The second signature is both efficient and fast because it points straight to the next set of results. That's because you provide the continuation token.

These methods work in Cosmonaut with both LINQ and SQL queries.

How can you use them?

The first signature is as simple to use as the following.

var firstPage = await booksStore.Query().WithPagination(1, 10).OrderBy(x => x.Name).ToListAsync();  
var secondPage = await booksStore.Query().WithPagination(2, 10).OrderBy(x => x.Name).ToListAsync();  

As you can see, this reads very simple and I think is very straightforward. The same thing with SQL would look like this:

var firstPage = await booksStore.Query("select * from c order by c.Name asc").WithPagination(1, 10).ToListAsync();  
var secondPage = await booksStore.Query("select * from c order by c.Name asc").WithPagination(2, 10).ToListAsync();  

These methods work with both the CosmonautClient and the CosmosStore methods.

However, as I mentioned before, this is the method that can get inefficient as you go to higher page numbers. That's why the WithPagination signature that supports the continuation token is recommended.

"But how do I get the continuation token?"

The pagination update also introduced a new results extension method called ToPagedListAsync(). This method returns a CosmosPagedResults object. This object contains 3 very handy properties for your paged operations:

  • Results, which is a list of the results for your query
  • HasNextPage, which is a boolean indicating if there are more pages
  • NextPageToken, which is a string providing you the next page continuation token, if any

Let's see how this works. I'll write the same two page code that I have above but this time I'll use the continuation token approach.

var firstPage = await booksStore.Query().WithPagination(1, 10).OrderBy(x => x.Name).ToPagedListAsync();  
var secondPage = await booksStore.Query().WithPagination(firstPage.NextPageToken, 10).OrderBy(x => x.Name).ToPagedListAsync();  

Simple? I hope so. In a real life scenario, you would get the continuation token, store it on the client side (web app, desktop app, mobile app) and provide it for the next page.

CosmosPagedResults also support a GetNextPageAsync method which automatically gets the token and size from the last page. The same two queries would look like this:

var firstPage = await booksStore.Query().WithPagination(1, 10).OrderBy(x => x.Name).ToPagedListAsync();  
var secondPage = await firstPage.GetNextPageAsync();  

Note:

All of the above works with the CosmonautClient querying methods as well:

var result = await cosmonautClient.Query<Book>("localtest", "shared").WithPagination(1, 1).ToListAsync();  

Pagination recommendations

Because page number and page size pagination goes though all the documents until it gets to the requested page, it can get slow and expensive. The recommended approach would be to use the page number and page size method once for the first page and get the results using the .ToPagedListAsync() method. This method will return the next continuation token and it will also tell you if there are more pages for this query. Then use the continuation token alternative of WithPagination to continue from your last query.

Keep in mind that this approach means that you have to keep state on the client for the next query, but that's what you'd do if you were using previous/next buttons anyway.

I hope this helps you make integration with CosmosDB easier. Don't forget to ask questions in the comments if something is unclear and feel free to suggest features or express ideas for Cosmonaut. Also, if you like the project then giving it a star on Github means a lot. Thank you.