facebook

Blog

Resta aggiornato

Vediamo come inviare informazioni dal server al client utilizzando ASP.NET Core, SignalR e Angular
Applicazioni real-time con SignalR, ASP.NET Core e Angular
martedì 09 Luglio 2019

Supponiamo di voler creare un’applicazione web di monitoring, che fornisca all’utente una dashboard, in grado di visualizzare una serie di informazioni che si aggiornano nel tempo.

Un primo approccio è chiamare una API ciclicamente a determinati intervalli di tempo (polling) per aggiornare i dati sulla dashboard. C’è un problema però: se non ci sono dati aggiornati stiamo inutilmente aumentando il traffico di rete con le nostre richieste. Un’alternativa è costituita dalla tecnica di long-polling: se il server non ha dati disponibili, invece di spedire una response vuota, può tenere viva la richiesta finché non succeda qualcosa o si raggiunga un tempo prefissato di timeout. Se ci sono nuovi dati, la response completa giunge al client. Un approccio completamente diverso è quello di invertire i ruoli: è il backend a contattare i client quando i nuovi dati sono disponibili (push).

Ricordiamo che HTML 5 ha standardizzato WebSocket, ossia una connessione permanente bidirezionale configurabile tramite un’interfaccia Javascript nei browser compatibili. Purtroppo, deve esserci pieno supporto a WebSocket, sia lato client (browsers) che lato server, per renderlo disponibile. Dobbiamo, quindi, prevedere meccanismi alternativi (fallback) che consentano alla nostra applicazione di funzionare sempre e comunque.

Microsoft ha pubblicato nel 2013 una libreria open source chiamata SignalR per ASP.NET che nel 2018 è stata riscritta per ASP.NET Core. SignalR astrae tutti i dettagli relativi ai meccanismi di comunicazione scegliendo il migliore tra quelli disponibili. Il risultato è quello di poter scrivere codice come se ci trovassimo sempre in modalità push. Con SignalR il server può chiamare un metodo JavaScript su tutti i suoi client connessi, oppure su uno specifico.

Creiamo un progetto ASP.NET Core con template web-api, eliminando il controller di esempio che viene generato. Mediante NuGet, aggiungiamo Microsoft.AspNet.SignalR al progetto, in modo da poter creare un Hub. L’hub è la pipeline ad alto livello, in grado di chiamare codice client inviando messaggi che contengono il nome e i parametri del metodo richiesto. Gli oggetti inviati come parametri vengono de-serializzati usando il protocollo opportuno. Il client cerca nel codice della pagina un metodo corrispondente al nome e, se lo trova, lo invoca passando come parametri i dati de-serializzati.

using Microsoft.AspNetCore.SignalR;
 
namespace SignalR.Hubs
{
    public class NotificationHub : Hub { }
}

Come probabilmente sapete, in ASP.NET Core la pipeline di gestione di una richiesta HTTP è configurabile aggiungendo dei middleware, che vanno a intercettare una richiesta, aggiungono la funzionalità configurata e la fanno proseguire al middleware successivo. Il middleware di SignalR deve essere preventivamente configurato aggiungendo nel metodo ConfigureServices della classe Startup, l’extension method services.AddSignalR(). A questo punto, possiamo aggiungere il middleware alla pipeline, utilizzando l’extension method app.UseSignalR() nel metodo Configure della classe Startup. Durante questa operazione, possiamo passare dei parametri di configurazione, tra cui la rotta del nostro hub:

app.UseSignalR(route =>
{
    route.MapHub<notificationhub>("/notificationHub");
})

Uno scenario interessante, che ci permette di vedere anche un’altra feature di ASP.NET Core, è l’hosting di un Hub SignalR nel contesto di un background worker process.

Immaginiamo di voler implementare il seguente caso d’uso:

  • eseguire della logica business
  • aspettare del tempo
  • decidere se fermarsi o ripetere il processo.

In ASP.NET Core, possiamo usare l’interfaccia IHostedService, messa a disposizione dal framework, per implementare l’esecuzione di processi in background in una applicazione .NET Core. I metodi da implementare sono StartAsync() e StopAsync(). Molto semplicemente, StartAsync è invocato allo startup dell’host, mentre StopAsync al suo shutdown.

