ChilliCream / graphql-platform

Welcome to the home of the Hot Chocolate GraphQL server for .NET, the Strawberry Shake GraphQL client for .NET and Banana Cake Pop the awesome Monaco based GraphQL IDE.
https://chillicream.com
MIT License
5.16k stars 736 forks source link

Inconsistent behavior for array nullability creates invalid schema when using collection resolvers #2565

Closed zaneclaes closed 2 years ago

zaneclaes commented 3 years ago

Describe the bug HotChocolate (appears) to treat ICollection<T> differently for models and for resolvers.

To Reproduce Using EFCore5, create a ParentModel and ChildModel with a N:M (many-to-many) relationship:

class ParentModel {
  public ICollection<ChildModel> children { get; set; } = new List<ChildModel>();
}

(and vice versa).

Note that the schema is of type [ChildModel!]!. This is correct, in that neither the array nor the child models may be null, per the lack of nullability in the source code.

However, now try to add a resolver to the parent model. E.g.,

descriptor.Field(p => p.children).ResolveWith(r => new List<ChildModel>());

In this case, the generated schema is [ChildModel]. This subsequently causes downstream errors with clients which depend on correct schema, such as graphql-codegen, requiring a ton of nullability boilerplate to fix.

Expected behavior Both code paths should respect nullability in the same fashion.

Desktop (please complete the following information):

zaneclaes commented 3 years ago

I was able to fix the schema by explicitly adding .Type<NonNullType<ListType<NonNullType<ChildModelObj>>>(), fwiw.

gojanpaolo commented 3 years ago

Note that we also need to explicitly handle null in the resolver function, at least when using DataLoader. A Cannot return null for non-nullable field error will be thrown if null is not handled explicitly,

e.g.

descriptor
    .Field(p => p.Children)
    .ResolveWith<MyResolver>(r => r.GetChildrenAsync(default!, default!, default))
    .Type<NonNullType<ListType<NonNullType<ChildModelObj>>>>();

// MyResolver
public async Task<IEnumerable<ChildModel>> GetChildrenAsync(
    Parent parent,
    ChildByParentIdDataLoader childByParentId,
    CancellationToken cancellationToken) =>
    await childByParentId.LoadAsync(parent.Id, cancellationToken)
    ?? Enumerable.Empty<ChildModel>(); // notice explicit null handling
michaelstaib commented 3 years ago

Completely overlooked this one.

michaelstaib commented 3 years ago

So this are different issues.... the first issue

ResolveWith(r => new List) is actually not meant be used with an expression .... ResolveWith is meant to refer to member expressions.

-> .ResolveWith(r => r.GetChildrenAsync(default!, default!, default))

the above is correct.

Resolve is for delegates.

michaelstaib commented 3 years ago

@gojanpaolo also this is correct. If you return null this is per spec invalid. That you want an empty list in this case is implementation detail to your specific case.

gojanpaolo commented 3 years ago

@michaelstaib I think the issue on my example is that the DataLoader.LoadAsync returns Task<TValue> where TValue is not marked as nullable. I believe it should be marked nullable to follow C#'s nullable reference types.

Here is what my DataLoader implementation looks like

public sealed class ChildrenByParentIdDataLoader : BatchDataLoader<Guid, IGrouping<int, ChildModel>>
{
    protected async override Task<IReadOnlyDictionary<int, IGrouping<int, ChildModel>>> LoadBatchAsync(IReadOnlyList<int> keys, CancellationToken cancellationToken)
    {
        ...
    }
}

... and the ChildrenByParentIdDataLoader.LoadAsync says it returns non-nullable Task<IGrouping<int, ChildModel>>

Odonno commented 3 years ago

Facing the same problem here. Adding the .Type() was my only idea, like @zaneclaes you did.

It's really annoying that it does not infer the NonNull or Null type from the Field() function...

stale[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.