Add real-time web functionality to Angular application using ASP.NET Core SignalR, Azure SignalR service and Azure SignalR Service bindings for Azure Functions 2.0

This is the next part of the series on developing and deploying

  • Angular, ASP.NET Core Web API and SQL Server to Azure Kubernetes Service
  • Function Apps using Azure Functions 2.0 runtime

In this article I am going to go through steps needed to add real-time web functionality to Angular App using ASP.NET Core SignalR and Azure SignalR Service bindings for Azure Functions 2.0. The specific topics which this article is going to cover are

  • Add ASP.NET Core SignalR to ASP.NET Core 2.1 Web API
    • ASP.NET Core SignalR
    • ASP.NET Core SignalR scale out using
      • Azure SignalR Service backplane
      • Redis Cache backplane
  • Publish/Subscribe messages to SignalR Hub from Angular App
  • Publish/Subscribe messages to SignalR Hub using Azure SignalR Service bindings for Azure Functions 2.0 from Angular App
  • Build Docker images and deploy to Azure Kubernetes Service

The previous articles of this series are

https://blogs.msdn.microsoft.com/atverma/2018/09/16/azure-kubernetes-service-aks-deploying-angular-asp-net-core-and-sql-server-on-linux/

https://blogs.msdn.microsoft.com/atverma/2018/10/19/asp-net-core-2-1-web-api-load-app-configuration-from-appsettings-json-dockerfile-environment-variables-azure-key-vault-secrets-and-kubernetes-configmaps-secrets/

https://blogs.msdn.microsoft.com/atverma/2018/09/26/azure-functions-2-0-create-debug-and-deploy-to-azure-kubernetes-service-aks/

https://blogs.msdn.microsoft.com/atverma/2018/10/29/azure-functions-2-0-create-function-app-from-docker-image-functions-triggered-by-cosmos-db-blob-storage-event-hub-and-signalr-service-bindings

The tools used to develop these components are Visual Studio for Mac/VS Code/VS 2017, AKS Dashboard, Docker for Desktop and kubectl. 

Add ASP.NET Core SignalR to ASP.NET Core 2.1 Web API

ASP.NET Core SignalR is an open-source library that simplifies adding real-time web functionality to apps. Real-time web functionality enables server-side code to push content to clients instantly. The next sections are going to go through changes needed to add SignalR to ASP.NET Core 2.1 Web API along with SignalR scale out using Azure SignalR Service and Redis backplanes.

ASP.NET Core SignalR

The main pointers about component diagram displayed below are

  • On navigating to Angular App (Browser), user will be authenticated and Access Token will be retrieved from Azure AD using MSAL
  • Angular App will pass the bearer token (JWT) in requests and ASP.NET Core 2.1 Web API will validate the Access Token
  • Angular App will establish connection to ASP.NET Core SignalR Hub (Web API) and publish/subscribe Messages
  • Angular App will establish connection to Azure SignalR Service using Function App endpoint and publish/subscribe Messages
  • Angular App, ASP.NET Core Web API and SQL Server are hosted in Azure Kubernetes Service. I have color coded the Pods assuming two Node configuration.

 

The steps needed to enable ASP.NET Core SignalR in ASP.NET Core 2.1 Web API are

  • Configure SignalR Hub:  The SignalR Hubs API enables you to call methods on connected clients from the server. In the server code, you define methods that are called by client. In the client code, you define methods that are called from the server. SignalR takes care of everything behind the scenes that makes real-time client-to-server and server-to-client communications possible. Add INotificationHub interface andNotificationHub  class which are needed to define methods that are called by client.

    public interface INotificationHub    {        Task MessageNotification(string message);        Task PublishMessageAck(string value);    }     public class NotificationHub : Hub<INotificationHub>    {        public async Task PublishMessage(string message)        {            await this.Clients.AllExcept(this.Context.ConnectionId).MessageNotification($"Broadcast: {message}");            await this.Clients.Caller.PublishMessageAck($"Broadcast Ack: {message}");        }        public override async Task OnConnectedAsync()        {            await Groups.AddToGroupAsync(Context.ConnectionId, "Users");            await base.OnConnectedAsync();        }        public override async Task OnDisconnectedAsync(Exception exception)        {            await Groups.RemoveFromGroupAsync(Context.ConnectionId, "Users");            await base.OnDisconnectedAsync(exception);        }    }

  • Specify SignalRConfigMode SingalRConfigMode = SignalRConfigMode.ASPNetCoreSignalR in Startup.cs class
  • Update ConfigureServices method and add SignalR service.

           services.AddSignalR(options =>           {                options.EnableDetailedErrors = true;           });

    • Update Configure method and add SignalR to IApplicationBuilder request execution pipeline. The SignalRHub variable value I have specified in sample is  /api/SignalRHub.

          app.UseSignalR((options) =>           {               options.MapHub<NotificationHub>(SignalRHub);           });

  • Allow the JWT authentication handler to read the access token from the query string when a WebSocket or server sent events request comes in Service.AddAuthentication(...).AddJwtBearer(...) method.

          OnMessageReceived = context =>         {              var accessToken = context.Request.Query["access_token"];              var path = context.HttpContext.Request.Path;              if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments(SignalRHub)))              {                  context.Token = accessToken;              }              return Task.CompletedTask;          },

  • Claims of the authorized user can be accessed by calling Context.User.Claims in NotificationHub class.

