How to Build Real-Time SignalR Hubs in C

This article provides a comprehensive guide to understanding ASP.NET Core SignalR and implementing real-time, bidirectional communication in modern C# applications. You will learn the core concepts behind SignalR, how the framework abstracts transport protocols like WebSockets, and the exact steps required to implement, configure, and consume a SignalR Hub for instant client-server data exchange.

What is ASP.NET Core SignalR?

ASP.NET Core SignalR is an open-source library that simplifies adding real-time web functionality to applications. Real-time functionality enables server-side code to push content to connected clients instantly as events occur, eliminating the need for clients to constantly poll the server for updates.

SignalR automatically chooses the best available transport method based on server and client capabilities:

  1. WebSockets: The preferred, high-performance transport for full-duplex communication.
  2. Server-Sent Events (SSE): A fallback that allows the server to push events to the client over standard HTTP.
  3. Long Polling: A secondary fallback that holds an HTTP request open until data is available.

The central programming abstraction in SignalR is the Hub. A Hub is a high-level pipeline that enables connected servers and clients to call methods on each other directly, passing strongly typed parameters and handling complex serialization automatically.


Step 1: Create the SignalR Hub

A Hub is created by inheriting from the Microsoft.AspNetCore.SignalR.Hub base class. Methods defined within this class can be invoked directly by connected clients, while the Clients property allows the server to invoke methods on one, several, or all connected clients.

using Microsoft.AspNetCore.SignalR;

namespace RealTimeApp.Hubs;

public class ChatHub : Hub
{
    // Method callable by clients to broadcast a message
    public async Task SendMessage(string user, string message)
    {
        // "ReceiveMessage" is the client-side handler name
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }

    // Optional lifecycle hooks
    public override async Task OnConnectedAsync()
    {
        await Clients.Caller.SendAsync("ReceiveMessage", "System", "Connected successfully.");
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        await base.OnDisconnectedAsync(exception);
    }
}

To avoid magic strings like "ReceiveMessage", define a client contract interface and inherit from Hub<T>:

public interface IChatClient
{
    Task ReceiveMessage(string user, string message);
}

public class StronglyTypedChatHub : Hub<IChatClient>
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.ReceiveMessage(user, message);
    }
}

Step 2: Register and Map the Hub in Program.cs

SignalR requires two primary configuration steps in an ASP.NET Core application: registering the SignalR services in the dependency injection container and mapping the Hub endpoint to an incoming route.

Open Program.cs and add the following configurations:

using RealTimeApp.Hubs;

var builder = WebApplication.CreateBuilder(args);

// Register SignalR services
builder.Services.AddSignalR();

var app = builder.Build();

app.UseRouting();

// Map the Hub to a public URL endpoint
app.MapHub<ChatHub>("/chatHub");

app.Run();

Step 3: Connect and Interact from a Client

Clients can be built using various technologies, including JavaScript/TypeScript, .NET, Java, and Python. Below are examples demonstrating bidirectional communication using both JavaScript and C#.

JavaScript Client Example

Using the @microsoft/signalr npm package or CDN:

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chatHub")
    .withAutomaticReconnect()
    .build();

// Listen for messages pushed from the server
connection.on("ReceiveMessage", (user, message) => {
    console.log(`${user}: ${message}`);
});

// Start the connection and send a message to the server
async function start() {
    try {
        await connection.start();
        console.log("Connected to SignalR Hub");
        
        // Invoke the server-side SendMessage method
        await connection.invoke("SendMessage", "Alice", "Hello World!");
    } catch (err) {
        console.error(err);
    }
}

start();

C# Client Example

Using the Microsoft.AspNetCore.SignalR.Client NuGet package:

using Microsoft.AspNetCore.SignalR.Client;

var connection = new HubConnectionBuilder()
    .WithUrl("https://localhost:5001/chatHub")
    .WithAutomaticReconnect()
    .Build();

// Register client-side handler
connection.On<string, string>("ReceiveMessage", (user, message) =>
{
    Console.WriteLine($"{user}: {message}");
});

await connection.StartAsync();

// Send message to the Hub
await connection.InvokeAsync("SendMessage", "DotNetClient", "Hello from C#!");

Step 4: Targeting Specific Audiences

SignalR Hubs provide granular control over message distribution through the Clients API:

  • All clients: Clients.All.SendAsync(...)
  • Current caller only: Clients.Caller.SendAsync(...)
  • Specific user: Clients.User(userId).SendAsync(...) (requires authentication configured)
  • Specific groups: Clients.Group("GroupName").SendAsync(...)

Clients can be dynamically added or removed from arbitrary groups:

public async Task JoinRoom(string roomName)
{
    await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
    await Clients.Group(roomName).SendAsync("ReceiveMessage", "System", $"{Context.ConnectionId} joined {roomName}.");
}

Scaling SignalR

In multi-server deployments (such as load-balanced web farms), clients connected to Server A cannot receive messages broadcast from Server B by default. To resolve this, configure a backplane using Redis (Microsoft.AspNetCore.SignalR.StackExchangeRedis) or the Azure SignalR Service, allowing messages to be coordinated across all active server instances seamlessly.