.NET SignalR: Real-Time Web Applications

Learn .NET SignalR: Real-Time Web Applications with practical examples, explanations, best practices, and real-world coding techniques.

ASP.NET Core SignalR is a library for building real-time applications that allow servers to push messages and updates to connected clients as events happen. In a traditional web application, a browser often requests new information from the server at regular intervals. With SignalR, the server can send updates to clients without requiring them to repeatedly ask whether something has changed.

SignalR is useful for chat applications, live notifications, collaborative editing, monitoring dashboards, multiplayer application features, and real-time order or job status updates. For example, a coding platform can use real-time communication to display execution progress, while an administrative dashboard can update counters as new events arrive. SignalR manages much of the connection handling and transport selection so developers can concentrate on application messages and behavior.

1. Understanding How SignalR Works

SignalR organizes server-side real-time communication around hubs. A hub is a high-level abstraction through which clients can invoke server methods and the server can send messages to individual clients, groups, or all connected clients. The hub acts as a communication endpoint rather than replacing your application's business logic or database layer.

A typical SignalR workflow begins when a client connects to a configured hub URL. The client can then invoke hub methods, and the server can send events to the client using a named method or event. For example, a chat client might invoke a server method named SendMessage, while the server broadcasts an event named ReceiveMessage to connected participants.

SignalR can use WebSockets when available and appropriate. It can also use Server-Sent Events or Long Polling, depending on client capabilities, server support, and connection configuration. The client and server negotiate a suitable transport where negotiation is enabled. Developers generally use the SignalR client library instead of implementing transport management manually.

2. Add SignalR to an ASP.NET Core Application

In a typical ASP.NET Core application, SignalR services are registered with the dependency injection container, and a hub endpoint is mapped into the HTTP request pipeline. Many project templates already contain most of the required hosting setup, so you generally only need to register SignalR and map the endpoint.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSignalR();
builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();
app.MapHub<ChatHub>("/hubs/chat");

app.Run();

The call to AddSignalR() registers the services required by SignalR. The call to MapHub<ChatHub>("/hubs/chat") exposes a real-time endpoint at the specified path. Clients must connect to the correct URL, including the appropriate host, scheme, and path for the environment.

The authentication and authorization middleware shown above assumes that the application has configured the relevant authentication scheme. Registering SignalR does not automatically secure a hub. Configure authentication and authorization according to your application's identity model, and apply authorization requirements to the hub or its methods where needed.

3. Create a SignalR Hub

A hub typically derives from Microsoft.AspNetCore.SignalR.Hub. Hub methods define operations that connected clients are permitted to invoke. Within those methods, the Clients property lets the server select recipients for outgoing messages.

using Microsoft.AspNetCore.SignalR;

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        if (string.IsNullOrWhiteSpace(user))
        {
            throw new HubException("A user name is required.");
        }

        if (string.IsNullOrWhiteSpace(message))
        {
            throw new HubException("A message is required.");
        }

        await Clients.All.SendAsync(
            "ReceiveMessage",
            user,
            message
        );
    }
}

In this example, the client invokes SendMessage with a user name and message. The hub validates the basic input and broadcasts the ReceiveMessage event to every connected client. Each receiving client must register a handler using that exact event name and expect the arguments in the correct order.

This is a simplified demonstration. In a production application, do not rely on a user name supplied by the client as proof of identity. Derive the authenticated user's identity from the server-side authentication context, validate message length and content, and decide which recipients should receive the message. Broadcasting to every connection is appropriate only when every connected client is allowed to see the data.

4. Connect a JavaScript Client

A browser application can connect to an ASP.NET Core hub using the official JavaScript client package, @microsoft/signalr. Install the package through your project's package manager, then create a connection to the hub endpoint and register handlers for server-sent events.

import * as signalR from "@microsoft/signalr";

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

connection.on("ReceiveMessage", (user, message) => {
  console.log(`${user}: ${message}`);
});

async function startConnection() {
  try {
    await connection.start();
    console.log("Connected to the chat hub.");
  } catch (error) {
    console.error("SignalR connection failed:", error);
  }
}

async function sendMessage(user, message) {
  try {
    await connection.invoke("SendMessage", user, message);
  } catch (error) {
    console.error("Unable to send the message:", error);
  }
}

startConnection();

The withUrl() method specifies the hub endpoint. The on() method registers a handler for an event sent by the server, while invoke() calls a server-side hub method and returns a promise that completes when the invocation finishes or fails.