In a single node/server scenario this works fine however when ASP.NET Core 2.1 Web API is scaled out e.g. when running more than one Pods, clients connected to SignalR Hub on one Pod won't receive message published on SignalR Hub on another Pod. ASP.NET Core SignalR supports Redis Cache and Azure SignalR Service scale out.

ASP.NET Core SignalR scale out using Redis Cache backplane

Redis cache can be used as backplane for ASP.NET Core SignalR scale out. Redis cache is used as Pub/Sub to forward messages to other servers. This option needs sticky sessions to be enabled. The only change in component diagram from previous implementation is that Web API Pods are communicating with Redis Cache service.

The code changes needed to use Redis backplane are

  • Install Microsoft.AspNetCore.SignalR.Redis Nuget package

  • Specify SignalRConfigMode SingalRConfigMode = SignalRConfigMode.Redis in Startup.cs class

  • Update ConfigureServices method and add AddRedis

            services.AddSignalR(options =>            {               options.EnableDetailedErrors = true;            }).AddRedis(RedisConnectionString);

  • Specify value of "RedisConnectionString": "REDIS_CONNECTION_STRING" in appsettings.json

ASP.NET Core SignalR scale out using Azure SignalR Service backplane

Azure SignalR Service can be used as backplane for ASP.NET Core SignalR scale out. This option doesn't need sticky sessions to be enabled.One of the key reasons to use the Azure SignalR Service is simplicity. With Azure SignalR Service, you don't need to handle problems like performance, scalability, availability. The only change in component diagram from previous implementation is Web API Pods are communicating with Azure SignalR Service.

The code changes needed to use Azure SignalR service are

  • Install Microsoft.Azure.SignalR Nuget package
  • Specify SignalRConfigMode SingalRConfigMode = SignalRConfigMode.AzureSignalRService in Startup.cs class
  • Update ConfigureServices method and add AddAzureSignalR

           services.AddSignalR(options =>           {                options.EnableDetailedErrors = true;           }).AddAzureSignalR(configure =>           {                configure.ConnectionString = AzureSignalRConnectionString;           });

  • Update Configure method and add UseAzureSignalR

          app.UseAzureSignalR((options) =>           {               options.MapHub<NotificationHub>(SignalRHub);           })

  • Specify value of "AzureSignalRConnectionString": "AZURE_SIGNALR_CONNECTION_STRING" in appsettings.json

Publish/Subscribe messages to SignalR Hub from Angular App

Angular App sample is updated to publish/subscribe to SignalR Hub. The main pointers about code snippet listed below are

  • Install aspnet/signalr which provides support for JavaScript and TypeScript clients for SignalR for ASP.NET Core
  • SignalR Hub URL is specified in ${this.config.get().API_URL}${this.config.get().SIGNALR_HUB}
  • Access Token is needed to authorize requests to SignalR Hub and is specified in { accessTokenFactory: () => this.authHelper.getAccessTokenFromCache() }
  • Retry connection interval is 10000
  • App subscribes to MessageNotification and PublishMessageAck messages of SignalR Hub
  • App sends messages to SignalR Hub using PublishMessage
  • Specify 'API_URL': 'WEB_API_URL' in 'environment.ts'

import { EventEmitter, Injectable, OnDestroy, OnInit } from '@angular/core';import { HubConnection, HubConnectionBuilder } from '@aspnet/signalr';import * as signalR from '@aspnet/signalr';import { AuthHelperService, AccessTokenInfo } from './auth-helper.service';import { Config } from './config/config';@Injectable({  providedIn: 'root',})export class SignalRService {  messageReceived = new EventEmitter<string>();  hubConnection: HubConnection;  constructor(private authHelper: AuthHelperService, private config: Config) {  }  init() {    this.createConnection();    this.startConnection();  }  private createConnection() {    this.hubConnection = new HubConnectionBuilder()      .withUrl(`${this.config.get().API_URL}${this.config.get().SIGNALR_HUB}`,        { accessTokenFactory: () => this.authHelper.getAccessTokenFromCache() })      .configureLogging(signalR.LogLevel.Information)      .build();    this.hubConnection.onclose(err => {      console.log('SignalR hub connection closed.');      this.stopHubAndunSubscribeToServerEvents();      this.restartConnection(err);    });  }  private restartConnection(err: Error): void {    console.log(`Error ${err}`);    console.log('Retrying connection to SignalR Hub ...');    setTimeout(() => {      this.startConnection();    }, 10000);  }  private startConnection(): void {    this.hubConnection      .start()      .then(() => {        console.log('SignalR Hub connection started');        this.subscribeToServerEvents();      })      .catch(err => {        this.restartConnection(err);      });  }  public publishMessage(message: string) {    this.hubConnection.invoke('PublishMessage', message);  }  private subscribeToServerEvents(): void {    this.hubConnection.on('MessageNotification', (data: any) => {      this.messageReceived.emit('MessageNotification:' + data);    });    this.hubConnection.on('PublishMessageAck', (data: any) => {      this.messageReceived.emit('MessageNotification - Ack :' + data);    });  }  private stopHubAndunSubscribeToServerEvents(): void {    this.hubConnection.off('MessageNotification');    this.hubConnection.off('PublishMessageAck');    this.hubConnection.stop().then(() => console.log('Hub connection stopped'));  }}

