A real-time Event Grid viewer with serverless SignalR
Tags: serverless, signalr, eventgrid, azure
Published: 2020-06-17 07:43:51

## A step by step guide to build an Event Grid viewer using serverless SignalR and Azure Functions

In a microservice architecture we might have several decoupled microservices driven by events (aka event-based architecture). At some point, a developer might want to see the current status of the system, just a big picture of what is going on. In a classic monolithic system it might be easy since we only have one big service to look at. In an event-based architecture we can use an **event viewer**, a simple tool for printing the different events that are occurring in our system in live time.

In this post you will find a step by step guide to build a real-time event viewer for Azure Event Grid based on a serverless SignalR service.

## The technology

First, let's take a look to the different services and tools we are going to use in this guide:

- **[Azure Event Grid:](https://docs.microsoft.com/en-us/azure/event-grid/overview)** a service for managing events. It takes care of routing the incoming events to the specific subscription based on filters.

- **[Azure Function:](https://docs.microsoft.com/en-us/azure/azure-functions/functions-overview)** a serverless computing service.

- **[Azure SignalR:](https://docs.microsoft.com/en-us/azure/azure-signalr/signalr-overview)** a service for enabling real-time web functionality to applications.

So, how do all these pieces fit together?

1. SignalR Client application (in this case an Angular based app) must connect to the SignalR service. It makes use of a negotiation API exposed in an Azure Function to get the information to connect to the SignalR service, the SignalR url and a corresponding token.

2. Our Azure Function will be triggered by new events in the system through Event Grid. Using Functions output binding it will push each event into the serverless SignalR service.

3. Client application will receive in real-time those events using WebSockets.

![Solution](https://dev-to-uploads.s3.amazonaws.com/i/bmznfolcxvblm16lfinp.PNG)

Now let's move on to explaining the whole process step by step in detail.

## 1. Creating the Azure Function App

To create the Azure Function App we will use the [Azure Function Core tools](https://github.com/Azure/azure-functions-core-tools).

```bash
npm i -g azure-functions-core-tools@3 --unsafe-perm true
```

In a fresh new folder we will create the Function App and the Function:

```bash
func init
func new -> select http-trigger -> choose a name
```

Now we have a fresh new Azure Function ready to be launched and tested:

```bash
func start
curl http://localhost:7071/api/CloudEventSubscription
```

## 2. Subscribing to Event Grid

Azure Event Grid supports two different event schemas:

- Event Grid Schema
- [Cloud Event Schema v1.0](https://github.com/cloudevents/spec)

In this guide, we are going to use Cloud Event Schema v1.0. If you want to use Event Grid Schema you will need to make some minor changes to the validation subscription logic.

Azure Functions have a built-in trigger for Event Grid. However, it only works with Event Grid Schema. Cloud Events subscriptions must be done using a http trigger and doing the subscription validation manually.

To receive events in our Function we need to update the *HttpTrigger* input binding to allow **OPTIONS** and **POST** requests. With CloudEvents schema, **OPTIONS** verb is used for validating  the subscription and **POST** for receiving the events.

```cs
[HttpTrigger(AuthorizationLevel.Function, "options", "post", Route = null)] HttpRequest req
```

Then add the logic to validate the subscription.

```cs
/*
Handle EventGrid subscription validation for CloudEventSchema v1.0
Validation request contains a key in the Webhook-Request-Origin
header, that key must be set in the Webhook-Allowed-Origin response
header to prove to Event Grid that this endpoint is capable of handling CloudEvents events.
*/
if (HttpMethods.IsOptions(req.Method))
{
    if(req.Headers.TryGetValue("Webhook-Request-Origin", out var headerValues))
    {
        var originValue = headerValues.FirstOrDefault();
        if(!string.IsNullOrEmpty(originValue))
        {
            req.HttpContext.Response.Headers.Add("Webhook-Allowed-Origin", originValue);
            return new OkResult();
        }

        return new BadRequestObjectResult("Missing 'Webhook-Request-Origin' header");
    }
}
```

The Function is ready to be subscribed to Event Grid, but we don't have the Event Grid resource created in Azure yet.

We are going to create the Azure resources directly from the console using [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest). Let's start creating a new resource group.

```bash
az group create --name event-grid-viewer-serverless --location northeurope
```

Now we can create the Event Grid Topic in that resource. To do that we need to make use of an [Azure CLI extension](https://github.com/Azure/azure-cli-extensions).

```bash
az extension add -n eventgrid
az eventgrid topic create --resource-group event-grid-viewer-serverless \
   --name topic --location northeurope --input-schema cloudeventschemav1_0
```

To create the subscription we need to expose the Azure Function using `ngrok` or any other similar tool.

```bash
az eventgrid event-subscription create \ 
   --source-resource-id /subscriptions/<your subscription id>/resourceGroups/event-grid-viewer-serverless/providers/Microsoft.EventGrid/topics/topic \
   --name serverless-signalr-function \
   --endpoint <your public Azure Function url>/api/CloudEventSubscription \ 
   --endpoint-type webhook \
   --event-delivery-schema cloudeventschemav1_0
```

Then we can test our fresh new subscription sending an event through Event Grid. To make the command easier we can create a sample CloudEvent event in a JSON file.

```json
{
   "specversion":"1.0",
   "type":"com.serverless.event",
   "source":"mysource/",
   "subject":"123",
   "id":"A234-1234-1234",
   "time":"2020-06-14T12:00:00Z",
   "datacontenttype":"application/json",
   "data":"{\"eventProperty\":\"eventValue\"}"
}
```

The event can be sent using `curl` but we need to specify the Topic URL and Key that can be retrieved directly from the command line.

```bash
// Get the Topic URL
az eventgrid topic show --name topic \
   -g event-grid-viewer-serverless \
   --query "endpoint" --output tsv

// Get the Topic key
az eventgrid topic key list --name topic \
   -g event-grid-viewer-serverless \
   --query "key1" --output tsv

// Send the event
curl --request POST \
   --header "Content-Type: application/cloudevents+json; charset=utf-8" \
   --header "aeg-sas-key: <Topic Key>" \
   --data @event.json <Topic URL>
```

## 3. Enabling real-time

As explained at the beginning of the post, we will use SignalR for enabling real-time in our web application. Since we don't have a back-end but a serverless Azure Function for handling the SignalR connection we will use SignalR in serverless mode. We can create it easily with Azure CLI.

```bash
az signalr create --name serverless-signalr \
   --resource-group event-grid-viewer-serverless \
   --sku Free_F1 --service-mode Serverless \
   --location northeurope
```

Then we must install the [Azure Functions Binding for SignalR](https://github.com/Azure/azure-functions-signalrservice-extension). It will be used to ease the interaction between the Function and SignalR.

```bash
func extensions install -p Microsoft.Azure.WebJobs.Extensions.SignalRService -v 1.0.0
```

Client applications need some credentials to connect to the SignalR service. We need a new  `negotiate` Function that, by making use of the Azure Functions Binding for SignalR, will return the credentials information to connect to SignalR.

```cs
[FunctionName("negotiate")]
public static SignalRConnectionInfo GetSignalRInfo(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req,
    [SignalRConnectionInfo(HubName = "hubName")] SignalRConnectionInfo connectionInfo)
{
    return connectionInfo;
}
```

By default, these bindings expect the SignalR **ConnectionString** to be set in a specific value on the application settings. To enable it locally we must add a new property to the `local.settings.json` file.

```json
{
    "IsEncrypted": false,
    "Values": {
        "AzureWebJobsStorage": "UseDevelopmentStorage=true",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet",
        "AzureSignalRConnectionString": "<Your SignalR Connection String>"
    }
}
```

Then, we want to push the incoming events from Event Grid to SignalR. First, we need to update the original Function to add an output binding for SignalR.

```cs
[SignalR(HubName = "hubName")] IAsyncCollector<SignalRMessage> signalRMessages
```

And add the logic to push the event to the SignalR service.

```cs
if(HttpMethods.IsPost(req.Method))
{
    string @event = await new StreamReader(req.Body).ReadToEndAsync();
    await signalRMessages.AddAsync(new SignalRMessage
    {
        Target = "newEvent",
        Arguments = new[] { @event }
    });
}
```

## 4. Client application

The last piece of the puzzle is the client application. First, it will negotiate the credentials with the Function, and then it will wait for new incoming messages from SignalR. In this tutorial we are going to use Angular (and Angular CLI) to create the client application.

```bash
ng new viewer-app
```

There is a npm package [@aspnet/signalr](https://www.npmjs.com/package/@aspnet/signalr) that makes the use of SignalR in the client side very easy.

```bash
npm i @aspnet/signalr --save
```

This package handles for you the negotiation step and the connection to SignalR. We can add some simple logic to receive the events from a specific hub and log it into the console.

```ts
import * as SignalR from '@aspnet/signalr';

export class AppComponent {

  title = 'viewer-app';

  private hubConnection: SignalR.HubConnection;

  constructor() {
    // Create connection
    this.hubConnection = new SignalR.HubConnectionBuilder()
      .withUrl("http://localhost:7071/api")
      .build();

    // Start connection. This will call the negotiate endpoint
    this.hubConnection
      .start();

    // Handle incoming events for the specific target
    this.hubConnection.on("newEvent", (event) => {
      console.log(event)
    });
  }
}
```

## To wrap up

In this tutorial we have created a simple but effective real-time event viewer for Event Grid using some cool stuff like serverless SignalR. This client application can be easily enhanced to show the events in the page rather than the console.

You can find a full example working with some minor UI enhancements on the following repo: https://github.com/DavidGSola/serverless-eventgrid-viewer