chkimes / graphql-net

Convert GraphQL to IQueryable
MIT License
891 stars 86 forks source link

Inline Fragments with type condition #54

Closed MarianPalkus closed 7 years ago

MarianPalkus commented 7 years ago

Great project, thanks for your work 👍

I am trying to implement inline fragments with type conditions analogous to the sample of the official graphql docs graphql docs.

Basically, there is an interface or base type (Character) and two concrete types (Human and Droid), the types are slightly modified for simplicity:

interface Character {
  id: ID!
  name: String!
}
type Human implements Character {
  id: ID!
  name: String!
  height: Float
}

type Droid implements Character {
  id: ID!
  name: String!
  primaryFunction: String
}

The query looks like this:

heros {
   __typename
    name
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }

A result could look like this:

"heros": [
   {
     "__typename": "Human",
      "name": "Han Solo",
      "height": 5.6430448
    },
   {
     "__typename": "Droid",
      "name": "R2-D2",
      "primaryFunction": "Astromech"
    }
]

I extended the EntityFrameworkExecutionTests as follows:

class Character
{
   public int Id { get; set; }
   public string Name { get; set; }
}

class Human : Character
{
   public double Height { get; set; }
}

class Droid : Character
{
   public string PrimaryFunction { get; set; }
}
private static void InitializeCharacterSchema(GraphQLSchema<EfContext> schema)
{
    var character = schema.AddType<Character>();
    character.AddField(c => c.Id);
    character.AddField(c => c.Name);

    var human = schema.AddType<Human>();
    human.AddField(c => c.Id);
    human.AddField(c => c.Name);
    human.AddField(h => h.Height);

    var droid = schema.AddType<Droid>();
    droid.AddField(c => c.Id);
    droid.AddField(c => c.Name);
    droid.AddField(h => h.PrimaryFunction);

    schema.AddField("hero", new {id = 0}, (db, args) => db.Heros.SingleOrDefault(h => h.Id == args.id));
    schema.AddListField("heros", db => db.Heros);
}
public IDbSet<Character> Heros { get; set; }
var human = new Human
{
    Id = 1,
    Name = "Han Solo",
    Height = 5.6430448
};
db.Heros.Add(human);
var droid = new Droid
{
    Id = 2,
    Name = "R2-D2",
    PrimaryFunction = "Astromech"
};
db.Heros.Add(droid);
[Test]
public void QueryInlineFragements()
{
    var schema = GraphQL<EfContext>.CreateDefaultSchema(() => new EfContext());
    InitializeCharacterSchema(schema);
    schema.Complete();

    var gql = new GraphQL<EfContext>(schema);
    var results = gql.ExecuteQuery("{ heros { name, __typename, ... on Human { height }, ... on Droid { primaryFunction } } }");
    Test.DeepEquals(results, "{ heros: [ { name: 'Han Solo', __typename: 'Human',  height: 5.6430448}, { name: 'R2-D2', __typename: 'Droid', primaryFunction: 'Astromech' } ] }");
}

This raises the following Exception:

