In this post, 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
The tools used to develop these components are Visual Studio for Mac/VS Code/VS 2017, AKS Dashboard, Docker for Desktop and kubectl.
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.
The main pointers about component diagram displayed below are
The steps needed to enable ASP.NET Core SignalR in ASP.NET Core 2.1 Web API are
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);
}
}
services.AddSignalR(options =>
{
options.EnableDetailedErrors = true;
});
IApplicationBuilder
request execution pipeline. The SignalRHub variable value I have specified in sample is /api/SignalRHub
. app.UseSignalR((options) =>
{
options.MapHub<NotificationHub>(SignalRHub);
});
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;
},
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.
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
services.AddSignalR(options =>
{
options.EnableDetailedErrors = true;
}).AddRedis(RedisConnectionString);
"RedisConnectionString": "REDIS_CONNECTION_STRING"
in appsettings.jsonAzure 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
services.AddSignalR(options =>
{
options.EnableDetailedErrors = true;
}).AddAzureSignalR(configure =>
{
configure.ConnectionString = AzureSignalRConnectionString;
});
app.UseAzureSignalR((options) =>
{
options.MapHub<NotificationHub>(SignalRHub);
})
"AzureSignalRConnectionString": "AZURE_SIGNALR_CONNECTION_STRING"
in appsettings.jsonAngular App sample is updated to publish/subscribe to SignalR Hub. The main pointers about code snippet listed below are
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'));
}
}
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
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<string>();
constructor(private http: HttpClient, private config: Config) {
}
private getConnectionInfo(): Observable<SignalRConnectionInfo> {
const requestUrl = `${this.config.get().FUNCTION_APP_URL}negotiate`;
return this.http.post<SignalRConnectionInfo>(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 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.
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.
Originally published on blogs.msdn.microsoft.com
Hope this post will surely help and you! Thanks for reading, Please share if you liked it!
#angular #asp-net #docker #kubernetes #azure