More Performant Serverless GraphQL with Azure Functions, GraphQL for .NET, and Cosmos DB

Almost a year ago I've written about setting up a GraphQL service on top of Azure Functions and Cosmos DB. Since that time I've had a number of questions and discussions around the subject. Stefan Schoof has even created an official sample based on a demo from that post. But I never had time to write more on the subject - until now. In this post I'm going to write about some key performance aspects when building such a service and maybe (just maybe) this will become a series.

I will continue building on top of the demo from the previous post. For context, below you can see the types and queries I'm going to start with.

internal class CharacterType : ObjectGraphType<Character>
{
    public CharacterType()
    {
        Field(t => t.CharacterId);
        Field(t => t.Name);
        Field(t => t.BirthYear);
    }
}
internal class PlanetType: ObjectGraphType<Planet>
{
    ...

    public PlanetType(IDocumentClient documentClient, IDataLoaderContextAccessor dataLoaderContextAccessor)
    {
        _documentClient = documentClient;

        Field(t => t.WorldId);
        Field(t => t.Name);

        Field<ListGraphType<CharacterType>>(
            "characters",
            resolve: context => return _documentClient.CreateDocumentQuery<Character>(_planetsCollectionUri, _feedOptions)
                                    .Where(c => c.HomeworldId == context.Source.WorldId)
        );
    }
}
internal class StarWarsQuery: ObjectGraphType
{
    ...

    public StarWarsQuery(IDocumentClient documentClient)
    {
        Field<ListGraphType<PlanetType>>(
            "planets",
            resolve: context => documentClient.CreateDocumentQuery<Planet>(_planetsCollectionUri, _feedOptions)
        );

        Field<ListGraphType<CharacterType>>(
            "characters",
            resolve: context => documentClient.CreateDocumentQuery<Character>(_charactersCollectionUri, _feedOptions)
        );
    }
}

As we are talking about performance, we should be measuring something. I'm going to use Azure Monitor to measure total request units for queries.

DataLoader

If you start searching for information about GraphQL performance, one of the first things you will probably encounter is DataLoader. It has a simple purpose, to reduce requests to the backend via batching and caching. What does it mean? Consider below GraphQL query.

{
    planets
    {
        name,
        characters
        {
            name,
            birthYear
        }
    }
}

It's grabbing a list of parents (planets) with their children (characters). For my simple dataset (it's available in SeedFunction in the demo project), this costs 32.15 RSU. Quite a lot. The reason is that GraphQL performs a separate query for children of every parent. This is known as "N+1 selects" problem and if you've been working with Entity Framework (or any other ORM) you probably know it very well. DataLoader is a solution to this problem. In order to use it, we need to inject IDataLoaderContextAccessor into our parent type and register a batch loader. Batch Loader is a simple method that will be given an enumeration of identifiers and is supposed to query all items in one batch. This can look like below.

internal class PlanetType: ObjectGraphType<Planet>
{
    ...

    public PlanetType(IDocumentClient documentClient, IDataLoaderContextAccessor dataLoaderContextAccessor)
    {
        _documentClient = documentClient;

        Field(t => t.WorldId);
        Field(t => t.Name);

        Field<ListGraphType<CharacterType>>(
            "characters",
            resolve: context => {
                var dataLoader = dataLoaderContextAccessor.Context
                    .GetOrAddCollectionBatchLoader<int, Character>("GetCharactersByHomeworldId", GetCharactersByHomeworldIdAsync);

                return dataLoader.LoadAsync(context.Source.WorldId);
            }
        );
    }

    private Task<ILookup<int, Character>&7gt; GetCharactersByHomeworldIdAsync(IEnumerable homeworldIds)
    {
        return Task.FromResult(_documentClient.CreateDocumentQuery<Character>(_planetsCollectionUri, _feedOptions)
            .Where(c => homeworldIds.Contains(c.HomeworldId))
            .ToLookup(c => c.HomeworldId)
        );
    }
}

For my data, this change brings the query cost down to 5.22 RSU. That's a huge difference and having DataLoader where appropriate should be one of your best practices for GraphQL.

