graphql-platform icon indicating copy to clipboard operation
graphql-platform copied to clipboard

many-to-many dataloader + selection + pagination empty result

Open PHILLIPS71 opened this issue 1 year ago • 4 comments

Product

Hot Chocolate

Version

14.0.0

Link to minimal reproduction

https://github.com/PHILLIPS71/hc-7615

Steps to reproduce

I have a many-to-many relationship between User and Location, and I’m using EF Core to manage the bridging table between them.

I’ve been trying to create a DataLoader that uses the ISelectorBuilder to project locations as a paged result when querying users, with some help from Michael in the Slack channel.

However, when I run it with the selection commented out, it returns an empty collection. But when I uncomment it, I get an error due to the dynamic type.

[DataLoader]
internal static ValueTask<Dictionary<Guid, Page<Location>>> GetLocationsByUserIdAsync(
    IReadOnlyList<Guid> keys,
    PagingArguments paging,
    ISelectorBuilder selector,
    ApplicationDbContext database,
    CancellationToken cancellation = default)
{
    return database
        .Users
        .AsNoTracking()
        .Where(x => keys.Contains(x.Id))
        .SelectMany(x => x.Locations.Select(l => new { Id = l.Id, Location = l }))
        .OrderBy(x => x.Location.Name)
        // uncommenting this will result is type errors
        // .Select(x => x.Id, x => x.Location, selector)
        .ToBatchPageAsync(x => x.Id, x => x.Location, paging, cancellation);
 }

If you execute the following query using the reproduction repository you will encounter and be able to reproduce the issue:

{
  user(take: 5) {
    id
    name
    locations(take: 5) {
      nodes {
        id
        name
      }
    }
  }
}

What is expected?

I'm looking for a way to use the new ISelectorBuilder with many-to-many relationships without having to manually introduce bridging tables, leaving that responsibility to EF Core instead.

What is actually happening?

If you're not using the ISelectorBuilder, you'll get an empty result. Trying to use the .Select function will cause the issue below due to the required dynamic type.

Relevant log output

The type arguments for method 'IQueryable<KeyValueResult<TKey,IEnumerable<TValue>>> GreenDonut.Selectors.SelectionDataLoaderExtensions.Select<T,TKey,TValue>(this IQueryable<T>, Expression<Func<T,TKey?>>, Expression<Func<T,IEnumerable<TValue>>>, ISelectorBuilder)' cannot be inferred from the usage. Try specifying the type arguments explicitly.

Additional context

No response

PHILLIPS71 avatar Oct 17 '24 06:10 PHILLIPS71

Upgrading to v15 still has the issue. I'm still not quite sure how to properly connect the new QueryContext<Location> that comes with v15. However, getting M:M relationships set up would be a solid step forward.

Here’s the v15 implementation:

[DataLoader]
internal static ValueTask<Dictionary<Guid, Page<Location>>> GetLocationsByUserAsync(
    IReadOnlyList<Guid> keys,
    PagingArguments paging,
    QueryContext<Location> query,
    ApplicationDbContext database,
    CancellationToken cancellation = default)
{
    return database
        .Users
        .AsNoTracking()
        .Where(user => keys.Contains(user.Id))
        .SelectMany(user => user.Locations.Select(location => new { UserId = user.Id, Location = location }))
        .OrderBy(user => user.Location.Name)
        .ToBatchPageAsync(x => x.UserId, x => x.Location, paging, cancellation);
}
"extensions": {
    "message": "Object of type 'Location' cannot be converted to type '<>f__AnonymousType0`2[System.Guid,Location]'.",
    "stackTrace": "   at System.RuntimeType.CheckValue(Object& value, Binder binder, CultureInfo culture, BindingFlags invokeAttr)
   at System.Reflection.MethodBaseInvoker.InvokeWithOneArg(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Delegate.DynamicInvokeImpl(Object[] args)
   at GreenDonut.Data.Cursors.CursorKey.GetValue(Object entity)
   at GreenDonut.Data.Cursors.CursorKey.TryFormat(Object entity, Span`1 buffer, Int32& written)
   at GreenDonut.Data.Cursors.CursorFormatter.Format[T](T entity, CursorKey[] keys)
   at GreenDonut.Data.PagingQueryableExtensions.<>c__DisplayClass11_0`1.<CreatePage>b__0(T item)
   at GreenDonut.Data.Page`1.CreateCursor(T item)
   at GreenDonut.Data.HotChocolatePaginationResultExtensions.CreateCursor[T](T item, Func`2 createCursor)
   at GreenDonut.Data.HotChocolatePaginationResultExtensions.CreateConnection[T](Page`1 page)
   at GreenDonut.Data.HotChocolatePaginationResultExtensions.ToConnectionAsync[T](Task`1 resultPromise)
   at Giantnodes.Service.Application.HttpApi.Types.Users.Objects.UserTypeResolvers.UserType_GetLocationsAsync_Resolver(IResolverContext context) in obj\\Debug\\net9.0\\HotChocolate.Types.Analyzers\\HotChocolateResolvers.735550c.g.cs:line 347
   at HotChocolate.Types.Helpers.FieldMiddlewareCompiler.<>c__DisplayClass9_0.<<CreateResolverMiddleware>b__0>d.MoveNext()
--- End of stack trace from previous location ---
   at HotChocolate.Types.UnwrapFieldMiddlewareHelper.<>c__DisplayClass0_1.<<CreateDataMiddleware>b__1>d.MoveNext()
--- End of stack trace from previous location ---
   at HotChocolate.Types.UnwrapFieldMiddlewareHelper.<>c__DisplayClass0_1.<<CreateDataMiddleware>b__1>d.MoveNext()
--- End of stack trace from previous location ---
   at HotChocolate.Types.Pagination.PagingMiddleware.InvokeAsync(IMiddlewareContext context)
   at HotChocolate.Execution.Processing.Tasks.ResolverTask.ExecuteResolverPipelineAsync(CancellationToken cancellationToken)
   at HotChocolate.Execution.Processing.Tasks.ResolverTask.TryExecuteAsync(CancellationToken cancellationToken)",
    "remotePath": ["node", "locations"]
}

PHILLIPS71 avatar Feb 12 '25 10:02 PHILLIPS71

The issue is the cursor expression ... it on my list

michaelstaib avatar Feb 18 '25 20:02 michaelstaib

We moved this to the next release as there are a couple of difficulties involved in solving this and in most cases you can write the query in a different way to get its solved.

michaelstaib avatar Mar 17 '25 08:03 michaelstaib

Hello there, I'm facing the same issue that @PHILLIPS71 mentioned. @michaelstaib you mentioned there are other ways to write the query for these kinds of scenarios, if you could kindly provide a simple example, would be amazing.

adamtrip avatar Oct 18 '25 16:10 adamtrip