GraphQL.Parser.SourceException : unknown fragment `on'' at Microsoft.FSharp.Core.Operators.Raise[T](Exception exn) at GraphQL.Parser.Utilities.failAt[a](SourceInfo pos, String msg) in C:\Projekte\graphql-net\GraphQL.Parser\Utilities.fs:line 49 at GraphQL.Parser.SchemaResolver.Resolver1.ResolveFragmentSpreadSelection(FragmentSpread pspread, SourceInfo pos) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 195 at GraphQL.Parser.SchemaResolver.Resolver1.ResolveSelection(Selection pselection, SourceInfo pos) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 221 at GraphQL.Parser.SchemaResolver.ResolveSelections@228.GenerateNext(IEnumerable1& next) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 229 at Microsoft.FSharp.Core.CompilerServices.GeneratedSequenceBase1.MoveNextImpl() at System.Collections.Generic.List1..ctor(IEnumerable1 collection) at Microsoft.FSharp.Collections.SeqModule.ToArray[T](IEnumerable1 source) at GraphQL.Parser.SchemaResolver.Resolver1.ResolveSelections(IEnumerable1 pselections) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 227 at GraphQL.Parser.SchemaResolver.Resolver1.ResolveFieldSelection(Field pfield, SourceInfo pos) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 163 at GraphQL.Parser.SchemaResolver.Resolver1.ResolveSelection(Selection pselection, SourceInfo pos) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 218 at GraphQL.Parser.SchemaResolver.ResolveSelections@228.GenerateNext(IEnumerable1& next) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 229 at Microsoft.FSharp.Core.CompilerServices.GeneratedSequenceBase1.MoveNextImpl() at System.Collections.Generic.List1..ctor(IEnumerable1 collection) at Microsoft.FSharp.Collections.SeqModule.ToArray[T](IEnumerable1 source) at GraphQL.Parser.SchemaResolver.Resolver1.ResolveSelections(IEnumerable1 pselections) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 227 at GraphQL.Parser.SchemaResolver.Resolver1.ResolveOperation(Operation poperation, SourceInfo pos) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 234 at GraphQL.Parser.SchemaResolver.ResolveOperations@277.GenerateNext(IEnumerable1& next) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 282 at Microsoft.FSharp.Core.CompilerServices.GeneratedSequenceBase1.MoveNextImpl() at System.Collections.Generic.List1..ctor(IEnumerable1 collection) at Microsoft.FSharp.Collections.SeqModule.ToArray[T](IEnumerable1 source) at GraphQL.Parser.SchemaResolver.DocumentContext1.ResolveOperations() in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 276 at GraphQL.Parser.GraphQLDocument1.Parse(ISchema1 schema, String source) in C:\Projekte\graphql-net\GraphQL.Parser\Integration\GraphQLDocument.fs:line 42 at GraphQL.Net.GraphQL`1.ExecuteQuery(String queryStr) in C:\Projekte\graphql-net\GraphQL.Net\GraphQL.cs:line 36 at Tests.EF.EntityFrameworkExecutionTests.QueryInlineFragements() in C:\Projekte\graphql-net\Tests.EF\EntityFrameworkExecutionTests.cs:line 222

  • Test Method 2
[Test]
public void QueryInlineFragements2()
{
    var schema = GraphQL<EfContext>.CreateDefaultSchema(() => new EfContext());
    InitializeCharacterSchema(schema);
    schema.Complete();

    var gql = new GraphQL<EfContext>(schema);
    var results = gql.ExecuteQuery("{ heros { name, __typename, ...human, ...droid } }, fragment human on Human { height }, fragment droid on Droid { primaryFunction }");
    Test.DeepEquals(results, "{ heros: [ { name: 'Han Solo', __typename: 'Human',  height: 5.6430448}, { name: 'R2-D2', __typename: 'Droid', primaryFunction: 'Astromech' } ] }");
}

This raises the following Exception:

