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?