Graphql-dotnet: More examples of complex type models?

Created on 29 Jun 2018  路  22Comments  路  Source: graphql-dotnet/graphql-dotnet

Once I get this working, I'll make some more concrete suggestions for the documentation. I'm really struggling to plug this into my existing EFCore-based project based solely on the documentation provided.

That said, where can I get some examples of more-complex type models? Two examples:

  • I have an id field that's byte[] in the DB but is displayed and queried as a string. At the schema level it's easy to do the conversion in the resolver, but at the type level, I keep getting issues:

    • "Cannot infer a Field name from the expression..."
      Field(x => x.OwnerId.ToString()).Name("Id").Description("The ID number of the player");
      Field(x => HelperFunction(x.OwnerId)).Name("Id").Description("The ID number of the player");

    • "The GraphQL type for Field: ... could not be derived implicitly. "
      Field(x => x.OwnerId).Name("Id").Description("The ID number of the player");

    I'm setting Name("Id") assuming that means I can refer to it in the query itself as Id and not OwnerId. Do I need to use middleware or something to mediate this conversion? Or is there some undocumented syntax that will do what I want? I see .Type(IGraphType) in the code, but I can't seem to find the syntax to make it work.


  • My EFCore models have lots of foreign key connections. For example, the Owner object has a list of OwnerNames objects listing past display names. The first of these is the currently active name. I want to add an abstract field that provides this name without having to fiddle with the full name history. I can't seem to figure out where I would do this.


Thanks for your time!

question

Most helpful comment

You can configure what GraphType to use for a field a couple different ways:

Field(x => x.OwnerId, type:typeof(StringGraphType))
  .Name("Id")
  .Resolve(context => context.Source.OwnerId.ToString());

Field<StringGraphType>("Id",
  resolve: context => ((Owner)context.Source).OwnerId.ToString());

Not sure if I understand fully what you want with the OwnerNames. Something like this?

public class OwnerGraphType : ObjectGraphType<Owner>
{
  public OwnerGraphType(IOwnerStore store)
  {
    Name = "Owner";

    Field<StringGraphType>("Id",
      resolve: context => ((Owner)context.Source).OwnerId.ToString());

    Field<StringGraphType>("Name",
      resolve: context => ((Owner)context.Source).OwnerNames.First());

    Field<StringGraphType>("Name",
      resolve: context => store.FetchName( ((Owner)context.Source).OwnerId ) );
  }
}

All 22 comments

You can configure what GraphType to use for a field a couple different ways:

Field(x => x.OwnerId, type:typeof(StringGraphType))
  .Name("Id")
  .Resolve(context => context.Source.OwnerId.ToString());

Field<StringGraphType>("Id",
  resolve: context => ((Owner)context.Source).OwnerId.ToString());

Not sure if I understand fully what you want with the OwnerNames. Something like this?

public class OwnerGraphType : ObjectGraphType<Owner>
{
  public OwnerGraphType(IOwnerStore store)
  {
    Name = "Owner";

    Field<StringGraphType>("Id",
      resolve: context => ((Owner)context.Source).OwnerId.ToString());

    Field<StringGraphType>("Name",
      resolve: context => ((Owner)context.Source).OwnerNames.First());

    Field<StringGraphType>("Name",
      resolve: context => store.FetchName( ((Owner)context.Source).OwnerId ) );
  }
}

Thank you. I was able to get things working.

  • The resolve: _ => .... usage only appears in the Mutations and Interfaces sections, and even there they're not clearly explained.

  • context.Source is also only shown in the Interfaces section and is similarly unexplained.

If you're open to it, I'll eventually do a pull request on the docs, making some suggestions on how to better walk n00bs through things. Thanks again!

Yeah ... so EVERY field has a resolver. If you do not provide one then the default is essentially the following:

Func<ResolveFieldContext, object> resolver = context =>
  ReflectionHelper.GetPropertyValue(context.Source, context.FieldName);

Field<StringGraphType>("Id", resolve: resolver);

See pull request #721

Moving my questions here as requested. You said that mutators could be added as simply as queries. My initial pass (v0.17.3) at this problem was as follows:

public class APSchemaRW : Schema
{
    public APSchemaRW(MyContext db)
    {
        Query = new APQuery(db);
        Mutation = new APMutator(db);
    }
}

