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.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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 download a finished project, you can clone the repo from Real-Time Charts SignalR source code.

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:

SingalR connected - SignalR with .NET Core and Angular

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:

SignalR chart completed - SignalR

This looks great. Our application is working as intended.

If we keep the app idle, after a while the SignalR will disconnect. Once the connection is lost, we have to refresh the page to reconnect. And that’s not a good user experience. But there is a solution. We can implement the automatic reconnection to force SignalR to reconnect after a couple of seconds. Feel free to read the linked article to learn more about this feature

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.

As we mentioned, with this setup, we communicate with all the subscribed clients. But if we want to make a communication with only a specific client, we can do that as well. You can read more about it in How to Send Client-Specific Messages with SiganlR article.

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:

SignalR - chart - two way communication

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
Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!