graphql-dotnet: Error trying to resolve field

I’m derp!

The issue is that EntityFramework is too smart and too forgiving, but GraphQL is not forgiving. đź’Ş

var applicationString = builder.Configuration.GetConnectionString("database") ?? throw new InvalidOperationException("'database' not found.");

builder.Services.AddDbContext<AppDbContext>(options => { options.UseSqlServer(applicationString); }, ServiceLifetime.Singleton);

Check the edit history kthxbai

Kind Regards,

jee-mj

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 19 (12 by maintainers)

Most upvoted comments

As for the data loader, the instructions you are following are out of date. The call to AddDataLoader here:

https://github.com/jee-mj/net6-graphql-ef-api/blob/main/Program.cs#L43

Already performs the work of these lines:

https://github.com/jee-mj/net6-graphql-ef-api/blob/main/Program.cs#L31-L32

It also adds the document listener to the execution. For reference, see the source of the AddDataLoader method:

https://github.com/graphql-dotnet/graphql-dotnet/blob/master/src/GraphQL.DataLoader/Extensions/DataLoaderGraphQLBuilderExtensions.cs#L16-L20

And the source of the AddDocumentListener method:

https://github.com/graphql-dotnet/graphql-dotnet/blob/master/src/GraphQL/Extensions/GraphQLBuilderExtensions.cs#L640-L646

I didn’t mention it before because the extra registration should have no effect.

Just FYI, you can also remove builder.Services.AddSingleton<ISchema, ApiSchema>(); because the schema is registered within the call to .AddSchema<ApiSchema>(). And if you want to remove the calls to register ApiQuery and RegistrationType, just add a call to .AddGraphTypes() within your Program.cs, which will scan the calling assembly for graph types and register them with your DI provider.

Just FYI, Entity Framework Core 7.0 relies on SqlClient 5.0.1, which has a catastrophic memory leak if you make repeated calls within a BackgroundService. See https://github.com/dotnet/SqlClient/issues/1810

I am working to re-produce the same setup in something that can be used as reference for the future by others.

One of my wish-list goals is to write a “real-world” sample GraphQL project that uses a Entity Framework back-end and utilizes data loaders and so on, including a React SPA demonstration application with JWT authentication and subscription support. So many of the samples available now can’t really be used for production applications, and/or don’t explain how to implement “real-world” tasks. You can build GraphQL.NET applications either by “code-first”, “schema-first”, or “type-first”, and having the same program implemented each way would allow people to compare writing styles to see what works best for them, and/or compare logic across writing styles.

Just FYI, my own GraphQL project that uses Entity Framework currently is a .NET 6 project, running with GraphQL.NET 7, and EF Core 3.1. The database project is currently also referenced by code that executes on .NET 4.8, so I am prevented from upgrading to EF 5.x or newer. Another of my projects uses the linq2db database layer rather than EF Core.

Transient DbContexts can provide a benefit where if you also have IApiRepository a transient, and then reconfigure the field resolvers as previously described (so IApiRepository is pulled from the DI scope of the HTTP context), then you can use parallel execution as each field resolver will receive its own instance of IApiRepository and the DbContext. This doesn’t work for my code because I have many services that operate on the DbContext, and it is essential that each DbContext is the same reference so that SaveChanges works properly.

A transient lifecycle for a DbContext can help in certain situations. In your case, it won’t, at least not yet. The thing is that since IApiRepository is a singleton, the DbContext will also be effectively a singleton. If you register IApiRepository as a transient, then it too will have a singleton effective lifetime because it is created with the graph type that references it.

Now if you’re thinking that you can just use a transient (or scoped) schema, that’s true, and in fact GraphQL.NET supports scoped schemas. But it takes a long time to construct a schema, and so it is not recommended for performance reasons.

If you did want to use a scoped schema, remove all your manual registrations as described above, adding the call to .AddGraphTypes(). Then change the .AddSchema<ApiSchema>() call to specify that it should be created as scoped.

There is a danger to transient DbContexts in that transient services are not disposed until the container that holds them is disposed. This is not an issue if the DbContext is created within a scope, but if it is repeatedly created from the root (e.g. from a singleton), then you will have a memory leak. See link here:

https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-guidelines#disposable-transient-services-captured-by-container

For any other configurations to the execution options, just call .ConfigureExecutionOptions(opts => { ... });

I’d check to be sure that matching versions of Microsoft.EntityFrameworkCore nuget packages are installed. Perhaps you have an older version of Microsoft.EntityFrameworkCore.SqlServer installed than the Microsoft.EntityFrameworkCore package.

I see you have this code in your sample:

builder.Services.AddDbContext<AppDbContext>(options => { options.UseSqlServer("YourConnectionString"); }, ServiceLifetime.Singleton);

This isn’t going to work long-term because Entity Framework does not support concurrent requests, and GraphQL.NET Server is going to execute each request it receives as soon as it arrives, which may be concurrent with another request. You do have queries configured to execute serially, so at least it should work for testing.

Long term you’ll want to have your DbContext and IApiRepository as scoped services, and change your field resolver like so:

    public class ApiQuery : ObjectGraphType
    {
        public ApiQuery(IDataLoaderContextAccessor accessor)
        {
            var loader = accessor.Context;
            Name = "ApiQuery";

            Field<RestaurantType, Restaurant>("restaurant")
                .Argument<NonNullGraphType<StringGraphType>>("id", "the restaurant identifier")
                .Resolve()
                .WithService<IApiRepository>()
                .ResolveAsync(async (context, repository) => await repository.GetRestaurantById(context.GetArgument<int>("id")).ConfigureAwait(false));
                
            Field<ListGraphType<RestaurantType>, IDataLoaderResult<List<Restaurant>>>("restaurants")
                .Resolve()
                .WithService<IApiRepository>()
                .ResolveAsync(async (context, repository) =>
                {
                    return loader?.GetOrAddLoader("GetAllRestaurants", repository.GetAllRestaurants)
                        .LoadAsync();
                });
        }
    }

I would also like to confirm that IApiRepository and the AppDbContext is uses are registered as a singletons in your program? (Note: Entity Framework contexts are scoped services.)

I think perhaps you’re misunderstanding what a data loader’s function is. Typically a data loader is used to group calls to the database for a set of ids. For instance, if you had a list of products, and each Product had a ManufacturerId, rather than pulling each manufacturer from the database individually, you can use a data loader to retrieve all the manufacturers at once. However, the root query does not typically need a data loader - it is already a single call that is not expected to be called again in the query.

Take the below sample:

{
  restaurants {   # typically the resolver would not have a data loader here
    name
    address {     # this would be a good use of a data loader, assuming the addresses were stored in a separate table
      city
    }
    menuItem {    # this is another type of use of a data loader
      name
      price
    }
  }
}

In the sample above, Address uses the GetOrAddBatchLoader method, as it is retrieving a single item per child id, and MenuItem uses the GetOrAddCollectionBatchLoader method, as it retrieves a list of items per parent id.

You can read more about data loaders here:

Regardless, I don’t immediately see anything wrong with the code sample you provided. Although perhaps not very useful, there is no reason why a SimpleDataLoader cannot be created to load the list of restaurants. I suggest turning on exception stack traces within responses to better diagnose the issue. If you can post the stack trace, I can take another look.

services.AddGraphQL(b => b
    // etc
    .AddErrorInfoProvider(options => options.ExposeExceptionDetails = true);
);

line: 33, column: 3

It points to coordinates into input GraphQL query string.

Thanks for the repro 👍 but we need an example query to pass it into your GraphQL server.