left-icon

ServiceStack Succinctly®
by Zoran Maksimovic

Previous
Chapter

of
A
A
A

CHAPTER 6

Pagination

Pagination


It's almost always a bad idea to return every available resource to the client at once. A technique that is usually referred to as pagination (or sometimes as paging mechanism, especially on webpages) is used for displaying a limited number of results where a returned data set is too big to be displayed in one place or at once.

Usually whenever pagination is implemented, this is followed by the information on how to get the next result set or how to get to the beginning of the list. RESTful services are not much different in this sense. It is possible to create a representation of resources by using pagination.

Pagination as a Representation

When building REST web services, we usually think about resources. The first question to pose is if the page or pagination is a resource on its own. In my opinion it is not, and, instead, the information about the pagination is just an integrated part of the resource itself. In other words, the information about how to page through, let’s say a collection of Products, will be embedded in the Product collection itself.

There are mainly three locations to place the information about pagination:

  • Directly in the URI path: /products/page/1.
  • Part of the URI query string: /products?page=1.
  • Range HTTP headers: a topic we won’t be covering in this book, but is worth mentioning.[22]

As previously mentioned, the first option is not the best idea because pagination is not a resource (this option, through the URI, looks like a page is a resource). So, I would generally skip this way of representing the pagination.

The second option represents a standard way of solving this problem as it enables the encoding of paging application directly in the query string. In our examples, we are going to use this approach.

Pagination query options

  1. Pagination query options

In this chapter, we are going to see how to implement pagination for the already existing Products that we have implemented in the ProductService. The pagination makes sense only when returning a list of objects and, therefore, the perfect candidate for us would be the GET /products method which returns the list of available products.

Query String

There are several ways and styles of expressing the paging components when it comes to the naming convention for the query string parameters. In some examples, you will see offset and limit being mentioned. In other examples, you will see skip and take. But I will use a more intuitive and simple way, which is page and size:

  • page: Represents the current page.
  • size: Represents the number of items to be displayed in a page.

Discoverability

In order to inform the client application that there is a way to navigate through pages, we will use the Links section. Discoverability is part of the aforementioned HATEOAS constraint. The basic idea is the service exposes links to resources available. In our case, they are next, previous, first, and last so that the client is able to discover the URI associated with the resource and to further navigate to the resource. It is a bit similar to navigating a webpage through a browser. The links are the shown to us and, by following the links, we can discover new resources (in this case, pages).

Actually, the logic is straightforward; the client will know which URI is available through the Link.Rel attribute.

  1. Pagination links

Link.Rel

Description

next

Goes to the next page.

previous

Goes to the previous page.

first

Goes to the first page, which is the beginning of the list.

last

Goes to the last page, which is the end of the list.

Service Model

You might remember the GetProducts object that was used to retrieve a list of Products. We will expand this object to contain the two aforementioned query string parameters, and we will add a List<Link> property to the ProductsResponse.

public class GetProducts

{

    public int? Page { getset; }

    public int? Size { getset; }

}

public class ProductsResponse

{

    public ProductsResponse()

    {

        Links = new List<Link>();

    }

    public List<ProductResponse> Products { getset; }

    public List<Link> Links { getset; }

}

public class ProductResponse

{

    public ProductResponse()

    {

        Links = new List<Link>();

    }

    public int Id { getset; }

    public string Name { getset; }

    public Status Status { getset; }

    public List<Link> Links { getset; }

}

Data Access Layer

In order to add some structure to the pagination, we introduce in the data access layer a new class, PagedListResult.[23] An instance of this class will be returned from the repository when searching for products. The PagedListResult class is implemented as follows.

public class PagedListResult<TEntity>

{

    public bool HasNext { getset; }

    public bool HasPrevious { getset; }

    public int Page { getset; }

    public int Size { getset; }

    public long LastPage()

    {

        long lastPage = (Count/Size);

        if ((lastPage*Size) < Count) { lastPage++;}

        return lastPage;

    }

    public long Count { getset; }

    public IList<TEntity> Entities { getset; }

}

