Record paging is a really common requirement for APIs that expose a lot of data. Paging in Azure Cosmos DB SQL API is done using continuation tokens. This post demonstrates how to use them to implement a paged API.
When querying Cosmos DB through the REST API you can specify a maximum count to return in the x-ms-max-item-count
header. This acts as a page size. If your query actually has more results than this then the response will include a continuation token. That token can be passed in another request in the x-ms-continuation
header to get the next page of results. The .NET SDK allows you to specify these values in a FeedOptions
object when building your query.
To demonstrate, I built a simple Azure Function with an HttpTrigger
that allows clients to request paged data from Cosmos DB. The complete code is below.
[FunctionName(“GetRecords“)] | |
public static async Task<IActionResult> GetRecords( | |
[HttpTrigger(AuthorizationLevel.Function, “get“, Route = “records“)] | |
HttpRequest req, | |
[CosmosDB(databaseName: “Records“, collectionName: “records“, ConnectionStringSetting = “CosmosDBConnection“)] | |
DocumentClient client) | |
{ | |
var queryParams = req.GetQueryParameterDictionary(); | |
// Maximum records to return (default to 50) | |
var count = Int32.Parse(queryParams.FirstOrDefault(q => q.Key == “count“).Value ?? “50“); | |
// Continuation token (for paging) | |
var continuationToken = queryParams.FirstOrDefault(q => q.Key == “continuationToken“).Value; | |
// Build query options | |
var feedOptions = new FeedOptions() | |
{ | |
MaxItemCount = count, | |
RequestContinuation = continuationToken | |
}; | |
var uri = UriFactory.CreateDocumentCollectionUri(“Records“, “records“); | |
var query = client.CreateDocumentQuery(uri, feedOptions) | |
.AsDocumentQuery(); | |
var results = await query.ExecuteNextAsync(); | |
return new OkObjectResult(new | |
{ | |
hasMoreResults = query.HasMoreResults, | |
pagingToken = query.HasMoreResults ? results.ResponseContinuation : null, | |
results = results.ToList() | |
}); | |
} |
A client first requests data by performing a GET request to api/records
. I am defaulting the MaxItemCount
to 50, but allow the caller to override that setting with a query string parameter. The response will look something like this:
{ | |
“hasMoreResults”: true, | |
“pagingToken”: “{\”token\”:\”AV40ANomRkpkAAAAAAAAAA==\”,\”range\”:{\”min\”:\”\”,\”max\”:\”FF\”}}“, | |
“results”: [{ }] | |
} |
To get the next page of data the user simply passes the continuation token in the query string, for example api/records?continuationToken={token}
.
There are some limitations with this method. For one, there is no way to retrieve a total record count without performing a second expensive query. Another concern is it leaks implementation details to the client by passing the Cosmos DB-specific token for paging. A better approach would be to cache this continuation token in Redis or table storage using your own unique string as a key. But I’ll leave that up to you to implement.