Aggiungiamo. quindi, al progetto una classe DashboardHostedService, che implementi IHostedService. Aggiungiamo nel metodo ConfigureServices della classe Startup la registrazione dell’interfaccia

services.AddHostedService<dashboardhostedservice>();

Nel costruttore della classe DashboardHostedService, iniettiamo IHubContext per accedere all’hub aggiunto alla nostra applicazione.

Nel metodo StartAsync, poi, impostiamo un timer, che ogni due secondi eseguirà il codice contenuto nel metodo DoWork(). Tale metodo invia un messaggio contenente quattro stringhe, generate in maniera casuale.

Ma a chi le trasmette? Nell’esempio, stiamo inviando il messaggio a tutti i client connessi. Tuttavia, SignalR offre la possibilità di mandare messaggi a singoli utenti o a gruppi di utenti. In quest’ articolo trovate i dettagli che coinvolgono le funzionalità di autenticazione e autorizzazione in Asp.NET Core. È interessante notare che un utente potrebbe essere collegato sia da un desktop, che da un telefono. Ciascun device avrebbe una connessione SignalR distinta, ma tutti e due sarebbero associati allo stesso utente.

using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;
using SignalR.Hubs;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
 
namespace SignalR
{
    public class DashboardHostedService: IHostedService
    {
        private Timer _timer;
        private readonly IHubContext<notificationhub> _hubContext;
 
        public DashboardHostedService(IHubContext<notificationhub> hubContext)
        {
            _hubContext = hubContext;
        }
 
        public Task StartAsync(CancellationToken cancellationToken)
        {
            _timer = new Timer(DoWork, null, TimeSpan.Zero,
            TimeSpan.FromSeconds(2));
 
            return Task.CompletedTask;
        }
 
        private void DoWork(object state)
        {
            _hubContext.Clients.All.SendAsync("SendMessage", 
                new {
                    val1 = getRandomString(),
                    val2 = getRandomString(),
                    val3 = getRandomString(),
                    val4 = getRandomString()
                });
        }
 
        public Task StopAsync(CancellationToken cancellationToken)
        {
            _timer?.Change(Timeout.Infinite, 0);
 
            return Task.CompletedTask;
        }
    }
}

Vediamo come gestire la parte client. Creiamo, ad esempio, un’applicazione Angular col comando ng new SignalR della Angular CLI. Installiamo il pacchetto node per SignalR (npm i @aspnet/signalr). Aggiungiamo un service, che ci permetterà di collegarci all’hub creato in precedenza e di ricevere i messaggi.

Ecco un primo possibile approccio, basato sulla restituzione, da parte del service nel metodo getMessage(), di un Observable<Message>, mediante l’uso di un Subject<Message> dichiarato privatamente (Message è una interface typescript, corrispondente all’oggetto restituito dal backend):

@Injectable({
 providedIn: 'root'
})
export class SignalRService {
 private message$: Subject<message>;
 private connection: signalR.HubConnection;
 
 constructor() {
   this.message$ = new Subject<message>();
   this.connection = new signalR.HubConnectionBuilder()
   .withUrl(environment.hubUrl)
   .build();
   this.connect();
 }
 private connect() {
   this.connection.start().catch(err => console.log(err));
   this.connection.on('SendMessage', (message) => {
     this.message$.next(message);
   });
 }
 public getMessage(): Observable<message> {
   return this.message$.asObservable();
 }
 public disconnect() {
   this.connection.stop();
 }
}

All’interno del constructor(), creiamo un oggetto di tipo signalR.HubConnection, che servirà per connetterci al server. Ad esso, mediante il file environment.ts, passiamo la URL del nostro hub:

this.connection = new signalR.HubConnectionBuilder()
   .withUrl(environment.hubUrl)
   .build();

Il constructor si occupa anche di invocare il metodo connect(), che effettua la connessione vera e propria, loggando eventuali errori in console.

this.connection.start().catch(err => console.log(err));
this.connection.on('SendMessage', (message) => {
  this.message$.next(message);
});

