Deeper dive into Azure SignalR Service
In the enterprise world, SignalR applications often come with high volume data flows and a large number of concurrent connections between the app and client. To handle that scenario, we have to set up the web farms with sticky sessions and a backplane like Redis to make sure messages are distributed to the right client. If we use Azure SignalR service, it will handle all those issues and we can focus only on business logic.
In addition to that, Azure SignalR Service works well with existing Asp.net Core SignalR Hub with fewer changes. We have to add a reference to Azure SignalR SDK and configure the Azure connection string in the application and then adding few lines of code
services.AddSignalR().AddAzureSignalR() and
app.UseAzureSignalR in Startup.cs.
Existing Asp.net Core SignalR client app works with Azure SignalR Service without any modification in the code. You can refer to my early article about “
How to build real time communication with cross platform devices using Azure SignalR Service” for more details.
As of today, if you want to implement duplex communication between the SignalR client and server using Azure SignalR Service, you must need ASP.net Core SignalR Server Hub(Web App). However, if you just want to push the messages from the server to clients (oneway), you can use Azure SignalR Service without having Asp.net Core SignalR Hub (Web App).
In the diagram above, we have two endpoints called Server Endpoint and Client End Point. With those End Points, SignalR Server and Client can connect to Azure SignalR Service without the need of Asp.net Core Web App.
Azure SignalR Service exposes a set of REST APIs to send messages to all clients from anywhere using any programming language or any REST client such as Postman. The Server REST API swagger documentation is in the following link.
https://github.com/Azure/azure-signalr/blob/dev/docs/swagger.json
Server Endpoint
REST APIs are only exposed on port 5002. In each HTTP request, an authorization header with a JSON Web Token (JWT) is required to authenticate with Azure SignalR Service. You should use the AccessKey in Azure SignalR Service instance's connection string to sign the generated JWT token.
Rest API URL
POST https://<service_endpoint>:5002/api/v1-preview/hub/<hub_name>
The body of the request is a JSON object with two properties:
Target - The target method you want to call in clients.
Arguments - an array of arguments you want to send to clients.
The API service authenticates REST call using JWT token, when you are generating the JWT token, use the access key in SignalR service connection string as a Secret Key and put it in the authentication header.
Client Endpoint
https://<service_endpoint>:5001/client/?hub=<hubName>
Clients also connect to Azure SignalR service using JWT token the same way as described above and each client will use some unique user id and the Client Endpoint URL to generate the token.
With all the details above, let us build a simple .Net Core Console app to broadcast messages using Azure SignalR Service Architecture. In this demo, we will see how the SignalR Console App server connects to Azure SignalR Service with REST API call to broadcast the messages to all connected console app clients in real time.
Steps
Creating Projects
We will be creating the following three projects.
- AzureSignalRConsoleApp.Server - .Net Core Console App
- AzureSignalRConsoleApp.Client - .Net Core Console App
- AzureSignalRConsoleApp.Utils - .Net Core Class Library
AzureSignalRConsoleApp.Utils
This class library holds the logic to generate the JWT token based on the access key from the Azure Connection string. It also holds the method to parse the Azure SignalR Connection String to get the endpoint and access key.
Nuget Packages Required
System.IdentityModel.Tokens.Jwt
- namespace AzureSignalRConsoleApp.Utils
- {
- public class ServiceUtils
- {
- private static readonly JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler();
-
- public string Endpoint { get; }
-
- public string AccessKey { get; }
-
- public ServiceUtils(string connectionString)
- {
- (Endpoint, AccessKey) = ParseConnectionString(connectionString);
- }
-
- public string GenerateAccessToken(string audience, string userId, TimeSpan? lifetime = null)
- {
- IEnumerable<Claim> claims = null;
- if (userId != null)
- {
- claims = new[]
- {
- new Claim(ClaimTypes.NameIdentifier, userId)
- };
- }
-
- return GenerateAccessTokenInternal(audience, claims, lifetime ?? TimeSpan.FromHours(1));
- }
-
- public string GenerateAccessTokenInternal(string audience, IEnumerable<Claim> claims, TimeSpan lifetime)
- {
- var expire = DateTime.UtcNow.Add(lifetime);
-
- var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(AccessKey));
- var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
-
- var token = JwtTokenHandler.CreateJwtSecurityToken(
- issuer: null,
- audience: audience,
- subject: claims == null ? null : new ClaimsIdentity(claims),
- expires: expire,
- signingCredentials: credentials);
- return JwtTokenHandler.WriteToken(token);
- }
-
- private static readonly char[] PropertySeparator = { ';' };
- private static readonly char[] KeyValueSeparator = { '=' };
- private const string EndpointProperty = "endpoint";
- private const string AccessKeyProperty = "accesskey";
-
- internal static (string, string) ParseConnectionString(string connectionString)
- {
- var properties = connectionString.Split(PropertySeparator, StringSplitOptions.RemoveEmptyEntries);
- if (properties.Length > 1)
- {
- var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- foreach (var property in properties)
- {
- var kvp = property.Split(KeyValueSeparator, 2);
- if (kvp.Length != 2) continue;
-
- var key = kvp[0].Trim();
- if (dict.ContainsKey(key))
- {
- throw new ArgumentException($"Duplicate properties found in connection string: {key}.");
- }
-
- dict.Add(key, kvp[1].Trim());
- }
-
- if (dict.ContainsKey(EndpointProperty) && dict.ContainsKey(AccessKeyProperty))
- {
- return (dict[EndpointProperty].TrimEnd('/'), dict[AccessKeyProperty]);
- }
- }
-
- throw new ArgumentException($"Connection string missing required properties {EndpointProperty} and {AccessKeyProperty}.");
- }
- }
- }
AzureSignalRConsoleApp.Server
This is the .net core SignalR Server console app to broadcast the messages via REST API call.
Nuget Packages Required
Microsoft.Extensions.Configuration.UserSecrets
Steps
Login to Azure Portal and get the Azure SignalR Service Connection String and store it in the UserSecrets.json.
Visual Studio does not provide the built-in support to manage User Secrets for .Net Core Console App. We have to manually create UserSecretsID element under PropertyGroup in the .csproj file and put the randomly generated GUID as below.