ChilliCream / graphql-workshop

Getting started with GraphQL on ASP.NET Core and Hot Chocolate - Workshop
465 stars 199 forks source link

I've just ran into the problem at the end of part 4 #39

Closed Sebosek closed 3 years ago

Sebosek commented 3 years ago

Hi,

I've just run into the problem at the end of part 4. I'm getting the following error:

query GetSpeaker {
  speaker(id: 1) {
    name
  }
}

>>

{
  "errors": [
    {
      "message": "The ID `1` has an invalid format."
    }
  ]
}

My version of app is available here https://github.com/Sebosek/ConferencePlanner, probably I missing something.

The SpeakerQueries contains ID attribute with type int,

namespace ConferencePlanner.GraphQl.Speakers
{
    [ExtendObjectType(Name = Consts.QUERY)]
    public class SpeakerQueries
    {
        [UseApplicationDbContext]
        public Task<List<Speaker>> GetSpeakersAsync([ScopedService] ApplicationDbContext context) =>
            context.Speakers.ToListAsync();

        public Task<Speaker> GetSpeakerAsync(
            [ID(nameof(Speaker))] int id, 
            SpeakerByIdDataLoader dataLoader,
            CancellationToken cancellationToken) => dataLoader.LoadAsync(id, cancellationToken);

        public async Task<IReadOnlyCollection<Speaker>> GetSpeakersAsync(
            [ID(nameof(Speaker))] int[] ids, 
            SpeakerByIdDataLoader dataLoader,
            CancellationToken cancellationToken) => 
            await dataLoader.LoadAsync(ids, cancellationToken).ConfigureAwait(false);
    }
}

The SpeakerType contains ImplementationNode,

namespace ConferencePlanner.GraphQl.Types
{
    public class SpeakerType : ObjectType<Speaker>
    {
        protected override void Configure(IObjectTypeDescriptor<Speaker> descriptor)
        {
            descriptor
                .ImplementsNode()
                .IdField(p => p.Id)
                .ResolveNode(WithDataLoader);

            descriptor
                .Field(f => f.SessionSpeakers)
                .ResolveWith<SpeakerResolvers>(t => t.GetSessionsAsync(default!, default!, default!, default))
                .UseDbContext<ApplicationDbContext>()
                .Name("sessions");
        }

        private static Task<Speaker> WithDataLoader(IResolverContext context, int id) =>
            context.DataLoader<SpeakerByIdDataLoader>().LoadAsync(id, context.RequestAborted);

        private class SpeakerResolvers
        {
            public async Task<IReadOnlyCollection<Session>> GetSessionsAsync(
                Speaker speaker,
                [ScopedService] ApplicationDbContext dbContext,
                SessionByIdDataLoader sessionById,
                CancellationToken cancellationToken)
            {
                var ids = await dbContext.Speakers
                    .Where(w => w.Id == speaker.Id)
                    .Include(i => i.SessionSpeakers)
                    .SelectMany(s => s.SessionSpeakers.Select(e => e.SessionId))
                    .ToListAsync(cancellationToken)
                    .ConfigureAwait(false);

                return await sessionById.LoadAsync(ids, cancellationToken);
            }
        }
    }
}

And in ConfigureServices Relay is enabled

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton(CreateAutomapper());
    services.AddPooledDbContextFactory<ApplicationDbContext>(options => 
        options.UseSqlite(CONNECTION_STRING).UseLoggerFactory(ApplicationDbContext.DbContextLoggerFactory));
    services
        .AddGraphQLServer()
        .AddQueryType(d => d.Name(Consts.QUERY))
            .AddTypeExtension<SpeakerQueries>()
            .AddTypeExtension<SessionQueries>()
            .AddTypeExtension<TrackQueries>()
        .AddMutationType(d => d.Name(Consts.MUTATION))
            .AddTypeExtension<SpeakerMutations>()
            .AddTypeExtension<SessionMutations>()
            .AddTypeExtension<TrackMutations>()
        .AddType<AttendeeType>()
        .AddType<SessionType>()
        .AddType<SpeakerType>()
        .AddType<TrackType>()
        .EnableRelaySupport()
        .AddDataLoader<SpeakerByIdDataLoader>()
        .AddDataLoader<SessionByIdDataLoader>();
}

Any idea, what should I check?

Sebosek commented 3 years ago

It turns out, once I went through step no. 3 in Enable Relay support, you're no longer using integers as ID. The description of scalar type ID doesn't help, I would say, it makes more confusion than helping, but yeah... Just stop using numbers as ID, there is apparently some magic behind Hot Chocolate.

The ID scalar type represents a unique identifier, often used to refetch an object or as key for a cache. 
The ID type appears in a JSON response as a String; however, it is not intended to be human-readable.
When expected as an input type, any string (such as "4") or integer (such as 4) input value will be accepted as an ID.
davorinjurkovic007 commented 2 years ago

Hi @Sebosek Can you please more explain this problem and give some code how should look like that part.

thanks

bpolojan commented 1 year ago

At Step 3 when you create a speaker the Id is not encrypted. And that's why the query GetSpeakerById works only for step 3. At step 4 encryption is added - so you need to create the speakers again with mutation.

mutation AddSpeaker{ addSpeaker(input: { name:"Speaker Name" bio:"Speaker Bio" webSite:"https://speaker.com" }) { speaker{ id, name } } }

Then you will receive a result with an encrypted Id: { "data": { "addSpeaker": { "speaker": { "id": "U3BlYWtlcgppMTE=", "name": "Speaker Name" } } } }

In the end you need to query using the encrypted Id :

query GetSpeakerById { speaker1: speaker(id: "U3BlYWtlcgppMTE=") { name id } } and you will get the expected result:

{ "data": { "speaker1": { "name": "Speaker Name", "id": "U3BlYWtlcgppMTE=" } } }