Azure SignalR Messaging With .Net Core Console App Server And Client

Real-time technologies are now part of every modern application and SignalR is the most popular .net library to create real-time scenarios. Recently, Microsoft announced the public preview of Azure SignalR, a cloud-based fully managed service to build real-time applications without worrying about capacity provisioning, scaling, or persistent connections. In this article, we are going to discuss how to create a .NET Core SignalR Server console app to broadcast the messages to all the connected clients in real-time without using Asp.net Core SignalR Web App. 
  
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).
Azure SignalR Messaging with .Net Core Console App Server and Client
 
Azure SignalR Messaging with .Net Core Console App Server and Client 
 
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.  
 
Azure SignalR Messaging with .Net Core Console App Server and Client 
 
Steps
Creating Projects
 
We will be creating the following three projects. 
 
Azure SignalR Messaging with .Net Core Console App Server and Client 
  • 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  
  1. namespace AzureSignalRConsoleApp.Utils  
  2. {  
  3.     public class ServiceUtils  
  4.     {  
  5.         private static readonly JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler();  
  6.   
  7.         public string Endpoint { get; }  
  8.   
  9.         public string AccessKey { get; }  
  10.   
  11.         public ServiceUtils(string connectionString)  
  12.         {  
  13.             (Endpoint, AccessKey) = ParseConnectionString(connectionString);  
  14.         }  
  15.   
  16.         public string GenerateAccessToken(string audience, string userId, TimeSpan? lifetime = null)  
  17.         {  
  18.             IEnumerable<Claim> claims = null;  
  19.             if (userId != null)  
  20.             {  
  21.                 claims = new[]  
  22.                 {  
  23.                     new Claim(ClaimTypes.NameIdentifier, userId)  
  24.                 };  
  25.             }  
  26.   
  27.             return GenerateAccessTokenInternal(audience, claims, lifetime ?? TimeSpan.FromHours(1));  
  28.         }  
  29.   
  30.         public string GenerateAccessTokenInternal(string audience, IEnumerable<Claim> claims, TimeSpan lifetime)  
  31.         {  
  32.             var expire = DateTime.UtcNow.Add(lifetime);  
  33.   
  34.             var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(AccessKey));  
  35.             var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);  
  36.   
  37.             var token = JwtTokenHandler.CreateJwtSecurityToken(  
  38.                 issuer: null,  
  39.                 audience: audience,  
  40.                 subject: claims == null ? null : new ClaimsIdentity(claims),  
  41.                 expires: expire,  
  42.                 signingCredentials: credentials);  
  43.             return JwtTokenHandler.WriteToken(token);  
  44.         }  
  45.   
  46.         private static readonly char[] PropertySeparator = { ';' };  
  47.         private static readonly char[] KeyValueSeparator = { '=' };  
  48.         private const string EndpointProperty = "endpoint";  
  49.         private const string AccessKeyProperty = "accesskey";  
  50.   
  51.         internal static (stringstring) ParseConnectionString(string connectionString)  
  52.         {  
  53.             var properties = connectionString.Split(PropertySeparator, StringSplitOptions.RemoveEmptyEntries);  
  54.             if (properties.Length > 1)  
  55.             {  
  56.                 var dict = new Dictionary<stringstring>(StringComparer.OrdinalIgnoreCase);  
  57.                 foreach (var property in properties)  
  58.                 {  
  59.                     var kvp = property.Split(KeyValueSeparator, 2);  
  60.                     if (kvp.Length != 2) continue;  
  61.   
  62.                     var key = kvp[0].Trim();  
  63.                     if (dict.ContainsKey(key))  
  64.                     {  
  65.                         throw new ArgumentException($"Duplicate properties found in connection string: {key}.");  
  66.                     }  
  67.   
  68.                     dict.Add(key, kvp[1].Trim());  
  69.                 }  
  70.   
  71.                 if (dict.ContainsKey(EndpointProperty) && dict.ContainsKey(AccessKeyProperty))  
  72.                 {  
  73.                     return (dict[EndpointProperty].TrimEnd('/'), dict[AccessKeyProperty]);  
  74.                 }  
  75.             }  
  76.   
  77.             throw new ArgumentException($"Connection string missing required properties {EndpointProperty} and {AccessKeyProperty}.");  
  78.         }  
  79.     }  
  80. }  
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.

Azure SignalR Messaging with .Net Core Console App Server and Client

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.
 
Azure SignalR Messaging with .Net Core Console App Server and Client
 
Open the command window from the root project location and run the following command to create the secrets.json file with the configuration data,

dotnet user-secrets set key value

Azure SignalR Messaging with .Net Core Console App Server and Client 
 
Goto %APPDATA%\Microsoft\UserSecrets Folder to verify the new folder exists in the same GUID and the secrets.json file is created with configuration data.
Azure SignalR Messaging with .Net Core Console App Server and Client
 
After the above setup configured successfully, we can load the configuration object with the following code.
  1. var configuration = new ConfigurationBuilder()  
  2.                .SetBasePath(Directory.GetCurrentDirectory())  
  3.                .AddUserSecrets<program>()  
  4.                .Build();  
