# GraphQL in .NET: Pagination, Filtering and Sorting

In our post [GraphQL in .NET with Hot Chocolate](https://blog.raulnq.com/graphql-in-net-with-hot-chocolate), we gave an introduction about how to use the library with a simple example. Today we want to extend that exercise with a set of common features such as pagination, filtering, and sorting, showing how easy it is to implement them with [Hot Chocolate](https://chillicream.com/docs/hotchocolate/v12).

## Prerequisites

We will use as a starting code the solution located [here](https://github.com/raulnq/graphql-sandbox/tree/main), clone or download the code. Let's start by adding a new NuGet package to the `api` project:

```bash
dotnet add api package HotChocolate.Data --version 12.14.0
``` 

> All HotChocolate.* packages need to have the same version.

Open the `Program.cs` file and replace the content with:

```csharp
using api;

var builder = WebApplication.CreateBuilder(args);
var posts = Enumerable.Range(0, 20).Select(index => new Post() { Body = $"Body {index}", Id = Guid.NewGuid(), Title = $"Title {index}" });
builder.Services.AddSingleton(new Storage() { Posts = new List<Post>(posts) });

builder.Services
    .AddGraphQLServer()
    .AddQueryType<PostQueriesType>()
    .AddMutationType<PostMutationsType>() ;

var app = builder.Build();
app.MapGraphQL();
app.Run();
``` 

## Pagination

### Offset-based

Offset-based pagination is a technique wherein the client requests two parameters to paginate through items in a collection: 

- `limit`: the number of records.
- `offset`:  number of records that need to be skipped.

Go to the `PostQueriesType.cs` file to add `UseOffsetPaging()` to the descriptor as follow:

```csharp
public class PostQueriesType : ObjectType<PostQueries>
{
    protected override void Configure(IObjectTypeDescriptor<PostQueries> descriptor)
    {
        descriptor
            .Field(f => f.GetPost(default!, default!))
            .Type<PostType>();

        descriptor
            .Field(f => f.GetPosts(default!))
            .Type<ListType<PostType>>()
            .UseOffsetPaging();
    }
}
``` 

And that's it. Hot Chocolate will add a middleware in charge of applying the pagination arguments to what we have returned. Run the application with the following command:

```bash
dotnet run --project api\api.csproj --urls="http://localhost:7121"
``` 

Open the URL `http://localhost:7121/graphql/` in your browser and run the following query in [Banana Cake Pops](https://chillicream.com/docs/bananacakepop):

```json
query {
  posts(skip: 2, take: 5) {
    items {
      title
      body
      id
    }
    pageInfo {
      hasNextPage
      hasPreviousPage
    }
  }
}
``` 

`PagingOptions` is available as an argument of the `UseOffsetPaging` method:

```csharp
descriptor
	.Field(f => f.GetPosts(default!))
	.Type<ListType<PostType>>()
	.UseOffsetPaging(options: new PagingOptions()
	{
		MaxPageSize = 10, 
		DefaultPageSize = 5, 
		IncludeTotalCount =true,
	});
``` 

### Cursor-based

A cursor is a unique identifier for a specific record, which acts as a pointer to the next record we want to start querying from to get the next page of results. We will update the `PostQueriesType.cs` file to use `UsePaging()` as follow:

```csharp
public class PostQueriesType : ObjectType<PostQueries>
{
    protected override void Configure(IObjectTypeDescriptor<PostQueries> descriptor)
    {
        descriptor
            .Field(f => f.GetPost(default!, default!))
            .Type<PostType>();

        descriptor
            .Field(f => f.GetPosts(default!))
            .Type<ListType<PostType>>()
            .UsePaging();
    }
}
``` 

Run the following query:

```json
query {
  posts(first: 2) {
    edges {
      cursor
      node {
        title
        body
        id
      }
    }
    pageInfo {
      hasNextPage
      hasPreviousPage
      startCursor
      endCursor
    }
  }
}
``` 

To get something like:

```json
{
  "data": {
    "posts": {
      "edges": [
        {
          "cursor": "MA==",
          "node": {
            "title": "Title 0",
            "body": "Body 0",
            "id": "f8641118a70c455c954f067a58b9c02e"
          }
        },
        {
          "cursor": "MQ==",
          "node": {
            "title": "Title 1",
            "body": "Body 1",
            "id": "1c631d9cb12b4d68960ff075ae3ab095"
          }
        }
      ],
      "pageInfo": {
        "hasNextPage": true,
        "hasPreviousPage": false,
        "startCursor": "MA==",
        "endCursor": "MQ=="
      }
    }
  }
}
``` 

Let's see the structure of the result:

- `Connections`: Instead of returning a list of entries, we return a Connection.
- `Edges`: We return an array of Edges in a Connection. An Edge has two properties, a `cursor`(a unique identifier for the entry) and a `node`(the entry itself).
- `PageInfo`: Information to aid in pagination.

The list of arguments that we can use are:

- `first`: Returns the first N elements from the list.
- `after`: Returns the elements in the list that come after the specified cursor.
- `last`: Returns the last N elements from the list.
- `before`: Returns the elements in the list that come before the specified cursor.

`PagingOptions` is available as an argument of the `UsePaging` method:

```csharp
descriptor
	.Field(f => f.GetPosts(default!))
	.Type<ListType<PostType>>()
	.UsePaging(options: new PagingOptions()
	{
		MaxPageSize = 10, 
		DefaultPageSize = 5, 
		IncludeTotalCount =true,
	});
``` 

## Filtering 

To use the Hot Chocolate default filtering implementation, we need to modify the `Program.cs` file with:

```csharp
builder.Services
    .AddGraphQLServer()
    .AddQueryType<PostQueriesType>()
    .AddMutationType<PostMutationsType>()
    .AddFiltering();
``` 

Then go to the `PostQueriesType.cs` file and update as follow:

```csharp
public class PostQueriesType : ObjectType<PostQueries>
{
    protected override void Configure(IObjectTypeDescriptor<PostQueries> descriptor)
    {
        descriptor
            .Field(f => f.GetPost(default!, default!))
            .Type<PostType>();

        descriptor
            .Field(f => f.GetPosts(default!))
            .Type<ListType<PostType>>()
            .UsePaging(options: new PagingOptions()
            {
                MaxPageSize = 10,
                DefaultPageSize = 5,
                IncludeTotalCount = true,
            })
            .UseFiltering();
    }
}
``` 

Run the application and go to `Schema Definition` to see the `PostFilterInput` type:

```json
input PostFilterInput {
  and: [PostFilterInput!]
  or: [PostFilterInput!]
  id: ComparableGuidOperationFilterInput
  title: StringOperationFilterInput
  body: StringOperationFilterInput
}

input StringOperationFilterInput {
  and: [StringOperationFilterInput!]
  or: [StringOperationFilterInput!]
  eq: String
  neq: String
  contains: String
  ncontains: String
  in: [String]
  nin: [String]
  startsWith: String
  nstartsWith: String
  endsWith: String
  nendsWith: String
}

input ComparableGuidOperationFilterInput {
  eq: UUID
  neq: UUID
  in: [UUID!]
  nin: [UUID!]
  gt: UUID
  ngt: UUID
  gte: UUID
  ngte: UUID
  lt: UUID
  nlt: UUID
  lte: UUID
  nlte: UUID
}
``` 

Let's run a query based on that definition:

```json
query {
  posts(where: { title:{ contains: "5" } }) {
    edges {
      cursor
      node {
        title
        body
        id
      }
    }
  }
}
``` 

If we don't want to expose all this filtering surface to our clients, we can customize what we offer and how. Let's rename the `title` argument to `topic` limit the search to only use `contains`, and disallow the use of the `and` operator. Create a `PostFilterType.cs` file with the following content:

```csharp
public class PostFilterType : FilterInputType<Post>
{
    protected override void Configure(
        IFilterInputTypeDescriptor<Post> descriptor)
    {
        descriptor.BindFieldsExplicitly();
        descriptor.Field(f => f.Title).Name("topic").Type<TitleOperationFilterInput>();
    }
}

public class TitleOperationFilterInput : StringOperationFilterInputType
{
    protected override void Configure(IFilterInputTypeDescriptor descriptor)
    {
        descriptor.Operation(DefaultFilterOperations.Contains).Type<StringType>();
        descriptor.AllowOr(false);
    }
}
``` 

Update the `PostQueriesType.cs` file with:

```csharp
public class PostQueriesType : ObjectType<PostQueries>
{
    protected override void Configure(IObjectTypeDescriptor<PostQueries> descriptor)
    {
        descriptor
            .Field(f => f.GetPost(default!, default!))
            .Type<PostType>();

        descriptor
            .Field(f => f.GetPosts(default!))
            .Type<ListType<PostType>>()
            .UsePaging(options: new PagingOptions()
            {
                MaxPageSize = 10,
                DefaultPageSize = 5,
                IncludeTotalCount = true,
            })
            .UseFiltering<PostFilterType>();
    }
}
``` 

Run the application, and let's see, again, the `Schema Definition`:

```json
input PostFilterInput {
  and: [PostFilterInput!]
  or: [PostFilterInput!]
  topic: TitleOperationFilterInput
}

input TitleOperationFilterInput {
  and: [TitleOperationFilterInput!]
  contains: String
}
``` 

## Sorting

To use the Hot Chocolate default sorting implementation, we need to modify the `Program.cs` file with:

```csharp
builder.Services
    .AddGraphQLServer()
    .AddQueryType<PostQueriesType>()
    .AddMutationType<PostMutationsType>()
    .AddFiltering()
    .AddSorting();
``` 

Then go to the `PostQueriesType.cs` file and update as follow:

```csharp
public class PostQueriesType : ObjectType<PostQueries>
{
    protected override void Configure(IObjectTypeDescriptor<PostQueries> descriptor)
    {
        descriptor
            .Field(f => f.GetPost(default!, default!))
            .Type<PostType>();

        descriptor
            .Field(f => f.GetPosts(default!))
            .Type<ListType<PostType>>()
            .UsePaging(options: new PagingOptions()
            {
                MaxPageSize = 10,
                DefaultPageSize = 5,
                IncludeTotalCount = true,
            })
            .UseFiltering<PostFilterType>()
            .UseSorting();
    }
}
``` 

> Note: If you use more than one middleware, keep in mind that ORDER MATTERS. The correct order is UsePaging > UseProjections > UseFiltering > UseSorting

Run the application and go to `Schema Definition` to see the `PostSortInput` type:

```json
input PostSortInput {
  id: SortEnumType
  title: SortEnumType
  body: SortEnumType
}

enum SortEnumType {
  ASC
  DESC
}
``` 

As we did with the filtering, we can customize the sorting. Let's limit the sorting to the `title` property. Create a `PostSortType.cs` file with the following content:

```csharp
public class PostSortType : SortInputType<Post>
{
    protected override void Configure(ISortInputTypeDescriptor<Post> descriptor)
    {
        descriptor.BindFieldsExplicitly();
        descriptor.Field(f => f.Title).Name("topic");
    }
}
``` 

Modify the `PostQueriesType.cs` file with:

```csharp
public class PostQueriesType : ObjectType<PostQueries>
{
    protected override void Configure(IObjectTypeDescriptor<PostQueries> descriptor)
    {
        descriptor
            .Field(f => f.GetPost(default!, default!))
            .Type<PostType>();

        descriptor
            .Field(f => f.GetPosts(default!))
            .Type<ListType<PostType>>()
            .UsePaging(options: new PagingOptions()
            {
                MaxPageSize = 10,
                DefaultPageSize = 5,
                IncludeTotalCount = true,
            })
            .UseFiltering<PostFilterType>()
            .UseSorting<PostSortType>();
    }
}
``` 

Start up the application and run the query:

```json
  posts( order: { topic: DESC } ) {
    edges {
      cursor
      node {
        title
        body
        id
      }
    }
  }
}
``` 

All these topics, and more, can be found in the official documentation [here](https://chillicream.com/docs/hotchocolate/v12/fetching-data). The code of this post is available [here](https://github.com/raulnq/graphql-sandbox/tree/data). Thanks, and happy coding.