Automatic reconnect is optional and does not retry every initial connection failure automatically. The example handles an initial connection failure by logging it; a production client may implement bounded retry logic for initial connection attempts. When reconnecting, also consider restoring application-specific subscriptions and refreshing any state that may have changed while the client was disconnected.

If the frontend and backend use different origins, configure a suitable CORS policy on the server and use the correct hub URL. CORS is an HTTP browser security mechanism, not an authentication system, and it should not be treated as a replacement for access control.

5. Send Messages to Different Recipients

SignalR provides several ways to target clients. Selecting the right recipient is essential because real-time messages may contain data intended for one user, a group of users, or all connected clients.

  • Clients.All sends a message to all connected clients on the current SignalR server context.
  • Clients.Caller sends a message only to the connection that invoked the hub method.
  • Clients.Others sends a message to other connections, excluding the caller.
  • Clients.Client(connectionId) targets a specific connection.
  • Clients.User(userId) targets connections associated with a user identifier recognized by SignalR.
  • Clients.Group(groupName) sends a message to connections in a particular group.

A connection represents an active SignalR connection, not necessarily a unique person. A single user may have multiple browser tabs or devices connected simultaneously. For user-specific messaging, configure a reliable user identifier strategy and ensure that the identifier comes from a trusted identity source.

Groups are useful for chat rooms, project workspaces, team dashboards, and other scoped communication. Users should be added to groups only after the server has verified their permission to access the relevant resource.

6. Use Groups for Scoped Broadcasting

Groups let you send updates to a selected set of connections without manually maintaining a list of connection identifiers in application code. A common example is a chat room where users subscribe to a room and receive messages for that room only.

using Microsoft.AspNetCore.SignalR;

public class RoomHub : Hub
{
    public async Task JoinRoom(string roomName)
    {
        if (string.IsNullOrWhiteSpace(roomName))
        {
            throw new HubException("A room name is required.");
        }

        // In production, authorize access to this room first.
        await Groups.AddToGroupAsync(
            Context.ConnectionId,
            roomName
        );

        await Clients.Group(roomName).SendAsync(
            "RoomNotice",
            "A participant joined the room."
        );
    }

    public async Task SendToRoom(string roomName, string message)
    {
        // Verify that the caller may send to this room.
        await Clients.Group(roomName).SendAsync(
            "ReceiveRoomMessage",
            message
        );
    }
}

This example illustrates group operations, but a room name supplied by a client must not automatically be trusted. A real system should verify room membership and access permissions before allowing a client to join or broadcast to a group. Otherwise, a client could attempt to access messages belonging to another team or conversation.

SignalR manages group membership for active connections, but groups are not a persistent authorization database. Membership should be re-established when appropriate after a reconnect, and the application must continue enforcing access rules as users' permissions change.

7. Secure Hubs with Authentication and Authorization

Real-time endpoints require the same security attention as ordinary API endpoints. You can apply authorization attributes to an entire hub or to individual hub methods. This restricts which authenticated users are permitted to connect or perform particular operations.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;

[Authorize]
public class NotificationsHub : Hub
{
    public async Task Subscribe()
    {
        var userId = Context.UserIdentifier;

        if (string.IsNullOrWhiteSpace(userId))
        {
            throw new HubException("User identity is unavailable.");
        }

        await Groups.AddToGroupAsync(
            Context.ConnectionId,
            $"user-{userId}"
        );
    }
}

The [Authorize] attribute requires the application to configure and use an appropriate authentication scheme. If only selected methods require a particular policy, apply the relevant authorization attribute to those methods and configure the corresponding policy. Authorization policies can include roles, claims, or other supported requirements.

Browser WebSocket and Server-Sent Events connections may rely on access tokens during connection establishment, depending on the client and authentication configuration. When using bearer tokens with the JavaScript client, configure token acquisition using the supported access-token provider and ensure tokens are validated on the server. Query-string tokens may appear in HTTP logs, so restrict logging and protect those values. Use HTTPS in production and avoid logging sensitive message content or credentials.

8. Handle Disconnects and Reconnection

Network interruptions, browser sleep, server restarts, and mobile connectivity changes can interrupt real-time connections. Clients should handle connection failures gracefully and present appropriate status information when real-time features are temporarily unavailable.

SignalR supports connection lifecycle callbacks such as OnConnectedAsync() and OnDisconnectedAsync(). These can be useful for lifecycle-related work, but connection state should not be confused with durable user presence. A disconnected client may reconnect, and a user may have several active connections.

