SignalR Real-time Applications: Building Live Features in .NET
Backend15 min read

SignalR Real-time Applications: Building Live Features in .NET

Build real-time web applications with SignalR. WebSockets, hubs, groups, scaling with Redis, and production deployment strategies.

Taha Kocal

Taha Kocal

Full Stack Developer

Apr 10, 2025
#SignalR#.NET#WebSockets#Real-time#C#

SignalR is a real-time communication library for ASP.NET Core that lets a server push messages to connected clients instantly, without the client polling for updates. It negotiates the best available transport automatically — WebSockets first, falling back to Server-Sent Events or long polling — and manages connections, reconnection, and message routing for you. Code is organized around hubs: server-side classes whose methods clients can invoke, and which can broadcast to all clients, a single user, or named groups. SignalR includes built-in authentication and authorization support, strongly-typed hubs for compile-time safety, and a Redis backplane for scaling across multiple server instances. It powers the features users now expect by default: live chat, notifications, presence indicators, and real-time dashboards. This guide walks through hubs, groups, JWT authentication, sending from services with IHubContext, the JavaScript client, scaling with Redis, and production tuning.

What is SignalR?

SignalR capabilities:

  • Automatic transport negotiation (WebSockets, Server-Sent Events, Long Polling)
  • Connection management and reconnection handling
  • Group-based messaging
  • Strongly-typed hubs for type safety
  • Built-in authentication and authorization
  • Scales with Redis backplane for multiple servers

Basic Setup

csharp
// Program.cs
builder.Services.AddSignalR();

var app = builder.Build();

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

app.Run();

How Do You Create a Hub?

Basic Chat Hub

csharp
public class ChatHub : Hub
{
    private readonly ILogger<ChatHub> _logger;

    public ChatHub(ILogger<ChatHub> logger)
    {
        _logger = logger;
    }

    // Called when a client connects
    public override async Task OnConnectedAsync()
    {
        _logger.LogInformation("Client connected: {ConnectionId}", Context.ConnectionId);
        await Clients.All.SendAsync("UserConnected", Context.ConnectionId);
        await base.OnConnectedAsync();
    }

    // Called when a client disconnects
    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        _logger.LogInformation("Client disconnected: {ConnectionId}", Context.ConnectionId);
        await Clients.All.SendAsync("UserDisconnected", Context.ConnectionId);
        await base.OnDisconnectedAsync(exception);
    }

    // Client can call this method
    public async Task SendMessage(string user, string message)
    {
        _logger.LogInformation("Message from {User}: {Message}", user, message);

        // Send to all connected clients
        await Clients.All.SendAsync("ReceiveMessage", user, message, DateTime.UtcNow);
    }

    // Send to specific user
    public async Task SendPrivateMessage(string targetUserId, string message)
    {
        await Clients.User(targetUserId).SendAsync("ReceivePrivateMessage", Context.UserIdentifier, message);
    }

    // Send to caller only
    public async Task Echo(string message)
    {
        await Clients.Caller.SendAsync("EchoResponse", message);
    }
}

Strongly-Typed Hub

csharp
// Define client interface
public interface IChatClient
{
    Task ReceiveMessage(string user, string message, DateTime timestamp);
    Task UserConnected(string connectionId);
    Task UserDisconnected(string connectionId);
    Task UserJoinedRoom(string user, string room);
    Task UserLeftRoom(string user, string room);
}

// Strongly-typed hub
public class ChatHub : Hub<IChatClient>
{
    public async Task SendMessage(string user, string message)
    {
        // Compile-time type checking!
        await Clients.All.ReceiveMessage(user, message, DateTime.UtcNow);
    }

    public async Task JoinRoom(string roomName)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
        await Clients.Group(roomName).UserJoinedRoom(Context.UserIdentifier!, roomName);
    }

    public async Task LeaveRoom(string roomName)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName);
        await Clients.Group(roomName).UserLeftRoom(Context.UserIdentifier!, roomName);
    }

    public async Task SendToRoom(string roomName, string message)
    {
        await Clients.Group(roomName).ReceiveMessage(Context.UserIdentifier!, message, DateTime.UtcNow);
    }
}

How Do You Target Users, Groups, and Connections?

