Real-Time Applications with SignalR in ASP.NET Core
Building Responsive Web Applications with Bidirectional Communication
In today's digital landscape, users expect instant updates and seamless interactions with web applications. Traditional request-response patterns often fall short of delivering truly responsive experiences. Enter SignalR, Microsoft's library for adding real-time web functionality to ASP.NET Core applications.
Looking to secure your real-time applications? Be sure to review our guide on Securing Your ASP.NET Applications: Top Security Practices.
Understanding Real-Time Web Communications
Before diving into SignalR, let's understand what makes an application "real-time" and explore the technical approaches to achieving this functionality.
The Evolution of Web Communication
Traditional web applications follow a request-response pattern where the client initiates all communication. This approach has limitations for applications that need to:
Push server updates immediately to clients
Maintain persistent connections for rapid interaction
Enable client-to-client communication
Minimize latency in data exchanges
Several techniques have emerged to address these limitations:
Polling: Clients periodically request updates (inefficient, high latency)
Long Polling: Clients hold connections open until the server responds (better, but still problematic)
Server-Sent Events (SSE): Server pushes data to clients (one-way only)
WebSockets: Full-duplex communication channel (ideal, but complex to implement)
Enter SignalR
SignalR abstracts the complexities of real-time communication, automatically selecting the best transport method available:
WebSockets (preferred)
Server-Sent Events
Long Polling (fallback)
This abstraction allows developers to focus on application logic rather than connection management and browser compatibility.
Getting Started with SignalR
Let's implement a basic SignalR application to understand its core components.
Setting Up Your Project
First, add the SignalR package to your ASP.NET Core application:
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
Next, configure SignalR in your Program.cs
:
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddSignalR();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
// Configure SignalR hub endpoints
app.MapHub<ChatHub>("/chatHub");
app.Run();
Creating a Hub
The hub is the core component of SignalR, handling client connections and method invocations:
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
public override async Task OnConnectedAsync()
{
await Clients.All.SendAsync("UserConnected", Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await Clients.All.SendAsync("UserDisconnected", Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}
}
Client-Side Implementation
Now let's create the client-side JavaScript to connect to our hub:
"use strict";
// Create connection
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.configureLogging(signalR.LogLevel.Information)
.build();
// Start the connection
async function startConnection() {
try {
await connection.start();
console.log("SignalR connected.");
} catch (err) {
console.log(err);
setTimeout(startConnection, 5000);
}
}
// Reconnect if the connection is lost
connection.onclose(async () => {
await startConnection();
});
// Handle receiving messages
connection.on("ReceiveMessage", function (user, message) {
const encodedUser = user;
const encodedMsg = message;
const listItem = document.createElement("li");
listItem.textContent = `${encodedUser}: ${encodedMsg}`;
document.getElementById("messagesList").appendChild(listItem);
});
// Handle user connected
connection.on("UserConnected", function (connectionId) {
const listItem = document.createElement("li");
listItem.textContent = `User connected: ${connectionId}`;
document.getElementById("usersList").appendChild(listItem);
});
// Handle user disconnected
connection.on("UserDisconnected", function (connectionId) {
const listItem = document.createElement("li");
listItem.textContent = `User disconnected: ${connectionId}`;
document.getElementById("usersList").appendChild(listItem);
});
// Send a message when the form is submitted
document.getElementById("sendButton").addEventListener("click", function (event) {
const user = document.getElementById("userInput").value;
const message = document.getElementById("messageInput").value;
connection.invoke("SendMessage", user, message).catch(function (err) {
console.error(err.toString());
});
event.preventDefault();
});
// Start the connection
startConnection();
Include the SignalR client library in your HTML:
<script src="~/lib/microsoft/signalr/dist/browser/signalr.min.js"></script>
Advanced SignalR Techniques
Let's explore more advanced concepts for building robust real-time applications.
Groups and User Management
SignalR allows organizing connections into groups for targeted messaging:
public class ChatHub : Hub
{
public async Task JoinRoom(string roomName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
await Clients.Group(roomName).SendAsync("UserJoinedRoom",
Context.ConnectionId, roomName);
}
public async Task LeaveRoom(string roomName)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName);
await Clients.Group(roomName).SendAsync("UserLeftRoom",
Context.ConnectionId, roomName);
}
public async Task SendMessageToGroup(string roomName, string user, string message)
{
await Clients.Group(roomName).SendAsync("ReceiveMessage", user, message);
}
}
User Identification and Authentication
Integrate SignalR with ASP.NET Core Identity for authenticated real-time communication:
[Authorize]
public class SecureChatHub : Hub
{
public override async Task OnConnectedAsync()
{
var username = Context.User.Identity.Name;
await Clients.All.SendAsync("UserConnected", username);
await base.OnConnectedAsync();
}
public async Task SendPrivateMessage(string receiverUsername, string message)
{
var senderUsername = Context.User.Identity.Name;
// Get user ID by username (assuming you have user management service)
var userIdLookupService = Context.GetHttpContext().RequestServices
.GetRequiredService<IUserIdLookupService>();
var receiverId = await userIdLookupService.GetUserIdByNameAsync(receiverUsername);
if (receiverId != null)
{
// Send to specific user by their user ID
await Clients.User(receiverId).SendAsync(
"ReceivePrivateMessage", senderUsername, message);
// Also send back to the sender
await Clients.Caller.SendAsync(
"ReceivePrivateMessage", $"You to {receiverUsername}", message);
}
}
}
Configure your application to use the authenticated user's name as the connection identifier:
services.AddSignalR().AddHubOptions<SecureChatHub>(options =>
{
options.EnableDetailedErrors = true;
})
.AddUserIdProvider<NameUserIdProvider>();
public class NameUserIdProvider : IUserIdProvider
{
public string GetUserId(HubConnectionContext connection)
{
return connection.User?.Identity?.Name;
}
}
Real-Time Notifications System
Implement a notifications system to keep users informed about application events:
public class NotificationHub : Hub
{
public async Task SendNotificationToAll(string message)
{
await Clients.All.SendAsync("ReceiveNotification", message);
}
public async Task SendNotificationToUser(string userId, string message)
{
await Clients.User(userId).SendAsync("ReceiveNotification", message);
}
}
Use this hub from your services:
public class OrderService
{
private readonly IHubContext<NotificationHub> _notificationHub;
public OrderService(IHubContext<NotificationHub> notificationHub)
{
_notificationHub = notificationHub;
}
public async Task ProcessOrderAsync(Order order)
{
// Process order logic
// ...
// Notify the customer
await _notificationHub.Clients.User(order.CustomerId)
.SendAsync("ReceiveNotification",
$"Your order #{order.OrderNumber} has been processed!");
// Notify administrators
await _notificationHub.Clients.Group("Administrators")
.SendAsync("ReceiveNotification",
$"Order #{order.OrderNumber} for customer {order.CustomerId} has been processed.");
}
}
Real-World Application Examples
Real-Time Dashboard
Implement a live dashboard that updates automatically:
public class DashboardHub : Hub
{
private readonly IMetricsService _metricsService;
private Timer _timer;
public DashboardHub(IMetricsService metricsService)
{
_metricsService = metricsService;
}
public override Task OnConnectedAsync()
{
// If this is the first connection, start the update timer
if (_timer == null)
{
_timer = new Timer(BroadcastMetrics, null, 0, 5000); // Update every 5 seconds
}
return base.OnConnectedAsync();
}
private async void BroadcastMetrics(object state)
{
try
{
var metrics = await _metricsService.GetCurrentMetricsAsync();
await Clients.All.SendAsync("UpdateDashboard", metrics);
}
catch (Exception ex)
{
// Log error
}
}
}
Collaborative Editing
Create a collaborative document editor:
public class DocumentHub : Hub
{
private readonly IDocumentRepository _documentRepository;
public DocumentHub(IDocumentRepository documentRepository)
{
_documentRepository = documentRepository;
}
public async Task JoinDocument(string documentId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, documentId);
var document = await _documentRepository.GetDocumentAsync(documentId);
await Clients.Caller.SendAsync("LoadDocument", document);
await Clients.OthersInGroup(documentId).SendAsync(
"UserJoined", Context.User.Identity.Name);
}
public async Task UpdateDocumentText(string documentId, string text)
{
await _documentRepository.UpdateDocumentTextAsync(documentId, text);
await Clients.OthersInGroup(documentId).SendAsync(
"DocumentUpdated", text, Context.User.Identity.Name);
}
public async Task UpdateCursorPosition(string documentId, int position)
{
await Clients.OthersInGroup(documentId).SendAsync(
"UpdateUserCursor", Context.User.Identity.Name, position);
}
}
Real-Time Gaming
Implement a multiplayer game with real-time interactions:
public class GameHub : Hub
{
private static Dictionary<string, GameRoom> _gameRooms =
new Dictionary<string, GameRoom>();
public async Task CreateGame(string gameId)
{
if (!_gameRooms.ContainsKey(gameId))
{
_gameRooms[gameId] = new GameRoom();
await Groups.AddToGroupAsync(Context.ConnectionId, gameId);
await Clients.Caller.SendAsync("GameCreated", gameId);
}
else
{
await Clients.Caller.SendAsync("GameExists", gameId);
}
}
public async Task JoinGame(string gameId)
{
if (_gameRooms.ContainsKey(gameId))
{
await Groups.AddToGroupAsync(Context.ConnectionId, gameId);
var playerId = Context.ConnectionId;
_gameRooms[gameId].Players.Add(playerId);
await Clients.Group(gameId).SendAsync("PlayerJoined", playerId);
await Clients.Caller.SendAsync("GameState", _gameRooms[gameId]);
}
}
public async Task MakeMove(string gameId, GameMove move)
{
if (_gameRooms.ContainsKey(gameId))
{
_gameRooms[gameId].ApplyMove(move);
await Clients.Group(gameId).SendAsync(
"MoveMade", Context.ConnectionId, move);
if (_gameRooms[gameId].CheckWinCondition())
{
await Clients.Group(gameId).SendAsync(
"GameOver", Context.ConnectionId);
}
}
}
}
Performance and Scaling Considerations
Connection Management
As your application grows, managing SignalR connections becomes critical:
services.AddSignalR(options =>
{
options.MaximumReceiveMessageSize = 102400; // 100 KB
options.StreamBufferCapacity = 10;
options.EnableDetailedErrors = true;
})
.AddHubOptions<ChatHub>(options =>
{
options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
options.KeepAliveInterval = TimeSpan.FromSeconds(15);
});
Scaling with Redis Backplane
For multi-server deployments, use Redis to coordinate SignalR instances:
services.AddSignalR()
.AddStackExchangeRedis(options =>
{
options.Configuration = Configuration.GetConnectionString("Redis");
options.ConfigurationOptions = new StackExchange.Redis.ConfigurationOptions
{
AbortOnConnectFail = false
};
});
Message Size Optimization
Be mindful of message sizes and frequency:
// Instead of sending full objects, send only what's needed
public async Task UpdateDashboard()
{
var metrics = await _metricsService.GetCurrentMetricsAsync();
// Only send the data that's changed
var deltaUpdates = metrics.Where(m => m.HasChanged).ToList();
if (deltaUpdates.Any())
{
await Clients.All.SendAsync("UpdateDashboard", deltaUpdates);
}
}
Best Practices for SignalR Development
Connection Management
Implement reconnection strategies
Handle connection events properly
Use connection IDs carefully
// Client-side reconnection with exponential backoff
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.withAutomaticReconnect([0, 2000, 10000, 30000]) // milliseconds
.build();
// Connection lifecycle events
connection.onreconnecting(error => {
const status = document.getElementById("connectionStatus");
status.textContent = "Reconnecting...";
status.className = "reconnecting";
});
connection.onreconnected(connectionId => {
const status = document.getElementById("connectionStatus");
status.textContent = "Connected";
status.className = "connected";
// Re-join groups or refresh state as needed
connection.invoke("JoinGroup", currentGroup);
});
connection.onclose(error => {
const status = document.getElementById("connectionStatus");
status.textContent = "Disconnected";
status.className = "disconnected";
// Handle complete connection loss
displayReconnectButton();
});
Error Handling
Implement proper error handling on both client and server
Log errors appropriately
public async Task SendMessage(string user, string message)
{
try
{
// Validate inputs
if (string.IsNullOrEmpty(user) || string.IsNullOrEmpty(message))
{
throw new HubException("User and message cannot be empty");
}
// Process and broadcast message
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
catch (Exception ex)
{
// Log the error
_logger.LogError(ex, "Error sending message");
// Notify caller of the error
await Clients.Caller.SendAsync("ErrorOccurred", "Failed to send message");
// Rethrow as HubException to send to the client
throw new HubException("An error occurred when sending the message");
}
}
Security Considerations
Authenticate and authorize connections
Validate all incoming data
Protect against message flooding
[Authorize]
public class SecureHub : Hub
{
private readonly IThrottlingService _throttlingService;
public SecureHub(IThrottlingService throttlingService)
{
_throttlingService = throttlingService;
}
public override async Task OnConnectedAsync()
{
var username = Context.User.Identity.Name;
var isAdmin = Context.User.IsInRole("Administrator");
if (isAdmin)
{
await Groups.AddToGroupAsync(Context.ConnectionId, "Administrators");
}
await base.OnConnectedAsync();
}
public async Task SendMessage(string message)
{
var username = Context.User.Identity.Name;
// Check for message flooding
if (!await _throttlingService.AllowMessageAsync(username))
{
throw new HubException("Message rate limit exceeded. Please slow down.");
}
// Validate and sanitize the message
message = SanitizeMessage(message);
await Clients.All.SendAsync("ReceiveMessage", username, message);
}
private string SanitizeMessage(string message)
{
// Implement appropriate sanitization logic
return System.Web.HttpUtility.HtmlEncode(message);
}
}
Common Pitfalls and Solutions
Problem: Messages Not Being Received
Solution: Check connection state and ensure proper hub method invocation:
// Always check connection state before sending
function sendMessage() {
if (connection.state === signalR.HubConnectionState.Connected) {
connection.invoke("SendMessage", user, message)
.catch(err => console.error(err));
} else {
console.log("Cannot send message: connection not in 'Connected' state.");
displayConnectionError();
}
}
Problem: Scaling Issues
Solution: Implement proper backplane and optimize message patterns:
// Use a scaleout backplane like Redis
services.AddSignalR().AddStackExchangeRedis(Configuration.GetConnectionString("Redis"));
// Optimize by sending targeted messages instead of broadcasting
public async Task UpdateRecord(string recordId, string field, string value)
{
// Instead of sending the whole record to everyone:
// await Clients.All.SendAsync("ReceiveRecord", record);
// Send just the change to those who need it:
await Clients.Group(recordId).SendAsync("FieldUpdated", recordId, field, value);
}
Problem: Memory Leaks in Long-Running Applications
Solution: Properly dispose of resources and monitor connection lifetime:
public class MonitoredHub : Hub
{
private readonly IConnectionTracker _connectionTracker;
public MonitoredHub(IConnectionTracker connectionTracker)
{
_connectionTracker = connectionTracker;
}
public override async Task OnConnectedAsync()
{
await _connectionTracker.AddConnectionAsync(
Context.ConnectionId, Context.User.Identity.Name);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await _connectionTracker.RemoveConnectionAsync(Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}
}
// In a background service
public class ConnectionMonitorService : BackgroundService
{
private readonly IConnectionTracker _connectionTracker;
private readonly IHubContext<MonitoredHub> _hubContext;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var staleConnections = await _connectionTracker.GetStaleConnectionsAsync();
foreach (var connectionId in staleConnections)
{
try
{
// Force disconnect stale connections
await _hubContext.Clients.Client(connectionId)
.SendAsync("ForceDisconnect", "Connection timed out");
}
catch (Exception)
{
// Connection already gone, just remove from tracking
}
await _connectionTracker.RemoveConnectionAsync(connectionId);
}
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}
Conclusion
SignalR transforms the capabilities of ASP.NET Core applications by enabling rich, real-time experiences. By abstracting away the complexities of different transport methods, it allows developers to focus on building interactive features rather than worrying about the underlying communication infrastructure.
As you implement SignalR in your applications, remember to:
Start with a solid architecture that considers connection management
Implement proper authentication and authorization
Plan for scale from the beginning
Follow best practices for performance optimization
Provide graceful fallbacks and reconnection strategies
With these considerations in mind, you can leverage SignalR to create dynamic, responsive applications that meet modern user expectations for real-time interaction.
Additional Resources
Stay Updated on Modern Web Tech!
Subscribe to ASP Today on Substack to receive regular updates, tips, and in-depth tutorials. Join our vibrant community on Substack Chat to connect with fellow developers, share experiences, and stay updated with the latest developments in the .NET ecosystem. Never miss an update on modern web development techniques.