private static ConditionalExpression GetMemberInit(GraphQLSchema <TContext> schema, Type queryType, IEnumerable <ExecSelection <Info> > selectionsEnumerable, Expression baseBindingExpr, ExpressionOptions options) { // Avoid possible multiple enumeration of selections-enumerable var selections = selectionsEnumerable as IList <ExecSelection <Info> > ?? selectionsEnumerable.ToList(); // 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) { // 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 typeNameExecSelection = new ExecSelection <Info>( new SchemaField( schema.Adapter.QueryTypes[graphQlType?.Name], typeNameField, schema.Adapter), new FSharpOption <string>(typeNameField?.Name), null, new WithSource <ExecArgument <Info> >[] { }, new WithSource <ExecDirective <Info> >[] { }, new WithSource <ExecSelection <Info> >[] { } ); bindings = bindings.Concat(new[] { GetBinding(schema, typeNameExecSelection, queryType, baseBindingExpr, options) }).ToList(); } } var memberInit = Expression.MemberInit(Expression.New(queryType), bindings); return(NullPropagate(baseBindingExpr, memberInit)); }
private static ConditionalExpression GetMemberInit(GraphQLSchema <TContext> schema, GraphQLType graphQlType, IEnumerable <ExecSelection <Info> > selectionsEnumerable, Expression parameterExpression, ExpressionOptions options) { var queryType = graphQlType.QueryType; // Avoid possible multiple enumeration of selections-enumerable var selections = selectionsEnumerable as IList <ExecSelection <Info> > ?? selectionsEnumerable.ToList(); // 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 == "__typename"); var bindingsWithPossibleDuplicates = selections.Where(s => !s.SchemaField.Info.Field.IsPost) .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) { // Find the base types' `__typename` field var typeNameField = graphQlType?.Fields.Find(f => f.Name == "__typename"); if (typeNameField != null && !typeNameField.IsPost) { var typeNameExecSelection = new ExecSelection <Info>( new SchemaField( schema.Adapter.QueryTypes[graphQlType?.Name], typeNameField, schema.Adapter), new FSharpOption <string>(typeNameField?.Name), null, new WithSource <ExecArgument <Info> >[] {}, new WithSource <ExecDirective <Info> >[] {}, new WithSource <ExecSelection <Info> >[] {} ); bindings = bindings .Where(b => b.Member.Name != "__typename") .Concat(new[] { GetBinding(schema, typeNameExecSelection, graphQlType, parameterExpression, options) }) .ToList(); } } var memberInit = Expression.MemberInit(Expression.New(queryType), bindings); return(NullPropagate(parameterExpression, memberInit)); }
private static MemberBinding GetBinding(GraphQLSchema <TContext> schema, ExecSelection <Info> 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); // expr is form of: (context, entity) => entity.Field var expr = field.GetExpression(map.Arguments.Values()); // The field might be defined on sub-types of the specified type, so add a type cast if necessary. // If appropriate, `entity.Field` becomes `(entity as TFieldDefiningType).Field` var typeCastedBaseExpression = needsTypeCheck ? Expression.TypeAs(baseBindingExpr, field.DefiningType.CLRType) : baseBindingExpr; // Replace (entity) with baseBindingExpr, note expression is no longer a LambdaExpression // `(context, entity) => entity.Field` becomes `someOtherEntity.Entity.Field` where baseBindingExpr is `someOtherEntity.Entity` var replacedBase = ParameterReplacer.Replace(expr.Body, expr.Parameters[1], typeCastedBaseExpression); // If the entity has to be casted, add a type check: // `(someOtherEntity.Entity as TFieldDefininingType).Field` becomes `(someOtherEntity.Entity is TFieldDefininingType) ? (someOtherEntity.Entity as TFieldDefininingType).Field : null` if (needsTypeCheck && options.TypeCheckInheritance) { // The expression type has to be nullable if (replacedBase.Type.IsValueType) { replacedBase = Expression.Convert(replacedBase, typeof(Nullable <>).MakeGenericType(replacedBase.Type)); } var typeCheck = Expression.TypeIs(baseBindingExpr, field.DefiningType.CLRType); var nullResult = Expression.Constant(null, replacedBase.Type); replacedBase = Expression.Condition(typeCheck, replacedBase, nullResult); } // This just makes sure that the (context) parameter is the same as the one used by the whole query var replacedContext = ParameterReplacer.Replace(replacedBase, expr.Parameters[0], GraphQLSchema <TContext> .DbParam); // 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 binding a single entity, just use the already built selector expression (replaced context) // Otherwise, if binding to a list, introduce a new parameter that will be used in a call to .Select var listParameter = Expression.Parameter (field.Type.CLRType, field.Type.CLRType.Name.Substring(0, 1).ToLower()); // TODO: Name conflicts in expressions? 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); // For single entities, we're done and we can just bind to the memberInit expression if (!field.IsList) { return(Expression.Bind(toMember, memberInit)); } // However for lists, we need to check for null and call .Select() and .ToList() first var selectLambda = Expression.Lambda(memberInit, listParameter); var call = Expression.Call(typeof(Enumerable), "Select", new[] { field.Type.CLRType, field.Type.QueryType }, replacedContext, selectLambda); var toList = Expression.Call(typeof(Enumerable), "ToList", new[] { field.Type.QueryType }, call); return(Expression.Bind(toMember, options.NullCheckLists ? (Expression)NullPropagate(replacedContext, toList) : toList)); }
public static object Execute (GraphQLSchema <TContext> schema, TContext context, GraphQLField field, ExecSelection <Info> query) { var mutReturn = field.RunMutation(context, query.Arguments.Values()); var queryableFuncExpr = field.GetExpression(query.Arguments.Values(), mutReturn); var replaced = (LambdaExpression)ParameterReplacer.Replace(queryableFuncExpr, queryableFuncExpr.Parameters[0], GraphQLSchema <TContext> .DbParam); // sniff queryable provider to determine how selector should be built var dummyQuery = replaced.Compile().DynamicInvoke(context, null); var queryType = dummyQuery.GetType(); var expressionOptions = schema.GetOptionsForQueryable(queryType); if (expressionOptions.UseBaseType) { queryType = queryType.BaseType; expressionOptions = schema.GetOptionsForQueryable(queryType); } var queryExecSelections = query.Selections.Values(); var selector = GetSelector(schema, field.Type, queryExecSelections, expressionOptions); if (field.ResolutionType != ResolutionType.Unmodified) { var selectorExpr = Expression.Quote(selector); // TODO: This should be temporary - queryable and enumerable should both work var body = replaced.Body; if (body.NodeType == ExpressionType.Convert) { body = ((UnaryExpression)body).Operand; } var call = Expression.Call(typeof(Queryable), "Select", new[] { field.Type.CLRType, field.Type.QueryType }, body, selectorExpr); var expr = Expression.Lambda(call, GraphQLSchema <TContext> .DbParam); var transformed = expr.Compile().DynamicInvoke(context); object results; switch (field.ResolutionType) { case ResolutionType.Unmodified: throw new Exception("Queries cannot have unmodified resolution. May change in the future."); case ResolutionType.ToList: var list = GenericQueryableCall <IEnumerable <object> >(transformed, q => q.ToList()); results = list.Select(o => MapResults(o, queryExecSelections, schema)).ToList(); break; case ResolutionType.FirstOrDefault: var fod = GenericQueryableCall(transformed, q => q.FirstOrDefault()); results = MapResults(fod, queryExecSelections, schema); break; case ResolutionType.First: var first = GenericQueryableCall(transformed, q => q.FirstOrDefault()); results = MapResults(first, queryExecSelections, schema); break; default: throw new ArgumentOutOfRangeException(); } return(results); } else { var invocation = Expression.Invoke(selector, replaced.Body); var expr = Expression.Lambda(invocation, GraphQLSchema <TContext> .DbParam); var result = expr.Compile().DynamicInvoke(context); return(MapResults(result, queryExecSelections, schema)); } }