SignalR: Building Real-Time Web Applications with .NET

SignalR: Building Real-Time Web Applications with .NET

Introduction

[Explain real-time requirements: instant updates without polling; SignalR abstracts WebSockets, Server-Sent Events, Long Polling.]

Prerequisites

  • .NET 8+ SDK
  • Basic ASP.NET Core knowledge
  • Client app (JavaScript, Blazor, or .NET)

SignalR Architecture

flowchart LR CLIENT1[Client 1] -->|WebSocket| HUB[SignalR Hub] CLIENT2[Client 2] -->|WebSocket| HUB CLIENT3[Client 3] -->|Long Polling| HUB HUB --> REDIS[(Redis Backplane)] REDIS --> SERVER1[App Instance 1] REDIS --> SERVER2[App Instance 2]

Core Concepts

Component Purpose Usage
Hub Server-side endpoint Broadcast, send to specific clients
HubConnection Client connection Invoke server methods, receive events
Groups Logical client grouping Chat rooms, user sessions
Backplane Scale-out coordination Redis, Azure SignalR Service

Step-by-Step Guide

Step 1: Create SignalR Hub

using Microsoft.AspNetCore.SignalR;

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }

    public async Task SendToGroup(string groupName, string user, string message)
    {
        await Clients.Group(groupName).SendAsync("ReceiveMessage", user, message);
    }

    public async Task JoinGroup(string groupName)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
        await Clients.Group(groupName).SendAsync("SystemMessage", $"{Context.User?.Identity?.Name} joined {groupName}");
    }

    public override async Task OnConnectedAsync()
    {
        await Clients.Caller.SendAsync("SystemMessage", "Welcome to the chat!");
        await base.OnConnectedAsync();
    }
}

Step 2: Register SignalR in Program.cs

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();

var app = builder.Build();
app.MapHub<ChatHub>("/chathub");
app.Run();

Step 3: JavaScript Client

<script src="https://cdn.jsdelivr.net/npm/@microsoft/signalr@latest/dist/browser/signalr.min.js"></script>
<script>
    const connection = new signalR.HubConnectionBuilder()
        .withUrl("/chathub")
        .withAutomaticReconnect()
        .build();

    connection.on("ReceiveMessage", (user, message) => {
        const li = document.createElement("li");
        li.textContent = `${user}: ${message}`;
        document.getElementById("messagesList").appendChild(li);
    });

    connection.start().catch(err => console.error(err));

    document.getElementById("sendButton").addEventListener("click", () => {
        const user = document.getElementById("userInput").value;
        const message = document.getElementById("messageInput").value;
        connection.invoke("SendMessage", user, message).catch(err => console.error(err));
    });
</script>

Step 4: Blazor Client

@page "/chat"
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager Navigation

<ul id="messagesList">
    @foreach (var message in messages)
    {
        <li>@message</li>
    }
</ul>

<input @bind="userInput" placeholder="User" />
<input @bind="messageInput" placeholder="Message" />
<button @onclick="Send">Send</button>

@code {
    private HubConnection? hubConnection;
    private List<string> messages = new();
    private string? userInput;
    private string? messageInput;

    protected override async Task OnInitializedAsync()
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
            .WithAutomaticReconnect()
            .Build();

        hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            messages.Add($"{user}: {message}");
            InvokeAsync(StateHasChanged);
        });

        await hubConnection.StartAsync();
    }

    private async Task Send()
    {
        if (hubConnection is not null)
        {
            await hubConnection.SendAsync("SendMessage", userInput, messageInput);
        }
    }

    public async ValueTask DisposeAsync()
    {
        if (hubConnection is not null)
        {
            await hubConnection.DisposeAsync();
        }
    }
}

Step 5: Authentication & Authorization

[Authorize]
public class SecureChatHub : Hub
{
    public async Task SendMessage(string message)
    {
        var user = Context.User?.Identity?.Name;
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

Client with JWT:

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", { accessTokenFactory: () => getJwtToken() })
    .build();

Step 6: Scale-Out with Redis Backplane

builder.Services.AddSignalR()
    .AddStackExchangeRedis(options =>
    {
        options.Configuration.EndPoints.Add("localhost:6379");
    });

Step 7: Azure SignalR Service (Managed)

builder.Services.AddSignalR()
    .AddAzureSignalR(options =>
    {
        options.ConnectionString = builder.Configuration["Azure:SignalR:ConnectionString"];
    });

Advanced Patterns

Strongly Typed Hubs

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

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

Send to Specific User

public async Task SendPrivateMessage(string toUserId, string message)
{
    await Clients.User(toUserId).SendAsync("ReceivePrivateMessage", Context.User?.Identity?.Name, message);
}

Server-to-Client Streaming

public async IAsyncEnumerable<DateTime> StreamData([EnumeratorCancellation] CancellationToken cancellationToken)
{
    while (!cancellationToken.IsCancellationRequested)
    {
        yield return DateTime.UtcNow;
        await Task.Delay(1000, cancellationToken);
    }
}

Performance Optimization

  • Use MessagePack protocol for binary serialization
  • Implement connection throttling
  • Monitor connection count and message throughput
  • Use Azure SignalR Service for > 1000 concurrent connections
builder.Services.AddSignalR()
    .AddMessagePackProtocol();

Monitoring

// Application Insights custom telemetry
public class ChatHub : Hub
{
    private readonly TelemetryClient telemetry;

    public ChatHub(TelemetryClient telemetry) => this.telemetry = telemetry;

    public async Task SendMessage(string user, string message)
    {
        telemetry.TrackEvent("ChatMessageSent", new Dictionary<string, string> { { "User", user } });
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

Troubleshooting

Issue: Connection fails with 404
Solution: Verify app.MapHub<ChatHub>("/chathub") route matches client URL

Issue: Messages not received on all instances
Solution: Implement Redis backplane or Azure SignalR Service

Issue: High latency
Solution: Check network; use Azure SignalR Service for global distribution

Best Practices

  • Use automatic reconnection on client
  • Implement exponential backoff for reconnection
  • Secure hubs with authorization policies
  • Monitor connection lifecycle events
  • Use groups for scalable broadcast patterns

Key Takeaways

  • SignalR simplifies real-time communication with WebSocket abstraction.
  • Hubs centralize server-side logic; clients invoke methods and receive events.
  • Azure SignalR Service handles scale-out automatically.
  • Authentication integrates seamlessly with ASP.NET Core identity.

Next Steps

  • Build live dashboard with real-time metrics
  • Implement notification system
  • Add presence indicators (online/offline status)

Additional Resources


Which real-time feature will you add first?