Search code examples
c#graphqlgraphql-dotnet

GraphQL models struggling with System.Text.Json.JsonException


I created a new .NET Core project and installed the packages GraphQL, GraphiQL and GraphQL.SystemTextJson.

When running the application this is what I get

enter image description here

Besides the exception message GraphiQL wasn't able to find a schema documentation.

First of all I have two entities, users and tasks.

public sealed class User
{
    public Guid Id { get; set; }
}

public sealed class Task
{
    public Guid Id { get; set; }
}

and both of them have their representing graph type

public sealed class UserType : ObjectGraphType<User>
{
    public UserType()
    {
        Name = nameof(User);
        Field(user => user.Id).Description("The user id.");
    }
}

public sealed class TaskType : ObjectGraphType<Task>
{
    public TaskType()
    {
        Name = nameof(Task);
        Field(task => task.Id).Description("The task id.");
    }
}

I created the query holding all the "actions" the client can execute

public sealed class GraphQLQuery : ObjectGraphType
{
    private readonly List<User> _users = new List<User>();
    private readonly List<Task> _tasks = new List<Task>();

    public GraphQLQuery()
    {
        Field<ListGraphType<UserType>>(
            "users",
            "Returns a collection of users.",
            resolve: context => _users);

        Field<ListGraphType<TaskType>>(
            "tasks",
            "Returns a collection of tasks.",
            resolve: context => _tasks);
    }
}

and register that query for the schema

public sealed class GraphQLSchema : GraphQL.Types.Schema
{
    public GraphQLSchema(GraphQLQuery graphQLQuery, IServiceProvider serviceProvider) : base(serviceProvider)
    {
        Query = graphQLQuery;
    }
}

In the startup file in the ConfigureServices I added this code to register all the required services before calling services.AddControllers()

serviceCollection
        .AddSingleton<IDocumentExecuter, DocumentExecuter>()
        .AddSingleton<IDocumentWriter, DocumentWriter>()
        .AddSingleton<ISchema, GraphQLSchema>()
        .AddSingleton<GraphQLQuery>()

and in the Configure method I call app.UseGraphiQl() at first.

The corresponding GraphQL request DTO

public sealed class GraphQLRequest
{
    public string OperationName { get; set; }
    public string Query { get; set; }

    [JsonConverter(typeof(ObjectDictionaryConverter))]
    public Dictionary<string, object> Variables { get; set; }
}

Lastly I implemented the API controller

[ApiController]
[Route("[controller]")]
public sealed class GraphQLController : Controller
{
    private readonly ISchema _schema;
    private readonly IDocumentExecuter _documentExecuter;

    public GraphQLController(ISchema schema, IDocumentExecuter documentExecuter)
    {
        _schema = schema;
        _documentExecuter = documentExecuter;
    }

    public async Task<IActionResult> Post([FromBody] GraphQLRequest graphQlRequest)
    {
        ExecutionOptions executionOptions = new ExecutionOptions()
        {
            Schema = _schema,
            Query = graphQlRequest.Query,
            Inputs = graphQlRequest.Variables?.ToInputs()
        };

        ExecutionResult executionResult = await _documentExecuter.ExecuteAsync(executionOptions);

        if (executionResult.Errors != null)
            return BadRequest(executionResult);

        return Ok(executionResult);
    }
}

Does someone know what's wrong here? I can't see problems like circular dependency etc.


When running the application the graphQlRequest contains the following values

  • OperationName: IntrospectionQuery
  • Query:

.

query IntrospectionQuery {
  __schema {
    queryType { name }
    mutationType { name }
    subscriptionType { name }
    types {
      ...FullType
    }
    directives {
      name
      description
      locations
      args {
        ...InputValue
      }
    }
  }
}

fragment FullType on __Type {
  kind
  name
  description
  fields(includeDeprecated: true) {
    name
    description
    args {
      ...InputValue
    }
    type {
      ...TypeRef
    }
    isDeprecated
    deprecationReason
  }
  inputFields {
    ...InputValue
  }
  interfaces {
    ...TypeRef
  }
  enumValues(includeDeprecated: true) {
    name
    description
    isDeprecated
    deprecationReason
  }
  possibleTypes {
    ...TypeRef
  }
}