But I end up with the following error: Error trying to resolve createProfile.

What could be causing that?

Here's the mutator code:

public class APMutator : ObjectGraphType
{
    public APMutator(MyContext db)
    {
        Field<UserType>(
            "createProfile",
            arguments: new QueryArguments(
                new QueryArgument<NonNullGraphType<ProfileInputType>> {Name = "input"}
            ),
            resolve: _ => {
                var profile = _.GetArgument<ProfileDTO>("input");
                //Do a bunch of database stuff with the input, 
                //ending with a proper database object that gets returned.
                return owner;
            }
        );
    }
}

Some error is happening in your resolver. You can set ExposeExceptions on ExecutionOptions and check the results of the request to get more detailed information.

Now a question around meaningful error messages. Is my understanding correct that to surface meaningful error messages, that can only be done through the validation rules? There's no way I can surface useful messages in resolvers unless ExposeExceptions is on?

So the implication is that if I want to validate the inputs to a specific mutation, I create a validation rule that runs every time, looking for when op.Name equals the name of my mutator. When it does, step through the variables doing whatever validation I need done (I assume through the Inputs property or one of the helpers). Yes?

If I need access to my database context, as long as I store it in my schema class, I should be able to access it via the Schema property, yes?

Thanks again for your time. Slowly but surely!

Well, op.Name seems to be blank. I assumed it would be the name I gave the field in my mutator (in this case, createProfile). This is my input. I want to validate the variables.

{
    "query": "mutation ($profile: ProfileInput!){ createProfile(input: $profile) {id}}",
    "variables": {
        "profile": {
            "name": "Perlk枚nig",
            "anonymous": false,
            "country": "CA"
        }
    }
}

And here's my naive first attempt:

    public class VRCheckProfileInput : IValidationRule
    {
        public INodeVisitor Validate(ValidationContext context)
        {
            return new EnterLeaveListener(_ =>
            {
                _.Match<Operation>(op =>
                {
                    if (op.Name == "createProfile")
                    {
                        var vars = context.GetRecursiveVariables(op);
                        //do stuff with the vars, returning errors as necessary
                    }
                });
            });
        }
    }

Is my understanding correct that to surface meaningful error messages, that can only be done through the validation rules? There's no way I can surface useful messages in resolvers unless ExposeExceptions is on?

ExposeExceptions just displays the whole exception including its stacktrace (essentially Exception.ToString()). The top level exception message is still displayed even if ExposeExceptions is false. This is handled a bit better in 2.0. You can add errors directly from your resolver in 2.0, and/or use try/catch in your resolver to throw a GraphQL .NET specific error instead.

In your example mutation you do not provide an operation name.

mutation ($profile: ProfileInput!){ createProfile(input: $profile) {id}}

createProfile is the field. You can add an operation name like such:

mutation MyMutation($profile: ProfileInput!){ createProfile(input: $profile) {id}}

MyMutation is now the operation name.

Here is a thread that talks about validating input: https://github.com/graphql-dotnet/graphql-dotnet/issues/694#issuecomment-397411404

Thanks again for all your help. I've been making great forward progress on my project. This next question will demonstrate my limited understanding of asynchronous paradigms in .NET.

In the resolver of one of my mutations, I need to make an incidental call to an async function partway through. I flag the lambda as async, but now I get errors trying to return the actual database object at the end of the mutation.

Field<MyType>(
    "myMutation",
    arguments: new QueryArguments(
        new QueryArgument<NonNullGraphType<MyInputType>> {Name = "input"}
    ),
    resolve: async _ => {
        //do a bunch of prep work

        //now call this completely incidental async method whose return value I don't care about
        //(specifically I'm emitting a message to an AWS SNS queue)
        await IncidentalAsyncMethod().ConfigureAwait(false);

        //now finish up the database work and return the updated database object
        return val; //which is of the type mapped to the `MyType` GraphQL schema
    }
);

No surprisingly, I'm told Cannot convert async lambda expression to delegate type 'Func<ResolveFieldContext<object>, object>'. What is the right way to do this?

I've been reading the MS docs on asynchronous programming and scouring StackOverflow. I even put the logic in a standalone function with the correct Task<MyDbObjType> return type (resolve: async _ => await HandleChallenge(_, db).ConfigureAwait(false) but to no avail.