GraphQL.Parser.SourceException : height'' is not a field of typeCharacter'' at Microsoft.FSharp.Core.Operators.Raise[T](Exception exn) at GraphQL.Parser.Utilities.failAt[a](SourceInfo pos, String msg) in C:\Projekte\graphql-net\GraphQL.Parser\Utilities.fs:line 49 at GraphQL.Parser.SchemaResolver.Resolver1.ResolveFieldSelection(Field pfield, SourceInfo pos) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 148 at GraphQL.Parser.SchemaResolver.Resolver1.ResolveSelection(Selection pselection, SourceInfo pos) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 218 at GraphQL.Parser.SchemaResolver.ResolveSelections@228.GenerateNext(IEnumerable1& next) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 229 at Microsoft.FSharp.Core.CompilerServices.GeneratedSequenceBase1.MoveNextImpl() at System.Collections.Generic.List1..ctor(IEnumerable1 collection) at Microsoft.FSharp.Collections.SeqModule.ToArray[T](IEnumerable1 source) at GraphQL.Parser.SchemaResolver.Resolver1.ResolveSelections(IEnumerable1 pselections) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 227 at GraphQL.Parser.SchemaResolver.Resolver1.ResolveFragment(Fragment pfrag, SourceInfo pos) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 184 at GraphQL.Parser.SchemaResolver.Resolver1.ResolveFragmentSpreadSelection(FragmentSpread pspread, SourceInfo pos) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 197 at GraphQL.Parser.SchemaResolver.Resolver1.ResolveSelection(Selection pselection, SourceInfo pos) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 221 at GraphQL.Parser.SchemaResolver.ResolveSelections@228.GenerateNext(IEnumerable1& next) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 229 at Microsoft.FSharp.Core.CompilerServices.GeneratedSequenceBase1.MoveNextImpl() at System.Collections.Generic.List1..ctor(IEnumerable1 collection) at Microsoft.FSharp.Collections.SeqModule.ToArray[T](IEnumerable1 source) at GraphQL.Parser.SchemaResolver.Resolver1.ResolveSelections(IEnumerable1 pselections) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 227 at GraphQL.Parser.SchemaResolver.Resolver1.ResolveFieldSelection(Field pfield, SourceInfo pos) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 163 at GraphQL.Parser.SchemaResolver.Resolver1.ResolveSelection(Selection pselection, SourceInfo pos) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 218 at GraphQL.Parser.SchemaResolver.ResolveSelections@228.GenerateNext(IEnumerable1& next) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 229 at Microsoft.FSharp.Core.CompilerServices.GeneratedSequenceBase1.MoveNextImpl() at System.Collections.Generic.List1..ctor(IEnumerable1 collection) at Microsoft.FSharp.Collections.SeqModule.ToArray[T](IEnumerable1 source) at GraphQL.Parser.SchemaResolver.Resolver1.ResolveSelections(IEnumerable1 pselections) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 227 at GraphQL.Parser.SchemaResolver.Resolver1.ResolveOperation(Operation poperation, SourceInfo pos) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 234 at GraphQL.Parser.SchemaResolver.ResolveOperations@277.GenerateNext(IEnumerable1& next) in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 282 at Microsoft.FSharp.Core.CompilerServices.GeneratedSequenceBase1.MoveNextImpl() at System.Collections.Generic.List1..ctor(IEnumerable1 collection) at Microsoft.FSharp.Collections.SeqModule.ToArray[T](IEnumerable1 source) at GraphQL.Parser.SchemaResolver.DocumentContext1.ResolveOperations() in C:\Projekte\graphql-net\GraphQL.Parser\Schema\SchemaResolver.fs:line 276 at GraphQL.Parser.GraphQLDocument1.Parse(ISchema1 schema, String source) in C:\Projekte\graphql-net\GraphQL.Parser\Integration\GraphQLDocument.fs:line 42 at GraphQL.Net.GraphQL1.ExecuteQuery(String queryStr) in C:\Projekte\graphql-net\GraphQL.Net\GraphQL.cs:line 36 at Tests.EF.EntityFrameworkExecutionTests.QueryInlineFragements2() in C:\Projekte\graphql-net\Tests.EF\EntityFrameworkExecutionTests.cs:line 234

I'm not really sure, whether my schema definition is wrong or i misunderstood the inline fragments/type condtions of graph-ql.

Do you have a suggestion or hint how to implement it correctly? Are type conditions already supported yet?

Thanks in advance :-)

MarianPalkus commented 7 years ago

A workaround might be something like this:

character.AddField("height", c => c is Human? (c as Human).Height : 0);
character.AddField("primaryFunction", c => c is Droid ? (c as Droid).PrimaryFunction : null);
chkimes commented 7 years ago

Hi Marian,

Thanks for the detailed issue! One of these is definitely a bug - the parser was recognizing on as a fragment instead of as a type condition. I just pushed a commit that should fix that.

However, in general this library does not yet support fragments with type conditions, inline or not. Right now it just does a dumb insertion of all the fields in the fragment into the selection set, then validates that against the selected type.

The reason we don't yet support type conditions is because they go hand-in-hand with inheritance, and inheritance is a hard problem when working with Queryable backends. For example, here are two possible ways you could write a query with inheritance:

db.Characters.Select(c => new {
    Name = c.Name,
    Height = ((Human)c).Height
})
db.Characters.Select(c => new {
    Name = c.Name,
    Height = c is Human ? (c as Human).Height : 0
})

Only one of those ways works with Entity Framework, and only one of those ways works with LINQ-To-Objects. Unfortunately, it's not the same one. This isn't an intractable issue, we already do provider sniffing elsewhere due to other inconsistencies, but it does add more effort.

The other issue is adding explicit support for inheritance in the schema. Right now all of the schema types are independent, with no relation to other types other than on their properties. With inheritance, that changes to having to do graph traversal to determine a single unifying type for a hierarchy. Again, not impossible, just a lot more work.

I've always planned to support inheritance at some point, but the difficulty vs. payoff has led to it being ranked pretty low in the list of priorities.

However, as you mentioned, it is possible to workaround by just defining all the types on the base type (e.g. Character). This is not ideal since it's not really following the spec, but I think that at least makes it usable for queries against a model that utilizes inheritance.

MarianPalkus commented 7 years ago

Ok, thanks a lot for clearing this up.