csharp
public class NotificationHub : Hub
{
    // Send to all clients
    public async Task BroadcastNotification(string message)
    {
        await Clients.All.SendAsync("Notification", message);
    }

    // Send to specific connection
    public async Task SendToConnection(string connectionId, string message)
    {
        await Clients.Client(connectionId).SendAsync("Notification", message);
    }

    // Send to specific user (by user ID from claims)
    public async Task SendToUser(string userId, string message)
    {
        await Clients.User(userId).SendAsync("Notification", message);
    }

    // Send to multiple users
    public async Task SendToUsers(IReadOnlyList<string> userIds, string message)
    {
        await Clients.Users(userIds).SendAsync("Notification", message);
    }

    // Send to group
    public async Task SendToGroup(string groupName, string message)
    {
        await Clients.Group(groupName).SendAsync("Notification", message);
    }

    // Send to all except caller
    public async Task SendToOthers(string message)
    {
        await Clients.Others.SendAsync("Notification", message);
    }

    // Send to all except specific connections
    public async Task SendToAllExcept(IReadOnlyList<string> excludedConnectionIds, string message)
    {
        await Clients.AllExcept(excludedConnectionIds).SendAsync("Notification", message);
    }
}

How Do You Authenticate SignalR Connections?

csharp
// Program.cs - Configure JWT for SignalR
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = jwtSettings.Issuer,
            ValidAudience = jwtSettings.Audience,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey))
        };

        // Configure for SignalR
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                // Read token from query string for WebSocket connections
                var accessToken = context.Request.Query["access_token"];

                var path = context.HttpContext.Request.Path;
                if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
                {
                    context.Token = accessToken;
                }
                return Task.CompletedTask;
            }
        };
    });

// Secure hub
[Authorize]
public class SecureHub : Hub
{
    public string? UserId => Context.UserIdentifier;
    public string? UserEmail => Context.User?.FindFirst(ClaimTypes.Email)?.Value;

    public override async Task OnConnectedAsync()
    {
        // User is authenticated here
        var userId = Context.UserIdentifier;
        await Groups.AddToGroupAsync(Context.ConnectionId, $"user:{userId}");
        await base.OnConnectedAsync();
    }
}

// Authorize specific methods
public class ChatHub : Hub
{
    [Authorize(Roles = "Admin")]
    public async Task DeleteMessage(Guid messageId)
    {
        // Only admins can call this
    }

    [Authorize(Policy = "PremiumUser")]
    public async Task SendPriorityMessage(string message)
    {
        // Only premium users can call this
    }
}

How Do You Send Messages from Outside a Hub?

csharp
// Inject IHubContext to send messages from services or controllers
public class OrderService : IOrderService
{
    private readonly IHubContext<NotificationHub> _hubContext;

    public OrderService(IHubContext<NotificationHub> hubContext)
    {
        _hubContext = hubContext;
    }

    public async Task CreateOrderAsync(Order order)
    {
        // Save order...

        // Notify the user
        await _hubContext.Clients.User(order.UserId.ToString())
            .SendAsync("OrderCreated", new
            {
                OrderId = order.Id,
                Status = order.Status,
                Total = order.TotalAmount
            });

        // Notify admins
        await _hubContext.Clients.Group("admins")
            .SendAsync("NewOrder", order.Id);
    }
}

// With strongly-typed hub
public class OrderService : IOrderService
{
    private readonly IHubContext<NotificationHub, INotificationClient> _hubContext;

    public async Task CreateOrderAsync(Order order)
    {
        await _hubContext.Clients.User(order.UserId.ToString())
            .OrderCreated(order.Id, order.Status);
    }
}

Client Implementation (JavaScript)

typescript
// Install: npm install @microsoft/signalr

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

class SignalRService {
  private connection: signalR.HubConnection;

  constructor() {
    this.connection = new signalR.HubConnectionBuilder()
      .withUrl("/hubs/chat", {
        accessTokenFactory: () => localStorage.getItem("token") || ""
      })
      .withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
      .configureLogging(signalR.LogLevel.Information)
      .build();

    this.registerHandlers();
  }

