Search code examples
c#asp.net-coregraphqlentity-framework-corehotchocolate

How to override the filtering LINQ query generated by HotChocolate?


I have a case where I need to send tens of thousands of ids to the graphql server in the filtering query.

The query now generated by the HT is something like this:

_dbContext.
  Forms
  .Where(c=>staticLiistOfIds.Contains(c.Id))
  .Select(c=>new {C.Name,C.Age});

I have two problems with this:

  1. slow performance
  2. SQL Server Limit I guess is around 32K

I have found a Nuget library to convert this static list to a temp table,so now I want to override the HT middle to rewrite the above query generated to the following:

_dbContext.
  Forms
  .Where(c=>_dbContext..AsQueryableValues(staticLiistOfIds).Contains(c.Id))
  .Select(c=>new {C.Name,C.Age});

This will create a temp table for this static list of ids so I will be able to solve the above two problems that I have.


Solution

  • So since i didn't get answers, I had to ask from the Slack of HotChocolate's Team and hopefully, they provided me with the documentation extending-filtering/extending-iqueryable:

    in case the link was broken, here is

    Extending IQueryable The default filtering implementation uses IQueryable under the hood. You can customize the translation of queries by registering handlers on the QueryableFilterProvider.

    The following example creates a StringOperationHandler that supports case-insensitive filtering:

    // The QueryableStringOperationHandler already has an implemenation of CanHandle
    // It checks if the field is declared in a string operation type and also checks if
    // the operation of this field uses the `Operation` specified in the override property further
    // below
    public class QueryableStringInvariantEqualsHandler : QueryableStringOperationHandler
    {
        // For creating a expression tree we need the `MethodInfo` of the `ToLower` method of string
        private static readonly MethodInfo _toLower = typeof(string)
            .GetMethods()
            .Single(
                x => x.Name == nameof(string.ToLower) &&
                x.GetParameters().Length == 0);
    
        // This is used to match the handler to all `eq` fields
        protected override int Operation => DefaultFilterOperations.Equals;
    
        public override Expression HandleOperation(
            QueryableFilterContext context,
            IFilterOperationField field,
            IValueNode value,
            object parsedValue)
        {
            // We get the instance of the context. This is the expression path to the propert
            // e.g. ~> y.Street
            Expression property = context.GetInstance();
    
            // the parsed value is what was specified in the query
            // e.g. ~> eq: "221B Baker Street"
            if (parsedValue is string str)
            {
                // Creates and returnes the operation
                // e.g. ~> y.Street.ToLower() == "221b baker street"
                return Expression.Equal(
                    Expression.Call(property, _toLower),
                    Expression.Constant(str.ToLower()));
            }
    
            // Something went wrong 😱
            throw new InvalidOperationException();
        }
    }
    

    This operation handler can be registered on the convention:

    public class CustomFilteringConvention : FilterConvention
    {
        protected override void Configure(IFilterConventionDescriptor descriptor)
        {
            descriptor.AddDefaults();
            descriptor.Provider(
                new QueryableFilterProvider(
                    x => x
                        .AddDefaultFieldHandlers()
                        .AddFieldHandler<QueryableStringInvariantEqualsHandler>()));
        }
    }
    
    // and then
    services.AddGraphQLServer()
        .AddFiltering<CustomFilteringConvention>();
    

    To make this registration easier, Hot Chocolate also supports convention and provider extensions. Instead of creating a custom FilterConvention, you can also do the following:

    services
        .AddGraphQLServer()
        .AddFiltering()
        .AddConvention<IFilterConvention>(
            new FilterConventionExtension(
                x => x.AddProviderExtension(
                    new QueryableFilterProviderExtension(
                        y => y.AddFieldHandler<QueryableStringInvariantEqualsHandler>()))));
    

    but I was suggested that doing this way(sending up to 100k list of string ids to the graphQL server) is not a good approach. so I decided to take another approach by writing a custom simple dynamic LINQ generates.

    Thanks.