Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/EntityGraphQL/Compiler/EntityGraphQLQueryWalker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,34 @@ protected override void VisitFragmentDefinition(FragmentDefinitionNode node, IGr
base.VisitFragmentDefinition(node, fragDef);
}

protected override void VisitInlineFragment(InlineFragmentNode node, IGraphQLNode? context)
{
if (context == null)
throw new EntityGraphQLCompilerException("context should not be null visiting inline fragment");

if (node.TypeCondition is not null && context is not null)
{
var type = schemaProvider.GetSchemaType(node.TypeCondition.Name.Value, requestContext);
if (type != null)
{
var fragParameter = Expression.Parameter(type.TypeDotnet, $"frag_{type.Name}");
var newContext = new GraphQLListSelectionField((GraphQLListSelectionField)context!, fragParameter);
base.VisitInlineFragment(node, newContext);

//copy the fragment fields over to the select context and cast the type so we can access the property
foreach (var queryField in newContext.QueryFields)
{
var fieldResult = new GraphQLScalarField(schemaProvider, queryField.Field, queryField.Name, queryField.NextFieldContext!, queryField.NextFieldContext as ParameterExpression ?? context.RootParameter, context, queryField.Arguments as Dictionary<string, object>);
context.AddField(fieldResult);
}
}
else
{
base.VisitInlineFragment(node, context);
}
}
}

protected override void VisitFragmentSpread(FragmentSpreadNode node, IGraphQLNode? context)
{
if (context == null)
Expand Down
11 changes: 11 additions & 0 deletions src/EntityGraphQL/Compiler/GqlNodes/BaseGraphQLField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ public BaseGraphQLField(ISchemaProvider schema, IField? field, string name, Expr
Field = field;
}

public BaseGraphQLField(BaseGraphQLField context, Expression? nextFieldContext)
{
Name = context.Name;
NextFieldContext = nextFieldContext;
RootParameter = context.RootParameter;
ParentNode = context.ParentNode;
this.Arguments = context.Arguments ?? new Dictionary<string, object>();
this.schema = context.schema;
Field = context.Field;
}

/// <summary>
/// Field is a complex expression (using a method or function) that returns a single object (not IEnumerable)
/// We wrap this is a function that does a null check and avoid duplicate calls on the method/service
Expand Down
5 changes: 5 additions & 0 deletions src/EntityGraphQL/Compiler/GqlNodes/BaseGraphQLQueryField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ protected BaseGraphQLQueryField(ISchemaProvider schema, IField? field, string na
{
}

protected BaseGraphQLQueryField(BaseGraphQLQueryField context, Expression? nextFieldContext)
: base(context, nextFieldContext)
{
}

public override IEnumerable<BaseGraphQLField> Expand(CompileContext compileContext, List<GraphQLFragmentStatement> fragments, bool withoutServiceFields, Expression fieldContext, ParameterExpression? docParam, object? docVariables)
{
var result = ProcessFieldDirectives(this, docParam, docVariables);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ public GraphQLListSelectionField(ISchemaProvider schema, IField? field, string n
this.ListExpression = nodeExpression;
}

public GraphQLListSelectionField(GraphQLListSelectionField context, ParameterExpression? nextFieldContext)
: base(context, nextFieldContext)
{
this.ListExpression = context.ListExpression;
}

/// <summary>
/// The dotnet Expression for this node. Could be as simple as (Person p) => p.Name
/// Or as complex as (DbContext ctx) => ctx.People.Where(...).Select(p => new {...}).First()
Expand Down
113 changes: 103 additions & 10 deletions src/EntityGraphQL/Compiler/Util/ExpressionUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -378,22 +378,115 @@ public static Expression CombineExpressions(Expression baseExp, Expression nextE
return Tuple.Create(exp, endExpression);
}

class ConversionVisitor : ExpressionVisitor
{
private Expression _param;
private Type _type;

public ConversionVisitor(Expression param, Type type)
{
_param = param;
_type = type;
}
protected override Expression VisitMember(MemberExpression node)
{
//don't convert param for __typename
if (node.Member.DeclaringType == typeof(ISchemaType))
return node;

return Expression.PropertyOrField(Expression.Convert(_param, _type), node.Member.Name);
}

}

/// <summary>
/// Makes a selection from a IEnumerable context
/// </summary>
public static Expression MakeSelectWithDynamicType(ParameterExpression currentContextParam, Expression baseExp, IDictionary<string, Expression> fieldExpressions)
{
if (!fieldExpressions.Any())
return baseExp;

var memberInit = CreateNewExpression(fieldExpressions, out Type dynamicType);
if (memberInit == null || dynamicType == null) // nothing to select
return baseExp;
var selector = Expression.Lambda(memberInit, currentContextParam);
var isQueryable = typeof(IQueryable).IsAssignableFrom(baseExp.Type);
var call = isQueryable ? MakeCallOnQueryable("Select", new Type[] { currentContextParam.Type, dynamicType }, baseExp, selector) :
MakeCallOnEnumerable("Select", new Type[] { currentContextParam.Type, dynamicType }, baseExp, selector);
return call;
return baseExp;

//get a list of distinct types asked for in the query (fragments for interfaces)
var validTypes = fieldExpressions.Values.OfType<MemberExpression>()
.Where(i => currentContextParam.Type.IsAssignableFrom(((MemberExpression)i).Expression.Type))
.Select(i => i.Expression.Type)
.Distinct();

//If 0 or 1 valid types then default to basic behaviour
if (validTypes.Count() < 2)
{
var memberInit = CreateNewExpression(fieldExpressions, out Type dynamicType);
if (memberInit == null || dynamicType == null) // nothing to select
return baseExp;
var selector = Expression.Lambda(memberInit, currentContextParam);
var isQueryable = typeof(IQueryable).IsAssignableFrom(baseExp.Type);
var call = isQueryable ? MakeCallOnQueryable("Select", new Type[] { currentContextParam.Type, dynamicType }, baseExp, selector) :
MakeCallOnEnumerable("Select", new Type[] { currentContextParam.Type, dynamicType }, baseExp, selector);
return call;
}
//make a query that checks type of object and returns the valid properties for that specific type
else
{
var baseDynamicType = LinqRuntimeTypeBuilder.GetDynamicType(
fieldExpressions
.Where(i => i.Value is MemberExpression)
.Where(i => ((MemberExpression)i.Value).Expression.Type == currentContextParam.Type || ((MemberExpression)i.Value).Expression.Type == typeof(ISchemaType))
.ToDictionary(i => i.Key, i => i.Value.Type)

);
if (baseDynamicType == null)
throw new EntityGraphQLCompilerException("Could not create dynamic type");


Expression? previous = null;
foreach (var type in validTypes)
{
var fieldsOnType = fieldExpressions
.Where(i => i.Value is MemberExpression)
.Where(i => ((MemberExpression)i.Value).Expression.Type.IsAssignableFrom(type) || ((MemberExpression)i.Value).Expression.Type == typeof(ISchemaType))
.ToDictionary(i => i.Key, i => i.Value);

var conversionVisitor = new ConversionVisitor(currentContextParam, type);

var specificDynamicType = LinqRuntimeTypeBuilder.GetDynamicType(fieldsOnType.ToDictionary(i => i.Key, i => i.Value.Type), parentType: baseDynamicType);
var constructor = specificDynamicType.GetConstructor(Type.EmptyTypes);
if (constructor == null)
throw new EntityGraphQLCompilerException("Could not create dynamic type");

var bindings = specificDynamicType
.GetFields()
.Where(p => fieldsOnType.Keys.Contains(p.Name))
.Select(p => Expression.Bind(p,
conversionVisitor.Visit(fieldsOnType[p.Name])
))
.OfType<MemberBinding>();

var newExp = Expression.New(constructor);
var memberInit = Expression.MemberInit(newExp, bindings);

if (previous != null)
{
previous = Expression.Condition(
test: Expression.TypeIs(currentContextParam, type),
ifTrue: memberInit,
ifFalse: previous,
type: baseDynamicType
);
}
else
{
previous = memberInit;
}
}

var selector = Expression.Lambda(previous, currentContextParam);
var isQueryable = typeof(IQueryable).IsAssignableFrom(baseExp.Type);

var call = isQueryable ? MakeCallOnQueryable("Select", new Type[] { currentContextParam.Type, baseDynamicType }, baseExp, selector) :
MakeCallOnEnumerable("Select", new Type[] { currentContextParam.Type, baseDynamicType }, baseExp, selector);
return call;
}
}

public static Expression? CreateNewExpression(IDictionary<string, Expression> fieldExpressions, out Type dynamicType)
Expand Down
11 changes: 7 additions & 4 deletions src/EntityGraphQL/Compiler/Util/LinqRuntimeTypeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ private static string GetTypeKey(Dictionary<string, Type> fields)
return fields.OrderBy(f => f.Key).Aggregate("anon.", (current, field) => current + field.Key + field.Value.GetHashCode());
}

public static Type GetDynamicType(Dictionary<string, Type> fields, string? typeName = null)
public static Type GetDynamicType(Dictionary<string, Type> fields, string? typeName = null, Type? parentType = null)
{
if (null == fields)
throw new ArgumentNullException(nameof(fields));
if (0 == fields.Count)
if (0 == fields.Count && parentType == null)
throw new ArgumentOutOfRangeException(nameof(fields), "fields must have at least 1 field definition");

string className = typeName != null ? $"anon.{typeName}" : GetTypeKey(fields);
Expand All @@ -44,10 +44,13 @@ public static Type GetDynamicType(Dictionary<string, Type> fields, string? typeN
if (builtTypes.ContainsKey(classId))
return builtTypes[classId];

var typeBuilder = moduleBuilder.DefineType(classId.ToString(), TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Serializable);

var typeBuilder = moduleBuilder.DefineType(classId.ToString(), TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Serializable, parentType);
foreach (var field in fields)
{
if (parentType != null && parentType.GetField(field.Key) != null)
continue;

typeBuilder.DefineField(field.Key, field.Value, FieldAttributes.Public);
}

Expand Down
8 changes: 6 additions & 2 deletions src/EntityGraphQL/Compiler/Util/ParameterReplacer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,12 @@ public Expression ReplaceByType(Expression node, Type toReplaceType, Expression

protected override Expression VisitParameter(ParameterExpression node)
{
if (toReplace != null && toReplace == node)
return newParam!;
if (toReplace != null && toReplace == node)
//leave param type so we can do conversions later,
//but if namespace is null then do conversion as it's not an entity type it's a generated type
//(need better way to detect this)
if(newParam!.Type.Namespace == null || newParam!.Type == node.Type)
return newParam!;
if (toReplaceType != null && node.NodeType == ExpressionType.Parameter && toReplaceType == node.Type)
return newParam!;
return node;
Expand Down
36 changes: 36 additions & 0 deletions src/tests/EntityGraphQL.Tests/QueryTests/QueryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using EntityGraphQL.Tests.ApiVersion1;
using Microsoft.CSharp.RuntimeBinder;
using Microsoft.EntityFrameworkCore;

namespace EntityGraphQL.Tests
{
Expand Down Expand Up @@ -356,6 +357,37 @@ public void TestInheritance()
name
}
}");
var context = new TestAbstractDataContext();
context.Animals.Add(new Dog() { Name = "steve", HasBone = true });
context.Animals.Add(new Cat() { Name = "george", Lives = 9 });

var qr = gql.ExecuteQuery(context, null, null);
dynamic animals = (dynamic)qr.Data["animals"];
// we only have the fields requested
Assert.Equal(2, animals.Count);

Assert.Equal("Dog", animals[0].__typename);
Assert.Equal("Cat", animals[1].__typename);
}

[Fact]
public void TestInheritanceExtraFields()
{
var schemaProvider = new TestAbstractDataGraphSchema();
var gql = new GraphQLCompiler(schemaProvider).Compile(@"
query {
animals {
__typename
name
... on Cat {
lives
}
...on Dog {
hasBone
}
}
}
");

var context = new TestAbstractDataContext();
context.Animals.Add(new Dog() { Name = "steve", HasBone = true });
Expand All @@ -367,7 +399,11 @@ public void TestInheritance()
Assert.Equal(2, animals.Count);

Assert.Equal("Dog", animals[0].__typename);
Assert.Equal("steve", animals[0].name);
Assert.True(animals[0].hasBone);
Assert.Equal("Cat", animals[1].__typename);
Assert.Equal("george", animals[1].name);
Assert.Equal(9, animals[1].lives);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public TestAbstractDataGraphSchema()
UpdateQuery(query =>
{
query.AddField(ctx => ctx.Animals, "All animals in the world");
});
});

}
}
}
Expand Down