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

Real-time technologies are now part of every modern applications 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 application without worrying about capacity provisioning, scaling, or persistent connections. In this article, we are going to discuss about how to create .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 Azure SignalR Service

In the enterprise world, SignalR applications often comes with high volume data flows and large number of concurrent connections between app and client. To handle that scenario, we have to setup 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 very less changes. We have to add 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 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 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 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 exposed 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://:5002/api/v1-preview/hub/

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 authentication header.

Client Endpoint

https://:5001/client/?hub=

Clients also connect to Azure SignalR service using JWT token same 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 connect 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 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 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
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 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.

  • Open the command window from the root project location and run the following command to create secrets.json file with the configuration data

dotnet user-secrets set key value

  • 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.

  • After the above setup configured successfully, we can load the configuration object with the following code.
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddUserSecrets<program>()
.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.
    amespace AzureSignalRConsoleApp.Server
    {
    class Program
    {
    private static readonly HttpClient httpClient = new HttpClient();
    private static readonly string hubName = "ConsoleAppBroadcaster";
    private static readonly string serverName = "Azure_SignalR_Server_1";
    private static ServiceUtils serviceUtils;

    static void Main(string[] args)
    {
    //Loading the Configuration Objects from UserSecrets
    var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddUserSecrets<program>()
    .Build();

    serviceUtils = new ServiceUtils(configuration["Azure:SignalR:ConnectionString"]);

    Console.WriteLine(" Azure SignalR Server Started.\n " +
    "Start typing and press enter to broadcast messages to all the connected clients.\n " +
    "Type quit to shut down the server!");
    while (true)
    {
    var data = Console.ReadLine();
    if (data.ToLower() == "quit") break;
    Broadcast(data);
    }

    Console.WriteLine("SignalR Server is shutting down");
    }

    private static async void Broadcast(string message)
    {
    var url = $"{serviceUtils.Endpoint}:5002/api/v1-preview/hub/{hubName.ToLower()}";
    var request = new HttpRequestMessage(HttpMethod.Post, new UriBuilder(url).Uri);

    request.Headers.Authorization =
    new AuthenticationHeaderValue("Bearer", serviceUtils.GenerateAccessToken(url, serverName));
    request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    var messageContent = new MessageContent() { Target = "SendMessage", Arguments = new[] { serverName, message } };
    request.Content = new StringContent(JsonConvert.SerializeObject(messageContent), Encoding.UTF8, "application/json");

    var response = await httpClient.SendAsync(request);
    if (response.StatusCode != HttpStatusCode.Accepted)
    {
    Console.WriteLine($"Sent error: {response.StatusCode}");
    }
    }
    }

    public class MessageContent
    {
    public string Target { get; set; }

    public object[] Arguments { get; set; }
    }
    }

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.
    var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddUserSecrets<program>()
    .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.
    namespace AzureSignalRConsoleApp.Client
    {
    class Program
    {
    private static readonly string userId = $"User {new Random().Next(1, 99)}";
    private static ServiceUtils serviceUtils;
    private static readonly string hubName = "ConsoleAppBroadcaster";
    private static HubConnection hubConnection;

    async static Task Main(string[] args)
    {
    var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddUserSecrets<program>()
    .Build();

    serviceUtils = new ServiceUtils(configuration["Azure:SignalR:ConnectionString"]);

    var url = $"{serviceUtils.Endpoint}:5001/client/?hub={hubName}";

    hubConnection = new HubConnectionBuilder()
    .WithUrl(url, option =>
    {
    option.AccessTokenProvider = () =>
    {
    return Task.FromResult(serviceUtils.GenerateAccessToken(url, userId));
    };
    }).Build();

    hubConnection.On<string string="">("SendMessage",
    (string server, string message) =>
    {
    Console.WriteLine($"Message from server {server}: {message}");
    });

    await hubConnection.StartAsync();
    Console.WriteLine("Client started... Press any key to close the connection");
    Console.ReadLine();
    await hubConnection.DisposeAsync();
    Console.WriteLine("Client is shutting down...");
    }
    }
    }

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 window. After that, start typing in server command window to send messages to all the clients in real-time.

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 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.

Happy Coding!