Scaling out a SignalR hub with Azure SignalR Service

In the previous article, we had a look at how to scale a SignalR hub by using Redis backplane. But you can also scale your hubs by using Azure SignalR Service. And this is what we will have a look at in this article.

Using Azure SignalR Service has some advantages over using Redis backplane. One of its core advantages is that you will no longer have to host and scale SignalR hub yourself. All you have to do is set up the service on Azure by completing a relatively simple form. And then you can connect your existing application to this service by making very few code changes.

Perhaps the only disadvantages of using Azure SignalR Service is that you will be limited to be using Azure as your hosting provider. It may be an issue if the rest of your assets are hosted on AWS or Google Cloud. And, just like it is with any cloud-hosted service, you will also be charged for the usage. However, if your application is in the need of being scaled out, then chances are that you will have little choice other than hosting it in cloud anyway.

Perhaps one notable difference between Azure SignalR Service and Redis backplane is that the former is a complete method of scaling SignalR hub, while the latter is the way of making multiple hub instances communicate with each other while your application has already been scaled out by replication. When you use Azure SignalR Service, hub connection from a client will be re-routed directly to it, while with Redis backplane, you still need to connect to one of your own application instances directly. So, if SignalR hub is the only bottleneck in your application that needs to be scaled out, you don’t even have to scale out your main application if you are using Azure SignalR Service. With Redis backplane, on the other hand, you still have to replicate the main ASP.NET Core application.

This article covers the following topics:

  • Setting up Azure SignalR Service
  • Adding Azure SignalR Service dependencies to your application
  • Overview of Azure SignalR Service REST API

By the end of this article, you will have learned how to set up Azure SignalR Service and how to connect your ASP.NET Core application to it.

Prerequisites

This article assumes that you already have set up your development environment. You will need the following:

  • A machine with either Windows, Mac OS or Linux operating system
  • A suitable IDE or code editor (Visual Studio, JetBrains Rider or VS Code)
  • .NET 6 SDK (or newer)

Also, since we are continuing to build on top of the application that we have developed in the previous article, we need the code that we have written previously. If you have skipped the previous articles, you can access the complete code from the following location in the GitHub repository:

https://github.com/fiodarsazanavets/SignalR-on-.NET-6—the-complete-guide/tree/main/Chapter-09/Part-03/LearningSignalR

The complete code samples from this article are available from the following location in the GitHub repo, which has separate folders corresponding to individual parts of the article:

https://github.com/fiodarsazanavets/SignalR-on-.NET-6—the-complete-guide/tree/main/Chapter-10

Setting up Azure SignalR Service

To create an instance of Azure SignalR Service, you will need to visit Azure Portal, which is accessible via the following URL:

https://portal.azure.com

In order to use this portal, you need to create a Microsoft account. If you don’t have one already, you will be automatically prompted to create it. And the process of creating an account is straight-forward, as you will be guided through the process.

Once you have Microsoft account set up, you will be guided on how to register with Azure. Normally, if this is your first account, you will be given $200 worth of credit, so you can try various services on the platform.

Once you’ve registered on Azure, you can start creating services on it. And the process will be straight-forward, as all the forms on Azure Portal have been designed to be as informative and as user-friendly as possible.

What you need to do next is enter Azure SignalR Service in the search box. In the results, you will be presented with a service template of this type. You will need to click on it, and this will take you to the set-up form.

The setup form will be relatively simple to use. You will need to specify the name of the service, its host location, and some other set-up information, such as payment tier. It’s up to you how you configure it, as all of the options will be explained to you on the form. But just bear in mind that it’s best to choose the geographic location of the hosting datacenter as close to your potential users as possible (or to your own location if you only intend to use it for test purposes). This is to ensure that you have as little latency as possible.

Once the service is created, you can open it to obtain its connection string. And this is what we need for the next step of the setup process. We can start adding necessary code changes to make our application use the Azure SignalR Service instance we have just created.

Adding Azure SignalR Service dependencies to your application

To add Azure SignalR Service dependencies to our application, we need to execute the following command inside SignalRHubs project folder:

dotnet add package Microsoft.Azure.SignalR

