C# SignalR
Introduction
In traditional web applications, communication typically flows in one direction: the client sends a request to the server, and the server responds. But what if you need to create applications where the server can push information to connected clients as events occur? This is where SignalR comes into play.
SignalR is an open-source library developed by Microsoft that enables real-time web functionality, allowing server-side code to push content to connected clients instantly as it becomes available. It simplifies the process of adding real-time web capabilities to applications, allowing you to create interactive web experiences such as chat applications, dashboards, gaming, and collaborative applications.
In this tutorial, you'll learn:
- What SignalR is and how it works
- How to set up a SignalR Hub
- Creating client and server connections
- Implementing real-time communication between clients
What is SignalR?
SignalR is an abstraction over various techniques used to establish persistent connections between clients and servers:
- WebSockets: The primary modern protocol for full-duplex communication
- Server-Sent Events (SSE): Enables servers to push updates to clients
- Long Polling: A fallback technique where the client continuously polls the server
The beauty of SignalR is that it automatically selects the best transport method available based on the capabilities of the server and client, without you needing to worry about the implementation details.
Getting Started with SignalR
Let's create a simple chat application to demonstrate how SignalR works.
Step 1: Create a New ASP.NET Core Project
First, let's create a new ASP.NET Core web application:
dotnet new webapp -n SignalRChatApp
cd SignalRChatApp
Step 2: Add SignalR to Your Project
Add the SignalR client library to your project:
dotnet add package Microsoft.AspNetCore.SignalR.Client
Step 3: Configure SignalR in Your Application
Open Program.cs
and update it to include SignalR services:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddSignalR(); // Add SignalR service
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub"); // Map the hub to a URL path
app.Run();
Step 4: Create a SignalR Hub
A Hub is the core component of SignalR that handles client-server communication. Create a new file called ChatHub.cs
in a new folder called Hubs
:
using Microsoft.AspNetCore.SignalR;
namespace SignalRChatApp.Hubs
{
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);
}
}
}
This hub defines:
- A
SendMessage
method that clients can call to broadcast messages to all connected clients OnConnectedAsync
andOnDisconnectedAsync
methods that get called when clients connect and disconnect
Step 5: Create the Chat Interface
Create a new file in wwwroot/js
called chat.js
:
"use strict";
var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();
// Disable the send button until connection is established
document.getElementById("sendButton").disabled = true;
connection.on("ReceiveMessage", function (user, message) {
var li = document.createElement("li");
document.getElementById("messagesList").appendChild(li);
// We want to encode the user input to prevent XSS
li.textContent = `${user}: ${message}`;
});
connection.on("UserConnected", function (connectionId) {
var li = document.createElement("li");
document.getElementById("messagesList").appendChild(li);
li.textContent = `User connected: ${connectionId}`;
});
connection.on("UserDisconnected", function (connectionId) {
var li = document.createElement("li");
document.getElementById("messagesList").appendChild(li);
li.textContent = `User disconnected: ${connectionId}`;
});
connection.start().then(function () {
document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
return console.error(err.toString());
});
document.getElementById("sendButton").addEventListener("click", function (event) {
var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
connection.invoke("SendMessage", user, message).catch(function (err) {
return console.error(err.toString());
});
event.preventDefault();
});
Now, create a new Razor Page called Chat.cshtml
in the Pages
folder:
@page
@model ChatModel
@{
ViewData["Title"] = "Chat";
}
<div class="container">
<div class="row">
<div class="col-12">
<h2>SignalR Chat</h2>
<hr />
</div>
</div>
<div class="row">
<div class="col-6">
<form>
<div class="form-group">
<label for="userInput">User</label>
<input type="text" class="form-control" id="userInput" />
</div>
<div class="form-group">
<label for="messageInput">Message</label>
<input type="text" class="form-control" id="messageInput" />
</div>
<button type="submit" id="sendButton" class="btn btn-primary">Send</button>
</form>
</div>
</div>
<div class="row">
<div class="col-12">
<hr />
</div>
</div>
<div class="row">
<div class="col-6">
<ul id="messagesList"></ul>
</div>
</div>
</div>
<script src="~/lib/microsoft/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>
And the code-behind file Chat.cshtml.cs
:
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace SignalRChatApp.Pages
{
public class ChatModel : PageModel
{
public void OnGet()
{
}
}
}
Step 6: Add SignalR Client Library
You need the SignalR JavaScript client. The easiest way to add it is using LibMan. Create a libman.json
file in the root of your project:
{
"version": "1.0",
"defaultProvider": "unpkg",
"libraries": [
{
"library": "@microsoft/signalr@latest",
"destination": "wwwroot/lib/microsoft/signalr/",
"files": [
"dist/browser/signalr.js",
"dist/browser/signalr.min.js"
]
}
]
}
And run:
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
libman restore
Step 7: Run the Application
Now run the application:
dotnet run
Navigate to https://localhost:7777/Chat
(the port might be different on your machine) to see the chat application in action. Open multiple browser windows to see real-time messages being exchanged.
How SignalR Works
Let's break down how this chat application works:
-
Server Side (Hub):
- The
ChatHub
class derives fromHub
, which provides methods for communicating with clients. - The
SendMessage
method receives a user name and message from any client. - It then broadcasts this message to all connected clients by calling
Clients.All.SendAsync()
. - The lifecycle methods
OnConnectedAsync
andOnDisconnectedAsync
track client connections.
- The
-
Client Side (JavaScript):
- We create a connection to our hub using
HubConnectionBuilder
. - We register handlers for messages sent from the server using
connection.on()
. - When a message is received, we add it to the messages list.
- The
connection.start()
method establishes the connection. - When the send button is clicked, we call the
SendMessage
method on the hub.
- We create a connection to our hub using
Advanced SignalR Features
User and Group Management
SignalR allows you to send messages to specific clients:
// Send to specific client
await Clients.Client(connectionId).SendAsync("ReceiveMessage", user, message);
// Send to all except the caller
await Clients.Others.SendAsync("ReceiveMessage", user, message);
// Send to the caller
await Clients.Caller.SendAsync("ReceiveMessage", user, message);
You can also manage groups of connections:
// Add to a group
public async Task JoinGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).SendAsync("ReceiveMessage",
"System", $"{Context.ConnectionId} has joined the group {groupName}");
}
// Send to a group
public async Task SendMessageToGroup(string groupName, string user, string message)
{
await Clients.Group(groupName).SendAsync("ReceiveMessage", user, message);
}
Scaling SignalR with Redis Backplane
When running SignalR in multiple server instances, you need a backplane to coordinate messages between servers:
builder.Services.AddSignalR()
.AddStackExchangeRedis("redis_connection_string");
Authentication and Authorization
You can secure your SignalR hub using authorization:
[Authorize]
public class SecureHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
Real-World Applications of SignalR
1. Real-time Dashboard
SignalR is perfect for creating dashboards that update in real-time. For example, monitoring system metrics:
public class DashboardHub : Hub
{
private readonly ISystemMetricsService _metricsService;
private Timer _timer;
public DashboardHub(ISystemMetricsService metricsService)
{
_metricsService = metricsService;
}
public override async Task OnConnectedAsync()
{
// Send initial data
var metrics = _metricsService.GetCurrentMetrics();
await Clients.Caller.SendAsync("UpdateMetrics", metrics);
await base.OnConnectedAsync();
}
// Server method to broadcast metrics updates every 5 seconds
public void StartMetricsUpdates()
{
_timer = new Timer(async _ =>
{
var metrics = _metricsService.GetCurrentMetrics();
await Clients.All.SendAsync("UpdateMetrics", metrics);
}, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
}
public override async Task OnDisconnectedAsync(Exception exception)
{
_timer?.Dispose();
await base.OnDisconnectedAsync(exception);
}
}
2. Collaborative Editing
SignalR can enable collaborative document editing where multiple users see changes in real-time:
public class DocumentHub : Hub
{
private static ConcurrentDictionary<string, string> _documents =
new ConcurrentDictionary<string, string>();
public async Task JoinDocument(string documentId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, documentId);
// Send current document state
if (_documents.TryGetValue(documentId, out string content))
{
await Clients.Caller.SendAsync("LoadDocument", content);
}
else
{
_documents[documentId] = "";
}
await Clients.Group(documentId).SendAsync("UserJoined", Context.ConnectionId);
}
public async Task UpdateDocument(string documentId, string content)
{
_documents[documentId] = content;
await Clients.OthersInGroup(documentId).SendAsync("DocumentUpdated", content);
}
public async Task LeaveDocument(string documentId)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, documentId);
await Clients.Group(documentId).SendAsync("UserLeft", Context.ConnectionId);
}
}
3. Notifications System
SignalR is ideal for implementing real-time notifications:
public class NotificationHub : Hub
{
public async Task SendNotificationToUser(string userId, string message)
{
await Clients.User(userId).SendAsync("ReceiveNotification", message);
}
public async Task BroadcastNotification(string message)
{
await Clients.All.SendAsync("ReceiveNotification", message);
}
}
Best Practices for Working with SignalR
- Handle Connection Failures: Implement reconnection logic on the client side.
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.withAutomaticReconnect([0, 2000, 10000, 30000]) // Retry after 0ms, 2s, 10s, 30s
.build();
connection.onreconnecting(error => {
console.log(`Connection lost due to: ${error}. Reconnecting...`);
document.getElementById("connectionStatus").innerText = "Reconnecting...";
});
connection.onreconnected(connectionId => {
console.log(`Connection reestablished. Connected with ID: ${connectionId}`);
document.getElementById("connectionStatus").innerText = "Connected";
});
- Use Strong Typing with Hubs
You can create strongly-typed hubs for better type safety and IntelliSense support:
public interface IChatClient
{
Task ReceiveMessage(string user, string message);
Task UserConnected(string connectionId);
Task UserDisconnected(string connectionId);
}
public class ChatHub : Hub<IChatClient>
{
public async Task SendMessage(string user, string message)
{
await Clients.All.ReceiveMessage(user, message);
}
public override async Task OnConnectedAsync()
{
await Clients.All.UserConnected(Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await Clients.All.UserDisconnected(Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}
}
-
Limit Message Size: SignalR messages should be kept small to ensure good performance.
-
Consider Message Delivery Guarantees: SignalR doesn't guarantee message delivery in all circumstances. For critical applications, implement additional confirmation mechanisms.
Summary
SignalR is a powerful library that enables real-time web applications in ASP.NET Core. It:
- Abstracts transport protocols (WebSockets, SSE, Long Polling)
- Provides a simple API for sending messages to clients
- Supports groups and user targeting
- Scales across multiple servers with a backplane
- Handles connection management
With SignalR, you can build interactive applications like chat systems, live dashboards, collaborative editing tools, and notification systems that provide engaging real-time user experiences.
Additional Resources
Exercises
- Basic: Extend the chat application to display a list of currently connected users.
- Intermediate: Create a simple collaborative drawing application where multiple users can draw on the same canvas in real-time.
- Advanced: Build a real-time dashboard that shows system metrics like CPU usage, memory utilization, and disk space using SignalR to push updates to clients.
- Challenge: Implement a multi-room chat application where users can create and join different chat rooms.
By exploring these exercises, you'll gain a deeper understanding of how SignalR works and how to use it in various real-world scenarios.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)