  private registerHandlers() {
    this.connection.on("ReceiveMessage", (user: string, message: string, timestamp: Date) => {
      console.log(`${user}: ${message}`);
      // Update UI
    });

    this.connection.on("UserConnected", (connectionId: string) => {
      console.log(`User connected: ${connectionId}`);
    });

    this.connection.onreconnecting((error) => {
      console.log("Reconnecting...", error);
    });

    this.connection.onreconnected((connectionId) => {
      console.log("Reconnected:", connectionId);
    });

    this.connection.onclose((error) => {
      console.log("Connection closed", error);
    });
  }

  async start() {
    try {
      await this.connection.start();
      console.log("SignalR Connected");
    } catch (err) {
      console.error("SignalR Connection Error:", err);
      setTimeout(() => this.start(), 5000);
    }
  }

  async sendMessage(user: string, message: string) {
    await this.connection.invoke("SendMessage", user, message);
  }

  async joinRoom(roomName: string) {
    await this.connection.invoke("JoinRoom", roomName);
  }

  async leaveRoom(roomName: string) {
    await this.connection.invoke("LeaveRoom", roomName);
  }
}

// Usage
const signalr = new SignalRService();
await signalr.start();
await signalr.sendMessage("John", "Hello everyone!");

How Do You Scale SignalR with a Redis Backplane?

bash
dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis
csharp
// Program.cs - Add Redis backplane for scaling
builder.Services.AddSignalR()
    .AddStackExchangeRedis(builder.Configuration.GetConnectionString("Redis")!, options =>
    {
        options.Configuration.ChannelPrefix = "MyApp";
    });

// With this configuration, messages are broadcast across all server instances
// through Redis pub/sub

Real-World Example: Live Dashboard

csharp
public interface IDashboardClient
{
    Task MetricsUpdated(DashboardMetrics metrics);
    Task OrderReceived(OrderSummary order);
    Task AlertTriggered(Alert alert);
}

public class DashboardHub : Hub<IDashboardClient>
{
    public override async Task OnConnectedAsync()
    {
        // Add to dashboard viewers group
        await Groups.AddToGroupAsync(Context.ConnectionId, "dashboard");
        await base.OnConnectedAsync();
    }
}

// Background service to push metrics
public class MetricsPublisher : BackgroundService
{
    private readonly IHubContext<DashboardHub, IDashboardClient> _hub;
    private readonly IMetricsService _metrics;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var metrics = await _metrics.GetCurrentMetricsAsync();

            await _hub.Clients.Group("dashboard").MetricsUpdated(metrics);

            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }
    }
}

// Order service pushing updates
public class OrderService
{
    private readonly IHubContext<DashboardHub, IDashboardClient> _hub;

    public async Task CreateOrderAsync(Order order)
    {
        // Save order...

        await _hub.Clients.Group("dashboard").OrderReceived(new OrderSummary
        {
            Id = order.Id,
            Customer = order.CustomerName,
            Total = order.TotalAmount,
            Timestamp = DateTime.UtcNow
        });
    }
}

Connection Limits and Performance

csharp
// Configure connection limits
builder.Services.AddSignalR(options =>
{
    options.MaximumReceiveMessageSize = 32 * 1024; // 32KB
    options.StreamBufferCapacity = 10;
    options.EnableDetailedErrors = builder.Environment.IsDevelopment();
    options.KeepAliveInterval = TimeSpan.FromSeconds(15);
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
    options.HandshakeTimeout = TimeSpan.FromSeconds(15);
});

// Use MessagePack for better performance
builder.Services.AddSignalR()
    .AddMessagePackProtocol();

Best Practices

SignalR best practices:

  • Use strongly-typed hubs for compile-time safety
  • Keep hub methods simple - delegate to services
  • Use groups for room-based features
  • Implement proper error handling
  • Add Redis backplane for multi-server deployments
  • Monitor connection counts and message throughput
  • Use MessagePack for better performance
  • Implement reconnection logic on the client

SignalR abstracts away the complexity of real-time communication. Use it for live dashboards, notifications, chat, and any feature where users expect immediate updates. For high-throughput scenarios, consider gRPC streaming instead.

Real-time features create engaging user experiences. SignalR makes implementing these features straightforward while handling connection management, reconnection, and scaling challenges for you.

Share this article

Related Articles