Publish/Subscribe messages to SignalR Hub using Azure SignalR Service bindings for Azure Functions 2.0 from Angular App

The previous blog of this series describes steps needed to develop a Function App with Azure SignalR Service bindings for Azure Functions 2.0. Angular App sample is updated to publish/subscribe to Azure SignalR Service Hub through Azure Functions (you don't need to host a web application at all). The main pointers about code snippet listed below are

  • Install aspnet/signalr which provides support for JavaScript and TypeScript clients for SignalR for ASP.NET Core
  • ${this.config.get().FUNCTION_APP_URL}negotiate post request will return SignalRConnectionInfo object having url and accessToken.
  • SignalRConnectionInfo is needed to connect to Azure SignalR Service Hub by specifying accessTokenFactory: () => info.accessToken and url in withUrl(url, options)
  • Retry connection interval is 10000
  • App sends messages to SignalR Hub by sending a post request to ${this.config.get().FUNCTION_APP_URL}sendmessage
  • App subscribes to SendMessage i.e. this.hubConnection.on('sendMessage', (data: any) of SignalR Hub
  • Specify 'FUNCTION_APP_URL': 'FUNCTION_APP_URLFUNCTION_APP_URL' in environment.ts

`` import { Injectable, EventEmitter, OnDestroy } from '@angular/core';import { HttpClient, HttpErrorResponse } from '@angular/common/http';import { HubConnection, IHttpConnectionOptions } from '@aspnet/signalr';import * as signalR from '@aspnet/signalr';import { Observable, throwError } from 'rxjs';import { SignalRConnectionInfo } from './signalRConnectionInfo';import { Config } from './config/config';@Injectable({    providedIn: 'root'})export class SignalRFuncService {    hubConnection: HubConnection;    messageReceived = new EventEmitter();    constructor(private http: HttpClient, private config: Config) {    }    private getConnectionInfo(): Observable {        const requestUrl = ${this.config.get().FUNCTION_APP_URL}negotiate;        return this.http.post(requestUrl, null);    }    init() {        this.getConnectionInfo().subscribe((info: SignalRConnectionInfo) => {            const options = {                accessTokenFactory: () => info.accessToken            };            this.createConnection(info.url, options);            this.startConnection();        },        error => {            console.error(An error occurred during init: ${error});            console.log('Retrying connection to Azure Function - SignalR Hub ...');            setTimeout(() => {                this.init();            }, 10000);        });    }    private createConnection(url: string, options: IHttpConnectionOptions) {        this.hubConnection = new signalR.HubConnectionBuilder()            .withUrl(url, options)            .configureLogging(signalR.LogLevel.Information)            .build();        this.hubConnection.onclose(err => {            console.log('Azure Function - SignalR Hub connection closed.');            this.stopHubAndunSubscribeToServerEvents();            this.restartConnection(err);        });    }    private startConnection(): void {        this.hubConnection            .start()            .then(() => {                console.log('Azure Function - SignalR Hub connection started.');                this.subscribeToServerEvents();            })            .catch(err => {                this.restartConnection(err);            });    }    private restartConnection(err: Error): void {        console.log(Error ${err});        console.log('Retrying connection to Azure Function - SignalR Hub ...');        setTimeout(() => {            this.startConnection();        }, 10000);    }    send(message: string) {        const requestUrl = ${this.config.get().FUNCTION_APP_URL}sendmessage;        this.http.post(requestUrl, message).subscribe(            (data: any) => console.log(Func Hub sendMessage: ${message}),            error => console.error(An error occurred in sendMessage: ${error})        );    }    private subscribeToServerEvents(): void {        this.hubConnection.on('sendMessage', (data: any) => {            this.messageReceived.emit('MessageNotification - Function: ' + data);        });    }    private stopHubAndunSubscribeToServerEvents(): void {        this.hubConnection.off('sendMessage');        this.hubConnection.stop().then(() => console.log('Hub connection stopped'));    }} export class SignalRConnectionInfo {    url: string;    accessToken: string;}

Build Docker Images and Deploy to Azure Kubernetes Service

Build docker images for ASP.NET Core Web API and Angular App and deploy these docker images to Azure Kubernetes Service cluster. After deployment, browse to Angular App and send message either using ASP.NET Core SignalR or Azure Functions 2.0 endpoint.

Summary

This completes the article on add real-time web functionality to Angular App using ASP.NET Core SignalR and Azure SignalR Service bindings for Azure Functions 2.0. The complete source code can be downloaded from GitHub-AzureFunctions and GitHub-AKS.