0

I am trying to understand what is the best approach to utilize query with filters in GraphQL for my scenario.

So far what I have learned is, GraphQL does not have filter concept. UI/Consumer can fetch records per their need. I have GroupDataLoader which takes Location and give me all records for that. This satisfies my 1st query.

Now on top of that, if I want to add other search criteria than I can make Type, Number, and Supplier as the optional parameters for my query, but I can only pass one parameter to .LoadAsync (or at least that's my understanding).

Question:

  1. Is it possible to use GroupDataLoader AND multiple search filter(s)?
  2. If no, then what is the best practice? Fetch all records and let UI/consumer pull other fields?

Scenario:

enter image description here

Expectation:

Queries:

  • Give me records for location ATL => Should return 3 records
  • Give me records for location ATL and Type Car => Should return 2 records
  • Give me records for location ATL and Type Car and Supplier Hertz => Should return 1 record

Code:

query.cs

[Authorize(policy: "ProjectAccess")]
public async Task<IEnumerable<Exception>> GetExceptionsAsync(
    string location, 
    [Service(ServiceKind.Synchronized)] IExceptionService exceptionService,
    long stockNumber = 0
    )
{
    return await exceptionService.GetExceptionsByStockNumberV2(location);
}

Service.cs


namespace Services
{
    public interface IExceptionService
    {
        Task<IEnumerable<Database.Entities.Sql.Exception>> GetExceptionsByStockNumberV2(
            string location, long stockNumber = 0);
    }

    public class ExceptionService: ServiceBase, IExceptionService
    {
        private readonly IProjectXSupplyChainDbManagement _projectXSupplyChainDbManagement;
        private readonly ExceptionGroupDataLoader _exceptionDataLoader;

        public ExceptionService(ILogger<ExceptionService> logger, IProjectXSupplyChainDbManagement projectXSupplyChainDbManagement, IMapper mapper, 
            ExceptionGroupDataLoader exceptionDataLoader)
        {
            _projectXSupplyChainDbManagement = projectXSupplyChainDbManagement;
            _exceptionDataLoader = exceptionDataLoader;
        }

        #region DATA LOADER

        public async Task<IEnumerable<Database.Entities.Sql.Exception>> GetExceptionsByStockNumberV2(
            string location, 
            long stockNumber = 0 // Not sure how such filters can be passed to dataLoader
            )
        {
            var result = await _exceptionDataLoader.LoadAsync(location);
            return result;
        }
        
        #endregion DATA LOADER
    }
}

GroupDataLoader.cs

namespace DataLoader;

public class ExceptionGroupDataLoader : GroupedDataLoader<string, Exception>
{
    private readonly IProjectXSupplyChainDbManagement _projectXSupplyChainDbManagement;
    
    public ExceptionGroupDataLoader(
        IBatchScheduler batchScheduler, 
        IProjectXSupplyChainDbManagement projectXSupplyChainDbManagement,
        DataLoaderOptions options = null) : base(batchScheduler, options)
    {
        _projectXSupplyChainDbManagement = projectXSupplyChainDbManagement;
    }

    protected override async Task<ILookup<string, Exception>> LoadGroupedBatchAsync(IReadOnlyList<string> keys, CancellationToken cancellationToken)
    {
        var result = await _projectXSupplyChainDbManagement
            .ExceptionRepository.GetExceptionByStockNumbers(keys, cancellationToken)
            .ToLookupAsync(x => x.Location, cancellationToken);

        return result;
    }
}

Repo.cs

namespace Repositories
{
    public interface IExceptionRepository
    {
        public IAsyncEnumerable<Entities.Sql.Exception> GetExceptionByStockNumbers(
            IReadOnlyList<string> locations,
            CancellationToken cancellationToken);
    }

    public class ExceptionRepository: IExceptionRepository
    {
        private readonly ProjectXSqlDbContext _projectXSqlDbContext;

        public ExceptionRepository(ProjectXSqlDbContext projectXSqlDbContext)
        {
            _projectXSqlDbContext = projectXSqlDbContext;
        }

        public  IAsyncEnumerable<Entities.Sql.Exception> GetExceptionByStockNumbers(
            IReadOnlyList<string> locations, 
            CancellationToken cancellationToken)
        {
            var exceptions = _projectXSqlDbContext.Exceptions
                .Where(_ => locations.Contains(_.Location))
                .AsAsyncEnumerable()
                // .ToDictionaryAsync(_ => _.Location, cancellationToken)
                ;
            return exceptions;
        }
    }
}

GThree
  • 2,708
  • 7
  • 34
  • 67

1 Answers1

0

I did this locally and think the below should represent what I did.

  1. Add the filters to the IExceptionService interface:

    Task<IEnumerable<Database.Entities.Sql.Exception>> GetExceptionsByStockNumberV2(
        string location, long stockNumber = 0, string type = null, string supplier = null);
    
  2. Update the ExceptionService implementation to include the nnew filters and pass them to the repository method:

    ````
    public async Task<IEnumerable<Database.Entities.Sql.Exception>> GetExceptionsByStockNumberV2(
        string location, 
        long stockNumber = 0,
        string type = null,
        string supplier = null)
    {
        var result = await _exceptionDataLoader.LoadAsync(location, stockNumber, type, supplier);
        return result;
    }
    
    
    
  3. Add the filters to the IExceptionRepository interface

    public IAsyncEnumerable<Entities.Sql.Exception> GetExceptionByStockNumbers(
        IReadOnlyList<string> locations,
        CancellationToken cancellationToken,
        long stockNumber = 0,
        string type = null,
        string supplier = null);
    
  4. Update the ExceptionRepository to apply the filters to the query

    public IAsyncEnumerable<Entities.Sql.Exception> GetExceptionByStockNumbers(
        IReadOnlyList<string> locations, 
        CancellationToken cancellationToken,
        long stockNumber = 0,
        string type = null,
        string supplier = null)
    {
        var query = _projectXSqlDbContext.Exceptions
            .Where(_ => locations.Contains(_.Location));
    
        if (stockNumber > 0)
        {
            query = query.Where(e => e.StockNumber == stockNumber);
        }
    
        if (!string.IsNullOrEmpty(type))
        {
            query = query.Where(e => e.Type == type);
        }
    
        if (!string.IsNullOrEmpty(supplier))
        {
            query = query.Where(e => e.Supplier == supplier);
        }
    
        return query.AsAsyncEnumerable();
    }
    

You may need to tweak the property names for the Where clauses to match the Exception entity property names

Locally I have in my Services folder:

````
using System.Collections.Generic;
using System.Linq;
using GraphQLExample.Models;
using GraphQLExample.Services;
using GraphQLExample.GraphQL;

namespace GraphQLExample.Services
{
    public class ExceptionService
    {
        private readonly List<InventoryException> _exceptions;

        public ExceptionService()
        {
            _exceptions = new List<InventoryException>
            {
                new InventoryException { Id = 1, Location = "ATL", Type = "Car", Supplier = "Hertz", StockNumber = 101 },
                new InventoryException { Id = 2, Location = "ATL", Type = "Car", Supplier = "Avis", StockNumber = 102 },
                new InventoryException { Id = 3, Location = "ATL", Type = "Truck", Supplier = "Budget", StockNumber = 103 }
            };
        }

        public IEnumerable<InventoryException> GetExceptionsByFilters(
            string location,
            string type = null,
            string supplier = null,
            long stockNumber = 0)
        {
            IQueryable<InventoryException> query = _exceptions.AsQueryable().Where(e => e.Location == location);

            if (!string.IsNullOrEmpty(type))
            {
                query = query.Where(e => e.Type == type);
            }

            if (!string.IsNullOrEmpty(supplier))
            {
                query = query.Where(e => e.Supplier == supplier);
            }

            if (stockNumber > 0)
            {
                query = query.Where(e => e.StockNumber == stockNumber);
            }

            return query.ToList();
        }
    }
}
````

Models folder:


    namespace GraphQLExample.Models
    {
        public class InventoryException
        {
            public int Id { get; set; }
            public string Location { get; set; }
            public string Type { get; set; }
            public string Supplier { get; set; }
            public long StockNumber { get; set; }
        }
    }
    ````

and

    ````
    namespace GraphQLExample.Models
    {
        public class InventoryExceptionType : ObjectType<InventoryException>
        {
        }
    }
    ````

GraphQL folder

    ````
    using System.Collections.Generic;
    using GraphQLExample.Services;
    using HotChocolate;
    using GraphQLExample.GraphQL;
    
    namespace GraphQLExample.GraphQL
    {
        public class Query
        {
            public IEnumerable<Models.InventoryException> GetExceptions(
                string location,
                [Service] ExceptionService exceptionService,
                string type = null,
                string supplier = null,
                long stockNumber = 0)
            {
                return exceptionService.GetExceptionsByFilters(location, type, supplier, stockNumber);
            }
        }
    }
    ````

and

    ````
    using HotChocolate.Types;
    using GraphQLExample.Models;
    using GraphQLExample.Services;
    
    namespace GraphQLExample.GraphQL
    {
        public class InventoryExceptionQueryType : ObjectType<Query>
        {
            protected override void Configure(IObjectTypeDescriptor<Query> descriptor)
            {
                descriptor
                    .Field(f => f.GetExceptions(default, default, default, default, default))
                    .Name("exceptions")
                    .Argument("location", a => a.Type<NonNullType<StringType>>())
                    .Argument("type", a => a.Type<StringType>()) // Make this argument nullable
                    .Argument("supplier", a => a.Type<StringType>()) // Make this argument nullable
                    .Argument("stockNumber", a => a.Type<IntType>()) // Make this argument nullable
                    .Type<ListType<NonNullType<InventoryExceptionType>>>();
            }
        }
    
    }
    ````
and return in Banana Cake Pop:
[![ATL][1]][1]
[![ATL and Car][2]][2]
[![ATL and Car and Hertz][3]][3]


  [1]: https://i.stack.imgur.com/HF5tz.png
  [2]: https://i.stack.imgur.com/QglOl.png
  [3]: https://i.stack.imgur.com/kEOIv.png
pauliec
  • 406
  • 2
  • 8