skip to content

🪆Nested Projections With Pagination In Gql Hot Chocolate

One of the key benefits of GraphQL is the ability to control the shape of the data returned, reducing over-fetching and under-fetching. we will explore nested projections with pagination in Gql Hot Chocolate to efficiently retrieve and paginate data in a GraphQL API

📽️Projection

In GraphQL, every request specifies exactly what data should be returned. This feature allows clients to retrieve only the necessary data, reducing the payload size and improving performance. Hot Chocolate leverages this concept by implementing projections, which directly project incoming queries to the underlying database.

✂️Pagination

Pagination is a common challenge when dealing with large datasets in backend implementations. Often, the volume of data is too large to pass directly to the consumer of the service. Pagination allows us to split the data into smaller, more manageable chunks, improving performance and reducing the strain on system resources.

⚠️Limitation

While projections can handle filtering and sorting efficiently, they have limitations when it comes to projecting paging over relations. When dealing with nested data structures, such as a user having multiple addresses, projecting pagination over relations becomes more complex. However, with some additional techniques, we can overcome this limitation.

Data Structure

Let’s consider a data structure where a user can have multiple addresses. Here’s an example of the C# classes representing this structure:

public class User {
    public Guid UserId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public List<Address> Addresses { get; set; } = new List<Address>();
}
 
public class Address {
    public Guid AddressId { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Country { get; set; }
}
public class User {
    public Guid UserId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public List<Address> Addresses { get; set; } = new List<Address>();
}
 
public class Address {
    public Guid AddressId { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Country { get; set; }
}

💡Solution

We can achieve efficient pagination and data projection by extending GraphQL types, implementing resolvers with data loaders, and utilizing middleware in Gql Hot Chocolate

Extend Base Node

To enable nested projections with pagination, we can extend the User type and add an Addresses field that returns the list of addresses associated with a user. This can be achieved using Hot Chocolate’s extension types:

UserExtensions.cs

[ExtendObjectType(typeof(User))]
public class UserExtensions
{
  public async Task<List<Address>> Addresses(...)
  {
    //...
  }
}
[ExtendObjectType(typeof(User))]
public class UserExtensions
{
  public async Task<List<Address>> Addresses(...)
  {
    //...
  }
}

Resolver and Middleware

To handle the loading of addresses efficiently and avoid the N+1 problem, we can utilize data loaders and Hot Chocolate middleware. Here’s an example implementation:

UserAddresses.cs

[UsePaging, UseProjection, UseFiltering, UseSorting]
public async Task<List<Address>> Addresses(
  [Parent] AddressFeed parent,
  [Service] IAddressFeedResolvers service,
  IResolverContext context
)
{
  var dataLoader = context.BatchDataLoader<Guid, List<Address>>(
    async (addressIds) =>
      await service.AddressDataLoader(addressIds, context)
  );
  return await dataLoader.LoadAsync(parent.AddressId);
}
[UsePaging, UseProjection, UseFiltering, UseSorting]
public async Task<List<Address>> Addresses(
  [Parent] AddressFeed parent,
  [Service] IAddressFeedResolvers service,
  IResolverContext context
)
{
  var dataLoader = context.BatchDataLoader<Guid, List<Address>>(
    async (addressIds) =>
      await service.AddressDataLoader(addressIds, context)
  );
  return await dataLoader.LoadAsync(parent.AddressId);
}

Data Loader

We create a data loader that takes a list of address IDs and retrieves the corresponding addresses in a single query. The data loader is used to load the addresses associated with a user.

The AddressDataLoader method handles the batching of address IDs and performs the database query to retrieve the addresses efficiently:

AddressDataLoader.cs

public async Task<IReadOnlyDictionary<Guid, List<Address>>> AddressDataLoader(
  IReadOnlyList<Guid> addressIds,
  IResolverContext context
){
  using var dbContext = await _dbFactory.CreateDbContextAsync();
  var address = await dbContext.Address
    .AsNoTracking()
    .Where(afi => addressIds.Contains(afi.AddressId))
    .Sort(context) //sort using resolver context
    .Filter(context) //filter using resolver context
    .Project(context) //project using resolver context
    .ToListAsync();
 
  return address
    .GroupBy(afi => afi.AddressId)
    .ToDictionary(k => k.Key, val => val.ToList());
}
public async Task<IReadOnlyDictionary<Guid, List<Address>>> AddressDataLoader(
  IReadOnlyList<Guid> addressIds,
  IResolverContext context
){
  using var dbContext = await _dbFactory.CreateDbContextAsync();
  var address = await dbContext.Address
    .AsNoTracking()
    .Where(afi => addressIds.Contains(afi.AddressId))
    .Sort(context) //sort using resolver context
    .Filter(context) //filter using resolver context
    .Project(context) //project using resolver context
    .ToListAsync();
 
  return address
    .GroupBy(afi => afi.AddressId)
    .ToDictionary(k => k.Key, val => val.ToList());
}

🤔Considerations

While nested projections with pagination in Gql Hot Chocolate provide an efficient way to retrieve and paginate data in a GraphQL API, there are a few considerations to keep in mind:

Paging Limitations

It’s important to note that paging in this context happens after the query execution. This means that the SQL query still returns all the addresses, but the GraphQL response is paginated. The advantage is that the consumer of the GraphQL query receives a smaller result set, such as 10 items, instead of potentially thousands. However, it’s worth considering the potential impact on performance if the underlying data set is exceptionally large.

Sorting Field Projection

When sorting is applied to a field, it requires that the field be projected in the query. This means that the consumer of the GraphQL API needs to explicitly select the sorted field in their query. Keep in mind that if the sorted field is not selected, the sorting will not be applied, and the results may not be in the expected order. It’s important to communicate this requirement to API consumers to ensure they receive the desired sorted results.