Broadcast method will take the input message from console window, build the httprequest along with generated JWT token in authorization header and make the REST API call to push the messages. 
  1. namespace AzureSignalRConsoleApp.Server  
  2. {  
  3.     class Program  
  4.     {  
  5.         private static readonly HttpClient httpClient = new HttpClient();  
  6.         private static readonly string hubName = "ConsoleAppBroadcaster";  
  7.         private static readonly string serverName = "Azure_SignalR_Server_1";  
  8.         private static ServiceUtils serviceUtils;  
  9.    
  10.         static void Main(string[] args)  
  11.         {  
  12.             //Loading the Configuration Objects from UserSecrets  
  13.             var configuration = new ConfigurationBuilder()  
  14.                .SetBasePath(Directory.GetCurrentDirectory())  
  15.                .AddUserSecrets<program>()  
  16.                .Build();  
  17.    
  18.             serviceUtils = new ServiceUtils(configuration["Azure:SignalR:ConnectionString"]);  
  19.    
  20.             Console.WriteLine(" Azure SignalR Server Started.\n " +  
  21.                               "Start typing and press enter to broadcast messages to all the connected clients.\n " +  
  22.                               "Type quit to shut down the server!");  
  23.             while (true)  
  24.             {  
  25.                 var data = Console.ReadLine();  
  26.                 if (data.ToLower() == "quit"break;  
  27.                 Broadcast(data);  
  28.             }  
  29.    
  30.             Console.WriteLine("SignalR Server is shutting down");  
  31.         }  
  32.    
  33.         private static async void Broadcast(string message)  
  34.         {  
  35.             var url = $"{serviceUtils.Endpoint}:5002/api/v1-preview/hub/{hubName.ToLower()}";  
  36.             var request = new HttpRequestMessage(HttpMethod.Post, new UriBuilder(url).Uri);  
  37.    
  38.             request.Headers.Authorization =  
  39.                 new AuthenticationHeaderValue("Bearer", serviceUtils.GenerateAccessToken(url, serverName));  
  40.             request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));  
  41.             var messageContent = new MessageContent() { Target = "SendMessage", Arguments = new[] { serverName, message } };  
  42.             request.Content = new StringContent(JsonConvert.SerializeObject(messageContent), Encoding.UTF8, "application/json");  
  43.    
  44.             var response = await httpClient.SendAsync(request);  
  45.             if (response.StatusCode != HttpStatusCode.Accepted)  
  46.             {  
  47.                 Console.WriteLine($"Sent error: {response.StatusCode}");  
  48.             }  
  49.         }  
  50.     }  
  51.    
  52.     public class MessageContent  
  53.     {  
  54.         public string Target { getset; }  
  55.    
  56.         public object[] Arguments { getset; }  
  57.     }  
  58. }  
AzureSignalRConsoleApp.Client
 
This is the .net core SignalR Client console app to receive the messages from Azure SignalR Service.

Nuget Packages Required
  • Microsoft.Extensions.Configuration.UserSecrets
  • Microsoft.AspNetCore.SignalR.Client
Steps 

In order to load the configuration object from User Secrets to load the Azure Connection String, we must follow the same steps as above. 
  1. var configuration = new ConfigurationBuilder()  
  2.                .SetBasePath(Directory.GetCurrentDirectory())  
  3.                .AddUserSecrets<program>()  
  4.                .Build();  
Generate the JWT access token using client endpoint URL and create hub connection with the client hub URL and the access token to establish the connection with Azure SignalR Service. In the Hub connection ON event, wire up with the same Target method to receive the message. 
  1. namespace AzureSignalRConsoleApp.Client  
  2. {  
  3.     class Program  
  4.     {  
  5.         private static readonly string userId = $"User {new Random().Next(1, 99)}";  
  6.         private static ServiceUtils serviceUtils;  
  7.         private static readonly string hubName = "ConsoleAppBroadcaster";  
  8.         private static HubConnection hubConnection;  
  9.    
  10.         async static Task Main(string[] args)  
  11.         {  
  12.             var configuration = new ConfigurationBuilder()  
  13.                 .SetBasePath(Directory.GetCurrentDirectory())  
  14.                 .AddUserSecrets<program>()  
  15.                 .Build();  
  16.    
  17.             serviceUtils = new ServiceUtils(configuration["Azure:SignalR:ConnectionString"]);  
  18.    
  19.             var url = $"{serviceUtils.Endpoint}:5001/client/?hub={hubName}";  
  20.    
  21.             hubConnection = new HubConnectionBuilder()  
  22.                 .WithUrl(url, option =>  
  23.                 {  
  24.                     option.AccessTokenProvider = () =>  
  25.                     {  
  26.                         return Task.FromResult(serviceUtils.GenerateAccessToken(url, userId));  
  27.                     };  
  28.                 }).Build();  
  29.    
  30.             hubConnection.On<string string="">("SendMessage",  
  31.                 (string server, string message) =>  
  32.                 {  
  33.                     Console.WriteLine($"Message from server {server}: {message}");  
  34.                 });  
  35.    
  36.             await hubConnection.StartAsync();  
  37.             Console.WriteLine("Client started... Press any key to close the connection");  
  38.             Console.ReadLine();  
  39.             await hubConnection.DisposeAsync();  
  40.             Console.WriteLine("Client is shutting down...");  
  41.         }  
  42.     }  
  43. }  
How it works
 
Now that we have completed the code, let us run the application to see the demo. First, launch the server and then launch more than one client app in multiple command windows. After that, start typing in server command window to send messages to all the clients in real-time.

 Azure SignalR Messaging with .Net Core Console App Server and Client

Conclusion
 
In this article, we discussed how to use Azure SignalR Service in .net core console app without using Asp.net Core Web App. In the real world, Azure SignalR Service can be integrated with other Azure services like serverless computing (Azure Functions) to push notification messages to all the connected clients in real time based on some trigger without hosting Asp.net Core Web App and managing the connection with clients. I have uploaded the entire source code in my GitHub repository.