Angular SignalR Real-Time Notifications – Complete Guide | FreeLearning365.com

Angular SignalR Real-Time Notifications – Complete Guide | FreeLearning365.com
FreeLearning365.com 🔔 Real-Time
⚡ SignalR

Angular SignalR Real-Time Notifications
Complete Guide with Source Code

“Instant updates, seamless user experience” — This guide provides a production-ready implementation of real-time notifications using Angular and ASP.NET Core SignalR. Learn to set up hubs, manage connections, send user-specific notifications, handle authentication, and implement best practices for reliability and performance.

🔍 1. Introduction

SignalR is a real-time communication library for ASP.NET Core that enables server-to-client push notifications. It automatically falls back to WebSockets, Server-Sent Events, or long polling, making it the ideal solution for real-time features like notifications, chat, live dashboards, and collaborative editing.

In this guide, we'll build a complete notification system where:

  • Users receive real-time notifications without refreshing the page.
  • Notifications are user-specific (private).
  • Authentication is integrated with JWT.
  • Connection management handles reconnection gracefully.
  • Notifications are displayed with a toast UI.
💡 Quick fact: SignalR supports WebSockets, which provide full-duplex communication, but gracefully falls back to other transports if WebSockets aren't available.

🖥️ 2. ASP.NET Core Backend

Let's build the SignalR Hub and configure the backend for real-time notifications.

📌 Step 1: Install SignalR NuGet Package

dotnet add package Microsoft.AspNetCore.SignalR

📌 Step 2: Create Notification Model

// Models/NotificationDto.cs public class NotificationDto { public string Id { get; set; } = Guid.NewGuid().ToString(); public string Title { get; set; } = string.Empty; public string Message { get; set; } = string.Empty; public string Type { get; set; } = "info"; // info, success, warning, error public string? Link { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public bool IsRead { get; set; } public string? UserId { get; set; } // Target user (null = broadcast) }

📌 Step 3: Create Notification Hub

// Hubs/NotificationHub.cs using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; [Authorize] public class NotificationHub : Hub { private readonly ILogger<NotificationHub> _logger; private static readonly Dictionary<string, HashSet<string>> _userConnections = new(); public NotificationHub(ILogger<NotificationHub> logger) { _logger = logger; } // ✅ Override OnConnectedAsync to track users public override Task OnConnectedAsync() { var userId = Context.User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; if (!string.IsNullOrEmpty(userId)) { lock (_userConnections) { if (!_userConnections.ContainsKey(userId)) { _userConnections[userId] = new HashSet<string>(); } _userConnections[userId].Add(Context.ConnectionId); } _logger.LogInformation($"User {userId} connected. ConnectionId: {Context.ConnectionId}"); } return base.OnConnectedAsync(); } // ✅ Override OnDisconnectedAsync to clean up public override Task OnDisconnectedAsync(Exception? exception) { var userId = Context.User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; if (!string.IsNullOrEmpty(userId)) { lock (_userConnections) { if (_userConnections.ContainsKey(userId)) { _userConnections[userId].Remove(Context.ConnectionId); if (_userConnections[userId].Count == 0) { _userConnections.Remove(userId); } } } _logger.LogInformation($"User {userId} disconnected. ConnectionId: {Context.ConnectionId}"); } return base.OnDisconnectedAsync(exception); } // ✅ Send notification to a specific user public async Task SendNotificationToUser(string userId, NotificationDto notification) { notification.UserId = userId; await Clients.User(userId).SendAsync("ReceiveNotification", notification); _logger.LogInformation($"Notification sent to user {userId}: {notification.Title}"); } // ✅ Send notification to all connected users (broadcast) public async Task BroadcastNotification(NotificationDto notification) { await Clients.All.SendAsync("ReceiveNotification", notification); _logger.LogInformation($"Broadcast notification: {notification.Title}"); } // ✅ Send notification to a group public async Task SendNotificationToGroup(string groupName, NotificationDto notification) { await Clients.Group(groupName).SendAsync("ReceiveNotification", notification); _logger.LogInformation($"Notification sent to group {groupName}: {notification.Title}"); } // ✅ Join a group public async Task JoinGroup(string groupName) { await Groups.AddToGroupAsync(Context.ConnectionId, groupName); _logger.LogInformation($"Connection {Context.ConnectionId} joined group {groupName}"); } // ✅ Leave a group public async Task LeaveGroup(string groupName) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName); _logger.LogInformation($"Connection {Context.ConnectionId} left group {groupName}"); } // ✅ Get online users (for admin) public Task<string[]> GetOnlineUsers() { lock (_userConnections) { return Task.FromResult(_userConnections.Keys.ToArray()); } } // ✅ Get user's connection count public Task<int> GetUserConnectionCount(string userId) { lock (_userConnections) { return Task.FromResult(_userConnections.ContainsKey(userId) ? _userConnections[userId].Count : 0); } } }