Next, we can open Program.cs of SignalRServer project and replace the line with AddSignalR call with the following:

builder.Services.AddSignalR().AddAzureSignalR();

We can do the same in SignalRServer2 project. And there are two ways of configuring it. AddAzureSignalR method can accept a string parameter representing the connection string of the Azure SignalR Service we have created earlier. But we can also leave it without the parameter. But to make it work, we will then need to use User Secrets tool inside the project.

If you don’t want to pass the connection string in the code of your Program.cs file, you would need to execute the following command inside the project folder:

dotnet user-secrets init

Then, you would need to execute the following command:

dotnet user-secrets set Azure:SignalR:ConnectionString "<Azure SignalR Service connection string"

Azure:SignalR:ConnectionString is the key of User Secrets tool that will be looked up by the middleware if no connection string is explicitly specified.

That’s all we need to do to connect our app to Azure SignalR Service. Your client connection will now be re-routed to it. However, the clients will still be able to trigger methods in your own Hub class. Only that anything that sends messages to the clients or groups thereof inside those methods will then reroute those calls to the cloud, so your own application won’t have to deal with the load.

There is only one extra thing that we need to apply to it if our existing app was using HubContext outside the hub, as the default IHubContext implementation isn’t compatible with Azure SignalR Service connection. But luckily, we won’t have to apply many code changes to make it work, as you shall see shortly.

Making HubContext work with Azure SignalR Server

As you may recall from the previous article, HubContext is a mechanism that allows you to send messages to SignalR hub clients from outside the hub class. For example, it is useful if you have a background process that needs to update clients on regular basis. Likewise, you may have some event listener in your server application that updates clients whenever some event gets triggered.

If we use a monolithic SignalR hub or scale it our via Redis backplane, the implementation of IHubContext interface will be automatically injected into the constructor of a class that needs to use it. Calling AddSignalR method on the service collection will ensure that all dependencies will be applied and that all appropriate implementations will be automatically resolved. However, this will not work if you use Azure SignalR Service. But the good news is that there is a library that allows you to inject an implementation of IHubContext that is specific to Azure SignalR Service. So, you will still be able to inject IHubContext instances into the classes. You will just need to make some changes to dependency injection logic.

Before we start, we need to install a NuGet package that contains the IHubContext implementation that we need. To do so, we will need to execute the following command inside SignalRHubs project folder:

dotnet add package Microsoft.Azure.SignalR.Management

Then, we will open Program.cs file of our SignalRServer project and add the following namespace reference to it:

using Microsoft.Azure.SignalR.Management;

We will then add the following code just before builder.Build call, replacing <Azure SignalR Service connection string> with the actual connection string that you can obtain from Azure SignalR Service instance:

var serviceManager = new ServiceManagerBuilder()
    .WithOptions(option =>
    {
        option.ConnectionString = "<Azure SignalR Service connection string>";
    })
    .BuildServiceManager();

var hubContext = await serviceManager.CreateHubContextAsync("LearningHub", CancellationToken.None);

builder.Services.Add(new ServiceDescriptor(typeof(IHubContext<Hub>), hubContext));

In our previous article, we have injected an instance of IHubContext<LearningHub> into the constructor of HomeController class. So, we just need to create another implementation of the same type and inject it into the application to make sure that we overwrite the default implementation. But since the ServiceManagerBuilder class doesn’t allow us to create an instance of IHubContext with a specific override of the Hub class, mapping the implementation that gets returned from the CreateHubContextAsync method to the IHubContext<LearningHub> interface will no longer work. Therefore we need to replace all the instances of IHubContext<LearningHub> with the generic IHubContext<Hub>.

ServiceManagerBuilder class allows us to build a service manager object that are specific to the instance of Azure SignalR Service implementation that we have connected to via our connection string. Then, we can call CreateHubContextAsync method on the service manager object to create an implementation of IHubContext that will work with Azure SignalR. This method accepts the name of the hub and a cancellation token.