ProductRepository contains a new method called GetPaged(). As shown in the following code example, instead of only returning the products, we will enrich the object with extra information such as HasNext, HasPrevious, and Count, which represents the total number of items. This will later enable the service to properly create page links. The implementation is basic; it doesn’t contain any code regarding sorting fields and direction in order to keep the example simple. The intention is not to show how to implement the repository code, but rather how to wire all the pieces together.

List<Product> Products = new List<Product>();

public PagedListResult<Product> GetPaged(int page, int size)

{

    var skip = (page == 0 || page == 1) ? 0 : (page - 1)*size;

    var take = size;

 

    IQueryable<Product> sequence = Products.AsQueryable();

 

    var resultCount = sequence.Count();

 

    //Getting the result.

    var result = (take > 0)

                        ? (sequence.Skip(skip).Take(take).ToList())

                        : (sequence.ToList());

 

    //Setting up the return object.

    bool hasNext = (skip > 0 || take > 0)

                    && (skip + take < resultCount);

 

    return new PagedListResult<Product>()

        {

            Page = page,

            Size = size,

            Entities = result,

            HasNext = hasNext,

            HasPrevious = (skip > 0),

            Count = resultCount

        };

}

Product Mapper

As shown previously when we looked at the ProductService, we will extend the ProductMapper class with a new method called ToProductsResponse() to which we will pass the PagedListResult<Product> structure. I have omitted the actual Links generation logic to keep the code succinct; I show only the NextLink() method. But the important thing to note here is that we will generate extra links as explained in the Discoverability section.

public ProductsResponse ToProductsResponse(PagedListResult<Product> products)

{

    var productList = ToProductResponseList(products.Entities.ToList());

 

    var productResponse = new ProductsResponse();

    productResponse.Products = productList;

   

    SelfLink(products, productResponse);

    NextLink(products, productResponse);

    PreviousLink(products, productResponse);

    FirstLink(products, productResponse);

    LastLink(products, productResponse);

    return productResponse;

}

private void NextLink(PagedListResult<Product> products,

                                            ProductsResponse productResponse)

{

    if (products.HasNext)

    {

        productResponse

            .Links.Add(NewLink("next""products?page={0}&size={1}"

                           .Fmt(products.Page + 1, products.Size)));

    }

}

Service Implementation

Getting the list of products now requires two scenarios. In the first scenario, we want the paging, and in the second scenario, all the data is returned. This is not necessarily a real-world scenario, but I think that it is important to show this kind of implementation as well. If the page and size parameters are not supplied, the Get method will return the full list of products. Otherwise, the paging will kick in.

public ProductsResponse Get(GetProducts request)

{

    ProductsResponse response;

    int? page = request.Page;

    int? size = request.Size;

 

    if (page.HasValue && size.HasValue)

    {

        //Get data from the database.

        PagedListResult<Product> products = 

                          ProductRepository.GetPaged(page.Value, size.Value);

 

        response = ProductMapper.ToProductsResponse(products);

    }

    else

    {

        var products = ProductRepository.GetAll();

        response = new ProductsResponse()

            {

                Products = ProductMapper.ToProductResponseList(products)

            };

    }

    return response;

}

And finally, when retrieving data, we can see that the links get updated every time there are available products. The response of the web service would look like the following if we were requesting GET /products?page=1&size=100.

<ProductsResponse xmlns:i="http://www.w3.org/2001/XMLSchema-instance">

  <Links>

    <Link>

      <Href>products?page=1&size=100</Href>

      <Rel>self</Rel>

      <Title>self</Title>

      <Type i:nil="true"/>

    </Link>

    <Link>

      <Href>products?page=2&size=100</Href>

      <Rel>next</Rel>

      <Title>next</Title>

      <Type i:nil="true"/>

    </Link>

    <Link>

      <Href>products?page=1&size=100</Href>

      <Rel>first</Rel>

      <Title>first</Title>

      <Type i:nil="true"/>

    </Link>

    <Link>

      <Href>products?page=10&size=100</Href>

      <Rel>last</Rel>

      <Title>last</Title>

      <Type i:nil="true"/>

    </Link>

  </Links>

  <Products><!--omitted for brevity--></Products>

</ProductsResponse>

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.