Querying Only Requested Fields

Another thing specific about GraphQL is that when a client makes a query, the list of fields always needs to be specified. Very often this list is only a subset of fields available for a type. Take a look at the below query.

{
    characters
    {
        characterId,
        name
    }
}

This costs 2.84 RSU and queries Cosmos DB for all fields on a character. It doesn't have to be like this. We can access the list of request fields through ResolveFieldContext.SubFields. This allows for building a simple helper that will create a tailored SQL query.

internal static class ResolveFieldContextExtensions
{
    public static SqlQuerySpec ToSqlQuerySpec<TSource>(this ResolveFieldContext<TSource> context)
    {
        StringBuilder queryTextStringBuilder = new StringBuilder();

        queryTextStringBuilder.Append("SELECT ");

        queryTextStringBuilder.Append(String.Join(
            ',',
            context.SubFields.Keys.Select(fieldName => $"c.{Char.ToUpperInvariant(fieldName[0])}{fieldName.Substring(1)}")));

        queryTextStringBuilder.Append(" FROM c");

        return new SqlQuerySpec(queryTextStringBuilder.ToString());
    }
}

Now we can just use a different overload of CreateDocumentQuery.

internal class StarWarsQuery: ObjectGraphType
{
    ...

    public StarWarsQuery(IDocumentClient documentClient)
    {
        ...

        Field<ListGraphType<CharacterType>>(
            "characters",
            resolve: context => documentClient.CreateDocumentQuery<Character>(_charactersCollectionUri, context.ToSqlQuerySpec(), _feedOptions)
        );
    }
}

For me, this brings the cost down to 2.79 RSU, where the difference is one field. On top of that, there is less data to push through the network.

Arguments

One more thing I wanted to touch here are arguments. This is a little bit different than DataLoader and requested fields. If you don't provide an implementation for arguments they are not supported at all. But somehow I felt they fit here as well.

GraphQL arguments have couple of purposes. One of them is querying only a subset of data. Below is a query for only a single character.

{
    characters(characterId: "6ca83ad0a77d4d45902354c416241688") {
          name,
          birthYear
      }
}

To support such an argument, one needs to change the query definition.

internal class StarWarsQuery: ObjectGraphType
{
    ...

    public StarWarsQuery(IDocumentClient documentClient)
    {
        ...

        Field<ListGraphType<CharacterType>7gt;(
            "characters",
            arguments: new QueryArguments(new QueryArgument<IdGraphType> { Name = "characterId" }),
            resolve: context => documentClient.CreateDocumentQuery<Character>(_charactersCollectionUri, context.ToSqlQuerySpec(), _feedOptions)
        );
    }
}

This will not make the query work, only pass. Without handling the argument, the entire list will be returned.

Handling the argument means grabbing its value from ResolveFieldContext.Arguments and adding it to he Cosmos DB query. One place to do it can be the previously created helper.

public static SqlQuerySpec ToSqlQuerySpec<TSource>(this ResolveFieldContext<Source> context)
{
    ...

    SqlParameterCollection parameters = new SqlParameterCollection();
    if (context.Arguments.Count > 0)
    {
        queryTextStringBuilder.Append(" WHERE ");

        foreach (KeyValuePair<string, object> argument in context.Arguments)
        {
            queryTextStringBuilder.Append($"c.{Char.ToUpperInvariant(argument.Key[0])}{argument.Key.Substring(1)} = @{argument.Key} AND ");
            parameters.Add(new SqlParameter("@" + argument.Key, argument.Value));
        }

        queryTextStringBuilder.Length = queryTextStringBuilder.Length - 5;
    }

    return new SqlQuerySpec(queryTextStringBuilder.ToString(), parameters);
}

The above code is demoware. It doesn't account for different argument types and various edge cases, so never use it in production. It's just a hint of how you can start building your own implementation.

What's Next?

Those are the aspects I wanted to touch in this post. As always, you can find the complete demo code on GitHub. I hope to write a few more posts on the subject to share the answers to other most common questions I've been asked regarding building a GraphQL service with Azure Functions and Cosmos DB.