.Net Core 2.1 ➕ SignalR ➕ JavaScript = ❤️
Introduction
Comme vous le savez, SignalR à été officiellement ré-écris pour .Net Core 2.1. D’ailleurs, qu’est-ce que SignalR? Bonne question, c’est une façon de communiquer avec un WebSocket, ou de façon dégradé, avec votre backend. Le serveur ici est écrit en .Net, mais si vous voulez avoir un serveur de WebSocket en JavaScript, vous avez d’autres choix (i.e.: Socket.IO + autres alternative). Cependant côté client, à mon avis, les autres librairies ne sont pas aussi bien développé que SignalR. SignalR vous permettra de vous y connecter avec quasi tous les langages connus. Afin de voir comment faire, le repository officiel GitHub de SignalR regorge d’exemples.
Comment marche les WebSocket?
Normalement, la première connection se fait avec un premier appel http(s). Ensuite, le lien se feras en WebSocket (ws ou wss). WS est l’accronyme pour WebSocket et le second S est pour Sécure, tout comme le S dans HTTPS.
But de l’article
Afin de ne pas recopier le tutoriel sur le site officiel, je vais bâtir ce qui suit:
- Un site web basé sur le modèle WebAPI afin d’éviter à avoir Razor + NPM + etc.
- Récupérer le JavaScript sans avoir NPM dans le projet
- Écrire le minimum de JavaScript (SignalR)
- Écrire le minimum de CSS (Picnic)
- Écrire le minimum de HTML (Only pure HTML)
Avec pour résultat:
- Un serveur qui exécutera en arrière plan un lancement d’événements aux 2 secondes sur un processus différent. (Voir BackgroundService)
- Un hub d’événements que nous appelleront ToastHub.
- Le front qui vous afficheras les notifications avec 3 niveaux d’alertes aléatoire
Création du projet
À priori, vous savez déjà comment créer le projet ;). Sinon, si vous êtes en ligne de commande, effectuez “dotnet new –help” et vous verrez les types de projets Template que vous avez sur votre pc. Normalement vous y trouverez WebAPI. Du coup dotnet new webapi
et n’oubliez pas que le “nom” du projet prendra le nom de votre répertoire (excepté si vous ajoutez l’argument a dotnet new). Dans mon cas, mon projet + espace de nommage sera HoNoSoFt.SignalR.Demo. 😉
Avant de continuer, assurez vous que ça fonctionne. F5 dans Visual Studio, ou dotnet run en ligne de commande. Normalement le site devrait s’ouvrir de lui-même.
Ajout des librairies +dépendances (DI)
Si vous avez la flemme, vous pouvez tout simplement ajouter le package NuGet directement dans le fichier csproj.
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.0.2" />
Sinon, si vous êtes plus du type à ouvrir le package manager, veuillez tout simplement ajouter AspNetCore.SignalR (Ici le AspNetCore est très important).
Si ce n’est pas déjà fait. Effectuer un build. Ça vous permettra de vous assurer que le package NuGet est bien arrivé sur votre machine.
Ouvrons le fichier Startup.cs et allons modifier la méthode Configure(…) et ConfigureServices(…). Nous allons ajouter les fichiers par défaut, les fichiers statique, les cookies, les politiques de CORS. Est-ce tout nécessaire? Probablement pas, mais c’est pas plus mal d’avoir ça de configurer.
public void ConfigureServices(IServiceCollection services) { // Enregistrement de nos services d'arrière plan (Injection de Dépendence) ////services.AddSingleton<IHostedService, ToastEventGenerator>(); // Fin de nos enregistrement de HUBs services.Configure(options => { options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); services.AddCors(options => options.AddPolicy("CorsPolicy", builder => { builder.AllowAnyMethod().AllowAnyHeader() .WithOrigins("http://localhost:5000") .AllowCredentials(); })); services.AddSignalR().AddJsonProtocol(options => { options.PayloadSerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); // Utile pour le JavaScript }); services.AddLogging(); // Pour voir un peu plus de logs et pouvoir utiliser ILogger<...> } public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseHsts(); } app.UseHttpsRedirection(); // Lignes ajoutées app.UseDefaultFiles(); app.UseStaticFiles(); app.UseCookiePolicy(); app.UseCors("CorsPolicy"); app.UseSignalR(routes => { routes.MapHub<Hubs.ToastHub>("/hubs/toast"); // <=== Ce sera utile pour la prochaine étape. }); // Fin de l'ajout app.UseMvc(); }
Ok, normalement vous devriez avoir une partie encore manquante. Votre implémentation de HUB.
Ici je met une implémentation qui n’utilise pas les authorizations. Du coup, vous ne pourrez pas avoir le nom de la personne qui se connecte. Histoire de garder ça simple et que ça marche partout (même Linux). Je n’ai pas activé l’identification par NTLM.
using System; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; namespace HoNoSoFt.SignalR.Demo.Hubs { //[Authorize] public class ToastHub : Hub { private readonly ILogger<ToastHub> _logger; public ToastHub(ILogger<ToastHub> logger) { _logger = logger; } public override Task OnConnectedAsync() { // Pour le Context.user, nous avons besoin de l'attribut Authorize(Authentication). _logger.LogInformation("Un utilisateur vient de se joindre."); return Task.CompletedTask; } public override Task OnDisconnectedAsync(Exception exception) { _logger.LogInformation("Un utilisateur vient de quitter."); return Task.CompletedTask; } } }
namespace HoNoSoFt.SignalR.Demo.Models { public enum ToastImportance { Low, Medium, High } }
Désormais, si vous exécuter votre projet. Ça devrait fonctionner (moyennant la correction de votre référence dans votre fichier Startup.cs).
Notez que nous n’avons toujours pas notre service en fond de tâche (BackgroundService).
Réccuppération de la librairie SignalR.js
Oui, c’est simple me direz-vous. Oui, mais non. Si vous êtes dans un projet en NodeJS oui, très simple. De même que, si votre projet vous pensez utiliser NPM/Bower ou Yarn. Dans ce cas-ci, gardons le projet sans références NPM.
Mais alors, comment réccuppérer le JavaScript et le glisser dans wwwroot/js/? C’est simple, ouvrez vous une invite commande, créez-vous un répertoire temporaire.
Exemple:
cd / mkdir tmp/signalr cd tmp/signalr npm init -f npm install @aspnet/signalr REM Vous pouvez ouvrir votre explorateur de fichier afin de copier/coller les fichiers qui seront dans: REM > node_modules\@aspnet\signalr\dist\browser\
Fichiers dans le répertoire
- signalr.js
- signalr.js.map
- signalr.min.js
- signalr.min.js.map
Les fichiers map, si vous ne le savez pas, seront utilisé lors de débuggage. Ça indique où se trouve le code dans les fichiers originaux. Dans notre cas, ce ne sera pas trop utile.
Copier ces fichiers dans votre répertoire “wwwroot/js/”.
Créer votre page html
Ici on va faire simple, nous avons du HTML pure ainsi que l’ajout des informations suivantes:
- Picnic.css (CDN)
- site.css (pas encore créé)
- SignalR comme librairie
Fichier: index.html
<!DOCTYPE html> <html lang="fr"> <head> <meta charset="utf-8" /> <title>SignalR Demo (Nothing fancy!)</title> <link rel="stylesheet" href="https://unpkg.com/picnic"> <link rel="stylesheet" href="./css/site.css" type="text/css" /> </head> <body style="zoom: 1;"> <nav> <a href="/" class="brand"><span>HoNoSoFt.SignalR.Demo</span></a> </nav> <main class="flex center"> <div> <p>Voici une petite démo avec SignalR.</p> <p><strong>SignalR Hub:</strong> <i>hubs/toast</i></p> </div> <div class="third" id="toastContainer"> </div> </main> <script src="./js/signalr.js"></script> <script> var _toastContainer = document.getElementById("toastContainer"); function addData(sender, importance, message) { var span = document.createElement("span"); span.innerText = "[" + importance + "] " + sender + " : " + message; span.className = "button stack " + getClassFromImportance(importance); _toastContainer.appendChild(span); } function getClassFromImportance(importance) { switch (importance) { case "High": return "error"; case "Medium": return "warning"; case "Low": return "success"; } } // Doit s'alligner avec ce que vous avez indiqué dans le Startup.cs // C'est ici que notre client va se connecter au HUB SignalR backend en WebSocket (ou dégradé) const connection = new signalR.HubConnectionBuilder() .withUrl("/hubs/toast") .configureLogging(signalR.LogLevel.Information) .build(); connection.on("SendNotification", function (sender, toastImportance, message) { addData(sender, toastImportance, message); }); connection.start().then(function () { var p = document.createElement("p"); p.innerText = "Connection au hub Toast réussi!"; p.className = "connected"; _toastContainer.appendChild(p); }); </script> </body> </html>
Ajout d’un minimum de style
Comme vous l’avez vu dans la section “head”, il nous manque 1 feuille de style.
La feuille de style est wwwroot/css/site.css:
#toastContainer { height: 300px; border: 1px #ddd solid; overflow: scroll; } main { padding: 80px 3em 0 3em; } .connected { color: forestgreen; font-weight: bold; }
Du coup, avant de continuer, vos fichiers devraient ressembler à ce qui suit:
Démarrer votre projet et regardez si au moins votre socket se connecte. (F12 et regarder la console sous chrome)
Ajout du BackgroundService pour générer des événements
Comme vous l’avez vu dans un article précédent, l’implémentation d’une classe qui hérite de BackgroundService implémente automatiquement IHostedService. Du coup, lorsque c’est enregistrer dans le Startup.cs, ça démarre automatiquement au lancement de l’application.
Créons un fichier BackgroundServices/ToastEventGenerator.cs:
using HoNoSoFt.SignalR.Demo.Hubs; using HoNoSoFt.SignalR.Demo.Models; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Hosting; using System; using System.Threading; using System.Threading.Tasks; namespace HoNoSoFt.SignalR.Demo.BackgroundServices { public class ToastEventGenerator : BackgroundService { private readonly IHubContext<ToastHub> _toastHubContext; /// <summary> /// The random, mais vous devriez utiliser la nouvelle classe secure random. /// </summary> private static Random _random = new Random((int)DateTime.Now.Ticks & 0x0000FFFF); public ToastEventGenerator(IHubContext<ToastHub> toastHubContext) { _toastHubContext = toastHubContext; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await BroadcastToastNotificationAsync((ToastImportance)_random.Next(0, 3), "Toast random") .ConfigureAwait(false); // Devrait être random await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false); } } private Task BroadcastToastNotificationAsync(ToastImportance toastImportance, string message) { return _toastHubContext .Clients .All .SendCoreAsync("SendNotification", new[] { "System", toastImportance.ToString(), message }); } } }
À ce moment-ci, vous avez sans doute dans votre Startup.cs une ligne en commentaire. Celle du BackgroundService, veuillez la réactiver.
// ... services.AddSingleton<IHostedService, ToastEventGenerator>(); // ...
Structure des fichiers (Final):
Redémarrer votre service et regardez le résultat 😀
Conclusion
Vous voyez, ce n’est pas si compliqué et ce qui est vraiment intéressant, c’est que ça fonctionne partout! Vous pouvez même utiliser un système tel que SignalR afin de faire des notifications en tant que Broker en se connectant sur un Bus de service tel que RabbitMQ, Azure Storage Queue, etc.
Code complet de la démo disponible sur GitHub.