Finally, we can add this instance of IHubContext implementation to our dependency injection container. We do so by calling Add on Services property of builder object. The parameter of this method is an instance of ServiceDescriptor. The first parameter that we pass into it is the name of the type that we are implementing, which is IHubContext<Hub>. The second parameter is the actual implementation, which, in our case, is the instance of the class that we have created. And we do this call as the last step before the app object is built. This is to make sure that we overwrite any existing implementation.

We can also create an instance of a strongly-typed SignalR Hub from the ServiceManagerBuilder. To do so, we will replace the line that creates the hubContext variable with the following:

var hubContext = await serviceManager.CreateHubContextAsync<ILearningHubClient>("LearningHub", CancellationToken.None);

But if you choose to do this, the dependency injection logic needs to be modified accordingly. It might even be easier to inject ServiceManager class and then call CreateHubContextAsync method in the constructors of the classes that need to access the appropriate IHubContext implementation.

But HubContext is not the only way you can send messages to a SignalR hub hosted inside Azure SignalR Service from your server application. Azure SignalR Service also has an inbuilt REST API, which allows you to send messages to it by making standard HTTP requests. And this is what we will have a look at next.

Overview of Azure SignalR Service REST API

The advantage of using REST API to send messages to Azure SignalR Service is that you won’t need to install any additional NuGet packages. You can use any HTTP clients, including the inbuilt ones. The disadvantage of using REST API is that you will have to write additional code that will convert the objects that are natively used by the hub into JSON payload of HTTP requests. And you will also need to implement your own authentication logic.

An now we will go ahead and add some code for sending messages to the hub by using a standard HttpClient class. We will do so inside HomeController.cs file of SignalRServer2 project. And before we start, we will need to ensure that we have all of the following namespace references:

using Newtonsoft.Json;
using System.Net.Http.Headers;
using System.Text;

Then, we will add the following private field and the constructor, replacing <instance-name> with the actual instance name from Azure SignalR Service instance:

private readonly string signalRHubUrl;

public HomeController()
{
    signalRHubUrl = "https://<instance-name>.service.signalr.net/api/v1/hubs/learningHub";
}

Then, we can add the following code to the Index action method, where we replace <your JWT> with the actual JWT value we can obtain from Azure:

using var client = new HttpClient();

var payloadMessage = new
{
    Target = "ReceiveMessage",
    Arguments = new[]
    {
        "Client connected to a secondary web application"
    }
};

var request = new HttpRequestMessage(HttpMethod.Post, new UriBuilder(signalRHubUrl).Uri);
request.Headers.Add("Authorization", "Bearer <your JWT>");
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Content = new StringContent(JsonConvert.SerializeObject(payloadMessage), Encoding.UTF8, "application/json");

var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

if (!response.IsSuccessStatusCode)
    throw new Exception("Failure sending SignalR message.");

So, here is what we’ve done. Whenever somebody opens the home page of SignalRServer2 application in the browser, all SignalR clients that are connected to the same hub hosted by the same Azure SignalR Service instance will receive the following message:

Client connected to a secondary web application

This is because, when we are sending the request, we are choosing the generic URL type that will broadcast the message to all clients. We will be triggering `ReceiveMessage` event listener on our clients, as this is what we have specified as our `target` field in the payload. The parameters of this event listener are specified by `arguments` field.

But this is not the only thing that you can do with the REST API of Azure SignalR Service. Here is the full list of the endpoints you can use.

Full list of Azure SignalR Service REST API endpoints

Broadcast a message to all clients connected to target hub

POST /api/v1/hubs/{hub}

Broadcast a message to all clients belong to the target user

POST /api/v1/hubs/{hub}/users/{id}

Send message to the specific connection

POST /api/v1/hubs/{hub}/connections/{connectionId}

Check if the connection with the given connectionId exists

GET /api/v1/hubs/{hub}/connections/{connectionId}

Close the client connection

DELETE /api/v1/hubs/{hub}/connections/{connectionId}

Broadcast a message to all clients within the target group

POST /api/v1/hubs/{hub}/groups/{group}

Check if there are any client connections inside the given group

GET /api/v1/hubs/{hub}/groups/{group}

Check if there are any client connections connected for the given user

GET /api/v1/hubs/{hub}/users/{user}