📌 Step 4: Configure SignalR in Program.cs

// Program.cs var builder = WebApplication.CreateBuilder(args); // Add SignalR builder.Services.AddSignalR(); // Add CORS for Angular builder.Services.AddCors(options => { options.AddPolicy("AllowAngular", policy => { policy.WithOrigins("http://localhost:4200") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); // ✅ Required for SignalR WebSockets }); }); var app = builder.Build(); app.UseCors("AllowAngular"); app.UseAuthentication(); app.UseAuthorization(); // Map SignalR Hub app.MapHub<NotificationHub>("/notificationHub"); app.Run();

📌 Step 5: Send Notification from Anywhere

// NotificationService.cs public class NotificationService { private readonly IHubContext<NotificationHub> _hubContext; public NotificationService(IHubContext<NotificationHub> hubContext) { _hubContext = hubContext; } public async Task SendNotification(string userId, string title, string message, string type = "info") { var notification = new NotificationDto { Title = title, Message = message, Type = type, CreatedAt = DateTime.UtcNow, UserId = userId }; await _hubContext.Clients.User(userId).SendAsync("ReceiveNotification", notification); } public async Task Broadcast(string title, string message, string type = "info") { var notification = new NotificationDto { Title = title, Message = message, Type = type, CreatedAt = DateTime.UtcNow }; await _hubContext.Clients.All.SendAsync("ReceiveNotification", notification); } }
✅ Done: The backend is ready! Now let's build the Angular client.

🅰️ 3. Angular Frontend

Now let's build the Angular client to connect to SignalR and receive notifications.

📌 Step 1: Install SignalR Client Package

npm install @microsoft/signalr

📌 Step 2: Create Notification Service

// notification.service.ts import { Injectable } from '@angular/core'; import * as signalR from '@microsoft/signalr'; import { Subject, Observable } from 'rxjs'; export interface Notification { id: string; title: string; message: string; type: 'info' | 'success' | 'warning' | 'error'; link?: string; createdAt: Date; isRead: boolean; } @Injectable({ providedIn: 'root' }) export class NotificationService { private hubConnection!: signalR.HubConnection; private notificationSubject = new Subject<Notification>(); private connectionStatusSubject = new Subject<signalR.HubConnectionState>(); private readonly hubUrl = 'https://localhost:5001/notificationHub'; constructor() { this.initializeConnection(); } private initializeConnection(): void { this.hubConnection = new signalR.HubConnectionBuilder() .withUrl(this.hubUrl, { accessTokenFactory: () => localStorage.getItem('access_token') || '' }) .withAutomaticReconnect([0, 2000, 5000, 10000, 20000]) // Exponential backoff .configureLogging(signalR.LogLevel.Information) .build(); // ✅ Listen for notifications this.hubConnection.on('ReceiveNotification', (notification: Notification) => { notification.createdAt = new Date(notification.createdAt); this.notificationSubject.next(notification); }); // ✅ Track connection status this.hubConnection.onreconnecting(() => { this.connectionStatusSubject.next(signalR.HubConnectionState.Reconnecting); }); this.hubConnection.onreconnected(() => { this.connectionStatusSubject.next(signalR.HubConnectionState.Connected); }); this.hubConnection.onclose(() => { this.connectionStatusSubject.next(signalR.HubConnectionState.Disconnected); }); } // ✅ Start connection async start(): Promise<void> { if (this.hubConnection.state === signalR.HubConnectionState.Disconnected) { try { await this.hubConnection.start(); this.connectionStatusSubject.next(signalR.HubConnectionState.Connected); console.log('SignalR connection established.'); } catch (error) { console.error('SignalR connection failed:', error); this.connectionStatusSubject.next(signalR.HubConnectionState.Disconnected); // Retry after 5 seconds setTimeout(() => this.start(), 5000); } } } // ✅ Stop connection async stop(): Promise<void> { if (this.hubConnection.state !== signalR.HubConnectionState.Disconnected) { try { await this.hubConnection.stop(); this.connectionStatusSubject.next(signalR.HubConnectionState.Disconnected); console.log('SignalR connection stopped.'); } catch (error) { console.error('Failed to stop SignalR:', error); } } } // ✅ Join a group async joinGroup(groupName: string): Promise<void> { await this.hubConnection.invoke('JoinGroup', groupName); } // ✅ Leave a group async leaveGroup(groupName: string): Promise<void> { await this.hubConnection.invoke('LeaveGroup', groupName); } // ✅ Send notification (from client to server) async sendNotificationToUser(userId: string, notification: Partial<Notification>): Promise<void> { await this.hubConnection.invoke('SendNotificationToUser', userId, notification); } // ✅ Broadcast notification async broadcastNotification(notification: Partial<Notification>): Promise<void> { await this.hubConnection.invoke('BroadcastNotification', notification); } // ✅ Get connection status getConnectionStatus(): Observable<signalR.HubConnectionState> { return this.connectionStatusSubject.asObservable(); } // ✅ Get notification stream getNotifications(): Observable<Notification> { return this.notificationSubject.asObservable(); } // ✅ Check if connected isConnected(): boolean { return this.hubConnection.state === signalR.HubConnectionState.Connected; } // ✅ Get connection state getConnectionState(): signalR.HubConnectionState { return this.hubConnection.state; } }

