SingalR is a library that helps us provide real-time web functionality to our applications. This means that our server can push data to any connected client as soon as that data is provided, in real-time, and vice versa.
In this article, we are going to show you how to use SignalR with .NET Core and Angular through a practical example.
We are going to simulate a real-time data flow by using the Timer
class in .NET Core and use that data to change the states of our Angular charts in real-time as well. For this example, we are going to use only one-way communication (from the server to the client), but we will add an additional feature to the example, to show the two-way communication as well (client-server-client).
If you want to watch a video on this topic, you can do that as well:
VIDEO: .NET Core with SignalR and Angular - Real-Time Charts video.
So, without further ado, let’s get started.
Creating Projects and Basic Configuration
First thing first.
Let’s create both the .NET Core and Angular projects. We are going to name them RealTimeCharts.Server
and RealTimeCharts.Client
respectively. For the .NET Core project, we are going to choose a Web API empty project and for the Angular side, we are creating an Angular project with no routings created and CSS for the styles. To learn more about .NET Core, you can read the .NET Core Web API Tutorial. For the detailed Angular development guide, you can read Angular Tutorial.
As soon as projects are created, we are going to switch to the server-side project and set up a basic configuration. To do that, let’s open the launchSettings.json
file and modify it accordingly:
{ "profiles": { "RealTimeCharts.Server": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } }
Our server-side project will run on localhost:5001
and the client side will run on localhost:4200
, so in order to establish communication between those two, we need to enable CORS. Let’s open the Program
class and modify it:
builder.Services.AddCors(options => { options.AddPolicy("CorsPolicy", builder => builder .WithOrigins("http://localhost:4200") .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials()); }); builder.Services.AddControllers(); var app = builder.Build(); // Configure the HTTP request pipeline. app.UseHttpsRedirection(); app.UseCors("CorsPolicy"); app.UseAuthorization();
Please note that we are not using the AllowAnyOrigin() method to enable cors from any origin, but we explicitly say which origin to allow WithOrigins(“http://localhost:4200”). We are doing this because from .NET Core 3.0 the combination of AllowAnyOrigin
and AllowCredentials
is considered as an insecure CORS configuration. For a more detailed guide about the CORS in .NET Core, you can read Enabling CORS in ASP.NET Core.
One additional thing. It is important to call the UseCors
method before the UseAuthorization
method.
That is it regarding the configuration. Let’s move on to the next part.
SignalR Installation, Hub, and Configuration
We need to install the SignalR library for the client side. To do that, we are going to open the Angular project in the Visual Studio Code and type the following command in the terminal window:
npm install @microsoft/signalr --save
That is it for now regarding the client side.
Let’s switch back to the server-side project and create a new folder Models
. In that folder, we are going to create a new class ChartModel
and modify it:
public class ChartModel { public List<int> Data { get; set; } public string? Label { get; set; } public string? BackgroundColor { get; set; } public ChartModel() { Data = new List<int>(); } }
This form of data is expected by the Angular Charts library (which is yet to be installed), thus the model properties Data
and Label
.
Having the model prepared, we are going to continue by creating a new folder HubConfig
and inside a new class ChartHub
:
public class ChartHub : Hub { }
As we can notice, our ChartHub
class must derive from the Hub
class, which is a base class for the SignalR hub. But why do we need this ChartHub
?
Well, a Hub
is a high-level pipeline that allows communication between client and server to call each other methods directly. So basically, a Hub
is a communication foundation between client and server while using SignalR.
Right now our ChartHub
class is empty because we don’t need any methods inside it, yet.
To complete the SignalR configuration, let’s modify the Program
class again:
builder.Services.AddSignalR(); ... app.MapControllers(); app.MapHub<ChartHub>("/chart");
In the first part, we add SignalR to the IService
collection using the AddSignalR
method. And then, we add SignalR to the request pipeline by pointing to our ChartHub
with the provided /chart
path.
Timer Implementation with DataManager and ChartController
To simulate a real-time data flow from the server, we are going to implement a Timer
class from the System.Threading
namespace. Let’s create a new folder TimerFeatures
and inside it a new class TimerManager
:
public class TimerManager { private Timer? _timer; private AutoResetEvent? _autoResetEvent; private Action? _action; public DateTime TimerStarted { get; set; } public bool IsTimerStarted { get; set; } public void PrepareTimer(Action action) { _action = action; _autoResetEvent = new AutoResetEvent(false); _timer = new Timer(Execute, _autoResetEvent, 1000, 2000); TimerStarted = DateTime.Now; IsTimerStarted = true; } public void Execute(object? stateInfo) { _action(); if ((DateTime.Now - TimerStarted).TotalSeconds > 60) { IsTimerStarted = false; _timer.Dispose(); } } }
We are using an Action
delegate to execute the passed callback function every two seconds. The timer will make a one-second pause before the first execution. Finally, we just create a sixty seconds time slot for execution, to avoid a limitless timer loop. If you want to learn more about delegates and how to use them to write better C# code, you can visit Delegates in C# article.
It is important to have a method that has one object parameter and returns a void result. The Timer
class expects that kind of method in its constructor.
After the TimeManager
implementation, let’s create a new DataStorage
folder and inside it a new DataManager
class. We are going to use this class to fake our data:
public class DataManager { public static List<ChartModel> GetData() { var r = new Random(); return new List<ChartModel>() { new ChartModel { Data = new List<int> { r.Next(1, 40) }, Label = "Data1", BackgroundColor = "#5491DA" }, new ChartModel { Data = new List<int> { r.Next(1, 40) }, Label = "Data2", BackgroundColor = "#E74C3C" }, new ChartModel { Data = new List<int> { r.Next(1, 40) }, Label = "Data3", BackgroundColor = "#82E0AA" }, new ChartModel { Data = new List<int> { r.Next(1, 40) }, Label = "Data4", BackgroundColor = "#E5E7E9" } }; } }
Next, we have to register TimerManager
as a service:
builder.Services.AddSingleton<TimerManager>();
Finally, to complete this section, we are going to create a new controller file ChartController
inside the Controllers
folder:
[Route("api/[controller]")] [ApiController] public class ChartController : ControllerBase { private readonly IHubContext<ChartHub> _hub; private readonly TimerManager _timer; public ChartController(IHubContext<ChartHub> hub, TimerManager timer) { _hub = hub; _timer = timer; } [HttpGet] public IActionResult Get() { if (!_timer.IsTimerStarted) _timer.PrepareTimer(() => _hub.Clients.All.SendAsync("TransferChartData", DataManager.GetData())); return Ok(new { Message = "Request Completed" }); } }
In this controller class, we are using the IHubContext
interface to create its instance via dependency injection. By using that instance object, we are able to access and call the hub methods. This is the reason why we don’t have any method in our ChartHub
class. We don’t need any yet, because we are providing just one-way communication (the server is sending data to the client only), and we can access all the hub methods with IHubContext
interface.
Furthermore, in the Get
action, we are instantiating the TimerManager
class and providing a callback function as a parameter. This callback function will be executed every two seconds.
Now, we have to pay attention to the _hub.Clients.All.SendAsync("transferchartdata", DataManager.GetData())
expression. With it, we are sending generated data to all subscribed clients to the transferchartdata
event. This means that every client if it has a listener on the transferchartdata
event, will receive data generated by the DataManager
class. And that is exactly what we are going to do in the next section.
Angular Chart and SignalR Listener
We have currently finished our work on the server side, so let’s switch to the client side.
To use charts in Angular, we are going to install two required libraries. First ng2-charts
:
npm install ng2-charts --save
And then chart.js
:
npm install chart.js --save
And finally, let’s modify the app.module.ts
file:
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { NgChartsModule } from 'ng2-charts'; import { HttpClientModule } from '@angular/common/http'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, NgChartsModule, HttpClientModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Of course, the HttpClientModule
is not required for the charts to work, but we are going to send the HTTP request to our server, so we need it.
To continue, we are going to create a service file for the sole purpose to wrap the SignalR logic:
ng g service services/signalr --skip-tests
Additionally, we are going to create an interface ChartModel
:
export interface ChartModel { data: [], label: string backgroundColor: string }
Having done that, let’s modify our service file:
import { Injectable } from '@angular/core'; import * as signalR from "@microsoft/signalr" import { ChartModel } from '../_interfaces/chartmodel.model'; @Injectable({ providedIn: 'root' }) export class SignalrService { public data: ChartModel[]; private hubConnection: signalR.HubConnection public startConnection = () => { this.hubConnection = new signalR.HubConnectionBuilder() .withUrl('https://localhost:5001/chart') .build(); this.hubConnection .start() .then(() => console.log('Connection started')) .catch(err => console.log('Error while starting connection: ' + err)) } public addTransferChartDataListener = () => { this.hubConnection.on('transferchartdata', (data) => { this.data = data; console.log(data); }); } }
First of all, we create the data
array which will hold the data fetched from the server and will provide a data source for the chart. In the startConnection
function, we build and start our connection as well as log the message in the console. Finally, we have the addTransferChartDataListener
function in which we subscribe to the transferchardata
event and accept the data from the server with the data
parameter. If we take a look at the Get
action in the ChartController
file, we are going to see that we broadcast the data on the same transferchartdata event: (_hub.Clients.All.SendAsync("transferchartdata", DataManager.GetData())
).
And yes, those must match.
All we have left to do, for now, is to modify the app.component.ts
file:
import { Component, OnInit } from '@angular/core'; import { SignalrService } from './services/signalr.service'; import { HttpClient } from '@angular/common/http'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { constructor(public signalRService: SignalrService, private http: HttpClient) { } ngOnInit() { this.signalRService.startConnection(); this.signalRService.addTransferChartDataListener(); this.startHttpRequest(); } private startHttpRequest = () => { this.http.get('https://localhost:5001/api/chart') .subscribe(res => { console.log(res); }) } }
This logic is straightforward. We just start the connection, add our listener, and send a request towards the Get
action on our server.
This should be our current result:
Excellent. We can see that our data is received in real-time and logged in the console window.
Of course, this is just part of our goal, so let’s get to the finish line.
To do that, let’s modify the app.component.html
file:
<div style="display: block" *ngIf='signalRService.data'> <canvas baseChart [datasets]="signalRService.data" [labels]="chartLabels" [options]="chartOptions" [legend]="chartLegend" [chartType]="chartType" </div>
And the app.component.ts
file:
import { Component, OnInit } from '@angular/core'; import { SignalrService } from './services/signalr.service'; import { HttpClient } from '@angular/common/http'; import { ChartConfiguration, ChartType } from 'chart.js'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { chartOptions: ChartConfiguration['options'] = { responsive: true, scales: { y: { min: 0 } } }; chartLabels: string[] = ['Real time data for the chart']; chartType: ChartType = 'bar'; chartLegend: boolean = true; constructor(public signalRService: SignalrService, private http: HttpClient) { } ngOnInit() { this.signalRService.startConnection(); this.signalRService.addTransferChartDataListener(); this.startHttpRequest(); } private startHttpRequest = () => { this.http.get('https://localhost:5001/api/chart') .subscribe(res => { console.log(res); }) } }
Now, our result should look like this:
This looks great. Our application is working as intended.
Sending Data via SignalR from the Client to the Server and Back
Until now, we’ve broadcasted data only from the server to the client (one-way communication). But what if we want to send some data from the client to the server and then broadcast it to all the subscribed clients (all of that via SignalR)? Well, we can do that as well.
So let’s imagine that we want to send the current data to some API as soon as we click on our chart, and then display them on any other client. To cover that example, we could create another Angular app, but for the sake of simplicity, we are going to implement all of that in our current app.
So, the first thing we want to do is to modify the ChartHub
class in .NET Core:
public class ChartHub : Hub { public async Task BroadcastChartData(List<ChartModel> data) => await Clients.All.SendAsync("broadcastchartdata", data); }
Because we are starting the SignalR communication from the client, we need a hub endpoint to Invoke our data. This BroadcastChartData
method will receive the message from the client and then broadcast that same message to all the clients that listen to the bradcastchratdata
event.
The second step is to modify the service file in Angular:
import { Injectable } from '@angular/core'; import * as signalR from "@microsoft/signalr" import { ChartModel } from '../_interfaces/chartmodel.model'; @Injectable({ providedIn: 'root' }) export class SignalrService { public data: ChartModel[]; public bradcastedData: ChartModel[]; private hubConnection: signalR.HubConnection public startConnection = () => { this.hubConnection = new signalR.HubConnectionBuilder() .withUrl('https://localhost:5001/chart') .build(); this.hubConnection .start() .then(() => console.log('Connection started')) .catch(err => console.log('Error while starting connection: ' + err)) } public addTransferChartDataListener = () => { this.hubConnection.on('transferchartdata', (data) => { this.data = data; console.log(data); }); } public broadcastChartData = () => { const data = this.data.map(m => { const temp = { data: m.data, label: m.label } return temp; }); this.hubConnection.invoke('broadcastchartdata', data) .catch(err => console.error(err)); } public addBroadcastChartDataListener = () => { this.hubConnection.on('broadcastchartdata', (data) => { this.bradcastedData = data; }) } }
The first function will send data to our Hub
endpoint and the second function will listen on the braodcastchartdata
event.
The third step is to modify the app.component.ts
file by adding a new function:
public chartClicked = (event) => { console.log(event); this.signalRService.broadcastChartData(); }
And finally, let’s provide the chartClicked
event for our chart:
ngOnInit() { this.signalRService.startConnection(); this.signalRService.addTransferChartDataListener(); this.signalRService.addBroadcastChartDataListener(); this.startHttpRequest(); } private startHttpRequest = () => { this.http.get('https://localhost:5001/api/chart') .subscribe(res => { console.log(res); }) } public chartClicked = (event) => { console.log(event); this.signalRService.broadcastChartData(); }
After all of the changes, we can inspect the result:
Excellent work. Everything works like a charm.
Of course, we can accomplish a lot more with SignalR and cover a whole load of features, but this is a good starting point for sure.
Conclusion
By reading this article, we’ve learned:
- How to install SignalR and prepare a basic configuration
- The way to use Timer in .NET Core
- How to provide a SignalR implementation on the client and server-side
- The way to use charts to consume real-time data sent via SignalR