Pagination

Applies To: yes OData Client V7 Loading large datasets can be slow. Services often rely on pagination to load the data incrementally to improve the response times and the user experience. Paging can be server-driven or client-driven.

Server-driven paging

In Server-driven paging, the server returns the first page of results. If total number of results is greater than the page size, the server returns the first page along with a nextlink that can be used to fetch the next page of results.

The OData Client deals with server-driven paging with the help of DataServiceQueryContinuation and DataServiceQueryContinuation<T>. These classes contain the nextLink of the partial set of items.

Top Level Pagination

Example:

DefaultContainer context = new DefaultContainer(new Uri("https://services.odata.org/v4/TripPinServiceRW/"));

// DataServiceQueryContinuation<T> contains the next link
DataServiceQueryContinuation<Person> nextLink = null;

// Get the first page
QueryOperationResponse<Person> response = await context.People.ExecuteAsync() as QueryOperationResponse<Person>;
int pageCount = 0;

do
{
    Console.WriteLine($"Page {++pageCount}");
    if (nextLink != null)
    {
        response = await context.ExecuteAsync<Person>(nextLink) as QueryOperationResponse<Person>;
    }

    // You must enumerate the response before calling GetContinuation below.
    foreach (Person person in response)
    {
        Console.WriteLine($"\tPerson Name: {person.FirstName}");
    }

}
// Loop if there is a next link
while ((nextLink = response.GetContinuation()) != null);

Nested Pagination

There are instances where we need to load pages of entities as well as pages of their related entities. The example below returns related Trips for each Person entity from the data service. It uses a do...while loop to paginate through Person entities and a nested while loop to paginate through the related Trips. We need to enumerate the response before calling GetContinuation on it.

DefaultContainer context = new DefaultContainer(new Uri("https://services.odata.org/v4/TripPinServiceRW/"));
int pageCount = 0;
DataServiceQueryContinuation<Person> nextLink = null;

try
{
    // Execute the query for all people and related trips,
    // and get the response object.
    QueryOperationResponse<Person> response = await
        context.People.Expand("Trips")
        .ExecuteAsync() as QueryOperationResponse<Person>;

    // With a paged response from the service, use a do...while loop
    // to enumerate the results before getting the next link.
    do
    {
        // Write the page number.
        Console.WriteLine($"Person Page {++pageCount}:");

        // If nextLink is not null, then there is a new page to load.
        if (nextLink != null)
        {
            // Load the new page from the next link URI.
            response = await context.ExecuteAsync<Person>(nextLink)
                as QueryOperationResponse<Person>;
        }

        // Enumerate the Person(s) in the response.
        foreach (Person person in response)
        {
            var innerPageCount = 0;
            Console.WriteLine($"\tPerson Name: {person.FirstName}");

            // Get the next link for the collection of related Trips.
            DataServiceQueryContinuation tripsNextLink =
                response.GetContinuation(person.Trips);

            if (person.Trips.Count > 0)
            {
                Console.WriteLine($"\t\tTrips Page {++innerPageCount}:");
            }
            foreach (Trip trip in person.Trips)
            {
                // Print out the trips in the first page.
                Console.WriteLine($"\t\t\tTripID: {trip.TripId} - Name: {trip.Name}");
            }

            while (tripsNextLink != null)
            {
                // Load the next page of Trips.
                var tripsResponse = await context.LoadPropertyAsync(person, "Trips", tripsNextLink);
                tripsNextLink = tripsResponse.GetContinuation();

                if (tripsResponse.Count > 0)
                {
                    Console.WriteLine($"\t\tTrips Page {++innerPageCount}:");
                }
                foreach (Trip trip in tripsResponse)
                {
                    // Print out the trips.
                    Console.WriteLine($"\t\t\tTripID: {trip.TripId} - Name: {trip.Name}");
                }
            }
        }
    }

    // Get the next link, and continue while there is a next link.
    while ((nextLink = response.GetContinuation()) != null);
}
catch (DataServiceQueryException ex)
{
    throw new ApplicationException(
        "An error occurred during query execution.", ex);
}

Below is the sample output (Assuming the Trippin service had a pagesize of 2):

Person Page 1:
        Person Name: Russell
            Trips Page 1:
                TripID: 0 - Name: $Trip in US
                TripID: 1003 - Name: $Trip in Beijing

            Trips Page 2:
                TripID: 1007 - Name: $Honeymoon

        Person Name: Scott
            Trips Page 1:
                TripID: 0 - Name: $Trip in US
                TripID: 2004 - Name: $Trip in Beijing
        .............................................
        .............................................
        .............................................

Person Page 2:
        Person Name: Marshall
            Trips Page 1:
               .............................................
               ............................................

        Person Name: Ryan
            Trips Page 1:
               .............................................
               .............................................

            Trips Page 2:
               .............................................
               .............................................

Client-driven paging

In client-driven paging, we request the server to return the specified number of results. There is no nextLink that is returned.

The OData Client deals with client-driven paging using $skip and $top query options.

The $top query option requests the number of items in the queried collection to be included in the result.

The $skip query option requests the number of items in the queried collection that are to be skipped and not included in the result.

For GET https://host/service/People?$skip=3&$top=5

DefaultContainer context = new DefaultContainer(new Uri("https://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/"));
IQueryable<Person> people =
    context.People
        .Skip(3)
        .Take(5);
foreach (Person person in people)
{
    Console.WriteLine($"Username: {person.UserName} First Name: {person.FirstName}");
}