There is an overload available, FieldAsync

Thank you for such a quick reply! That one change fixed everything!

And now a "blue screen of death." Everything was working fine this morning. I added some new types, linked them to existing ones, and am now developing the queries and mutations. I finally deployed for testing and am greeted on all endpoints with the following:

{
    "errors": [
        {
            "message": "GraphQL.ExecutionError: No parameterless constructor defined for this object. ---> System.MissingMethodException: No parameterless constructor defined for this object.\n   at System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean& canBeCached, RuntimeMethodHandleInternal& ctor)\n   at System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark)\n   at System.Activator.CreateInstance(Type type, Boolean nonPublic)\n   at System.Activator.CreateInstance(Type type)\n   at GraphQL.Types.Schema.<CreateTypesLookup>b__56_2(Type type)\n   at GraphQL.Types.GraphTypesLookup.AddTypeIfNotRegistered(Type type, TypeCollectionContext context)\n   at GraphQL.Types.GraphTypesLookup.HandleField(Type parentType, FieldType field, TypeCollectionContext context)\n   at GraphQL.EnumerableExtensions.Apply[T](IEnumerable`1 items, Action`1 action)\n   at GraphQL.Types.GraphTypesLookup.AddType(IGraphType type, TypeCollectionContext context)\n   at GraphQL.Types.GraphTypesLookup.HandleField(Type parentType, FieldType field, TypeCollectionContext context)\n   at GraphQL.EnumerableExtensions.Apply[T](IEnumerable`1 items, Action`1 action)\n   at GraphQL.Types.GraphTypesLookup.AddType(IGraphType type, TypeCollectionContext context)\n   at GraphQL.EnumerableExtensions.Apply[T](IEnumerable`1 items, Action`1 action)\n   at GraphQL.Types.GraphTypesLookup.Create(IEnumerable`1 types, IEnumerable`1 directives, Func`2 resolveType, IFieldNameConverter fieldNameConverter)\n   at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)\n   at System.Lazy`1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)\n   at System.Lazy`1.CreateValue()\n   at GraphQL.Types.Schema.get_AllTypes()\n   at GraphQL.Instrumentation.FieldMiddlewareBuilder.ApplyTo(ISchema schema)\n   at GraphQL.DocumentExecuter.<ExecuteAsync>d__8.MoveNext()\n   --- End of inner exception stack trace ---",
            "code": "MISSING_METHOD"
        }
    ]
}

I see there are past issues with this error message, and it appears to be related to dependency injection, but I haven't knowingly done anything related to DI. I just created new types for existing database objects and expanded existing types to link to them. My frustration is that there's no line number or function name, or even a GraphQL type name. How does one go about tracking this problem down?

Dependencies include the following. I'm using lazy loading.

    <PackageReference Include="EntityFrameworkCore.Scaffolding.Handlebars" Version="1.5.1" />
    <PackageReference Include="graphql" Version="2.0.0-alpha-938" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.1.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="2.1.1" />
    <PackageReference Include="pomelo.entityframeworkcore.mysql" Version="2.1.1" />

After painstaking commenting and uncommenting of new code, I finally narrowed the problem down to a couple new types. I don't see what's wrong yet. What I'm doing in these types I've done in others. But I'll hammer at it after a good night's rest.

If you create a .NET Type without a parameterless constructor, you have to provide a way for this library to construct that object. The GraphType's are created on initialization and cached. This library uses an abstraction for type resolution. In 0.x that is a simple func, in 2.0 it is IDependencyResolver.

https://github.com/graphql-dotnet/graphql-dotnet/blob/4e74411859c27c3e44c8b3751abfcb51f2f1adcf/src/GraphQL/Types/Schema.cs#L59-L73

The default implementation uses Activator.CreateInstance to construct the types. Activator.CreateInstance requires a parameterless constructor. That is the error you're getting.

https://github.com/graphql-dotnet/graphql-dotnet/blob/4e74411859c27c3e44c8b3751abfcb51f2f1adcf/src/GraphQL/IDependencyResolver.cs#L44-L47

So if you want to construct types without a parameterless constructor, you need to provide an implementation of the IDependencyResolver interface that knows how to construct your types.

The library provides a FuncDependencyResolver to make integration with DI containers easier.
The examples how how to do it in.NET Core.

