Solved: GraphQL and Hot Chocolate Workshop issues with the latest versions of Hot Chocolate and .NET Top-Level Statements

I started working on the official “Getting started with GraphQL on ASP.NET Core and Hot Chocolate – Workshop” tutorial, but since the workshop is a bit outdated, it targets .NET 5 and an older version of Hot Chocolate. I am using the latest .NET 7 (at the time of this writing) and top-level statements. In this post, I will address the issues I ran into due to Hot Chocolate version changes from 11 to 13.

Hot Chocolate is a powerful .NET GraphQL server I have wanted to learn about for some time.

Issues and Other Changes

Lesson 1

In the latest .NET and top-level statements there is no Startup.cs file, so whenever they mention adding something to Startup.cs file, add it to Program.cs.

To configure the GraphQL middleware just add app.MapGraphQL();

var app = builder.Build();
app.MapGraphQL();
app.MapGet("/", () => "Hello World!");
app.Run();

Lesson 3

To support pooled DB context, besides replacing services.AddDbContext with services.AddPooledDbContextFactory as the guide states, you will need to register the ApplicationDbContext like this: services.RegisterDbContext(DbContextKind.Pooled). Also, make sure to register the context after services.AddGraphQLserver().

services
    .AddPooledDbContextFactory<ApplicationDbContext>(options => 
        options.UseSqlite("Data Source=conferences.db")
    )
    .AddGraphQLServer()
    .RegisterDbContext<ApplicationDbContext>(DbContextKind.Pooled)

Remove [ScopedService] annotation, it is obsolete. Registering the DB context as outlined above replaces this annotation.

The error message I got before doing the above:

    {
      "message": "Unexpected Execution Error",
      "locations": [
        {
          "line": 25,
          "column": 3
        }
      ],
      "path": [
        "a"
      ],
      "extensions": {
        "message": "No service for type 'ConferencePlanner.GraphQL.Data.ApplicationDbContext' has been registered.",
        "stackTrace": "<<OMITTED FOR BREVITY>>"
      }
    },

When adding the SpeakerByIdDataLoader class, make the BatchDataLoader TValueSpeaker – nullable.

public class SpeakerByIdDataLoader : BatchDataLoader<int, Speaker?>
{
    // <<omitted for brevity>>

    protected override async Task<IReadOnlyDictionary<int, Speaker?>> LoadBatchAsync
    (
        // <<omitted for brevity>>
    )
    {
        // <<omitted for brevity>>
    }
}

Not making Speaker nullable will result in an error when searching for a specific speaker id that does not exist in the database

{
  "errors": [
    {
      "message": "Cannot return null for non-nullable field.",
      "locations": [
        {
          "line": 46,
          "column": 3
        }
      ],
      "path": [
        "b"
      ],
      "extensions": {
        "code": "HC0018"
      }
    }
  ]
}

Basically making the return type in the GraphQL schema definition as not required.

type Query {
  speakers: [Speaker!]!
  speaker(id: Int!): Speaker
}

When adding the SpeakerType class, you will need to add a [Parent] annotation to the Speaker parameter of SpeakerResolvers.GetSessionsAsync method.

    private class SpeakerResolvers
    {
        public async Task<IEnumerable<Session>> GetSessionsAsync
        (
            [Parent] Speaker speaker,
            ApplicationDbContext dbContext,
            SessionByIdDataLoader sessionById,
            CancellationToken cancellationToken
        )
        {
            // <<omitted for brevity>>
        }
    }

To fix the following error:

    {
      "message": "There was no argument with the name `speaker` found on the field `sessions`.",
      "locations": [
        {
          "line": 56,
          "column": 9
        }
      ],
      "path": [
        "speakers",
        3,
        "sessions"
      ],
      "extensions": {
        "fieldName": "sessions",
        "argumentName": "speaker"
      }
    },

References:

Lesson 4

To enable Relay support, replace services.EnableRelaySupport() with services.AddGlobalObjectIdentification(). The former has been deprecated.

When creating *Input records with parameters as global identifiers ([ID]), do not write them as parameters, but rather as properties.

// =======  Instead of this  =======
public record AddSessionInput(
    string Title,
    string? Abstract,
    [ID(nameof(Speaker))]
    IReadOnlyList<int> SpeakerIds
);

// =======  Do this  =======
public record AddSessionInput
{
    public string Title { get; set; }
    public string? Abstract { get; set; }
    [ID(nameof(Speaker))] public IReadOnlyList<int> SpeakerIds { get; set; }   
}

The global identification annotations do not work as parameters, only as properties when used this way. In the first instance above, the GraphQL compiled schema will take ints as inputs, as opposed to the generated base64 global identifier.

Generated schemas

input AddSessionInput {
  title: String!
  abstract: String
  speakerIds: [int!]!   # <== Speaker ids as ints
}

input AddSessionInput {
  title: String!
  abstract: String
  speakerIds: [ID!]!  # <== Speaker ids as global identifiers (ID)
}

Records that should be changed as outlined above:

  • AddSessionInput
  • RenameTrackInput
  • ScheduleSessionInput

References:

Lesson 7

Rewrite CheckInAttendeeInput record to have properties instead of parameters.

public record CheckInAttendeeInput
{
    [ID(nameof(Session))] public int  SessionId { get; set; }
    [ID(nameof(Attendee))] public int AttendeeId { get; set; }
}

Lesson 8

Register the DB Context after adding GraphQL for each test case

.AddPooledDbContextFactory<ApplicationDbContext>(
    options => options.UseInMemoryDatabase("Data Source=conferences.db")
)
.AddGraphQL()
.RegisterDbContext<ApplicationDbContext>(DbContextKind.Pooled)
// <<OMITTED FOR BREVITY>>
.BuildSchemaAsync();

The test cases will fail with the following error message if RegisterDbContext is missing.

HotChocolate.SchemaException: For more details look at the `Errors` property.

HotChocolate.SchemaException
For more details look at the `Errors` property.

1. Unable to infer or resolve a schema type from the type reference `IDbContextTransaction (Input)`. (HotChocolate.Types.InputObjectType<Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade>)
2. Unable to infer or resolve a schema type from the type reference `IModel (Input)`.

<<STACK TRACE OMITTED FOR BREVITY>>

Final Code Repo

I have created the following GitHub repo with the final workshop code. Hopefully, you find it useful.

https://github.com/esausilva/hot-chocolate-graphql-workshop-updated

Conclusion

Well, I had fun working my way through the workshop and figuring out the issues with newer versions of Hot Chocolate and .NET 7. I just wish the maintainers would update the workshop.

If you liked this tutorial, share it on your social media and you can follow me on Twitter or LinkedIn.


Consider giving back by getting me a coffee (or a couple) by clicking the following button:

Spread the love

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.