Learn how to build a real-time, live-updating data dashboard using Angular on the frontend and ASP.NET Core with SignalR on the backend. This guide walks through every step — from project setup to production deployment — with full code examples, best practices, and SEO-friendly structure for your own blog or documentation.
Table of Contents
- Introduction
- What You Will Build
- Prerequisites
- Understanding the Architecture
- Part 1 (Beginner): Setting Up the ASP.NET Core Backend
- Part 2 (Beginner): Adding SignalR for Real-Time Communication
- Part 3 (Beginner): Setting Up the Angular Frontend
- Part 4 (Intermediate): Connecting Angular to SignalR
- Part 5 (Intermediate): Building the Dashboard UI with Live Charts
- Part 6 (Intermediate): Styling the Dashboard
- Part 7 (Advanced): Handling Reconnection and Error States
- Part 8 (Advanced): Adding Authentication with JWT
- Part 9 (Advanced): Scaling SignalR with Redis Backplane
- Part 10 (Advanced): Performance Optimization
- Part 11 (Advanced): Testing the Application
- Part 12 (Advanced): Deploying to Production
- Common Errors and Troubleshooting
- Best Practices Checklist
- Frequently Asked Questions (FAQ)
- Conclusion
1. Introduction
Real-time dashboards have become a core requirement in modern web applications — whether you're tracking stock prices, monitoring IoT sensor data, displaying server health metrics, or showing live sales numbers. Instead of forcing users to refresh a page to see updated data, a live data dashboard pushes new information to the browser the moment it becomes available.
In this guide, you will learn how to build a complete live data dashboard using two of the most popular technologies in the enterprise web development world: Angular for the frontend and ASP.NET Core for the backend, connected through SignalR, Microsoft's real-time communication library.
This tutorial is structured to take you from absolute beginner concepts all the way to advanced, production-ready practices including authentication, scaling, and deployment. Whether you are a student building your first real-time app or a professional developer architecting a production dashboard, this guide has something for you.
2. What You Will Build
By the end of this tutorial, you will have a fully functional application consisting of:
- An ASP.NET Core Web API backend that generates and broadcasts live data (for example, simulated stock prices, server metrics, or sensor readings).
- A SignalR Hub that pushes updates to connected clients instantly, without polling.
- An Angular application that connects to the SignalR hub and updates its UI in real time.
- Live charts built with a charting library (Chart.js or ngx-charts) that animate as new data arrives.
- JWT authentication to secure the dashboard so only logged-in users can view live data.
- A production deployment setup using Docker and IIS/Azure.
3. Prerequisites
Before starting, make sure you have the following installed and a basic understanding of these technologies.
Software Requirements
- Node.js (LTS version) and npm
-
Angular CLI (
npm install -g @angular/cli) - .NET SDK (latest LTS version of .NET)
- Visual Studio Code or Visual Studio 2022
- Git for version control
- (Optional, for advanced sections) Docker Desktop and a Redis instance
Knowledge Requirements
- Basic understanding of TypeScript and Angular components/services
- Basic understanding of C# and ASP.NET Core controllers
- Familiarity with REST APIs (helpful but not mandatory)
If you are completely new to Angular or ASP.NET Core, don't worry — each step includes explanations of why we are doing something, not just how.
4. Understanding the Architecture
Before writing any code, it's important to understand how the pieces fit together.
┌─────────────────────┐ WebSocket / SignalR ┌──────────────────────┐
│ │ <---------------------------------> │ │
│ Angular Frontend │ │ ASP.NET Core Backend│
│ (Dashboard UI + │ REST API (initial data) │ (Web API + SignalR │
│ SignalR Client) │ <---------------------------------- │ Hub + Data Service)│
│ │ │ │
└─────────────────────┘ └──────────────────────┘
│
▼
┌──────────────────────┐
│ Data Source (DB, │
│ IoT feed, simulated │
│ data generator, etc)│
└──────────────────────┘
Key concept: Instead of the Angular app repeatedly calling an API (polling) every few seconds to check for new data, the ASP.NET Core backend uses SignalR to push data directly to connected clients over a persistent WebSocket connection. This is far more efficient and provides true real-time updates.
We will use a REST API call for the initial page load (to get historical/starting data) and SignalR for all subsequent live updates.
5. Part 1 (Beginner): Setting Up the ASP.NET Core Backend
Step 1.1: Create the Web API Project
Open your terminal and run:
dotnet new webapi -n LiveDashboard.Api
cd LiveDashboard.Api
This creates a new ASP.NET Core Web API project. If you're prompted to choose between minimal APIs and controllers, this guide uses controllers for clarity, but the concepts apply equally to minimal API style.
Step 1.2: Project Structure
Organize your project with the following folders for clarity as the app grows:
LiveDashboard.Api/
├── Controllers/
├── Hubs/
├── Models/
├── Services/
├── Program.cs
└── appsettings.json
Create these folders inside your project directory.
Step 1.3: Define the Data Model
Create Models/MetricData.cs:
namespace LiveDashboard.Api.Models
{
public class MetricData
{
public string MetricName { get; set; } = string.Empty;
public double Value { get; set; }
public DateTime Timestamp { get; set; }
}
}
This simple model represents a single data point — for example, a CPU usage percentage, a stock price, or a temperature reading.
Step 1.4: Create a Simulated Data Service
In a real application, this data might come from a database, a message queue, or an external API. For this tutorial, we'll simulate live data so you can see the dashboard working immediately.
Create Services/DataGeneratorService.cs:
using LiveDashboard.Api.Models;
namespace LiveDashboard.Api.Services
{
public class DataGeneratorService
{
private readonly Random _random = new();
public MetricData GenerateMetric(string metricName, double baseValue, double variance)
{
var change = (_random.NextDouble() * 2 - 1) * variance;
return new MetricData
{
MetricName = metricName,
Value = Math.Round(baseValue + change, 2),
Timestamp = DateTime.UtcNow
};
}
}
}
Register this service in Program.cs (we'll come back to Program.cs again shortly):
builder.Services.AddSingleton<DataGeneratorService>();
Step 1.5: Create a Basic REST Endpoint
Create Controllers/MetricsController.cs to allow the Angular app to fetch the initial set of data when it first loads:
using Microsoft.AspNetCore.Mvc;
using LiveDashboard.Api.Services;
namespace LiveDashboard.Api.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class MetricsController : ControllerBase
{
private readonly DataGeneratorService _generator;
public MetricsController(DataGeneratorService generator)
{
_generator = generator;
}
[HttpGet("snapshot")]
public IActionResult GetSnapshot()
{
var data = new[]
{
_generator.GenerateMetric("CPU", 45, 10),
_generator.GenerateMetric("Memory", 60, 8),
_generator.GenerateMetric("RequestsPerSecond", 120, 30)
};
return Ok(data);
}
}
}
Step 1.6: Enable CORS
Since Angular (running on port 4200 in development) and ASP.NET Core (running on a different port) are different origins, you need to enable CORS. Add this to Program.cs:
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAngularDev", policy =>
{
policy.WithOrigins("http://localhost:4200")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
And later, in the middleware pipeline:
app.UseCors("AllowAngularDev");
Tip: AllowCredentials() is required for SignalR connections to work correctly with CORS.
Step 1.7: Run and Verify
Run the backend with:
dotnet run
Visit https://localhost:<port>/api/metrics/snapshot in your browser or via a tool like Postman to confirm you receive a JSON array of metric data.
At this point, you have a working REST API. Next, we'll add real-time capability using SignalR.
6. Part 2 (Beginner): Adding SignalR for Real-Time Communication
Step 2.1: What Is SignalR?
SignalR is a library built into ASP.NET Core that simplifies adding real-time web functionality. It abstracts away the complexity of WebSockets, automatically falling back to other transport methods (like Server-Sent Events or long polling) when WebSockets aren't available, and handles connection management for you.
Step 2.2: Create the Hub
A "Hub" in SignalR is the core component that handles communication between server and clients. Create Hubs/DashboardHub.cs:
using Microsoft.AspNetCore.SignalR;
namespace LiveDashboard.Api.Hubs
{
public class DashboardHub : Hub
{
public override async Task OnConnectedAsync()
{
await Clients.Caller.SendAsync("ReceiveMessage", "Connected to live dashboard hub.");
await base.OnConnectedAsync();
}
}
}
This hub doesn't need many methods yet — most of the data pushing will happen from a background service, not from client-invoked methods. The hub mainly serves as the "pipe" through which data flows.
Step 2.3: Register SignalR in Program.cs
Update Program.cs:
using LiveDashboard.Api.Hubs;
using LiveDashboard.Api.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddSignalR();
builder.Services.AddSingleton<DataGeneratorService>();
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAngularDev", policy =>
{
policy.WithOrigins("http://localhost:4200")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
var app = builder.Build();
app.UseCors("AllowAngularDev");
app.UseRouting();
app.MapControllers();
app.MapHub<DashboardHub>("/hubs/dashboard");
app.Run();
The key line here is app.MapHub<DashboardHub>("/hubs/dashboard"), which exposes the hub at a specific URL that the Angular client will connect to.
Step 2.4: Create a Background Service to Push Live Data
This is where the "live" part of the dashboard comes to life. We'll create a hosted background service that runs continuously, generates new data every few seconds, and broadcasts it to all connected clients via the hub.
Create Services/MetricBroadcastService.cs:
using Microsoft.AspNetCore.SignalR;
using LiveDashboard.Api.Hubs;
namespace LiveDashboard.Api.Services
{
public class MetricBroadcastService : BackgroundService
{
private readonly IHubContext<DashboardHub> _hubContext;
private readonly DataGeneratorService _generator;
public MetricBroadcastService(IHubContext<DashboardHub> hubContext, DataGeneratorService generator)
{
_hubContext = hubContext;
_generator = generator;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var cpu = _generator.GenerateMetric("CPU", 45, 10);
var memory = _generator.GenerateMetric("Memory", 60, 8);
var rps = _generator.GenerateMetric("RequestsPerSecond", 120, 30);
await _hubContext.Clients.All.SendAsync("ReceiveMetric", cpu, stoppingToken);
await _hubContext.Clients.All.SendAsync("ReceiveMetric", memory, stoppingToken);
await _hubContext.Clients.All.SendAsync("ReceiveMetric", rps, stoppingToken);
await Task.Delay(2000, stoppingToken); // push every 2 seconds
}
}
}
}
Register the background service in Program.cs:
builder.Services.AddHostedService<MetricBroadcastService>();
How this works: IHubContext<DashboardHub> lets you send messages to clients from anywhere in your application (not just from within the hub itself) — perfect for a background service that has no direct connection to a specific client, but needs to broadcast to all of them. Clients.All.SendAsync("ReceiveMetric", ...) sends a message named "ReceiveMetric" with the metric data as a payload to every connected client.
Step 2.5: Verify with a Test Client (Optional)
Before building the Angular frontend, you can test the SignalR hub using a tool like the SignalR test client in Postman or a simple HTML page with the SignalR JavaScript client. This isn't required, but it can help isolate backend issues from frontend issues if something doesn't work later.
At this point, your backend is complete for the beginner phase: it has a REST endpoint for the initial snapshot and a SignalR hub broadcasting new data every two seconds.
7. Part 3 (Beginner): Setting Up the Angular Frontend
Step 3.1: Create the Angular Project
In a separate folder (sibling to your backend project), run:
ng new live-dashboard-client --routing --style=scss
cd live-dashboard-client
Choose the default options when prompted, or customize as you prefer.
Step 3.2: Install the SignalR Client Library
Angular communicates with the SignalR hub using Microsoft's official JavaScript/TypeScript client:
npm install @microsoft/signalr
Step 3.3: Install a Charting Library
For this tutorial we'll use Chart.js with the ng2-charts Angular wrapper, since it's lightweight and beginner-friendly. (Advanced alternative: ngx-charts, covered briefly later.)
npm install chart.js ng2-charts
Step 3.4: Project Structure
Generate the core building blocks:
ng generate service services/signalr
ng generate service services/metrics
ng generate component components/dashboard
ng generate component components/metric-card
This gives you a clean separation: a service to manage the SignalR connection, a service to manage metric state, and components for the visual layer.
Step 3.5: Define the Metric Model
Create src/app/models/metric-data.model.ts:
export interface MetricData {
metricName: string;
value: number;
timestamp: string;
}
This mirrors the C# MetricData class on the backend, ensuring type safety across the stack.
At this point, your Angular project is scaffolded and ready to be wired up to the live backend. In the next section, we'll connect it to SignalR.
8. Part 4 (Intermediate): Connecting Angular to SignalR
Step 4.1: Build the SignalR Service
This service will manage the connection lifecycle (connect, disconnect, reconnect) and expose an Observable stream of incoming metric data that any component can subscribe to.
Edit src/app/services/signalr.service.ts:
import { Injectable } from '@angular/core';
import * as signalR from '@microsoft/signalr';
import { Subject } from 'rxjs';
import { MetricData } from '../models/metric-data.model';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class SignalrService {
private hubConnection!: signalR.HubConnection;
private metricSubject = new Subject<MetricData>();
public metric$ = this.metricSubject.asObservable();
public startConnection(): void {
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl(`${environment.apiBaseUrl}/hubs/dashboard`)
.withAutomaticReconnect()
.configureLogging(signalR.LogLevel.Information)
.build();
this.hubConnection
.start()
.then(() => console.log('SignalR connection started'))
.catch(err => console.error('Error starting SignalR connection:', err));
this.registerHandlers();
}
private registerHandlers(): void {
this.hubConnection.on('ReceiveMetric', (data: MetricData) => {
this.metricSubject.next(data);
});
this.hubConnection.on('ReceiveMessage', (message: string) => {
console.log('Server message:', message);
});
}
public stopConnection(): void {
this.hubConnection?.stop();
}
}
Key concepts explained:
-
HubConnectionBuilder().withUrl(...)configures the connection to point at the hub endpoint we exposed on the backend (/hubs/dashboard). -
.withAutomaticReconnect()tells SignalR to automatically attempt reconnection if the connection drops — essential for a reliable live dashboard. -
this.hubConnection.on('ReceiveMetric', ...)registers a handler for the exact same message name ("ReceiveMetric") that the backend uses inSendAsync("ReceiveMetric", ...). These names must match exactly. - We use an RxJS
Subjectto convert SignalR's callback-based API into an Observable stream, which fits naturally into Angular's reactive patterns.
Step 4.2: Create the Environment Configuration
Update src/environments/environment.ts:
export const environment = {
production: false,
apiBaseUrl: 'https://localhost:7000' // match your backend's actual port
};
And src/environments/environment.prod.ts for production builds:
export const environment = {
production: true,
apiBaseUrl: 'https://your-production-api.com'
};
Step 4.3: Build the Metrics Service
This service will fetch the initial snapshot via REST and maintain the current state of each metric as live updates arrive.
Edit src/app/services/metrics.service.ts:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject } from 'rxjs';
import { MetricData } from '../models/metric-data.model';
import { environment } from '../../environments/environment';
import { SignalrService } from './signalr.service';
@Injectable({
providedIn: 'root'
})
export class MetricsService {
private metricsMap = new Map<string, MetricData>();
private metricsSubject = new BehaviorSubject<MetricData[]>([]);
public metrics$ = this.metricsSubject.asObservable();
constructor(private http: HttpClient, private signalr: SignalrService) {
this.signalr.metric$.subscribe(metric => this.upsertMetric(metric));
}
public loadInitialSnapshot(): void {
this.http
.get<MetricData[]>(`${environment.apiBaseUrl}/api/metrics/snapshot`)
.subscribe(initialData => {
initialData.forEach(m => this.metricsMap.set(m.metricName, m));
this.emitMetrics();
});
}
private upsertMetric(metric: MetricData): void {
this.metricsMap.set(metric.metricName, metric);
this.emitMetrics();
}
private emitMetrics(): void {
this.metricsSubject.next(Array.from(this.metricsMap.values()));
}
}
Why a Map keyed by metric name? Each new live update replaces the latest value for that specific metric (e.g., CPU), rather than appending to an ever-growing list. This keeps the current-state view of the dashboard accurate and memory-efficient. (We'll handle historical trend data separately for charts in the next section.)
Step 4.4: Enable HttpClient
Make sure HttpClient is available. In a standalone Angular app (app.config.ts):
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient()
]
};
If you're using an NgModule-based app instead, import HttpClientModule into your AppModule.
Step 4.5: Initialize the Connection in the Dashboard Component
Edit src/app/components/dashboard/dashboard.component.ts:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MetricsService } from '../../services/metrics.service';
import { SignalrService } from '../../services/signalr.service';
import { MetricData } from '../../models/metric-data.model';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [CommonModule],
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent implements OnInit, OnDestroy {
metrics: MetricData[] = [];
constructor(
private metricsService: MetricsService,
private signalrService: SignalrService
) {}
ngOnInit(): void {
this.signalrService.startConnection();
this.metricsService.loadInitialSnapshot();
this.metricsService.metrics$.subscribe(data => {
this.metrics = data;
});
}
ngOnDestroy(): void {
this.signalrService.stopConnection();
}
}
At this stage, your Angular app connects to the backend, fetches an initial snapshot, and listens for live updates. Next, we'll render this data visually.
9. Part 5 (Intermediate): Building the Dashboard UI with Live Charts
Step 5.1: Basic Metric Cards (Numbers View)
Update dashboard.component.html:
<div class="dashboard">
<h1>Live Data Dashboard</h1>
<div class="metric-grid">
<app-metric-card
*ngFor="let metric of metrics"
[name]="metric.metricName"
[value]="metric.value"
[timestamp]="metric.timestamp">
</app-metric-card>
</div>
</div>
Edit metric-card.component.ts:
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-metric-card',
standalone: true,
imports: [CommonModule],
templateUrl: './metric-card.component.html',
styleUrls: ['./metric-card.component.scss']
})
export class MetricCardComponent {
@Input() name = '';
@Input() value = 0;
@Input() timestamp = '';
}
metric-card.component.html:
<div class="metric-card">
<h3>{{ name }}</h3>
<p class="value">{{ value }}</p>
<small>Updated: {{ timestamp | date:'mediumTime' }}</small>
</div>
This gives you a simple, working live-updating numeric dashboard. Now let's add line charts to visualize trends over time.
Step 5.2: Setting Up ng2-charts
In your standalone component or module, import the chart provider. With standalone components, register it in app.config.ts:
import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
provideCharts(withDefaultRegisterables())
]
};
Step 5.3: Create a Live Chart Component
Generate a new component:
ng generate component components/live-chart
This component needs to maintain a rolling history of values for a given metric (e.g., the last 30 data points) so the chart shows a trend rather than just the latest number.
Edit live-chart.component.ts:
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BaseChartDirective } from 'ng2-charts';
import { ChartConfiguration, ChartData } from 'chart.js';
@Component({
selector: 'app-live-chart',
standalone: true,
imports: [CommonModule, BaseChartDirective],
templateUrl: './live-chart.component.html',
styleUrls: ['./live-chart.component.scss']
})
export class LiveChartComponent implements OnChanges {
@Input() label = '';
@Input() value = 0;
@Input() timestamp = '';
private readonly maxPoints = 30;
public lineChartData: ChartData<'line'> = {
labels: [],
datasets: [
{
data: [],
label: this.label,
borderColor: '#3f8cff',
backgroundColor: 'rgba(63, 140, 255, 0.15)',
fill: true,
tension: 0.35,
pointRadius: 0
}
]
};
public lineChartOptions: ChartConfiguration['options'] = {
responsive: true,
animation: { duration: 300 },
scales: {
x: { display: false },
y: { beginAtZero: false }
},
plugins: {
legend: { display: false }
}
};
ngOnChanges(changes: SimpleChanges): void {
if (changes['value'] && !changes['value'].firstChange) {
this.addDataPoint();
} else if (changes['value'] && changes['value'].firstChange) {
this.addDataPoint();
}
}
private addDataPoint(): void {
const labels = this.lineChartData.labels as string[];
const dataset = this.lineChartData.datasets[0].data as number[];
labels.push(new Date(this.timestamp).toLocaleTimeString());
dataset.push(this.value);
if (labels.length > this.maxPoints) {
labels.shift();
dataset.shift();
}
// Reassign to trigger change detection in ng2-charts
this.lineChartData = {
...this.lineChartData,
labels: [...labels],
datasets: [{ ...this.lineChartData.datasets[0], data: [...dataset] }]
};
}
}
live-chart.component.html:
<div class="chart-wrapper">
<h4>{{ label }} Trend</h4>
<canvas
baseChart
[data]="lineChartData"
[options]="lineChartOptions"
type="line">
</canvas>
</div>
Step 5.4: Wire the Chart into the Dashboard
Update dashboard.component.html to include the chart alongside the cards:
<div class="dashboard">
<h1>Live Data Dashboard</h1>
<div class="metric-grid">
<app-metric-card
*ngFor="let metric of metrics"
[name]="metric.metricName"
[value]="metric.value"
[timestamp]="metric.timestamp">
</app-metric-card>
</div>
<div class="chart-grid">
<app-live-chart
*ngFor="let metric of metrics"
[label]="metric.metricName"
[value]="metric.value"
[timestamp]="metric.timestamp">
</app-live-chart>
</div>
</div>
Don't forget to add LiveChartComponent to the imports array of DashboardComponent since both are standalone.
At this point, your dashboard displays both live numeric values and animated trend lines that update in real time as the backend pushes new data every two seconds.
Step 5.5: Alternative — Using ngx-charts (Advanced Option)
If you prefer a more "Angular-native" charting library with declarative templates instead of imperative chart configuration objects, consider @swimlane/ngx-charts. It integrates well with Angular's change detection and offers built-in chart types (line, bar, gauge, pie) ideal for dashboards. The setup process is similar: install the package, import the chart module, and bind data arrays to chart components via [results] inputs. Many production dashboards mix both libraries depending on the visualization needed — for example, ngx-charts for gauges and Chart.js for dense time-series lines.
10. Part 6 (Intermediate): Styling the Dashboard
A functional dashboard is only half the job — visual clarity matters, especially for monitoring tools that may be displayed on a large screen for extended periods (e.g., a NOC wall display).
Step 6.1: Grid Layout
In dashboard.component.scss:
.dashboard {
padding: 2rem;
background: #0f172a;
min-height: 100vh;
color: #e2e8f0;
font-family: 'Inter', sans-serif;
h1 {
font-weight: 600;
margin-bottom: 1.5rem;
}
}
.metric-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.chart-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.5rem;
}
Step 6.2: Card Styling
In metric-card.component.scss:
.metric-card {
background: #1e293b;
border-radius: 12px;
padding: 1.25rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
transition: transform 0.2s ease;
&:hover {
transform: translateY(-2px);
}
h3 {
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #94a3b8;
margin-bottom: 0.5rem;
}
.value {
font-size: 2rem;
font-weight: 700;
color: #3f8cff;
margin: 0;
}
small {
color: #64748b;
}
}
Step 6.3: Visual Feedback on Update
A nice UX touch for live dashboards is briefly highlighting a value when it changes, so users can visually notice an update without staring at numbers. You can achieve this with a CSS animation triggered by an Angular class binding tied to a "just updated" flag, toggled with a short setTimeout whenever new data arrives. This kind of micro-interaction significantly improves the perceived "liveness" of the dashboard.
11. Part 7 (Advanced): Handling Reconnection and Error States
Production dashboards must gracefully handle network interruptions. SignalR's .withAutomaticReconnect() handles basic reconnection, but you should also surface connection status to users and handle edge cases.
Step 7.1: Expose Connection State
Update signalr.service.ts to track and expose connection status:
import { BehaviorSubject } from 'rxjs';
public connectionState$ = new BehaviorSubject<'connected' | 'reconnecting' | 'disconnected'>('disconnected');
public startConnection(): void {
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl(`${environment.apiBaseUrl}/hubs/dashboard`)
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
.build();
this.hubConnection.onreconnecting(() => this.connectionState$.next('reconnecting'));
this.hubConnection.onreconnected(() => this.connectionState$.next('connected'));
this.hubConnection.onclose(() => this.connectionState$.next('disconnected'));
this.hubConnection
.start()
.then(() => this.connectionState$.next('connected'))
.catch(() => this.connectionState$.next('disconnected'));
this.registerHandlers();
}
The array [0, 2000, 5000, 10000, 30000] passed to withAutomaticReconnect defines custom retry delays (in milliseconds): retry immediately, then after 2s, 5s, 10s, and 30s, after which SignalR stops retrying automatically (you can add logic to manually retry beyond this).
Step 7.2: Display a Connection Banner
In dashboard.component.html, add a status banner:
<div class="connection-banner" *ngIf="connectionState !== 'connected'" [ngClass]="connectionState">
<span *ngIf="connectionState === 'reconnecting'">Reconnecting to live data...</span>
<span *ngIf="connectionState === 'disconnected'">Disconnected. Attempting to reconnect...</span>
</div>
Subscribe to connectionState$ in the component class just like you did for metrics. This small addition dramatically improves trust in the dashboard — users immediately know if the data they're looking at might be stale.
Step 7.3: Handle Manual Reconnection
If automatic reconnection exhausts its retries, implement a manual fallback, for instance using setInterval to attempt hubConnection.start() again every 30 seconds while in the disconnected state, with exponential backoff capped at a reasonable maximum to avoid hammering the server.
Step 7.4: Graceful Degradation
Consider what happens if SignalR/WebSockets are entirely blocked (e.g., by a restrictive corporate proxy). SignalR automatically falls back to Server-Sent Events or long polling, but if you want a guaranteed fallback, you could implement a periodic REST polling mechanism as a last resort, triggered only when the connectionState$ remains disconnected for an extended period.
12. Part 8 (Advanced): Adding Authentication with JWT
Most production dashboards display sensitive business data and need to be secured. Here's how to add JWT-based authentication to both the backend and the SignalR connection.
Step 8.1: Backend — Add JWT Authentication
Install the JWT package:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Configure it in Program.cs:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var jwtKey = builder.Configuration["Jwt:Key"]!;
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
};
// Required so SignalR can read the access token from the query string,
// since WebSocket connections cannot set custom Authorization headers.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization();
Add the middleware:
app.UseAuthentication();
app.UseAuthorization();
Secure the hub and controller:
[Authorize]
public class DashboardHub : Hub { ... }
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class MetricsController : ControllerBase { ... }
Important detail: Browsers cannot attach custom Authorization headers to native WebSocket connections, so SignalR's JavaScript client sends the token as a query string parameter (access_token) instead. The OnMessageReceived event above reads that token specifically for requests to the /hubs path.
Step 8.2: Create a Simple Login Endpoint (for demo purposes)
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IConfiguration _config;
public AuthController(IConfiguration config) => _config = config;
[HttpPost("login")]
public IActionResult Login([FromBody] LoginRequest request)
{
// Replace with real user validation against your identity store
if (request.Username != "demo" || request.Password != "password")
return Unauthorized();
var claims = new[] { new Claim(ClaimTypes.Name, request.Username) };
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]!));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddHours(2),
signingCredentials: creds);
return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) });
}
}
public record LoginRequest(string Username, string Password);
Add the corresponding Jwt:Key, Jwt:Issuer, and Jwt:Audience values to appsettings.json.
Step 8.3: Angular — Send the Token with SignalR
Update signalr.service.ts to pass the access token when building the connection:
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl(`${environment.apiBaseUrl}/hubs/dashboard`, {
accessTokenFactory: () => localStorage.getItem('authToken') || ''
})
.withAutomaticReconnect()
.build();
Step 8.4: Add an Angular HTTP Interceptor for REST Calls
To attach the token to your regular REST API calls (like /api/metrics/snapshot), add an HttpInterceptorFn:
import { HttpInterceptorFn } from '@angular/common/http';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = localStorage.getItem('authToken');
const cloned = token
? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
: req;
return next(cloned);
};
Register it in app.config.ts:
provideHttpClient(withInterceptors([authInterceptor]))
Step 8.5: Build a Minimal Login Component
Create a simple login form that calls /api/auth/login, stores the returned token in localStorage (or, for better security, a memory-based store with refresh tokens in an HttpOnly cookie for production-grade apps), and redirects to the dashboard route. Use an Angular route guard (CanActivateFn) to prevent unauthenticated access to the dashboard route.
Security note: Storing JWTs in localStorage is convenient but vulnerable to XSS attacks. For production systems handling sensitive data, consider using HttpOnly, Secure cookies combined with short-lived access tokens and a refresh token rotation strategy.
13. Part 9 (Advanced): Scaling SignalR with Redis Backplane
If you deploy your ASP.NET Core backend across multiple server instances (for load balancing or high availability), SignalR connections will be distributed across those instances. By default, a message broadcast from one server instance won't reach clients connected to a different instance. This is solved using a backplane.
Step 9.1: Install the Redis Backplane Package
dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis
Step 9.2: Configure It in Program.cs
builder.Services.AddSignalR()
.AddStackExchangeRedis(builder.Configuration["Redis:ConnectionString"]!, options =>
{
options.Configuration.ChannelPrefix = StackExchange.Redis.RedisChannel.Literal("LiveDashboard");
});
With this in place, when MetricBroadcastService calls Clients.All.SendAsync(...) on one server instance, Redis ensures the message is relayed to all other instances, which then forward it to their own connected clients. This makes your dashboard horizontally scalable.
Step 9.3: When Do You Need This?
For a single-instance deployment (most small to mid-size applications), you do not need a backplane. Only add this complexity once you're running multiple instances behind a load balancer — typically in higher-traffic production environments.
14. Part 10 (Advanced): Performance Optimization
Step 10.1: Throttle High-Frequency Updates
If your real data source produces updates faster than the UI needs to render them (e.g., hundreds of updates per second), avoid pushing every single update to clients. Instead, batch or throttle on the backend — for example, aggregate updates and broadcast at a fixed interval (every 500ms–1s) using a buffer, rather than sending on every change.
On the Angular side, you can also throttle using RxJS:
import { throttleTime } from 'rxjs/operators';
this.signalrService.metric$
.pipe(throttleTime(300))
.subscribe(metric => this.upsertMetric(metric));
Step 10.2: Use OnPush Change Detection
For dashboard components receiving frequent updates, switch to ChangeDetectionStrategy.OnPush to avoid unnecessary re-renders across the entire component tree:
@Component({
selector: 'app-metric-card',
changeDetection: ChangeDetectionStrategy.OnPush,
...
})
Combined with immutable data patterns (creating new objects/arrays rather than mutating existing ones, as we did in the chart component), OnPush significantly reduces CPU usage on busy dashboards.
Step 10.3: Limit Chart History
Capping chart data to a fixed window (we used maxPoints = 30 earlier) prevents memory growth and keeps rendering fast, especially important if the dashboard runs unattended for hours or days (e.g., on an office wall display).
Step 10.4: Use trackBy for *ngFor
When rendering lists of metric cards or charts, always use trackBy to prevent Angular from destroying and recreating DOM elements unnecessarily on every update:
<app-metric-card *ngFor="let metric of metrics; trackBy: trackByName" ... ></app-metric-card>
trackByName(index: number, metric: MetricData): string {
return metric.metricName;
}
Step 10.5: Compress SignalR Messages (Advanced)
For very data-heavy dashboards, consider using MessagePack instead of the default JSON protocol for SignalR, which reduces payload size and improves throughput:
dotnet add package Microsoft.AspNetCore.SignalR.Protocols.MessagePack
npm install @microsoft/signalr-protocol-msgpack
Configure it on both server (AddSignalR().AddMessagePackProtocol()) and client (.withHubProtocol(new MessagePackHubProtocol())).
15. Part 11 (Advanced): Testing the Application
Step 11.1: Backend Unit Tests
Use xUnit to test your services in isolation. For example, test that DataGeneratorService.GenerateMetric returns values within the expected variance range:
[Fact]
public void GenerateMetric_ReturnsValueWithinVarianceRange()
{
var service = new DataGeneratorService();
var result = service.GenerateMetric("CPU", 50, 10);
Assert.InRange(result.Value, 40, 60);
Assert.Equal("CPU", result.MetricName);
}
Step 11.2: Integration Testing the Hub
ASP.NET Core supports integration testing using WebApplicationFactory combined with a real SignalR client connecting to an in-memory test server, allowing you to verify that messages broadcast from the hub are actually received by a connected client.
Step 11.3: Angular Unit Tests
Use Jasmine/Karma (or Jest, if configured) to test that MetricsService correctly upserts metrics:
it('should update existing metric value on new data', () => {
service['upsertMetric']({ metricName: 'CPU', value: 55, timestamp: '2026-01-01T00:00:00Z' });
service['upsertMetric']({ metricName: 'CPU', value: 70, timestamp: '2026-01-01T00:00:05Z' });
service.metrics$.subscribe(metrics => {
const cpu = metrics.find(m => m.metricName === 'CPU');
expect(cpu?.value).toBe(70);
});
});
Step 11.4: End-to-End Testing
Use Playwright or Cypress to verify the full flow: load the dashboard, confirm the initial snapshot renders, wait for a live update, and assert that displayed values change without a page reload.
16. Part 12 (Advanced): Deploying to Production
Step 16.1: Build the Angular App for Production
ng build --configuration production
This outputs static files to dist/live-dashboard-client, which can be served by any static file host or reverse proxy.
Step 16.2: Publish the ASP.NET Core Backend
dotnet publish -c Release -o ./publish
Step 16.3: Dockerize Both Applications
Backend Dockerfile:
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 80
ENTRYPOINT ["dotnet", "LiveDashboard.Api.dll"]
Frontend Dockerfile (served via Nginx):
FROM node:20 AS build
WORKDIR /app
COPY . .
RUN npm install && npm run build -- --configuration production
FROM nginx:alpine
COPY --from=build /app/dist/live-dashboard-client /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
A minimal nginx.conf should include a WebSocket-friendly proxy configuration if Nginx also handles routing to the backend, ensuring Upgrade and Connection headers are forwarded correctly for SignalR's WebSocket transport.
Step 16.4: docker-compose for Local Orchestration
version: '3.8'
services:
api:
build: ./LiveDashboard.Api
ports:
- "5000:80"
environment:
- ASPNETCORE_ENVIRONMENT=Production
client:
build: ./live-dashboard-client
ports:
- "4200:80"
depends_on:
- api
Step 16.5: Deploying to Azure
For Azure App Service deployments, deploy the ASP.NET Core backend to an App Service (with WebSockets enabled in the Configuration settings — this is required for SignalR to function correctly) and the Angular frontend to Azure Static Web Apps or a separate App Service serving static files. If you expect multiple backend instances, enable Azure SignalR Service instead of self-hosting SignalR, which handles scaling and connection management for you and removes the need for a Redis backplane.
Step 16.6: Deploying to IIS
If deploying to IIS, ensure the WebSocket Protocol feature is enabled in Windows Server roles/features, and that the ASP.NET Core Module v2 is installed. Without WebSocket support enabled at the IIS/server level, SignalR will silently fall back to long polling, which still works but is less efficient.
Step 16.7: HTTPS and Security Headers
In production, always serve both frontend and backend over HTTPS — mixed content (HTTPS frontend calling an HTTP backend) will be blocked by browsers, and WebSocket connections (wss://) require a secure context when the page itself is loaded over HTTPS.
17. Common Errors and Troubleshooting
Error: "Failed to complete negotiation with the server"
Usually caused by CORS misconfiguration or the backend not running. Confirm AllowCredentials() is set and that the origin matches exactly (including protocol and port).
Error: SignalR connects but no data arrives
Double-check that the event name in hubConnection.on('ReceiveMetric', ...) exactly matches the string used in SendAsync("ReceiveMetric", ...) on the backend — these are case-sensitive string matches, not type-checked.
Error: Connection works locally but fails after deployment
Almost always a missing WebSocket configuration at the hosting/proxy layer (IIS, Nginx, or a load balancer not configured for WebSocket passthrough).
Charts not updating despite new data arriving
Verify you are creating new array/object references when updating chart data (as shown in Step 5.3) rather than mutating existing objects in place — Angular and Chart.js both rely on reference changes to detect updates efficiently.
401 Unauthorized when connecting to the hub after adding JWT auth
Confirm the accessTokenFactory is correctly returning a valid, non-expired token, and that the OnMessageReceived handler on the backend is scoped to the /hubs path.
18. Best Practices Checklist
- Use SignalR for live updates; use REST only for the initial data snapshot.
- Always implement
.withAutomaticReconnect()and surface connection status in the UI. - Cap historical data arrays to avoid unbounded memory growth in long-running dashboard sessions.
- Use
OnPushchange detection andtrackByfor performance on data-heavy dashboards. - Secure both the REST API and the SignalR hub with
[Authorize]once authentication is added. - Enable WebSockets explicitly on your hosting environment (IIS, Azure App Service, Nginx).
- Use a Redis backplane or Azure SignalR Service only once you scale beyond a single backend instance.
- Throttle extremely high-frequency data sources before broadcasting to clients.
- Always test reconnection behavior by manually killing and restarting the backend during development.
19. Frequently Asked Questions (FAQ)
Q: Do I need SignalR, or can I just poll the API every few seconds?
Polling works for simple cases but is less efficient and introduces latency. SignalR provides true push-based real-time updates with lower overhead once a connection is established, making it the better choice for dashboards needing sub-second responsiveness.
Q: Can I use this approach with React or Vue instead of Angular?
Yes. The backend (ASP.NET Core + SignalR) is frontend-agnostic. Microsoft provides the same @microsoft/signalr JavaScript client that works with any frontend framework, not just Angular.
Q: What's the difference between SignalR and raw WebSockets?
SignalR is a higher-level abstraction built on top of WebSockets (with automatic fallback to other transports). It handles connection management, reconnection, and message serialization for you, whereas raw WebSockets require you to build all of that yourself.
Q: How do I handle dashboards with many different data types (not just numeric metrics)?
Use a more generic message envelope (e.g., a DashboardEvent with a type field and a polymorphic payload) instead of a single MetricData model, and route messages to different handlers/components based on type on the client side.
Q: Is SignalR suitable for very high-frequency data (thousands of updates per second)?
For extremely high-frequency data, consider batching updates on the server before broadcasting (as discussed in the performance section) rather than sending every single data point individually.
20. Conclusion
You've now built a complete, real-time live data dashboard from the ground up — starting with a simple ASP.NET Core Web API, layering in SignalR for true real-time push updates, building a reactive Angular frontend with live-updating charts, and progressing through advanced topics like authentication, scaling, performance optimization, and production deployment.
This architecture pattern — REST for initial state, SignalR for live updates, RxJS for reactive state management on the client — is a robust foundation you can extend to almost any real-time use case: IoT monitoring, financial dashboards, server health monitors, live order tracking, or collaborative applications.
From here, consider exploring related topics such as integrating a real database with Entity Framework Core instead of simulated data, adding role-based dashboards where different users see different metrics, or building alerting rules that trigger notifications when metric thresholds are crossed.
If you found this guide helpful, consider bookmarking it as a reference architecture for your next real-time Angular + ASP.NET Core project.
Top comments (1)
Nice I love this post, I need this post because I work on this type of project right now.