📌 Step 3: Notification Toast Component

// notification-toast.component.ts @Component({ selector: 'app-notification-toast', templateUrl: './notification-toast.component.html', styleUrls: ['./notification-toast.component.css'], standalone: true, imports: [CommonModule] }) export class NotificationToastComponent implements OnInit { notifications: Notification[] = []; private toastTimeout: any; constructor(private notificationService: NotificationService) {} ngOnInit(): void { this.notificationService.getNotifications().subscribe(notification => { this.notifications.push(notification); this.showToast(notification); // Auto dismiss after 5 seconds setTimeout(() => { this.removeNotification(notification.id); }, 5000); }); } private showToast(notification: Notification): void { // Show toast using a toast library or custom UI console.log(`[${notification.type}] ${notification.title}: ${notification.message}`); // In a real app, use ngx-toastr, Angular Material snackbar, or custom toast } removeNotification(id: string): void { this.notifications = this.notifications.filter(n => n.id !== id); } getIcon(type: string): string { const icons: Record<string, string> = { info: 'ℹ️', success: '✅', warning: '⚠️', error: '❌' }; return icons[type] || 'ℹ️'; } getBgColor(type: string): string { const colors: Record<string, string> = { info: '#3b82f6', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' }; return colors[type] || '#3b82f6'; } }

📌 Step 4: App Initialization

// app.component.ts @Component({ selector: 'app-root', templateUrl: './app.component.html', standalone: true, imports: [CommonModule, RouterOutlet, NotificationToastComponent] }) export class AppComponent implements OnInit { connectionStatus = signalR.HubConnectionState.Disconnected; isConnected = false; constructor(private notificationService: NotificationService) {} async ngOnInit(): Promise<void> { // Start SignalR connection after authentication this.notificationService.getConnectionStatus().subscribe(status => { this.connectionStatus = status; this.isConnected = status === signalR.HubConnectionState.Connected; }); // Start connection if user is authenticated if (localStorage.getItem('access_token')) { await this.notificationService.start(); } } async onLogin(): Promise<void> { // After successful login, start SignalR await this.notificationService.start(); } async onLogout(): Promise<void> { await this.notificationService.stop(); } }

📌 Step 5: Toast Template

<!-- notification-toast.component.html --> <div class="toast-container"> <div *ngFor="let notif of notifications" class="toast-item" [style.border-left-color]="getBgColor(notif.type)" [class.entering]="true"> <span class="toast-icon">{{ getIcon(notif.type) }}</span> <div class="toast-content"> <div class="toast-title">{{ notif.title }}</div> <div class="toast-message">{{ notif.message }}</div> <div class="toast-time">{{ notif.createdAt | date:'shortTime' }}</div> </div> <button class="toast-close" (click)="removeNotification(notif.id)">✕</button> </div> </div>

🔐 4. Authentication & Security

SignalR integrates seamlessly with ASP.NET Core authentication. Here's how to secure your hub and pass JWT tokens from Angular.

📌 Backend: Configure JWT Authentication

// Program.cs builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { 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("/notificationHub")) { context.Token = accessToken; } return Task.CompletedTask; } }; 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(builder.Configuration["Jwt:Key"]) ) }; });

📌 Frontend: Send Token with Connection

The accessTokenFactory in the HubConnectionBuilder automatically adds the token to the connection request. This works for WebSockets and HTTP fallbacks.

.withUrl(this.hubUrl, { accessTokenFactory: () => localStorage.getItem('access_token') || '' })

📌 User Identification

SignalR uses IUserIdProvider to map connections to users. The default implementation uses NameIdentifier claim. Ensure your JWT includes this claim.

var claims = new[] { new Claim(ClaimTypes.NameIdentifier, user.Id), new Claim(ClaimTypes.Email, user.Email), new Claim(ClaimTypes.Name, user.UserName) };

👥 5. Groups & User-Specific Notifications

SignalR groups allow you to send notifications to a subset of users. This is useful for team notifications, project updates, or role-based messaging.

📌 Joining Groups on Connection

You can automatically add users to groups based on their role or team:

