Skip to content

Extensibility of schema provider / statement execution #499

Description

@denwanke

We are currently evaluating EntityGraphQL in our project to provide GQL API for the existing data engine based on EF6 model-first with dynamically built EDM and Entity SQL queries. With some nasty workarounds we've managed EntityGraphQL to work with our data engine and now want to make things more smooth.

As our data engine doesn't rely on expression trees but instead on ESQL, the current implementation of ExecutableGraphQLStatement.ExecuteAsync() calling CompileAndExecuteNodeAsync() can obviously not be used. So there should be an extension point somewhere around that place to enable custom query processing.
As for now, we had to re-implement the entire ExecuteRequestAsync() in SchemaProvider, as the method (or better say the private one DoExecuteRequestAsync) is not virtual and also re-implement MapGraphQL() extension as the current one is looking for SchemaProvider service and not just for ISchemaProvider (that is actually quite strange, rendering a true service injection impossible).

Activity

  1. lukemurray commented on Apr 16, 2026

    @lukemurray
    Collaborator

    The comment about SchemaProvider and ISchemaProvider is valid.

    Regarding the rest of it are you saying you want to replace at execution by getting the expression tree to you building something that turns that into ESQL or you want to replace the compiling of the graphql document completely - e.g building you're own ESQL or nodes etc? As there are 2 stages in the library 1. graphql doc to field nodes 2. those get built into expression trees.

  2. denwanke commented on Apr 21, 2026

    @denwanke
    Author

    We want to take the parsed GQL document and get the resulting data from our own ESQL-based query engine using the GQL data fields requested. So we definitely use stage 1. ("graphql doc to field nodes") but actually bypass (replace) stage 2 (directly calling our engine instead of producing expression trees).

    To give you an idea, the following is our current "(re-)implementation" based on 6.0.0-beta3:

    public new async Task<QueryResult> ExecuteRequestAsync(QueryRequest gql, IServiceProvider serviceProvider, ClaimsPrincipal user, ExecutionOptions options, CancellationToken ct)
        {
            QueryResult result;
    
            try
            {
                options ??= new ExecutionOptions();
                GraphQLDocument compiledQuery = null;
    
                //TODO: cache, etc...
                compiledQuery = GraphQLParser.Parse(gql, this);
    
                BaseGraphQLField[] dataFields = null;
                var op = compiledQuery.Operations.FirstOrDefault(o => string.IsNullOrEmpty(gql.OperationName) || o.Name == gql.OperationName);
                if (op is GraphQLQueryStatement)
                {
                    dataFields = op.QueryFields.Where(f => !IsSystemField(f)).ToArray();
                    Array.ForEach(dataFields, f => op.QueryFields.Remove(f));
                }
    
                /*TODO: 
                QueryLimitsValidator.Validate(compiledQuery, gql.OperationName, gql.Variables, options);
                var fieldRateLimiter = options.FieldRateLimitService ?? (IFieldRateLimitService?)serviceProvider?.GetService(typeof(IFieldRateLimitService));
                AggregateFieldRateLimitLease? leases = null;
                if (fieldRateLimiter != null)
                    leases = await FieldRateLimitExecutor.AcquireAsync(compiledQuery, gql.OperationName, fieldRateLimiter, user, options.UserKeySelector, ct);
                */
                
                var requestContext = new QueryRequestContext(AuthorizationService, user);
    
                result = await compiledQuery.ExecuteQueryAsync<TContextType>(default, serviceProvider, gql.Variables, gql.OperationName, requestContext, options, ct);
    
                if (!result.HasErrors() && dataFields?.Length > 0)
                {
                    IArgumentsTracker docVariables = null; //TODO: op.BuildDocumentVariables(ref variables);
                    var cc = new CompileContext(options, null, requestContext, op.OpVariableParameter, docVariables, ct);
                    Func<BaseGraphQLField, ParameterExpression, IEnumerable<BaseGraphQLField>> expand = (f, fc) =>
                        f.Expand(cc, compiledQuery.Fragments, true, fc, op.OpVariableParameter, docVariables);
                    result = await ExecuteDataFieldsAsync(result, dataFields, serviceProvider, expand);
                }
            }
            catch (Exception ex)
            {
                //TODO: result = HandleException(ex);
                throw;
            }
    
            return result;
        }
    

    So, we actually detect all the data fields, remove them from the query and call the query's ExecuteQueryAsync() for sake of possible introspection (parts). Then we produce the results for the data fields like follows:

    public async Task<QueryResult> ExecuteDataFieldsAsync(QueryResult result, BaseGraphQLField[] dataFields, IServiceProvider serviceProvider,
          Func<BaseGraphQLField, ParameterExpression, IEnumerable<BaseGraphQLField>> expandField)
      {
          var gqlValidator = serviceProvider.GetService<IGraphQLValidator>();
          var dataService = serviceProvider.GetRequiredService<IDataService>();
          
          var data = new ConcurrentDictionary<string, object>(result.Data ?? []);
    
          //TODO: parallelize?
          foreach (var field in dataFields)
          {
              var res = field switch
              {
                  GraphQLListSelectionField lsf => await ExecuteDataFieldAsync(lsf, dataService, expandField),
                  _ => throw UnsupportedFieldException(field)
              };
    
              data[field.Name] = res;
          }
    
          result.SetData(data);
    
          if (gqlValidator?.Errors.Count > 0)
              result.AddErrors(gqlValidator.Errors);
    
          if (result.Data?.Count == 0 && result.HasErrorKey())
              result.RemoveDataKey();
    
          return result;
      }
    

    using a helper method:

    
        private async Task<IEnumerable<object>> ExecuteDataFieldAsync(GraphQLListSelectionField field, IDataServicet dataService, 
            Func<BaseGraphQLField, ParameterExpression, IEnumerable<BaseGraphQLField>> expandField)
        {
            field.Arguments.TryGetValue(DataServiceGqlAdapter.FilterArgumentName, out var filter);
            field.Arguments.TryGetValue(DataServiceGqlAdapter.ShapingArgumentName, out var shaping);
    
            var paramExpression = (ParameterExpression)field.NextFieldContext!;
    
            var qs = new DataService.QueryStructure
            {
                EntityName = paramExpression.Type.Name,
                Filter = (string)filter
            };
    
            var properties = TranslateQueryProperties(field, [], f => expandField(f, paramExpression));
    
            var records = dataService.GetAsyncDataRecords(qs, properties, (DataShaping)shaping);
    
            return await records.ToArrayAsync();
        }
    

    where TranslateQueryProperties() recursively iterates all the data fields using field.QueryFields.SelectMany(expand); and creates "selector properties" for our query engine, IDataService.GetAsyncDataRecords().

    Precisely, it means that schema field resolvers are currently not called in runtime at all, which is not necessarily a good thing.

  3. lukemurray commented on Apr 22, 2026

    @lukemurray
    Collaborator

    Quick question, when you say EF6 do you mean EF6 from original .NET Framework, before the "core" and open source releases? if. so what framework are you targeting / running on?

  4. denwanke commented on Apr 22, 2026

    @denwanke
    Author

    Yes indeed it's about "classic" EF6, even using Model-First approach. But we're targeting .NET 10 since EF 6.5 supports NetStandard 2.1

  5. lukemurray commented on Apr 22, 2026

    @lukemurray
    Collaborator

    check #514 out to see if that follows.

    I don't want the library to spend too much time of this type of thing TBH. but if we can easily split it and add an interface that works, that I support that.

  6. denwanke commented on Apr 24, 2026

    @denwanke
    Author

    PR #514 looks very promising. We have managed to get our code to work with one little hitch: when traversing document fields, we'are currently calling BaseGraphQLField.Expand() in oder to get all the children (including those defined in fragments).

    However, BaseGraphQLField.Expand() requires a CompileContext, which we create like this:

    var op = document.Operations.FirstOrDefault(o => string.IsNullOrEmpty(operationName) || o.Name == operationName);
    IArgumentsTracker docVariables = op.BuildDocumentVariables(ref variables); <--- WON'T COMPILE
    var cc = new CompileContext(options, null, requestContext, op.OpVariableParameter, docVariables, ct);
    

    Then we construct a field expansion callback like follows:

    Func<BaseGraphQLField, ParameterExpression, IEnumerable<BaseGraphQLField>> expand = (f, fc) =>
        f.Expand(cc, compiledQuery.Fragments, true, fc, op.OpVariableParameter, docVariables);
    

    and then supply it to our traverse routine (see the code excerpt in the previous comment.

    The problem is that ExecutableGraphQLStatement.BuildDocumentVariables() is currently protected (hence the code won't compile). Could you just make it public? Or maybe provide some convenience factory method to create a CompileContext instance from within IGraphQLDocumentExecutor.ExecuteAsync() like in ExecutableGraphQLStatement? Or even better to get the entire field expansion callback from ExecutableGraphQLStatement.

  7. denwanke commented on Jul 10, 2026

    @denwanke
    Author

    Hi, any updates on this?

  8. lukemurray commented on Jul 21, 2026

    @lukemurray
    Collaborator

    have not had a chance to return to this.

    Does #514 allow you to do what you need?

  9. denwanke commented on Jul 21, 2026

    @denwanke
    Author

    Please have a look on my previous comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions