Azure Functions with ComsosDB and GraphQL
If you’re looking to try out GraphQL with Azure Functions and CosmosDB, look no further. I wanted to take the time to put together a fully functioning sandbox project which you can download and tinker with as there are a few snippets online, but nothing that pulled together all of the pieces, especially with the data loaders.
Why Am I Here?
If you are here, I presume you have already heard the gospel of GraphQL and you just want to figure out how to make it work with Functions and Cosmos.
For the rest of the readers, GraphQL is a “query language for your API”. I like to think of it as an application layer that allows you to specify the types of objects you want by prototype. In other words, tell your API the shape of the data you want, and your GQL layer converts that into a series of queries to whatever data store you are using and you get back what you asked for in the same shape.
GQL is not a magic bullet; it requires a lot of rather tedious work, in fact, to implement. If you are familiar with FluentNHibernate and class mapping, then you already have the gist of what needs to be done to get GQL to work. The essence is creating a set of mappings that describe how to satisfy the prototypes submitted as queries.
So Why Would I Want That?
There are three key benefits in my mind.
- Flexibility – You get a query entry point which is quite flexible and you can execute multiple types of queries from a single entry point. In some cases, this can be very useful compared to having discrete, single purpose REST API endpoints. In other cases? I think it can actually be quite verbose and be somewhat more challenging to use than a simple set of parameters.
- Data Shaping – You get results which match the shape of the prototype. For example, if you only ask for the id and name properties for an entity, then you only get those properties back. If you are familiar with OData, there are a lot of similarities (though it turns out that there are folks with some very strong opinions on this topic). There are many benefits including masking server side domain model properties from the front-end (which you can also do with a simple view model) and of course, reduced network throughput by only returning the properties that are needed at the UI.
- Reducing Roundtrips – The Data Loader concept allows batching of nested queries to reduce round trips to query for related objects which you want to display in a single view. In general, its even more efficient to write a back-end query that performs the relational joins or additional lookups, but you lose the flexibility aspect since your front-end API is now tightly coupled to a specific back-end operation rather than a generalized batch load operation.
Depending on the nature of your application and your team, implementing GQL for your application can actually represent a significant overhead with only minimal benefit. This is especially true, in my opinion, when you don’t really need the flexibility and your front-end teams are working closely with your back-end teams (or they’re the same team!) as you can build even more efficient mechanisms. However, its easy to see how this could be an incredibly powerful framework to teams where the separation of responsibilities is very discrete as it allows the back-end teams, the application layer teams, and the front-end teams to provide a very flexible mechanism to build a variety of use cases using only a small number of interfaces between them.
In general, I think GQL seems to be a better fit for bigger organizations with more discrete teams and responsibilities producing Internet facing applications.
There are additional benefits as well and other patterns which can be built on top of GQL, but that is a topic for another day.
Running the Sample
If you’re following along at home, you can grab the full source code from here: https://github.com/CharlieDigital/AzFunctionsGraphQL (I strongly recommend doing so because there is a lot of code which I’m simply going to summarize here). I assume you already have the CosmosDB emulator installed.
The example models a simple system where there are Stores which have Tools which are available to rent. Each Tool belongs to exactly one Store. In Cosmos, we could embed the Tool into the Store, but this could be problematic as the number of Tools in a Store is unbounded (which could lead to massive documents) and updating an individual Tool’s status would require a large quantity of RU/s.
To run the sample, create a ToolStore database in Cosmos and using Postman or another HTTP utility, issue the following query:
1 |
GET http://localhost:7071/api/init |
This will initialize the database with some sample data.
To query the database, try the following:
On to the Code!
Start by creating a new Functions project and installing the following packages:
1 2 3 4 |
install-package microsoft.azure.functions.extensions install-package microsoft.azure.cosmos install-package graphql install-package graphql.systemtextjson |
We’re using the GraphQL.NET library.
Next, write your data access layer more or less how you would normally write it. In this case, we’re using a repository pattern to encapsulate the actual queries to Cosmos.
Now on top of this, we layer GQL. This requires that we create a set of artifacts:
- View model types (/Core/Model/GraphViewModels). This is not strictly necessary; it is perfectly fine to use your domain model types if you’d like, but you will find a disconnect between your GQL layer and your domain layer entities without a set of view models. For example, in your domain model, you may have an entity called Store which does not have a property Tools
- Mapping types (/Core/Model/GraphTypes). If you’ve worked with FluentNHibernate, then you are familiar with mapping classes which describe to the ORM how the shape of the data maps from one layer to another. This is precisely what the graph type definition classes do. It’s important to understand that GQL is not an ORM like Hibernate or Entity Framework in the sense that it has no intelligence on how to look up relationships; such actions are always explicitly implemented in the mapping types.
- Query types (/Core/Model/Queries). These classes define the universe of queries which can be satisfied by the GQL layer by defining incoming query parameters, mutations, and resolvers (e.g. functions which perform lookups).
- Schema types (/Core/Model/Schemas). The schema represents the universe of operations and entities that the GQL layer operates on. Our queries will be executed against a schema.
In the Functions startup, we register our dependencies:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// Registrations for domain data access. builder.Services.AddSingleton(new CosmosClient( Environment.GetEnvironmentVariable("CosmosEndpoint"), Environment.GetEnvironmentVariable("CosmosAuthKey"))); builder.Services.AddSingleton<CosmosGateway>(); builder.Services.AddSingleton<StoreRepository>(); builder.Services.AddSingleton<ToolRepository>(); // Core services for GraphQL builder.Services.AddSingleton<JsonSerializerOptions>(); builder.Services.AddSingleton(new ErrorInfoProviderOptions()); builder.Services.AddSingleton<IErrorInfoProvider, ErrorInfoProvider>(); builder.Services.AddSingleton<IDocumentExecuter, DocumentExecuter>(); builder.Services.AddSingleton<IDocumentWriter, DocumentWriter>(); // Needed for DataLoader builder.Services.AddSingleton<IServiceProvider>(s => new FuncServiceProvider(s.GetRequiredService)); builder.Services.AddSingleton<IDataLoaderContextAccessor, DataLoaderContextAccessor>(); builder.Services.AddSingleton<DataLoaderDocumentListener>(); // App specific registrations for GraphQL builder.Services.AddSingleton<StoreType>(); builder.Services.AddSingleton<ToolType>(); builder.Services.AddSingleton<StoreQuery>(); builder.Services.AddSingleton<StoreSchema>(); |
It is tedious without some helper classes as you will need to add each mapping type class.
The Function itself is quite simple. First our constructor accepts the injected dependencies that we’ll need:
1 2 3 4 5 6 7 8 9 10 11 |
public QueryService(IDocumentWriter documentWriter, DataLoaderDocumentListener listener, StoreSchema schema, CosmosGateway cosmos, StoreRepository stores, ToolRepository tools) { _documentWriter = documentWriter; _listener = listener; _schema = schema; _cosmos = cosmos; _stores = stores; _tools = tools; } |
Then we write a simple Function endpoint to accept our queries:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
[FunctionName("Query")] public async Task<IActionResult> ExecuteQuery( [HttpTrigger(AuthorizationLevel.Function, "post", Route = "query")] HttpRequest req, ILogger log) { string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); string json = await _schema.ExecuteAsync(_documentWriter, options => { options.Query = requestBody; options.Listeners.Add(_listener); // Supports DataLoader options.UnhandledExceptionDelegate = context => { // Exceptions do not bubble out; must be explicitly handled. log.LogError(context.Exception.ToString()); }; }); return new OkObjectResult(json); } |
For service level authentication and authorization, check out my other article on Azure Functions and JWT.
The first piece of the GQL implementation of interest is the query:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public StoreQuery(StoreRepository stores) { Field<StoreType>("stores", // Define the list of available arguments; one for each one for this scope arguments: new QueryArguments( new List<QueryArgument> { // This argument allows us to filter the store by ID. new QueryArgument<GuidGraphType> { Name = "id" } }), // Define the resolver which consumes the arguments resolve: context => { Guid storeId = context.GetArgument<Guid>("id"); Store store = stores.GetByIdAsync(storeId).Result; // Note that we convert this to the view model. The tools property is resolved separately by the DataLoader. return new StoreV { Id = store.Id, Name = store.Name, PhoneNumber = store.PhoneNumber }; }); } |
This is a very barebones implementation and in fact, does not even provide the mechanism to load all stores.
Now that we have the mechanism to load a store, the question is how we load the tools. I suppose that there are number of ways that this could work and differs by your underlying database and ORM. If your ORM, for example, already has its own mapping layer that can lazy load the tools property, it could conceivably be done using your ORM directly rather than writing another query. However, in this case, we need to add an implementation to our mapping class for stores:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
public class StoreType : ObjectGraphType<StoreV> { private ToolRepository _tools; /// <summary> /// Injection constructor. /// </summary> /// <param name="loaderContext">The injected loader context for DataLoader.</param> /// <param name="tools">The injected repository for tools for lookup of store tools.</param> public StoreType(IDataLoaderContextAccessor loaderContext, ToolRepository tools) { _tools = tools; Field(s => s.Id); Field(s => s.Name); Field(s => s.PhoneNumber); // Now we want the tools; this is how we wire up the DataLoader. Field<ListGraphType<ToolType>>("tools", resolve: context => { // Absolutely no idea what the string name is for. IDataLoader<Guid, IEnumerable<ToolV>> loader = loaderContext.Context.GetOrAddCollectionBatchLoader<Guid, ToolV>( "GetToolsByStoreId", GetToolsByStoreIdAsync); return loader.LoadAsync(context.Source.Id); }); } /// <summary> /// This function is invoked to load the sub-tree of tools by store. /// </summary> /// <param name="storeIds">The batched store IDs.</param> /// <returns>A lookup list of tools by store IDs.</returns> public async Task<ILookup<Guid, ToolV>> GetToolsByStoreIdAsync(IEnumerable<Guid> storeIds) { List<ToolV> tools = (await _tools.GetItemsFiltered(0, 100, t => t.Name, SortDirection.Ascending, t => storeIds.Contains(t.StoreId))) .Select(t => new ToolV { // TODO: Use some other mechanism to do this more efficiently! Id = t.Id, Name = t.Name, HourlyPrice = t.HourlyPrice, StoreId = t.StoreId }).ToList(); return tools.ToLookup(t => t.StoreId); } } |
This class and the function perform a batch lookup of Tools by Stores using the data loader pattern. It’s simply a mechanism for batching the retrieval of nested dependencies to reduce the number of roundtrips to the backend.
Wrap Up
This sample isn’t exhaustive, but you should be able to clone, compile, and get it running to experiment with whether GQL is right for your project. A big reason why I wanted to push this out there is because there is no complete implementation of Functions with GQL; hopefully, you found this useful! I think that teams have to evaluate carefully whether the benefits that GQL brings align with the structure of the team and the nature of the system.
1 Response
[…] If you liked this article, you might also be interested in Azure Functions with CosmosDB and GraphQL. […]