Add a connection to the target group

PUT /api/v1/hubs/{hub}/groups/{group}/connections/{connectionId}

Remove a connection from the target group

DELETE /api/v1/hubs/{hub}/groups/{group}/connections/{connectionId}

Check whether a user exists in the target group

GET /api/v1/hubs/{hub}/groups/{group}/users/{user}

Add a user to the target group

PUT /api/v1/hubs/{hub}/groups/{group}/users/{user}

Remove a user from the target group

DELETE /api/v1/hubs/{hub}/groups/{group}/users/{user}

Remove a user from all groups

DELETE /api/v1/hubs/{hub}/users/{user}/groups

As you can see, there are more actions you can do via the REST API than you could via HubContext. For example, you can send messages to all connections that are associated with a specific users. Azure SignalR Service supports this out of the box without having to implement a custom dictionary or associating user-specific connections with user-specific groups. In order to use this feature, all you need to do is add nameid attribute to the payload of your JWT. And this will be your user id that you can then specify in the URL.

Authenticating into Azure SirnalR Service REST API

The REST API of Azure SignalR Service uses standard JWT as bearer token inside Authorization header. Both OpenID Connect and OAuth protocols can be applied in the context of this REST API. For specific application of these protocols in the context of Azure SignalR Service, you can visit Azure SignalR Service authentication link in the further reading section.

There are two important points to remember when applying JWT to Azure SignalR Service REST API endpoints:

  1. The payload needs to have aud (audience) field and its value should match the URL that you are sending the request to.
  2. The payload needs to have exp (expiry) field and it should contain epoch time of token’s expiry.

And this concludes the article on Azure SignalR Service. Let’s summarize what we’ve learned.

Summary

In this article, you have learned that you can scale your SignalR hub via Azure SignalR Service. To do so, you won’t have to make any code changes to the existing hubs. You just need to add an additional NuGet package to your server application and apply additional method call to your middleware setup.

You have also learned that the standard HubContext doesn’t work with Azure SignalR Service out of the box. If you use injectable IHubContext interface, you need to map it to a specific implementation from Azure SignalR Service Management NuGet package to make it compatible with Azure SignalR Service.

But now you also know that you don’t have to use HubContext to send messages to Azure SignalR Service from your server. The service supports a standard REST API, which you can use without any additional NuGet packages.

And this concludes the series of articles on applying SignalR library in the context of .NET 6.

Further reading

What is Azure SignalR Service?: https://docs.microsoft.com/en-us/azure/azure-signalr/signalr-overview

Scale ASP.NET Core SignalR applications with Azure SignalR Service: https://docs.microsoft.com/en-us/azure/azure-signalr/signalr-concept-scale-aspnet-core

Build real-time Apps with Azure Functions and Azure SignalR Service: https://docs.microsoft.com/en-us/azure/azure-signalr/signalr-concept-azure-functions

REST API in Azure SignalR Service: https://github.com/Azure/azure-signalr/blob/dev/docs/rest-api.md

Azure SignalR Service REST API quickstart: https://docs.microsoft.com/en-us/azure/azure-signalr/signalr-quickstart-rest-api

Azure SignalR Service Management SDK: https://github.com/Azure/azure-signalr/blob/dev/docs/management-sdk-guide.md

Safe storage of app secrets in development in ASP.NET Core: https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets

Azure SignalR Service authentication: https://docs.microsoft.com/en-us/azure/azure-signalr/signalr-concept-authenticate-oauth

Wrapping up

This series of articles has provided a complete guidance on how to use SignalR. After reading all the articles, you should be fully equipped to apply the library in the context of your own problem domain.

If there is anything that you would like to be covered in more depth on my blog, this is how you can get in touch with me either via Twitter or LinkedIn:

Twitter: https://twitter.com/FSazanavets

LinkedIn: https://www.linkedin.com/in/fiodar-sazanavets/

I hope that you found this series of articles useful and I am looking forward to hearing from you if you have any questions of feedback.

Yours truly

Fiodar Sazanavets


P.S. This article is a chapter from the book SignalR on .NET 6 — the complete guide.