Backend Systemsbeginner
SignalR Hubs — Your First Real-Time App
Build real-time features with ASP.NET Core SignalR: hubs, client connections, broadcasting messages, groups, and a working chat example.
Asma HafeezApril 17, 20264 min read
dotnetsignalrreal-timewebsocketsaspnet-core
SignalR Hubs
SignalR is ASP.NET Core's real-time communication library. It uses WebSockets when available and falls back to long polling — you write one API, SignalR handles the transport.
How SignalR Works
Client (JavaScript) Server (ASP.NET Core)
| |
|-- connection.start() ------> |
|<-- connected -------------- |
| |
|-- Invoke("SendMessage") ---> Hub method runs
|<-- ReceiveMessage ---------- Server pushes to clients
| |The Hub is the server-side class. Clients connect to it, invoke methods on it, and receive messages from it.
Server Setup
Bash
# Already included in ASP.NET Core — no extra package needed
dotnet new web -n ChatAppC#
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();
var app = builder.Build();
app.UseStaticFiles();
app.MapHub<ChatHub>("/chathub");
app.Run();The Hub
C#
// Hubs/ChatHub.cs
using Microsoft.AspNetCore.SignalR;
public class ChatHub : Hub
{
// Client calls this method to send a message
public async Task SendMessage(string user, string message)
{
// Broadcast to ALL connected clients
await Clients.All.SendAsync("ReceiveMessage", user, message, DateTime.UtcNow);
}
// Called when a client connects
public override async Task OnConnectedAsync()
{
var connectionId = Context.ConnectionId;
Console.WriteLine($"Client connected: {connectionId}");
await base.OnConnectedAsync();
}
// Called when a client disconnects
public override async Task OnDisconnectedAsync(Exception? exception)
{
Console.WriteLine($"Client disconnected: {Context.ConnectionId}");
await base.OnDisconnectedAsync(exception);
}
}Targeting Specific Clients
C#
public class ChatHub : Hub
{
// Send to the caller only
public async Task Echo(string message)
{
await Clients.Caller.SendAsync("ReceiveMessage", message);
}
// Send to everyone except the caller
public async Task Broadcast(string message)
{
await Clients.Others.SendAsync("ReceiveMessage", message);
}
// Send to a specific connection
public async Task Whisper(string toConnectionId, string message)
{
await Clients.Client(toConnectionId).SendAsync("ReceiveMessage", message);
}
}Groups — Rooms and Channels
C#
public class ChatHub : Hub
{
public async Task JoinRoom(string roomName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
await Clients.Group(roomName).SendAsync("UserJoined", Context.ConnectionId, roomName);
}
public async Task LeaveRoom(string roomName)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName);
}
public async Task SendToRoom(string roomName, string message)
{
await Clients.Group(roomName).SendAsync("ReceiveMessage", Context.ConnectionId, message);
}
}JavaScript Client
HTML
<!-- wwwroot/index.html -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/7.0.0/signalr.min.js"></script>
<script>
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chathub")
.withAutomaticReconnect()
.build();
// Listen for messages from server
connection.on("ReceiveMessage", (user, message, timestamp) => {
const li = document.createElement("li");
li.textContent = `[${new Date(timestamp).toLocaleTimeString()}] ${user}: ${message}`;
document.getElementById("messagesList").appendChild(li);
});
// Start connection
connection.start()
.then(() => console.log("Connected!"))
.catch(err => console.error(err));
// Send message
document.getElementById("sendButton").addEventListener("click", () => {
const user = document.getElementById("userInput").value;
const msg = document.getElementById("messageInput").value;
connection.invoke("SendMessage", user, msg)
.catch(err => console.error(err));
});
// Join a room
connection.invoke("JoinRoom", "general");
</script>Pushing from Server (Outside Hub)
C#
// Inject IHubContext to push from controllers or background services
public class OrderController : ControllerBase
{
private readonly IHubContext<OrderHub> _hub;
public OrderController(IHubContext<OrderHub> hub)
{
_hub = hub;
}
[HttpPost("{id}/ship")]
public async Task<IActionResult> ShipOrder(int id)
{
// ... process order ...
// Push update to ALL clients
await _hub.Clients.All.SendAsync("OrderShipped", new { orderId = id, status = "Shipped" });
return Ok();
}
}Authentication with SignalR
C#
// Hub with auth
[Authorize]
public class SecureHub : Hub
{
public async Task SendMessage(string message)
{
var userId = Context.User?.FindFirstValue(ClaimTypes.NameIdentifier);
await Clients.All.SendAsync("ReceiveMessage", userId, message);
}
}
// JavaScript — pass JWT
const connection = new signalR.HubConnectionBuilder()
.withUrl("/securehub", {
accessTokenFactory: () => localStorage.getItem("token")
})
.build();Key Takeaways
- SignalR abstracts WebSockets — it falls back to Server-Sent Events then long polling automatically
- Hub methods are called by clients;
Clients.*sends from server to clients - Groups implement rooms and channels — add/remove connections per message
- Use
IHubContext<T>to push from outside the hub (controllers, background services) - Add
withAutomaticReconnect()on the client — networks drop, connections should recover
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.