Un component che voglia mostrare i messaggi provenienti dal backend (dopo aver iniettato il servizio nel constructor), deve sottoscriversi al metodo getMessage() e gestire il messaggio arrivato. Nel caso di AppComponent, ad esempio:

@Component({
 selector: 'app-root',
 templateUrl: './app.component.html',
 styleUrls: ['./app.component.css']
})
export class AppComponent implements OnDestroy {
 private signalRSubscription: Subscription;
 
 public content: Message;
 
 constructor(private signalrService: SignalRService) {
   this.signalRSubscription = this.signalrService.getMessage().subscribe(
     (message) => {
       this.content = message;
   });
 }
 ngOnDestroy(): void {
   this.signalrService.disconnect();
   this.signalRSubscription.unsubscribe();
 }
}

L’utilizzo di un Subject <Message> consente di gestire, in più componenti contemporaneamente e indipendentemente, il Message restituito dall’hub (sia per il subscribe che per l’unsubscribe), ma bisogna evitare le insidie di un uso poco attento del Subject. Prendiamo in considerazione la seguente versione di getMessage():

public getMessage(): Observable<message> {
   return this.message$;
}

Ora il componente è in grado di emettere anch’esso un Message mediante questo semplice codice:

const produceMessage = this.signalrService.getMessage() as Subject<any>;
 produceMessage.next( {val1: 'a'});

Questo codice, invece, solleva un’eccezione se il metodo getMessage() restituisce il Subject<Message> asObservable!

Un secondo approccio, più semplice, che possiamo utilizzare nel caso un solo componente sia interessato a gestire i messaggi provenienti dal backend, è il seguente:

@Injectable({
 providedIn: 'root'
})
export class SignalrService {
 connection: signalR.HubConnection;
 
 constructor() {
   this.connection = new signalR.HubConnectionBuilder()
   .withUrl(environment.hubAddress)
   .build();
   this.connect();
 }
 
 public connect() {
   if (this.connection.state === signalR.HubConnectionState.Disconnected) {
     this.connection.start().catch(err => console.log(err));
   }
 }
 
 public getMessage(next) {
     this.connection.on('SendMessage', (message) => {
       next(message);
     });
 }
 
 public disconnect() {
   this.connection.stop();
 }
}

Al metodo getMessage, passiamo semplicemente la funzione di callback, che prenderà come argomento il message proveniente dal backend. In questo caso, AppComponent può diventare:

public content: IMessage;
constructor(private signalrService: SignalrService) {
   this.signalrService.getMessage(
     (message: IMessage) => {
       this.content = message;
     }
   );
}
ngOnDestroy(): void {
   this.signalrService.disconnect();
}

Ultime righe di codice, rispettivamente in app.component.html e app.component.css, per dare un minimo di stile, e l’applicazione è completata.

<div style="text-align:center">
  <h1>
    DASHBOARD
  </h1>
</div>
<div class="card-container">
  <div class="card">
    <div class="container">
      <h4><b>Valore 1</b></h4>
      <p>{{content.val1}}</p>
    </div>
  </div>
  <div class="card">
    <div class="container">
      <h4><b>Valore 2</b></h4>
      <p>{{content.val2}}</p>
    </div>
  </div>
  <div class="card">
    <div class="container">
      <h4><b>Valore 3</b></h4>
      <p>{{content.val3}}</p>
    </div>
  </div>
  <div class="card">
    <div class="container">
      <h4><b>Valore 4</b></h4>
      <p>{{content.val4}}</p>
    </div>
  </div>
</div>
 
.card-container {
  display: flex;
  flex-wrap: wrap;
}
 
.card {
  box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
  transition: 0.3s;
  width: 40%;
  flex-grow: 1;
  margin: 10px;
}
 
.card:hover {
  box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
}
 
.container {
  padding: 2px 16px;
}

Avviamo prima il backend e poi il frontend e controlliamo il risultato finale:

Sembra tutto ok! Trovate il codice qui: https://github.com/AARNOLD87/SignalRWithAngular.

Al prossimo articolo!

Scopri di più da Blexin

Abbonati ora per continuare a leggere e avere accesso all'archivio completo.

Continue reading