services.AddSingleton<IDependencyResolver>(s =>
  new FuncDependencyResolver(s.GetRequiredService));

https://github.com/graphql-dotnet/examples/blob/cbb177b606b1750a112ab4574d6c288e77f5342f/src/AspNetCoreCustom/Example/Startup.cs#L18

Then you need to make sure all of your GraphTypes and their dependencies are registered with your container, so they can be resolved through your container.

Thank you for your patient reply. What you stated I gleaned from my searching, but none of my types have parametered constructors. Only the queries and schemas do, and they've worked fine up to now.

I've traced the problem to one new type in particular (GamesDataType), but I can't figure out why. So here's one type that pulls in the offending one:

public class GamesMetaType : ObjectGraphType<GamesMeta>
{
    public GamesMetaType()
    {
        //Fields that work fine are omitted

        //This one triggers the problem. If I comment it out, the query runs fine.
        Field<ListGraphType<GamesDataType>>(
            "games",
            description: "All current instances of this game",
            resolve: _ => ((GamesMeta)_.Source).GamesData.ToArray()
        );
    }
}

And the error happens even if I empty out the GamesDataType object. I was hoping I could trace it to a specific field, but nope:

public class GamesDataType : ObjectGraphType<GamesData> 
{
    GamesDataType()
    {
    }
}
{
    "errors": [
        {
            "message": "GraphQL.ExecutionError: No parameterless constructor defined for this object. ---> System.MissingMethodException: No parameterless constructor defined for this object.\n   at System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean& canBeCached, RuntimeMethodHandleInternal& ctor)\n   at System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark)\n   at System.Activator.CreateInstance(Type type, Boolean nonPublic)\n   at System.Activator.CreateInstance(Type type)\n   at GraphQL.Types.Schema.<CreateTypesLookup>b__56_2(Type type)\n   at GraphQL.Types.GraphTypesLookup.AddTypeIfNotRegistered(Type type, TypeCollectionContext context)\n   at GraphQL.Types.GraphTypesLookup.HandleField(Type parentType, FieldType field, TypeCollectionContext context)\n   at GraphQL.EnumerableExtensions.Apply[T](IEnumerable`1 items, Action`1 action)\n   at GraphQL.Types.GraphTypesLookup.AddType(IGraphType type, TypeCollectionContext context)\n   at GraphQL.Types.GraphTypesLookup.HandleField(Type parentType, FieldType field, TypeCollectionContext context)\n   at GraphQL.EnumerableExtensions.Apply[T](IEnumerable`1 items, Action`1 action)\n   at GraphQL.Types.GraphTypesLookup.AddType(IGraphType type, TypeCollectionContext context)\n   at GraphQL.Types.GraphTypesLookup.HandleField(Type parentType, FieldType field, TypeCollectionContext context)\n   at GraphQL.EnumerableExtensions.Apply[T](IEnumerable`1 items, Action`1 action)\n   at GraphQL.Types.GraphTypesLookup.AddType(IGraphType type, TypeCollectionContext context)\n   at GraphQL.Types.GraphTypesLookup.HandleField(Type parentType, FieldType field, TypeCollectionContext context)\n   at GraphQL.EnumerableExtensions.Apply[T](IEnumerable`1 items, Action`1 action)\n   at GraphQL.Types.GraphTypesLookup.AddType(IGraphType type, TypeCollectionContext context)\n   at GraphQL.EnumerableExtensions.Apply[T](IEnumerable`1 items, Action`1 action)\n   at GraphQL.Types.GraphTypesLookup.Create(IEnumerable`1 types, IEnumerable`1 directives, Func`2 resolveType, IFieldNameConverter fieldNameConverter)\n   at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)\n   at System.Lazy`1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)\n   at System.Lazy`1.CreateValue()\n   at GraphQL.Types.Schema.get_AllTypes()\n   at GraphQL.Instrumentation.FieldMiddlewareBuilder.ApplyTo(ISchema schema)\n   at GraphQL.DocumentExecuter.<ExecuteAsync>d__8.MoveNext()\n   --- End of inner exception stack trace ---",
            "code": "MISSING_METHOD"
        }
    ]
}

I'm baffled. Here are the DB objects created by EFCore scaffolding (database first):

GamesMeta:

using System;
using System.Collections.Generic;

namespace abstractplay.DB
{
    public partial class GamesMeta
    {
        public GamesMeta()
        {
            Challenges = new HashSet<Challenges>();
            GamesData = new HashSet<GamesData>();
            GamesMetaStatus = new HashSet<GamesMetaStatus>();
            GamesMetaTags = new HashSet<GamesMetaTags>();
            GamesMetaVariants = new HashSet<GamesMetaVariants>();
        }

        public byte[] GameId { get; set; }
        public string Shortcode { get; set; }
        public string Name { get; set; }
        public DateTime? LiveDate { get; set; }
        public string Description { get; set; }
        public string Url { get; set; }
        public bool IsLive { get; set; }
        public byte[] PublisherId { get; set; }
        public string PlayerCounts { get; set; }
        public string Version { get; set; }
        public string State { get; set; }
        public string Changelog { get; set; }

        public virtual GamesMetaPublishers Publisher { get; set; }
        public virtual ICollection<Challenges> Challenges { get; set; }
        public virtual ICollection<GamesData> GamesData { get; set; }
        public virtual ICollection<GamesMetaStatus> GamesMetaStatus { get; set; }
        public virtual ICollection<GamesMetaTags> GamesMetaTags { get; set; }
        public virtual ICollection<GamesMetaVariants> GamesMetaVariants { get; set; }
    }
}

GamesData DB object:

using System;
using System.Collections.Generic;

namespace abstractplay.DB
{
    public partial class GamesData
    {
        public GamesData()
        {
            GamesDataChats = new HashSet<GamesDataChats>();
            GamesDataClocks = new HashSet<GamesDataClocks>();
            GamesDataPlayers = new HashSet<GamesDataPlayers>();
            GamesDataStates = new HashSet<GamesDataStates>();
            GamesDataWhoseturn = new HashSet<GamesDataWhoseturn>();
        }

        public byte[] EntryId { get; set; }
        public byte[] GameMetaId { get; set; }
        public bool Closed { get; set; }
        public bool Alert { get; set; }
        public string Variants { get; set; }
        public ushort ClockStart { get; set; }
        public ushort ClockInc { get; set; }
        public ushort ClockMax { get; set; }
        public bool ClockFrozen { get; set; }

        public virtual GamesMeta GameMeta { get; set; }
        public virtual ICollection<GamesDataChats> GamesDataChats { get; set; }
        public virtual ICollection<GamesDataClocks> GamesDataClocks { get; set; }
        public virtual ICollection<GamesDataPlayers> GamesDataPlayers { get; set; }
        public virtual ICollection<GamesDataStates> GamesDataStates { get; set; }
        public virtual ICollection<GamesDataWhoseturn> GamesDataWhoseturn { get; set; }
    }
}

I thought maybe it was because the type never appeared in the root query. So I added it there and removed it in the type I listed above. So the following is a field in the root query:

Field<ListGraphType<GamesDataType>>(
    "games",
    description: "All games in progress",
    resolve: _ => db.GamesData.ToArray()
);

Same error, but with a shorter stack trace. db is the database context passed in through the schema. So the query has a parametered constructor, but none of the types do.

public class GamesDataType : ObjectGraphType<GamesData> 
{
    GamesDataType()
    {
    }
}

This has a private parameterless constructor, but not a public one. Activator.CreateInstance also requires it to be public.

public class GamesDataType : ObjectGraphType<GamesData> 
{
    public GamesDataType()
    {
    }
}

sighs Well that's one of the more embarrassing public coding errors I've made. That fixed the problem.

Is there any way I can buy you a beer or something?

The library itself, and your willingness to help, has made it possible for me to make more progress on my hobby project than I thought possible this month. Many thanks.

Np, it took me a few passes to see it myself! 馃槅 You're not the first to get tripped up by this, so perhaps I can catch this specific error when using the Default dependency resolver and provide a more helpful one.

@Perlkonig Can we consider the problems resolved and close this issue?

Sure. I've been sidetracked. Once I'm able to get back to my project, I'll submit more concrete PRs.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

sergeyshaykhullin picture sergeyshaykhullin  路  39Comments

Grauenwolf picture Grauenwolf  路  26Comments

Shane32 picture Shane32  路  52Comments

justusburger picture justusburger  路  22Comments

pekkah picture pekkah  路  52Comments