Practical .NET

Creating Flexible Queries with Parameters in GraphQL

GraphQL gives clients who call your Web services the ability to specify what properties of your data objects they want. Here are two ways to let those clients also specify which data objects they want.

In an earlier article, I showed how to create a Web service that would accept GraphQL queries (a GraphQL query uses a JSON object to describe the data to be returned). This URL, for example, would be used to retrieve, from the service at localhost:64447/graphql, a collection of Customer objects with each object including the customer's customerId and name properties:

http://localhost:64447/graphql?query={customers{customerId, name}} 

However, for this Web service to be genuinely useful it would need to support a wider variety of queries. For example, at the very least, clients should be able to provide criteria to select which Customer objects are returned from your server. You should be support queries that specify the customer using the customerId. In GraphQL, that query would look like this:

http://localhost:64447/graphql?query={customeru( id:1 ) {customerId, name}}

Comparing these two queries shows two differences:

  • The first query begins "customers" and the second begins "customeru"
  • The second query, in addition to specifying the fields to be retrieved (customerId and name) provides a parameter specifying which customer is to be retrieved (the one with an id of 1)

Extending the original GraphQL-enabled service from my original article to support more queries can be done in, at least, two ways. First, I'll show a solution that requires the client to distinguish between the two queries, as I've done with my customers and customeru examples. But, with that done, I'll also provide another solution that doesn't require the client to use the "right" query name -- the service will figure out what to do based on the parameters provided.

Enhancing Accessing the Data
A GraphQL-enabled repository uses a schema class to define the queries it supports. That schema class accepts a resolver that handles fetching the data from the data source. And that resolver depends on a repository class that actually fetches the data.

In the example from my previous column, my resolver is a class that uses Entity Framework to return a List of Customer objects. The key method is a "get all" method that looks like this:

public IList<Customer> GetAll()
{
  CustContext db = new CustContext();
  return db.Customers.ToList();
}

That method doesn't need to be changed to support the new query format that specifies a single customer ... but, if I leave the method as is, I'm going to have a terrifically inefficient application: This method will retrieve every Customer object in order for the service to return just the requested Customer object.

I can, however, rewrite that "get all" method so that I returns an IQueryable result. If I do that, then any query I run against this method will be merged with the query issued to the database (I discussed this nifty feature in another post).

The change is simple: I change my method's return type to IQueryable and change the last line to return an AsQueryable result instead of a List. The new version of the method looks like this:

public IQueryable<Customer> GetAll()
{
   CustContext db = new CustContext();
   return db.Customers.AsQueryable();
}

Enhancing the Resolver
With that change made, I can move up to make the necessary changes to the schema's resolver (a class called CustomerOGT that inherits from the ObjectGraphType class). That CustomerOGT class has a nested class called GetAllQuery and, within that GetAllQuery class, I used the Field method, inherited through ObjectGraphType, to return all my Customer objects.

Here's that original code. The Field's name parameter ties this method to my customers query while the resolve parameter is passed a lambda expression that calls the GetAll method on my repository:

public class GetAllQuery : ObjectGraphType
{
   public GetAllQuery(CustomerRepository custRepo)
   {
      Field<ListGraphType<CustomerOGT>>(
                  name: "helloWorld",
                  resolve: context => custRepo.GetAll()
                );
   }
}

Obviously, now that my resolver is supporting multiple queries, I need to do some renaming. My nested class should now be called CustomerQueries, for example.

But, as long as I'm here, I should also switch from using the Field method to using the FieldAsync method (using asynchronous methods in a Web service can substantially improve the application's scalability by releasing otherwise idle threads waiting for data to be used by other requests). That change requires me to add the async keyword to the lambda expression in the resolve parameter and change the expression to use ListAsync.

Here's the replacement for the original call to the Field method, now supporting asynchronous processing:

public class CustomerQueries : ObjectGraphType<Customer>
{
  public CustomerQueries(CustomerRepository custRepo)
  {
     FieldAsync<ListGraphType<CustomerOGT>>(
               name: "customers",
               resolve: async context =>
                       await custRepo.GetAll().ToListAsync()
);

Adding a New Query
I'm now ready to add support for my customeru query by adding a new call to the FieldAsync method, using the method's name parameter to tie the method to my new customeru query. The start of that call would look like this:

FieldAsync<ListGraphType<CustomerOGT>>(
   name: "customeru",

Unlike my customers query, my customeru query accepts a single parameter. You define a query's parameters in the FieldAsync's arguments parameter, passing a QueryArguments object (and I do realize that last sentence was inherently confusing). I use that QueryArguments object to specify all the parameters my query requires, using QueryArgument objects to define each parameter.

Since my customeru query has only one parameter (the one called id), I can define that single parameter like this:

arguments: new QueryArguments(
                new QueryArgument<IdGraphType> { Name = "id" }),

My resolve method also needs to include code to grab the value of any arguments passed to the query. So, at the top of my resolve parameter for my customeru query, I grab the value of my query's id parameter, like this:

resolve: async context =>
               {
                  var id = context.GetArgument<int>("id");

If my query isn't passed an id parameter, the GetArgument method won't blow up. Instead, the method will return null (a feature I'll take advantage of later).

Finally, in my resolve method, I call my GetAll method using a Where method to specify the customer I want and calling ToListAsync to asynchronously convert the result into a collection:

return await custRepo.GetAll().Where(c => c.CustomerId == id).ToListAsync(); });

And that's it: My GraphQL-enabled service now supports both my customers and customeru queries.

An Alternative Implementation
In some ways, however, this isn't a great solution because it requires the client to distinguish between the customers and customeru queries. I can, instead, use a single FieldAsync call, define my parameter(s) on it, and then check to see which parameters were passed. In my example, if no id parameter is passed, I return all the Customer objects; if an id parameter is passed, I return a single Customer object.

If I'm going to do that then I might as well support a bunch of parameters: In addition to letting clients request customers by customerId, I can also let clients supply a fullName parameter and return customers with a matching FullName property. The start of my single FieldAsync method would now look like this to support both parameters:

FieldAsync<ListGraphType<CustomerOGT>>(
                     name: "customers",
                     arguments: new QueryArguments(
                            new QueryArgument<IdGraphType> { Name = "id" },
                           new QueryArgument<IdGraphType> { 
                                                      Name = "fullName" }
               ),

At the start of the resolve parameter's lambda expression, I'll need to use nullable types when retrieving QueryArgument values in order to handle absent parameters. Revising my old code to support both parameters looks like this:

resolve: async context =>
                {
                    int? id = context.GetArgument<int?>("id");
                    var fullName = context.GetArgument<string>("fullName");

Finally, I need a query that will handle any combination of those parameters. Unfortunately, LINQ doesn't (yet) support generating dynamic queries and I'd prefer not to write some complex logic to choose between pre-defined LINQ statements. Instead, I'll use a LINQ query that searches on the CustomerId property if it's present and on the CustomerId and FullName properties if they're present..

Here's the end of my resolve parameter that supports any combination of the two parameters:

return await custRepo.GetAll().Where(
       c => (id == null || c.CustomerId == id) &&
              (fullName == null || c.FullName == fullName)).ToListAsync();}

Now, a client can submit all these queries to my service:

http://localhost:64447/graphql?query={customers{customerId, name}}
http://localhost:64447/graphql?query={customers( id:1 ) {customerId, name}}
http://localhost:64447/graphql?query={customers( fullName:"Peter Vogel" ) {customerId, name}}
http://localhost:64447/graphql?query={customers( id:1, fullName:"Peter Vogel" ) 
                                                {customerId, name}}

And, I suspect, developers using my service will appreciate that flexibility.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube