many-to-many dataloader + selection + pagination empty result
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
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"]
}
The issue is the cursor expression ... it on my list
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.
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.