Skip to main content

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:

bash
dotnet new webapp -n SignalRChatApp
cd SignalRChatApp

Step 2: Add SignalR to Your Project

Add the SignalR client library to your project:

bash
dotnet add package Microsoft.AspNetCore.SignalR.Client

Step 3: Configure SignalR in Your Application

Open Program.cs and update it to include SignalR services:

csharp
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:

csharp
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 and OnDisconnectedAsync 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:

javascript
"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:

cshtml
@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:

csharp
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:

json
{
"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:

bash
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
libman restore

Step 7: Run the Application

Now run the application:

bash
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:

  1. Server Side (Hub):

    • The ChatHub class derives from Hub, 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 and OnDisconnectedAsync track client connections.
  2. 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.

Advanced SignalR Features

User and Group Management

SignalR allows you to send messages to specific clients:

csharp
// 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:

csharp
// 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:

csharp
builder.Services.AddSignalR()
.AddStackExchangeRedis("redis_connection_string");

Authentication and Authorization

You can secure your SignalR hub using authorization:

csharp
[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:

csharp
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:

csharp
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:

csharp
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

  1. Handle Connection Failures: Implement reconnection logic on the client side.
javascript
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";
});
  1. Use Strong Typing with Hubs

You can create strongly-typed hubs for better type safety and IntelliSense support:

csharp
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);
}
}
  1. Limit Message Size: SignalR messages should be kept small to ensure good performance.

  2. 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

  1. Official SignalR Documentation
  2. GitHub Repository for SignalR
  3. SignalR Tutorial - Microsoft Learn

Exercises

  1. Basic: Extend the chat application to display a list of currently connected users.
  2. Intermediate: Create a simple collaborative drawing application where multiple users can draw on the same canvas in real-time.
  3. 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.
  4. 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! :)