Dès qu’on prononce les mots temps réel en .NET, SignalR arrive immédiatement dans la discussion. C’est presque un réflexe. WebSockets, hubs, méthodes, groupes… l’outil est puissant et éprouvé.
Mais avec un peu de recul, on se rend compte que beaucoup d’applications n’ont pas réellement besoin de toute cette mécanique.
Dans un backoffice, un dashboard, une interface d’administration ou un écran de suivi de traitement, le besoin est d’être notifié quand quelque chose change côté serveur.
C’est précisément là que SSE (Server-Sent Events) trouve toute sa place.
Le principe
SSE repose sur l’idée que le navigateur ouvre une connexion HTTP longue vers le serveur, et celui-ci envoie des messages dès qu’il a quelque chose à dire.
Pas de va-et-vient permanent, pas de négociation de protocole, juste un flux texte qui reste ouvert.
Et contrairement à ce qu’on pourrait croire, ce mécanisme n’a rien d’exotique : il est standardisé, supporté nativement par les navigateurs modernes, et fonctionne parfaitement au-dessus d’une infrastructure HTTP classique.
Pourquoi SSE
Quand un article est publié, quand une tâche longue progresse, quand une donnée est mise à jour en base, le client n’a pas besoin de répondre immédiatement. Il doit être informé.
SSE excelle dans ce scénario. Il est léger, lisible, facile à déboguer, et beaucoup moins intrusif qu’une solution full WebSocket.
SignalR reste évidemment pertinent pour du chat, de la collaboration en direct ou des interactions complexes. Mais pour pousser des événements serveur vers une UI, SSE fait souvent le travail avec beaucoup moins de friction.
SSE en ASP.NET Core
Côté serveur, l’implémentation est étonnamment directe. On expose un endpoint HTTP qui ne se ferme pas immédiatement, on définit le bon Content-Type, et on écrit dans la réponse au fil de l’eau.
Il faut forcer l’envoi des données, sinon le navigateur ne reçoit rien tant que le buffer n’est pas vidé.
app.MapGet("/api/events", async (HttpContext ctx) =>
{
ctx.Response.ContentType = "text/event-stream";
ctx.Response.Headers.Append("Cache-Control", "no-cache");
ctx.Response.Headers.Append("Connection", "keep-alive");
ctx.Response.Headers.Append("X-Accel-Buffering", "no");
var cancellationToken = ctx.RequestAborted;
var eventId = 0;
var lastHeartbeat = DateTime.UtcNow;
try
{
while (!cancellationToken.IsCancellationRequested)
{
var now = DateTime.UtcNow;
// Heartbeat toutes les 15 secondes
if ((now - lastHeartbeat).TotalSeconds >= 15)
{
await ctx.Response.WriteAsync($": heartbeat {now:O}\n\n");
await ctx.Response.Body.FlushAsync();
lastHeartbeat = now;
}
// Event toutes les 2 secondes
eventId++;
var eventData = new
{
type = "tick",
value = eventId,
at = now.ToString("O")
};
await ctx.Response.WriteAsync($"event: message\n");
await ctx.Response.WriteAsync($"id: {eventId}\n");
await ctx.Response.WriteAsync($"data: {System.Text.Json.JsonSerializer.Serialize(eventData)}\n\n");
await ctx.Response.Body.FlushAsync();
// Attendre 2 secondes avant le prochain event
await Task.Delay(2000, cancellationToken);
}
}
catch (OperationCanceledException)
{
// Client a fermé la connexion, c'est normal
}
});
Pas de framework additionnel, pas de dépendance lourde, juste de l’HTTP bien utilisé.
Le client : écouter plutôt que poller
Côté navigateur, l’API EventSource fait tout le travail. Elle ouvre la connexion, gère la reconnexion automatique et expose les événements reçus.
useEffect(() => {
const es = new EventSource("/api/events");
es.addEventListener("message", (e) => {
const data = JSON.parse((e as MessageEvent).data);
console.log("SSE:", data);
});
return () => es.close();
}, []);
À partir de là, on peut afficher une notification ou de déclencher un rechargement ciblé; on sort du schéma du polling toutes les 5 secondes.
Authentification
EventSource ne permet pas d’envoyer des headers personnalisés, ce qui complique l’utilisation d’un JWT passé via Authorization.
Si l’application utilise une authentification par cookie (ce qui est très fréquent pour un backoffice), tout fonctionne sans effort : le cookie est envoyé automatiquement.
Dans les cas où le Bearer token est incontournable, on a deux options. Passer le token dans l’URL, avec toutes les précautions que cela implique, ou abandonner EventSource pour un fetch en streaming afin de garder la main sur les headers. C’est un peu plus verbeux, mais parfaitement viable.
SSE en production
Un flux SSE reste une connexion longue. Il faut donc penser aux intermédiaires : reverse proxy, load balancer, timeouts.
Un heartbeat envoyé régulièrement suffit à éviter la majorité des coupures silencieuses. Une ligne commentée envoyée toutes les quinze secondes maintient la connexion active.
Pour les architectures multi-instances, le principe est le même qu’avec SignalR : l’événement doit être produit depuis une source partagée. Redis, Service Bus ect… font parfaitement l’affaire pour propager l’information vers les clients connectés.
Une approche qui fonctionne
Un pattern revient souvent : utiliser SSE uniquement comme un signal, et conserver le reste via des appels HTTP classiques.
Le serveur pousse un événement indiquant qu’un article a changé.
Le client reçoit l’information, puis appelle l’API REST pour récupérer la donnée à jour.
Ce découplage garde l’API propre, limite la taille des messages SSE et simplifie la maintenance.
SSE est un outil simple, parfois même sous-estimé, mais efficace quand on l’utilise au bon endroit.
Dans beaucoup des backoffices modernes, il permet d’obtenir une UI réactive, fluide et lisible… sans avoir l’impression de sortir l’artillerie lourde à chaque notification.
Télécharger le sample sur GitHub
https://github.com/xraboteu/sse-react-dotnet