fragment InputValue on __InputValue {
  name
  description
  type { ...TypeRef }
  defaultValue
}

fragment TypeRef on __Type {
  kind
  name
  ofType {
    kind
    name
    ofType {
      kind
      name
      ofType {
        kind
        name
        ofType {
          kind
          name
          ofType {
            kind
            name
            ofType {
              kind
              name
              ofType {
                kind
                name
              }
            }
          }
        }
      }
    }
  }
}

I migrated to .NET 5 now and get this error instead

enter image description here


I added a reproduction repository

https://github.com/olaf-svenson/graphql-net-reproduction


Solution

  • Your error in .Net 5 related to unregistered graph types. If you enable all exceptions and disable "Just My Code" in debug settings you will see this error

    System.InvalidOperationException: 'Required service for type API.GraphTypes.UserType not found'
    This exception was originally thrown at this call stack:
        GraphQL.Utilities.ServiceProviderExtensions.GetRequiredService(System.IServiceProvider, System.Type) in ServiceProviderExtensions.cs
        GraphQL.Types.Schema.CreateTypesLookup.AnonymousMethod__68_1(System.Type) in Schema.cs
        GraphQL.Types.GraphTypesLookup.Create.AnonymousMethod__0(System.Type) in GraphTypesLookup.cs
        GraphQL.Types.GraphTypesLookup.AddTypeIfNotRegistered(System.Type, GraphQL.Types.TypeCollectionContext) in GraphTypesLookup.cs
        GraphQL.Types.GraphTypesLookup.HandleField(GraphQL.Types.IComplexGraphType, GraphQL.Types.FieldType, GraphQL.Types.TypeCollectionContext, bool) in GraphTypesLookup.cs
        GraphQL.Types.GraphTypesLookup.AddType(GraphQL.Types.IGraphType, GraphQL.Types.TypeCollectionContext) in GraphTypesLookup.cs
        GraphQL.Types.GraphTypesLookup.Create(System.Collections.Generic.IEnumerable<GraphQL.Types.IGraphType>, System.Collections.Generic.IEnumerable<GraphQL.Types.DirectiveGraphType>, System.Func<System.Type, GraphQL.Types.IGraphType>, GraphQL.Conversion.INameConverter, bool) in GraphTypesLookup.cs
        GraphQL.Types.Schema.CreateTypesLookup() in Schema.cs
        System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
    

    Adding UserType and TaskType to DI container solves this error.

    Now, your original problem: you should use IDocumentWriter to write the response, you can't just serialize executionResult by returning Ok(executionResult). Use this code to write response (stolen from official graphql-dotnet/examples repo):

    private async Task WriteResponseAsync(HttpContext context, ExecutionResult result)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = result.Errors?.Any() == true ? (int)HttpStatusCode.BadRequest : (int)HttpStatusCode.OK;
    
        await _documentWriter.WriteAsync(context.Response.Body, result);
    }
    

    The updated GraphQLController.cs

    [ApiController]
    [Route("[controller]")]
    public sealed class GraphQLController : Controller
    {
        private readonly ISchema _schema;
        private readonly IDocumentExecuter _documentExecuter;
        private readonly IDocumentWriter _documentWriter;
    
        public GraphQLController(ISchema schema, IDocumentExecuter documentExecuter, IDocumentWriter documentWriter)
        {
            _schema = schema;
            _documentExecuter = documentExecuter;
            _documentWriter = documentWriter;
        }
    
        public async Task Post([FromBody] GraphQLRequest graphQlRequest)
        {
            ExecutionOptions executionOptions = new ExecutionOptions()
            {
                Schema = _schema,
                Query = graphQlRequest.Query,
                Inputs = graphQlRequest.Variables?.ToInputs()
            };
    
            ExecutionResult executionResult = await _documentExecuter.ExecuteAsync(executionOptions);
    
            await WriteResponseAsync(HttpContext, executionResult);
        }
    
        private async Task WriteResponseAsync(HttpContext context, ExecutionResult result)
        {
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = result.Errors?.Any() == true ? (int)HttpStatusCode.BadRequest : (int)HttpStatusCode.OK;
        
            await _documentWriter.WriteAsync(context.Response.Body, result);
        }
    }