chkimes / graphql-net

Convert GraphQL to IQueryable

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Not Recognized Args by creating at runtime args Type

Djangoum opened this issue · comments

Hi, I'm trying to write an automated entityFramework implementation schema builder on GraphQL.Net.

And I'm generating the Args Type dynamically with System.Reflection.Emit namespace methods.

Everything seems to be Ok when i'm generating my GraphQl.Net Schema but when i try to query the Args are not recognized by the parser.

     public static object CompileQueryType(Type SourceType)
     {
        List<Field> fields = new List<Field>();

        foreach(var prop in SourceType.GetProperties())
        {
            fields.Add(new Field() { Name = prop.Name, Type = prop.PropertyType });
        }

        TypeBuilder tb = GetTypeBuilder(SourceType.Name);
        ConstructorBuilder constructor = tb.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);

        foreach (var field in fields)
            CreateProperty(tb, field.Name, field.Type);

        Type objectType = tb.CreateType();
        object instance = Activator.CreateInstance(objectType);
        return instance;
    }

    private static TypeBuilder GetTypeBuilder(string TypeName)
    {
        var typeSignature = TypeName;
        var an = new AssemblyName(typeSignature);
        AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);
        ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
        TypeBuilder tb = moduleBuilder.DefineType(typeSignature,
                TypeAttributes.Public |
                TypeAttributes.Class |
                TypeAttributes.AutoClass |
                TypeAttributes.AnsiClass |
                TypeAttributes.BeforeFieldInit |
                TypeAttributes.AutoLayout,
                null);
        return tb;
    }

    private static void CreateProperty(TypeBuilder tb, string propertyName, Type propertyType)
    {
        FieldBuilder fieldBuilder = tb.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);

        PropertyBuilder propertyBuilder = tb.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);
        MethodBuilder getPropMthdBldr = tb.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName, propertyType, Type.EmptyTypes);

        ILGenerator getIl = getPropMthdBldr.GetILGenerator();

        getIl.Emit(OpCodes.Ldarg_0);
        getIl.Emit(OpCodes.Ldfld, fieldBuilder);
        getIl.Emit(OpCodes.Ret);

        MethodBuilder setPropMthdBldr =
            tb.DefineMethod("set_" + propertyName,
              MethodAttributes.Public |
              MethodAttributes.SpecialName,
              null, new[] { propertyType });

        ILGenerator setIl = setPropMthdBldr.GetILGenerator();
        Label modifyProperty = setIl.DefineLabel();
        Label exitSet = setIl.DefineLabel();

        setIl.MarkLabel(modifyProperty);
        setIl.Emit(OpCodes.Ldarg_0);
        setIl.Emit(OpCodes.Ldarg_1);
        setIl.Emit(OpCodes.Stfld, fieldBuilder);

        setIl.Emit(OpCodes.Nop);
        setIl.MarkLabel(exitSet);
        setIl.Emit(OpCodes.Ret);

        propertyBuilder.SetGetMethod(getPropMthdBldr);
        propertyBuilder.SetSetMethod(setPropMthdBldr);
    }

This is the functions i'm using to create the Args Type at run time.

I'm doing this because i need to make not nullable properties nullable properties so i can ommit then on query building. Query building is handled by Dynamic Linq.

I think the problem is to passing an object as TArgs, but i dont realize how to solve it.

Sorry for my many english mistakes and thank you!

Also i want to congrat you about this project is amazing!

Greetings.

Hi @Djangoum,

Could you also post some of the schema setup code where you're using these generated types with GraphQL.Net?

I won't know for sure until I see what you've got, but I suspect that you'll need to use reflection to call the setup methods on the schema. Currently the schema setup methods use generics to get compile-time type information, but your types only exist at runtime.

This is the file where i have all the code written. The process begans at "RegisterDbContext" method.
I know it's not the best structure ever but it's my first approach to my objective :P

Thanks for your quick response.

Greetings!

using GraphQl.Data;
using GraphQl.EntityFramework;
using GraphQL.Net;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Diagnostics;
using System.Linq;
using System.Linq.Dynamic;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading.Tasks;

namespace GraphQl.EntityFramework
{
    public partial class GraphQL<TContext> 
        where TContext : DbContext, new()
    {
        public static GraphQLSchema<TContext> RegisterDbContext(GraphQLSchema<TContext> schema = null)           
        {
            if (schema == null)
                schema = GraphQL.Net.GraphQL<TContext>.CreateDefaultSchema(() => new TContext());

            PropertyInfo[] properties = typeof(TContext).GetProperties();


            foreach (PropertyInfo property in properties)
            {
                if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(IDbSet<>))
                {
                    var GenericType = property.PropertyType.GetGenericArguments()[0];
                    MethodInfo addType = schema.GetType().GetMethod("AddType").MakeGenericMethod(GenericType);
                    var TypeBuilder = addType.Invoke(schema, new object[] { null, null });
                    TypeBuilder.GetType().GetMethod("AddAllFields").Invoke(TypeBuilder, null);

                    Type GraphQlEntityFrameworkType = typeof(GraphQL<TContext>);

                    MethodInfo CreateQueryByFieldMethod = GraphQlEntityFrameworkType.GetMethod("CreateQueryByField").MakeGenericMethod(GenericType);
                    CreateQueryByFieldMethod.Invoke(null, new object[] { schema, property.Name });
                 }
            }

            schema.Complete();
            return schema;
        }       