Automatic reconnect on the client can recover from many temporary interruptions, but it does not guarantee that missed messages will be replayed. If your application needs reliable delivery, message history, or event recovery, persist important events in an appropriate storage system and let clients fetch missing data after reconnecting. SignalR is a real-time communication layer, not by itself a durable message queue or event database.

9. Scale SignalR for Production

When an application runs on multiple server instances, each instance may maintain its own client connections. A message sent from one instance will not necessarily reach clients connected to another instance unless the deployment provides an appropriate distribution mechanism.

Two common options are a Redis backplane and Azure SignalR Service. A Redis backplane distributes SignalR messages among application instances using Redis infrastructure. Azure SignalR Service provides a managed service designed to handle SignalR client connections and coordinate message delivery for supported application configurations.

The right option depends on the hosting platform, connection volume, latency requirements, infrastructure maintenance responsibilities, and operational budget. Scaling decisions should also account for any load-balancer requirements, connection affinity where relevant, service limits, and monitoring. Test reconnect behavior and message routing with the actual deployment architecture rather than assuming a locally working hub will behave identically when distributed.

10. Common Mistakes to Avoid

  • Broadcasting everything to everyone: Use user-specific targets or groups when messages are not intended for every connected client.
  • Trusting client-supplied identities: Use authenticated server-side identity information rather than trusting a user name passed in a method argument.
  • Skipping authorization checks: Confirm access to rooms, projects, and other resources before adding connections to groups or sending protected data.
  • Assuming reconnect restores all state: Re-establish application subscriptions and retrieve missed data when required.
  • Confusing connection IDs with user IDs: One user may own several simultaneous connections, and individual connection IDs can change across connections.
  • Ignoring message volume: Large payloads and frequent broadcasts can increase bandwidth, memory use, and server load. Send only the data clients need.
  • Relying on local tests for scale behavior: Validate message distribution, authentication, reconnects, and capacity under realistic deployment conditions.

11. Summary

SignalR provides a convenient foundation for real-time .NET applications by handling connection management, client-to-server invocations, and server-to-client messages. Hubs expose operations, clients register handlers for named events, and recipient selectors allow messages to be sent to specific connections, authenticated users, groups, or all connected clients.

To get started, register SignalR services, create a hub, map its endpoint, and connect a browser client using the official SignalR client library. As the application grows, add authorization, validate inputs, plan for disconnections, and choose an appropriate scaling strategy. For a complete production implementation, combine SignalR with robust configuration management, reliable exception handling, and suitable application architecture.

Code Examples

using Microsoft.AspNetCore.SignalR;

public class ChatHub : Hub
{
    public async Task SendMessage(string message)
    {
        if (string.IsNullOrWhiteSpace(message))
        {
            throw new HubException("Message cannot be empty.");
        }

        var user = Context.UserIdentifier ?? "Anonymous";

        await Clients.All.SendAsync(
            "ReceiveMessage",
            user,
            message
        );
    }
}

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSignalR();

var app = builder.Build();

app.MapHub<ChatHub>("/hubs/chat");

app.Run();
    
import * as signalR from "@microsoft/signalr";

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

connection.on("ReceiveMessage", (user: string, message: string) => {
  console.log(`${user}: ${message}`);
});

async function start(): Promise<void> {
  try {
    await connection.start();
    console.log("Connected to SignalR.");
  } catch (error) {
    console.error("Initial connection failed:", error);
  }
}

async function sendMessage(message: string): Promise<void> {
  try {
    await connection.invoke("SendMessage", message);
  } catch (error) {
    console.error("Message could not be sent:", error);
  }
}

void start();
    
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;

[Authorize]
public class NotificationsHub : Hub
{
    public async Task SubscribeToNotifications()
    {
        var userId = Context.UserIdentifier;

        if (string.IsNullOrWhiteSpace(userId))
        {
            throw new HubException("Authenticated user ID is missing.");
        }

        await Groups.AddToGroupAsync(
            Context.ConnectionId,
            $"user-{userId}"
        );
    }
}

// From an injected IHubContext<NotificationsHub>:
// await hubContext.Clients
//     .Group($"user-{userId}")
//     .SendAsync("NotificationReceived", notification);
    

Pro Tip

Treat SignalR messages as API contracts: keep event names and payloads stable, authorize every protected operation, and never send sensitive data through Clients.All. For multi-instance deployments, choose and test a suitable backplane or managed SignalR service.

🎨 IgKnight Docs

Master HTML, CSS, JS, Python, .NET and more with easy-to-follow lessons and practical code examples.

© 2025 IgKnight Learning Platform. All rights reserved.