Skip to content

Cannot use UseProjection with UseConnection and PageConnection<T> #8278

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
mathieu-radyo opened this issue May 9, 2025 · 2 comments
Closed

Comments

@mathieu-radyo
Copy link

Product

Hot Chocolate

Version

15.1.3

Link to minimal reproduction

https://github.com/mathieu-radyo/HotChocolate-IssueReproductions

Steps to reproduce

  1. Create a GraphQL query using [UseConnection], [UseFiltering], [UseSorting] and [UseProjection] on a resolver that returns PageConnection<T>.
  2. Try to query a parent entity and its related children (e.g. countries and their cities).
  3. Run the query via GraphQL Playground or Studio.

Example code:

[UseConnection]
[UseProjection]
[UseFiltering]
[UseSorting]
public static async Task<PageConnection<Country>> GetCountriesAsync(
    [Service] WorldContext context,
    PagingArguments pagingArguments,
    QueryContext<Country> query,
    CancellationToken cancellationToken = default)
{
    var page = await context.Countries.AsNoTracking()
        .With(query, c => c.IfEmpty(i => i.AddAscending(e => e.Id)))
        .ToPageAsync(pagingArguments, cancellationToken);

    return new PageConnection<Country>(page);
}

GraphQL query used:

query {
  countries(first: 5) {
    pageInfo {
      hasNextPage
      hasPreviousPage
      startCursor
      endCursor
      forwardCursors {
        page
        cursor
      }
      backwardCursors {
        page
        cursor
      }
    }
    nodes {
      id
      name
      emoji
      emojiU
      capital
      cities {
        name
        wikiDataId
      }
    }
    edges {
      cursor
    }
    totalCount
  }
}

What is expected?

I expect to be able to combine [UseConnection] with [UseProjection] to:

  • Benefit from relative cursor-based pagination (forwardCursors, backwardCursors)
  • Apply filtering, sorting, and projection
  • Fetch related child entities like cities without overfetching

What is actually happening?

  • The query fails at startup or runtime with the following error:

    System.ArgumentException: Type 'HotChocolate.Types.Pagination.PageConnection`1[...]' does not have a default constructor (Parameter 'type')
    
  • If I remove [UseProjection], the query runs — but child entities like cities are no longer included, even if defined in the GraphQL schema.

Relevant log output

Additional context

The issue is reproducible in this minimal GitHub repo:
🔗 https://github.com/mathieu-radyo/HotChocolate-IssueReproductions

Questions:

  • Is this limitation expected?
  • Will UseProjection eventually support PageConnection<T>?
  • Is there a recommended workaround to get child entities with relative cursors?

Thanks in advance for your help and your work on this great library!

@glen-84
Copy link
Collaborator

glen-84 commented May 9, 2025

The [UseProjection] attribute is for the old style of projections, where you return an IQueryable.

The [UseConnection] attribute is also not necessary.

When using the new style of projections, lists will not be projected by default (see this issue for the reasoning).

It is recommended that you define a resolver for each related collection (f.e. cities), and that these resolvers (in fact almost all object resolvers) use a DataLoader.

So in your CountryType, you'd have something like this:

public static async Task<PageConnection<City>> GetCitiesAsync(
    [Parent(requires: nameof(Country.Id))] Country country,
    WorldContext context,
    PagingArguments pagingArguments,
    QueryContext<City> query,
    CancellationToken cancellationToken = default)
{
    var page = await context.Cities.AsNoTracking()
        .Where(c => c.CountryId == country.Id)
        .With(query, c => c.IfEmpty(i => i.AddAscending(e => e.Id)))
        .ToPageAsync(pagingArguments, cancellationToken);

    return new PageConnection<City>(page);
}

... but using DataLoader instead, to avoid the N+1 problem (i.e. to send a single query for all of the cities).

PS. You can also run the query using Nitro at http://localhost:5000/graphql/. 😉

@mathieu-radyo
Copy link
Author

Thanks a lot for your detailed explanation — it makes much more sense to me now.

Initially, it didn’t feel intuitive to separate the query into one for the countries and another one per relation (like cities), especially when I wanted to fetch the full list in one go. But now I understand the reasoning and the advantage of this pattern, particularly in the context of nested pagination and avoiding overfetching.

I had tried using a DataLoader before but didn’t implement it correctly — I was retrieving all fields of the child entity instead of selecting only what was needed.

Now I’ve successfully set up a DataLoader with a selector, like so:

internal static class CityDataLoader
{
    [DataLoader]
    public static async Task<IReadOnlyDictionary<int, City[]>> CitiesByCountryIdAsync(
        IReadOnlyList<int> countryIds,
        WorldContext dbContext,
        ISelectorBuilder selector,
        CancellationToken cancellationToken)
    {
        return await dbContext.Countries
            .AsNoTracking()
            .Where(t => countryIds.Contains(t.Id))
            .Select(t => t.Id, t => t.Cities, selector)
            .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken);
    }
}

And in my CountryType:

[ObjectType<Country>]
public static partial class CountryType
{
    public static async Task<IEnumerable<City>> GetCitiesAsync(
        [Parent] Country track,
        ICitiesByCountryIdDataLoader citiesByCountryId,
        ISelection selection,
        CancellationToken cancellationToken)
    {
        return await citiesByCountryId
            .Select(selection)
            .LoadAsync(track.Id, cancellationToken) ?? [];
    }
}

Now everything works smoothly — and efficiently. Thanks again for pointing me in the right direction! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants