Développement Web

SignalR en ASP.NET Core 10 : chat temps réel, groupes, Redis backplane et OpenTelemetry

8 min de lecture

SignalR en 2026 : encore la voie royale pour le temps réel

Server-Sent Events (SSE) sont arrivés en natif dans ASP.NET Core 10 et couvrent élégamment les flux unidirectionnels (serveur → client). gRPC streaming gère parfaitement les RPC binaires bidirectionnels avec contrat fort. Mais pour le bidirectionnel sur navigateur, avec reconnexion automatique, groupes d’abonnés, protocole binaire MessagePack en option et scaling horizontal via Redis ou Azure SignalR Service, SignalR reste le compagnon le plus complet du développeur ASP.NET Core. La doc officielle le confirme : SignalR est intégré au framework partagé Microsoft.AspNetCore.App depuis .NET Core 3.0, et les améliorations 2026 portent sur l’observabilité (métriques d’authentification, métriques de circuit) et la qualité du protocole transport plutôt que sur l’API publique des Hubs.

Cette série bâtit une application de chat temps réel pas à pas : projet vide, premier Hub, client JavaScript natif, groupes, authentification, mise à l’échelle avec Redis, et instrumentation OpenTelemetry. Chaque étape inclut un signal de réussite vérifiable.

Étape 1 : Initialiser le projet et installer SignalR

SignalR fait partie du framework partagé ASP.NET Core, donc aucun package NuGet supplémentaire n’est nécessaire côté serveur. Créez un projet web vide :

dotnet new web -n ChatTempsReel
cd ChatTempsReel

Dans Program.cs, ajoutez le service SignalR :

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();
var app = builder.Build();
app.UseDefaultFiles();
app.UseStaticFiles();
app.Run();

Créez un dossier wwwroot à la racine du projet — il servira à héberger les fichiers HTML/JS du client. Signal de réussite : dotnet run démarre sans erreur et expose un serveur sur le port indiqué dans la console.

Étape 2 : Écrire un premier Hub

Créez le fichier Hubs/ChatHub.cs :

using Microsoft.AspNetCore.SignalR;

namespace ChatTempsReel.Hubs;

public class ChatHub : Hub
{
    public async Task EnvoyerMessage(string utilisateur, string message)
    {
        await Clients.All.SendAsync("RecevoirMessage", utilisateur, message);
    }
}

Un Hub est une classe C# qui expose des méthodes appelables depuis le client. La propriété Clients.All représente tous les clients connectés au Hub. SendAsync invoque une méthode JavaScript RecevoirMessage sur chacun d’eux, avec les paramètres sérialisés en JSON (ou MessagePack si configuré). Mappez ensuite le Hub dans Program.cs avant app.Run() :

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

Le chemin /chat sera l’URL d’attachement WebSocket utilisée par les clients. Signal de réussite : aucune exception au démarrage et l’endpoint http://localhost:5000/chat répond avec une page d’erreur SignalR qui indique « Connection ID required » — c’est le signe que le Hub est actif et attend une connexion correcte.

Étape 3 : Client JavaScript natif via npm

Initialisez un mini-projet npm dans le dossier wwwroot :

cd wwwroot
npm init -y
npm install @microsoft/signalr

La librairie cliente officielle vit dans le paquet npm @microsoft/signalr (changement de nom depuis @aspnet/signalr en .NET Core 3.0). Créez index.html :

<!DOCTYPE html>
<html>
<body>
  <input id="user" placeholder="Pseudo" />
  <input id="msg" placeholder="Message" />
  <button id="send">Envoyer</button>
  <ul id="log"></ul>
  <script type="module">
    import * as signalR from './node_modules/@microsoft/signalr/dist/browser/signalr.js';

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

    cnx.on("RecevoirMessage", (u, m) => {
      const li = document.createElement("li");
      li.textContent = u + " : " + m;
      document.getElementById("log").appendChild(li);
    });

    document.getElementById("send").onclick = () => {
      const u = document.getElementById("user").value;
      const m = document.getElementById("msg").value;
      cnx.invoke("EnvoyerMessage", u, m);
    };

    cnx.start().catch(err => console.error(err));
  </script>
</body>
</html>

L’appel withAutomaticReconnect() est essentiel : il déclenche jusqu’à quatre tentatives de reconnexion (0s, 2s, 10s, 30s par défaut) si le WebSocket se ferme. Ouvrez deux onglets sur http://localhost:5000/, tapez des messages des deux côtés. Signal de réussite : chaque message envoyé par un onglet apparaît instantanément dans la liste de l’autre.

Étape 4 : Groupes pour cibler des sous-ensembles de clients

Pour des salons de discussion thématiques (par exemple #dev et #marketing), SignalR fournit les groupes. Ajoutez à ChatHub :

public async Task RejoindreSalon(string salon)
{
    await Groups.AddToGroupAsync(Context.ConnectionId, salon);
    await Clients.Group(salon).SendAsync("Systeme", $"Un utilisateur a rejoint #{salon}");
}

public async Task EnvoyerASalon(string salon, string utilisateur, string message)
{
    await Clients.Group(salon).SendAsync("RecevoirMessage", utilisateur, message);
}

Côté JavaScript, ajoutez deux boutons « Rejoindre » et « Envoyer dans #dev » qui appellent cnx.invoke("RejoindreSalon", "dev") puis cnx.invoke("EnvoyerASalon", "dev", u, m). Un message envoyé dans #dev n’est reçu que par les clients ayant explicitement rejoint ce groupe. Signal de réussite : ouvrir trois onglets — deux dans #dev, un dans #marketing — confirmer que seuls les deux premiers reçoivent les messages du salon #dev.

Étape 5 : Sécuriser avec authentification

Restreindre l’accès au Hub à des utilisateurs authentifiés est une exigence quasi-systématique. Ajoutez l’authentification JWT et l’attribut [Authorize] :

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

[Authorize]
public class ChatHub : Hub
{
    public override Task OnConnectedAsync()
    {
        var nom = Context.User?.Identity?.Name ?? "anonyme";
        return Clients.All.SendAsync("Systeme", $"{nom} s'est connecté");
    }
}

Côté client, le token doit être transmis dans la query string parce que les WebSockets ne gèrent pas les headers Authorization personnalisés au démarrage :

const cnx = new signalR.HubConnectionBuilder()
  .withUrl("/chat", {
    accessTokenFactory: () => localStorage.getItem("jwt")
  })
  .build();

SignalR injecte automatiquement le token via l’URL ?access_token=... au moment de la handshake. Côté serveur, configurez JwtBearer pour lire ce paramètre :

options.Events = new JwtBearerEvents
{
    OnMessageReceived = ctx =>
    {
        var accessToken = ctx.Request.Query["access_token"];
        var path = ctx.HttpContext.Request.Path;
        if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/chat"))
            ctx.Token = accessToken;
        return Task.CompletedTask;
    }
};

Signal de réussite : un client sans token reçoit immédiatement une erreur 401 et la connexion WebSocket échoue ; avec un token valide, le Hub démarre normalement et affiche le pseudo authentifié.

Étape 6 : Mise à l’échelle horizontale avec Redis

Si vous prévoyez de servir plus de 5 000 connexions concurrentes ou si vous avez plusieurs instances de votre application derrière un load balancer, vous devez introduire un backplane. SignalR supporte deux options officielles : Redis (open source, auto-hébergé) et Azure SignalR Service (managé). Pour Redis, installez :

dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis

Configurez :

builder.Services.AddSignalR()
    .AddStackExchangeRedis("localhost:6379", options => {
        options.Configuration.ChannelPrefix = StackExchange.Redis.RedisChannel.Literal("ChatApp");
    });

Lancez Redis localement via Docker pour tester :

docker run -d -p 6379:6379 --name redis-signalr redis:7-alpine

Lancez maintenant deux instances de votre application sur les ports 5001 et 5002 (dotnet run --urls "http://localhost:5001"). Ouvrez deux navigateurs pointant chacun sur un port différent. Un message envoyé sur le port 5001 est reçu par le client connecté au port 5002 grâce à Redis qui relaie le message entre les deux instances. Attention : avec Redis backplane, votre load balancer doit activer les sticky sessions (cookie de routage) car SignalR exige qu’une connexion donnée reste sur la même instance pour toute sa durée. Signal de réussite : redis-cli MONITOR affiche les messages PubSub circulant entre les instances en temps réel.

Étape 7 : Protocole MessagePack pour réduire la bande passante

Par défaut, SignalR transporte les messages en JSON. Pour des charges utiles importantes (envoi régulier de tableaux de quotes financières, télémétrie IoT, jeux), MessagePack est environ 30 à 50 % plus compact. Installez :

dotnet add package Microsoft.AspNetCore.SignalR.Protocols.MessagePack

Configurez côté serveur :

builder.Services.AddSignalR().AddMessagePackProtocol();

Côté client JavaScript :

npm install @microsoft/signalr-protocol-msgpack
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";

const cnx = new signalR.HubConnectionBuilder()
  .withUrl("/chat")
  .withHubProtocol(new MessagePackHubProtocol())
  .build();

Le serveur et le client négocient automatiquement le protocole au handshake. Signal de réussite : Wireshark ou l’onglet Network du navigateur affiche des frames WebSocket binaires (au lieu du JSON lisible), et la taille moyenne par message chute sensiblement sur une charge réaliste.

Étape 8 : Observabilité avec OpenTelemetry

SignalR expose nativement des Activity et Meter compatibles OpenTelemetry. Pour exporter vers un backend (Jaeger, Tempo, Application Insights), installez :

dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol

Configurez :

builder.Services.AddOpenTelemetry()
    .WithTracing(t => t.AddAspNetCoreInstrumentation().AddOtlpExporter())
    .WithMetrics(m => m.AddMeter("Microsoft.AspNetCore.SignalR.Server").AddOtlpExporter());

Les compteurs exposés incluent signalr.server.connection.count, signalr.server.connection.duration, et la durée de chaque invocation Hub. Visualisez ces métriques dans Grafana ou Application Insights : vous voyez en temps réel le nombre de connexions par instance, le temps moyen passé par les clients connectés, et les goulets d’étranglement éventuels sur les méthodes Hub. Signal de réussite : un dashboard Grafana montre une courbe régulière du nombre de connexions actives et la latence des invocations en sub-milliseconde sur un trafic local.

Bonnes pratiques pour production

Trois règles s’imposent. Premièrement, ne stockez jamais d’état métier dans un Hub ; les Hubs sont transients (créés à chaque invocation), utilisez plutôt un service DI singleton ou un cache distribué. Deuxièmement, isolez les méthodes longues (envoi d’email, calcul lourd) avec BackgroundService ou des Channels — un Hub bloqué pénalise toutes les invocations sur cette instance. Troisièmement, configurez explicitement le timeout serveur : options.KeepAliveInterval = TimeSpan.FromSeconds(15) et options.ClientTimeoutInterval = TimeSpan.FromSeconds(30) améliorent significativement la détection de clients morts comparée aux valeurs par défaut. Avec ces réglages et Redis derrière un load balancer sticky, une application SignalR gère sans peine 50 000 connexions concurrentes sur un cluster modeste de cinq nœuds.

Service ITSkillsCenter

Site ou application web sur mesure

Conception Pro + Nom de domaine 1 an + Hébergement 1 an + Formation + Support 6 mois. Accès et code livrés. À partir de 350 000 FCFA.

Demander un devis
Publicité