        public static void CreateQueryByField<TEntity>(GraphQLSchema<TContext> schema, string Field)
            where TEntity : class, new()
        {
            var argumentObject = CompileQueryType(typeof(TEntity));
            var suposedArgumentObjet = new TEntity();
            schema.AddListField(Field, argumentObject, (db, args) => db.Set<TEntity>().Where(CreateConditionalExpression(db, args)));
        }
        
        public static string CreateConditionalExpression<TEntity> (TContext db, TEntity args)
            where TEntity: class, new()
        {
            StringBuilder query = new StringBuilder();
            var argType = args.GetType();

            List<PropertyInfo> Properties = argType.GetProperties().ToList();
            foreach (var prop in Properties)
            {
                if(prop.GetValue(args) != null)
                {
                    if (string.IsNullOrEmpty(query.ToString()))
                    {
                        query.Append($" {prop.Name} = {prop.GetValue(args)} ");
                        continue;
                    }

                    query.Append($" AND {prop.Name} = {prop.GetValue(args)} ");
                }
            }

            return query.ToString();
        }


        public static object CompileQueryType(Type SourceType)
        {
            List<Field> fields = new List<Field>();

            foreach(var prop in SourceType.GetProperties())
            {
                fields.Add(new Field() { Name = prop.Name, Type = prop.PropertyType });
            }

            TypeBuilder tb = GetTypeBuilder(SourceType.Name);
            ConstructorBuilder constructor = tb.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);

            foreach (var field in fields)
                CreateProperty(tb, field.Name, field.Type);

            Type objectType = tb.CreateType();
            object instance = Activator.CreateInstance(objectType);
            return instance;
        }

        private static TypeBuilder GetTypeBuilder(string TypeName)
        {
            var typeSignature = TypeName;
            var an = new AssemblyName(typeSignature);
            AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);
            ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
            TypeBuilder tb = moduleBuilder.DefineType(typeSignature,
                    TypeAttributes.Public |
                    TypeAttributes.Class |
                    TypeAttributes.AutoClass |
                    TypeAttributes.AnsiClass |
                    TypeAttributes.BeforeFieldInit |
                    TypeAttributes.AutoLayout,
                    null);
            return tb;
        }

        private static void CreateProperty(TypeBuilder tb, string propertyName, Type propertyType)
        {
            FieldBuilder fieldBuilder = tb.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);

            PropertyBuilder propertyBuilder = tb.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);
            MethodBuilder getPropMthdBldr = tb.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName, propertyType, Type.EmptyTypes);

            ILGenerator getIl = getPropMthdBldr.GetILGenerator();

            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Ldfld, fieldBuilder);
            getIl.Emit(OpCodes.Ret);

            MethodBuilder setPropMthdBldr =
                tb.DefineMethod("set_" + propertyName,
                  MethodAttributes.Public |
                  MethodAttributes.SpecialName,
                  null, new[] { propertyType });

            ILGenerator setIl = setPropMthdBldr.GetILGenerator();
            Label modifyProperty = setIl.DefineLabel();
            Label exitSet = setIl.DefineLabel();

            setIl.MarkLabel(modifyProperty);
            setIl.Emit(OpCodes.Ldarg_0);
            setIl.Emit(OpCodes.Ldarg_1);
            setIl.Emit(OpCodes.Stfld, fieldBuilder);

            setIl.Emit(OpCodes.Nop);
            setIl.MarkLabel(exitSet);
            setIl.Emit(OpCodes.Ret);

            propertyBuilder.SetGetMethod(getPropMthdBldr);
            propertyBuilder.SetSetMethod(setPropMthdBldr);
        }

    }

    public class Field
    {
        public string Name { get; set; }
        public Type Type { get; set; }
    }
}

So there are a few issues you'll encounter with getting this to work. The first is the one that you're immediately encountering with passing a compile-time object instead of TArgs when you call schema.AddListField. You don't know TArgs at compile-time so you'll have to use reflection to execute the method.

Take a look at: https://msdn.microsoft.com/en-us/library/system.reflection.methodinfo.makegenericmethod(v=vs.110).aspx

To call a generic method by reflection, you have to do something like this (approximation - not exact):

// get the MethodInfo for AddListField
// note: AddListField has overloads, so you may need to use GetMethods and
//       filter to call the one you want
var method = typeof(SchemaExtensions).GetMethod("AddListField");

// explicitly specify the generic types for the method
var genericMethod = method.MakeGenericMethod(contextType, argsType, entityType);

// invoke the method and pass in our parameters
genericMethod.Invoke(null, new object[] { schema, name, null, expression });

The other issue you might run into is that I don't think that GraphQL.Net and System.Linq.Dynamic will combine very well. I haven't tested it myself, but GraphQL.Net works by gluing expressions together and I'm not sure that dynamically generated expressions will work. However, it's possible that they will so I'd start by trying to fix the above piece about generics first and then seeing how the rest of it works.

Hi @ckimes89, with the help of my friend @magicheron , we have done what you suggested, we call the method "AddListField" by reflection.

This is solved and the code will be uploaded soon to a github repo. I can post you here if you want when it's uploaded.

Thanks for your support!

@Djangoum Good to hear! I'd be interested to see the result when it's up on github.