Updated on

To push live data from ASP.NET Core into an Angular chart, the server exposes a SignalR Hub and the Angular client connects to it with @microsoft/signalr, registering a handler through hubConnection.on() for the method name the server invokes.

The server never asks the browser for anything. It calls Clients.All.SendAsync("methodName", data) whenever it has new data, and every connected client’s handler runs.

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.

What Is SignalR and How Does It Work With Angular?

SignalR is a library that lets an ASP.NET Core server push data to connected clients the moment it has that data, instead of making the browser ask for it on a timer.

Three pieces carry the whole model. A hub is a server class deriving from Hub that both sides call methods on. A connection is one client’s live link to that hub. A client method is a name the browser registers a handler for, which the server invokes by that name.

Angular sits on the client side. The @microsoft/signalr package builds the connection with HubConnectionBuilder, and hubConnection.on('methodName', handler) registers the function SignalR runs when data arrives.

The transport underneath is chosen for us. SignalR negotiates WebSockets first and falls back to Server-Sent Events or long polling where WebSockets are unavailable.

Nothing in the hub knows it is talking to Angular. The same hub serves a React app, a .NET client, or a mobile client without a line changing.

A hub can also be reached from outside itself, and we cover that in calling a SignalR hub from a controller.

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:

The embedded video shows an earlier version of TimerManager that also created an AutoResetEvent and did not null-check its fields; the code below drops both, which changes nothing about how the timer behaves.

public class TimerManager
{
    private Timer? _timer;
    private Action? _action;
    public DateTime TimerStarted { get; set; }
    public bool IsTimerStarted { get; set; }

    public void PrepareTimer(Action action)
    {
        _action = action;
        _timer = new Timer(Execute, null, 1000, 2000);
        TimerStarted = DateTime.Now;
        IsTimerStarted = true;
    }

    public void Execute(object? stateInfo)
    {
        _action?.Invoke();

        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. For the class itself, we have a whole article on the Timer class in C#, and where the repeating work is awaitable, PeriodicTimer, the modern alternative, reads better than a callback. There are also other ways to run background tasks in ASP.NET Core when the work outgrows a demo timer.

Is this material useful to you? Consider subscribing and get ASP.NET Core Web API Best Practices eBook for FREE!

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 TimerManager 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 listening for the TransferChartData client method. This means that every client that has a listener on that name will receive data generated by the DataManager class. And that is exactly what we are going to do in the next section.

How Do We Connect an Angular App to a SignalR Hub?

Four steps connect an Angular application to a hub, and they are the same four for any hub.

Install the client package with npm install @microsoft/signalr. That is the only SignalR dependency the browser needs, and it is independent of the chart library.

Build the connection inside a service rather than a component. new signalR.HubConnectionBuilder().withUrl(hubUrl).build() creates it, and .start() returns a promise that resolves once the handshake finishes.

Register the handlers on that connection, once. hubConnection.on('transferchartdata', data => …) names the client method the server will invoke, and the handler receives whatever the server passed to SendAsync.

Keep the service root-provided so every component shares one connection. A connection built per component opens one socket per component, and handlers registered in a component’s ngOnInit are registered again every time that component is recreated.

The chart then binds straight to the service’s data. Angular re-renders whenever the handler replaces it, so there is no refresh call to write.

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: number[],
  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 transferchartdata client method 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 under the same name: (_hub.Clients.All.SendAsync("TransferChartData", DataManager.GetData())).

And yes, those must match — though not on case. The JavaScript client lowercases both the name we register and the name arriving from the server, so TransferChartData and transferchartdata meet in the middle. The .NET SignalR client does not do this, so a .NET client has to spell the name exactly as the server sends it.

One note on the hub address: withUrl('https://localhost:5001/chart') is a development URL written by hand, and in anything deployed it comes from environment.ts rather than from the service file.

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 implements OnInit {
  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:

SignalR connected - SignalR with .NET Core and Angular

Is this material useful to you? Consider subscribing and get ASP.NET Core Web API Best Practices eBook for FREE!

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"
          (chartClick)="chartClicked($event)"></canvas>
</div>
<br><br>
<ul>
  <li *ngFor='let data of signalRService.broadcastedData'>
    <strong>Label: </strong> {{data.label}} - <strong>Value: </strong> {{data.data[0]}}
  </li>
</ul>

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 implements OnInit {
  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 broadcastchartdata client method.

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 SignalR: Send a Message to a Specific Client in ASP.NET Core 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 broadcastedData: 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.broadcastedData = data;
      })
    }
}

The first function will send data to our Hub endpoint and the second function will listen on the broadcastchartdata client method.

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.

Where Do the .NET and Angular Sides Have to Agree?

Five things have to line up across the two projects, and every one of them fails quietly rather than loudly.

The hub route has to match. app.MapHub<ChartHub>("/chart") on the server and the path inside withUrl() on the client are the same string, and a mismatch shows up as a failed negotiation rather than an exception.

The CORS policy has to name the Angular origin by hand. SignalR sends credentials, and AllowAnyOrigin cannot be combined with AllowCredentials, so the development address is listed explicitly.

Method names do not have to match on case. The JavaScript client lowercases both the name we register and the name arriving from the server, so SendAsync("TransferChartData", …) reaches a handler registered as 'transferchartdata'.

Property names do change shape. C# PascalCase properties arrive as camelCase JSON, which is why the Angular interface declares label where the C# model declares Label.

The last two are the expensive ones, because neither of them puts anything in the console.

What must line upOn the .NET sideOn the Angular sideWhat a mismatch looks like
Hub routeapp.MapHub<ChartHub>("/chart").withUrl('https://localhost:5001/chart')POST /negotiate returns 404 and the connection never starts
Allowed origin.WithOrigins("http://localhost:4200") with .AllowCredentials()the address ng serve printsThe preflight comes back without Access-Control-Allow-Origin, and the browser logs a CORS error
Server → client method nameSendAsync("TransferChartData", data)hubConnection.on('transferchartdata', …)Nothing. The JavaScript client matches these case-insensitively
Client → server method namepublic async Task BroadcastChartData(…)hubConnection.invoke('broadcastchartdata', data)The promise rejects with Failed to invoke '…' due to an error on the server. HubException: Method does not exist.
Payload property namespublic string? Label { get; set; }label: stringundefined in the chart, and no error anywhere
Argument count and orderthe hub method's parameter listthe arguments after the method nameThe promise rejects with Failed to invoke '…' due to an error on the server, before the hub method runs

The case rule in row three is about the JavaScript client specifically. A .NET SignalR client registering On("transferchartdata") against a hub that sends "TransferChartData" never fires, so a .NET client has to reproduce the server’s spelling exactly.

The same two-sided contract shows up on other front ends too, and the same real-time chart in Blazor WebAssembly builds it against an identical hub.

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

If you want to go deeper on the API side of an application like this, take a look at the Ultimate ASP.NET Core Web API course.

Tested with .NET 10.0.10, Angular 13.2 and @microsoft/signalr 6.0.4.