DEV Community

Cover image for Building a Live Data Dashboard with Angular and ASP.NET Core: A Complete Step-by-Step Guide (Beginner to Advanced)
Ravi Vishwakarma
Ravi Vishwakarma

Posted on

Building a Live Data Dashboard with Angular and ASP.NET Core: A Complete Step-by-Step Guide (Beginner to Advanced)

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

  1. Introduction
  2. What You Will Build
  3. Prerequisites
  4. Understanding the Architecture
  5. Part 1 (Beginner): Setting Up the ASP.NET Core Backend
  6. Part 2 (Beginner): Adding SignalR for Real-Time Communication
  7. Part 3 (Beginner): Setting Up the Angular Frontend
  8. Part 4 (Intermediate): Connecting Angular to SignalR
  9. Part 5 (Intermediate): Building the Dashboard UI with Live Charts
  10. Part 6 (Intermediate): Styling the Dashboard
  11. Part 7 (Advanced): Handling Reconnection and Error States
  12. Part 8 (Advanced): Adding Authentication with JWT
  13. Part 9 (Advanced): Scaling SignalR with Redis Backplane
  14. Part 10 (Advanced): Performance Optimization
  15. Part 11 (Advanced): Testing the Application
  16. Part 12 (Advanced): Deploying to Production
  17. Common Errors and Troubleshooting
  18. Best Practices Checklist
  19. Frequently Asked Questions (FAQ)
  20. 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)│
                                                              └──────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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; }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
            };
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Register this service in Program.cs (we'll come back to Program.cs again shortly):

builder.Services.AddSingleton<DataGeneratorService>();
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    });
});
Enter fullscreen mode Exit fullscreen mode

And later, in the middleware pipeline:

app.UseCors("AllowAngularDev");
Enter fullscreen mode Exit fullscreen mode

Tip: AllowCredentials() is required for SignalR connections to work correctly with CORS.

Step 1.7: Run and Verify

Run the backend with:

dotnet run
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Register the background service in Program.cs:

builder.Services.AddHostedService<MetricBroadcastService>();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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 in SendAsync("ReceiveMetric", ...). These names must match exactly.
  • We use an RxJS Subject to 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
};
Enter fullscreen mode Exit fullscreen mode

And src/environments/environment.prod.ts for production builds:

export const environment = {
  production: true,
  apiBaseUrl: 'https://your-production-api.com'
};
Enter fullscreen mode Exit fullscreen mode

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()));
  }
}
Enter fullscreen mode Exit fullscreen mode

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()
  ]
};
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 = '';
}
Enter fullscreen mode Exit fullscreen mode

metric-card.component.html:

<div class="metric-card">
  <h3>{{ name }}</h3>
  <p class="value">{{ value }}</p>
  <small>Updated: {{ timestamp | date:'mediumTime' }}</small>
</div>
Enter fullscreen mode Exit fullscreen mode

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())
  ]
};
Enter fullscreen mode Exit fullscreen mode

Step 5.3: Create a Live Chart Component

Generate a new component:

ng generate component components/live-chart
Enter fullscreen mode Exit fullscreen mode

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] }]
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

live-chart.component.html:

<div class="chart-wrapper">
  <h4>{{ label }} Trend</h4>
  <canvas
    baseChart
    [data]="lineChartData"
    [options]="lineChartOptions"
    type="line">
  </canvas>
</div>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Add the middleware:

app.UseAuthentication();
app.UseAuthorization();
Enter fullscreen mode Exit fullscreen mode

Secure the hub and controller:

[Authorize]
public class DashboardHub : Hub { ... }

[Authorize]
[ApiController]
[Route("api/[controller]")]
public class MetricsController : ControllerBase { ... }
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

Register it in app.config.ts:

provideHttpClient(withInterceptors([authInterceptor]))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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");
    });
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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,
  ...
})
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
trackByName(index: number, metric: MetricData): string {
  return metric.metricName;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
  });
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 OnPush change detection and trackBy for 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)

Collapse
 
anna_hajare_9520a4c4d15ee profile image
Anna Hajare

Nice I love this post, I need this post because I work on this type of project right now.