Skip to content

Projections with QueryContext are not projecting collection navigation properties / fields #8179

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
invalidoperation opened this issue Mar 26, 2025 · 9 comments

Comments

@invalidoperation
Copy link

Product

Hot Chocolate

Version

15.1.1

Link to minimal reproduction

https://github.com/invalidoperation/HC_QueryContextCollectionNavigationsReproduction

Steps to reproduce

Run this query in Nitro:

query {
  authorsWithQueryContext {
    name
    books {
      title
    }
  }
   authorsWithQueryable {
    name
    books {
      title
    }
  }
}

What is expected?

Both authorsWithQueryContext and authorsWithQueryable should correctly expand and project books collection

What is actually happening?

Only authorsWithQueryable correctly expands books. books Field is empty for authorsWithQueryContext:

{
  "data": {
    "authorsWithQueryContext": [
      {
        "name": "John Doe",
        "books": []
      },
      {
        "name": "Max Mustermann",
        "books": []
      }
    ],
    "authorsWithQueryable": [
      {
        "name": "John Doe",
        "books": [
          {
            "title": "Book 1"
          },
          {
            "title": "Book 2"
          }
        ]
      },
      {
        "name": "Max Mustermann",
        "books": [
          {
            "title": "Buch 1"
          },
          {
            "title": "Buch 2"
          }
        ]
      }
    ]
  }
}

Expression in QueryContext.Selector is root => new Author() {Name = root.Name}, books field is ignored.

Relevant log output

Additional context

Collection Fields are not correctly projected when using the new QueryContext with HC 15. When using IQueryable as return type with projections, this works without issues.

I could already trace the root problem to this line: https://github.com/ChilliCream/graphql-platform/blob/main/src/HotChocolate/Core/src/Execution.Projections/SelectionExpressionBuilder.cs#L199

 if (selection.Field.Type.IsListType() && !namedType.IsLeafType()
            || selection.Field.PureResolver is null
            || selection.Field.ResolverMember?.ReflectedType != selection.Field.DeclaringType.RuntimeType)
        {
            return;
        }

It seems all ListTypes, which are not LeafTypes, are early returned in SelectionExpressionBuilder.CollectSelection and not further projected.

@michaelstaib
Copy link
Member

This is by design as you should have a DataLoader in this place to take over. This way you will get more predictable peformance and also things like nested pagination etc work. There is a new YouTube episode coming next week explaining the dos/donts of projections.

@invalidoperation
Copy link
Author

@michaelstaib Thank you for your answer. I get the point of the more predictable performance and nested pagination, but when using a ORM like Entity Framework, all navigation properties will essentially be useless when using QueryContext and creating separate DataLoaders for every single navigation property may cause a somewhat unecessary overhead. Also, navigation properties are still necessary for nested filtering and sorting, while manually added Fields do not support this, at least as far as i know?

I'm currently in the process of refactoring a fairly big project with an ever bigger domain model, which up till now is using IQueryables (returned via MediatR queries in the Application Layer) in the GraphQL Layer and having DataLoaders declared also in the GraphQL Layer, to instead utilizing QueryContext and handling DataLoaders and Query Logic fully in the Application Layer. Having to create additional Resolvers for every single collection navigation property would propably tripple the workload, as there are a lot of navigation properties where we do not have additional MediatR queries created for and were up till now just relying on EF with navigation properties.

Please don't get me wrong, I really appreciate this new feature, but having some opt-in for also handling collections would be a real benefit when using an ORM and migrating from IQueryables to QueryContexts

@CronKz
Copy link
Contributor

CronKz commented Mar 26, 2025

What about adding this through source generation?

@glen-84
Copy link
Collaborator

glen-84 commented Mar 26, 2025

all navigation properties will essentially be useless when using QueryContext and creating separate DataLoaders for every single navigation property may cause a somewhat unecessary overhead

Not all navigation properties, only collections.

Joining collections leads to cartesian explosion, and how would you paginate the data (for both the root and sub collections)?

@invalidoperation
Copy link
Author