// NotificationHub.cs - OnConnectedAsync public override async Task OnConnectedAsync() { var userId = Context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; var roles = Context.User?.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value); if (roles != null) { foreach (var role in roles) { await Groups.AddToGroupAsync(Context.ConnectionId, $"role-{role}"); } } // Add user to a personal group if (!string.IsNullOrEmpty(userId)) { await Groups.AddToGroupAsync(Context.ConnectionId, $"user-{userId}"); } await base.OnConnectedAsync(); }

📌 Sending to Groups

// Send to a specific role await Clients.Group("role-admin").SendAsync("ReceiveNotification", notification); // Send to a specific user (via user group) await Clients.Group($"user-{userId}").SendAsync("ReceiveNotification", notification);

📌 Angular: Join Group on Login

// After authentication await this.notificationService.joinGroup('role-admin');

🔄 6. Reconnection & Error Handling

Network interruptions are inevitable. SignalR's automatic reconnection ensures your app stays connected with minimal disruption.

📌 Automatic Reconnect Configuration

.withAutomaticReconnect([0, 2000, 5000, 10000, 20000])

This specifies the delay in milliseconds between reconnect attempts. The array defines exponential backoff: 0ms, 2s, 5s, 10s, 20s, then it continues with the last value.

📌 Handling Connection Events

this.hubConnection.onreconnecting((error) => { console.log('Reconnecting...', error); this.connectionStatusSubject.next(HubConnectionState.Reconnecting); // Show "Reconnecting..." UI }); this.hubConnection.onreconnected((connectionId) => { console.log('Reconnected with ID:', connectionId); this.connectionStatusSubject.next(HubConnectionState.Connected); // Hide reconnecting UI, resubscribe to groups if needed }); this.hubConnection.onclose((error) => { console.log('Connection closed:', error); this.connectionStatusSubject.next(HubConnectionState.Disconnected); // Try reconnecting manually if needed setTimeout(() => this.start(), 5000); });

📌 UI Status Indicator

<!-- Show connection status --> <div class="connection-status" [class.connected]="isConnected" [class.reconnecting]="isReconnecting"> <span class="status-dot"></span> {{ connectionStatusLabel }} </div>
✅ Best practice: Always show connection status to the user and provide a manual reconnect button for edge cases.

🔧 7. Advanced Patterns

Here are some advanced patterns for production-ready SignalR applications.

📌 Scaling with Azure SignalR Service

For production, use Azure SignalR Service to handle many concurrent connections and scale across multiple servers:

builder.Services.AddSignalR().AddAzureSignalR(options => { options.Endpoints = new ServiceEndpoint[] { new ServiceEndpoint(builder.Configuration["AzureSignalR:ConnectionString"]) }; });

📌 Message Persistence

Store notifications in a database so users can see past notifications after reconnecting:

// Store notification in DB public async Task SendNotificationToUser(string userId, NotificationDto notification) { // Save to database await _notificationRepository.SaveAsync(notification); // Send real-time await Clients.User(userId).SendAsync("ReceiveNotification", notification); } // Client can load historical notifications [HttpGet("api/notifications")] public async Task<IEnumerable<NotificationDto>> GetNotifications(int count = 50) { return await _notificationRepository.GetRecentAsync(User.GetUserId(), count); }

📌 Typed Hubs

Use typed hubs for strongly-typed client methods:

// INotificationClient.cs public interface INotificationClient { Task ReceiveNotification(NotificationDto notification); Task ReceiveBulkNotifications(List<NotificationDto> notifications); } // NotificationHub.cs public class NotificationHub : Hub<INotificationClient> { public async Task SendNotificationToUser(string userId, NotificationDto notification) { await Clients.User(userId).ReceiveNotification(notification); } }
❓ Frequently Asked Questions

🏆 9. Best Practices

  • Use WebSockets when possible: SignalR automatically negotiates the best transport, but WebSockets provide the lowest latency.
  • Enable automatic reconnection: Use withAutomaticReconnect() with exponential backoff.
  • Authenticate all hubs: Use [Authorize] on hubs to prevent unauthorized access.
  • Send tokens via query string: For WebSockets, tokens are passed via query string or the accessTokenFactory.
  • Clean up on disconnect: Override OnDisconnectedAsync to clean up group memberships.
  • Use groups for scalability: Groups reduce the number of messages sent to individual connections.
  • Limit message size: SignalR has a default message size limit. Use AddSignalR(options => options.MaximumReceiveMessageSize) to adjust.
  • Monitor with logging: Enable logging on both client and server for debugging.
  • Use IHubContext for server-to-client communication: This allows you to send notifications from anywhere in your app.
  • Implement connection status UI: Always show the user their connection status.
  • Use Azure SignalR Service for production: It handles scaling, load balancing, and high availability.
✅ Final thought: SignalR is a powerful tool for building real-time features. With the implementation in this guide, you can build a robust notification system that scales from small projects to enterprise applications. Remember to monitor performance, handle errors gracefully, and always prioritize security.

Post a Comment

0 Comments