GraphQL & Azure Functions - Part 2

In this article series, we will look at implementing GraphQL with Azure Functions. In Part 1 of the series, we looked at implementing a GraphQL server that runs as Azure Functions and we discussed queries and mutations. In this part, we will look at subscriptions.  

GraphQL & Azure Functions - Part 2

Subscriptions are a stand-out feature of GraphQL. Simply putting, subscriptions are all about pushed updates from the server. Clients can choose to subscribe for updates and the server pushes the updates to subscribed clients. Subscriptions add real-time capabilities to your application. 

Simple, right? But there is more to it than meets the eye. 

Let us understand what the concept is and what goes on in the server when you have subscriptions set up. Unlike queries or mutations, subscriptions return a response stream. It can be thought of as a little pubsub system in the server. There are two streams - the source stream(event stream) and the response stream.  A source stream is a sequence of events. Events are placed into the stream, and whenever an event is placed into the event stream, it triggers the execution of a resolver corresponding to the event and it places a response into the response stream. Typically, a mutation can trigger an event. For e.g, we could trigger an event (publish) when a person is added, when a person is deleted, or when a person is updated. This event is placed into an event stream. Our subscription resolver that listens to an event(person added, person deleted or person updated) gets triggered and places a response into the corresponding response stream. When a client subscribes to your GraphQL server, they subscribe to this response stream for a particular event. The event stream (source stream) is internal to the server and is what causes the response stream. So basically subscriptions are code that runs in response to an event and every time the event is triggered the code executes and places a response into the response stream. 

Hot Chocolate, at the moment, does not support subscriptions in the isolated worker process model. The main reason, as I understand, is clarity around streaming from the isolated worker process model. But there is support for subscriptions in the in-process model. So I will be using an in-process model Azure Function in this article. 

In my Azure Function, I have the package HotChocolate.AzureFunctions installed. I am using the version 13.0.0-preview.96. The setup of my model, repository, query, and mutation is the same as that in the isolated worker process model version of the GraphQL server. I need the query in place as we always need a query in a GraphQL server. We need mutations because they trigger events as we discussed above. In my example, every time a person is added, an event is raised by the mutation. To achieve this we need to modify the AddPerson() method.

public class Mutation
{
   public async Task<Person> AddPerson(Person person, [Service] PersonRepository personRepository, [Service] ITopicEventSender topicEventSender)
	{
		personRepository.AddPerson(person);
		await topicEventSender.SendAsync("Person Added", person); 
		return person;
	}
}

We need to inject a ITopicEventSender using resolver injection. This service is responsible for sending/triggering the event. Whenever this resolver is triggered as a result of a mutation query, we first persist it to our data store. Then we can use the injected ITopicEventSender to trigger an event called Person Added. The Person Added is my topic. It is a convention in pubsub systems and I like to think of it as a holder for all my events that trigger some code immediately. The newly added person entity is sent as the event message or the payload. So that is event triggered. 

This event should then trigger the execution of some code, This is where our Subscription class comes in. In my class, there is a method, our resolver, that runs every time an event is triggered. The Subscribe annotation on the method marks the method as a subscription resolver, the code that executes when the event is triggered. The Topic attribute with the name specified denoted the topic it listens to. It is the same as the topic specified in the mutation resolver and must match for subscriptions to work. Finally, there is the EventMessage attribute on the person argument which denotes that the entity must be resolved from the event message. And my resolver returns the event message. You can adjust it to suit your needs. It might be that you only send an id as a part of the event message which is then again looked up in the subscription resolver before being sent to the clients. 

public class Subscription
{
	[Subscribe]
	[Topic("Person Added")]

	public Person OnAddPerson([EventMessage] Person person)
	{
		return person;
	}
}

So our AddPerson() method listens for any Person Added event on the server and runs when the event is triggered. The person entity is then streamed to any clients subscribed to the subscriptions (real-time updates). My subscription has one field called OnAddPerson

We also need to specify our Subscription types like Query and Mutation types. This is done at startup which we have to come up with as shown below. AddGraphQLFunction() is used to add the GraphQL server with AddQueryType<Query>() specifying the query type and AddMutationType<Mutation>() specifying the mutation type. Likewise, we have AddSubscriptionType<Subscription>() to specify our subscription type. In addition to this, we need the backing pubsub for handling the events (publish events and subscribe to events). I am using the in-memory provider with the AddInMemorySubscriptions(). But for production, a scalable system like Redis should be used. Redis has a fan-out mode that ensures that mutations raised on one instance of the function trigger the event on all the other function instances as the scale so that clients subscribed to any function instance can get the updates. It keeps the events synchronised between all the function instances.

Any clients using my GraphQL server can now subscribe for real-time updates by using the GraphQL query as shown below. Whenever a person is added (using the mutation by any client) it triggers the event which causes my subscriber resolver to run and push the update to any subscribed clients. 

subscription{
  onAddPerson {
    id
    name
  }
}

Subscriptions, unlike queries and mutations, are stateful. They are long-standing, persistent connections between your client apps and the server. Typically they work over WebSockets. But WebSocket connections to Azure Functions are not possible so that is closed roads there! But we have a much nicer solution for this. 

Hot Chocolate supports graphql-sse that makes use of the Server-Sent Events(SSE) protocol. With SSE, the client opens a persistent connection to the server. But unlike WebSockets, the communication is only one way - from server to client. This works well considering the fact that subscriptions are updates pushed from the server. The client subscribes to updates using the query above and using graphql-sse can get updates directly from the azure function. No web sockets are involved! No other service between the client and the functions apart from the backing pubsub either! 

In conclusion, GraphQL on Azure Functions brings all the advantages of both GraphQL and Azure Functions to the plate. We can expose the business layer in a very cost-effective way at the same time ensuring high availability, performance, and ease of deployment. In addition, we can take a step closer to a cloud-native architecture.