all navigation properties will essentially be useless when using QueryContext and creating separate DataLoaders for every single navigation property may cause a somewhat unecessary overhead

Not all navigation properties, only collections.

Yes, you are right, I meant collection navigation properties

Joining collections leads to cartesian explosion, and how would you paginate the data (for both the root and sub collections)?

Up until this change, we made a distinction between collections where we just knew that there won't be many relations - for those we just relied on the navigation properties - and collections where we knew they would have too many results, where we either disabled the field via configuration or created a separate resolver with pagination. We also relied on EF Core's Split Query feature to prevent cartesian explosions.

But nonetheless, I think I get the point why this isn't supported anymore. Thanks for your replies, I highly anticipate Michael's YouTube video regarding this topic 😄

@michaelstaib
Copy link
Member

If you're happy with the old way of doing projections—why change?

The new DataLoader-based data APIs are still evolving. We’re actively iterating and adding lots of new features, and I expect that over the next few dot releases, much of our focus will remain on improving GreenDonut.

And that brings us to the core point: this is a DataLoader feature. The new data APIs are designed to be more hands-on—they give you control over how things are fetched and by which keys we break things down. If you don’t care much about the SQL being generated or the performance characteristics, then the old projections with split queries might suit you just fine.

But the new data APIs are built on top of GreenDonut, not Hot Chocolate. They are fundamentally DataLoader-centric. While we started with EF Core, they’re not limited to ORMs—they’re designed to work with anything you can fetch using DataLoader.

Split queries are transparent to the user—you don’t have control over how or when things are split. This feature is really on the other end of the spectrum. Cartesian explosions are just one concern; we’re also looking at how other data features like aggregations fit into layered architectures.

Also, in Hot Chocolate, we’ve revamped the paging APIs to give you more control. With the new PageConnection<T>, you can now opt out of the “magic” and handle paging logic yourself, if that’s what your use case requires.

With these new APIs, you get full control, but that also means you need to be more explicit about what you want. That’s the whole philosophy behind them. It will take us some time to get all the features in that we envision for this space—but it’s very much a feature area where we don’t want “magic.”


Looking ahead, we’ll end up with three distinct approaches to working with data APIs—and Fusion will be the overarching layer that can unify them and enable distributed operations:

  • A database-first approach (like PostGraphile), which we’ll support via SQL Kata in v17/18.
  • The traditional projections approach, which will continue to evolve—v17 will bring enhancements like requirement integration and support for nested pagination.
  • And finally, the GreenDonut-driven approach, optimized for layered architectures where the application layer is built on top of the DataLoader pattern.

@michaelstaib
Copy link
Member

Just as a side note ... we do support collections if they are meant as scalars. We will treat them as scalars. At the moment you can do that by using requirements. Although we are working on a better API for 15.2

@michaelstaib
Copy link
Member

https://youtu.be/dYSqssul4jY

@invalidoperation
Copy link
Author

@michaelstaib Thank you very much for the explanations and the video, this was very informative! We are going to continue the refactoring of our project, I think with the newly provided information we can also resolve our edge cases regarding the collections.

I have some additional questions regarding this topic, but I'm not sure if this is the right place for this to ask - if not, I apologize.

  1. Regarding the PageConnection, I up till now used the HotChocolatePaginationResultExtensions.ToConnectionAsync() extension method (based on your Blog Post regarding HC 15), which returns just a Connection instead of a PageConnection - would you suggest to better use the new PageConnection(page) approach or are there any other disadvantages in using Connection over PageConnection?

  2. What would be the best approach to handle Projections on Mutation results? We currently are returning IQueryables, which are filtered on the mutated entity. I also tested to inject the QueryContext (and also using ISelection.AsSelector<>), but it seems this only works if the mutation conventions are disabled - else the SelectionExpressionBuilder throws an The selection set is empty. Exception - I'm not sure if this is a bug, in this case I can open an additional issue with more details, if this is by design or if using QueryContext in Mutations is an antipattern?

Thank you again for your detailed explanations!

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

4 participants