mercredi 29 septembre 2021

GraphQL dotnet - Add fields at runtime using reflection

We have a decent sized dataset that we are using graphql dotnet to query. We have custom logic in place to parse the GQL query string and generate a function that will select the appropriate fields from the database and not the full object.

As such, the 'Query' object in our schema has a load of almost identical calls to Field in it which just look an object up by it's Id, issue the parsed select command, and return the resulting object.

For example:

// expression based department query
Field<DepartmentType>(
    "department",
    arguments: new QueryArguments(
        new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "organisationId" },
        new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "employerId" },
        new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "Id" }),
    resolve: context =>
    {
        var query = context.Document.OriginalQuery.Substring(context.Document.OriginalQuery.IndexOf("{"));

        var organisationId = new Guid(context.GetArgument<string>("organisationId"));
        var employerId = new Guid(context.GetArgument<string>("employerId"));
        var Id = new Guid(context.GetArgument<string>("Id"));

        appContext.OrganisationId = organisationId;
        appContext.EmployerId = employerId;

        var retVal = database.Departments()
            .Where(x => x.Id == Id && x.Deleted != true);

        var selectStatement = GraphDocumentParser.GenerateSelect(query, "department");
        var builder = new ExpressionBuilder();
        var statement = builder.BuildSelector<Department, Department>(selectStatement);
        var result = retVal.Select(statement);

        return result.FirstOrDefault();
    }).AuthorizeWith(Constants.GraphQL.Policies.API_ACCESS);

So I thought that I could just use reflection to loop our graphql types like this:

var queryableObjectTypes = Assembly.GetExecutingAssembly().GetTypes()
    .Where(x => x.IsClass && x.Namespace == "my.namespace.query.api" && !x.FullName.Contains("+<>c"))
    .ToList();

//Loop through our GQL types
foreach (var queryableObjectType in queryableObjectTypes)
{
    Console.WriteLine(queryableObjectType.FullName);

    //Grab the fields property from the type
    var fieldsProperty = queryableObjectType.GetProperties().FirstOrDefault(x => x.Name == "Fields");

    //If this has our 'fields' property which we require
    if (fieldsProperty != null)
    {
        //Get the instance of this
        var instanceOfType = database.GetServiceProvider().GetService(queryableObjectType);

        //If this fields is the type we definitely want - i.e.actually get the value from the
        // instance and check it
            if (fieldsProperty.GetValue(instanceOfType) is GraphQL.Types.TypeFields typeFields)
        {
            //If this type contains an employerid, then we need to generate a query for this
            if (typeFields.Any(x => x.Name.ToLower() == "employerid"))
            { ... } //These are the objects that I want to generate fields for

Which does give me the correct collection of GQL Type objects that I can loop over and use to add a field (any one of our objects with an employerId... which I will blacklist a few of later on, but that would be a simple list to skip over)

I can then parse various bits like the 'base' entity that this type points to, and get most of the required data for the Field to add to our GQL query.

I am able to get almost everything I need to create a field type to add to our query... as shown here:

var f = new FieldType();
f.Name = genericTypeNameFormatted;
f.Description = string.Empty;
f.Type = queryableObjectType;
f.Resolver = ????????
f.Arguments = new QueryArguments(
    new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "organisationId" },
    new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "employerId" },
    new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "Id" });
f.AuthorizeWith(Constants.GraphQL.Policies.API_ACCESS);

However, I am totally stuck with the mix of generics etc.. required to create the resolver. It's definition is as follows:

public class AsyncFieldResolver<TSourceType, TReturnType> : IFieldResolver<Task<TReturnType>>
{
    private readonly Func<IResolveFieldContext<TSourceType>, Task<TReturnType>> _resolver;

    /// <inheritdoc cref="AsyncFieldResolver{TReturnType}.AsyncFieldResolver(Func{IResolveFieldContext, Task{TReturnType}})"/>
    public AsyncFieldResolver(Func<IResolveFieldContext<TSourceType>, Task<TReturnType>> resolver)
    {
        _resolver = resolver ?? throw new ArgumentNullException(nameof(resolver), "A resolver function must be specified");
    }

    /// <inheritdoc cref="AsyncFieldResolver{TReturnType}.Resolve(IResolveFieldContext)"/>
    public Task<TReturnType> Resolve(IResolveFieldContext context) => _resolver(context.As<TSourceType>());

    object IFieldResolver.Resolve(IResolveFieldContext context) => Resolve(context);
}

Is it possible to create a Func<IResolveFieldContext<TSource>, Task<TReturn>> using data from reflection etc? or is it actually not possible?





Aucun commentaire:

Enregistrer un commentaire