How to implement offset pagination with Connection in Hot Chocolate?

228 views Asked by At

As doc says Note: Connections are often associated with cursor-based pagination, due to the use of a cursor. Nonetheless, since the specification describes the cursor as opaque, it can be used to facilitate an offset as well.

Note: While we support offset-based pagination, we highly encourage the use of Connections instead. Connections provide an abstraction which makes it easier to switch to another pagination mechanism later on.

but no example provided.

Any idea how to implement offset pagination with Connection in Hot Chocolate? Its not clear for me.

3

There are 3 answers

0
Stephen Raman On

The information here is based on an implementation of HotChocolate v13

I did find that the HotChocolate v12 documentation has some additional information on Pagination that helps. Best to read both.
https://chillicream.com/docs/hotchocolate/v12/fetching-data/pagination https://chillicream.com/docs/hotchocolate/v13/fetching-data/pagination

Using the [UsePaging] attribute on the query automatically adds the connection to the schema. In the below example, I have a type called AreaType. The AreaTypesConnection was automatically created when using [UsePaging]. You will need to perform some operations on the client-side to populate the parameters highlighted below. These parameters configure the cursor.

enter image description here

Under the connection, the nodes contains your collection of items returned by the query (e.g. areaTypes). The totalCount returns the number of items being retrieved.

enter image description here

Under pageInfo, there are additional values that HotChocolate populates to help configure the cursor on the client-side.

enter image description here

If you were strictly looking for configuring cursor pagination on the client-side, I would not search for HotChocolate examples specifically. I would google the client-side technology you are using (e.g. "cursor pagination in angular"). Combined with the above information should help.

Of course, you can opt to not use connections. You can use the [UseOffsetPaging] attribute instead of the [UsePaging] attribute. When using the [UseOffsetPaging] attribute, a collection segment is created instead of a connection. There is a little more ease in this option. You only need to configure skip and take parameters.

enter image description here

When viewing the collection segment, instead of nodes, the collection of items is simply called items. You can still retrieve your totalCount which can be used to determine your skip and take parameter values. Under pageInfo, is additional properties populated by HotChocolate that you may also need to determine your skip and take parameter values.

enter image description here

0
larahina On

I've found myself asking exactly the same question when reading the Hot Chocolate docs. Although I haven't found a completely satisfying answer for myself, I'll share my insights on this. Hope it helps you to implement offset paging with connections.

You could use the [UseOffsetPaging] middleware to implement offest paging, but as the docs say this is discouraged, so it's not the solution we're looking for.

The GraphQL Pagination best practices describe three different approaches how to implement paginagion: offset-based friends(first:2 offset:2), id-based friends(first:2 after:$friendId) and cursor-based friends(first:2 after:$friendCursor). It recommends using the cursor-based approach with opaque cursors because this can be used to implement both offset-based and id-based paginagion by making the cursor either the offset or the ID. So the key question is, how's the cursor defined.

I've found, that the Hot Chocolate middleware uses the offset as cursor per default. Imagine a database containing some products data and a query resolver exposing the product data using the paging middleware. I'm using the Entity Framework Core integration to fetch the product items from the database.

public class Product
{
    [ID]
    public int Id { get; set; }
    public string? Name { get; set; }
}
public class Query
{
    [UsePaging(IncludeTotalCount = true, AllowBackwardPagination = false)]
    [UseSorting]
    public IQueryable<Product> GetProducts(ApplicationDbContext dbContext)
    {
        return dbContext.Products;
    }
}

This creates the following schema reference: products connection schema reference

When querying the products with the following query:

query Products{
  products(first: 3, order: {id: ASC}){
    edges{
      cursor
      node{
        id
        name
      }
    }
    totalCount
  }
}

it returns

{
  "data": {
    "products": {
      "edges": [
        {
          "cursor": "MA==", #base64 decoded: "0"
          "node": {
            "id": "3",
            "name": "Product A"
          }
        },
        {
          "cursor": "MQ==", #base64 decoded: "1"
          "node": {
            "id": "5",
            "name": "Product B"
          }
        },
        {
          "cursor": "Mg==", #base64 decoded: "2"
          "node": {
            "id": "8",
            "name": "Product C"
          }
        }
      ],
      "totalCount": 6
    }
  }
}

So Hot Chocolate is actually using an offset-based paging. If you want to query n items from index i you can do this by querying products(first: $n, after: $cursor) where $cursor is a base64 encoded string containing the result of i-1.

This way you can implement an offset-based paging using GraphQL connections.

Where I haven't found a satisfying answer ist how to actually use id-based pagination with Hot Chocolate. It would then use the product Id as cursor instead of the offset. I imagine there should be a way to configure Hot Chocolate to use the ID as cursor, but unfortunately I haven't found a way to do that yet...

0
iCodeSometime On

It's pretty straightforward! Just return the index + 1 as the endCursor.

private List<int> _data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
[UsePaging(MaxPageSize = 10, AllowBackwardPagination = false)]
public Connection<int> GetMyThing(string? after, int first = 5)
{
    var skip = int.Parse(after ?? "0");
    var edges = new List<Edge<int>>();
    var index = 0;
    foreach (var item in _data.Skip(skip).Take(first))
    {
        edges.Add(new Edge<int>(item, (skip + index + 1).ToString()));
        index++;
    }
    var pageInfo = new ConnectionPageInfo(
        _data.Count > skip + first, // has next page
        skip > 0, // has previous page
        edges.First().Cursor, // startCursor
        edges.Last().Cursor); // endCursor
    return new Connection<int>(edges, pageInfo);
}

https://chillicream.com/docs/hotchocolate/v13/fetching-data/pagination/#custom-pagination-logic