From a9eaba3822175c4c870a351f61de6833c94ef504 Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Wed, 7 Jun 2017 20:08:26 +0200 Subject: [PATCH 01/33] Fix introspection result for enums. #47 --- GraphQL.Net/GraphQLSchema.cs | 36 ++++++++++++++------ GraphQL.Net/GraphQLType.cs | 3 +- GraphQL.Net/InteropHelpers.cs | 11 +++++- GraphQL.Net/SchemaAdapters/Schema.cs | 2 +- GraphQL.Net/SchemaAdapters/SchemaField.cs | 4 +-- GraphQL.Net/SchemaAdapters/SchemaRootType.cs | 3 ++ GraphQL.Net/SchemaAdapters/SchemaType.cs | 7 ++++ GraphQL.Parser.Test/SchemaTest.fs | 2 ++ GraphQL.Parser/Integration/CS.fs | 6 ++-- GraphQL.Parser/Schema/SchemaAST.fs | 1 + GraphQL.Parser/SchemaTools/Introspection.fs | 7 +++- GraphQL.Parser/SchemaTools/TypeHandlers.fs | 3 +- 12 files changed, 65 insertions(+), 20 deletions(-) diff --git a/GraphQL.Net/GraphQLSchema.cs b/GraphQL.Net/GraphQLSchema.cs index fd58454..7764211 100644 --- a/GraphQL.Net/GraphQLSchema.cs +++ b/GraphQL.Net/GraphQLSchema.cs @@ -1,10 +1,12 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using GraphQL.Net.SchemaAdapters; using GraphQL.Parser; +using Microsoft.FSharp.Core; namespace GraphQL.Net { @@ -34,7 +36,7 @@ public GraphQLSchema(Func contextCreator) : this() ContextCreator = contextCreator; } - public void AddEnum(string name = null, string prefix = null) where TEnum : struct // wish we could do where TEnum : Enum + public void AddEnum(string name = null, string prefix = null) where TEnum : struct // wish we could do where TEnum : Enum => VariableTypes.AddType(_ => TypeHandler.Enum(name ?? typeof(TEnum).Name, prefix ?? "")); public void AddScalar(TRepr shape, Func validate, Func translate, string name = null) @@ -66,7 +68,9 @@ public GraphQLTypeBuilder AddType(string name = null if (_types.Any(t => t.CLRType == type)) throw new ArgumentException("Type has already been added"); - var gqlType = new GraphQLType(type) { IsScalar = type.IsPrimitive, Description = description ?? "" }; + var typeKind = type.IsEnum ? TypeKind.ENUM : (type.IsPrimitive ? TypeKind.SCALAR : TypeKind.OBJECT); + + var gqlType = new GraphQLType(type) { TypeKind = typeKind, Description = description ?? "" }; if (!string.IsNullOrEmpty(name)) gqlType.Name = name; _types.Add(gqlType); @@ -176,12 +180,12 @@ private static void CompleteTypes(IEnumerable types) private static void CompleteType(GraphQLType type) { // validation maybe perform somewhere else - if (type.IsScalar && type.OwnFields.Count != 0) + if (type.TypeKind == TypeKind.SCALAR && type.OwnFields.Count != 0) throw new Exception("Scalar types must not have any fields defined."); // TODO: Schema validation exception? - if (!type.IsScalar && type.OwnFields.Count == 0) + if (type.TypeKind != TypeKind.SCALAR && type.OwnFields.Count == 0) throw new Exception("Non-scalar types must have at least one field defined."); // TODO: Schema validation exception? - if (type.IsScalar) + if (type.TypeKind == TypeKind.SCALAR) { type.QueryType = type.CLRType; return; @@ -205,7 +209,7 @@ private static void CompleteType(GraphQLType type) var fields = fieldGroupedByName.Select(g => g.First()); - var fieldDict = fields.Where(f => !f.IsPost).ToDictionary(f => f.Name, f => f.Type.IsScalar ? TypeHelpers.MakeNullable(f.Type.CLRType) : typeof(object)); + var fieldDict = fields.Where(f => !f.IsPost).ToDictionary(f => f.Name, f => f.Type.TypeKind == TypeKind.SCALAR ? TypeHelpers.MakeNullable(f.Type.CLRType) : typeof(object)); type.QueryType = DynamicTypeBuilder.CreateDynamicType(type.Name + Guid.NewGuid(), fieldDict); } @@ -217,18 +221,28 @@ private void AddDefaultTypes() ischema.AddListField("types", s => s.Types); ischema.AddField("queryType", s => s.QueryType); ischema.AddField("mutationType", s => s.MutationType.OrDefault()); + ischema.AddField("subscriptionType", s => s.MutationType.OrDefault()); ischema.AddListField("directives", s => s.Directives); var itype = AddType("__Type"); - itype.AddField("kind", t => t.Kind); + itype.AddField("kind", t => t.Kind.ToString()); itype.AddField("name", t => t.Name.OrDefault()); itype.AddField("description", t => t.Description.OrDefault()); - // TODO: support includeDeprecated filter argument - itype.AddListField("fields", t => t.Fields.OrDefault()); + itype.AddListField( + "fields", + new { includeDeprecated = false }, + (c, args, t) => + t.Fields.Map(fields => fields.Where(f => !f.IsDeprecated || args.includeDeprecated)).OrDefault()); itype.AddListField("inputFields", t => t.InputFields.OrDefault()); itype.AddField("ofType", s => s.OfType.OrDefault()); itype.AddListField("interfaces", s => s.Interfaces.OrDefault()); itype.AddListField("possibleTypes", s => s.PossibleTypes.OrDefault()); + itype.AddListField( + "enumValues", + new { includeDeprecated = false }, + (c, args, t) => t.EnumValues.Map( + enumValues => enumValues.Where(f => !f.IsDeprecated || args.includeDeprecated) + ).OrDefault()); var ifield = AddType("__Field"); @@ -268,7 +282,7 @@ private void AddDefaultTypes() private void AddTypeNameFields() { var method = GetType().GetMethod("AddTypeNameField", BindingFlags.Instance | BindingFlags.NonPublic); - foreach (var type in _types.Where(t => !t.IsScalar)) + foreach (var type in _types.Where(t => t.TypeKind != TypeKind.SCALAR)) { var genMethod = method.MakeGenericMethod(type.CLRType); genMethod.Invoke(this, new object[] { type }); @@ -370,7 +384,7 @@ internal GraphQLFieldBuilder AddUnmodifiedMutationInternal _types.FirstOrDefault(t => t.CLRType == type) - ?? new GraphQLType(type) { IsScalar = true }; + ?? new GraphQLType(type) { TypeKind = TypeKind.SCALAR }; internal IEnumerable Types => _types; } diff --git a/GraphQL.Net/GraphQLType.cs b/GraphQL.Net/GraphQLType.cs index 506d07f..f12732c 100644 --- a/GraphQL.Net/GraphQLType.cs +++ b/GraphQL.Net/GraphQLType.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using GraphQL.Parser; namespace GraphQL.Net { @@ -21,7 +22,7 @@ public GraphQLType(Type type) public List IncludedTypes { get; set; } public Type CLRType { get; set; } public Type QueryType { get; set; } - public bool IsScalar { get; set; } // TODO: TypeKind? + public TypeKind TypeKind { get; set; } // Returns own fields and the fields of all included types. public IEnumerable GetQueryFields() diff --git a/GraphQL.Net/InteropHelpers.cs b/GraphQL.Net/InteropHelpers.cs index d03135b..cd33bc1 100644 --- a/GraphQL.Net/InteropHelpers.cs +++ b/GraphQL.Net/InteropHelpers.cs @@ -1,4 +1,5 @@ -using Microsoft.FSharp.Core; +using System; +using Microsoft.FSharp.Core; namespace GraphQL.Net { @@ -6,5 +7,13 @@ public static class InteropHelpers { public static T OrDefault(this FSharpOption option) => option == null ? default(T) : option.Value; + + public static FSharpOption Map(this FSharpOption opt, Func f) + { + return OptionModule.Map( + FSharpFunc.FromConverter(a => f(a)), + opt + ); + } } } diff --git a/GraphQL.Net/SchemaAdapters/Schema.cs b/GraphQL.Net/SchemaAdapters/Schema.cs index 2726748..47211d5 100644 --- a/GraphQL.Net/SchemaAdapters/Schema.cs +++ b/GraphQL.Net/SchemaAdapters/Schema.cs @@ -34,7 +34,7 @@ public Schema(GraphQLSchema schema) : base(schema) RootType = new SchemaRootType(this, schema.GetGQLType(typeof(TContext))); _schema = schema; _queryTypes = schema.Types - .Where(t => !t.IsScalar) + .Where(t => t.TypeKind != TypeKind.SCALAR) .Select(OfType) .ToDictionary(t => t.TypeName, t => t as ISchemaQueryType); } diff --git a/GraphQL.Net/SchemaAdapters/SchemaField.cs b/GraphQL.Net/SchemaAdapters/SchemaField.cs index 56a4158..646e282 100644 --- a/GraphQL.Net/SchemaAdapters/SchemaField.cs +++ b/GraphQL.Net/SchemaAdapters/SchemaField.cs @@ -16,7 +16,7 @@ public SchemaField(ISchemaQueryType declaringType, GraphQLField field, Sch DeclaringType = declaringType; _field = field; _schema = schema; - if (_field.Type.IsScalar) + if (_field.Type.TypeKind == TypeKind.SCALAR) { var varType = _schema.GraphQLSchema.VariableTypes.VariableTypeOf(_field.Type.CLRType); FieldType = SchemaFieldType.NewValueField(varType); @@ -37,7 +37,7 @@ public SchemaField(ISchemaQueryType declaringType, GraphQLField field, Sch public override IReadOnlyDictionary> Arguments { get; } public override Complexity EstimateComplexity(IEnumerable> args) { - if (_field.Type.IsScalar) return Complexity.Zero; // scalars are practically free to select + if (_field.Type.TypeKind == TypeKind.SCALAR) return Complexity.Zero; // scalars are practically free to select if (!_field.IsList) return Complexity.One; return _field.Complexity ?? (args.Any(a => a.ArgumentName.Equals("id", StringComparison.OrdinalIgnoreCase)) ? Complexity.One diff --git a/GraphQL.Net/SchemaAdapters/SchemaRootType.cs b/GraphQL.Net/SchemaAdapters/SchemaRootType.cs index ca2ebcb..258421f 100644 --- a/GraphQL.Net/SchemaAdapters/SchemaRootType.cs +++ b/GraphQL.Net/SchemaAdapters/SchemaRootType.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using GraphQL.Parser; using GraphQL.Parser.CS; @@ -16,5 +17,7 @@ public SchemaRootType(Schema schema, GraphQLType baseQueryType) public override IReadOnlyDictionary> Fields { get; } public override string TypeName => "SchemaRoot"; + + public override IEnumerable> PossibleTypes => new Collection>(); } } diff --git a/GraphQL.Net/SchemaAdapters/SchemaType.cs b/GraphQL.Net/SchemaAdapters/SchemaType.cs index cfa76e5..e1c25f9 100644 --- a/GraphQL.Net/SchemaAdapters/SchemaType.cs +++ b/GraphQL.Net/SchemaAdapters/SchemaType.cs @@ -10,6 +10,7 @@ class SchemaType : SchemaQueryTypeCS { private readonly GraphQLType _type; private readonly Lazy>> _fields; + private readonly Lazy>> _possibleTypes; internal SchemaType(GraphQLType type, Schema schema) { @@ -25,11 +26,17 @@ internal SchemaType(GraphQLType type, Schema schema) return dict; } )); + + _possibleTypes = new Lazy>>( + () => type.IncludedTypes.Select(schema.OfType) + // Add possible types recursively + .SelectMany(t => new List> { t }.Concat(t.PossibleTypes))); } public override IReadOnlyDictionary> Fields => _fields.Value; public override string TypeName => _type.Name; public override string Description => _type.Description; public override Info Info => new Info(_type); + public override IEnumerable> PossibleTypes => _possibleTypes.Value; } } diff --git a/GraphQL.Parser.Test/SchemaTest.fs b/GraphQL.Parser.Test/SchemaTest.fs index 22f7b36..208095e 100644 --- a/GraphQL.Parser.Test/SchemaTest.fs +++ b/GraphQL.Parser.Test/SchemaTest.fs @@ -77,6 +77,7 @@ type UserType() = "id", new IdArgument() :> _ |]) |] |> dictionary :> _ + member this.PossibleTypes = Seq.empty type RootType() = member private this.Field(name, fieldType : SchemaFieldType, args) = @@ -105,6 +106,7 @@ type RootType() = "id", new IdArgument() :> _ |]) |] |> dictionary :> _ + member this.PossibleTypes = Seq.empty type FakeSchema() = let root = new RootType() :> ISchemaQueryType<_> diff --git a/GraphQL.Parser/Integration/CS.fs b/GraphQL.Parser/Integration/CS.fs index e708e51..cf14884 100644 --- a/GraphQL.Parser/Integration/CS.fs +++ b/GraphQL.Parser/Integration/CS.fs @@ -52,13 +52,15 @@ type SchemaQueryTypeCS<'s>() = abstract member Description : string default this.Description = null abstract member Info : 's - default this.Info = Unchecked.defaultof<'s> + default this.Info = Unchecked.defaultof<'s> abstract member Fields : IReadOnlyDictionary> + abstract member PossibleTypes : IEnumerable> interface ISchemaQueryType<'s> with member this.TypeName = this.TypeName member this.Description = this.Description |> obj2option - member this.Info = this.Info + member this.Info = this.Info member this.Fields = this.Fields + member this.PossibleTypes = this.PossibleTypes [] type SchemaFieldCS<'s>() = diff --git a/GraphQL.Parser/Schema/SchemaAST.fs b/GraphQL.Parser/Schema/SchemaAST.fs index dc19bf5..a7093db 100644 --- a/GraphQL.Parser/Schema/SchemaAST.fs +++ b/GraphQL.Parser/Schema/SchemaAST.fs @@ -119,6 +119,7 @@ type ISchemaQueryType<'s> = /// Get the fields of this type, keyed by name. /// May be empty, for example if the type is a primitive. abstract member Fields : IReadOnlyDictionary> + abstract member PossibleTypes : IEnumerable> /// Represents a named core type, e.g. a "Time" type represented by an ISO-formatted string. /// The type may define validation rules that run on values after they have been checked to /// match the given core type. diff --git a/GraphQL.Parser/SchemaTools/Introspection.fs b/GraphQL.Parser/SchemaTools/Introspection.fs index 345c635..f32167a 100644 --- a/GraphQL.Parser/SchemaTools/Introspection.fs +++ b/GraphQL.Parser/SchemaTools/Introspection.fs @@ -105,11 +105,14 @@ type IntroType = else IntroType.Of(varType.Type) static member Of(queryType : ISchemaQueryType<'s>) = let fields = queryType.Fields.Values |> Seq.map IntroField.Of + let possibleTypes = queryType.PossibleTypes |> Seq.map IntroType.Of + let typeKind = if queryType.PossibleTypes |> Seq.isEmpty then TypeKind.OBJECT else TypeKind.INTERFACE { IntroType.Default with - Kind = TypeKind.OBJECT + Kind = typeKind Name = Some queryType.TypeName Description = queryType.Description Fields = fields |> Some + PossibleTypes = possibleTypes |> Some Interfaces = Some Seq.empty } static member Of(fieldType : SchemaFieldType<'s>) = @@ -200,6 +203,7 @@ type IntroSchema = Types : IntroType seq QueryType : IntroType MutationType : IntroType option + SubscriptionType : IntroType option Directives : IntroDirective seq } static member Of(schema : ISchema<'s>) = @@ -211,6 +215,7 @@ type IntroSchema = ] |> Seq.concat QueryType = IntroType.Of(schema.RootType) MutationType = None // TODO: support mutation schema + SubscriptionType = None // TODO: support subscription schema Directives = schema.Directives.Values |> Seq.map IntroDirective.Of } diff --git a/GraphQL.Parser/SchemaTools/TypeHandlers.fs b/GraphQL.Parser/SchemaTools/TypeHandlers.fs index c6c0315..dc868dd 100644 --- a/GraphQL.Parser/SchemaTools/TypeHandlers.fs +++ b/GraphQL.Parser/SchemaTools/TypeHandlers.fs @@ -472,6 +472,7 @@ type RootTypeHandler(metaHandler : IMetaTypeHandler) as this = typesByName.Add(name, definedType) match definedType.BottomType with | EnumType enumType -> + typesByName.Add(enumType.EnumName, CoreVariableType.EnumType enumType) for { ValueName = valueName } as typeValue in enumType.Values.Values do match enumValuesByName.TryFind(valueName) with | None -> @@ -479,7 +480,7 @@ type RootTypeHandler(metaHandler : IMetaTypeHandler) as this = { Type = enumType Value = typeValue - } + } | Some existing -> invalid <| sprintf "The enum value name ``%s'' is defined more than once, in enum types ``%s'' and ``%s''" From b69f557c3fb5407a50a4d4b8412236d69120181e Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Wed, 5 Jul 2017 11:59:22 +0200 Subject: [PATCH 02/33] [WIP] rewrite implementation of interfaces and union types. #47 --- GraphQL.Net/DynamicTypeBuilder.cs | 67 +++- GraphQL.Net/Executor.cs | 209 ++++++------ GraphQL.Net/GraphQL.Net.csproj | 1 + GraphQL.Net/GraphQLSchema.cs | 126 +++----- GraphQL.Net/GraphQLType.cs | 23 +- GraphQL.Net/GraphQLTypeBuilder.cs | 24 +- GraphQL.Net/IGraphQLType.cs | 12 + GraphQL.Net/SchemaAdapters/SchemaRootType.cs | 3 +- GraphQL.Net/SchemaAdapters/SchemaType.cs | 14 +- GraphQL.Parser.Test/SchemaTest.fs | 2 + GraphQL.Parser/Integration/CS.fs | 2 + GraphQL.Parser/Schema/SchemaAST.fs | 1 + GraphQL.Parser/SchemaTools/Introspection.fs | 3 +- Tests/GenericTests.cs | 8 +- Tests/IntrospectionTests.cs | 318 ++++++++++++++++++- Tests/MemContext.cs | 51 ++- 16 files changed, 620 insertions(+), 244 deletions(-) create mode 100644 GraphQL.Net/IGraphQLType.cs diff --git a/GraphQL.Net/DynamicTypeBuilder.cs b/GraphQL.Net/DynamicTypeBuilder.cs index 9e1c423..d0948dd 100644 --- a/GraphQL.Net/DynamicTypeBuilder.cs +++ b/GraphQL.Net/DynamicTypeBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Reflection.Emit; @@ -17,45 +18,77 @@ static DynamicTypeBuilder() ModuleBuilder = assemblyBuilder.DefineDynamicModule(AssemblyName + ".dll"); } - public static Type CreateDynamicType(string name, Dictionary properties) + public static Type CreateDynamicType(string name, Dictionary properties, IEnumerable implementedInterfaces) { var typeBuilder = ModuleBuilder.DefineType(AssemblyName + "." + name, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.Serializable | TypeAttributes.BeforeFieldInit); + + foreach (var implementedInterface in implementedInterfaces) + { + typeBuilder.AddInterfaceImplementation(implementedInterface); + } + foreach (var prop in properties) CreateProperty(typeBuilder, prop.Key, prop.Value); return typeBuilder.CreateType(); } + public static Type CreateDynamicInterface(string name, Dictionary properties) + { + var typeBuilder = ModuleBuilder.DefineType(AssemblyName + "." + name, + TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract); + + foreach (var prop in properties) + CreateProperty(typeBuilder, prop.Key, prop.Value, true); + return typeBuilder.CreateType(); + } - private static void CreateProperty(TypeBuilder typeBuilder, string name, Type type) + private static void CreateProperty(TypeBuilder typeBuilder, string name, Type type, bool isAbstract = false) { - var fieldBuilder = typeBuilder.DefineField("_" + name.ToLower(), type, FieldAttributes.Private); + FieldBuilder fieldBuilder = null; + if (!isAbstract) { + fieldBuilder = typeBuilder.DefineField("_" + name.ToLower(), type, FieldAttributes.Private); + } var propertyBuilder = typeBuilder.DefineProperty(name, PropertyAttributes.HasDefault, type, null); - - propertyBuilder.SetGetMethod(CreateGetMethod(typeBuilder, fieldBuilder, name, type)); - propertyBuilder.SetSetMethod(CreateSetMethod(typeBuilder, fieldBuilder, name, type)); + propertyBuilder.SetGetMethod(CreateGetMethod(typeBuilder, fieldBuilder, name, type, isAbstract)); + propertyBuilder.SetSetMethod(CreateSetMethod(typeBuilder, fieldBuilder, name, type, isAbstract)); } - const MethodAttributes MethodAttrs = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig; - private static MethodBuilder CreateGetMethod(TypeBuilder typeBuilder, FieldInfo fieldBuilder, string name, Type type) + const MethodAttributes MethodAttrs = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.Virtual | MethodAttributes.HideBySig; + + private static MethodBuilder CreateGetMethod(TypeBuilder typeBuilder, FieldInfo fieldBuilder, string name, + Type type, bool isAbstract) { + if (isAbstract) + { + return typeBuilder.DefineMethod("get_" + name, MethodAttributes.Virtual | MethodAttributes.Abstract | MethodAttributes.Public, type, Type.EmptyTypes); + } + var methodBuilder = typeBuilder.DefineMethod("get_" + name, MethodAttrs, type, Type.EmptyTypes); var generator = methodBuilder.GetILGenerator(); generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Ldfld, fieldBuilder); generator.Emit(OpCodes.Ret); - + return methodBuilder; } - private static MethodBuilder CreateSetMethod(TypeBuilder typeBuilder, FieldInfo fieldBuilder, string name, Type type) + private static MethodBuilder CreateSetMethod(TypeBuilder typeBuilder, FieldInfo fieldBuilder, string name, Type type, + bool isAbstract) { - var methodBuilder = typeBuilder.DefineMethod("set" + name, MethodAttrs, null, new[] { type }); - var generator = methodBuilder.GetILGenerator(); - generator.Emit(OpCodes.Ldarg_0); - generator.Emit(OpCodes.Ldarg_1); - generator.Emit(OpCodes.Stfld, fieldBuilder); - generator.Emit(OpCodes.Ret); - + var attrs = MethodAttrs; + if (isAbstract) + { + attrs = attrs | MethodAttributes.Abstract | MethodAttributes.Virtual; + } + var methodBuilder = typeBuilder.DefineMethod("set" + name, attrs, null, new[] { type }); + if (!isAbstract) + { + var generator = methodBuilder.GetILGenerator(); + generator.Emit(OpCodes.Ldarg_0); + generator.Emit(OpCodes.Ldarg_1); + generator.Emit(OpCodes.Stfld, fieldBuilder); + generator.Emit(OpCodes.Ret); + } return methodBuilder; } } diff --git a/GraphQL.Net/Executor.cs b/GraphQL.Net/Executor.cs index efbe284..8a7f4f4 100644 --- a/GraphQL.Net/Executor.cs +++ b/GraphQL.Net/Executor.cs @@ -105,61 +105,44 @@ private static IDictionary MapResults(object queryObject, IEnume return null; var dict = new Dictionary(); var type = queryObject.GetType(); + + // TODO: Improve performance/efficiency + var graphQlQueryType = schema.Types.First(t => t.QueryType == queryObject.GetType()); + + var queryTypeToSelections = CreateQueryTypeToSelectionsMapping(graphQlQueryType, selections.ToList()); foreach (var map in selections) { var key = map.Name; var field = map.SchemaField.Field(); - var obj = field.IsPost - ? field.PostFieldFunc() - : type.GetProperty(field.Name).GetGetMethod().Invoke(queryObject, new object[] { }); + object obj = null; + if (field.IsPost) + { + obj = field.PostFieldFunc(); + } + else if (type.GetProperty(field.Name) != null && queryTypeToSelections.ContainsKey(type)) + { + var typeSelections = queryTypeToSelections[type]; + if (typeSelections.Any(s => s.Name == field.Name)) + { + obj = type.GetProperty(field.Name).GetGetMethod().Invoke(queryObject, new object[] {}); + } + else + { + continue; + } + } + else + { + continue; + } - // Filter fields for selections with type conditions - the '__typename'-property has to be present. - if (map.TypeCondition != null) + if (key == "__typename") { - // The selection has a type condition, i.e. the result has a type with included types. - // The result contains all fields of all included types and has to be filtered. - // Sample: - // - // Types: - // - Character [name: string] - // - Human [height: float] extends Character - // - Stormtrooper [specialization: string] extends Human - // - Droid [orimaryFunction: string] extends Character - // - // Sample query: - // query { heros { name, ... on Human { height }, ... on Stormtrooper { specialization }, ... on Droid { primaryFunction } } } - // - // The ExecSelection for 'height', 'specialization' and 'primaryFunction' have a type condition with the following type names: - // - height: Human - // - specialization: Stormtrooper - // - primaryFunction: Droid - // - // To filter the result properly, we have to consider the following cases: - // - Human: - // - Include: 'name', 'height' - // - Exclude: 'specialization', 'primaryFunction' - // => (1) Filter results of selections with a type-condition-name != result.__typename - // - // - Stormtrooper - // - Include: 'name', 'height', and 'specialization' - // - Exclude: 'primaryFunction' - // => Same as Human (1), but: - // => (2) include results of selections with the same type-condition-name of any ancestor-type. - // - var selectionConditionTypeName = map.TypeCondition.Value?.TypeName; - var typenameProp = type.GetRuntimeProperty(TypenameFieldSelector); - var resultTypeName = (string)typenameProp?.GetValue(queryObject); - - // (1) type-condition-name != result.__typename - if (selectionConditionTypeName != resultTypeName) + if (graphQlQueryType != null) { - // (2) Check ancestor types - var resultGraphQlType = schema.Types.FirstOrDefault(t => t.Name == resultTypeName); - if (resultGraphQlType != null && !IsEqualToAnyAncestorType(resultGraphQlType, selectionConditionTypeName)) - { - continue; - } + var specificTypenameField = graphQlQueryType.Fields.FirstOrDefault(f => f.Name == "__typename"); + obj = specificTypenameField != null ? specificTypenameField.PostFieldFunc() : obj; } } @@ -202,12 +185,7 @@ private static IDictionary MapResults(object queryObject, IEnume } return dict; } - - private static bool IsEqualToAnyAncestorType(GraphQLType type, string referenceTypeName) - { - return type != null && (type.Name == referenceTypeName || IsEqualToAnyAncestorType(type?.BaseType, referenceTypeName)); - } - + private static LambdaExpression GetSelector(GraphQLSchema schema, GraphQLType gqlType, IEnumerable> selections, ExpressionOptions options) { var parameter = Expression.Parameter(gqlType.CLRType, "p"); @@ -215,59 +193,79 @@ private static LambdaExpression GetSelector(GraphQLSchema schema, Grap return Expression.Lambda(init, parameter); } - private static ConditionalExpression GetMemberInit(GraphQLSchema schema, Type queryType, IEnumerable> selectionsEnumerable, Expression baseBindingExpr, ExpressionOptions options) + + private static IDictionary>> CreateQueryTypeToSelectionsMapping( + GraphQLType queryGraphQlType, IList> selections) + { + var abstractFieldSelection = selections + .Where(m => !m.SchemaField.Field().IsPost) + .Where(s => s.TypeCondition == null); + var typeConditionSelection = selections + .Where(m => !m.SchemaField.Field().IsPost) + .Where(s => s.TypeCondition != null) + .GroupBy( + s => s.TypeCondition?.Value?.Type().QueryType ?? queryGraphQlType.QueryType + ) + .ToDictionary(s => s.Key, s => s.AsEnumerable()); + + return new List {queryGraphQlType}.Concat(queryGraphQlType.PossibleTypes) + .ToDictionary( + t => t.QueryType, + t => + abstractFieldSelection + .Concat( + // Add type condition selection bindings + typeConditionSelection.ContainsKey(t.QueryType) + ? typeConditionSelection[t.QueryType] + : new List>()) + .Concat( + t.Interfaces.SelectMany(i => typeConditionSelection.ContainsKey(i.QueryType) + ? typeConditionSelection[i.QueryType] + : new List>())) + ); + } + + private static ConditionalExpression GetMemberInit(GraphQLSchema schema, Type queryType, IEnumerable> selectionsEnumerable, Expression parameterExpression, ExpressionOptions options) { // Avoid possible multiple enumeration of selections-enumerable var selections = selectionsEnumerable as IList> ?? selectionsEnumerable.ToList(); + var queryGraphQlType = schema.GetGQLType(parameterExpression.Type); - // The '__typename'-field selection has to be added for queries with type conditions - var typeConditionButNoTypeNameSelection = selections.Any() && - selections.Any(s => s.TypeCondition != null); - - // Any '__typename' selection have to be replaced by the '__typename' selection of the target type' '__typename'-field. - var typeNameConditionHasToBeReplaced = selections.Any(s => s.Name == TypenameFieldSelector); - - // Remove all '__typename'-selections as well as duplicates in the selections caused by fragments type condition selections. - selections = selections - .Where(s => s.Name != TypenameFieldSelector) - .GroupBy(s => s.Name) - .Select(g => g.First()) - .ToList(); - - var bindings = selections - .Where(m => !m.SchemaField.Field().IsPost) - .Select(map => GetBinding(schema, map, queryType, baseBindingExpr, options)) - .ToList(); - - // Add selection for '__typename'-field of the proper type - if (typeConditionButNoTypeNameSelection || typeNameConditionHasToBeReplaced) + // Iff there are are any type conditions, the query type must be abstract, so the query type cannot be instantiated. + if (queryGraphQlType.TypeKind == TypeKind.INTERFACE || queryGraphQlType.TypeKind == TypeKind.UNION) { - // Find the base types' `__typename` field - var graphQlType = schema.GetGQLType(baseBindingExpr.Type); - while (graphQlType?.BaseType != null) - { - graphQlType = graphQlType.BaseType; - } - var typeNameField = graphQlType?.OwnFields.Find(f => f.Name == TypenameFieldSelector); - if (typeNameField != null && !typeNameField.IsPost) + var queryTypeToBindings = CreateQueryTypeToSelectionsMapping(queryGraphQlType, selections) + .ToDictionary( + p => p.Key, + p => p.Value.Select(s => GetBinding(schema, s, p.Key, parameterExpression, options)) + ); + + // Generate a nested if-else-expression with possible types. + // Add `null` as fallback value + var firstType = queryGraphQlType.PossibleTypes.First(); + Expression baseElseExpr = Expression.MemberInit(Expression.New(firstType.QueryType), queryTypeToBindings[firstType.QueryType]); + Expression elseExpr = Expression.TypeAs(baseElseExpr, queryType); + + // Add type checks for all possible types + foreach (var possibleGraphQLType in queryGraphQlType.PossibleTypes) { - var typeNameExecSelection = new ExecSelection( - new SchemaField( - schema.Adapter.QueryTypes[graphQlType?.Name], - typeNameField, - schema.Adapter), - new FSharpOption(typeNameField?.Name), - null, - new WithSource>[] { }, - new WithSource>[] { }, - new WithSource>[] { } - ); - bindings = bindings.Concat(new[] { GetBinding(schema, typeNameExecSelection, queryType, baseBindingExpr, options) }).ToList(); + // Map the query type bindings to the related condition type bindings. + var bindings = queryTypeToBindings[possibleGraphQLType.QueryType]; + var testExpr = Expression.TypeIs(parameterExpression, possibleGraphQLType.CLRType); + var expr = Expression.MemberInit(Expression.New(possibleGraphQLType.QueryType), bindings); + var exprWithCast = Expression.TypeAs(expr, queryType); + elseExpr = Expression.Condition(testExpr, exprWithCast, elseExpr); } - } - var memberInit = Expression.MemberInit(Expression.New(queryType), bindings); - return NullPropagate(baseBindingExpr, memberInit); + //var memberInit = Expression.Lambda>(elseExpr, param); + return NullPropagate(parameterExpression, elseExpr); + } + else + { + var bindings = selections.Where(s => !s.SchemaField.Info.Field.IsPost).Select(s => GetBinding(schema, s, queryType, parameterExpression, options)); + var memberInit = Expression.MemberInit(Expression.New(queryType), bindings); + return NullPropagate(parameterExpression, memberInit); + } } private static ConditionalExpression NullPropagate(Expression baseExpr, Expression returnExpr) @@ -279,8 +277,19 @@ private static ConditionalExpression NullPropagate(Expression baseExpr, Expressi private static MemberBinding GetBinding(GraphQLSchema schema, ExecSelection map, Type toType, Expression baseBindingExpr, ExpressionOptions options) { var field = map.SchemaField.Field(); - var needsTypeCheck = baseBindingExpr.Type != field.DefiningType.CLRType; - var toMember = toType.GetProperty(map.SchemaField.FieldName); + var needsTypeCheck = baseBindingExpr.Type != map.SchemaField.DeclaringType?.Info?.Type?.CLRType; + + toType = map.TypeCondition?.Value != null ? map.TypeCondition.Value.Type().QueryType : toType; + + var toMember = toType.GetProperty( + map.SchemaField.FieldName, + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + + if (toMember == null) + { + var graphqlType = schema.GetGQLType(toType); + throw new Exception($"The field '{map.SchemaField.FieldName}' does not exist on type '{graphqlType.Name}'."); + } // expr is form of: (context, entity) => entity.Field var expr = field.GetExpression(map.Arguments.Values()); diff --git a/GraphQL.Net/GraphQL.Net.csproj b/GraphQL.Net/GraphQL.Net.csproj index 06a4f1e..9b90f64 100644 --- a/GraphQL.Net/GraphQL.Net.csproj +++ b/GraphQL.Net/GraphQL.Net.csproj @@ -58,6 +58,7 @@ + diff --git a/GraphQL.Net/GraphQLSchema.cs b/GraphQL.Net/GraphQLSchema.cs index 7764211..6cb00a3 100644 --- a/GraphQL.Net/GraphQLSchema.cs +++ b/GraphQL.Net/GraphQLSchema.cs @@ -1,12 +1,10 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using GraphQL.Net.SchemaAdapters; using GraphQL.Parser; -using Microsoft.FSharp.Core; namespace GraphQL.Net { @@ -78,6 +76,21 @@ public GraphQLTypeBuilder AddType(string name = null return new GraphQLTypeBuilder(this, gqlType); } + public GraphQLTypeBuilder AddInterfaceType(string name = null, string description = null) + { + var type = typeof(TInterface); + if (_types.Any(t => t.CLRType == type)) + throw new ArgumentException("Type has already been added"); + + var gqlType = new GraphQLType(type) + { + TypeKind = TypeKind.INTERFACE, + Description = description ?? "" + }; + _types.Add(gqlType); + return new GraphQLTypeBuilder(this, gqlType); + } + public GraphQLTypeBuilder GetType() { var type = _types.FirstOrDefault(t => t.CLRType == typeof(TEntity)); @@ -123,54 +136,15 @@ public void Complete() VariableTypes.Complete(); // The order is important: - // (1) Build type groups. - // (2) Add the '__typename' field to every type. - // (3) Complete the types (generate the query-type). - BuildTypeGroups(_types); + // (1) Add the '__typename' field to every type. + // (2) Complete the types (generate the query-type). AddTypeNameFields(); CompleteTypes(_types); Adapter = new Schema(this); Completed = true; } - - private static void BuildTypeGroups(List types) - { - //TODO: support type unions - // Build inheritance hierarchy - foreach (var graphQLType in types) - { - RelateTypeWithAncestorTypes(graphQLType, types); - RelateTypeWithImplementedInterfaces(graphQLType, types); - } - } - - // Relates the specified graphQlType to the first ancestor graphql type by adding it to the ancestor's IncludedTypes-property. - private static void RelateTypeWithAncestorTypes(GraphQLType graphQLType, List types) - { - // Walk up the type hierarchy and try to find an ancestor type for which there is a related GraphQlType. - var ancestorClrType = graphQLType.CLRType; - GraphQLType ancestorGraphQlType = null; - - while (ancestorClrType != null && ancestorGraphQlType == null) - { - ancestorClrType = ancestorClrType.BaseType; - ancestorGraphQlType = types.Find(t => t.CLRType == ancestorClrType); - } - - ancestorGraphQlType?.IncludedTypes.Add(graphQLType); - graphQLType.BaseType = ancestorGraphQlType; - } - - private static void RelateTypeWithImplementedInterfaces(GraphQLType graphQLType, List types) - { - foreach (var interf in graphQLType.CLRType.GetInterfaces()) - { - var graphQlInterfaceType = types.Find(t => t.CLRType == interf); - graphQlInterfaceType?.IncludedTypes.Add(graphQLType); - } - } - + private static void CompleteTypes(IEnumerable types) { foreach (var type in types.Where(t => t.QueryType == null)) @@ -180,9 +154,9 @@ private static void CompleteTypes(IEnumerable types) private static void CompleteType(GraphQLType type) { // validation maybe perform somewhere else - if (type.TypeKind == TypeKind.SCALAR && type.OwnFields.Count != 0) + if (type.TypeKind == TypeKind.SCALAR && type.Fields.Count != 0) throw new Exception("Scalar types must not have any fields defined."); // TODO: Schema validation exception? - if (type.TypeKind != TypeKind.SCALAR && type.OwnFields.Count == 0) + if (type.TypeKind != TypeKind.SCALAR && type.Fields.Count == 0) throw new Exception("Non-scalar types must have at least one field defined."); // TODO: Schema validation exception? if (type.TypeKind == TypeKind.SCALAR) @@ -210,7 +184,16 @@ private static void CompleteType(GraphQLType type) var fields = fieldGroupedByName.Select(g => g.First()); var fieldDict = fields.Where(f => !f.IsPost).ToDictionary(f => f.Name, f => f.Type.TypeKind == TypeKind.SCALAR ? TypeHelpers.MakeNullable(f.Type.CLRType) : typeof(object)); - type.QueryType = DynamicTypeBuilder.CreateDynamicType(type.Name + Guid.NewGuid(), fieldDict); + + if (type.TypeKind == TypeKind.INTERFACE) + { + type.QueryType = DynamicTypeBuilder.CreateDynamicInterface(type.Name + Guid.NewGuid(), fieldDict); + } + else + { + type.QueryType = DynamicTypeBuilder.CreateDynamicType(type.Name + Guid.NewGuid(), fieldDict, + type.Interfaces.Select(i => i.QueryType)); + } } private void AddDefaultTypes() @@ -291,46 +274,25 @@ private void AddTypeNameFields() private void AddTypeNameField(GraphQLType type) { + /* + * GraphQL Specification: + * 4.1.4: + * Type Name Introspection + * GraphQL supports type name introspection at any point within a query by the meta field + * __typename: String! when querying against any Object, Interface, or Union. It returns the + * name of the object type currently being queried. + * This is most often used when querying against Interface or Union types to identify which + * actual type of the possible types has been returned. + * This field is implicit and does not appear in the fields list in any defined type. + */ var builder = new GraphQLTypeBuilder(this, type); - if (!type.IncludedTypes.Any()) - { - // No included types, type name is constant. - builder.AddPostField("__typename", () => type.Name); - } - else - { - var param = Expression.Parameter(typeof(TEntity)); - - // Generate a nested if-else-expression with all included types starting at the leaves of the type hierarchy tree. - // Add the base type name as the last else-expression - Expression elseExpr = Expression.Constant(type.CLRType.Name); - - var includedTypes = type.IncludedTypes.SelectMany(SelectIncludedTypesRecursive); - - // Add type checks for all included types# - foreach (var includedType in includedTypes) - { - var testExpr = Expression.TypeIs(param, includedType.CLRType); - var expr = Expression.Constant(includedType.CLRType.Name); - elseExpr = Expression.Condition(testExpr, expr, elseExpr); - } - //var lambda = Expression.Lambda>(Expression.Block(exprs), param); - var lambda = Expression.Lambda>(elseExpr, param); - - builder.AddField("__typename", lambda); - } - } - - // Returns all included types of the hierarchy tree, ordered from "root" to the "leaves". - private static IEnumerable SelectIncludedTypesRecursive(GraphQLType type) - { - return new[] { type }.Concat(type.IncludedTypes.SelectMany(SelectIncludedTypesRecursive)); + builder.AddPostField("__typename", () => type.Name); } // This signature is pretty complicated, but necessarily so. // We need to build a function that we can execute against passed in TArgs that - // will return a base expression for combining with selectors (stored on GraphQLType.OwnFields) + // will return a base expression for combining with selectors (stored on GraphQLType.Fields) // This used to be a Func>, i.e. a function that returned a queryable given a context and arguments. // However, this wasn't good enough since we needed to be able to reference the (db) parameter in the expressions. // For example, being able to do: @@ -380,7 +342,7 @@ internal GraphQLFieldBuilder AddUnmodifiedMutationInternal GetGQLType(typeof(TContext)).OwnFields.FirstOrDefault(f => f.Name == name); + internal GraphQLField FindField(string name) => GetGQLType(typeof(TContext)).Fields.FirstOrDefault(f => f.Name == name); internal override GraphQLType GetGQLType(Type type) => _types.FirstOrDefault(t => t.CLRType == type) diff --git a/GraphQL.Net/GraphQLType.cs b/GraphQL.Net/GraphQLType.cs index f12732c..b4b2652 100644 --- a/GraphQL.Net/GraphQLType.cs +++ b/GraphQL.Net/GraphQLType.cs @@ -1,39 +1,34 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using GraphQL.Parser; namespace GraphQL.Net { - internal class GraphQLType + internal class GraphQLType : IGraphQLType { public GraphQLType(Type type) { CLRType = type; Name = type.Name; - OwnFields = new List(); - IncludedTypes = new List(); + Fields = new List(); + PossibleTypes = new List(); + Interfaces = new List(); } public string Name { get; set; } public string Description { get; set; } - public List OwnFields { get; set; } - public GraphQLType BaseType { get; set; } - public List IncludedTypes { get; set; } + public List Fields { get; set; } + public List PossibleTypes { get; set; } + public List Interfaces { get; set; } public Type CLRType { get; set; } public Type QueryType { get; set; } public TypeKind TypeKind { get; set; } - // Returns own fields and the fields of all included types. public IEnumerable GetQueryFields() { - return OwnFields.Concat(IncludedTypes.SelectMany(t => t.GetQueryFields()).Where(f => f.Name != "__typename")); - } - - // Returns own fields and the fields of all included types. - public IEnumerable GetAllFieldIncludeBaseType() - { - return BaseType != null ? OwnFields.Concat(BaseType.GetAllFieldIncludeBaseType()) : OwnFields; + return Fields; } } } \ No newline at end of file diff --git a/GraphQL.Net/GraphQLTypeBuilder.cs b/GraphQL.Net/GraphQLTypeBuilder.cs index d3d0fc3..100531c 100644 --- a/GraphQL.Net/GraphQLTypeBuilder.cs +++ b/GraphQL.Net/GraphQLTypeBuilder.cs @@ -43,8 +43,8 @@ public Func>> AdjustExprFunc< } // See GraphQLSchema.AddField for an explanation of the type of exprFunc, since it follows similar reasons - // TL:DR; OwnFields can have parameters passed in, so the Expression to be used is dependent on TArgs - // OwnFields can use TContext as well, so we have to return an Expression> and replace the TContext parameter when needed + // TL:DR; Fields can have parameters passed in, so the Expression to be used is dependent on TArgs + // Fields can use TContext as well, so we have to return an Expression> and replace the TContext parameter when needed public GraphQLFieldBuilder AddField(string name, Func>> exprFunc) => AddFieldInternal(name, exprFunc); @@ -55,7 +55,7 @@ public GraphQLFieldBuilder AddListField(string internal GraphQLFieldBuilder AddFieldInternal(string name, Func>> exprFunc) { var field = GraphQLField.New(_schema, name, exprFunc, typeof (TField), _type); - _type.OwnFields.Add(field); + _type.Fields.Add(field); return new GraphQLFieldBuilder(field); } @@ -63,7 +63,7 @@ internal GraphQLFieldBuilder AddFieldInternal(s internal GraphQLFieldBuilder AddListFieldInternal(string name, Func>>> exprFunc) { var field = GraphQLField.New(_schema, name, exprFunc, typeof (IEnumerable), _type); - _type.OwnFields.Add(field); + _type.Fields.Add(field); return new GraphQLFieldBuilder(field); } @@ -71,7 +71,7 @@ internal GraphQLFieldBuilder AddListFieldInternal AddMutationInternal(string name, Func>> exprFunc, Func mutation) { var field = GraphQLField.NewMutation(_schema, name, exprFunc, typeof (TField), _type, mutation); - _type.OwnFields.Add(field); + _type.Fields.Add(field); return new GraphQLFieldBuilder(field); } @@ -79,7 +79,7 @@ internal GraphQLFieldBuilder AddMutationInternal AddListMutationInternal(string name, Func>>> exprFunc, Func mutation) { var field = GraphQLField.NewMutation(_schema, name, exprFunc, typeof (IEnumerable), _type, mutation); - _type.OwnFields.Add(field); + _type.Fields.Add(field); return new GraphQLFieldBuilder(field); } @@ -126,7 +126,15 @@ public GraphQLFieldBuilder AddListField(string name, E public void AddAllFields() { foreach (var prop in typeof (TEntity).GetProperties(BindingFlags.Public | BindingFlags.Instance)) - _type.OwnFields.Add(CreateGenericField(prop)); + _type.Fields.Add(CreateGenericField(prop)); + } + + public GraphQLTypeBuilder AddInterface( + GraphQLTypeBuilder interfaceTypeBuilder) + { + _type.Interfaces.Add(interfaceTypeBuilder._type); + interfaceTypeBuilder._type.PossibleTypes.Add(_type); + return this; } // unsafe generic magic to create a GQLField instance @@ -148,7 +156,7 @@ private GraphQLField CreateGenericField(PropertyInfo prop) public GraphQLFieldBuilder AddPostField(string name, Func fieldFunc) { var field = GraphQLField.Post(_schema, name, fieldFunc); - _type.OwnFields.Add(field); + _type.Fields.Add(field); return new GraphQLFieldBuilder(field); } } diff --git a/GraphQL.Net/IGraphQLType.cs b/GraphQL.Net/IGraphQLType.cs new file mode 100644 index 0000000..23dbc92 --- /dev/null +++ b/GraphQL.Net/IGraphQLType.cs @@ -0,0 +1,12 @@ +using System; +using GraphQL.Parser; + +namespace GraphQL.Net +{ + public interface IGraphQLType + { + string Name { get; } + Type CLRType { get; } + TypeKind TypeKind { get; } + } +} \ No newline at end of file diff --git a/GraphQL.Net/SchemaAdapters/SchemaRootType.cs b/GraphQL.Net/SchemaAdapters/SchemaRootType.cs index 258421f..81b0385 100644 --- a/GraphQL.Net/SchemaAdapters/SchemaRootType.cs +++ b/GraphQL.Net/SchemaAdapters/SchemaRootType.cs @@ -10,7 +10,7 @@ class SchemaRootType : SchemaQueryTypeCS { public SchemaRootType(Schema schema, GraphQLType baseQueryType) { - Fields = baseQueryType.OwnFields + Fields = baseQueryType.Fields .Select(f => new SchemaField(this, f, schema)) .ToDictionary(f => f.FieldName, f => f as ISchemaField); } @@ -19,5 +19,6 @@ public SchemaRootType(Schema schema, GraphQLType baseQueryType) public override string TypeName => "SchemaRoot"; public override IEnumerable> PossibleTypes => new Collection>(); + public override IEnumerable> Interfaces => new Collection>(); } } diff --git a/GraphQL.Net/SchemaAdapters/SchemaType.cs b/GraphQL.Net/SchemaAdapters/SchemaType.cs index e1c25f9..19ed4a6 100644 --- a/GraphQL.Net/SchemaAdapters/SchemaType.cs +++ b/GraphQL.Net/SchemaAdapters/SchemaType.cs @@ -11,11 +11,12 @@ class SchemaType : SchemaQueryTypeCS private readonly GraphQLType _type; private readonly Lazy>> _fields; private readonly Lazy>> _possibleTypes; + private readonly Lazy>> _interfaces; internal SchemaType(GraphQLType type, Schema schema) { _type = type; - _fields = new Lazy>>(() => type.GetAllFieldIncludeBaseType() + _fields = new Lazy>>(() => type.GetQueryFields() .Select(f => new SchemaField(this, f, schema)) // There might be duplicates (i.e. '__typename' on types with a base type) - ignore them. .Aggregate( @@ -26,11 +27,13 @@ internal SchemaType(GraphQLType type, Schema schema) return dict; } )); - + _possibleTypes = new Lazy>>( - () => type.IncludedTypes.Select(schema.OfType) - // Add possible types recursively - .SelectMany(t => new List> { t }.Concat(t.PossibleTypes))); + () => type.PossibleTypes.Select(schema.OfType)); + + _interfaces = new Lazy>>( + () => type.Interfaces.Select(schema.OfType)); + } public override IReadOnlyDictionary> Fields => _fields.Value; @@ -38,5 +41,6 @@ internal SchemaType(GraphQLType type, Schema schema) public override string Description => _type.Description; public override Info Info => new Info(_type); public override IEnumerable> PossibleTypes => _possibleTypes.Value; + public override IEnumerable> Interfaces => _interfaces.Value; } } diff --git a/GraphQL.Parser.Test/SchemaTest.fs b/GraphQL.Parser.Test/SchemaTest.fs index 208095e..72537b9 100644 --- a/GraphQL.Parser.Test/SchemaTest.fs +++ b/GraphQL.Parser.Test/SchemaTest.fs @@ -78,6 +78,7 @@ type UserType() = |]) |] |> dictionary :> _ member this.PossibleTypes = Seq.empty + member this.Interfaces = Seq.empty type RootType() = member private this.Field(name, fieldType : SchemaFieldType, args) = @@ -107,6 +108,7 @@ type RootType() = |]) |] |> dictionary :> _ member this.PossibleTypes = Seq.empty + member this.Interfaces = Seq.empty type FakeSchema() = let root = new RootType() :> ISchemaQueryType<_> diff --git a/GraphQL.Parser/Integration/CS.fs b/GraphQL.Parser/Integration/CS.fs index cf14884..6d552b3 100644 --- a/GraphQL.Parser/Integration/CS.fs +++ b/GraphQL.Parser/Integration/CS.fs @@ -55,12 +55,14 @@ type SchemaQueryTypeCS<'s>() = default this.Info = Unchecked.defaultof<'s> abstract member Fields : IReadOnlyDictionary> abstract member PossibleTypes : IEnumerable> + abstract member Interfaces : IEnumerable> interface ISchemaQueryType<'s> with member this.TypeName = this.TypeName member this.Description = this.Description |> obj2option member this.Info = this.Info member this.Fields = this.Fields member this.PossibleTypes = this.PossibleTypes + member this.Interfaces = this.Interfaces [] type SchemaFieldCS<'s>() = diff --git a/GraphQL.Parser/Schema/SchemaAST.fs b/GraphQL.Parser/Schema/SchemaAST.fs index a7093db..db932ed 100644 --- a/GraphQL.Parser/Schema/SchemaAST.fs +++ b/GraphQL.Parser/Schema/SchemaAST.fs @@ -120,6 +120,7 @@ type ISchemaQueryType<'s> = /// May be empty, for example if the type is a primitive. abstract member Fields : IReadOnlyDictionary> abstract member PossibleTypes : IEnumerable> + abstract member Interfaces : IEnumerable> /// Represents a named core type, e.g. a "Time" type represented by an ISO-formatted string. /// The type may define validation rules that run on values after they have been checked to /// match the given core type. diff --git a/GraphQL.Parser/SchemaTools/Introspection.fs b/GraphQL.Parser/SchemaTools/Introspection.fs index f32167a..5e477c3 100644 --- a/GraphQL.Parser/SchemaTools/Introspection.fs +++ b/GraphQL.Parser/SchemaTools/Introspection.fs @@ -107,13 +107,14 @@ type IntroType = let fields = queryType.Fields.Values |> Seq.map IntroField.Of let possibleTypes = queryType.PossibleTypes |> Seq.map IntroType.Of let typeKind = if queryType.PossibleTypes |> Seq.isEmpty then TypeKind.OBJECT else TypeKind.INTERFACE + let interfaces = queryType.Interfaces |> Seq.map IntroType.Of { IntroType.Default with Kind = typeKind Name = Some queryType.TypeName Description = queryType.Description Fields = fields |> Some PossibleTypes = possibleTypes |> Some - Interfaces = Some Seq.empty + Interfaces = Some interfaces } static member Of(fieldType : SchemaFieldType<'s>) = match fieldType with diff --git a/Tests/GenericTests.cs b/Tests/GenericTests.cs index 1d17b6a..161aa34 100644 --- a/Tests/GenericTests.cs +++ b/Tests/GenericTests.cs @@ -170,7 +170,7 @@ public static void Fragements(GraphQL gql) { var results = gql.ExecuteQuery( "{ heros { name, __typename, ...human, ...stormtrooper, ...droid } }, " + - "fragment human on Human { height }, " + + "fragment human on IHuman { height }, " + "fragment stormtrooper on Stormtrooper { specialization }, " + "fragment droid on Droid { primaryFunction }"); Test.DeepEquals( @@ -185,7 +185,7 @@ public static void Fragements(GraphQL gql) public static void InlineFragements(GraphQL gql) { var results = gql.ExecuteQuery( - "{ heros { name, __typename, ... on Human { height }, ... on Stormtrooper { specialization }, " + + "{ heros { name, __typename, ... on IHuman { height }, ... on Stormtrooper { specialization }, " + "... on Droid { primaryFunction } } }"); Test.DeepEquals( results, @@ -199,7 +199,7 @@ public static void InlineFragements(GraphQL gql) public static void InlineFragementWithListField(GraphQL gql) { var results = gql.ExecuteQuery( - "{ heros { name, __typename, ... on Human { height, vehicles { name } }, ... on Stormtrooper { specialization }, " + + "{ heros { name, __typename, ... on IHuman { height, vehicles { name } }, ... on Stormtrooper { specialization }, " + "... on Droid { primaryFunction } } }"); Test.DeepEquals( results, @@ -275,7 +275,7 @@ public static void FragementWithMultipleTypenameFields(GraphQL(GraphQL gql) { var results = gql.ExecuteQuery( - "{ heros { ...stormtrooper, __typename, ... on Human {name}, ... on Droid {name}}}, fragment stormtrooper on Stormtrooper { name, height, specialization, __typename } "); + "{ heros { ...stormtrooper, __typename, ... on IHuman {name}, ... on Droid {name}}}, fragment stormtrooper on Stormtrooper { name, height, specialization, __typename } "); Test.DeepEquals( results, "{ heros: [ " + diff --git a/Tests/IntrospectionTests.cs b/Tests/IntrospectionTests.cs index 032f40a..096c399 100644 --- a/Tests/IntrospectionTests.cs +++ b/Tests/IntrospectionTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using NUnit.Framework.Internal; namespace Tests { @@ -16,6 +17,14 @@ public void TypeDirectFields() Test.DeepEquals(results, "{ __type: { name: 'User', description: '', kind: 'OBJECT' } }"); } + [Test] + public void EnumTypeDirectFields() + { + var gql = MemContext.CreateDefaultContext(); + var results = gql.ExecuteQuery("{ __type(name: \"AccountType\") { name, description, kind } }"); + Test.DeepEquals(results, "{ __type: { name: 'AccountType', description: null, kind: 'ENUM' } }"); + } + [Test] public void TypeWithChildFields() { @@ -52,7 +61,7 @@ public void ChildFieldType() { name: 'name', type: { name: 'String', kind: 'SCALAR', ofType: null } }, { name: 'account', type: { name: 'Account', kind: 'OBJECT', ofType: null } }, { name: 'nullRef', type: { name: 'NullRef', kind: 'OBJECT', ofType: null } }, - { name: 'total', type: { name: null, kind: 'NON_NULL', ofType: { name: 'Int', kind: 'SCALAR' } } }, + { name: 'total', type: { name: null, kind: 'NON_NULL', ofType: { name: 'Int', kind: 'SCALAR' } } }, { name: 'accountPaid', type: { name: null, kind: 'NON_NULL', ofType: { name: 'Boolean', kind: 'SCALAR' } } }, { name: 'abc', type: { name: 'String', kind: 'SCALAR', ofType: null } }, { name: 'sub', type: { name: 'Sub', kind: 'OBJECT', ofType: null } }, @@ -71,6 +80,7 @@ public void SchemaTypes() var schema = (IDictionary) gql.ExecuteQuery("{ __schema { types { name, kind, interfaces { name } } } }")["__schema"]; var types = (List>) schema["types"]; + Console.WriteLine(gql.ExecuteQuery("{ __schema { types { name, kind, interfaces { name } } } }")["__schema"]); var intType = types.First(t => (string) t["name"] == "Int"); Assert.AreEqual(intType["name"], "Int"); Assert.AreEqual(intType["kind"].ToString(), "SCALAR"); @@ -81,5 +91,311 @@ public void SchemaTypes() Assert.AreEqual(userType["kind"].ToString(), "OBJECT"); Assert.AreEqual(((List>)userType["interfaces"]).Count, 0); } + + [Test] + public void FieldArgsQuery() + { + var gql = MemContext.CreateDefaultContext(); + var results = gql.ExecuteQuery("{ __schema { queryType { fields { name, args { name, description, type { name, kind, ofType { name, kind } }, defaultValue } } } } }"); + + Test.DeepEquals( + results, + @"{ + ""__schema"": { + ""queryType"": { + ""fields"": [ + { + ""name"": ""users"", + ""args"": [] + }, + { + ""name"": ""user"", + ""args"": [ + { + ""name"": ""id"", + ""description"": null, + ""type"": { + ""name"": null, + ""kind"": ""NON_NULL"", + ""ofType"": { + ""name"": ""Int"", + ""kind"": ""SCALAR"" + } + }, + ""defaultValue"": null + } + ] + }, + { + ""name"": ""account"", + ""args"": [ + { + ""name"": ""id"", + ""description"": null, + ""type"": { + ""name"": null, + ""kind"": ""NON_NULL"", + ""ofType"": { + ""name"": ""Int"", + ""kind"": ""SCALAR"" + } + }, + ""defaultValue"": null + } + ] + }, + { + ""name"": ""accountPaidBy"", + ""args"": [ + { + ""name"": ""paid"", + ""description"": null, + ""type"": { + ""name"": ""DateTime"", + ""kind"": ""INPUT_OBJECT"", + ""ofType"": null + }, + ""defaultValue"": null + } + ] + }, + { + ""name"": ""accountsByGuid"", + ""args"": [ + { + ""name"": ""guid"", + ""description"": null, + ""type"": { + ""name"": null, + ""kind"": ""NON_NULL"", + ""ofType"": { + ""name"": ""Guid"", + ""kind"": ""SCALAR"" + } + }, + ""defaultValue"": null + } + ] + }, + { + ""name"": ""accountsByType"", + ""args"": [ + { + ""name"": ""accountType"", + ""description"": null, + ""type"": { + ""name"": null, + ""kind"": ""NON_NULL"", + ""ofType"": { + ""name"": ""AccountType"", + ""kind"": ""ENUM"" + } + }, + ""defaultValue"": null + } + ] + }, + { + ""name"": ""mutateMes"", + ""args"": [ + { + ""name"": ""id"", + ""description"": null, + ""type"": { + ""name"": null, + ""kind"": ""NON_NULL"", + ""ofType"": { + ""name"": ""Int"", + ""kind"": ""SCALAR"" + } + }, + ""defaultValue"": null + } + ] + }, + { + ""name"": ""mutate"", + ""args"": [ + { + ""name"": ""id"", + ""description"": null, + ""type"": { + ""name"": null, + ""kind"": ""NON_NULL"", + ""ofType"": { + ""name"": ""Int"", + ""kind"": ""SCALAR"" + } + }, + ""defaultValue"": null + }, + { + ""name"": ""newVal"", + ""description"": null, + ""type"": { + ""name"": null, + ""kind"": ""NON_NULL"", + ""ofType"": { + ""name"": ""Int"", + ""kind"": ""SCALAR"" + } + }, + ""defaultValue"": null + } + ] + }, + { + ""name"": ""addMutate"", + ""args"": [ + { + ""name"": ""newVal"", + ""description"": null, + ""type"": { + ""name"": null, + ""kind"": ""NON_NULL"", + ""ofType"": { + ""name"": ""Int"", + ""kind"": ""SCALAR"" + } + }, + ""defaultValue"": null + } + ] + }, + { + ""name"": ""hero"", + ""args"": [ + { + ""name"": ""id"", + ""description"": null, + ""type"": { + ""name"": null, + ""kind"": ""NON_NULL"", + ""ofType"": { + ""name"": ""Int"", + ""kind"": ""SCALAR"" + } + }, + ""defaultValue"": null + } + ] + }, + { + ""name"": ""heros"", + ""args"": [] + }, + { + ""name"": ""__schema"", + ""args"": [] + }, + { + ""name"": ""__type"", + ""args"": [ + { + ""name"": ""name"", + ""description"": null, + ""type"": { + ""name"": ""String"", + ""kind"": ""SCALAR"", + ""ofType"": null + }, + ""defaultValue"": null + } + ] + }, + { + ""name"": ""__typename"", + ""args"": [] + } + ] + } + } + }"); + } + + [Test] + public void FullIntrospectionQuery() + { + var gql = MemContext.CreateDefaultContext(); + var results = gql.ExecuteQuery( + @"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 + } + } + } + } + " + ); + Test.DeepEquals( + results, + @"{}" + ); + } } } diff --git a/Tests/MemContext.cs b/Tests/MemContext.cs index 3dd22c2..2e1fe9a 100644 --- a/Tests/MemContext.cs +++ b/Tests/MemContext.cs @@ -96,7 +96,7 @@ public MemContext() public List Accounts { get; set; } = new List(); public List MutateMes { get; set; } = new List(); public List NullRefs { get; set; } = new List(); - public List Heros { get; set; } = new List(); + public List Heros { get; set; } = new List(); public List Vehicles { get; set; } = new List(); public static GraphQL CreateDefaultContext() @@ -201,10 +201,25 @@ private static void InitializeNullRefSchema(GraphQLSchema schema) private static void InitializeCharacterSchema(GraphQLSchema schema) { - schema.AddType().AddAllFields(); - schema.AddType().AddAllFields(); - schema.AddType().AddAllFields(); - schema.AddType().AddAllFields(); + var characterInterface = schema.AddInterfaceType(); + characterInterface.AddAllFields(); + var humanInterface = schema.AddInterfaceType(); + humanInterface.AddAllFields(); + + var humanType = schema.AddType(); + humanType.AddAllFields(); + humanType.AddInterface(characterInterface); + humanType.AddInterface(humanInterface); + + var stormtrooperType = schema.AddType(); + stormtrooperType.AddAllFields(); + stormtrooperType.AddInterface(characterInterface); + stormtrooperType.AddInterface(humanInterface); + + var droidType = schema.AddType(); + droidType.AddAllFields(); + droidType.AddInterface(characterInterface); + schema.AddType().AddAllFields(); schema.AddField("hero", new { id = 0 }, (db, args) => db.Heros.AsQueryable().SingleOrDefault(h => h.Id == args.id)); @@ -275,25 +290,39 @@ public class NullRef public int Id { get; set; } } - public class Character + public interface ICharacter { - public int Id { get; set; } - public string Name { get; set; } + int Id { get; set; } + string Name { get; set; } + } + + public interface IHuman + { + double Height { get; set; } + ICollection Vehicles { get; set; } } - public class Human : Character + public class Human : ICharacter, IHuman { + public int Id { get; set; } + public string Name { get; set; } public double Height { get; set; } public ICollection Vehicles { get; set; } } - public class Stormtrooper : Human + public class Stormtrooper : ICharacter, IHuman { + public int Id { get; set; } + public string Name { get; set; } + public double Height { get; set; } public string Specialization { get; set; } + public ICollection Vehicles { get; set; } } - public class Droid : Character + public class Droid : ICharacter { + public int Id { get; set; } + public string Name { get; set; } public string PrimaryFunction { get; set; } } From 209a27a4fd62fffa6d5668bd82ca957f17413306 Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Thu, 6 Jul 2017 12:20:55 +0200 Subject: [PATCH 03/33] [WIP] update implementation of graphql interfaces and union types. Reflect changes in unit test. #47 --- GraphQL.Net/DynamicTypeBuilder.cs | 39 ++++-- GraphQL.Net/Executor.cs | 157 ++++++++++++++-------- GraphQL.Net/GraphQLField.cs | 8 +- GraphQL.Net/GraphQLFieldBuilder.cs | 6 + GraphQL.Net/GraphQLSchema.cs | 98 ++++++++++---- GraphQL.Net/GraphQLTypeBuilder.cs | 2 + GraphQL.Net/SchemaExtensions.cs | 4 +- Tests.EF/EntityFrameworkExecutionTests.cs | 84 ++++++++---- Tests/GenericTests.cs | 64 ++++----- Tests/InMemoryExecutionTests.cs | 18 +-- Tests/MemContext.cs | 56 +++++--- 11 files changed, 356 insertions(+), 180 deletions(-) diff --git a/GraphQL.Net/DynamicTypeBuilder.cs b/GraphQL.Net/DynamicTypeBuilder.cs index d0948dd..2f0edda 100644 --- a/GraphQL.Net/DynamicTypeBuilder.cs +++ b/GraphQL.Net/DynamicTypeBuilder.cs @@ -1,8 +1,10 @@ using System; +using System.CodeDom; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; +using GraphQL.Parser; namespace GraphQL.Net { @@ -18,30 +20,47 @@ static DynamicTypeBuilder() ModuleBuilder = assemblyBuilder.DefineDynamicModule(AssemblyName + ".dll"); } - public static Type CreateDynamicType(string name, Dictionary properties, IEnumerable implementedInterfaces) + public static Type CreateDynamicType(string name, IEnumerable fields) { var typeBuilder = ModuleBuilder.DefineType(AssemblyName + "." + name, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.Serializable | TypeAttributes.BeforeFieldInit); - - foreach (var implementedInterface in implementedInterfaces) - { - typeBuilder.AddInterfaceImplementation(implementedInterface); - } + var properties = ConvertFieldsToProperties(fields); foreach (var prop in properties) CreateProperty(typeBuilder, prop.Key, prop.Value); return typeBuilder.CreateType(); } - public static Type CreateDynamicInterface(string name, Dictionary properties) + + public static Type CreateDynamicUnionTypeOrInterface(string name, IEnumerable fields, IEnumerable possibleTypes) { var typeBuilder = ModuleBuilder.DefineType(AssemblyName + "." + name, - TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract); - + TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | + TypeAttributes.Serializable | TypeAttributes.BeforeFieldInit); + // Create an union type containing all properties of its possible types. + // Prefix all properties of a possible type to avoid conflicts of property names + var subTypeProperties = possibleTypes + .SelectMany( + t => t.Fields.Select(f => new {Name = t.Name + "$$$" + f.Name, Type = GetFieldPropertyType(f)})); + var properties = + subTypeProperties.Concat(fields.Select(f => new {Name = f.Name, Type = GetFieldPropertyType(f)})); foreach (var prop in properties) - CreateProperty(typeBuilder, prop.Key, prop.Value, true); + CreateProperty(typeBuilder, prop.Name, prop.Type); + return typeBuilder.CreateType(); } + private static Type GetFieldPropertyType(GraphQLField field) + { + return field.Type.TypeKind == TypeKind.SCALAR + ? TypeHelpers.MakeNullable(field.Type.CLRType) + : typeof(object); + } + + private static IDictionary ConvertFieldsToProperties(IEnumerable fields) + { + return fields.Where(f => !f.IsPost).ToDictionary(f => f.Name, GetFieldPropertyType); + } + private static void CreateProperty(TypeBuilder typeBuilder, string name, Type type, bool isAbstract = false) { FieldBuilder fieldBuilder = null; diff --git a/GraphQL.Net/Executor.cs b/GraphQL.Net/Executor.cs index 8a7f4f4..9e80f66 100644 --- a/GraphQL.Net/Executor.cs +++ b/GraphQL.Net/Executor.cs @@ -13,8 +13,6 @@ namespace GraphQL.Net { internal static class Executor { - private const string TypenameFieldSelector = "__typename"; - public static object Execute (GraphQLSchema schema, TContext context, GraphQLField field, ExecSelection query) { @@ -110,43 +108,87 @@ private static IDictionary MapResults(object queryObject, IEnume var graphQlQueryType = schema.Types.First(t => t.QueryType == queryObject.GetType()); var queryTypeToSelections = CreateQueryTypeToSelectionsMapping(graphQlQueryType, selections.ToList()); + if (!queryTypeToSelections.ContainsKey(type)) + { + return dict; + } + foreach (var map in selections) { var key = map.Name; var field = map.SchemaField.Field(); + var typeConditionType = map.TypeCondition?.Value?.Type(); + var propertyName = (typeConditionType != null + ? typeConditionType.Name + "$$$" // TODO: Extract utility method + : "") + field.Name; object obj = null; if (field.IsPost) { obj = field.PostFieldFunc(); } - else if (type.GetProperty(field.Name) != null && queryTypeToSelections.ContainsKey(type)) + else if (type.GetProperty(propertyName) != null) { - var typeSelections = queryTypeToSelections[type]; - if (typeSelections.Any(s => s.Name == field.Name)) - { - obj = type.GetProperty(field.Name).GetGetMethod().Invoke(queryObject, new object[] {}); - } - else - { - continue; - } + obj = type.GetProperty(propertyName).GetGetMethod().Invoke(queryObject, new object[] {}); } else { continue; } - if (key == "__typename") + + // Filter fields for selections with type conditions - the '__typename'-property has to be present. + if (map.TypeCondition != null) { - if (graphQlQueryType != null) + // The selection has a type condition, i.e. the result has a type with included types. + // The result contains all fields of all included types and has to be filtered. + // Sample: + // + // Types: + // - Character [name: string] + // - Human [height: float] extends Character + // - Stormtrooper [specialization: string] extends Human + // - Droid [orimaryFunction: string] extends Character + // + // Sample query: + // query { heros { name, ... on Human { height }, ... on Stormtrooper { specialization }, ... on Droid { primaryFunction } } } + // + // The ExecSelection for 'height', 'specialization' and 'primaryFunction' have a type condition with the following type names: + // - height: Human + // - specialization: Stormtrooper + // - primaryFunction: Droid + // + // To filter the result properly, we have to consider the following cases: + // - Human: + // - Include: 'name', 'height' + // - Exclude: 'specialization', 'primaryFunction' + // => (1) Filter results of selections with a type-condition-name != result.__typename + // + // - Stormtrooper + // - Include: 'name', 'height', and 'specialization' + // - Exclude: 'primaryFunction' + // => Same as Human (1), but: + // => (2) include results of selections with the same type-condition-name of any ancestor-type. + // + var selectionConditionTypeName = map.TypeCondition.Value?.TypeName; + var typenameProp = type.GetRuntimeProperty("__typename"); + var resultTypeName = (string) typenameProp?.GetValue(queryObject); + + // (1) type-condition-name != result.__typename + if (selectionConditionTypeName != resultTypeName) { - var specificTypenameField = graphQlQueryType.Fields.FirstOrDefault(f => f.Name == "__typename"); - obj = specificTypenameField != null ? specificTypenameField.PostFieldFunc() : obj; + // (2) Check ancestor types + var resultGraphQlType = schema.Types.FirstOrDefault(t => t.Name == resultTypeName); + if (resultGraphQlType != null && + resultGraphQlType.Name != selectionConditionTypeName && + resultGraphQlType.Interfaces.All(i => i.Name != selectionConditionTypeName)) + { + continue; + } } } - if (obj == null) + if (obj == null && !dict.ContainsKey(key)) { dict.Add(key, null); continue; @@ -154,7 +196,9 @@ private static IDictionary MapResults(object queryObject, IEnume if (field.IsPost && map.Selections.Any()) { - var selector = GetSelector(schema, field.Type, map.Selections.Values(), new ExpressionOptions(null, castAssignment: true, nullCheckLists: true, typeCheckInheritance: true)); + var selector = GetSelector(schema, field.Type, map.Selections.Values(), + new ExpressionOptions(null, castAssignment: true, nullCheckLists: true, + typeCheckInheritance: true)); obj = selector.Compile().DynamicInvoke(obj); } @@ -192,8 +236,7 @@ private static LambdaExpression GetSelector(GraphQLSchema schema, Grap var init = GetMemberInit(schema, gqlType.QueryType, selections, parameter, options); return Expression.Lambda(init, parameter); } - - + private static IDictionary>> CreateQueryTypeToSelectionsMapping( GraphQLType queryGraphQlType, IList> selections) { @@ -229,43 +272,48 @@ private static ConditionalExpression GetMemberInit(GraphQLSchema schem { // Avoid possible multiple enumeration of selections-enumerable var selections = selectionsEnumerable as IList> ?? selectionsEnumerable.ToList(); - var queryGraphQlType = schema.GetGQLType(parameterExpression.Type); - // Iff there are are any type conditions, the query type must be abstract, so the query type cannot be instantiated. - if (queryGraphQlType.TypeKind == TypeKind.INTERFACE || queryGraphQlType.TypeKind == TypeKind.UNION) - { - var queryTypeToBindings = CreateQueryTypeToSelectionsMapping(queryGraphQlType, selections) - .ToDictionary( - p => p.Key, - p => p.Value.Select(s => GetBinding(schema, s, p.Key, parameterExpression, options)) - ); + // The '__typename'-field selection has to be added for queries with type conditions + var typeConditionButNoTypeNameSelection = selections.Any() && + selections.Any(s => s.TypeCondition != null); - // Generate a nested if-else-expression with possible types. - // Add `null` as fallback value - var firstType = queryGraphQlType.PossibleTypes.First(); - Expression baseElseExpr = Expression.MemberInit(Expression.New(firstType.QueryType), queryTypeToBindings[firstType.QueryType]); - Expression elseExpr = Expression.TypeAs(baseElseExpr, queryType); + // Any '__typename' selection have to be replaced by the '__typename' selection of the target type' '__typename'-field. + var typeNameConditionHasToBeReplaced = selections.Any(s => s.Name == "__typename"); - // Add type checks for all possible types - foreach (var possibleGraphQLType in queryGraphQlType.PossibleTypes) - { - // Map the query type bindings to the related condition type bindings. - var bindings = queryTypeToBindings[possibleGraphQLType.QueryType]; - var testExpr = Expression.TypeIs(parameterExpression, possibleGraphQLType.CLRType); - var expr = Expression.MemberInit(Expression.New(possibleGraphQLType.QueryType), bindings); - var exprWithCast = Expression.TypeAs(expr, queryType); - elseExpr = Expression.Condition(testExpr, exprWithCast, elseExpr); - } + var bindings = + selections.Where(s => !s.SchemaField.Info.Field.IsPost) + .Select(s => GetBinding(schema, s, queryType, parameterExpression, options)); - //var memberInit = Expression.Lambda>(elseExpr, param); - return NullPropagate(parameterExpression, elseExpr); - } - else + // Add selection for '__typename'-field of the proper type + if (typeConditionButNoTypeNameSelection || typeNameConditionHasToBeReplaced) { - var bindings = selections.Where(s => !s.SchemaField.Info.Field.IsPost).Select(s => GetBinding(schema, s, queryType, parameterExpression, options)); - var memberInit = Expression.MemberInit(Expression.New(queryType), bindings); - return NullPropagate(parameterExpression, memberInit); + // Find the base types' `__typename` field + var graphQlType = schema.GetGQLTypeByQueryType(queryType); + var typeNameField = graphQlType?.Fields.Find(f => f.Name == "__typename"); + if (typeNameField != null && !typeNameField.IsPost) + { + var typeNameExecSelection = new ExecSelection( + new SchemaField( + schema.Adapter.QueryTypes[graphQlType?.Name], + typeNameField, + schema.Adapter), + new FSharpOption(typeNameField?.Name), + null, + new WithSource>[] {}, + new WithSource>[] {}, + new WithSource>[] {} + ); + bindings = + bindings + .Where(b => b.Member.Name != "__typename") + .Concat(new[] + {GetBinding(schema, typeNameExecSelection, queryType, parameterExpression, options)}) + .ToList(); + } } + + var memberInit = Expression.MemberInit(Expression.New(queryType), bindings); + return NullPropagate(parameterExpression, memberInit); } private static ConditionalExpression NullPropagate(Expression baseExpr, Expression returnExpr) @@ -278,11 +326,12 @@ private static MemberBinding GetBinding(GraphQLSchema schema, ExecSele { var field = map.SchemaField.Field(); var needsTypeCheck = baseBindingExpr.Type != map.SchemaField.DeclaringType?.Info?.Type?.CLRType; - - toType = map.TypeCondition?.Value != null ? map.TypeCondition.Value.Type().QueryType : toType; + var isTypeConditionSelection = map.TypeCondition?.Value != null; + var propertyName = (isTypeConditionSelection ? map.TypeCondition?.Value.Type().Name + "$$$" : "") + + map.SchemaField.FieldName; var toMember = toType.GetProperty( - map.SchemaField.FieldName, + propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); if (toMember == null) diff --git a/GraphQL.Net/GraphQLField.cs b/GraphQL.Net/GraphQLField.cs index 58a4f35..b0c533d 100644 --- a/GraphQL.Net/GraphQLField.cs +++ b/GraphQL.Net/GraphQLField.cs @@ -19,7 +19,7 @@ internal class GraphQLField public bool IsMutation { get; protected set; } - protected Type FieldCLRType { get; set; } + internal Type FieldCLRType { get; set; } protected Type ArgsCLRType { get; set; } internal GraphQLType DefiningType { get; private set; } internal GraphQLSchema Schema { get; set; } @@ -35,6 +35,12 @@ internal class GraphQLField private GraphQLType _type; public GraphQLType Type => _type ?? (_type = Schema.GetGQLType(FieldCLRType)); + //TODO: Necessary for union types - find better solution + internal void SetReturnType(GraphQLType type) + { + _type = type; + } + public virtual IEnumerable> Arguments => TypeHelpers.GetArgs(Schema.VariableTypes, ArgsCLRType); diff --git a/GraphQL.Net/GraphQLFieldBuilder.cs b/GraphQL.Net/GraphQLFieldBuilder.cs index 34fccb1..fa7e079 100644 --- a/GraphQL.Net/GraphQLFieldBuilder.cs +++ b/GraphQL.Net/GraphQLFieldBuilder.cs @@ -24,6 +24,12 @@ public GraphQLFieldBuilder WithComplexity(long min, long max) return this; } + public GraphQLFieldBuilder WithReturnType(IGraphQLType type) + { + _field.SetReturnType(type as GraphQLType); + return this; + } + // TODO: This should be removed once we figure out a better way to do it internal GraphQLFieldBuilder WithResolutionType(ResolutionType type) { diff --git a/GraphQL.Net/GraphQLSchema.cs b/GraphQL.Net/GraphQLSchema.cs index 6cb00a3..03f591e 100644 --- a/GraphQL.Net/GraphQLSchema.cs +++ b/GraphQL.Net/GraphQLSchema.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Reflection.Emit; using GraphQL.Net.SchemaAdapters; using GraphQL.Parser; @@ -12,6 +13,7 @@ public abstract class GraphQLSchema { internal readonly VariableTypes VariableTypes = new VariableTypes(); internal abstract GraphQLType GetGQLType(Type type); + internal abstract GraphQLType GetGQLTypeByQueryType(Type queryType); } public class GraphQLSchema : GraphQLSchema @@ -76,6 +78,23 @@ public GraphQLTypeBuilder AddType(string name = null return new GraphQLTypeBuilder(this, gqlType); } + public IGraphQLType AddUnionType(string name, IEnumerable possibleTypes, + string description = null) + { + if (_types.Any(t => t.Name == name)) + throw new ArgumentException("Union type with same name has already been added: " + name); + + var gqlType = new GraphQLType(typeof(object)) + { + TypeKind = TypeKind.UNION, + Name = name, + Description = description ?? "", + PossibleTypes = possibleTypes.Cast().ToList() + }; + _types.Add(gqlType); + return gqlType; + } + public GraphQLTypeBuilder AddInterfaceType(string name = null, string description = null) { var type = typeof(TInterface); @@ -155,9 +174,9 @@ private static void CompleteType(GraphQLType type) { // validation maybe perform somewhere else if (type.TypeKind == TypeKind.SCALAR && type.Fields.Count != 0) - throw new Exception("Scalar types must not have any fields defined."); // TODO: Schema validation exception? + throw new Exception("Scalar and union types must not have any fields defined."); // TODO: Schema validation exception? if (type.TypeKind != TypeKind.SCALAR && type.Fields.Count == 0) - throw new Exception("Non-scalar types must have at least one field defined."); // TODO: Schema validation exception? + throw new Exception("Non-scalar and non-union types must have at least one field defined."); // TODO: Schema validation exception? if (type.TypeKind == TypeKind.SCALAR) { @@ -166,33 +185,14 @@ private static void CompleteType(GraphQLType type) } var allFields = type.GetQueryFields(); - - // For union types there may duplicate fields. Validate those and remove duplicates - var fieldGroupedByName = allFields.GroupBy(f => f.Name).ToList(); - - // All fields with the same name have to be of the same type. - foreach (var fieldGroup in fieldGroupedByName) - { - var typeOfFirstField = fieldGroup.FirstOrDefault()?.Type; - if (fieldGroup.Any(f => f.Type?.CLRType?.IsAssignableFrom(typeOfFirstField?.CLRType) == false && typeOfFirstField?.CLRType?.IsAssignableFrom(f.Type?.CLRType) == false)) - { - var fieldName = fieldGroup.FirstOrDefault()?.Name; - throw new ArgumentException($"The type '{type.Name}' has multiple fields named '{fieldName}' with different types."); - } - } - - var fields = fieldGroupedByName.Select(g => g.First()); - - var fieldDict = fields.Where(f => !f.IsPost).ToDictionary(f => f.Name, f => f.Type.TypeKind == TypeKind.SCALAR ? TypeHelpers.MakeNullable(f.Type.CLRType) : typeof(object)); - - if (type.TypeKind == TypeKind.INTERFACE) + if (type.TypeKind == TypeKind.INTERFACE || type.TypeKind == TypeKind.UNION) { - type.QueryType = DynamicTypeBuilder.CreateDynamicInterface(type.Name + Guid.NewGuid(), fieldDict); + type.QueryType = DynamicTypeBuilder.CreateDynamicUnionTypeOrInterface(type.Name + Guid.NewGuid(), + allFields, type.PossibleTypes); } else { - type.QueryType = DynamicTypeBuilder.CreateDynamicType(type.Name + Guid.NewGuid(), fieldDict, - type.Interfaces.Select(i => i.QueryType)); + type.QueryType = DynamicTypeBuilder.CreateDynamicType(type.Name + Guid.NewGuid(), allFields); } } @@ -287,9 +287,47 @@ private void AddTypeNameField(GraphQLType type) */ var builder = new GraphQLTypeBuilder(this, type); - builder.AddPostField("__typename", () => type.Name); + if (!type.PossibleTypes.Any()) + { + // No included types, type name is constant. + builder.AddPostField("__typename", () => type.Name); + } + else + { + var param = Expression.Parameter(typeof(TEntity)); + + // Generate a nested if-else-expression with all included types starting at the leaves of the type hierarchy tree. + // Add the base type name as the last else-expression + Expression elseExpr = Expression.Constant(type.CLRType.Name); + + var includedTypes = + type.PossibleTypes.SelectMany(SelectIncludedTypesRecursive) + .Where( + t => + t.TypeKind != TypeKind.INTERFACE && t.TypeKind != TypeKind.UNION && + t.CLRType != typeof(object)); + + // Add type checks for all included types# + foreach (var includedType in includedTypes) + { + var testExpr = Expression.TypeIs(param, includedType.CLRType); + var expr = Expression.Constant(includedType.CLRType.Name); + elseExpr = Expression.Condition(testExpr, expr, elseExpr); + } + + //var lambda = Expression.Lambda>(Expression.Block(exprs), param); + var lambda = Expression.Lambda>(elseExpr, param); + + builder.AddField("__typename", lambda); + } } + // Returns all included types of the hierarchy tree, ordered from "root" to the "leaves". + private static IEnumerable SelectIncludedTypesRecursive(GraphQLType type) + { + return new[] { type }.Union(type.PossibleTypes.SelectMany(SelectIncludedTypesRecursive)); + } + // This signature is pretty complicated, but necessarily so. // We need to build a function that we can execute against passed in TArgs that // will return a base expression for combining with selectors (stored on GraphQLType.Fields) @@ -345,8 +383,12 @@ internal GraphQLFieldBuilder AddUnmodifiedMutationInternal GetGQLType(typeof(TContext)).Fields.FirstOrDefault(f => f.Name == name); internal override GraphQLType GetGQLType(Type type) - => _types.FirstOrDefault(t => t.CLRType == type) - ?? new GraphQLType(type) { TypeKind = TypeKind.SCALAR }; + => _types.FirstOrDefault(t => t.CLRType == type) ?? new GraphQLType(type) {TypeKind = TypeKind.SCALAR}; + + internal override GraphQLType GetGQLTypeByQueryType(Type queryType) + => + _types.FirstOrDefault(t => t.QueryType == queryType) ?? + new GraphQLType(queryType) {TypeKind = TypeKind.SCALAR}; internal IEnumerable Types => _types; } diff --git a/GraphQL.Net/GraphQLTypeBuilder.cs b/GraphQL.Net/GraphQLTypeBuilder.cs index 100531c..53109aa 100644 --- a/GraphQL.Net/GraphQLTypeBuilder.cs +++ b/GraphQL.Net/GraphQLTypeBuilder.cs @@ -17,6 +17,8 @@ internal GraphQLTypeBuilder(GraphQLSchema schema, GraphQLType type) _type = type; } + public IGraphQLType GraphQLType => _type; + // This overload is provided to the user so they can shape TArgs with an anonymous type and rely on type inference for type parameters // e.g. AddField("profilePic", new { size = 0 }, (db, user) => db.ProfilePics.Where(p => p.UserId == u.Id && p.Size == args.size)); [Obsolete] diff --git a/GraphQL.Net/SchemaExtensions.cs b/GraphQL.Net/SchemaExtensions.cs index 992aad2..8f3106f 100644 --- a/GraphQL.Net/SchemaExtensions.cs +++ b/GraphQL.Net/SchemaExtensions.cs @@ -97,7 +97,9 @@ private static Func db.Entities.Where(args) into args => db => db.Entities.Where(args) - public static GraphQLFieldBuilder AddListField(this GraphQLSchema context, string name, Expression>> queryableGetter) + public static GraphQLFieldBuilder AddListField( + this GraphQLSchema context, string name, + Expression>> queryableGetter) { return context.AddFieldInternal(name, GetFinalQueryFunc>(queryableGetter), ResolutionType.ToList); } diff --git a/Tests.EF/EntityFrameworkExecutionTests.cs b/Tests.EF/EntityFrameworkExecutionTests.cs index 085476d..7e961f7 100644 --- a/Tests.EF/EntityFrameworkExecutionTests.cs +++ b/Tests.EF/EntityFrameworkExecutionTests.cs @@ -187,14 +187,47 @@ private static void InitializeNullRefSchema(GraphQLSchema schema) private static void InitializeCharacterSchema(GraphQLSchema schema) { - schema.AddType().AddAllFields(); - schema.AddType().AddAllFields(); - schema.AddType().AddAllFields(); - schema.AddType().AddAllFields(); - schema.AddType().AddAllFields(); + var characterInterface = schema.AddInterfaceType(); + characterInterface.AddAllFields(); + + var humanInterface = schema.AddInterfaceType(); + humanInterface.AddAllFields(); + humanInterface.AddInterface(characterInterface); + + var humanType = schema.AddType(); + humanType.AddAllFields(); + humanType.AddInterface(characterInterface); + humanType.AddInterface(humanInterface); + + var stormtrooperType = schema.AddType(); + stormtrooperType.AddAllFields(); + stormtrooperType.AddInterface(characterInterface); + stormtrooperType.AddInterface(humanInterface); + + var droidType = schema.AddType(); + droidType.AddAllFields(); + droidType.AddInterface(characterInterface); + schema.AddUnionType("OtherUnionType01", new List()); + + var heroUnionType = schema.AddUnionType( + "Hero", + new[] + { + // TODO: ORDER MATTERS FOR TYPENAME RESOLUTION + characterInterface.GraphQLType, + humanInterface.GraphQLType, + humanType.GraphQLType, + stormtrooperType.GraphQLType, + droidType.GraphQLType + }); + - schema.AddField("hero", new { id = 0 }, (db, args) => db.Heros.SingleOrDefault(h => h.Id == args.id)); - schema.AddListField("heros", db => db.Heros.AsQueryable()); + schema.AddUnionType("OtherUnionType02", new List()); + + schema.AddType().AddAllFields(); + schema.AddField("hero", new {id = 0}, (db, args) => db.Heros.SingleOrDefault(h => h.Id == args.id)); + schema.AddListField("heros", db => db.Heros.AsQueryable()) + .WithReturnType(heroUnionType); } [Test] @@ -244,23 +277,23 @@ private static void InitializeCharacterSchema(GraphQLSchema schema) [Test] public void ChildFieldWithParameters() => GenericTests.ChildFieldWithParameters(MemContext.CreateDefaultContext()); [Test] - public static void Fragements() => GenericTests.Fragements(CreateDefaultContext()); + public static void Fragments() => GenericTests.Fragments(CreateDefaultContext()); [Test] - public static void InlineFragements() => GenericTests.InlineFragements(CreateDefaultContext()); + public static void InlineFragments() => GenericTests.InlineFragments(CreateDefaultContext()); [Test] - public static void InlineFragementWithListField() => GenericTests.InlineFragementWithListField(CreateDefaultContext()); + public static void InlineFragmentWithListField() => GenericTests.InlineFragmentWithListField(CreateDefaultContext()); [Test] - public static void FragementWithMultiLevelInheritance() => GenericTests.FragementWithMultiLevelInheritance(CreateDefaultContext()); + public static void FragmentWithMultiLevelInheritance() => GenericTests.FragmentWithMultiLevelInheritance(CreateDefaultContext()); [Test] - public static void InlineFragementWithoutTypenameField() => GenericTests.InlineFragementWithoutTypenameField(CreateDefaultContext()); + public static void InlineFragmentWithoutTypenameField() => GenericTests.InlineFragmentWithoutTypenameField(CreateDefaultContext()); [Test] - public static void FragementWithoutTypenameField() => GenericTests.FragementWithoutTypenameField(CreateDefaultContext()); + public static void FragmentWithoutTypenameField() => GenericTests.FragmentWithoutTypenameField(CreateDefaultContext()); [Test] - public static void InlineFragementWithoutTypenameFieldWithoutOtherFields() => GenericTests.InlineFragementWithoutTypenameFieldWithoutOtherFields(CreateDefaultContext()); + public static void InlineFragmentWithoutTypenameFieldWithoutOtherFields() => GenericTests.InlineFragmentWithoutTypenameFieldWithoutOtherFields(CreateDefaultContext()); [Test] - public static void FragementWithMultipleTypenameFields() => GenericTests.FragementWithMultipleTypenameFields(CreateDefaultContext()); + public static void FragmentWithMultipleTypenameFields() => GenericTests.FragmentWithMultipleTypenameFields(CreateDefaultContext()); [Test] - public static void FragementWithMultipleTypenameFieldsMixedWithInlineFragment() => GenericTests.FragementWithMultipleTypenameFieldsMixedWithInlineFragment(CreateDefaultContext()); + public static void FragmentWithMultipleTypenameFieldsMixedWithInlineFragment() => GenericTests.FragmentWithMultipleTypenameFieldsMixedWithInlineFragment(CreateDefaultContext()); [Test] public void AddAllFields() @@ -301,7 +334,7 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) public IDbSet Accounts { get; set; } public IDbSet MutateMes { get; set; } public IDbSet NullRefs { get; set; } - public IDbSet Heros { get; set; } + public IDbSet Heros { get; set; } public IDbSet Vehicles { get; set; } } @@ -343,24 +376,29 @@ class NullRef public int Id { get; set; } } - class Character + class ICharacter { public int Id { get; set; } public string Name { get; set; } } - - class Human : Character + + class IHuman : ICharacter { public double Height { get; set; } public ICollection Vehicles { get; set; } } - class Stormtrooper : Human + class Human : IHuman + { + + } + + class Stormtrooper : IHuman { public string Specialization { get; set; } } - class Droid : Character + class Droid : ICharacter { public string PrimaryFunction { get; set; } } @@ -370,7 +408,7 @@ class Vehicle public int Id { get; set; } public string Name { get; set; } public int HumanId { get; set; } - public virtual Human Human { get; set; } + public virtual IHuman Human { get; set; } } } } diff --git a/Tests/GenericTests.cs b/Tests/GenericTests.cs index 161aa34..790b010 100644 --- a/Tests/GenericTests.cs +++ b/Tests/GenericTests.cs @@ -166,53 +166,53 @@ public static void ChildFieldWithParameters(GraphQL gql) Test.DeepEquals(results, "{ account: { id: 1, name: 'My Test Account', firstUserWithActive: null } }"); } - public static void Fragements(GraphQL gql) + public static void Fragments(GraphQL gql) { var results = gql.ExecuteQuery( - "{ heros { name, __typename, ...human, ...stormtrooper, ...droid } }, " + - "fragment human on IHuman { height }, " + - "fragment stormtrooper on Stormtrooper { specialization }, " + - "fragment droid on Droid { primaryFunction }"); + "{ heros { ...human, ...stormtrooper, ...droid } }, " + + "fragment human on Human { name, height, __typename }, " + + "fragment stormtrooper on Stormtrooper { name, height, specialization, __typename }, " + + "fragment droid on Droid { name, primaryFunction, __typename }"); Test.DeepEquals( results, "{ heros: [ " + - "{ name: 'Han Solo', __typename: 'Human', height: 5.6430448}, " + - "{ name: 'FN-2187', __typename: 'Stormtrooper', height: 4.9, specialization: 'Imperial Snowtrooper'}, " + - "{ name: 'R2-D2', __typename: 'Droid', primaryFunction: 'Astromech' } ] }" + "{ name: 'Han Solo', height: 5.6430448, __typename: 'Human' }, " + + "{ name: 'FN-2187', height: 4.9, specialization: 'Imperial Snowtrooper', __typename: 'Stormtrooper' }, " + + "{ name: 'R2-D2', primaryFunction: 'Astromech', __typename: 'Droid' } ] }" ); } - public static void InlineFragements(GraphQL gql) + public static void InlineFragments(GraphQL gql) { var results = gql.ExecuteQuery( - "{ heros { name, __typename, ... on IHuman { height }, ... on Stormtrooper { specialization }, " + + "{ heros { __typename, ... on ICharacter { name }, ... on IHuman { height }, ... on Stormtrooper { specialization }, " + "... on Droid { primaryFunction } } }"); Test.DeepEquals( results, "{ heros: [ " + - "{ name: 'Han Solo', __typename: 'Human', height: 5.6430448}, " + - "{ name: 'FN-2187', __typename: 'Stormtrooper', height: 4.9, specialization: 'Imperial Snowtrooper'}, " + - "{ name: 'R2-D2', __typename: 'Droid', primaryFunction: 'Astromech' } ] }" + "{ __typename: 'Human', name: 'Han Solo', height: 5.6430448 }, " + + "{ __typename: 'Stormtrooper', name: 'FN-2187', height: 4.9, specialization: 'Imperial Snowtrooper' }, " + + "{ __typename: 'Droid', name: 'R2-D2', primaryFunction: 'Astromech' } ] }" ); } - public static void InlineFragementWithListField(GraphQL gql) + public static void InlineFragmentWithListField(GraphQL gql) { var results = gql.ExecuteQuery( - "{ heros { name, __typename, ... on IHuman { height, vehicles { name } }, ... on Stormtrooper { specialization }, " + + "{ heros { __typename, ... on ICharacter { name }, ... on IHuman { height, vehicles { name } }, ... on Stormtrooper { specialization }, " + "... on Droid { primaryFunction } } }"); Test.DeepEquals( results, "{ heros: [ " + - "{ name: 'Han Solo', __typename: 'Human', height: 5.6430448, vehicles: [ {name: 'Millennium falcon'}] }, " + - "{ name: 'FN-2187', __typename: 'Stormtrooper', height: 4.9, vehicles: [ {name: 'Speeder bike'}], specialization: 'Imperial Snowtrooper'}, " + - "{ name: 'R2-D2', __typename: 'Droid', primaryFunction: 'Astromech' } ] }" + "{ __typename: 'Human', name: 'Han Solo', height: 5.6430448, vehicles: [ { name: 'Millennium falcon' } ] }, " + + "{ __typename: 'Stormtrooper', name: 'FN-2187', height: 4.9, vehicles: [ { name: 'Speeder bike' } ], specialization: 'Imperial Snowtrooper' }, " + + "{ __typename: 'Droid', name: 'R2-D2', primaryFunction: 'Astromech' } ] }" ); } - public static void FragementWithMultiLevelInheritance(GraphQL gql) + public static void FragmentWithMultiLevelInheritance(GraphQL gql) { - var results = gql.ExecuteQuery("{ heros { name, __typename, ... on Stormtrooper { height, specialization } } }"); + var results = gql.ExecuteQuery("{ heros { ... on ICharacter { name, __typename }, ... on Stormtrooper { height, specialization } } }"); Test.DeepEquals( results, "{ heros: [ " + @@ -222,9 +222,9 @@ public static void FragementWithMultiLevelInheritance(GraphQL(GraphQL gql) + public static void InlineFragmentWithoutTypenameField(GraphQL gql) { - var results = gql.ExecuteQuery("{ heros { name, ... on Stormtrooper { height, specialization } } }"); + var results = gql.ExecuteQuery("{ heros { ... on ICharacter { name }, ... on Stormtrooper { height, specialization } } }"); Test.DeepEquals( results, "{ heros: [ " + @@ -234,7 +234,7 @@ public static void InlineFragementWithoutTypenameField(GraphQL(GraphQL gql) + public static void InlineFragmentWithoutTypenameFieldWithoutOtherFields(GraphQL gql) { var results = gql.ExecuteQuery("{ heros { ... on Stormtrooper { height, specialization } } }"); Test.DeepEquals( @@ -246,10 +246,10 @@ public static void InlineFragementWithoutTypenameFieldWithoutOtherFields(GraphQL gql) + public static void FragmentWithoutTypenameField(GraphQL gql) { var results = gql.ExecuteQuery( - "{ heros { name, ...stormtrooper } }, fragment stormtrooper on Stormtrooper { height, specialization } "); + "{ heros { ...character, ...stormtrooper } }, fragment character on ICharacter { name }, fragment stormtrooper on Stormtrooper { height, specialization } "); Test.DeepEquals( results, "{ heros: [ " + @@ -259,10 +259,10 @@ public static void FragementWithoutTypenameField(GraphQL gql ); } - public static void FragementWithMultipleTypenameFields(GraphQL gql) + public static void FragmentWithMultipleTypenameFields(GraphQL gql) { var results = gql.ExecuteQuery( - "{ heros { name, ...stormtrooper, __typename } }, fragment stormtrooper on Stormtrooper { height, specialization, __typename } "); + "{ heros { ...character, ...stormtrooper, __typename } }, fragment character on ICharacter { name }, fragment stormtrooper on Stormtrooper { height, specialization, __typename } "); Test.DeepEquals( results, "{ heros: [ " + @@ -272,16 +272,16 @@ public static void FragementWithMultipleTypenameFields(GraphQL(GraphQL gql) + public static void FragmentWithMultipleTypenameFieldsMixedWithInlineFragment(GraphQL gql) { var results = gql.ExecuteQuery( - "{ heros { ...stormtrooper, __typename, ... on IHuman {name}, ... on Droid {name}}}, fragment stormtrooper on Stormtrooper { name, height, specialization, __typename } "); + "{ heros { ...stormtrooper, ... on Human {name}, ... on Droid {name}, __typename}}, fragment stormtrooper on Stormtrooper { name, height, specialization, __typename } "); Test.DeepEquals( results, "{ heros: [ " + - "{ __typename: 'Human', name: 'Han Solo'}, " + - "{ name: 'FN-2187', height: 4.9, specialization: 'Imperial Snowtrooper', __typename: 'Stormtrooper'}, " + - "{ __typename: 'Droid', name: 'R2-D2'} ] }" + "{ name: 'Han Solo', __typename: 'Human' }, " + + "{ name: 'FN-2187', height: 4.9, specialization: 'Imperial Snowtrooper', __typename: 'Stormtrooper' }, " + + "{ name: 'R2-D2', __typename: 'Droid' } ] }" ); } } diff --git a/Tests/InMemoryExecutionTests.cs b/Tests/InMemoryExecutionTests.cs index 3ff7d0b..42a4b82 100644 --- a/Tests/InMemoryExecutionTests.cs +++ b/Tests/InMemoryExecutionTests.cs @@ -32,15 +32,15 @@ public class InMemoryExecutionTests [Test] public void ByteArrayParameter() => GenericTests.ByteArrayParameter(MemContext.CreateDefaultContext()); [Test] public void ChildListFieldWithParameters() => GenericTests.ChildListFieldWithParameters(MemContext.CreateDefaultContext()); [Test] public void ChildFieldWithParameters() => GenericTests.ChildFieldWithParameters(MemContext.CreateDefaultContext()); - [Test] public static void Fragements() => GenericTests.Fragements(MemContext.CreateDefaultContext()); - [Test] public static void InlineFragements() => GenericTests.InlineFragements(MemContext.CreateDefaultContext()); - [Test] public static void InlineFragementWithListField() => GenericTests.InlineFragementWithListField(MemContext.CreateDefaultContext()); - [Test] public static void FragementWithMultiLevelInheritance() => GenericTests.FragementWithMultiLevelInheritance(MemContext.CreateDefaultContext()); - [Test] public static void InlineFragementWithoutTypenameField() => GenericTests.InlineFragementWithoutTypenameField(MemContext.CreateDefaultContext()); - [Test] public static void FragementWithoutTypenameField() => GenericTests.FragementWithoutTypenameField(MemContext.CreateDefaultContext()); - [Test] public static void InlineFragementWithoutTypenameFieldWithoutOtherFields() => GenericTests.InlineFragementWithoutTypenameFieldWithoutOtherFields(MemContext.CreateDefaultContext()); - [Test] public static void FragementWithMultipleTypenameFields() => GenericTests.FragementWithMultipleTypenameFields(MemContext.CreateDefaultContext()); - [Test] public static void FragementWithMultipleTypenameFieldsMixedWithInlineFragment() => GenericTests.FragementWithMultipleTypenameFieldsMixedWithInlineFragment(MemContext.CreateDefaultContext()); + [Test] public static void Fragments() => GenericTests.Fragments(MemContext.CreateDefaultContext()); + [Test] public static void InlineFragments() => GenericTests.InlineFragments(MemContext.CreateDefaultContext()); + [Test] public static void InlineFragmentWithListField() => GenericTests.InlineFragmentWithListField(MemContext.CreateDefaultContext()); + [Test] public static void FragmentWithMultiLevelInheritance() => GenericTests.FragmentWithMultiLevelInheritance(MemContext.CreateDefaultContext()); + [Test] public static void InlineFragmentWithoutTypenameField() => GenericTests.InlineFragmentWithoutTypenameField(MemContext.CreateDefaultContext()); + [Test] public static void FragmentWithoutTypenameField() => GenericTests.FragmentWithoutTypenameField(MemContext.CreateDefaultContext()); + [Test] public static void InlineFragmentWithoutTypenameFieldWithoutOtherFields() => GenericTests.InlineFragmentWithoutTypenameFieldWithoutOtherFields(MemContext.CreateDefaultContext()); + [Test] public static void FragmentWithMultipleTypenameFields() => GenericTests.FragmentWithMultipleTypenameFields(MemContext.CreateDefaultContext()); + [Test] public static void FragmentWithMultipleTypenameFieldsMixedWithInlineFragment() => GenericTests.FragmentWithMultipleTypenameFieldsMixedWithInlineFragment(MemContext.CreateDefaultContext()); [Test] public void AddAllFields() diff --git a/Tests/MemContext.cs b/Tests/MemContext.cs index 2e1fe9a..80bf233 100644 --- a/Tests/MemContext.cs +++ b/Tests/MemContext.cs @@ -78,7 +78,7 @@ public MemContext() { Id = 1, Name = "Millennium falcon", - OwnerId = human.Id + HumanId = human.Id }; Vehicles.Add(vehicle); human.Vehicles = new List { vehicle }; @@ -86,7 +86,7 @@ public MemContext() { Id = 2, Name = "Speeder bike", - OwnerId = stormtrooper.Id + HumanId = stormtrooper.Id }; Vehicles.Add(vehicle2); stormtrooper.Vehicles = new List { vehicle2 }; @@ -96,8 +96,8 @@ public MemContext() public List Accounts { get; set; } = new List(); public List MutateMes { get; set; } = new List(); public List NullRefs { get; set; } = new List(); - public List Heros { get; set; } = new List(); - public List Vehicles { get; set; } = new List(); + List Heros { get; set; } = new List(); + List Vehicles { get; set; } = new List(); public static GraphQL CreateDefaultContext() { @@ -203,6 +203,7 @@ private static void InitializeCharacterSchema(GraphQLSchema schema) { var characterInterface = schema.AddInterfaceType(); characterInterface.AddAllFields(); + var humanInterface = schema.AddInterfaceType(); humanInterface.AddAllFields(); @@ -220,10 +221,22 @@ private static void InitializeCharacterSchema(GraphQLSchema schema) droidType.AddAllFields(); droidType.AddInterface(characterInterface); - schema.AddType().AddAllFields(); + var characterUnionType = schema.AddUnionType( + "Hero", + new[] + { + // TODO: ORDER MATTERS FOR TYPENAME RESOLUTION + characterInterface.GraphQLType, + humanInterface.GraphQLType, + humanType.GraphQLType, + stormtrooperType.GraphQLType, + droidType.GraphQLType + }); - schema.AddField("hero", new { id = 0 }, (db, args) => db.Heros.AsQueryable().SingleOrDefault(h => h.Id == args.id)); - schema.AddListField("heros", db => db.Heros.AsQueryable()); + schema.AddType().AddAllFields(); + schema.AddField("hero", new { id = 0 }, (db, args) => db.Heros.SingleOrDefault(h => h.Id == args.id)); + schema.AddListField("heros", db => db.Heros.AsQueryable()) + .WithReturnType(characterUnionType); } } @@ -290,46 +303,45 @@ public class NullRef public int Id { get; set; } } - public interface ICharacter + interface ICharacter { int Id { get; set; } string Name { get; set; } } - public interface IHuman + class Hero : ICharacter + { + public int Id { get; set; } + public string Name { get; set; } + } + + interface IHuman { double Height { get; set; } ICollection Vehicles { get; set; } } - public class Human : ICharacter, IHuman + class Human : Hero, IHuman { - public int Id { get; set; } - public string Name { get; set; } public double Height { get; set; } public ICollection Vehicles { get; set; } } - public class Stormtrooper : ICharacter, IHuman + class Stormtrooper : Human { - public int Id { get; set; } - public string Name { get; set; } - public double Height { get; set; } public string Specialization { get; set; } - public ICollection Vehicles { get; set; } } - public class Droid : ICharacter + class Droid : Hero { - public int Id { get; set; } - public string Name { get; set; } public string PrimaryFunction { get; set; } } - public class Vehicle + class Vehicle { public int Id { get; set; } public string Name { get; set; } - public int OwnerId { get; set; } + public int HumanId { get; set; } + public virtual IHuman Human { get; set; } } } \ No newline at end of file From eb6a0fff737e4ba1ae800dcea99cadc741597f13 Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Thu, 6 Jul 2017 13:03:38 +0200 Subject: [PATCH 04/33] [WIP]: fix handling of type conditions on non-abstract types. #47 --- GraphQL.Net/DynamicTypeBuilder.cs | 7 +++--- GraphQL.Net/Executor.cs | 30 ++++++++++++++--------- GraphQL.Net/GraphQLSchema.cs | 11 +++++++-- Tests.EF/EntityFrameworkExecutionTests.cs | 3 ++- Tests/MemContext.cs | 7 +++--- 5 files changed, 38 insertions(+), 20 deletions(-) diff --git a/GraphQL.Net/DynamicTypeBuilder.cs b/GraphQL.Net/DynamicTypeBuilder.cs index 2f0edda..6c110f6 100644 --- a/GraphQL.Net/DynamicTypeBuilder.cs +++ b/GraphQL.Net/DynamicTypeBuilder.cs @@ -30,8 +30,9 @@ public static Type CreateDynamicType(string name, IEnumerable fiel CreateProperty(typeBuilder, prop.Key, prop.Value); return typeBuilder.CreateType(); } - - public static Type CreateDynamicUnionTypeOrInterface(string name, IEnumerable fields, IEnumerable possibleTypes) + + public static Type CreateDynamicUnionTypeOrInterface(string name, IEnumerable fields, + IEnumerable possibleTypes, Func createPossibleTypePropertyName) { var typeBuilder = ModuleBuilder.DefineType(AssemblyName + "." + name, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | @@ -40,7 +41,7 @@ public static Type CreateDynamicUnionTypeOrInterface(string name, IEnumerable t.Fields.Select(f => new {Name = t.Name + "$$$" + f.Name, Type = GetFieldPropertyType(f)})); + t => t.Fields.Select(f => new {Name = createPossibleTypePropertyName(t.Name, f.Name), Type = GetFieldPropertyType(f)})); var properties = subTypeProperties.Concat(fields.Select(f => new {Name = f.Name, Type = GetFieldPropertyType(f)})); foreach (var prop in properties) diff --git a/GraphQL.Net/Executor.cs b/GraphQL.Net/Executor.cs index 9e80f66..5574077 100644 --- a/GraphQL.Net/Executor.cs +++ b/GraphQL.Net/Executor.cs @@ -105,7 +105,7 @@ private static IDictionary MapResults(object queryObject, IEnume var type = queryObject.GetType(); // TODO: Improve performance/efficiency - var graphQlQueryType = schema.Types.First(t => t.QueryType == queryObject.GetType()); + var graphQlQueryType = schema.GetGQLTypeByQueryType(queryObject.GetType()); var queryTypeToSelections = CreateQueryTypeToSelectionsMapping(graphQlQueryType, selections.ToList()); if (!queryTypeToSelections.ContainsKey(type)) @@ -118,9 +118,8 @@ private static IDictionary MapResults(object queryObject, IEnume var key = map.Name; var field = map.SchemaField.Field(); var typeConditionType = map.TypeCondition?.Value?.Type(); - var propertyName = (typeConditionType != null - ? typeConditionType.Name + "$$$" // TODO: Extract utility method - : "") + field.Name; + var propertyName = CreatePropertyName(graphQlQueryType, typeConditionType, field.Name); + object obj = null; if (field.IsPost) @@ -322,22 +321,31 @@ private static ConditionalExpression NullPropagate(Expression baseExpr, Expressi return Expression.Condition(equals, Expression.Constant(null, returnExpr.Type), returnExpr); } + private static string CreatePropertyName(IGraphQLType graphQlType, IGraphQLType typeConditionTypeName, + string fieldName) + { + var isAbstractType = graphQlType.TypeKind == TypeKind.UNION || + graphQlType.TypeKind == TypeKind.INTERFACE; + return isAbstractType && typeConditionTypeName != null + ? GraphQLSchema.CreatePossibleTypePropertyName(typeConditionTypeName.Name, fieldName) + : fieldName; + } + private static MemberBinding GetBinding(GraphQLSchema schema, ExecSelection map, Type toType, Expression baseBindingExpr, ExpressionOptions options) { var field = map.SchemaField.Field(); var needsTypeCheck = baseBindingExpr.Type != map.SchemaField.DeclaringType?.Info?.Type?.CLRType; - var isTypeConditionSelection = map.TypeCondition?.Value != null; - var propertyName = (isTypeConditionSelection ? map.TypeCondition?.Value.Type().Name + "$$$" : "") + - map.SchemaField.FieldName; - + // TODO: Pass GraphQlType as argument + var graphQlType = schema.GetGQLTypeByQueryType(toType); + var propertyName = CreatePropertyName(graphQlType, map.TypeCondition?.Value?.Type(), + map.SchemaField.FieldName); var toMember = toType.GetProperty( propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); - + if (toMember == null) { - var graphqlType = schema.GetGQLType(toType); - throw new Exception($"The field '{map.SchemaField.FieldName}' does not exist on type '{graphqlType.Name}'."); + throw new Exception($"The field '{map.SchemaField.FieldName}' does not exist on type '{graphQlType.Name}'."); } // expr is form of: (context, entity) => entity.Field var expr = field.GetExpression(map.Arguments.Values()); diff --git a/GraphQL.Net/GraphQLSchema.cs b/GraphQL.Net/GraphQLSchema.cs index 03f591e..07645fa 100644 --- a/GraphQL.Net/GraphQLSchema.cs +++ b/GraphQL.Net/GraphQLSchema.cs @@ -187,8 +187,10 @@ private static void CompleteType(GraphQLType type) var allFields = type.GetQueryFields(); if (type.TypeKind == TypeKind.INTERFACE || type.TypeKind == TypeKind.UNION) { - type.QueryType = DynamicTypeBuilder.CreateDynamicUnionTypeOrInterface(type.Name + Guid.NewGuid(), - allFields, type.PossibleTypes); + type.QueryType = DynamicTypeBuilder.CreateDynamicUnionTypeOrInterface( + type.Name + Guid.NewGuid(), + allFields, type.PossibleTypes, + CreatePossibleTypePropertyName); } else { @@ -391,5 +393,10 @@ internal override GraphQLType GetGQLTypeByQueryType(Type queryType) new GraphQLType(queryType) {TypeKind = TypeKind.SCALAR}; internal IEnumerable Types => _types; + + public static string CreatePossibleTypePropertyName(string possibleTypeName, string fieldName) + { + return possibleTypeName + "$$$" + fieldName; + } } } diff --git a/Tests.EF/EntityFrameworkExecutionTests.cs b/Tests.EF/EntityFrameworkExecutionTests.cs index 7e961f7..0a63bfe 100644 --- a/Tests.EF/EntityFrameworkExecutionTests.cs +++ b/Tests.EF/EntityFrameworkExecutionTests.cs @@ -225,7 +225,8 @@ private static void InitializeCharacterSchema(GraphQLSchema schema) schema.AddUnionType("OtherUnionType02", new List()); schema.AddType().AddAllFields(); - schema.AddField("hero", new {id = 0}, (db, args) => db.Heros.SingleOrDefault(h => h.Id == args.id)); + schema.AddField("hero", new {id = 0}, (db, args) => db.Heros.FirstOrDefault(h => h.Id == args.id)) + .WithReturnType(heroUnionType); schema.AddListField("heros", db => db.Heros.AsQueryable()) .WithReturnType(heroUnionType); } diff --git a/Tests/MemContext.cs b/Tests/MemContext.cs index 80bf233..d050015 100644 --- a/Tests/MemContext.cs +++ b/Tests/MemContext.cs @@ -221,7 +221,7 @@ private static void InitializeCharacterSchema(GraphQLSchema schema) droidType.AddAllFields(); droidType.AddInterface(characterInterface); - var characterUnionType = schema.AddUnionType( + var heroUnionType = schema.AddUnionType( "Hero", new[] { @@ -234,9 +234,10 @@ private static void InitializeCharacterSchema(GraphQLSchema schema) }); schema.AddType().AddAllFields(); - schema.AddField("hero", new { id = 0 }, (db, args) => db.Heros.SingleOrDefault(h => h.Id == args.id)); + schema.AddField("hero", new { id = 0 }, (db, args) => db.Heros.SingleOrDefault(h => h.Id == args.id)) + .WithReturnType(heroUnionType); schema.AddListField("heros", db => db.Heros.AsQueryable()) - .WithReturnType(characterUnionType); + .WithReturnType(heroUnionType); } } From 28bd998197ddc2ab12d626cd850050c9ba11e36c Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Thu, 6 Jul 2017 14:02:34 +0200 Subject: [PATCH 05/33] Clean up. --- GraphQL.Net/Executor.cs | 19 ++++++++----------- GraphQL.Net/GraphQLField.cs | 2 +- GraphQL.Net/GraphQLFieldBuilder.cs | 3 +-- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/GraphQL.Net/Executor.cs b/GraphQL.Net/Executor.cs index 5574077..6d98f8c 100644 --- a/GraphQL.Net/Executor.cs +++ b/GraphQL.Net/Executor.cs @@ -103,8 +103,6 @@ private static IDictionary MapResults(object queryObject, IEnume return null; var dict = new Dictionary(); var type = queryObject.GetType(); - - // TODO: Improve performance/efficiency var graphQlQueryType = schema.GetGQLTypeByQueryType(queryObject.GetType()); var queryTypeToSelections = CreateQueryTypeToSelectionsMapping(graphQlQueryType, selections.ToList()); @@ -232,7 +230,7 @@ private static IDictionary MapResults(object queryObject, IEnume private static LambdaExpression GetSelector(GraphQLSchema schema, GraphQLType gqlType, IEnumerable> selections, ExpressionOptions options) { var parameter = Expression.Parameter(gqlType.CLRType, "p"); - var init = GetMemberInit(schema, gqlType.QueryType, selections, parameter, options); + var init = GetMemberInit(schema, gqlType, selections, parameter, options); return Expression.Lambda(init, parameter); } @@ -267,8 +265,9 @@ private static IDictionary>> CreateQueryTy ); } - private static ConditionalExpression GetMemberInit(GraphQLSchema schema, Type queryType, IEnumerable> selectionsEnumerable, Expression parameterExpression, ExpressionOptions options) + private static ConditionalExpression GetMemberInit(GraphQLSchema schema, GraphQLType graphQlType, IEnumerable> selectionsEnumerable, Expression parameterExpression, ExpressionOptions options) { + var queryType = graphQlType.QueryType; // Avoid possible multiple enumeration of selections-enumerable var selections = selectionsEnumerable as IList> ?? selectionsEnumerable.ToList(); @@ -281,13 +280,12 @@ private static ConditionalExpression GetMemberInit(GraphQLSchema schem var bindings = selections.Where(s => !s.SchemaField.Info.Field.IsPost) - .Select(s => GetBinding(schema, s, queryType, parameterExpression, options)); + .Select(s => GetBinding(schema, s, graphQlType, parameterExpression, options)); // Add selection for '__typename'-field of the proper type if (typeConditionButNoTypeNameSelection || typeNameConditionHasToBeReplaced) { // Find the base types' `__typename` field - var graphQlType = schema.GetGQLTypeByQueryType(queryType); var typeNameField = graphQlType?.Fields.Find(f => f.Name == "__typename"); if (typeNameField != null && !typeNameField.IsPost) { @@ -306,7 +304,7 @@ private static ConditionalExpression GetMemberInit(GraphQLSchema schem bindings .Where(b => b.Member.Name != "__typename") .Concat(new[] - {GetBinding(schema, typeNameExecSelection, queryType, parameterExpression, options)}) + {GetBinding(schema, typeNameExecSelection, graphQlType, parameterExpression, options)}) .ToList(); } } @@ -331,12 +329,11 @@ private static string CreatePropertyName(IGraphQLType graphQlType, IGraphQLType : fieldName; } - private static MemberBinding GetBinding(GraphQLSchema schema, ExecSelection map, Type toType, Expression baseBindingExpr, ExpressionOptions options) + private static MemberBinding GetBinding(GraphQLSchema schema, ExecSelection map, GraphQLType graphQlType, Expression baseBindingExpr, ExpressionOptions options) { + var toType = graphQlType.QueryType; var field = map.SchemaField.Field(); var needsTypeCheck = baseBindingExpr.Type != map.SchemaField.DeclaringType?.Info?.Type?.CLRType; - // TODO: Pass GraphQlType as argument - var graphQlType = schema.GetGQLTypeByQueryType(toType); var propertyName = CreatePropertyName(graphQlType, map.TypeCondition?.Value?.Type(), map.SchemaField.FieldName); var toMember = toType.GetProperty( @@ -391,7 +388,7 @@ private static MemberBinding GetBinding(GraphQLSchema schema, ExecSele var bindChildrenTo = map.SchemaField.Field().IsList ? listParameter : replacedContext; // Now that we have our new binding parameter, build the tree for the rest of the children - var memberInit = GetMemberInit(schema, field.Type.QueryType, map.Selections.Values(), bindChildrenTo, options); + var memberInit = GetMemberInit(schema, field.Type, map.Selections.Values(), bindChildrenTo, options); // For single entities, we're done and we can just bind to the memberInit expression if (!field.IsList) diff --git a/GraphQL.Net/GraphQLField.cs b/GraphQL.Net/GraphQLField.cs index b0c533d..12c6b89 100644 --- a/GraphQL.Net/GraphQLField.cs +++ b/GraphQL.Net/GraphQLField.cs @@ -35,7 +35,7 @@ internal class GraphQLField private GraphQLType _type; public GraphQLType Type => _type ?? (_type = Schema.GetGQLType(FieldCLRType)); - //TODO: Necessary for union types - find better solution + // Set return type, e.g. union types. internal void SetReturnType(GraphQLType type) { _type = type; diff --git a/GraphQL.Net/GraphQLFieldBuilder.cs b/GraphQL.Net/GraphQLFieldBuilder.cs index fa7e079..22319f3 100644 --- a/GraphQL.Net/GraphQLFieldBuilder.cs +++ b/GraphQL.Net/GraphQLFieldBuilder.cs @@ -29,8 +29,7 @@ public GraphQLFieldBuilder WithReturnType(IGraphQLType type) _field.SetReturnType(type as GraphQLType); return this; } - - // TODO: This should be removed once we figure out a better way to do it + internal GraphQLFieldBuilder WithResolutionType(ResolutionType type) { _field.ResolutionType = type; From 64cddcf658d5773c1c6ea1ed333b10cc20905b08 Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Thu, 6 Jul 2017 14:09:51 +0200 Subject: [PATCH 06/33] Increase max recursion depth to support full introspection query. --- GraphQL.Parser/Schema/SchemaResolver.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GraphQL.Parser/Schema/SchemaResolver.fs b/GraphQL.Parser/Schema/SchemaResolver.fs index 8fe2bf6..e9b621c 100644 --- a/GraphQL.Parser/Schema/SchemaResolver.fs +++ b/GraphQL.Parser/Schema/SchemaResolver.fs @@ -115,7 +115,7 @@ type Resolver<'s> , recursionDepth : int , fragmentContext : string list ) = - static let maxRecursionDepth = 10 // should be plenty for real queries + static let maxRecursionDepth = 20 // should be plenty for real queries member private __.ResolveArguments ( schemaArgs : IReadOnlyDictionary> , pargs : ParserAST.Argument WithSource seq From 32ff5de6883d3dbfb7f25064f6ecf350356f4a86 Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Thu, 6 Jul 2017 14:10:02 +0200 Subject: [PATCH 07/33] Update uni test for introspection. --- Tests/IntrospectionTests.cs | 90 +++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/Tests/IntrospectionTests.cs b/Tests/IntrospectionTests.cs index 096c399..c6f2ba0 100644 --- a/Tests/IntrospectionTests.cs +++ b/Tests/IntrospectionTests.cs @@ -320,82 +320,96 @@ public void FullIntrospectionQuery() var results = gql.ExecuteQuery( @"query IntrospectionQuery { __schema { - queryType { name } - mutationType { name } - subscriptionType { name } - types { + queryType { name } + mutationType { name } + subscriptionType { name } + types { ...FullType - } - directives { + } + directives { name description locations args { - ...InputValue + ...InputValue } - } - } + } } - fragment FullType on __Type { + } + fragment FullType on __Type { kind name description fields(includeDeprecated: true) { - name - description - args { + name + description + args { ...InputValue - } - type { + } + type { ...TypeRef - } - isDeprecated - deprecationReason + } + isDeprecated + deprecationReason } inputFields { - ...InputValue + ...InputValue } interfaces { - ...TypeRef + ...TypeRef } enumValues(includeDeprecated: true) { - name - description - isDeprecated - deprecationReason + name + description + isDeprecated + deprecationReason } possibleTypes { - ...TypeRef - } + ...TypeRef } - fragment InputValue on __InputValue { + } + fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue - } - fragment TypeRef on __Type { + } + 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 + ofType { + kind + name + } + } + } + } } - } - } + } } + } " ); - Test.DeepEquals( - results, - @"{}" - ); + // Must not throw + // TODO: Add assertions } } } From a21cd33974d04a753c886cd4a4cdfa2859ea83d5 Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Tue, 22 Aug 2017 20:10:31 +0200 Subject: [PATCH 08/33] [WIP] Contiune work in on full introspection support and fix issues related to type conditions in queries. #47 --- GraphQL.Net/DynamicTypeBuilder.cs | 13 ++++++-- GraphQL.Net/Executor.cs | 34 +++++++++++++------- GraphQL.Net/GraphQL.cs | 5 +++ GraphQL.Net/GraphQLField.cs | 2 +- GraphQL.Net/GraphQLFieldBuilder.cs | 5 +++ GraphQL.Net/GraphQLSchema.cs | 9 +++--- GraphQL.Net/SchemaAdapters/SchemaField.cs | 8 +++-- GraphQL.Net/SchemaAdapters/SchemaRootType.cs | 2 +- GraphQL.Parser.Test/SchemaTest.fs | 2 ++ GraphQL.Parser/Integration/CS.fs | 2 ++ GraphQL.Parser/Schema/SchemaAST.fs | 1 + GraphQL.Parser/SchemaTools/Introspection.fs | 7 +++- 12 files changed, 67 insertions(+), 23 deletions(-) diff --git a/GraphQL.Net/DynamicTypeBuilder.cs b/GraphQL.Net/DynamicTypeBuilder.cs index 6c110f6..450296d 100644 --- a/GraphQL.Net/DynamicTypeBuilder.cs +++ b/GraphQL.Net/DynamicTypeBuilder.cs @@ -24,8 +24,17 @@ public static Type CreateDynamicType(string name, IEnumerable fiel { var typeBuilder = ModuleBuilder.DefineType(AssemblyName + "." + name, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.Serializable | TypeAttributes.BeforeFieldInit); - - var properties = ConvertFieldsToProperties(fields); + + var graphQlFields = fields as GraphQLField[] ?? fields.ToArray(); + if (graphQlFields.Count() != graphQlFields.Select(f => f.Name).Distinct().Count()) + { + var firstDuplicatedFieldName = graphQlFields.GroupBy(f => f.Name) + .Where(g => g.Count() > 1) + .Select(g => g.Key) + .FirstOrDefault(); + throw new Exception("Duplicated field name '" + firstDuplicatedFieldName + "' on type '" + name + "'."); + } + var properties = ConvertFieldsToProperties(graphQlFields); foreach (var prop in properties) CreateProperty(typeBuilder, prop.Key, prop.Value); return typeBuilder.CreateType(); diff --git a/GraphQL.Net/Executor.cs b/GraphQL.Net/Executor.cs index 6d98f8c..54190d1 100644 --- a/GraphQL.Net/Executor.cs +++ b/GraphQL.Net/Executor.cs @@ -23,7 +23,13 @@ public static object Execute // sniff queryable provider to determine how selector should be built var dummyQuery = replaced.Compile().DynamicInvoke(context, null); + var queryType = dummyQuery.GetType(); + if (queryType.Namespace == "System.Data.Entity.DynamicProxies") + { + queryType = queryType.BaseType; + } + var queryExecSelections = query.Selections.Values(); var selector = GetSelector(schema, field.Type, queryExecSelections, schema.GetOptionsForQueryable(queryType)); @@ -116,7 +122,7 @@ private static IDictionary MapResults(object queryObject, IEnume var key = map.Name; var field = map.SchemaField.Field(); var typeConditionType = map.TypeCondition?.Value?.Type(); - var propertyName = CreatePropertyName(graphQlQueryType, typeConditionType, field.Name); + var propertyName = field.Name == "__typename" ? field.Name : CreatePropertyName(graphQlQueryType, typeConditionType, field.Name); object obj = null; @@ -277,10 +283,15 @@ private static ConditionalExpression GetMemberInit(GraphQLSchema schem // Any '__typename' selection have to be replaced by the '__typename' selection of the target type' '__typename'-field. var typeNameConditionHasToBeReplaced = selections.Any(s => s.Name == "__typename"); - - var bindings = + + var bindingsWithPossibleDuplicates = selections.Where(s => !s.SchemaField.Info.Field.IsPost) - .Select(s => GetBinding(schema, s, graphQlType, parameterExpression, options)); + .Where(s => s.Name != "__typename") + .Select(s => GetBinding(schema, s, graphQlType, parameterExpression, options)) + .ToList(); + var uniqueBindingMemberNames = bindingsWithPossibleDuplicates.Select(b => b.Member.Name).Distinct(); + var bindings = + uniqueBindingMemberNames.Select(name => bindingsWithPossibleDuplicates.First(b => b.Member.Name == name)); // Add selection for '__typename'-field of the proper type if (typeConditionButNoTypeNameSelection || typeNameConditionHasToBeReplaced) @@ -302,8 +313,8 @@ private static ConditionalExpression GetMemberInit(GraphQLSchema schem ); bindings = bindings - .Where(b => b.Member.Name != "__typename") - .Concat(new[] + .Where(b => b.Member.Name != "__typename") + .Concat(new[] {GetBinding(schema, typeNameExecSelection, graphQlType, parameterExpression, options)}) .ToList(); } @@ -319,13 +330,14 @@ private static ConditionalExpression NullPropagate(Expression baseExpr, Expressi return Expression.Condition(equals, Expression.Constant(null, returnExpr.Type), returnExpr); } - private static string CreatePropertyName(IGraphQLType graphQlType, IGraphQLType typeConditionTypeName, + private static string CreatePropertyName(IGraphQLType graphQlType, IGraphQLType typeConditionType, string fieldName) { - var isAbstractType = graphQlType.TypeKind == TypeKind.UNION || - graphQlType.TypeKind == TypeKind.INTERFACE; - return isAbstractType && typeConditionTypeName != null - ? GraphQLSchema.CreatePossibleTypePropertyName(typeConditionTypeName.Name, fieldName) + var isObjectAbstractType = graphQlType.TypeKind == TypeKind.UNION || + graphQlType.TypeKind == TypeKind.INTERFACE; + var isTypeConditionInterface = typeConditionType?.TypeKind == TypeKind.INTERFACE; + return isObjectAbstractType && !isTypeConditionInterface && typeConditionType != null + ? GraphQLSchema.CreatePossibleTypePropertyName(typeConditionType.Name, fieldName) : fieldName; } diff --git a/GraphQL.Net/GraphQL.cs b/GraphQL.Net/GraphQL.cs index 3c6e37f..d6299f7 100644 --- a/GraphQL.Net/GraphQL.cs +++ b/GraphQL.Net/GraphQL.cs @@ -23,6 +23,11 @@ public static GraphQLSchema CreateDefaultSchema(Func creatio return Schema = new GraphQLSchema(creationFunc); } + public EnumValue ResolveEnumValue(string name) + { + return _schema.VariableTypes.ResolveEnumValue(name); + } + public static IDictionary Execute(string query) { var gql = new GraphQL(); diff --git a/GraphQL.Net/GraphQLField.cs b/GraphQL.Net/GraphQLField.cs index 12c6b89..6d7400c 100644 --- a/GraphQL.Net/GraphQLField.cs +++ b/GraphQL.Net/GraphQLField.cs @@ -12,7 +12,7 @@ internal class GraphQLField { public string Name { get; protected set; } public string Description { get; set; } - public bool IsList { get; protected set; } + public bool IsList { get; internal set; } public bool IsPost { get; protected set; } public Func PostFieldFunc { get; protected set; } diff --git a/GraphQL.Net/GraphQLFieldBuilder.cs b/GraphQL.Net/GraphQLFieldBuilder.cs index 22319f3..db88591 100644 --- a/GraphQL.Net/GraphQLFieldBuilder.cs +++ b/GraphQL.Net/GraphQLFieldBuilder.cs @@ -33,6 +33,11 @@ public GraphQLFieldBuilder WithReturnType(IGraphQLType type) internal GraphQLFieldBuilder WithResolutionType(ResolutionType type) { _field.ResolutionType = type; + + if (_field.IsList && (type == ResolutionType.First || type == ResolutionType.FirstOrDefault)) + { + _field.IsList = false; + } return this; } } diff --git a/GraphQL.Net/GraphQLSchema.cs b/GraphQL.Net/GraphQLSchema.cs index 07645fa..cf6d2a7 100644 --- a/GraphQL.Net/GraphQLSchema.cs +++ b/GraphQL.Net/GraphQLSchema.cs @@ -22,7 +22,6 @@ public class GraphQLSchema : GraphQLSchema private readonly List _types = new List(); private readonly List _expressionOptions = new List(); internal bool Completed; - public static readonly ParameterExpression DbParam = Expression.Parameter(typeof(TContext), "db"); public GraphQLSchema() @@ -35,7 +34,7 @@ public GraphQLSchema(Func contextCreator) : this() { ContextCreator = contextCreator; } - + public void AddEnum(string name = null, string prefix = null) where TEnum : struct // wish we could do where TEnum : Enum => VariableTypes.AddType(_ => TypeHandler.Enum(name ?? typeof(TEnum).Name, prefix ?? "")); @@ -78,13 +77,13 @@ public GraphQLTypeBuilder AddType(string name = null return new GraphQLTypeBuilder(this, gqlType); } - public IGraphQLType AddUnionType(string name, IEnumerable possibleTypes, + public IGraphQLType AddUnionType(string name, IEnumerable possibleTypes, Type type= null, string description = null) { if (_types.Any(t => t.Name == name)) throw new ArgumentException("Union type with same name has already been added: " + name); - var gqlType = new GraphQLType(typeof(object)) + var gqlType = new GraphQLType(type ?? typeof(object)) { TypeKind = TypeKind.UNION, Name = name, @@ -206,7 +205,7 @@ private void AddDefaultTypes() ischema.AddListField("types", s => s.Types); ischema.AddField("queryType", s => s.QueryType); ischema.AddField("mutationType", s => s.MutationType.OrDefault()); - ischema.AddField("subscriptionType", s => s.MutationType.OrDefault()); + ischema.AddField("subscriptionType", s => s.SubscriptionType.OrDefault()); ischema.AddListField("directives", s => s.Directives); var itype = AddType("__Type"); diff --git a/GraphQL.Net/SchemaAdapters/SchemaField.cs b/GraphQL.Net/SchemaAdapters/SchemaField.cs index 646e282..b6f955c 100644 --- a/GraphQL.Net/SchemaAdapters/SchemaField.cs +++ b/GraphQL.Net/SchemaAdapters/SchemaField.cs @@ -19,9 +19,12 @@ public SchemaField(ISchemaQueryType declaringType, GraphQLField field, Sch if (_field.Type.TypeKind == TypeKind.SCALAR) { var varType = _schema.GraphQLSchema.VariableTypes.VariableTypeOf(_field.Type.CLRType); + if (varType?.Type == null) + { + throw new Exception("Field has unknown return type. " + declaringType.TypeName + "." + _field.Name); + } FieldType = SchemaFieldType.NewValueField(varType); - } - else + } else { FieldType = SchemaFieldType.NewQueryField(_schema.OfType(_field.Type));; } @@ -33,6 +36,7 @@ public SchemaField(ISchemaQueryType declaringType, GraphQLField field, Sch public override string FieldName => _field.Name; public override string Description => _field.Description; + public override bool IsList => _field.IsList; public override Info Info => new Info(_field); public override IReadOnlyDictionary> Arguments { get; } public override Complexity EstimateComplexity(IEnumerable> args) diff --git a/GraphQL.Net/SchemaAdapters/SchemaRootType.cs b/GraphQL.Net/SchemaAdapters/SchemaRootType.cs index 81b0385..dffe7c0 100644 --- a/GraphQL.Net/SchemaAdapters/SchemaRootType.cs +++ b/GraphQL.Net/SchemaAdapters/SchemaRootType.cs @@ -16,7 +16,7 @@ public SchemaRootType(Schema schema, GraphQLType baseQueryType) } public override IReadOnlyDictionary> Fields { get; } - public override string TypeName => "SchemaRoot"; + public override string TypeName => "queryType"; public override IEnumerable> PossibleTypes => new Collection>(); public override IEnumerable> Interfaces => new Collection>(); diff --git a/GraphQL.Parser.Test/SchemaTest.fs b/GraphQL.Parser.Test/SchemaTest.fs index 72537b9..f717116 100644 --- a/GraphQL.Parser.Test/SchemaTest.fs +++ b/GraphQL.Parser.Test/SchemaTest.fs @@ -49,6 +49,7 @@ type UserType() = member __.DeclaringType = upcast this member __.FieldType = fieldType member __.FieldName = name + member __.IsList = false member __.Description = Some ("Description of " + name) member __.Info = "Info for " + name member __.Arguments = args |> dictionary :> _ @@ -86,6 +87,7 @@ type RootType() = member __.DeclaringType = upcast this member __.FieldType = fieldType member __.FieldName = name + member __.IsList = false member __.Description = Some ("Description of " + name) member __.Info = "Info for " + name member __.Arguments = args |> dictionary :> _ diff --git a/GraphQL.Parser/Integration/CS.fs b/GraphQL.Parser/Integration/CS.fs index 6d552b3..23b1b83 100644 --- a/GraphQL.Parser/Integration/CS.fs +++ b/GraphQL.Parser/Integration/CS.fs @@ -69,6 +69,7 @@ type SchemaFieldCS<'s>() = abstract member DeclaringType : ISchemaQueryType<'s> abstract member FieldType : SchemaFieldType<'s> abstract member FieldName : string + abstract member IsList : bool abstract member Description : string default this.Description = null abstract member Info : 's @@ -81,6 +82,7 @@ type SchemaFieldCS<'s>() = member this.DeclaringType = this.DeclaringType member this.FieldType = this.FieldType member this.FieldName = this.FieldName + member this.IsList = this.IsList member this.Description = this.Description |> obj2option member this.Info = this.Info member this.Arguments = this.Arguments diff --git a/GraphQL.Parser/Schema/SchemaAST.fs b/GraphQL.Parser/Schema/SchemaAST.fs index db932ed..c3a82a3 100644 --- a/GraphQL.Parser/Schema/SchemaAST.fs +++ b/GraphQL.Parser/Schema/SchemaAST.fs @@ -140,6 +140,7 @@ and ISchemaField<'s> = abstract member DeclaringType : ISchemaQueryType<'s> abstract member FieldType : SchemaFieldType<'s> abstract member FieldName : string + abstract member IsList : bool abstract member Description : string option /// Get the possible arguments of this field, keyed by name. /// May be empty if the field accepts no arguments. diff --git a/GraphQL.Parser/SchemaTools/Introspection.fs b/GraphQL.Parser/SchemaTools/Introspection.fs index 5e477c3..629fd46 100644 --- a/GraphQL.Parser/SchemaTools/Introspection.fs +++ b/GraphQL.Parser/SchemaTools/Introspection.fs @@ -132,7 +132,12 @@ and IntroField = } static member Of(field : ISchemaField<'s>) = let args = field.Arguments.Values |> Seq.map IntroInputValue.Of - let ty = IntroType.Of(field.FieldType) + let ty = if field.IsList then + { IntroType.Default with + Kind = TypeKind.LIST + OfType = IntroType.Of(field.FieldType) |> Some + } + else IntroType.Of(field.FieldType) { Name = field.FieldName Description = field.Description From 9c4d9ee4515c8de54f5fee9c9a1c67e1262ff969 Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Sun, 27 Aug 2017 19:59:49 +0200 Subject: [PATCH 09/33] Update project files to support mac os development. --- .gitignore | 1 + GraphQL.Parser.Test/GraphQL.Parser.Test.fsproj | 3 +++ GraphQL.Parser/GraphQL.Parser.fsproj | 3 +++ 3 files changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index e3467fc..5986788 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ bld/ # Roslyn cache directories *.ide/ .vs/ +.idea/ # MSTest test Results [Tt]est[Rr]esult*/ diff --git a/GraphQL.Parser.Test/GraphQL.Parser.Test.fsproj b/GraphQL.Parser.Test/GraphQL.Parser.Test.fsproj index ce90bd9..2511996 100644 --- a/GraphQL.Parser.Test/GraphQL.Parser.Test.fsproj +++ b/GraphQL.Parser.Test/GraphQL.Parser.Test.fsproj @@ -51,6 +51,9 @@ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets + + /Library/Frameworks/Mono.framework/Versions/Current/lib/mono/Microsoft F#/v4.0/Microsoft.FSharp.Targets + diff --git a/GraphQL.Parser/GraphQL.Parser.fsproj b/GraphQL.Parser/GraphQL.Parser.fsproj index eb74606..954192b 100644 --- a/GraphQL.Parser/GraphQL.Parser.fsproj +++ b/GraphQL.Parser/GraphQL.Parser.fsproj @@ -49,6 +49,9 @@ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets + + /Library/Frameworks/Mono.framework/Versions/Current/lib/mono/Microsoft F#/v4.0/Microsoft.FSharp.Targets + From 63d15af6215e4fb6da69619d9d953163258b0f4e Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Sun, 27 Aug 2017 20:40:02 +0200 Subject: [PATCH 10/33] Fix unit tests. --- Tests/GenericTests.cs | 10 +++++----- Tests/InMemoryExecutionTests.cs | 2 +- Tests/MemContext.cs | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Tests/GenericTests.cs b/Tests/GenericTests.cs index 790b010..605bf4c 100644 --- a/Tests/GenericTests.cs +++ b/Tests/GenericTests.cs @@ -185,7 +185,7 @@ public static void Fragments(GraphQL gql) public static void InlineFragments(GraphQL gql) { var results = gql.ExecuteQuery( - "{ heros { __typename, ... on ICharacter { name }, ... on IHuman { height }, ... on Stormtrooper { specialization }, " + + "{ heros { __typename, ... on Droid { name }, ... on Human { name, height }, ... on Stormtrooper { name, height, specialization }, " + "... on Droid { primaryFunction } } }"); Test.DeepEquals( results, @@ -199,7 +199,7 @@ public static void InlineFragments(GraphQL gql) public static void InlineFragmentWithListField(GraphQL gql) { var results = gql.ExecuteQuery( - "{ heros { __typename, ... on ICharacter { name }, ... on IHuman { height, vehicles { name } }, ... on Stormtrooper { specialization }, " + + "{ heros { __typename, ... on Droid { name }, ... on Human { name, height, vehicles { name } }, ... on Stormtrooper { name, height, vehicles { name }, specialization } " + "... on Droid { primaryFunction } } }"); Test.DeepEquals( results, @@ -224,7 +224,7 @@ public static void FragmentWithMultiLevelInheritance(GraphQL public static void InlineFragmentWithoutTypenameField(GraphQL gql) { - var results = gql.ExecuteQuery("{ heros { ... on ICharacter { name }, ... on Stormtrooper { height, specialization } } }"); + var results = gql.ExecuteQuery("{ heros { ... on Human { name }, ... on Droid { name }, ... on Stormtrooper { name, height, specialization } } }"); Test.DeepEquals( results, "{ heros: [ " + @@ -249,7 +249,7 @@ public static void InlineFragmentWithoutTypenameFieldWithoutOtherFields(GraphQL gql) { var results = gql.ExecuteQuery( - "{ heros { ...character, ...stormtrooper } }, fragment character on ICharacter { name }, fragment stormtrooper on Stormtrooper { height, specialization } "); + "{ heros { ...human, ...droid, ...stormtrooper } }, fragment human on Human { name }, fragment droid on Droid { name }, fragment stormtrooper on Stormtrooper { name, height, specialization } "); Test.DeepEquals( results, "{ heros: [ " + @@ -262,7 +262,7 @@ public static void FragmentWithoutTypenameField(GraphQL gql) public static void FragmentWithMultipleTypenameFields(GraphQL gql) { var results = gql.ExecuteQuery( - "{ heros { ...character, ...stormtrooper, __typename } }, fragment character on ICharacter { name }, fragment stormtrooper on Stormtrooper { height, specialization, __typename } "); + "{ heros { ...human, ...droid, ...stormtrooper, __typename } }, fragment human on Human { name }, fragment droid on Droid { name }, fragment stormtrooper on Stormtrooper { name, height, specialization, __typename } "); Test.DeepEquals( results, "{ heros: [ " + diff --git a/Tests/InMemoryExecutionTests.cs b/Tests/InMemoryExecutionTests.cs index 42a4b82..971aff6 100644 --- a/Tests/InMemoryExecutionTests.cs +++ b/Tests/InMemoryExecutionTests.cs @@ -35,7 +35,6 @@ public class InMemoryExecutionTests [Test] public static void Fragments() => GenericTests.Fragments(MemContext.CreateDefaultContext()); [Test] public static void InlineFragments() => GenericTests.InlineFragments(MemContext.CreateDefaultContext()); [Test] public static void InlineFragmentWithListField() => GenericTests.InlineFragmentWithListField(MemContext.CreateDefaultContext()); - [Test] public static void FragmentWithMultiLevelInheritance() => GenericTests.FragmentWithMultiLevelInheritance(MemContext.CreateDefaultContext()); [Test] public static void InlineFragmentWithoutTypenameField() => GenericTests.InlineFragmentWithoutTypenameField(MemContext.CreateDefaultContext()); [Test] public static void FragmentWithoutTypenameField() => GenericTests.FragmentWithoutTypenameField(MemContext.CreateDefaultContext()); [Test] public static void InlineFragmentWithoutTypenameFieldWithoutOtherFields() => GenericTests.InlineFragmentWithoutTypenameFieldWithoutOtherFields(MemContext.CreateDefaultContext()); @@ -46,6 +45,7 @@ public class InMemoryExecutionTests public void AddAllFields() { var schema = GraphQL.CreateDefaultSchema(() => new MemContext()); + schema.AddType().AddAllFields(); schema.AddType().AddAllFields(); schema.AddType().AddAllFields(); schema.AddField("user", new { id = 0 }, (db, args) => db.Users.AsQueryable().FirstOrDefault(u => u.Id == args.id)); diff --git a/Tests/MemContext.cs b/Tests/MemContext.cs index d050015..41a9891 100644 --- a/Tests/MemContext.cs +++ b/Tests/MemContext.cs @@ -226,7 +226,6 @@ private static void InitializeCharacterSchema(GraphQLSchema schema) new[] { // TODO: ORDER MATTERS FOR TYPENAME RESOLUTION - characterInterface.GraphQLType, humanInterface.GraphQLType, humanType.GraphQLType, stormtrooperType.GraphQLType, From 48dd229c284f594587973968fca17b7b2acbdb87 Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Sun, 27 Aug 2017 20:50:41 +0200 Subject: [PATCH 11/33] Fix unit tests. --- Tests.EF/EntityFrameworkExecutionTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests.EF/EntityFrameworkExecutionTests.cs b/Tests.EF/EntityFrameworkExecutionTests.cs index 0a63bfe..185b3e2 100644 --- a/Tests.EF/EntityFrameworkExecutionTests.cs +++ b/Tests.EF/EntityFrameworkExecutionTests.cs @@ -284,8 +284,6 @@ private static void InitializeCharacterSchema(GraphQLSchema schema) [Test] public static void InlineFragmentWithListField() => GenericTests.InlineFragmentWithListField(CreateDefaultContext()); [Test] - public static void FragmentWithMultiLevelInheritance() => GenericTests.FragmentWithMultiLevelInheritance(CreateDefaultContext()); - [Test] public static void InlineFragmentWithoutTypenameField() => GenericTests.InlineFragmentWithoutTypenameField(CreateDefaultContext()); [Test] public static void FragmentWithoutTypenameField() => GenericTests.FragmentWithoutTypenameField(CreateDefaultContext()); @@ -300,6 +298,7 @@ private static void InitializeCharacterSchema(GraphQLSchema schema) public void AddAllFields() { var schema = GraphQL.CreateDefaultSchema(() => new EfContext()); + schema.AddType().AddAllFields(); schema.AddType().AddAllFields(); schema.AddType().AddAllFields(); schema.AddField("user", new { id = 0 }, (db, args) => db.Users.FirstOrDefault(u => u.Id == args.id)); From 0373e39baed72ea36f8da6ac9d878475fc5bfacd Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Sun, 3 Sep 2017 15:22:03 +0200 Subject: [PATCH 12/33] Add star wars unit tests (according to tests of graphql-js implementation). --- Tests.EF/EntityFrameworkExecutionTests.cs | 224 +++++++------------ Tests/GenericTests.cs | 3 +- Tests/InMemoryExecutionTests.cs | 10 + Tests/IntrospectionTests.cs | 1 - Tests/MemContext.cs | 163 +++----------- Tests/StarWarsTestSchema.cs | 252 ++++++++++++++++++++++ Tests/StarWarsTests.cs | 27 +++ Tests/Tests.csproj | 2 + 8 files changed, 396 insertions(+), 286 deletions(-) create mode 100644 Tests/StarWarsTestSchema.cs create mode 100644 Tests/StarWarsTests.cs diff --git a/Tests.EF/EntityFrameworkExecutionTests.cs b/Tests.EF/EntityFrameworkExecutionTests.cs index 185b3e2..a3e6a03 100644 --- a/Tests.EF/EntityFrameworkExecutionTests.cs +++ b/Tests.EF/EntityFrameworkExecutionTests.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using System.Data.Entity; -using System.Data.Entity.Migrations; using System.Linq; -using System.Linq.Expressions; using GraphQL.Net; using NUnit.Framework; using SQLite.CodeFirst; @@ -51,43 +49,10 @@ public static void Init() db.Users.Add(user2); db.MutateMes.Add(new MutateMe()); - var human = new Human + foreach (var character in StarWarsTestSchema.CreateData()) { - Id = 1, - Name = "Han Solo", - Height = 5.6430448 - }; - db.Heros.Add(human); - var stormtrooper = new Stormtrooper - { - Id = 2, - Name = "FN-2187", - Height = 4.9, - Specialization = "Imperial Snowtrooper" - }; - db.Heros.Add(stormtrooper); - var droid = new Droid - { - Id = 3, - Name = "R2-D2", - PrimaryFunction = "Astromech" - }; - db.Heros.Add(droid); - var vehicle = new Vehicle - { - Id = 1, - Name = "Millennium falcon", - Human = human - }; - db.Vehicles.Add(vehicle); - - var vehicle2 = new Vehicle - { - Id = 2, - Name = "Speeder bike", - Human = stormtrooper - }; - db.Vehicles.Add(vehicle2); + db.Heros.Add(character); + } db.SaveChanges(); } @@ -96,7 +61,7 @@ public static void Init() private static GraphQL CreateDefaultContext() { var schema = GraphQL.CreateDefaultSchema(() => new EfContext()); - schema.AddScalar(new { year = 0, month = 0, day = 0 }, ymd => new DateTime(ymd.year, ymd.month, ymd.day)); + schema.AddScalar(new {year = 0, month = 0, day = 0}, ymd => new DateTime(ymd.year, ymd.month, ymd.day)); InitializeUserSchema(schema); InitializeAccountSchema(schema); InitializeMutationSchema(schema); @@ -116,11 +81,11 @@ private static void InitializeUserSchema(GraphQLSchema schema) user.AddField("total", (db, u) => db.Users.Count()); user.AddField("accountPaid", (db, u) => u.Account.Paid); user.AddPostField("abc", () => GetAbcPostField()); - user.AddPostField("sub", () => new Sub { Id = 1 }); + user.AddPostField("sub", () => new Sub {Id = 1}); schema.AddType().AddField(s => s.Id); schema.AddListField("users", db => db.Users); - schema.AddField("user", new { id = 0 }, (db, args) => db.Users.FirstOrDefault(u => u.Id == args.id)); + schema.AddField("user", new {id = 0}, (db, args) => db.Users.FirstOrDefault(u => u.Id == args.id)); } private static string GetAbcPostField() => "easy as 123"; // mimic an in-memory function @@ -136,17 +101,19 @@ private static void InitializeAccountSchema(GraphQLSchema schema) account.AddField(a => a.AccountType); account.AddListField(a => a.Users); account.AddListField("activeUsers", (db, a) => a.Users.Where(u => u.Active)); - account.AddListField("usersWithActive", new { active = false }, (db, args, a) => a.Users.Where(u => u.Active == args.active)); - account.AddField("firstUserWithActive", new { active = false }, (db, args, a) => a.Users.FirstOrDefault(u => u.Active == args.active)); + account.AddListField("usersWithActive", new {active = false}, + (db, args, a) => a.Users.Where(u => u.Active == args.active)); + account.AddField("firstUserWithActive", new {active = false}, + (db, args, a) => a.Users.FirstOrDefault(u => u.Active == args.active)); - schema.AddField("account", new { id = 0 }, (db, args) => db.Accounts.FirstOrDefault(a => a.Id == args.id)); + schema.AddField("account", new {id = 0}, (db, args) => db.Accounts.FirstOrDefault(a => a.Id == args.id)); schema.AddField - ("accountPaidBy", new { paid = default(DateTime) }, - (db, args) => db.Accounts.AsQueryable().FirstOrDefault(a => a.PaidUtc <= args.paid)); - schema.AddListField("accountsByGuid", new { guid = Guid.Empty }, - (db, args) => db.Accounts.AsQueryable().Where(a => a.SomeGuid == args.guid)); - schema.AddListField("accountsByType", new { accountType = AccountType.None }, - (db, args) => db.Accounts.AsQueryable().Where(a => a.AccountType == args.accountType)); + ("accountPaidBy", new {paid = default(DateTime)}, + (db, args) => db.Accounts.AsQueryable().FirstOrDefault(a => a.PaidUtc <= args.paid)); + schema.AddListField("accountsByGuid", new {guid = Guid.Empty}, + (db, args) => db.Accounts.AsQueryable().Where(a => a.SomeGuid == args.guid)); + schema.AddListField("accountsByType", new {accountType = AccountType.None}, + (db, args) => db.Accounts.AsQueryable().Where(a => a.AccountType == args.accountType)); schema.AddEnum(prefix: "accountType_"); //add this enum just so it is part of the schema schema.AddEnum(prefix: "materialType_"); @@ -157,9 +124,10 @@ private static void InitializeMutationSchema(GraphQLSchema schema) var mutate = schema.AddType(); mutate.AddAllFields(); - schema.AddField("mutateMes", new { id = 0 }, (db, args) => db.MutateMes.AsQueryable().FirstOrDefault(a => a.Id == args.id)); + schema.AddField("mutateMes", new {id = 0}, + (db, args) => db.MutateMes.AsQueryable().FirstOrDefault(a => a.Id == args.id)); schema.AddMutation("mutate", - new { id = 0, newVal = 0 }, + new {id = 0, newVal = 0}, (db, args) => { var mutateMe = db.MutateMes.First(m => m.Id == args.id); @@ -168,10 +136,10 @@ private static void InitializeMutationSchema(GraphQLSchema schema) }, (db, args) => db.MutateMes.AsQueryable().FirstOrDefault(a => a.Id == args.id)); schema.AddMutation("addMutate", - new { newVal = 0 }, + new {newVal = 0}, (db, args) => { - var newMutate = new MutateMe { Value = args.newVal }; + var newMutate = new MutateMe {Value = args.newVal}; db.MutateMes.Add(newMutate); db.SaveChanges(); return newMutate.Id; @@ -187,112 +155,110 @@ private static void InitializeNullRefSchema(GraphQLSchema schema) private static void InitializeCharacterSchema(GraphQLSchema schema) { - var characterInterface = schema.AddInterfaceType(); - characterInterface.AddAllFields(); - - var humanInterface = schema.AddInterfaceType(); - humanInterface.AddAllFields(); - humanInterface.AddInterface(characterInterface); - - var humanType = schema.AddType(); - humanType.AddAllFields(); - humanType.AddInterface(characterInterface); - humanType.AddInterface(humanInterface); - - var stormtrooperType = schema.AddType(); - stormtrooperType.AddAllFields(); - stormtrooperType.AddInterface(characterInterface); - stormtrooperType.AddInterface(humanInterface); - - var droidType = schema.AddType(); - droidType.AddAllFields(); - droidType.AddInterface(characterInterface); - schema.AddUnionType("OtherUnionType01", new List()); - - var heroUnionType = schema.AddUnionType( - "Hero", - new[] - { - // TODO: ORDER MATTERS FOR TYPENAME RESOLUTION - characterInterface.GraphQLType, - humanInterface.GraphQLType, - humanType.GraphQLType, - stormtrooperType.GraphQLType, - droidType.GraphQLType - }); - - - schema.AddUnionType("OtherUnionType02", new List()); - - schema.AddType().AddAllFields(); - schema.AddField("hero", new {id = 0}, (db, args) => db.Heros.FirstOrDefault(h => h.Id == args.id)) - .WithReturnType(heroUnionType); - schema.AddListField("heros", db => db.Heros.AsQueryable()) - .WithReturnType(heroUnionType); + StarWarsTestSchema.Create(schema, db => db.Heros.AsQueryable()); } [Test] public void LookupSingleEntity() => GenericTests.LookupSingleEntity(CreateDefaultContext()); + [Test] public void AliasOneField() => GenericTests.AliasOneField(CreateDefaultContext()); + [Test] public void NestedEntity() => GenericTests.NestedEntity(CreateDefaultContext()); + [Test] public void NoUserQueryReturnsNull() => GenericTests.NoUserQueryReturnsNull(CreateDefaultContext()); + [Test] public void CustomFieldSubQuery() => GenericTests.CustomFieldSubQuery(CreateDefaultContext()); + [Test] - public void CustomFieldSubQueryUsingContext() => GenericTests.CustomFieldSubQueryUsingContext(CreateDefaultContext()); + public void CustomFieldSubQueryUsingContext() => + GenericTests.CustomFieldSubQueryUsingContext(CreateDefaultContext()); + [Test] public void List() => GenericTests.List(CreateDefaultContext()); + [Test] public void ListTypeIsList() => GenericTests.ListTypeIsList(CreateDefaultContext()); + [Test] public void NestedEntityList() => GenericTests.NestedEntityList(CreateDefaultContext()); + [Test] public void PostField() => GenericTests.PostField(CreateDefaultContext()); + [Test] public void PostFieldSubQuery() => GenericTests.PostFieldSubQuery(CreateDefaultContext()); + [Test] public void TypeName() => GenericTests.TypeName(CreateDefaultContext()); + [Test] public void DateTimeFilter() => GenericTests.DateTimeFilter(CreateDefaultContext()); + [Test] public void EnumerableSubField() => GenericTests.EnumerableSubField(CreateDefaultContext()); + [Test] public void SimpleMutation() => GenericTests.SimpleMutation(CreateDefaultContext()); + [Test] public void MutationWithReturn() => GenericTests.MutationWithReturn(CreateDefaultContext()); + [Test] public void NullPropagation() => GenericTests.NullPropagation(CreateDefaultContext()); + [Test] public void GuidField() => GenericTests.GuidField(CreateDefaultContext()); + [Test] public void GuidParameter() => GenericTests.GuidParameter(CreateDefaultContext()); + [Test] public void EnumFieldQuery() => GenericTests.EnumFieldQuery(CreateDefaultContext()); + [Test] public void ByteArrayParameter() => GenericTests.ByteArrayParameter(CreateDefaultContext()); + [Test] - public void ChildListFieldWithParameters() => GenericTests.ChildListFieldWithParameters(MemContext.CreateDefaultContext()); + public void ChildListFieldWithParameters() => + GenericTests.ChildListFieldWithParameters(MemContext.CreateDefaultContext()); + [Test] - public void ChildFieldWithParameters() => GenericTests.ChildFieldWithParameters(MemContext.CreateDefaultContext()); + public void ChildFieldWithParameters() => + GenericTests.ChildFieldWithParameters(MemContext.CreateDefaultContext()); + [Test] public static void Fragments() => GenericTests.Fragments(CreateDefaultContext()); + [Test] public static void InlineFragments() => GenericTests.InlineFragments(CreateDefaultContext()); + [Test] - public static void InlineFragmentWithListField() => GenericTests.InlineFragmentWithListField(CreateDefaultContext()); + public static void InlineFragmentWithListField() => + GenericTests.InlineFragmentWithListField(CreateDefaultContext()); + [Test] - public static void InlineFragmentWithoutTypenameField() => GenericTests.InlineFragmentWithoutTypenameField(CreateDefaultContext()); + public static void InlineFragmentWithoutTypenameField() => + GenericTests.InlineFragmentWithoutTypenameField(CreateDefaultContext()); + [Test] - public static void FragmentWithoutTypenameField() => GenericTests.FragmentWithoutTypenameField(CreateDefaultContext()); + public static void FragmentWithoutTypenameField() => + GenericTests.FragmentWithoutTypenameField(CreateDefaultContext()); + [Test] - public static void InlineFragmentWithoutTypenameFieldWithoutOtherFields() => GenericTests.InlineFragmentWithoutTypenameFieldWithoutOtherFields(CreateDefaultContext()); + public static void InlineFragmentWithoutTypenameFieldWithoutOtherFields() => + GenericTests.InlineFragmentWithoutTypenameFieldWithoutOtherFields(CreateDefaultContext()); + [Test] - public static void FragmentWithMultipleTypenameFields() => GenericTests.FragmentWithMultipleTypenameFields(CreateDefaultContext()); + public static void FragmentWithMultipleTypenameFields() => + GenericTests.FragmentWithMultipleTypenameFields(CreateDefaultContext()); + [Test] - public static void FragmentWithMultipleTypenameFieldsMixedWithInlineFragment() => GenericTests.FragmentWithMultipleTypenameFieldsMixedWithInlineFragment(CreateDefaultContext()); + public static void FragmentWithMultipleTypenameFieldsMixedWithInlineFragment() => + GenericTests.FragmentWithMultipleTypenameFieldsMixedWithInlineFragment(CreateDefaultContext()); [Test] public void AddAllFields() @@ -301,7 +267,7 @@ public void AddAllFields() schema.AddType().AddAllFields(); schema.AddType().AddAllFields(); schema.AddType().AddAllFields(); - schema.AddField("user", new { id = 0 }, (db, args) => db.Users.FirstOrDefault(u => u.Id == args.id)); + schema.AddField("user", new {id = 0}, (db, args) => db.Users.FirstOrDefault(u => u.Id == args.id)); schema.Complete(); var gql = new GraphQL(schema); @@ -322,7 +288,9 @@ static EfContext() Environment.SetEnvironmentVariable("AppendManifestToken_SQLiteProviderManifest", ";BinaryGUID=True;"); } - public EfContext() : base("DefaultConnection") { } + public EfContext() : base("DefaultConnection") + { + } protected override void OnModelCreating(DbModelBuilder modelBuilder) { @@ -334,8 +302,7 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) public IDbSet Accounts { get; set; } public IDbSet MutateMes { get; set; } public IDbSet NullRefs { get; set; } - public IDbSet Heros { get; set; } - public IDbSet Vehicles { get; set; } + public IDbSet Heros { get; set; } } class User @@ -358,7 +325,7 @@ class Account public bool Paid { get; set; } public DateTime? PaidUtc { get; set; } public Guid SomeGuid { get; set; } - public byte[] ByteArray { get; set; } = { 1, 2, 3, 4 }; + public byte[] ByteArray { get; set; } = {1, 2, 3, 4}; public AccountType AccountType { get; set; } @@ -375,40 +342,5 @@ class NullRef { public int Id { get; set; } } - - class ICharacter - { - public int Id { get; set; } - public string Name { get; set; } - } - - class IHuman : ICharacter - { - public double Height { get; set; } - public ICollection Vehicles { get; set; } - } - - class Human : IHuman - { - - } - - class Stormtrooper : IHuman - { - public string Specialization { get; set; } - } - - class Droid : ICharacter - { - public string PrimaryFunction { get; set; } - } - - class Vehicle - { - public int Id { get; set; } - public string Name { get; set; } - public int HumanId { get; set; } - public virtual IHuman Human { get; set; } - } } -} +} \ No newline at end of file diff --git a/Tests/GenericTests.cs b/Tests/GenericTests.cs index 605bf4c..68ffcee 100644 --- a/Tests/GenericTests.cs +++ b/Tests/GenericTests.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using GraphQL.Net; using NUnit.Framework; diff --git a/Tests/InMemoryExecutionTests.cs b/Tests/InMemoryExecutionTests.cs index 971aff6..66a13e3 100644 --- a/Tests/InMemoryExecutionTests.cs +++ b/Tests/InMemoryExecutionTests.cs @@ -41,6 +41,16 @@ public class InMemoryExecutionTests [Test] public static void FragmentWithMultipleTypenameFields() => GenericTests.FragmentWithMultipleTypenameFields(MemContext.CreateDefaultContext()); [Test] public static void FragmentWithMultipleTypenameFieldsMixedWithInlineFragment() => GenericTests.FragmentWithMultipleTypenameFieldsMixedWithInlineFragment(MemContext.CreateDefaultContext()); + [Test] + public static void StarWarsBasicQueryHero() => + StarWarsTests.BasicQueryHero(MemContext.CreateDefaultContext()); + [Test] + public static void StarWarsBasicQueryHeroWithIdAndFriends() => + StarWarsTests.BasicQueryHeroWithIdAndFriends(MemContext.CreateDefaultContext()); + [Test] + public static void StarWarsBasicQueryHeroWithIdAndFriendsOfFriends() => + StarWarsTests.BasicQueryHeroWithIdAndFriendsOfFriends(MemContext.CreateDefaultContext()); + [Test] public void AddAllFields() { diff --git a/Tests/IntrospectionTests.cs b/Tests/IntrospectionTests.cs index c6f2ba0..9c2509c 100644 --- a/Tests/IntrospectionTests.cs +++ b/Tests/IntrospectionTests.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using NUnit.Framework.Internal; namespace Tests { diff --git a/Tests/MemContext.cs b/Tests/MemContext.cs index 41a9891..a30b8b6 100644 --- a/Tests/MemContext.cs +++ b/Tests/MemContext.cs @@ -27,7 +27,7 @@ public MemContext() Active = true }; Users.Add(user); - account.Users = new List { user }; + account.Users = new List {user}; var account2 = new Account { Id = 2, @@ -49,55 +49,16 @@ public MemContext() Id = 1, Value = 0, }); - account2.Users = new List { user2 }; + account2.Users = new List {user2}; - var human = new Human - { - Id = 1, - Name = "Han Solo", - Height = 5.6430448 - }; - Heros.Add(human); - var stormtrooper = new Stormtrooper - { - Id = 2, - Name = "FN-2187", - Height = 4.9, - Specialization = "Imperial Snowtrooper" - }; - Heros.Add(stormtrooper); - var droid = new Droid - { - Id = 3, - Name = "R2-D2", - PrimaryFunction = "Astromech" - }; - Heros.Add(droid); - - var vehicle = new Vehicle - { - Id = 1, - Name = "Millennium falcon", - HumanId = human.Id - }; - Vehicles.Add(vehicle); - human.Vehicles = new List { vehicle }; - var vehicle2 = new Vehicle - { - Id = 2, - Name = "Speeder bike", - HumanId = stormtrooper.Id - }; - Vehicles.Add(vehicle2); - stormtrooper.Vehicles = new List { vehicle2 }; + Heros.AddRange(StarWarsTestSchema.CreateData()); } public List Users { get; set; } = new List(); public List Accounts { get; set; } = new List(); public List MutateMes { get; set; } = new List(); public List NullRefs { get; set; } = new List(); - List Heros { get; set; } = new List(); - List Vehicles { get; set; } = new List(); + public List Heros { get; set; } = new List(); public static GraphQL CreateDefaultContext() { @@ -109,7 +70,7 @@ public static GraphQL CreateDefaultContext() public static GraphQLSchema CreateDefaultSchema() { var schema = GraphQL.CreateDefaultSchema(() => new MemContext()); - schema.AddScalar(new { year = 0, month = 0, day = 0 }, ymd => new DateTime(ymd.year, ymd.month, ymd.day)); + schema.AddScalar(new {year = 0, month = 0, day = 0}, ymd => new DateTime(ymd.year, ymd.month, ymd.day)); InitializeUserSchema(schema); InitializeAccountSchema(schema); InitializeMutationSchema(schema); @@ -128,11 +89,12 @@ public static void InitializeUserSchema(GraphQLSchema schema) user.AddField("total", (db, u) => db.Users.Count); user.AddField("accountPaid", (db, u) => u.Account.Paid); user.AddPostField("abc", () => GetAbcPostField()); - user.AddPostField("sub", () => new Sub { Id = 1 }); + user.AddPostField("sub", () => new Sub {Id = 1}); schema.AddType().AddField(s => s.Id); schema.AddListField("users", db => db.Users.AsQueryable()); - schema.AddField("user", new { id = 0 }, (db, args) => db.Users.AsQueryable().FirstOrDefault(u => u.Id == args.id)); + schema.AddField("user", new {id = 0}, + (db, args) => db.Users.AsQueryable().FirstOrDefault(u => u.Id == args.id)); } private static string GetAbcPostField() => "easy as 123"; // mimic an in-memory function @@ -148,17 +110,20 @@ public static void InitializeAccountSchema(GraphQLSchema schema) account.AddField(a => a.AccountType); account.AddListField(a => a.Users); account.AddListField("activeUsers", (db, a) => a.Users.Where(u => u.Active)); - account.AddListField("usersWithActive", new { active = false }, (db, args, a) => a.Users.Where(u => u.Active == args.active)); - account.AddField("firstUserWithActive", new { active = false }, (db, args, a) => a.Users.FirstOrDefault(u => u.Active == args.active)); + account.AddListField("usersWithActive", new {active = false}, + (db, args, a) => a.Users.Where(u => u.Active == args.active)); + account.AddField("firstUserWithActive", new {active = false}, + (db, args, a) => a.Users.FirstOrDefault(u => u.Active == args.active)); - schema.AddField("account", new { id = 0 }, (db, args) => db.Accounts.AsQueryable().FirstOrDefault(a => a.Id == args.id)); + schema.AddField("account", new {id = 0}, + (db, args) => db.Accounts.AsQueryable().FirstOrDefault(a => a.Id == args.id)); schema.AddField - ("accountPaidBy", new { paid = default(DateTime) }, - (db, args) => db.Accounts.AsQueryable().FirstOrDefault(a => a.PaidUtc <= args.paid)); - schema.AddListField("accountsByGuid", new { guid = Guid.Empty }, - (db, args) => db.Accounts.AsQueryable().Where(a => a.SomeGuid == args.guid)); + ("accountPaidBy", new {paid = default(DateTime)}, + (db, args) => db.Accounts.AsQueryable().FirstOrDefault(a => a.PaidUtc <= args.paid)); + schema.AddListField("accountsByGuid", new {guid = Guid.Empty}, + (db, args) => db.Accounts.AsQueryable().Where(a => a.SomeGuid == args.guid)); - schema.AddListField("accountsByType", new { accountType = AccountType.None }, + schema.AddListField("accountsByType", new {accountType = AccountType.None}, (db, args) => db.Accounts.AsQueryable().Where(a => a.AccountType == args.accountType)); schema.AddEnum(prefix: "accountType_"); @@ -171,9 +136,10 @@ private static void InitializeMutationSchema(GraphQLSchema schema) var mutate = schema.AddType(); mutate.AddAllFields(); - schema.AddField("mutateMes", new { id = 0 }, (db, args) => db.MutateMes.AsQueryable().FirstOrDefault(a => a.Id == args.id)); + schema.AddField("mutateMes", new {id = 0}, + (db, args) => db.MutateMes.AsQueryable().FirstOrDefault(a => a.Id == args.id)); schema.AddMutation("mutate", - new { id = 0, newVal = 0 }, + new {id = 0, newVal = 0}, (db, args) => { var mutateMe = db.MutateMes.First(m => m.Id == args.id); @@ -181,10 +147,10 @@ private static void InitializeMutationSchema(GraphQLSchema schema) }, (db, args) => db.MutateMes.AsQueryable().FirstOrDefault(a => a.Id == args.id)); schema.AddMutation("addMutate", - new { newVal = 0 }, + new {newVal = 0}, (db, args) => { - var newMutate = new MutateMe { Value = args.newVal }; + var newMutate = new MutateMe {Value = args.newVal}; db.MutateMes.Add(newMutate); // simulate Id being set by database newMutate.Id = db.MutateMes.Max(m => m.Id) + 1; @@ -201,42 +167,7 @@ private static void InitializeNullRefSchema(GraphQLSchema schema) private static void InitializeCharacterSchema(GraphQLSchema schema) { - var characterInterface = schema.AddInterfaceType(); - characterInterface.AddAllFields(); - - var humanInterface = schema.AddInterfaceType(); - humanInterface.AddAllFields(); - - var humanType = schema.AddType(); - humanType.AddAllFields(); - humanType.AddInterface(characterInterface); - humanType.AddInterface(humanInterface); - - var stormtrooperType = schema.AddType(); - stormtrooperType.AddAllFields(); - stormtrooperType.AddInterface(characterInterface); - stormtrooperType.AddInterface(humanInterface); - - var droidType = schema.AddType(); - droidType.AddAllFields(); - droidType.AddInterface(characterInterface); - - var heroUnionType = schema.AddUnionType( - "Hero", - new[] - { - // TODO: ORDER MATTERS FOR TYPENAME RESOLUTION - humanInterface.GraphQLType, - humanType.GraphQLType, - stormtrooperType.GraphQLType, - droidType.GraphQLType - }); - - schema.AddType().AddAllFields(); - schema.AddField("hero", new { id = 0 }, (db, args) => db.Heros.SingleOrDefault(h => h.Id == args.id)) - .WithReturnType(heroUnionType); - schema.AddListField("heros", db => db.Heros.AsQueryable()) - .WithReturnType(heroUnionType); + StarWarsTestSchema.Create(schema, db => db.Heros.AsQueryable()); } } @@ -280,7 +211,7 @@ public class Account public bool Paid { get; set; } public DateTime? PaidUtc { get; set; } public Guid SomeGuid { get; set; } - public byte[] ByteArray { get; set; } = { 1, 2, 3, 4 }; + public byte[] ByteArray { get; set; } = {1, 2, 3, 4}; public List Users { get; set; } @@ -302,46 +233,4 @@ public class NullRef { public int Id { get; set; } } - - interface ICharacter - { - int Id { get; set; } - string Name { get; set; } - } - - class Hero : ICharacter - { - public int Id { get; set; } - public string Name { get; set; } - } - - interface IHuman - { - double Height { get; set; } - ICollection Vehicles { get; set; } - } - - class Human : Hero, IHuman - { - public double Height { get; set; } - public ICollection Vehicles { get; set; } - } - - class Stormtrooper : Human - { - public string Specialization { get; set; } - } - - class Droid : Hero - { - public string PrimaryFunction { get; set; } - } - - class Vehicle - { - public int Id { get; set; } - public string Name { get; set; } - public int HumanId { get; set; } - public virtual IHuman Human { get; set; } - } } \ No newline at end of file diff --git a/Tests/StarWarsTestSchema.cs b/Tests/StarWarsTestSchema.cs new file mode 100644 index 0000000..2491c9a --- /dev/null +++ b/Tests/StarWarsTestSchema.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using GraphQL.Net; + +namespace Tests +{ + public class StarWarsTestSchema + { + /* + * The star wars schema is based on: + * https://github.com/graphql/graphql-js/blob/master/src/__tests__/starWarsSchema.js + * + * Using our shorthand to describe type systems, the type system for our + * Star Wars example is: + * + * enum Episode { NEWHOPE, EMPIRE, JEDI } + * + * interface Character { + * id: String! + * name: String + * friends: [Character] + * appearsIn: [Episode] + * } + * + * type Human implements Character { + * id: String! + * name: String + * friends: [Character] + * appearsIn: [Episode] + * homePlanet: String + * } + * + * type Droid implements Character { + * id: String! + * name: String + * friends: [Character] + * appearsIn: [Episode] + * primaryFunction: String + * } + * + * type Query { + * hero(episode: Episode): Character + * human(id: String!): Human + * droid(id: String!): Droid + * } + */ + + public enum EpisodeEnum + { + NEWHOPE = 4, + EMPIRE = 5, + JEDI = 6 + } + + public interface ICharacter + { + string Id { get; set; } + string Name { get; set; } + ICollection Friends { get; set; } + ICollection AppearsIn { get; set; } + string SecretBackstory { get; set; } + } + + public class Human : ICharacter + { + public string Id { get; set; } + public string Name { get; set; } + public ICollection Friends { get; set; } + public ICollection AppearsIn { get; set; } + public string SecretBackstory { get; set; } + public string HomePlanet { get; set; } + } + + public class Droid : ICharacter + { + public string Id { get; set; } + public string Name { get; set; } + public ICollection Friends { get; set; } + public ICollection AppearsIn { get; set; } + public string SecretBackstory { get; set; } + public string PrimaryFunction { get; set; } + } + + public static void Create(GraphQLSchema schema, + Func> herosProviderFunc) + { + schema.AddEnum(); + + var characterInterface = schema.AddInterfaceType(); + characterInterface.AddAllFields(); + + var humanType = schema.AddType(); + humanType.AddAllFields(); + humanType.AddInterface(characterInterface); + + var droidType = schema.AddType(); + droidType.AddAllFields(); + droidType.AddInterface(characterInterface); + + schema.AddField( + "hero", + new {episode = EpisodeEnum.NEWHOPE}, + (db, args) => args.episode == EpisodeEnum.EMPIRE + // Luke is the hero of Episode V. + ? herosProviderFunc(db).FirstOrDefault(h => h.Id == "1000") + // Artoo is the hero otherwise. + : herosProviderFunc(db).FirstOrDefault(h => h.Id == "2001")); + schema.AddField("human", new {id = ""}, + (db, args) => herosProviderFunc(db).OfType().FirstOrDefault(c => c.Id == args.id)); + schema.AddField("droid", new {id = ""}, + (db, args) => herosProviderFunc(db).OfType().FirstOrDefault(c => c.Id == args.id)); + } + + + public static ICollection CreateData() + { + var luke = new Human + { + Id = "1000", + Name = "Luke Skywalker", + AppearsIn = new List + { + EpisodeEnum.EMPIRE, + EpisodeEnum.JEDI, + EpisodeEnum.NEWHOPE + }, + HomePlanet = "Tatooine" + }; + var vader = new Human + { + Id = "1001", + Name = "Darth Vader", + AppearsIn = new List + { + EpisodeEnum.EMPIRE, + EpisodeEnum.JEDI, + EpisodeEnum.NEWHOPE + }, + HomePlanet = "Tatooine" + }; + var han = new Human + { + Id = "1002", + Name = "Han Solo", + AppearsIn = new List + { + EpisodeEnum.EMPIRE, + EpisodeEnum.JEDI, + EpisodeEnum.NEWHOPE + } + }; + var leia = new Human + { + Id = "1003", + Name = "Leia Organa", + AppearsIn = new List + { + EpisodeEnum.EMPIRE, + EpisodeEnum.JEDI, + EpisodeEnum.NEWHOPE + }, + HomePlanet = "Alderaan" + }; + var tarkin = new Human + { + Id = "1004", + Name = "Wilhuff Tarkin", + AppearsIn = new List + { + EpisodeEnum.NEWHOPE + } + }; + var threepio = new Droid + { + Id = "2000", + Name = "C-3PO", + AppearsIn = new List + { + EpisodeEnum.EMPIRE, + EpisodeEnum.JEDI, + EpisodeEnum.NEWHOPE + }, + PrimaryFunction = "Protocol" + }; + var artoo = new Droid + { + Id = "2001", + Name = "R2-D2", + AppearsIn = new List + { + EpisodeEnum.EMPIRE, + EpisodeEnum.JEDI, + EpisodeEnum.NEWHOPE + }, + PrimaryFunction = "Astromech" + }; + luke.Friends = new List + { + han, + leia, + threepio, + artoo + }; + vader.Friends = new List + { + tarkin + }; + han.Friends = new List + { + luke, + leia, + artoo + }; + leia.Friends = new List + { + luke, + han, + threepio, + artoo + }; + tarkin.Friends = new List + { + vader + }; + threepio.Friends = new List + { + luke, + han, + leia, + artoo + }; + artoo.Friends = new List + { + luke, + han, + leia + }; + + return new List + { + luke, + vader, + han, + leia, + tarkin, + threepio, + artoo + }; + } + } +} \ No newline at end of file diff --git a/Tests/StarWarsTests.cs b/Tests/StarWarsTests.cs new file mode 100644 index 0000000..07f56af --- /dev/null +++ b/Tests/StarWarsTests.cs @@ -0,0 +1,27 @@ +using GraphQL.Net; + +namespace Tests +{ + public class StarWarsTests + { + public static void BasicQueryHero(GraphQL gql) + { + var results = gql.ExecuteQuery("query HeroNameQuery { hero { name } }"); + Test.DeepEquals(results, "{ hero: { name: 'R2-D2' } }"); + } + + public static void BasicQueryHeroWithIdAndFriends(GraphQL gql) + { + var results = gql.ExecuteQuery("query HeroNameQuery { hero { id, name, friends { name } } }"); + Test.DeepEquals(results, + "{ hero: { id: '2001', name: 'R2-D2', friends: [ { name: 'Luke Skywalker' },{ name: 'Han Solo' }, { name: 'Leia Organa'} ] } }"); + } + + public static void BasicQueryHeroWithIdAndFriendsOfFriends(GraphQL gql) + { + var results = gql.ExecuteQuery("query HeroNameQuery { hero { id, name, friends { name, appearsIn, friends { name } } } }"); + Test.DeepEquals(results, + "{ hero: { id: '2001', name: 'R2-D2', friends: [ { name: 'Luke Skywalker' },{ name: 'Han Solo' }, { name: 'Leia Organa'} ] } }"); + } + } +} \ No newline at end of file diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 721a786..100178a 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -64,6 +64,8 @@ + + From f3468c91383e336a884bcbf29988d08341333bfe Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Sun, 3 Sep 2017 15:47:38 +0200 Subject: [PATCH 13/33] Fix issue related to fields of type lists of scalar. --- GraphQL.Net/DynamicTypeBuilder.cs | 4 +++- GraphQL.Net/Executor.cs | 14 +++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/GraphQL.Net/DynamicTypeBuilder.cs b/GraphQL.Net/DynamicTypeBuilder.cs index 450296d..4ad3d83 100644 --- a/GraphQL.Net/DynamicTypeBuilder.cs +++ b/GraphQL.Net/DynamicTypeBuilder.cs @@ -62,7 +62,9 @@ public static Type CreateDynamicUnionTypeOrInterface(string name, IEnumerable).MakeGenericType(field.Type.CLRType) + : TypeHelpers.MakeNullable(field.Type.CLRType)) : typeof(object); } diff --git a/GraphQL.Net/Executor.cs b/GraphQL.Net/Executor.cs index 54190d1..e6f3022 100644 --- a/GraphQL.Net/Executor.cs +++ b/GraphQL.Net/Executor.cs @@ -388,9 +388,17 @@ private static MemberBinding GetBinding(GraphQLSchema schema, ExecSele // If there aren't any children, then we can assume that this is a scalar entity and we don't have to map child fields if (!map.Selections.Any()) { - if (options.CastAssignment && expr.Body.Type != toMember.PropertyType) - replacedContext = Expression.Convert(replacedContext, toMember.PropertyType); - return Expression.Bind(toMember, replacedContext); + if (!field.IsList) + { + if (options.CastAssignment && expr.Body.Type != toMember.PropertyType) + replacedContext = Expression.Convert(replacedContext, toMember.PropertyType); + return Expression.Bind(toMember, replacedContext); + } + + return Expression.Bind(toMember, + options.NullCheckLists + ? NullPropagate(replacedContext, replacedContext) + : replacedContext); } // If binding a single entity, just use the already built selector expression (replaced context) From 7d1d3884742b86409ae981e94de4b6f9f90c2db4 Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Sun, 3 Sep 2017 16:08:52 +0200 Subject: [PATCH 14/33] Add star wars unit tests (according to tests of graphql-js implementation). --- Tests/InMemoryExecutionTests.cs | 24 ++++- Tests/StarWarsTestSchema.cs | 24 ++--- Tests/StarWarsTests.cs | 150 +++++++++++++++++++++++++++++++- 3 files changed, 181 insertions(+), 17 deletions(-) diff --git a/Tests/InMemoryExecutionTests.cs b/Tests/InMemoryExecutionTests.cs index 66a13e3..477e12a 100644 --- a/Tests/InMemoryExecutionTests.cs +++ b/Tests/InMemoryExecutionTests.cs @@ -44,12 +44,34 @@ public class InMemoryExecutionTests [Test] public static void StarWarsBasicQueryHero() => StarWarsTests.BasicQueryHero(MemContext.CreateDefaultContext()); + [Test] public static void StarWarsBasicQueryHeroWithIdAndFriends() => StarWarsTests.BasicQueryHeroWithIdAndFriends(MemContext.CreateDefaultContext()); + [Test] public static void StarWarsBasicQueryHeroWithIdAndFriendsOfFriends() => - StarWarsTests.BasicQueryHeroWithIdAndFriendsOfFriends(MemContext.CreateDefaultContext()); + StarWarsTests.BasicQueryHeroWithFriendsOfFriends(MemContext.CreateDefaultContext()); + + [Test] + public static void StarWarsBasicQueryFetchLuke() => + StarWarsTests.BasicQueryFetchLuke(MemContext.CreateDefaultContext()); + + [Test] + public static void StarWarsFragmentsDuplicatedContent() => + StarWarsTests.FragmentsDuplicatedContent(MemContext.CreateDefaultContext()); + + [Test] + public static void StarWarsFragmentsAvoidDuplicatedContent() => + StarWarsTests.FragmentsAvoidDuplicatedContent(MemContext.CreateDefaultContext()); + + [Test] + public static void StarWarsTypenameR2Droid() => + StarWarsTests.TypenameR2Droid(MemContext.CreateDefaultContext()); + + [Test] + public static void StarWarsTypenameLukeHuman() => + StarWarsTests.TypenameLukeHuman(MemContext.CreateDefaultContext()); [Test] public void AddAllFields() diff --git a/Tests/StarWarsTestSchema.cs b/Tests/StarWarsTestSchema.cs index 2491c9a..8a953e7 100644 --- a/Tests/StarWarsTestSchema.cs +++ b/Tests/StarWarsTestSchema.cs @@ -121,9 +121,9 @@ public static ICollection CreateData() Name = "Luke Skywalker", AppearsIn = new List { + EpisodeEnum.NEWHOPE, EpisodeEnum.EMPIRE, - EpisodeEnum.JEDI, - EpisodeEnum.NEWHOPE + EpisodeEnum.JEDI }, HomePlanet = "Tatooine" }; @@ -133,9 +133,9 @@ public static ICollection CreateData() Name = "Darth Vader", AppearsIn = new List { + EpisodeEnum.NEWHOPE, EpisodeEnum.EMPIRE, - EpisodeEnum.JEDI, - EpisodeEnum.NEWHOPE + EpisodeEnum.JEDI }, HomePlanet = "Tatooine" }; @@ -145,9 +145,9 @@ public static ICollection CreateData() Name = "Han Solo", AppearsIn = new List { + EpisodeEnum.NEWHOPE, EpisodeEnum.EMPIRE, - EpisodeEnum.JEDI, - EpisodeEnum.NEWHOPE + EpisodeEnum.JEDI } }; var leia = new Human @@ -156,9 +156,9 @@ public static ICollection CreateData() Name = "Leia Organa", AppearsIn = new List { + EpisodeEnum.NEWHOPE, EpisodeEnum.EMPIRE, - EpisodeEnum.JEDI, - EpisodeEnum.NEWHOPE + EpisodeEnum.JEDI }, HomePlanet = "Alderaan" }; @@ -177,9 +177,9 @@ public static ICollection CreateData() Name = "C-3PO", AppearsIn = new List { + EpisodeEnum.NEWHOPE, EpisodeEnum.EMPIRE, - EpisodeEnum.JEDI, - EpisodeEnum.NEWHOPE + EpisodeEnum.JEDI }, PrimaryFunction = "Protocol" }; @@ -189,9 +189,9 @@ public static ICollection CreateData() Name = "R2-D2", AppearsIn = new List { + EpisodeEnum.NEWHOPE, EpisodeEnum.EMPIRE, - EpisodeEnum.JEDI, - EpisodeEnum.NEWHOPE + EpisodeEnum.JEDI }, PrimaryFunction = "Astromech" }; diff --git a/Tests/StarWarsTests.cs b/Tests/StarWarsTests.cs index 07f56af..530f2fd 100644 --- a/Tests/StarWarsTests.cs +++ b/Tests/StarWarsTests.cs @@ -16,12 +16,154 @@ public static void BasicQueryHeroWithIdAndFriends(GraphQL gq Test.DeepEquals(results, "{ hero: { id: '2001', name: 'R2-D2', friends: [ { name: 'Luke Skywalker' },{ name: 'Han Solo' }, { name: 'Leia Organa'} ] } }"); } - - public static void BasicQueryHeroWithIdAndFriendsOfFriends(GraphQL gql) + + public static void BasicQueryHeroWithFriendsOfFriends(GraphQL gql) { - var results = gql.ExecuteQuery("query HeroNameQuery { hero { id, name, friends { name, appearsIn, friends { name } } } }"); + var results = + gql.ExecuteQuery( + "query HeroNameQuery { hero { name, friends { name, appearsIn, friends { name } } } }"); Test.DeepEquals(results, - "{ hero: { id: '2001', name: 'R2-D2', friends: [ { name: 'Luke Skywalker' },{ name: 'Han Solo' }, { name: 'Leia Organa'} ] } }"); + @"{hero: { + name: 'R2-D2', + friends: [ + { + name: 'Luke Skywalker', + appearsIn: [ 'NEWHOPE', 'EMPIRE', 'JEDI' ], + friends: [ + { + name: 'Han Solo', + }, + { + name: 'Leia Organa', + }, + { + name: 'C-3PO', + }, + { + name: 'R2-D2', + } + ] + }, + { + name: 'Han Solo', + appearsIn: [ 'NEWHOPE', 'EMPIRE', 'JEDI' ], + friends: [ + { + name: 'Luke Skywalker', + }, + { + name: 'Leia Organa', + }, + { + name: 'R2-D2', + } + ] + }, + { + name: 'Leia Organa', + appearsIn: [ 'NEWHOPE', 'EMPIRE', 'JEDI' ], + friends: [ + { + name: 'Luke Skywalker', + }, + { + name: 'Han Solo', + }, + { + name: 'C-3PO', + }, + { + name: 'R2-D2', + } + ] + } + ] + } + }"); + } + + public static void BasicQueryFetchLuke(GraphQL gql) + { + var results = gql.ExecuteQuery("query FetchLukeQuery { human(id: \"1000\") { name } }"); + Test.DeepEquals(results, + "{ human: { name: 'Luke Skywalker' } }"); + } + + public static void FragmentsDuplicatedContent(GraphQL gql) + { + var results = gql.ExecuteQuery( + @" + query DuplicateFields { + luke: human(id: ""1000"") { + name + homePlanet + } + leia: human(id: ""1003"") { + name + homePlanet + } + } + " + ); + Test.DeepEquals(results, + @" + { + luke: { + name: 'Luke Skywalker', + homePlanet: 'Tatooine' + }, + leia: { + name: 'Leia Organa', + homePlanet: 'Alderaan' + } + } + "); + } + + public static void FragmentsAvoidDuplicatedContent(GraphQL gql) + { + var results = gql.ExecuteQuery( + @"query UseFragment { + luke: human(id: ""1000"") { + ...HumanFragment + } + leia: human(id: ""1003"") { + ...HumanFragment + } + } + fragment HumanFragment on Human { + name + homePlanet + } + " + ); + Test.DeepEquals(results, + @" + { + luke: { + name: 'Luke Skywalker', + homePlanet: 'Tatooine' + }, + leia: { + name: 'Leia Organa', + homePlanet: 'Alderaan' + } + } + "); + } + + public static void TypenameR2Droid(GraphQL gql) + { + var results = gql.ExecuteQuery("query CheckTypeOfR2 { hero { __typename, name } }"); + Test.DeepEquals(results, + "{ hero: { __typename: 'Droid', name: 'R2-D2' } }"); + } + + public static void TypenameLukeHuman(GraphQL gql) + { + var results = gql.ExecuteQuery("query CheckTypeOfLuke { hero(episode: EMPIRE) { __typename, name } }"); + Test.DeepEquals(results, + "{ hero: { __typename: 'Human', name: 'Luke Skywalker' } }"); } } } \ No newline at end of file From c8459283a2eb04aba5bba9b9becfe59e60dfb4d1 Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Sun, 3 Sep 2017 18:21:16 +0200 Subject: [PATCH 15/33] Add more star wars unit tests. --- GraphQL.Net/GraphQLSchema.cs | 5 +- Tests/GenericTests.cs | 139 +++----------------------------- Tests/InMemoryExecutionTests.cs | 13 +-- Tests/IntrospectionTests.cs | 36 +++++++-- Tests/StarWarsTests.cs | 40 ++++++++- 5 files changed, 90 insertions(+), 143 deletions(-) diff --git a/GraphQL.Net/GraphQLSchema.cs b/GraphQL.Net/GraphQLSchema.cs index cf6d2a7..2c7d018 100644 --- a/GraphQL.Net/GraphQLSchema.cs +++ b/GraphQL.Net/GraphQLSchema.cs @@ -132,11 +132,12 @@ private void AddDefaultExpressionOptions() // these will execute in reverse order // Default - WithExpressionOptions(t => true, castAssignment: true, nullCheckLists: false, typeCheckInheritance: false); + WithExpressionOptions(t => true, castAssignment: true, nullCheckLists: false, typeCheckInheritance: true); // In-memory against IEnumerable or Schema (introspection) WithExpressionOptions(t => t.FullName.StartsWith("System.Linq.EnumerableQuery") - || t.FullName.StartsWith("GraphQL.Parser"), castAssignment: true, nullCheckLists: true, typeCheckInheritance: true); + || t.FullName.StartsWith("GraphQL.Parser"), + castAssignment: true, nullCheckLists: true, typeCheckInheritance: true); // Entity Framework WithExpressionOptions(t => t.FullName.StartsWith("System.Data.Entity"), castAssignment: false, nullCheckLists: false, typeCheckInheritance: false); diff --git a/Tests/GenericTests.cs b/Tests/GenericTests.cs index 68ffcee..6e49833 100644 --- a/Tests/GenericTests.cs +++ b/Tests/GenericTests.cs @@ -122,7 +122,8 @@ public static void GuidField(GraphQL gql) public static void GuidParameter(GraphQL gql) { - var results = gql.ExecuteQuery("{ accountsByGuid(guid:\"00000000-0000-0000-0000-000000000000\") { id, someGuid } }"); + var results = + gql.ExecuteQuery("{ accountsByGuid(guid:\"00000000-0000-0000-0000-000000000000\") { id, someGuid } }"); Test.DeepEquals(results, @"{ accountsByGuid: [ { id: 1, someGuid: '00000000-0000-0000-0000-000000000000' }, @@ -144,13 +145,15 @@ public static void EnumFieldQuery(GraphQL gql) public static void ByteArrayParameter(GraphQL gql) { var results = gql.ExecuteQuery("{ account(id:1) { id, byteArray } }"); - Test.DeepEquals(results, "{ account: { id: 1, byteArray: 'AQIDBA==' } }"); // [1, 2, 3, 4] serialized to base64 by Json.NET + Test.DeepEquals(results, + "{ account: { id: 1, byteArray: 'AQIDBA==' } }"); // [1, 2, 3, 4] serialized to base64 by Json.NET } public static void ChildListFieldWithParameters(GraphQL gql) { var results = gql.ExecuteQuery("{ account(id:1) { id, name, usersWithActive(active:true) { id, name } } }"); - Test.DeepEquals(results, "{ account: { id: 1, name: 'My Test Account', usersWithActive: [{ id: 1, name: 'Joe User' }] } }"); + Test.DeepEquals(results, + "{ account: { id: 1, name: 'My Test Account', usersWithActive: [{ id: 1, name: 'Joe User' }] } }"); results = gql.ExecuteQuery("{ account(id:1) { id, name, usersWithActive(active:false) { id, name } } }"); Test.DeepEquals(results, "{ account: { id: 1, name: 'My Test Account', usersWithActive: [] } }"); @@ -158,130 +161,14 @@ public static void ChildListFieldWithParameters(GraphQL gql) public static void ChildFieldWithParameters(GraphQL gql) { - var results = gql.ExecuteQuery("{ account(id:1) { id, name, firstUserWithActive(active:true) { id, name } } }"); - Test.DeepEquals(results, "{ account: { id: 1, name: 'My Test Account', firstUserWithActive: { id: 1, name: 'Joe User' } } }"); + var results = + gql.ExecuteQuery("{ account(id:1) { id, name, firstUserWithActive(active:true) { id, name } } }"); + Test.DeepEquals(results, + "{ account: { id: 1, name: 'My Test Account', firstUserWithActive: { id: 1, name: 'Joe User' } } }"); - results = gql.ExecuteQuery("{ account(id:1) { id, name, firstUserWithActive(active:false) { id, name } } }"); + results = gql.ExecuteQuery( + "{ account(id:1) { id, name, firstUserWithActive(active:false) { id, name } } }"); Test.DeepEquals(results, "{ account: { id: 1, name: 'My Test Account', firstUserWithActive: null } }"); } - - public static void Fragments(GraphQL gql) - { - var results = gql.ExecuteQuery( - "{ heros { ...human, ...stormtrooper, ...droid } }, " + - "fragment human on Human { name, height, __typename }, " + - "fragment stormtrooper on Stormtrooper { name, height, specialization, __typename }, " + - "fragment droid on Droid { name, primaryFunction, __typename }"); - Test.DeepEquals( - results, - "{ heros: [ " + - "{ name: 'Han Solo', height: 5.6430448, __typename: 'Human' }, " + - "{ name: 'FN-2187', height: 4.9, specialization: 'Imperial Snowtrooper', __typename: 'Stormtrooper' }, " + - "{ name: 'R2-D2', primaryFunction: 'Astromech', __typename: 'Droid' } ] }" - ); - } - - public static void InlineFragments(GraphQL gql) - { - var results = gql.ExecuteQuery( - "{ heros { __typename, ... on Droid { name }, ... on Human { name, height }, ... on Stormtrooper { name, height, specialization }, " + - "... on Droid { primaryFunction } } }"); - Test.DeepEquals( - results, - "{ heros: [ " + - "{ __typename: 'Human', name: 'Han Solo', height: 5.6430448 }, " + - "{ __typename: 'Stormtrooper', name: 'FN-2187', height: 4.9, specialization: 'Imperial Snowtrooper' }, " + - "{ __typename: 'Droid', name: 'R2-D2', primaryFunction: 'Astromech' } ] }" - ); - } - - public static void InlineFragmentWithListField(GraphQL gql) - { - var results = gql.ExecuteQuery( - "{ heros { __typename, ... on Droid { name }, ... on Human { name, height, vehicles { name } }, ... on Stormtrooper { name, height, vehicles { name }, specialization } " + - "... on Droid { primaryFunction } } }"); - Test.DeepEquals( - results, - "{ heros: [ " + - "{ __typename: 'Human', name: 'Han Solo', height: 5.6430448, vehicles: [ { name: 'Millennium falcon' } ] }, " + - "{ __typename: 'Stormtrooper', name: 'FN-2187', height: 4.9, vehicles: [ { name: 'Speeder bike' } ], specialization: 'Imperial Snowtrooper' }, " + - "{ __typename: 'Droid', name: 'R2-D2', primaryFunction: 'Astromech' } ] }" - ); - } - - public static void FragmentWithMultiLevelInheritance(GraphQL gql) - { - var results = gql.ExecuteQuery("{ heros { ... on ICharacter { name, __typename }, ... on Stormtrooper { height, specialization } } }"); - Test.DeepEquals( - results, - "{ heros: [ " + - "{ name: 'Han Solo', __typename: 'Human'}, " + - "{ name: 'FN-2187', __typename: 'Stormtrooper', height: 4.9, specialization: 'Imperial Snowtrooper'}, " + - "{ name: 'R2-D2', __typename: 'Droid' } ] }" - ); - } - - public static void InlineFragmentWithoutTypenameField(GraphQL gql) - { - var results = gql.ExecuteQuery("{ heros { ... on Human { name }, ... on Droid { name }, ... on Stormtrooper { name, height, specialization } } }"); - Test.DeepEquals( - results, - "{ heros: [ " + - "{ name: 'Han Solo'}, " + - "{ name: 'FN-2187', height: 4.9, specialization: 'Imperial Snowtrooper'}, " + - "{ name: 'R2-D2' } ] }" - ); - } - - public static void InlineFragmentWithoutTypenameFieldWithoutOtherFields(GraphQL gql) - { - var results = gql.ExecuteQuery("{ heros { ... on Stormtrooper { height, specialization } } }"); - Test.DeepEquals( - results, - "{ heros: [ " + - "{ }, " + - "{ height: 4.9, specialization: 'Imperial Snowtrooper'}, " + - "{ } ] }" - ); - } - - public static void FragmentWithoutTypenameField(GraphQL gql) - { - var results = gql.ExecuteQuery( - "{ heros { ...human, ...droid, ...stormtrooper } }, fragment human on Human { name }, fragment droid on Droid { name }, fragment stormtrooper on Stormtrooper { name, height, specialization } "); - Test.DeepEquals( - results, - "{ heros: [ " + - "{ name: 'Han Solo',}, " + - "{ name: 'FN-2187', height: 4.9, specialization: 'Imperial Snowtrooper'}, " + - "{ name: 'R2-D2', } ] }" - ); - } - - public static void FragmentWithMultipleTypenameFields(GraphQL gql) - { - var results = gql.ExecuteQuery( - "{ heros { ...human, ...droid, ...stormtrooper, __typename } }, fragment human on Human { name }, fragment droid on Droid { name }, fragment stormtrooper on Stormtrooper { name, height, specialization, __typename } "); - Test.DeepEquals( - results, - "{ heros: [ " + - "{ name: 'Han Solo', __typename: 'Human'}, " + - "{ name: 'FN-2187', height: 4.9, specialization: 'Imperial Snowtrooper', __typename: 'Stormtrooper'}, " + - "{ name: 'R2-D2', __typename: 'Droid'} ] }" - ); - } - - public static void FragmentWithMultipleTypenameFieldsMixedWithInlineFragment(GraphQL gql) - { - var results = gql.ExecuteQuery( - "{ heros { ...stormtrooper, ... on Human {name}, ... on Droid {name}, __typename}}, fragment stormtrooper on Stormtrooper { name, height, specialization, __typename } "); - Test.DeepEquals( - results, - "{ heros: [ " + - "{ name: 'Han Solo', __typename: 'Human' }, " + - "{ name: 'FN-2187', height: 4.9, specialization: 'Imperial Snowtrooper', __typename: 'Stormtrooper' }, " + - "{ name: 'R2-D2', __typename: 'Droid' } ] }" - ); - } } -} +} \ No newline at end of file diff --git a/Tests/InMemoryExecutionTests.cs b/Tests/InMemoryExecutionTests.cs index 477e12a..fa64304 100644 --- a/Tests/InMemoryExecutionTests.cs +++ b/Tests/InMemoryExecutionTests.cs @@ -32,15 +32,6 @@ public class InMemoryExecutionTests [Test] public void ByteArrayParameter() => GenericTests.ByteArrayParameter(MemContext.CreateDefaultContext()); [Test] public void ChildListFieldWithParameters() => GenericTests.ChildListFieldWithParameters(MemContext.CreateDefaultContext()); [Test] public void ChildFieldWithParameters() => GenericTests.ChildFieldWithParameters(MemContext.CreateDefaultContext()); - [Test] public static void Fragments() => GenericTests.Fragments(MemContext.CreateDefaultContext()); - [Test] public static void InlineFragments() => GenericTests.InlineFragments(MemContext.CreateDefaultContext()); - [Test] public static void InlineFragmentWithListField() => GenericTests.InlineFragmentWithListField(MemContext.CreateDefaultContext()); - [Test] public static void InlineFragmentWithoutTypenameField() => GenericTests.InlineFragmentWithoutTypenameField(MemContext.CreateDefaultContext()); - [Test] public static void FragmentWithoutTypenameField() => GenericTests.FragmentWithoutTypenameField(MemContext.CreateDefaultContext()); - [Test] public static void InlineFragmentWithoutTypenameFieldWithoutOtherFields() => GenericTests.InlineFragmentWithoutTypenameFieldWithoutOtherFields(MemContext.CreateDefaultContext()); - [Test] public static void FragmentWithMultipleTypenameFields() => GenericTests.FragmentWithMultipleTypenameFields(MemContext.CreateDefaultContext()); - [Test] public static void FragmentWithMultipleTypenameFieldsMixedWithInlineFragment() => GenericTests.FragmentWithMultipleTypenameFieldsMixedWithInlineFragment(MemContext.CreateDefaultContext()); - [Test] public static void StarWarsBasicQueryHero() => StarWarsTests.BasicQueryHero(MemContext.CreateDefaultContext()); @@ -64,6 +55,10 @@ public static void StarWarsFragmentsDuplicatedContent() => [Test] public static void StarWarsFragmentsAvoidDuplicatedContent() => StarWarsTests.FragmentsAvoidDuplicatedContent(MemContext.CreateDefaultContext()); + + [Test] + public static void StarWarsFragmentsInlineFragments() => + StarWarsTests.FragmentsInlineFragments(MemContext.CreateDefaultContext()); [Test] public static void StarWarsTypenameR2Droid() => diff --git a/Tests/IntrospectionTests.cs b/Tests/IntrospectionTests.cs index 9c2509c..5ed8505 100644 --- a/Tests/IntrospectionTests.cs +++ b/Tests/IntrospectionTests.cs @@ -265,14 +265,14 @@ public void FieldArgsQuery() ""name"": ""hero"", ""args"": [ { - ""name"": ""id"", + ""name"": ""episode"", ""description"": null, ""type"": { ""name"": null, ""kind"": ""NON_NULL"", ""ofType"": { - ""name"": ""Int"", - ""kind"": ""SCALAR"" + ""name"": ""EpisodeEnum"", + ""kind"": ""ENUM"" } }, ""defaultValue"": null @@ -280,8 +280,34 @@ public void FieldArgsQuery() ] }, { - ""name"": ""heros"", - ""args"": [] + ""name"": ""human"", + ""args"": [ + { + ""name"": ""id"", + ""description"": null, + ""type"": { + ""name"": ""String"", + ""kind"": ""SCALAR"", + ""ofType"": null + }, + ""defaultValue"": null + } + ] + }, + { + ""name"": ""droid"", + ""args"": [ + { + ""name"": ""id"", + ""description"": null, + ""type"": { + ""name"": ""String"", + ""kind"": ""SCALAR"", + ""ofType"": null + }, + ""defaultValue"": null + } + ] }, { ""name"": ""__schema"", diff --git a/Tests/StarWarsTests.cs b/Tests/StarWarsTests.cs index 530f2fd..dc82f83 100644 --- a/Tests/StarWarsTests.cs +++ b/Tests/StarWarsTests.cs @@ -152,13 +152,51 @@ fragment HumanFragment on Human { "); } + public static void FragmentsInlineFragments(GraphQL gql) + { + var results = gql.ExecuteQuery( + @"query UseInlineFragments { + luke: hero(episode: EMPIRE) { + ...HumanFragment, + ...DroidFragment + } + r2d2: hero { + ...HumanFragment, + ...DroidFragment + } + } + fragment HumanFragment on Human { + name + homePlanet + } + fragment DroidFragment on Droid { + name + primaryFunction + } + " + ); + Test.DeepEquals(results, + @" + { + luke: { + name: 'Luke Skywalker', + homePlanet: 'Tatooine' + }, + r2d2: { + name: 'R2-D2', + primaryFunction: 'Astromech' + } + } + "); + } + public static void TypenameR2Droid(GraphQL gql) { var results = gql.ExecuteQuery("query CheckTypeOfR2 { hero { __typename, name } }"); Test.DeepEquals(results, "{ hero: { __typename: 'Droid', name: 'R2-D2' } }"); } - + public static void TypenameLukeHuman(GraphQL gql) { var results = gql.ExecuteQuery("query CheckTypeOfLuke { hero(episode: EMPIRE) { __typename, name } }"); From 29977d9679a3768766cdba38beef80f98a162f54 Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Sun, 3 Sep 2017 19:56:59 +0200 Subject: [PATCH 16/33] Add more star wars unit tests. --- Tests/InMemoryExecutionTests.cs | 12 +++++++++++ Tests/StarWarsTests.cs | 38 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/Tests/InMemoryExecutionTests.cs b/Tests/InMemoryExecutionTests.cs index fa64304..de75d92 100644 --- a/Tests/InMemoryExecutionTests.cs +++ b/Tests/InMemoryExecutionTests.cs @@ -68,6 +68,18 @@ public static void StarWarsTypenameR2Droid() => public static void StarWarsTypenameLukeHuman() => StarWarsTests.TypenameLukeHuman(MemContext.CreateDefaultContext()); + [Test] + public static void StarWarsIntrospectionDroidType() => + StarWarsTests.IntrospectionDroidType(MemContext.CreateDefaultContext()); + + [Test] + public static void StarWarsIntrospectionDroidTypeKind() => + StarWarsTests.IntrospectionDroidTypeKind(MemContext.CreateDefaultContext()); + + [Test] + public static void StarWarsIntrospectionCharacterInterface() => + StarWarsTests.IntrospectionCharacterInterface(MemContext.CreateDefaultContext()); + [Test] public void AddAllFields() { diff --git a/Tests/StarWarsTests.cs b/Tests/StarWarsTests.cs index dc82f83..a2c1fd0 100644 --- a/Tests/StarWarsTests.cs +++ b/Tests/StarWarsTests.cs @@ -203,5 +203,43 @@ public static void TypenameLukeHuman(GraphQL gql) Test.DeepEquals(results, "{ hero: { __typename: 'Human', name: 'Luke Skywalker' } }"); } + + public static void IntrospectionDroidType(GraphQL gql) + { + var results = gql.ExecuteQuery( + @"query IntrospectionDroidTypeQuery { + __type(name: ""Droid"") { + name + } + }"); + Test.DeepEquals(results, + "{ __type: { name: 'Droid' } }"); + } + + public static void IntrospectionDroidTypeKind(GraphQL gql) + { + var results = gql.ExecuteQuery( + @"query IntrospectionDroidKindQuery { + __type(name: ""Droid"") { + name + kind + } + }"); + Test.DeepEquals(results, + "{ __type: { name: 'Droid', kind: 'OBJECT' } }"); + } + + public static void IntrospectionCharacterInterface(GraphQL gql) + { + var results = gql.ExecuteQuery( + @"query IntrospectionCharacterKindQuery { + __type(name: ""ICharacter"") { + name + kind + } + }"); + Test.DeepEquals(results, + "{ __type: { name: 'ICharacter', kind: 'INTERFACE' } }"); + } } } \ No newline at end of file From acf49e19db94c65a6e77795b46e905d5ef58e79f Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Sun, 3 Sep 2017 19:58:14 +0200 Subject: [PATCH 17/33] Update EF unit tests. --- Tests.EF/EntityFrameworkExecutionTests.cs | 46 ++++++++++++++++------- Tests/InMemoryExecutionTests.cs | 1 + 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/Tests.EF/EntityFrameworkExecutionTests.cs b/Tests.EF/EntityFrameworkExecutionTests.cs index a3e6a03..27738e4 100644 --- a/Tests.EF/EntityFrameworkExecutionTests.cs +++ b/Tests.EF/EntityFrameworkExecutionTests.cs @@ -231,34 +231,52 @@ public void ChildFieldWithParameters() => GenericTests.ChildFieldWithParameters(MemContext.CreateDefaultContext()); [Test] - public static void Fragments() => GenericTests.Fragments(CreateDefaultContext()); + public static void StarWarsBasicQueryHero() => + StarWarsTests.BasicQueryHero(MemContext.CreateDefaultContext()); [Test] - public static void InlineFragments() => GenericTests.InlineFragments(CreateDefaultContext()); + public static void StarWarsBasicQueryHeroWithIdAndFriends() => + StarWarsTests.BasicQueryHeroWithIdAndFriends(MemContext.CreateDefaultContext()); [Test] - public static void InlineFragmentWithListField() => - GenericTests.InlineFragmentWithListField(CreateDefaultContext()); + public static void StarWarsBasicQueryHeroWithIdAndFriendsOfFriends() => + StarWarsTests.BasicQueryHeroWithFriendsOfFriends(MemContext.CreateDefaultContext()); [Test] - public static void InlineFragmentWithoutTypenameField() => - GenericTests.InlineFragmentWithoutTypenameField(CreateDefaultContext()); + public static void StarWarsBasicQueryFetchLuke() => + StarWarsTests.BasicQueryFetchLuke(MemContext.CreateDefaultContext()); + + [Test] + public static void StarWarsFragmentsDuplicatedContent() => + StarWarsTests.FragmentsDuplicatedContent(MemContext.CreateDefaultContext()); + + [Test] + public static void StarWarsFragmentsAvoidDuplicatedContent() => + StarWarsTests.FragmentsAvoidDuplicatedContent(MemContext.CreateDefaultContext()); + + [Test] + public static void StarWarsFragmentsInlineFragments() => + StarWarsTests.FragmentsInlineFragments(MemContext.CreateDefaultContext()); + + [Test] + public static void StarWarsTypenameR2Droid() => + StarWarsTests.TypenameR2Droid(MemContext.CreateDefaultContext()); [Test] - public static void FragmentWithoutTypenameField() => - GenericTests.FragmentWithoutTypenameField(CreateDefaultContext()); + public static void StarWarsTypenameLukeHuman() => + StarWarsTests.TypenameLukeHuman(MemContext.CreateDefaultContext()); [Test] - public static void InlineFragmentWithoutTypenameFieldWithoutOtherFields() => - GenericTests.InlineFragmentWithoutTypenameFieldWithoutOtherFields(CreateDefaultContext()); + public static void StarWarsIntrospectionDroidType() => + StarWarsTests.IntrospectionDroidType(MemContext.CreateDefaultContext()); [Test] - public static void FragmentWithMultipleTypenameFields() => - GenericTests.FragmentWithMultipleTypenameFields(CreateDefaultContext()); + public static void StarWarsIntrospectionDroidTypeKind() => + StarWarsTests.IntrospectionDroidTypeKind(MemContext.CreateDefaultContext()); [Test] - public static void FragmentWithMultipleTypenameFieldsMixedWithInlineFragment() => - GenericTests.FragmentWithMultipleTypenameFieldsMixedWithInlineFragment(CreateDefaultContext()); + public static void StarWarsIntrospectionCharacterInterface() => + StarWarsTests.IntrospectionCharacterInterface(MemContext.CreateDefaultContext()); [Test] public void AddAllFields() diff --git a/Tests/InMemoryExecutionTests.cs b/Tests/InMemoryExecutionTests.cs index de75d92..1da05dc 100644 --- a/Tests/InMemoryExecutionTests.cs +++ b/Tests/InMemoryExecutionTests.cs @@ -32,6 +32,7 @@ public class InMemoryExecutionTests [Test] public void ByteArrayParameter() => GenericTests.ByteArrayParameter(MemContext.CreateDefaultContext()); [Test] public void ChildListFieldWithParameters() => GenericTests.ChildListFieldWithParameters(MemContext.CreateDefaultContext()); [Test] public void ChildFieldWithParameters() => GenericTests.ChildFieldWithParameters(MemContext.CreateDefaultContext()); + [Test] public static void StarWarsBasicQueryHero() => StarWarsTests.BasicQueryHero(MemContext.CreateDefaultContext()); From 9295ececc140f82c96b9bde184ef6490e4ce986f Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Thu, 14 Sep 2017 19:43:01 +0200 Subject: [PATCH 18/33] Make unit test green. --- GraphQL.Parser.Test/SchemaTest.fs | 203 ++++++++++++++++-------------- 1 file changed, 107 insertions(+), 96 deletions(-) diff --git a/GraphQL.Parser.Test/SchemaTest.fs b/GraphQL.Parser.Test/SchemaTest.fs index f717116..36b4e11 100644 --- a/GraphQL.Parser.Test/SchemaTest.fs +++ b/GraphQL.Parser.Test/SchemaTest.fs @@ -19,134 +19,131 @@ //LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE //SOFTWARE. - namespace GraphQL.Parser.Test -open NUnit.Framework + open GraphQL.Parser +open NUnit.Framework // Tests that the schema resolution code works as expected with a pretend schema. - // Metadata type of our fake schema is just a string. type FakeData = string -type NameArgument() = +type NameArgument() = interface ISchemaArgument with member this.ArgumentName = "name" member this.ArgumentType = (PrimitiveType StringType).Nullable() member this.Description = Some "argument for filtering by name" member this.Info = "Fake name arg info" -type IdArgument() = +type IdArgument() = interface ISchemaArgument with member this.ArgumentName = "id" member this.ArgumentType = (PrimitiveType IntType).NotNullable() member this.Description = Some "argument for filtering by id" member this.Info = "Fake id arg info" -type UserType() = - member private this.Field(name, fieldType : SchemaFieldType, args) = +type UserType() = + + member private this.Field(name, fieldType : SchemaFieldType, args) = { new ISchemaField with - member __.DeclaringType = upcast this - member __.FieldType = fieldType - member __.FieldName = name - member __.IsList = false - member __.Description = Some ("Description of " + name) - member __.Info = "Info for " + name - member __.Arguments = args |> dictionary :> _ - member __.EstimateComplexity(args) = - match fieldType with - | ValueField _ -> Exactly(0L) - | QueryField _ -> - if args |> Seq.exists(fun a -> a.ArgumentName = "id") then - Exactly(1L) - else if args |> Seq.exists(fun a -> a.ArgumentName = "name") then - Range(0L, 30L) - else - Range(1L, 100L) - } + member __.DeclaringType = upcast this + member __.FieldType = fieldType + member __.FieldName = name + member __.IsList = false + member __.Description = Some("Description of " + name) + member __.Info = "Info for " + name + member __.Arguments = args |> dictionary :> _ + member __.EstimateComplexity(args) = + match fieldType with + | ValueField _ -> Exactly(0L) + | QueryField _ -> + if args |> Seq.exists (fun a -> a.ArgumentName = "id") then Exactly(1L) + else if args |> Seq.exists (fun a -> a.ArgumentName = "name") then Range(0L, 30L) + else Range(1L, 100L) } + interface ISchemaQueryType with member this.TypeName = "User" member this.Description = Some "Complex user type" member this.Info = "Fake user type info" - member this.Fields = - [| - "id", this.Field("id", ValueField (RootTypeHandler.Default.VariableTypeOf(typeof)), [||]) - "name", this.Field("name", ValueField (RootTypeHandler.Default.VariableTypeOf(typeof)), [||]) - "friend", this.Field("friend", QueryField (this :> ISchemaQueryType<_>), - [| - "name", new NameArgument() :> _ - "id", new IdArgument() :> _ - |]) - |] |> dictionary :> _ + + member this.Fields = + [| "id", this.Field("id", ValueField(RootTypeHandler.Default.VariableTypeOf(typeof)), [||]) + "name", this.Field("name", ValueField(RootTypeHandler.Default.VariableTypeOf(typeof)), [||]) + "friend", + this.Field("friend", QueryField(this :> ISchemaQueryType<_>), + [| "name", new NameArgument() :> _ + "id", new IdArgument() :> _ |]) |] + |> dictionary :> _ + member this.PossibleTypes = Seq.empty member this.Interfaces = Seq.empty -type RootType() = - member private this.Field(name, fieldType : SchemaFieldType, args) = +type RootType() = + + member private this.Field(name, fieldType : SchemaFieldType, args) = { new ISchemaField with - member __.DeclaringType = upcast this - member __.FieldType = fieldType - member __.FieldName = name - member __.IsList = false - member __.Description = Some ("Description of " + name) - member __.Info = "Info for " + name - member __.Arguments = args |> dictionary :> _ - member __.EstimateComplexity(args) = - if args |> Seq.exists(fun a -> a.ArgumentName = "id") then - Exactly(1L) - else - Range(1L, 100L) - } + member __.DeclaringType = upcast this + member __.FieldType = fieldType + member __.FieldName = name + member __.IsList = false + member __.Description = Some("Description of " + name) + member __.Info = "Info for " + name + member __.Arguments = args |> dictionary :> _ + member __.EstimateComplexity(args) = + if args |> Seq.exists (fun a -> a.ArgumentName = "id") then Exactly(1L) + else Range(1L, 100L) } + interface ISchemaQueryType with member this.TypeName = "Root" member this.Description = Some "Root context type" member this.Info = "Fake root type info" - member this.Fields = - [| - "user", this.Field("user", QueryField (new UserType()), - [| - "name", new NameArgument() :> _ - "id", new IdArgument() :> _ - |]) - |] |> dictionary :> _ + + member this.Fields = + [| "user", + this.Field("user", QueryField(new UserType()), + [| "name", new NameArgument() :> _ + "id", new IdArgument() :> _ |]) |] + |> dictionary :> _ + member this.PossibleTypes = Seq.empty member this.Interfaces = Seq.empty -type FakeSchema() = +type FakeSchema() = let root = new RootType() :> ISchemaQueryType<_> - let types = - [ - root - new UserType() :> _ - ] |> Seq.map (fun t -> t.TypeName, t) |> dictionary + + let types = + [ root + new UserType() :> _ ] + |> Seq.map (fun t -> t.TypeName, t) + |> dictionary + interface ISchema with member this.Directives = emptyDictionary member this.VariableTypes = emptyDictionary member this.QueryTypes = upcast types member this.ResolveEnumValueByName(name) = None // no enums member this.RootType = root - [] -type SchemaTest() = +type SchemaTest() = let schema = new FakeSchema() :> ISchema<_> - let good source = + + let good source = let doc = GraphQLDocument.Parse(schema, source) - if doc.Operations.Count <= 0 then - failwith "No operations in document!" + if doc.Operations.Count <= 0 then failwith "No operations in document!" for op in doc.Operations do printfn "Operation complexity: %A" (op.Value.EstimateComplexity()) - let bad reason source = - try + + let bad reason source = + try ignore <| GraphQLDocument.Parse(schema, source) failwith "Document resolved against schema when it shouldn't have!" - with - | :? SourceException as ex -> + with :? SourceException as ex -> if (ex.Message.Contains(reason)) then () else reraise() + [] - member __.TestGoodUserQuery() = - good @" + member __.TestGoodUserQuery() = good @" { user(id: 1) { id @@ -158,10 +155,9 @@ type SchemaTest() = } } " - + [] - member __.TestBogusArgument() = - bad "unknown argument ``occupation''" @" + member __.TestBogusArgument() = bad "unknown argument ``occupation''" @" { user(id: 1) { id @@ -173,10 +169,9 @@ type SchemaTest() = } } " - + [] - member __.TestBogusArgumentType() = - bad "invalid argument ``id''" @" + member __.TestBogusArgumentType() = bad "invalid argument ``id''" @" { user(id: 1) { id @@ -188,10 +183,9 @@ type SchemaTest() = } } " - + [] - member __.TestBogusRootField() = - bad "``team'' is not a field of type ``Root''" @" + member __.TestBogusRootField() = bad "``team'' is not a field of type ``Root''" @" { team { id @@ -199,10 +193,9 @@ type SchemaTest() = } } " - + [] - member __.TestBogusSubField() = - bad "``parent'' is not a field of type ``User''" @" + member __.TestBogusSubField() = bad "``parent'' is not a field of type ``User''" @" { user { id @@ -214,10 +207,9 @@ type SchemaTest() = } } " - + [] - member __.TestRecursionDepth() = - bad "exceeded maximum recursion depth" @" + member __.TestRecursionDepth() = bad "exceeded maximum recursion depth" @" { user { friend(name: ""bob"") { @@ -231,8 +223,28 @@ type SchemaTest() = friend(name: ""bob"") { friend(name: ""bob"") { friend(name: ""bob"") { - id - name + friend(name: ""bob"") { + friend(name: ""bob"") { + friend(name: ""bob"") { + friend(name: ""bob"") { + friend(name: ""bob"") { + friend(name: ""bob"") { + friend(name: ""bob"") { + friend(name: ""bob"") { + friend(name: ""bob"") { + friend(name: ""bob"") { + id + name + } + } + } + } + } + } + } + } + } + } } } } @@ -247,10 +259,9 @@ type SchemaTest() = } } " - + [] - member __.TestRecursionBan() = - bad "fragment ``friendNamedBobForever'' is recursive" @" + member __.TestRecursionBan() = bad "fragment ``friendNamedBobForever'' is recursive" @" fragment friendNamedBobForever on User { friend(name: ""bob"") { ...friendNamedBobForever @@ -261,4 +272,4 @@ fragment friendNamedBobForever on User { ...friendNamedBobForever } } -" \ No newline at end of file +" From 21e6c17c31fff4b8d3d5cb16cd85a880d1b6bbf1 Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Thu, 14 Sep 2017 19:45:56 +0200 Subject: [PATCH 19/33] Revert special handling of EF proxies in executor. --- GraphQL.Net/Executor.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/GraphQL.Net/Executor.cs b/GraphQL.Net/Executor.cs index e6f3022..057dc99 100644 --- a/GraphQL.Net/Executor.cs +++ b/GraphQL.Net/Executor.cs @@ -23,13 +23,7 @@ public static object Execute // sniff queryable provider to determine how selector should be built var dummyQuery = replaced.Compile().DynamicInvoke(context, null); - var queryType = dummyQuery.GetType(); - if (queryType.Namespace == "System.Data.Entity.DynamicProxies") - { - queryType = queryType.BaseType; - } - var queryExecSelections = query.Selections.Values(); var selector = GetSelector(schema, field.Type, queryExecSelections, schema.GetOptionsForQueryable(queryType)); From 7dbb076124c558a317006d8c0d97a99a0282da0a Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Thu, 14 Sep 2017 20:18:32 +0200 Subject: [PATCH 20/33] [wip] start implementation of introspection mutation type. --- GraphQL.Net/GraphQL.Net.csproj | 2 ++ GraphQL.Net/SchemaAdapters/Schema.cs | 6 +++++ .../SchemaAdapters/SchemaMutationType.cs | 25 +++++++++++++++++++ GraphQL.Net/SchemaAdapters/SchemaQueryType.cs | 25 +++++++++++++++++++ GraphQL.Parser.Test/SchemaTest.fs | 4 +++ GraphQL.Parser/Integration/CS.fs | 4 +++ GraphQL.Parser/Schema/SchemaAST.fs | 2 ++ GraphQL.Parser/SchemaTools/Introspection.fs | 4 +-- 8 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 GraphQL.Net/SchemaAdapters/SchemaMutationType.cs create mode 100644 GraphQL.Net/SchemaAdapters/SchemaQueryType.cs diff --git a/GraphQL.Net/GraphQL.Net.csproj b/GraphQL.Net/GraphQL.Net.csproj index 9b90f64..36dcc75 100644 --- a/GraphQL.Net/GraphQL.Net.csproj +++ b/GraphQL.Net/GraphQL.Net.csproj @@ -62,6 +62,8 @@ + + diff --git a/GraphQL.Net/SchemaAdapters/Schema.cs b/GraphQL.Net/SchemaAdapters/Schema.cs index 47211d5..60ccb39 100644 --- a/GraphQL.Net/SchemaAdapters/Schema.cs +++ b/GraphQL.Net/SchemaAdapters/Schema.cs @@ -10,6 +10,7 @@ namespace GraphQL.Net.SchemaAdapters abstract class Schema : SchemaCS { internal readonly GraphQLSchema GraphQLSchema; + protected Schema(GraphQLSchema schema) { GraphQLSchema = schema; @@ -24,6 +25,7 @@ public SchemaType OfType(GraphQLType type) return _typeMap[type] = new SchemaType(type, this); } } + class Schema : Schema { private readonly GraphQLSchema _schema; @@ -32,6 +34,8 @@ class Schema : Schema public Schema(GraphQLSchema schema) : base(schema) { RootType = new SchemaRootType(this, schema.GetGQLType(typeof(TContext))); + QueryType = new SchemaQueryType(this, schema.GetGQLType(typeof(TContext))); + MutationType = new SchemaMutationType(this, schema.GetGQLType(typeof(TContext))); _schema = schema; _queryTypes = schema.Types .Where(t => t.TypeKind != TypeKind.SCALAR) @@ -46,6 +50,8 @@ public override IReadOnlyDictionary VariableTypes => _schema.VariableTypes.TypeDictionary; public override ISchemaQueryType RootType { get; } + public override ISchemaQueryType QueryType { get; } + public override ISchemaQueryType MutationType { get; } public override EnumValue ResolveEnumValue(string name) { diff --git a/GraphQL.Net/SchemaAdapters/SchemaMutationType.cs b/GraphQL.Net/SchemaAdapters/SchemaMutationType.cs new file mode 100644 index 0000000..7857681 --- /dev/null +++ b/GraphQL.Net/SchemaAdapters/SchemaMutationType.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using GraphQL.Parser; +using GraphQL.Parser.CS; + +namespace GraphQL.Net.SchemaAdapters +{ + class SchemaMutationType : SchemaQueryTypeCS + { + public SchemaMutationType(Schema schema, GraphQLType baseQueryType) + { + Fields = baseQueryType.Fields + .Where(f => f.IsMutation) + .Select(f => new SchemaField(this, f, schema)) + .ToDictionary(f => f.FieldName, f => f as ISchemaField); + } + + public override IReadOnlyDictionary> Fields { get; } + public override string TypeName => "queryType"; + + public override IEnumerable> PossibleTypes => new Collection>(); + public override IEnumerable> Interfaces => new Collection>(); + } +} \ No newline at end of file diff --git a/GraphQL.Net/SchemaAdapters/SchemaQueryType.cs b/GraphQL.Net/SchemaAdapters/SchemaQueryType.cs new file mode 100644 index 0000000..28d9ee7 --- /dev/null +++ b/GraphQL.Net/SchemaAdapters/SchemaQueryType.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using GraphQL.Parser; +using GraphQL.Parser.CS; + +namespace GraphQL.Net.SchemaAdapters +{ + class SchemaQueryType : SchemaQueryTypeCS + { + public SchemaQueryType(Schema schema, GraphQLType baseQueryType) + { + Fields = baseQueryType.Fields + .Where(f => !f.IsMutation) + .Select(f => new SchemaField(this, f, schema)) + .ToDictionary(f => f.FieldName, f => f as ISchemaField); + } + + public override IReadOnlyDictionary> Fields { get; } + public override string TypeName => "queryType"; + + public override IEnumerable> PossibleTypes => new Collection>(); + public override IEnumerable> Interfaces => new Collection>(); + } +} diff --git a/GraphQL.Parser.Test/SchemaTest.fs b/GraphQL.Parser.Test/SchemaTest.fs index 36b4e11..cd45b01 100644 --- a/GraphQL.Parser.Test/SchemaTest.fs +++ b/GraphQL.Parser.Test/SchemaTest.fs @@ -110,6 +110,8 @@ type RootType() = type FakeSchema() = let root = new RootType() :> ISchemaQueryType<_> + let query = new RootType() :> ISchemaQueryType<_> + let mutation = new RootType() :> ISchemaQueryType<_> let types = [ root @@ -123,6 +125,8 @@ type FakeSchema() = member this.QueryTypes = upcast types member this.ResolveEnumValueByName(name) = None // no enums member this.RootType = root + member this.QueryType = query + member this.MutationType = mutation [] type SchemaTest() = diff --git a/GraphQL.Parser/Integration/CS.fs b/GraphQL.Parser/Integration/CS.fs index 23b1b83..3bc55c1 100644 --- a/GraphQL.Parser/Integration/CS.fs +++ b/GraphQL.Parser/Integration/CS.fs @@ -39,12 +39,16 @@ type SchemaCS<'s>() = abstract member ResolveEnumValue : name : string -> EnumValue default this.ResolveEnumValue(_) = Unchecked.defaultof<_> abstract member RootType : ISchemaQueryType<'s> + abstract member QueryType : ISchemaQueryType<'s> + abstract member MutationType : ISchemaQueryType<'s> interface ISchema<'s> with member this.Directives = this.Directives member this.QueryTypes = this.QueryTypes member this.VariableTypes = this.VariableTypes member this.ResolveEnumValueByName(name) = this.ResolveEnumValue(name) |> obj2option member this.RootType = this.RootType + member this.QueryType = this.QueryType + member this.MutationType = this.MutationType [] type SchemaQueryTypeCS<'s>() = diff --git a/GraphQL.Parser/Schema/SchemaAST.fs b/GraphQL.Parser/Schema/SchemaAST.fs index c3a82a3..6464008 100644 --- a/GraphQL.Parser/Schema/SchemaAST.fs +++ b/GraphQL.Parser/Schema/SchemaAST.fs @@ -170,6 +170,8 @@ and ISchema<'s> = /// The top-level type that queries select from. /// Most likely this will correspond to your DB context type. abstract member RootType : ISchemaQueryType<'s> + abstract member QueryType : ISchemaQueryType<'s> + abstract member MutationType : ISchemaQueryType<'s> /// A value within the GraphQL document. This is fully resolved, not a variable reference. and Value = | PrimitiveValue of Primitive diff --git a/GraphQL.Parser/SchemaTools/Introspection.fs b/GraphQL.Parser/SchemaTools/Introspection.fs index 629fd46..fb907a9 100644 --- a/GraphQL.Parser/SchemaTools/Introspection.fs +++ b/GraphQL.Parser/SchemaTools/Introspection.fs @@ -219,8 +219,8 @@ type IntroSchema = schema.VariableTypes.Values |> Seq.map IntroType.Of schema.QueryTypes.Values |> Seq.map IntroType.Of ] |> Seq.concat - QueryType = IntroType.Of(schema.RootType) - MutationType = None // TODO: support mutation schema + QueryType = IntroType.Of(schema.QueryType) + MutationType = IntroType.Of(schema.MutationType) |> Some SubscriptionType = None // TODO: support subscription schema Directives = schema.Directives.Values |> Seq.map IntroDirective.Of From 32809f427e7a7908cb82c0ae85958af263753bbe Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Mon, 18 Sep 2017 09:55:02 +0200 Subject: [PATCH 21/33] Fix typo. --- GraphQL.Net/GraphQL.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GraphQL.Net/GraphQL.cs b/GraphQL.Net/GraphQL.cs index d6299f7..6469b26 100644 --- a/GraphQL.Net/GraphQL.cs +++ b/GraphQL.Net/GraphQL.cs @@ -52,7 +52,7 @@ public IDictionary ExecuteQuery(string queryStr, TContext queryC throw new InvalidOperationException("Schema must be Completed before executing a query. Try calling the schema's Complete method."); if (queryContext == null) - throw new ArgumentException("Contexst must not be null."); + throw new ArgumentException("Context must not be null."); var document = GraphQLDocument.Parse(_schema.Adapter, queryStr); var context = DefaultExecContext.Instance; // TODO use a real IExecContext to support passing variables From 192afd3b550ad7382fdb27b6e3fa3b0f3b77e83a Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Mon, 18 Sep 2017 09:55:13 +0200 Subject: [PATCH 22/33] Fix unit tests assertion. --- Tests/IntrospectionTests.cs | 105 +++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 50 deletions(-) diff --git a/Tests/IntrospectionTests.cs b/Tests/IntrospectionTests.cs index 5ed8505..ba15594 100644 --- a/Tests/IntrospectionTests.cs +++ b/Tests/IntrospectionTests.cs @@ -95,7 +95,8 @@ public void SchemaTypes() public void FieldArgsQuery() { var gql = MemContext.CreateDefaultContext(); - var results = gql.ExecuteQuery("{ __schema { queryType { fields { name, args { name, description, type { name, kind, ofType { name, kind } }, defaultValue } } } } }"); + var results = gql.ExecuteQuery( + "query SchemaQuery { __schema { queryType {... TypeFragment }, mutationType {... TypeFragment } } }, fragment TypeFragment on __Type { fields { name, args { name, description, type { name, kind, ofType { name, kind } }, defaultValue } } }"); Test.DeepEquals( results, @@ -212,55 +213,6 @@ public void FieldArgsQuery() } ] }, - { - ""name"": ""mutate"", - ""args"": [ - { - ""name"": ""id"", - ""description"": null, - ""type"": { - ""name"": null, - ""kind"": ""NON_NULL"", - ""ofType"": { - ""name"": ""Int"", - ""kind"": ""SCALAR"" - } - }, - ""defaultValue"": null - }, - { - ""name"": ""newVal"", - ""description"": null, - ""type"": { - ""name"": null, - ""kind"": ""NON_NULL"", - ""ofType"": { - ""name"": ""Int"", - ""kind"": ""SCALAR"" - } - }, - ""defaultValue"": null - } - ] - }, - { - ""name"": ""addMutate"", - ""args"": [ - { - ""name"": ""newVal"", - ""description"": null, - ""type"": { - ""name"": null, - ""kind"": ""NON_NULL"", - ""ofType"": { - ""name"": ""Int"", - ""kind"": ""SCALAR"" - } - }, - ""defaultValue"": null - } - ] - }, { ""name"": ""hero"", ""args"": [ @@ -333,6 +285,59 @@ public void FieldArgsQuery() ""args"": [] } ] + }, + ""mutationType"": { + ""fields"": [ + { + ""name"": ""mutate"", + ""args"": [ + { + ""name"": ""id"", + ""description"": null, + ""type"": { + ""name"": null, + ""kind"": ""NON_NULL"", + ""ofType"": { + ""name"": ""Int"", + ""kind"": ""SCALAR"" + } + }, + ""defaultValue"": null + }, + { + ""name"": ""newVal"", + ""description"": null, + ""type"": { + ""name"": null, + ""kind"": ""NON_NULL"", + ""ofType"": { + ""name"": ""Int"", + ""kind"": ""SCALAR"" + } + }, + ""defaultValue"": null + } + ] + }, + { + ""name"": ""addMutate"", + ""args"": [ + { + ""name"": ""newVal"", + ""description"": null, + ""type"": { + ""name"": null, + ""kind"": ""NON_NULL"", + ""ofType"": { + ""name"": ""Int"", + ""kind"": ""SCALAR"" + } + }, + ""defaultValue"": null + } + ] + } + ] } } }"); From 2ad4c565f234386ed7118bf250acaeb06592f9e9 Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Mon, 18 Sep 2017 10:39:11 +0200 Subject: [PATCH 23/33] Fix EF unit test setup for star wars schema. --- Tests.EF/EntityFrameworkExecutionTests.cs | 3 +++ Tests/StarWarsTestSchema.cs | 23 +++++++---------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/Tests.EF/EntityFrameworkExecutionTests.cs b/Tests.EF/EntityFrameworkExecutionTests.cs index 27738e4..bdb6e7e 100644 --- a/Tests.EF/EntityFrameworkExecutionTests.cs +++ b/Tests.EF/EntityFrameworkExecutionTests.cs @@ -313,6 +313,9 @@ public EfContext() : base("DefaultConnection") protected override void OnModelCreating(DbModelBuilder modelBuilder) { Database.SetInitializer(new SqliteDropCreateDatabaseWhenModelChanges(modelBuilder)); + modelBuilder.Entity() + .HasMany(c => c.Friends) + .WithMany(); base.OnModelCreating(modelBuilder); } diff --git a/Tests/StarWarsTestSchema.cs b/Tests/StarWarsTestSchema.cs index 8a953e7..e04fcab 100644 --- a/Tests/StarWarsTestSchema.cs +++ b/Tests/StarWarsTestSchema.cs @@ -53,32 +53,23 @@ public enum EpisodeEnum JEDI = 6 } - public interface ICharacter - { - string Id { get; set; } - string Name { get; set; } - ICollection Friends { get; set; } - ICollection AppearsIn { get; set; } - string SecretBackstory { get; set; } - } - - public class Human : ICharacter + // Character "interface" declared as an abstract class to support EF tests + public abstract class ICharacter { public string Id { get; set; } public string Name { get; set; } - public ICollection Friends { get; set; } + public virtual ICollection Friends { get; set; } public ICollection AppearsIn { get; set; } public string SecretBackstory { get; set; } + } + + public class Human : ICharacter + { public string HomePlanet { get; set; } } public class Droid : ICharacter { - public string Id { get; set; } - public string Name { get; set; } - public ICollection Friends { get; set; } - public ICollection AppearsIn { get; set; } - public string SecretBackstory { get; set; } public string PrimaryFunction { get; set; } } From d6bcda5da03edf8c16d027ce190fedd2e94fec3c Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Tue, 19 Sep 2017 07:36:17 +0000 Subject: [PATCH 24/33] Updates docs/queries_and_mutations/inline-fragments.md Auto commit by GitBook Editor --- SUMMARY.md | 17 ++-- .../queries_and_mutations/inline-fragments.md | 85 ++++++++----------- 2 files changed, 46 insertions(+), 56 deletions(-) diff --git a/SUMMARY.md b/SUMMARY.md index 10bd135..2c76b1c 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -3,14 +3,15 @@ * [Read Me](README.md) * [Introduction](docs/introduction/README.md) * [Basics](docs/basics/README.md) - * [Build a Schema](docs/basics/build_a_schema.md) - * [Executing a Query](docs/basics/executing_a_query.md) + * [Build a Schema](docs/basics/build_a_schema.md) + * [Executing a Query](docs/basics/executing_a_query.md) * [Queries and Mutations](docs/queries_and_mutations/README.md) - * [Fields](docs/queries_and_mutations/fields.md) - * [Arguments](docs/queries_and_mutations/arguments.md) - * [Aliases](docs/queries_and_mutations/aliases.md) - * [Fragments](docs/queries_and_mutations/fragments.md) - * [Mutations](docs/queries_and_mutations/mutations.md) - * [Inline Fragments](docs/queries_and_mutations/inline-fragments.md) + * [Fields](docs/queries_and_mutations/fields.md) + * [Arguments](docs/queries_and_mutations/arguments.md) + * [Aliases](docs/queries_and_mutations/aliases.md) + * [Fragments](docs/queries_and_mutations/fragments.md) + * [Mutations](docs/queries_and_mutations/mutations.md) + * [Inline Fragments](docs/queries_and_mutations/inline-fragments.md) + * Changelog * [Development](docs/development/README.md) diff --git a/docs/queries_and_mutations/inline-fragments.md b/docs/queries_and_mutations/inline-fragments.md index cf73e00..eb744d8 100644 --- a/docs/queries_and_mutations/inline-fragments.md +++ b/docs/queries_and_mutations/inline-fragments.md @@ -1,9 +1,10 @@ -#Inline Fragments +# Inline Fragments [GraphQL docs](http://graphql.org/learn/queries/#inline-fragments): ->Like many other type systems, GraphQL schemas include the ability to define interfaces and union types. Learn about them in the schema guide. ->If you are querying a field that returns an interface or a union type, you will need to use inline fragments to access data on the underlying concrete type. +> Like many other type systems, GraphQL schemas include the ability to define interfaces and union types. Learn about them in the schema guide. +> +> If you are querying a field that returns an interface or a union type, you will need to use inline fragments to access data on the underlying concrete type. Let's look at an example query with inline fragments: @@ -14,9 +15,6 @@ query Heros { ... on Droid { primaryFunction } - ... on Stormtrooper { - specialization - } ... on Human { height } @@ -25,6 +23,7 @@ query Heros { ``` The expected result looks like this: + ```json { "heros": [ @@ -32,11 +31,6 @@ The expected result looks like this: "name": "Han Solo", "height": 5.6430448 }, - { - "name": "FN-2187", - "specialization": "Imperial Snowtrooper", - "height": 4.9 - }, { "name": "R2-D2", "primaryFunction": "Astromech" @@ -46,53 +40,48 @@ The expected result looks like this: ``` The date model can be implemented as follows: + ```csharp -class Character +interface ICharacter // NOTE: to support EF, this might be an abstract class { - public int Id { get; set; } - public string Name { get; set; } + int Id { get; set; } + string Name { get; set; } } -class Human : Character +class Human : ICharacter { + public int Id { get; set; } + public string Name { get; set; } public double Height { get; set; } } -class Stormtrooper : Human -{ - public string Specialization { get; set; } -} -class Droid : Character +class Droid : ICharacter { + public int Id { get; set; } + public string Name { get; set; } public string PrimaryFunction { get; set; } } ``` With the following context and data: + ```csharp class Context { - public IList Heros { get; set; } + public IList Heros { get; set; } } ... var defaultContext = new Context { - Heros = new List { + Heros = new List { new Human { Id = 1, Name = "Han Solo", Height = 5.6430448 }, - new Stormtrooper - { - Id = 2, - Name = "FN-2187", - Height = 4.9, - Specialization = "Imperial Snowtrooper" - }, new Droid { Id = 3, @@ -104,13 +93,21 @@ var defaultContext = new Context ``` The schema can be defined as follows: + ```csharp var schema = GraphQL.CreateDefaultSchema(() => defaultContext); -schema.AddType().AddAllFields(); -schema.AddType().AddAllFields(); -schema.AddType().AddAllFields(); -schema.AddType().AddAllFields(); - schema.AddListField( +var characterInterface = schema.AddInterfaceType(); +characterInterface.AddAllFields(); + +var humanType = schema.AddType(); +humanType.AddAllFields(); +humanType.AddInterface(characterInterface); + +var droidType = schema.AddType(); +droidType.AddAllFields(); +droidType.AddInterface(characterInterface); + +schema.AddListField( "heros", db => db.Heros.AsQueryable() ); @@ -118,6 +115,7 @@ schema.Complete(); ``` Now we can run the query: + ```csharp var gql = new GraphQL(schema); var queryResult = gql.ExecuteQuery( @@ -127,9 +125,6 @@ var queryResult = gql.ExecuteQuery( ... on Droid { primaryFunction } - ... on Stormtrooper { - specialization - } ... on Human { height } @@ -141,14 +136,8 @@ var queryResult = gql.ExecuteQuery( The result is as expected, see examples/08-inline-fragments for a running example. > **Note:** -> -> If two types with a common base type (which may be `object`) both have a property with the same name but with different types, the schema builder will raise an error: -> ``` ->at GraphQL.Net.GraphQLSchema`1.CompleteType(GraphQLType type) - at GraphQL.Net.GraphQLSchema`1.CompleteTypes(IEnumerable`1 types) - at GraphQL.Net.GraphQLSchema`1.Complete() - at _08_inline_fragments.InlineFragmentsExample.RunExample() -Result Message: System.ArgumentException : The type 'Character' has multiple fields named 'test' with different types. ->``` -> This may be supported in the future. -> As a workaround the properties can be added to the grapqhl types using different field names. +> +> Prior to version 0.3.6 class hierarchy has been automatically reflected in GraphQL types which are defined in the schema. This has been removed, since defining inheritance hierarchy in GraphQL's type system is difficult to implement \(and may not be desired\). + + + From d0e1e18f16ea0f95672660f59e335eb417653944 Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Tue, 19 Sep 2017 07:48:47 +0000 Subject: [PATCH 25/33] Updates schema-and-types/interfaces.md Auto commit by GitBook Editor --- SUMMARY.md | 5 ++++- docs/queries_and_mutations/changelog.md | 20 ++++++++++++++++++++ docs/queries_and_mutations/interfaces.md | 0 docs/queries_and_mutations/union-types.md | 0 schema-and-types.md | 6 ++++++ schema-and-types/interfaces.md | 6 ++++++ schema-and-types/union-types.md | 6 ++++++ 7 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 docs/queries_and_mutations/changelog.md create mode 100644 docs/queries_and_mutations/interfaces.md create mode 100644 docs/queries_and_mutations/union-types.md create mode 100644 schema-and-types.md create mode 100644 schema-and-types/interfaces.md create mode 100644 schema-and-types/union-types.md diff --git a/SUMMARY.md b/SUMMARY.md index 2c76b1c..9db9390 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -12,6 +12,9 @@ * [Fragments](docs/queries_and_mutations/fragments.md) * [Mutations](docs/queries_and_mutations/mutations.md) * [Inline Fragments](docs/queries_and_mutations/inline-fragments.md) - * Changelog +* [Schema and Types](schema-and-types.md) + * [Interfaces](schema-and-types/interfaces.md) + * [Union types](schema-and-types/union-types.md) +* [Changelog](docs/queries_and_mutations/changelog.md) * [Development](docs/development/README.md) diff --git a/docs/queries_and_mutations/changelog.md b/docs/queries_and_mutations/changelog.md new file mode 100644 index 0000000..de68a8d --- /dev/null +++ b/docs/queries_and_mutations/changelog.md @@ -0,0 +1,20 @@ +# Changelog + +## 0.3.6 + +* **Breaking Changes:** + + * Automatic reflect class hierarchy in GraphQL types which have been added to the schema has been removed. Use `GraphQLSchema.AddInterfaceType` and `GraphQLSchema.AddUnionType` to migrate your schema if necessary. + +* **Additional API:** + + * `GraphQLSchema.AddInterfaceType` : See documentation [Interfaces](/schema-and-types/interfaces.md). + + * `GraphQLSchema.AddUnionType`: See documentation [Union types](/schema-and-types/union-types.md). + +* **Improvements:** + + * Better support of introspection. + + + diff --git a/docs/queries_and_mutations/interfaces.md b/docs/queries_and_mutations/interfaces.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/queries_and_mutations/union-types.md b/docs/queries_and_mutations/union-types.md new file mode 100644 index 0000000..e69de29 diff --git a/schema-and-types.md b/schema-and-types.md new file mode 100644 index 0000000..8ccd4a7 --- /dev/null +++ b/schema-and-types.md @@ -0,0 +1,6 @@ +# Schema and Types + + + + + diff --git a/schema-and-types/interfaces.md b/schema-and-types/interfaces.md new file mode 100644 index 0000000..a32d1a5 --- /dev/null +++ b/schema-and-types/interfaces.md @@ -0,0 +1,6 @@ +# Interfaces + + + + + diff --git a/schema-and-types/union-types.md b/schema-and-types/union-types.md new file mode 100644 index 0000000..8d42b85 --- /dev/null +++ b/schema-and-types/union-types.md @@ -0,0 +1,6 @@ +# Union Types + + + + + From e291a233493429248b7908218b7afad190089342 Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Tue, 19 Sep 2017 07:52:42 +0000 Subject: [PATCH 26/33] Updates README.md Auto commit by GitBook Editor --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 309e916..0ecc68f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # GraphQL.Net An implementation of GraphQL for .NET and IQueryable -[docs](https://ckimes89.gitbooks.io/graphql-net/content) +[Gitbook documentation](https://ckimes89.gitbooks.io/graphql-net/content) ## Description Many of the .NET GraphQL implementations that have come out so far only seem to work in memory. @@ -144,6 +144,9 @@ Add GraphQL.Net to your project via the Package Manager Console. PM> Install-Package GraphQL.Net ``` +## Changelog +The changelog can be found [docs/changelog](/docs/queries_and_mutations/changelog.md). + ## TODO Support directives like @skip. Support field arguments with complex types. From 4f2345e1bcb718aaae5f7f64b7a01435f338858d Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Tue, 19 Sep 2017 07:54:00 +0000 Subject: [PATCH 27/33] Updates docs/queries_and_mutations/changelog.md Auto commit by GitBook Editor --- docs/queries_and_mutations/changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/queries_and_mutations/changelog.md b/docs/queries_and_mutations/changelog.md index de68a8d..a51f5bc 100644 --- a/docs/queries_and_mutations/changelog.md +++ b/docs/queries_and_mutations/changelog.md @@ -16,5 +16,7 @@ * Better support of introspection. +For details see PR \#83 \([https://github.com/ckimes89/graphql-net/pull/83](https://github.com/ckimes89/graphql-net/pull/83)\). + From 6787460291ddd3ed625b3c00e01c450d08606539 Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Tue, 19 Sep 2017 08:00:37 +0000 Subject: [PATCH 28/33] Updates docs/queries_and_mutations/inline-fragments.md Auto commit by GitBook Editor --- schema-and-types/interfaces.md | 52 ++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/schema-and-types/interfaces.md b/schema-and-types/interfaces.md index a32d1a5..8e8d3b6 100644 --- a/schema-and-types/interfaces.md +++ b/schema-and-types/interfaces.md @@ -1,6 +1,58 @@ # Interfaces +> Like many type systems, GraphQL supports interfaces. An \_Interface \_is an abstract type that includes a certain set of fields that a type must include to implement the interface. +[http://graphql.org/learn/schema/\#interfaces](http://graphql.org/learn/schema/#interfaces) +Consider the example interface `Character` from the GraphQL docs: +```graphql +interface Character { + id: ID! + name: String! + friends: [Character] + appearsIn: [Episode]! +} + +type Human implements Character { + id: ID! + name: String! + friends: [Character] + appearsIn: [Episode]! + starships: [Starship] + totalCredits: Int +} + +type Droid implements Character { + id: ID! + name: String! + friends: [Character] + appearsIn: [Episode]! + primaryFunction: String +} +``` + +The interface can be added to the schema using `GraphQLSchema.AddInterfaceType`: + +```csharp +var schema = GraphQL.CreateDefaultSchema(() => defaultContext); +var characterInterface = schema.AddInterfaceType(); +characterInterface.AddAllFields(); + +var humanType = schema.AddType(); +humanType.AddAllFields(); +humanType.AddInterface(characterInterface); + +var droidType = schema.AddType(); +droidType.AddAllFields(); +droidType.AddInterface(characterInterface); + +schema.AddListField( + "heros", + db => db.Heros.AsQueryable() + ); +schema.Complete(); +``` + +See [Inline Fragments](/docs/queries_and_mutations/inline-fragments.md) for usage with inline fragments. From efa1d5bedd8b5bf62a7176f9c1794e43f837c44e Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Tue, 19 Sep 2017 08:11:18 +0000 Subject: [PATCH 29/33] Updates schema-and-types/union-types.md Auto commit by GitBook Editor --- schema-and-types/union-types.md | 47 +++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/schema-and-types/union-types.md b/schema-and-types/union-types.md index 8d42b85..76ef9c6 100644 --- a/schema-and-types/union-types.md +++ b/schema-and-types/union-types.md @@ -1,6 +1,53 @@ # Union Types +> Union types are very similar to interfaces, but they don't get to specify any common fields between the types. +[http://graphql.org/learn/schema/\#union-types](http://graphql.org/learn/schema/#union-types) + +Consider the example union type `SearchResult` from the GraphQL docs: + +```graphql +union SearchResult = Human | Droid | Starship +``` + +.. and the example query: + +```graphql +{ + search(text: "an") { + ... on Human { + name + height + } + ... on Droid { + name + primaryFunction + } + ... on Starship { + name + length + } + } +} +``` + +The union type can be implemented using `GraphQLSchema.AddUnionType`: + +```csharp +var humanType = schema.AddType(); +humanType.AddAllFields(); + +var droidType = schema.AddType(); +droidType.AddAllFields(); + +var starshipType = schema.AddType(); +starshipType.AddAllFields(); + +var searchResult = schema.AddUnionType("SearchResult", new[] {droidType.GraphQLType, humanType.GraphQLType, starshipType.GraphQLType}); + +schema.AddField("searchResult", new {text = ""}, (db, args) => /* Search for and return human, droid or starship. */) + .WithReturnType(searchResult); +``` From 8f48c83a65b7c8b7b746d19208268ab489c7c67c Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Tue, 19 Sep 2017 08:38:27 +0000 Subject: [PATCH 30/33] Updates schema-and-types/union-types.md Auto commit by GitBook Editor --- schema-and-types/union-types.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/schema-and-types/union-types.md b/schema-and-types/union-types.md index 76ef9c6..5c0295c 100644 --- a/schema-and-types/union-types.md +++ b/schema-and-types/union-types.md @@ -36,7 +36,7 @@ The union type can be implemented using `GraphQLSchema.AddUnionType`: ```csharp var humanType = schema.AddType(); humanType.AddAllFields(); - + var droidType = schema.AddType(); droidType.AddAllFields(); @@ -45,8 +45,13 @@ starshipType.AddAllFields(); var searchResult = schema.AddUnionType("SearchResult", new[] {droidType.GraphQLType, humanType.GraphQLType, starshipType.GraphQLType}); -schema.AddField("searchResult", new {text = ""}, (db, args) => /* Search for and return human, droid or starship. */) - .WithReturnType(searchResult); + schema.AddField("search", new {text = ""}, + (db, args) => args.text == "starship" + ? new Starship() as object + : (args.text == "droid" + ? new Droid() as object + : new Human() as object)) + .WithReturnType(searchResult); ``` From 9810ea2e07a91526485a37a7c8bb3449306864dd Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Tue, 19 Sep 2017 10:39:52 +0200 Subject: [PATCH 31/33] Add unit test for union type. --- Tests.EF/EntityFrameworkExecutionTests.cs | 12 ++++++++ Tests/InMemoryExecutionTests.cs | 12 ++++++++ Tests/IntrospectionTests.cs | 15 ++++++++++ Tests/StarWarsTestSchema.cs | 24 +++++++++++++-- Tests/StarWarsTests.cs | 36 +++++++++++++++++++++++ 5 files changed, 97 insertions(+), 2 deletions(-) diff --git a/Tests.EF/EntityFrameworkExecutionTests.cs b/Tests.EF/EntityFrameworkExecutionTests.cs index bdb6e7e..1967877 100644 --- a/Tests.EF/EntityFrameworkExecutionTests.cs +++ b/Tests.EF/EntityFrameworkExecutionTests.cs @@ -277,6 +277,18 @@ public static void StarWarsIntrospectionDroidTypeKind() => [Test] public static void StarWarsIntrospectionCharacterInterface() => StarWarsTests.IntrospectionCharacterInterface(MemContext.CreateDefaultContext()); + + [Test] + public static void UnionTypeStarship() => + StarWarsTests.UnionTypeStarship(MemContext.CreateDefaultContext()); + + [Test] + public static void UnionTypeHuman() => + StarWarsTests.UnionTypeHuman(MemContext.CreateDefaultContext()); + + [Test] + public static void UnionTypeDroid() => + StarWarsTests.UnionTypeDroid(MemContext.CreateDefaultContext()); [Test] public void AddAllFields() diff --git a/Tests/InMemoryExecutionTests.cs b/Tests/InMemoryExecutionTests.cs index 1da05dc..32a0df5 100644 --- a/Tests/InMemoryExecutionTests.cs +++ b/Tests/InMemoryExecutionTests.cs @@ -81,6 +81,18 @@ public static void StarWarsIntrospectionDroidTypeKind() => public static void StarWarsIntrospectionCharacterInterface() => StarWarsTests.IntrospectionCharacterInterface(MemContext.CreateDefaultContext()); + [Test] + public static void UnionTypeStarship() => + StarWarsTests.UnionTypeStarship(MemContext.CreateDefaultContext()); + + [Test] + public static void UnionTypeHuman() => + StarWarsTests.UnionTypeHuman(MemContext.CreateDefaultContext()); + + [Test] + public static void UnionTypeDroid() => + StarWarsTests.UnionTypeDroid(MemContext.CreateDefaultContext()); + [Test] public void AddAllFields() { diff --git a/Tests/IntrospectionTests.cs b/Tests/IntrospectionTests.cs index ba15594..90a8333 100644 --- a/Tests/IntrospectionTests.cs +++ b/Tests/IntrospectionTests.cs @@ -261,6 +261,21 @@ public void FieldArgsQuery() } ] }, + { + ""name"": ""search"", + ""args"": [ + { + ""name"": ""text"", + ""description"": null, + ""type"": { + ""name"": ""String"", + ""kind"": ""SCALAR"", + ""ofType"": null + }, + ""defaultValue"": null + } + ] + }, { ""name"": ""__schema"", ""args"": [] diff --git a/Tests/StarWarsTestSchema.cs b/Tests/StarWarsTestSchema.cs index e04fcab..bd3c8d4 100644 --- a/Tests/StarWarsTestSchema.cs +++ b/Tests/StarWarsTestSchema.cs @@ -73,11 +73,18 @@ public class Droid : ICharacter public string PrimaryFunction { get; set; } } + public class Starship + { + public string Id { get; set; } + public string Name { get; set; } + public double Length { get; set; } + } + public static void Create(GraphQLSchema schema, Func> herosProviderFunc) { schema.AddEnum(); - + var characterInterface = schema.AddInterfaceType(); characterInterface.AddAllFields(); @@ -89,6 +96,13 @@ public static void Create(GraphQLSchema schema, droidType.AddAllFields(); droidType.AddInterface(characterInterface); + var starshipType = schema.AddType(); + starshipType.AddAllFields(); + + var searchResult = + schema.AddUnionType("SearchResult", + new[] {droidType.GraphQLType, humanType.GraphQLType, starshipType.GraphQLType}); + schema.AddField( "hero", new {episode = EpisodeEnum.NEWHOPE}, @@ -101,9 +115,15 @@ public static void Create(GraphQLSchema schema, (db, args) => herosProviderFunc(db).OfType().FirstOrDefault(c => c.Id == args.id)); schema.AddField("droid", new {id = ""}, (db, args) => herosProviderFunc(db).OfType().FirstOrDefault(c => c.Id == args.id)); + schema.AddField("search", new {text = ""}, + (db, args) => args.text == "starship" + ? new Starship() as object + : (args.text == "droid" + ? new Droid() as object + : new Human() as object)) + .WithReturnType(searchResult); } - public static ICollection CreateData() { var luke = new Human diff --git a/Tests/StarWarsTests.cs b/Tests/StarWarsTests.cs index a2c1fd0..492400e 100644 --- a/Tests/StarWarsTests.cs +++ b/Tests/StarWarsTests.cs @@ -241,5 +241,41 @@ public static void IntrospectionCharacterInterface(GraphQL g Test.DeepEquals(results, "{ __type: { name: 'ICharacter', kind: 'INTERFACE' } }"); } + + public static void UnionTypeStarship(GraphQL gql) + { + var results = gql.ExecuteQuery( + @"query UnionTypeStarshipQuery { + search(text: ""starship"") { + __typename + } + }"); + Test.DeepEquals(results, + "{ search: { __typename: 'Starship'} }"); + } + + public static void UnionTypeHuman(GraphQL gql) + { + var results = gql.ExecuteQuery( + @"query UnionTypeStarshipQuery { + search(text: ""human"") { + __typename + } + }"); + Test.DeepEquals(results, + "{ search: { __typename: 'Human'} }"); + } + + public static void UnionTypeDroid(GraphQL gql) + { + var results = gql.ExecuteQuery( + @"query UnionTypeStarshipQuery { + search(text: ""droid"") { + __typename + } + }"); + Test.DeepEquals(results, + "{ search: { __typename: 'Droid'} }"); + } } } \ No newline at end of file From 6e77e59eb4f9d18c4029968e114446583fe91132 Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Tue, 19 Sep 2017 08:47:43 +0000 Subject: [PATCH 32/33] Updates docs/queries_and_mutations/changelog.md Auto commit by GitBook Editor --- docs/queries_and_mutations/changelog.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/queries_and_mutations/changelog.md b/docs/queries_and_mutations/changelog.md index a51f5bc..277aebe 100644 --- a/docs/queries_and_mutations/changelog.md +++ b/docs/queries_and_mutations/changelog.md @@ -15,8 +15,7 @@ * **Improvements:** * Better support of introspection. + * Fix issue related to fields of type list of scalar For details see PR \#83 \([https://github.com/ckimes89/graphql-net/pull/83](https://github.com/ckimes89/graphql-net/pull/83)\). - - From e1dbed9cd1c5d08e56d4c862cc5a2b798da2c681 Mon Sep 17 00:00:00 2001 From: Marian Palkus Date: Mon, 6 Nov 2017 08:45:50 +0100 Subject: [PATCH 33/33] Update schema API for union types. --- GraphQL.Net/Executor.cs | 2 +- GraphQL.Net/GraphQL.Net.csproj | 1 - GraphQL.Net/GraphQLFieldBuilder.cs | 4 ++-- GraphQL.Net/GraphQLSchema.cs | 30 +++++++++++++++++++++--------- GraphQL.Net/GraphQLType.cs | 6 +++--- GraphQL.Net/GraphQLTypeBuilder.cs | 3 --- GraphQL.Net/IGraphQLType.cs | 12 ------------ Tests/StarWarsTestSchema.cs | 7 +++---- 8 files changed, 30 insertions(+), 35 deletions(-) delete mode 100644 GraphQL.Net/IGraphQLType.cs diff --git a/GraphQL.Net/Executor.cs b/GraphQL.Net/Executor.cs index ddaa0da..c3081fe 100644 --- a/GraphQL.Net/Executor.cs +++ b/GraphQL.Net/Executor.cs @@ -332,7 +332,7 @@ private static ConditionalExpression NullPropagate(Expression baseExpr, Expressi return Expression.Condition(equals, Expression.Constant(null, returnExpr.Type), returnExpr); } - private static string CreatePropertyName(IGraphQLType graphQlType, IGraphQLType typeConditionType, + private static string CreatePropertyName(GraphQLType graphQlType, GraphQLType typeConditionType, string fieldName) { var isObjectAbstractType = graphQlType.TypeKind == TypeKind.UNION || diff --git a/GraphQL.Net/GraphQL.Net.csproj b/GraphQL.Net/GraphQL.Net.csproj index 36dcc75..ab679eb 100644 --- a/GraphQL.Net/GraphQL.Net.csproj +++ b/GraphQL.Net/GraphQL.Net.csproj @@ -58,7 +58,6 @@ - diff --git a/GraphQL.Net/GraphQLFieldBuilder.cs b/GraphQL.Net/GraphQLFieldBuilder.cs index db88591..f2ca44f 100644 --- a/GraphQL.Net/GraphQLFieldBuilder.cs +++ b/GraphQL.Net/GraphQLFieldBuilder.cs @@ -24,9 +24,9 @@ public GraphQLFieldBuilder WithComplexity(long min, long max) return this; } - public GraphQLFieldBuilder WithReturnType(IGraphQLType type) + public GraphQLFieldBuilder WithReturnType(string graphQlTypeName) { - _field.SetReturnType(type as GraphQLType); + _field.SetReturnType(_field.Schema.GetGQLTypeByName(graphQlTypeName)); return this; } diff --git a/GraphQL.Net/GraphQLSchema.cs b/GraphQL.Net/GraphQLSchema.cs index 2c7d018..869f3d0 100644 --- a/GraphQL.Net/GraphQLSchema.cs +++ b/GraphQL.Net/GraphQLSchema.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; -using System.Reflection.Emit; using GraphQL.Net.SchemaAdapters; using GraphQL.Parser; @@ -13,6 +12,7 @@ public abstract class GraphQLSchema { internal readonly VariableTypes VariableTypes = new VariableTypes(); internal abstract GraphQLType GetGQLType(Type type); + internal abstract GraphQLType GetGQLTypeByName(string GraphQlTypeName); internal abstract GraphQLType GetGQLTypeByQueryType(Type queryType); } @@ -77,7 +77,7 @@ public GraphQLTypeBuilder AddType(string name = null return new GraphQLTypeBuilder(this, gqlType); } - public IGraphQLType AddUnionType(string name, IEnumerable possibleTypes, Type type= null, + public void AddUnionType(string name, IEnumerable possibleCLRTypes, Type type = null, string description = null) { if (_types.Any(t => t.Name == name)) @@ -88,10 +88,9 @@ public IGraphQLType AddUnionType(string name, IEnumerable possible TypeKind = TypeKind.UNION, Name = name, Description = description ?? "", - PossibleTypes = possibleTypes.Cast().ToList() + PossibleCLRTypes = possibleCLRTypes.ToList() }; _types.Add(gqlType); - return gqlType; } public GraphQLTypeBuilder AddInterfaceType(string name = null, string description = null) @@ -155,8 +154,10 @@ public void Complete() VariableTypes.Complete(); // The order is important: - // (1) Add the '__typename' field to every type. - // (2) Complete the types (generate the query-type). + // (1) Resolve possible Types of unions + // (2) Add the '__typename' field to every type. + // (3) Complete the types (generate the query-type). + ResolvePossibleTypesOfUnions(); AddTypeNameFields(); CompleteTypes(_types); @@ -164,13 +165,13 @@ public void Complete() Completed = true; } - private static void CompleteTypes(IEnumerable types) + private void CompleteTypes(IEnumerable types) { foreach (var type in types.Where(t => t.QueryType == null)) CompleteType(type); } - private static void CompleteType(GraphQLType type) + private void CompleteType(GraphQLType type) { // validation maybe perform somewhere else if (type.TypeKind == TypeKind.SCALAR && type.Fields.Count != 0) @@ -264,13 +265,21 @@ private void AddDefaultTypes() .FirstOrDefault(t => t.Name.OrDefault() == args.name)); } + private void ResolvePossibleTypesOfUnions() + { + foreach (var type in _types.Where(t => t.TypeKind == TypeKind.UNION)) + { + type.PossibleTypes = type.PossibleCLRTypes.Select(GetGQLType).ToList(); + } + } + private void AddTypeNameFields() { var method = GetType().GetMethod("AddTypeNameField", BindingFlags.Instance | BindingFlags.NonPublic); foreach (var type in _types.Where(t => t.TypeKind != TypeKind.SCALAR)) { var genMethod = method.MakeGenericMethod(type.CLRType); - genMethod.Invoke(this, new object[] { type }); + genMethod.Invoke(this, new object[] {type}); } } @@ -387,6 +396,9 @@ internal GraphQLFieldBuilder AddUnmodifiedMutationInternal _types.FirstOrDefault(t => t.CLRType == type) ?? new GraphQLType(type) {TypeKind = TypeKind.SCALAR}; + internal override GraphQLType GetGQLTypeByName(string name) + => _types.FirstOrDefault(t => t.Name == name); + internal override GraphQLType GetGQLTypeByQueryType(Type queryType) => _types.FirstOrDefault(t => t.QueryType == queryType) ?? diff --git a/GraphQL.Net/GraphQLType.cs b/GraphQL.Net/GraphQLType.cs index b4b2652..d5b89fc 100644 --- a/GraphQL.Net/GraphQLType.cs +++ b/GraphQL.Net/GraphQLType.cs @@ -1,18 +1,17 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; using GraphQL.Parser; namespace GraphQL.Net { - internal class GraphQLType : IGraphQLType + internal class GraphQLType { public GraphQLType(Type type) { CLRType = type; Name = type.Name; Fields = new List(); + PossibleCLRTypes = new List(); PossibleTypes = new List(); Interfaces = new List(); } @@ -20,6 +19,7 @@ public GraphQLType(Type type) public string Name { get; set; } public string Description { get; set; } public List Fields { get; set; } + public List PossibleCLRTypes { get; set; } public List PossibleTypes { get; set; } public List Interfaces { get; set; } public Type CLRType { get; set; } diff --git a/GraphQL.Net/GraphQLTypeBuilder.cs b/GraphQL.Net/GraphQLTypeBuilder.cs index 53109aa..fd66b4b 100644 --- a/GraphQL.Net/GraphQLTypeBuilder.cs +++ b/GraphQL.Net/GraphQLTypeBuilder.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -17,8 +16,6 @@ internal GraphQLTypeBuilder(GraphQLSchema schema, GraphQLType type) _type = type; } - public IGraphQLType GraphQLType => _type; - // This overload is provided to the user so they can shape TArgs with an anonymous type and rely on type inference for type parameters // e.g. AddField("profilePic", new { size = 0 }, (db, user) => db.ProfilePics.Where(p => p.UserId == u.Id && p.Size == args.size)); [Obsolete] diff --git a/GraphQL.Net/IGraphQLType.cs b/GraphQL.Net/IGraphQLType.cs deleted file mode 100644 index 23dbc92..0000000 --- a/GraphQL.Net/IGraphQLType.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using GraphQL.Parser; - -namespace GraphQL.Net -{ - public interface IGraphQLType - { - string Name { get; } - Type CLRType { get; } - TypeKind TypeKind { get; } - } -} \ No newline at end of file diff --git a/Tests/StarWarsTestSchema.cs b/Tests/StarWarsTestSchema.cs index bd3c8d4..68567f2 100644 --- a/Tests/StarWarsTestSchema.cs +++ b/Tests/StarWarsTestSchema.cs @@ -99,9 +99,8 @@ public static void Create(GraphQLSchema schema, var starshipType = schema.AddType(); starshipType.AddAllFields(); - var searchResult = - schema.AddUnionType("SearchResult", - new[] {droidType.GraphQLType, humanType.GraphQLType, starshipType.GraphQLType}); + schema.AddUnionType("SearchResult", + new[] {typeof(Droid), typeof(Human), typeof(Starship)}); schema.AddField( "hero", @@ -121,7 +120,7 @@ public static void Create(GraphQLSchema schema, : (args.text == "droid" ? new Droid() as object : new Human() as object)) - .WithReturnType(searchResult); + .WithReturnType("SearchResult"); } public static ICollection CreateData()