{"id":3471,"date":"2021-07-19T08:05:45","date_gmt":"2021-07-19T08:05:45","guid":{"rendered":"https:\/\/cloudsurfers.it\/?p=3471"},"modified":"2021-07-23T07:34:29","modified_gmt":"2021-07-23T07:34:29","slug":"applicazioni-in-tempo-reale-utilizzando-blazor-webassemby-signalr-e-c9","status":"publish","type":"post","link":"https:\/\/cloudsurfers.it\/index.php\/applicazioni-in-tempo-reale-utilizzando-blazor-webassemby-signalr-e-c9\/","title":{"rendered":"Applicazioni in tempo reale utilizzando Blazor WebAssemby, SignalR e C#9"},"content":{"rendered":"\n<h3 class=\"wp-block-heading\">Cos\u2019\u00e8 Blazor<\/h3>\n\n\n\n<p>Blazor \u00e8 visto come un\u2019importante libreria per sviluppare applicazioni full stack usando .Net.<\/p>\n\n\n\n<p>Con l\u2019avvento delle Single Page Application, dove l\u2019interfaccia viene creata dinamicamente all\u2019interno della pagina HTML senza la necessit\u00e0 di ri-caricare la pagina stessa, \u00a0ha cominciato a diffondersi l\u2019utilizzo da parte degli sviluppatori di librerie o, addirittura framework, come React, Angular e Vue.<\/p>\n\n\n\n<p>Con Blazor, Microsoft \u00e8 intenzionata a fornire allo sviluppatore un unico mezzo con il quale sviluppare a 360\u00b0 sfruttando l\u2019esperienza accumulata nell\u2019ambiente .NET. Utilizzando codice .NET \u00e8 infatti possibile usufruire delle caratteristiche dei linguaggi pensati per le SPA come, ad esempio, ri-caricare solo parti della pagina che devono essere aggiornate, riducendo il carico sul server e rendendolo pi\u00f9 immediato all\u2019utente finale .<\/p>\n\n\n\n<p>Blazor utilizza le Razor Pages con le quali rappresentare l\u2019interfaccia utente e che possono essere annidate e anche ri-condivise in progetti differenti.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\"><strong>Differenza tra Blazor Server e Blazor WebAssembly<\/strong><\/h6>\n\n\n\n<p>Con Blazor Server (rilasciato con .NET Core 3) &nbsp;il codice viene elaborato su server e lo scambio tra client e server viene effettuato attraverso SignalR.<\/p>\n\n\n\n<p>Con Blazor WebAssembly (rilasciata con .NET Core 3.1)&nbsp; &nbsp;l\u2019applicazione viene compilata e eseguita direttamente nel browser e nello stesso processo. Tale applicazione, visto che il codice e le relative dipendenze vengono scaricate nel browser, pu\u00f2 essere utilizzata anche off-line. E\u2019 chiamato WebAssembly poich\u00e9 rispetta il target di compilazione standard per la programmazione, lo &nbsp;sviluppo di applicazioni client e server e la loro distribuzione.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Cos\u2019\u00e8 SignalR<\/h3>\n\n\n\n<div class=\"wp-block-group\"><div class=\"wp-block-group__inner-container is-layout-flow wp-block-group-is-layout-flow\">\n<p>E\u2019 una libreria per sviluppatori che aggiunge agli applicativi la funzionalit\u00e0 Real-Time. Permette al server sia di spedire  i dati al client non appena disponibili e senza aspettare la sua richiesta, sia di rimanere in attesa di una richiesta di nuove informazioni  da parte dello stesso client.<\/p>\n\n\n\n<p>Pu\u00f2 essere usato quindi per sviluppare applicativi come chat, dashboard, App di monitoraggio, App per la modifica simultanea dei documenti, moduli per vedere l\u2019avanzamento di progetti in real-time o attivit\u00e0 e applicazioni che richiedono aggiornamenti ad alta frequenza come i giochi .<\/p>\n\n\n\n<p>SignalR usa&nbsp;<em>gli hub per<\/em>&nbsp;comunicare tra client e server.<\/p>\n\n\n\n<p>Un hub \u00e8 una pipeline di alto livello che consente a un client e a un server di chiamare metodi l&#8217;uno sull&#8217;altro. Le comunicazioni con SignalR possono avvenire con l\u2019ausilio di due protocolli: Json (testo) e MessagePack (binario).<\/p>\n\n\n\n<p>La comunicazione avviene inviando messaggi, serializzati secondo il protocollo scelto, che contengono nome e parametri del metodo lato client . Chi \u00e8 in ascolto cerca di trovare una corrispondenza e una volta trovata, chiama il metodo e gli passa i parametri deserializzati.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"295\" src=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine1-1024x295.png\" alt=\"\" class=\"wp-image-3482\" srcset=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine1-1024x295.png 1024w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine1-300x87.png 300w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine1-768x222.png 768w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine1-600x173.png 600w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine1.png 1189w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Cenni di C#9<\/h3>\n\n\n\n<p>Nel progetto verranno utilizzate le ultime peculiarit\u00e0 di C# senza entrare nel dettaglio ma, semplicemente, mostrandone il loro utilizzo.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Definiamo come sar\u00e0 l&#8217;applicazione<\/h3>\n\n\n\n<p>L\u2019applicazione che andremo a costruire sar\u00e0 un\u2019applicazione web dove gli utenti potranno connettersi e partecipare ad una chat comune.<\/p>\n\n\n\n<p>Il nostro scopo sar\u00e0 creare un\u2019applicazione dove l\u2019utente pu\u00f2 :<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Connettersi ad una chat room<\/li><li>Vedere chi \u00e8 connesso in quel momento&nbsp;<\/li><li>Essere notificato dell\u2019avvenuta connessione di un nuovo utente o di chi ha deciso di lasciare<\/li><li>Vedere tutti messaggi scambiati nella chat<\/li><\/ul>\n\n\n\n<p>Le caratteristiche appena elencate saranno tutte <strong><em>informazioni in real-time<\/em><\/strong>.<\/p>\n<\/div><\/div>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"642\" height=\"361\" src=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine2.png\" alt=\"\" class=\"wp-image-3485\" srcset=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine2.png 642w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine2-300x169.png 300w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine2-640x361.png 640w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine2-600x337.png 600w\" sizes=\"auto, (max-width: 642px) 100vw, 642px\" \/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Creiamo la nostra solution e installiamo i pacchetti necessari.<\/h3>\n\n\n\n<p>Al fine di sviluppare il nostro esercizio \u00e8 necessario prima di tutto creare il progetto e installarvi gli opportuni pacchetti.<\/p>\n\n\n\n<p>Per iniziare, quindi, posizionarsi nella cartella dove si vuole lavorare (per me che sto lavorando su W10 \u00e8 &nbsp;<em>C:\\Sviluppo<\/em>) &nbsp;e eseguire il comando relativo (da linea di comando o da terminale di Visual Studio):<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><tbody><tr><td><code>cd C:\\Sviluppo<\/code><\/td><\/tr><tr><td><code>dotnet new blazorwasm --hosted --output BlazorChatTest<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Questo comando creer\u00e0 una soluzione composta da tre progetti:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><em>BlazorChatTest.Client<\/em>: che conterr\u00e0 tutte&nbsp; le classi utili alla rendering dell\u2019interfaccia utente.<\/li><li><em>BlazorChatTest.Server<\/em>: che conterr\u00e0 tutte le classi della nostra REST API.<\/li><li><em>BlazorChatTest.Shared<\/em>: che conterr\u00e0 classi e moduli utilizzati sia lato client che lato server.<\/li><\/ul>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"558\" height=\"302\" src=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine3.png\" alt=\"\" class=\"wp-image-3487\" srcset=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine3.png 558w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine3-300x162.png 300w\" sizes=\"auto, (max-width: 558px) 100vw, 558px\" \/><\/figure>\n\n\n\n<p>Ovviamente, se si vuole effettuare le modifiche in sicurezza, consiglio di inizializzare un repository in git con il seguente codice<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><tbody><tr><td><code>C:\\Sviluppo&gt; cd BlazorChatTest<\/code><\/td><\/tr><tr><td><code>dotnet new gitignore<\/code><\/td><\/tr><tr><td><code>git init<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Nell\u2019implementazione dell\u2019esercizio, il lato client avr\u00e0 la necessit\u00e0 di chiamare i metodi esposti dalla nostra REST API utilizzando il servizio <em>HttpClient<\/em>. Sar\u00e0, quindi, necessario installare il pacchetto <em>NuGet Microsoft.Extention.Http<\/em> eseguendo le seguenti istruzioni:<\/p>\n\n\n\n<p>Posizionarsi nella cartella del progetto client (<em>C:\\Sviluppo\\BlazorChatTest\\Client<\/em>)<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><tbody><tr><td><code>C:\\Sviluppo\\BlazorChatTest&gt; cd Client&nbsp;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>E eseguire il seguente comando<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><tbody><tr><td><code>dotnet add package Microsoft.Extensions.Http --version 5.0.0<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Essendo un\u2019applicazione real-time che si appoggia a SignalR sar\u00e0 necessario, sempre nella cartella client, installare anche il relativo pacchetto che ci permetter\u00e0 di comunicare con il lato server e viceversa<\/p>\n\n\n\n<p>Digitare quindi il seguente comando e premere invio<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><tbody><tr><td><code>dotnet add package Microsoft.AspNetCore.SignalR.Client --version 5.0.0<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Adesso che abbiamo installato tutti i pacchetti necessari, possiamo procedere all\u2019implementazione del nostro esercizio.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Mettiamo le basi per il nostro real-time<\/h3>\n\n\n\n<p>Registriamo il nostro hub!!!!!.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">Lato Server<\/h6>\n\n\n\n<p>Se tutto il procedimento precedente \u00e8 andato a buon fine vi troverete nel progetto <em>BlazorChatTest.Server <\/em>il file chiamato <em>Startup.cs <\/em>dove vengono registrati tutti i servizi necessari all\u2019App (metodo <code>ConfigureServices<\/code>) e il suo ordine di elaborazione richieste (metodo <code>Configure<\/code>)<\/p>\n\n\n\n<p>L\u2019hub, che non \u00e8 altro che un servizio, viene registrato come segue:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n public void ConfigureServices(IServiceCollection services)\n {\n      services.AddSignalR();\n      \/\/ ... omissis ...\n      services.AddResponseCompression(opts =&gt;\n      {\n         opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(\n                    new&#x5B;] { &quot;application\/octet-stream&quot; });\n      });\n }\n<\/pre><\/div>\n\n\n<p>E poi, successivamente mappato con il nome di <em>\/chathub<\/em><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\npublic void Configure(IApplicationBuilder app, IWebHostEnvironment env)\n        {\n            app.UseResponseCompression();\n            \/\/... omissis ...\n            app.UseEndpoints(endpoints =&gt;\n            {\n                endpoints.MapRazorPages();\n                endpoints.MapControllers();\n                endpoints.MapHub&lt;ChatHub&gt;(&quot;\/chathub&quot;);\n                endpoints.MapFallbackToFile(&quot;index.html&quot;);\n            });\n        }\n<\/pre><\/div>\n\n\n<p>Il metodo&nbsp; <code>endpoints.MapHub <\/code>\u00e8 quello con il quale la nostra classe <code>ChatHub <\/code>(di cui parleremo fra poco) fornir\u00e0 i metodi per lo scambio di informazioni.<\/p>\n\n\n\n<p>Dato che a larghezza di banda potrebbe essere una risorsa limitata, \u00e8 importante comprimere i dati scambiati tra client e server. La registrazione del servizio <code>ResponseCompression <\/code>viene in nostro aiuto.<\/p>\n\n\n\n<p>In generale \u00e8 corretto affermare che la riduzione delle dimensioni dei dati&nbsp; aumenta, in genere, la velocit\u00e0 di risposta di un&#8217;applicazione e riduce le dimensioni del payload.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">SignalR<\/h6>\n\n\n\n<p>A questo punto , creiamo una cartella chiamata <em>Hub<\/em> e aggiungiamo un file chiamato <em>ChatHub.cs<\/em>.<\/p>\n\n\n\n<p>Questo file conterr\u00e0 la nostra classe <code>ChatHub <\/code>ed un&#8217; interfaccia <code>IChatHub <\/code>che tipizzer\u00e0 i gestori di eventi da utilizzare.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n    public interface IChatHub\n    {\n        Task MessageAdded(Message message);\n        Task UserAdded(List&lt;User&gt; users);\n        Task UserDeleted(List&lt;User&gt; users);\n        Task PrivateMessageAdded(Message message);\n    }\n    public class ChatHub : Hub&lt;IChatHub&gt;\n    {\n      \/\/... omissis\n     }\n<\/pre><\/div>\n\n\n<p><em><span class=\"has-inline-color has-vivid-red-color\"><code>MessageAdded <\/code><\/span><\/em>verr\u00e0 utilizzato per avvisare che un messaggio \u00e8 stato aggiunto<\/p>\n\n\n\n<p><em><span class=\"has-inline-color has-vivid-red-color\"><code>UserAdded <\/code><\/span><\/em>avviser\u00e0 che un utente si \u00e8 connesso<\/p>\n\n\n\n<p><em><span class=\"has-inline-color has-vivid-red-color\"><code>UserDeleted <\/code><\/span><\/em>avviser\u00e0 che un utente ha lasciato la chat.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">Lato Client<\/h6>\n\n\n\n<p>Per stabilire una connessione real-time sul client \u00e8 necessario registrare un\u2019istanza <code><em>HubConnection <\/em><\/code>nel metodo <code><em>Main <\/em><\/code>della classe <code><em>Program <\/em><\/code>, utilizzando <code><em>HubConnecionBuilder<\/em><\/code><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n public class Program\n    {\n        public static async Task Main(string&#x5B;] args)\n        {\n            var builder = WebAssemblyHostBuilder.CreateDefault(args);\n           \/\/... omissis\n            builder.Services.AddSingleton&lt;HubConnection&gt;(sp =&gt; {\n                var navigationManager = sp.GetRequiredService&lt;NavigationManager&gt;();\n                return new HubConnectionBuilder() .WithUrl(navigationManager.ToAbsoluteUri(&quot;\/chathub&quot;))\n                  .WithAutomaticReconnect()\n                  .Build();\n            });\n            \/\/... omissis\n        }\n    }\n<\/pre><\/div>\n\n\n<p>Una volta registrata la connessione, dovr\u00e0 essere fatta partire con il metodo <code> StartAsync()<\/code> nella fase di inizializzazione dal componente principale <code>App.razor<\/code>&nbsp;<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n@code {\n    private CancellationTokenSource cts = new CancellationTokenSource();\n    protected override void OnInitialized()\n    {\n        ConnectWithRetryAsync(cts.Token);\n\t\tHubConnection.Closed += error =&gt;\n        {\n            return ConnectWithRetryAsync(cts.Token);\n        };\n    }\n    private async Task&lt;bool&gt; ConnectWithRetryAsync(CancellationToken token)\n    {\n        while (true)\n        {\n            try\n            {\n                await HubConnection.StartAsync(token);\n                return true;\n            }\n            catch when (token.IsCancellationRequested)\n            {\n                return false;\n            }\n            catch\n            {\n\t\t\t\tawait Task.Delay(5000);\n            }\n        }\n    }\n<\/pre><\/div>\n\n\n<p>E\u2019 importante notare che:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Il tentativo di connessione avviene in modo asincrono con il metodo <code>await HubConnection.StartAsync(token)<\/code>&nbsp;e quindi in modo indipendentemente dalla inizializzazione dell\u2019applicazione. Se non fosse cosi, infatti , un eventuale errore di connessione, provocherebbe il blocco di tutto il rendering dell\u2019applicazione stessa.<\/li><li>E che il sistema prova a connettersi ripetutamente e, anche quando i tentativi di connessione vengono esauriti, viene ristabilito il tentativo di connessione in modo manuale.<\/li><\/ul>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n catch when (token.IsCancellationRequested)\n {\n   return false;\n }\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\">Le classi condivise<\/h3>\n\n\n\n<p>Una volta fatte tutte le registrazioni del caso, analizziamo i file da implementare nel progetto.<\/p>\n\n\n\n<p>Blazor mette a disposizione le basi per la tipizzazione forte, e tutti i sui benefici, creando un progetto di nome BlazorChattest.Shared dove inserire tutti i file utilizzati sia dal client, che dal server.<\/p>\n\n\n\n<p>Queste classi verranno utilizzate sia come input\/output dalla REST API , sia dalle Razor Pages nel progetto client e saranno <em><strong><span class=\"has-inline-color has-black-color\">record immutabili<\/span><\/strong><\/em>.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">C# 9 e i record immutabili.<\/h6>\n\n\n\n<p>Una nuova peculiarit\u00e0 di C# 9 sono le classi immutabili le cui propriet\u00e0 non possono essere modificate nel corso della vita dell\u2019oggetto stesso. Il compilatore, infatti, non permetter\u00e0 che una classe di questo tipo venga modificata una volta istanziata se non dichiarandone una nuova istanza.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"850\" height=\"212\" src=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine10.png\" alt=\"\" class=\"wp-image-3499\" srcset=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine10.png 850w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine10-300x75.png 300w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine10-768x192.png 768w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine10-600x150.png 600w\" sizes=\"auto, (max-width: 850px) 100vw, 850px\" \/><\/figure>\n\n\n\n<p>I record (cosi si chiamano) &nbsp;sono utilizzati per restituire liste di dati o dettagli in sola lettura e non possono essere utilizzati per il binding dei dati lato client (verrebbe restituita un\u2019eccezione dal compilatore tutte le volte che l\u2019utente cerca di modificare il dato).E\u2019 necessario quindi appoggiarsi all\u2019utilizzo di classi standard per modificare le propriet\u00e0 della pagina.<\/p>\n\n\n\n<p>Nell\u2019immagine seguente ho illustrato il comportamento del compilatore nel caso di dichiarazione di una classe <code>ExampleRecord <\/code>e di successiva istanza e tentativo di modifica della sua propriet\u00e0 dichiarata <code>init <\/code>invece che <code>set<\/code>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"875\" height=\"540\" src=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine11.png\" alt=\"\" class=\"wp-image-3501\" srcset=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine11.png 875w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine11-300x185.png 300w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine11-768x474.png 768w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine11-600x370.png 600w\" sizes=\"auto, (max-width: 875px) 100vw, 875px\" \/><\/figure>\n\n\n\n<p>Il principale vantaggio di questi record e che i loro dati non posso essere modificati in modo erroneo, nel corso dello sviluppo dell\u2019applicazione, sia lato server che lato client. Per poter per\u00f2 garantire il binding \u00e8 necessario mantenere due modelli anzich\u00e9 uno. (il back side dei record)<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">Tipizziamo ancora un po&#8217;<\/h6>\n\n\n\n<p>Una cosa molto importante \u00e8 incapsulare tutte le chiamate alla nostra REST API in un\u2019unica classe per essere facilitati nelle operazioni di sincronizzazione delle REST API stesse.<\/p>\n\n\n\n<p>Nel progetto <em>Shared<\/em> aggiungiamo un file chiamato <em>ChatHttpClient.cs<\/em> che avr\u00e0 il compito di interrogare tutti i metodi REST API forniti dal nostro lato server.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n   public class ChatHttpClient\n    {\n        private readonly HttpClient http;\n        public ChatHttpClient(HttpClient http)\n        {}\n        public async Task&lt;Chat&#x5B;]&gt; GetChats()\n        {}\n        public async Task&lt;Chat&gt; GetChat(Guid chatId)\n        {}\n        public async Task&lt;User&gt; GetUser(Guid chatId, string username)\n        {}\n        public async Task&lt;HttpResponseMessage&gt; AddUser(AddingUserModel aum)\n        {}\n       \n        public async Task&lt;HttpResponseMessage&gt; AddChat(Chat cht)\n        {}\n        public async Task&lt;HttpResponseMessage&gt; AddMessage(AddingMessageModel amm)\n        {}\n        public async Task&lt;HttpResponseMessage&gt; DeleteUser(Guid chatId, string username)\n        {}\n    }\n}\n<\/pre><\/div>\n\n\n<p><em>N.B<\/em>. <em>I metodi sono vuoti poich\u00e9 \u00e8 necessario implementare prima la classe lato server che esporr\u00e0 le chiamate REST<\/em> <em>API<\/em>. Non ti preoccupare, lo faremo a breve!!!<\/p>\n\n\n\n<p>Un volta scritta la classe sar\u00e0 necessario registrarla nel nostro progetto <em>Client <\/em>e pi\u00f9 precisamente nel metodo <code>Main<em> <\/em><\/code>della classe<em> <\/em><code>Program<\/code><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\npublic static async Task Main(string&#x5B;] args)\n{\n\tvar builder = WebAssemblyHostBuilder.CreateDefault(args);\n\t\/\/... omissis\n\tvar baseAddress = new Uri(builder.HostEnvironment.BaseAddress);\n\t\/\/... omissis\n\tbuilder.Services.AddHttpClient&lt;ChatHttpClient&gt;(client =&gt; client.BaseAddress = baseAddress);\n\t\/\/... omissis\n}\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\">Ho proprio voglia di cominciare a chattare!!<\/h3>\n\n\n\n<p>Dopo aver registrato il nostro hub e chiarito alcune nozioni base, \u00e8 arrivata l\u2019ora implementare il nostro esercizio.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">La base<\/h6>\n\n\n\n<p>Per poter iniziare a chattare dobbiamo, prima di tutto, impostare la nostra classe controller lato server che ci fornir\u00e0 i nostri dati. Implementiamo, quindi , il nostro controller standard, chiamato <code>ChatController<\/code>, con l\u2019attributo <code>[ApiController]<\/code> ereditato dalla classe <code>Controller <\/code>e gli innestiamo, grazie alla <em>Dependency Injection<\/em> il nostro <code>hubContext <\/code>che ci servir\u00e0 a scatenare gli eventi in real-time che comunicheranno i dati al client.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n    &#x5B;ApiController]\n    &#x5B;Route(&quot;api\/&#x5B;controller]&quot;)]\n    public class ChatController : Controller\n    {\n        private IHubContext&lt;ChatHub, IChatHub&gt; hubContext;\n        public ChatController(IHubContext&lt;ChatHub, IChatHub&gt; chatHub)\n        {\n            this.hubContext = chatHub;\n        }\n        private static ConcurrentBag&lt;Chat&gt; chats = new ConcurrentBag&lt;Chat&gt; {\n          new Chat (&quot;Chat Room&quot;)\n        };\n        \/\/... omissis\n<\/pre><\/div>\n\n\n<p>In questo esercizio opteremo per una lista in memoria che simuler\u00e0 il nostro database e verr\u00e0 rappresentata dal record <code>Chat <\/code>(sentitevi liberi di replicare il tutto con un database persistente).<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n    public record Chat\n    {\n        public Guid Id { get; init; } = Guid.NewGuid();\n        public string Name { get; init; }\n        public List&lt; Message&gt; messages=new List&lt; Message&gt;();\n        public List&lt;User&gt; users = new List&lt;User&gt;();\n        \n        public Chat(string name)\n        {\n            Id = Guid.NewGuid();\n            Name = name;\n        }\n    }\n<\/pre><\/div>\n\n\n<p><code>Chat <\/code>conterr\u00e0 i dati relativi agli utenti connessi ed ai messaggi mandati tra di loro.<\/p>\n\n\n\n<p>Il record <code>User <\/code>conterr\u00e0 il nome dell\u2019utente e un id di tipo <code>Guid <\/code>e rappresenter\u00e0 gli utenti che scelgono di entrare in chat.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nnamespace BlazorChatTest.Shared\n{\n    public record User\n    {\n        public Guid Id { get; init; }\n        public string Name { get; init; }\n        public User(string name)\n        {\n            Name = name;\n            Id = Guid.NewGuid();\n        }\n    }\n    public record ExampleRecord\n    {\n        public string? exampleField { get; init; }\n    }\n}\n<\/pre><\/div>\n\n\n<p>Mentre il record <code>Message <\/code>rappresenter\u00e0 i messaggi scambiati tra gli utenti connessi e conterr\u00e0 quello che \u00e8 stato scritto dall\u2019utente (<code>Body<\/code>), l\u2019utente stesso (<code>User<\/code>) e la data di inserimento del messaggio (<code>InsertDate<\/code>).<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nnamespace BlazorChatTest.Shared\n{\n   public  record Message\n    {\n        public Message(string body,User user)\n        {\n            Body = body;\n            InsertDate = DateTime.Now;\n            User = user;\n        }\n        public User User { get; init; }\n        public Guid Id { get; init; } = Guid.NewGuid();\n        public string Body { get; init; }\n        public DateTime InsertDate { get; init; }\n    }\n}\n<\/pre><\/div>\n\n\n<p>A questo punto abbiamo identificato la classe lato server che ci fornir\u00e0 i dati e la classe lato client che la interrogher\u00e0 per recuperarli. Possiamo quindi iniziare a implementare la nostra App per raggiungere il nostro scopo.<\/p>\n\n\n\n<h5 class=\"wp-block-heading\">Connettersi ad una chat room<\/h5>\n\n\n\n<p>Prima di tutto cominciamo a caricare nel nostro menu laterale le chat disponibili (una sola nel nostro esercizio).<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"441\" height=\"334\" src=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine18.png\" alt=\"\" class=\"wp-image-3507\" srcset=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine18.png 441w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/Immagine18-300x227.png 300w\" sizes=\"auto, (max-width: 441px) 100vw, 441px\" \/><\/figure>\n\n\n\n<p>Per farlo \u00e8 necessario<\/p>\n\n\n\n<p>Aggiungere un metodo nel nostro controller chiamato <code>GetChats()<\/code>,<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n&#x5B;HttpGet()]\npublic IEnumerable&amp;lt;Chat&amp;gt; GetChats()\n{\n    return chats;\n}\n<\/pre><\/div>\n\n\n<p>Implementare il metodo <code>GetChats() <\/code>della classe <code>ChatHttpClient <\/code>con la chiamata al metodo REST API definito nel controller,<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\npublic async Task&lt;Chat&gt; GetChat(Guid chatId)\n{\n     return await this.http.GetFromJsonAsync&lt;Chat&gt;($&quot;api\/chat\/{chatId}&quot;);\n}\n<\/pre><\/div>\n\n\n<p>E implementare il caricamento della lista nella nostra Razor Page chiamata <em>NavMenu.razor<\/em><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n\t\t@*... omissis *@\n\t\t@foreach (var chat in chats.OrderBy(s =&gt; s.Name))\n\t\t{\n\t\t\tvar link = $&quot;chatroom\/{chat.Id}&quot;;\n\t\t\t&lt;li class=&quot;nav-item px-3&quot;&gt;\n\t\t\t\t&lt;NavLink class=&quot;nav-link&quot; href=&quot;@link&quot;&gt;\n\t\t\t\t\t&lt;span class=&quot;oi oi-list-rich&quot; aria-hidden=&quot;true&quot;&gt;&lt;\/span&gt; @chat.Name\n\t\t\t\t&lt;\/NavLink&gt;\n\t\t\t&lt;\/li&gt;\n\t\t}\n\t&lt;\/ul&gt;\n&lt;\/div&gt;\n}\n@code {\n\tprivate Chat&#x5B;] chats;\n\tprotected override async Task OnInitializedAsync()\n\t{\n\t\tchats = await ChatHttpClient.GetChats();\n\t}\n\t\/\/... omissis\n}\n<\/pre><\/div>\n\n\n<p>Da notare che <code><span style=\"color:#a84c0a\" class=\"has-inline-color\"><em>chatroom\/{chat.Id}<\/em><\/span><\/code> \u00e8 il link al nostro componente principale <em>ChatRoom.razor<\/em> che permetter\u00e0 all\u2019utente di registrarsi alla chat, di visualizzare i messaggi mandati da altri e vedere chi \u00e8 connesso in quel momento.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">Vedere chi \u00e8 connesso in quel momento<\/h6>\n\n\n\n<p>Prima di tutto si devono implementare i metodi&nbsp; lato server che gestiscano l\u2019aggiunta, in caso di nuova connessione, e la cancellazione, in caso di abbandono, degli utenti.<\/p>\n\n\n\n<p>Nella nostra classe <code>ChatController <\/code>aggiungiamo due metodi :<\/p>\n\n\n\n<p><code>AddUser <\/code>per aggiungere l\u2019utente appena connesso e <code>DeleteUser <\/code>per eliminare l\u2019utente che ha lasciato la chat<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n        &#x5B;HttpPut()]\n        public  ActionResult AddUser(&#x5B;FromBody] AddingUserModel aum)\n        {\n            var chat = chats.SingleOrDefault(t =&gt; t.Id == aum.ChatId);\n            if (chat != null)\n            {\n                var user = chat.users.SingleOrDefault(t =&gt; t.Name == aum.Name);\n                if (user == null)\n                {\n                    user  = new User(aum.Name);\n                    chat.users.Add(user);\n                    this.hubContext.Clients.All.UserAdded(chat.users);\n                }\n                return new JsonResult(user);\n            }\n            else\n            {\n                return NotFound();\n            }\n        }\n        &#x5B;HttpDelete()]\n        public ActionResult DeleteUser(Guid chatId, string username)\n        {\n            var chat = chats.SingleOrDefault(t =&gt; t.Id == chatId);\n            if (chat != null)\n            {\n                var user = chat.users.SingleOrDefault(t =&gt; t.Name == username);\n                if (user != null)\n                {\n                    chat.users.Remove(user);\n                    this.hubContext.Clients.All.UserDeleted(chat.users);\n                }\n                return new JsonResult(&quot;OK&quot;);\n            }\n            else\n            {\n                return NotFound();\n            }\n        }\n<\/pre><\/div>\n\n\n<p>N.B. <em>Attraverso l\u2019utilizzo di<code> hubContext.Client.All<\/code> potremo accedere agli eventi a noi utili come <code>UserDeleted <\/code>e <code>UserAdded<\/code>, che trasmetteranno al client la lista di utenti attualmente connessi.<\/em><\/p>\n\n\n\n<p>Lato client, invece, nella pagina chiamata <em>UserList.razor<\/em> , iniettiamo <code>HubConnection <\/code>ad inizio pagina,<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n@inject HubConnection HubConnection\n<\/pre><\/div>\n\n\n<p>e registriamo gli eventi SignalR nel metodo <code>OnInitializedAsync <\/code>(cio\u00e8 quando il componente viene inizializzato)<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n    protected override async Task OnInitializedAsync()\n    {\n        HubConnection.On&lt;List&lt;User&gt;&gt;(&quot;UserAdded&quot;, users =&gt;\n        {\n            chat.users = users;\n            StateHasChanged();\n        });\n        HubConnection.On&lt;List&lt;User&gt;&gt;(&quot;UserDeleted&quot;, users =&gt;\n        {\n            chat.users = users;\n            StateHasChanged();\n        });\n    }\n<\/pre><\/div>\n\n\n<p>Il nostro hub, grazie al metodo <code>On<\/code>, si mette \u2018<em>in ascolto<\/em>\u2019&nbsp; di due gestori eventi :<\/p>\n\n\n\n<p>&nbsp;<code>UserAdded <\/code>e <code>UserDeleted <\/code>ai quali il server comunicher\u00e0 la nuova lista di utenti attualmente collegati.<\/p>\n\n\n\n<p>Quando un gestore degli eventi viene \u2018<em>aggiornato\u2019<\/em>, il metodo <code>StateHasChanged()<\/code> permette al componente di visualizzare i nuovi dati attraverso la loro ri-renderizzazione.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">Essere notificato dell\u2019avvenuta connessione di un nuovo utente o di chi ha deciso di lasciare<\/h6>\n\n\n\n<p>Quando un utente si inserisce o abbandona la chat, oltre che aggiornare l\u2019elenco degli utenti connessi, l\u2019applicazione dovr\u00e0 notificare a tutti i presenti l\u2019evento appena accaduto mandando un messaggio.<\/p>\n\n\n\n<p>Per far questo \u00e8 necessario <\/p>\n\n\n\n<p>aggiungere al controller il metodo <code>AddMessage<\/code>, con il quale il nuovo messaggio verr\u00e0 associato alla chat e notificato al client con<code> this.hubContext.Clients.All.MessageAdded(msg)<\/code>,<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n        &#x5B;HttpPost()]\n        public ActionResult AddMessage(&#x5B;FromBody]  AddingMessageModel msgModel)\n        {\n            Guid chatId = msgModel.ChatId;\n           string userName= msgModel.UserName;\n            string message = msgModel.TextMessage;\n            var chat = chats.SingleOrDefault(t =&gt; t.Id == chatId);\n            if (chat != null)\n            {\n                var u= chat.users.SingleOrDefault(u =&gt; u.Name == userName);\n                if (u==null)\n                {\n                    u = new User(userName);\n                }\n                var msg = new Message(message,u);\n                chat.messages.Add(msg);\n                this.hubContext.Clients.All.MessageAdded(msg);\n                return new JsonResult(msg);\n            }\n            else\n            {\n                return NotFound();\n            }\n        }\n<\/pre><\/div>\n\n\n<p>implementare  il metodo <code>AddMessage <\/code>della classe <code>ChatHttpClient <\/code>con la quale si effettueranno le chiamata al server ,<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\npublic async Task&amp;lt;HttpResponseMessage&amp;gt; AddMessage(AddingMessageModel amm)\n{\n            return await this.http.PostAsJsonAsync&amp;lt;AddingMessageModel&amp;gt;(&quot;api\/chat&quot;, amm);\n}\n<\/pre><\/div>\n\n\n<p>e implementare la parte di codice nella Razor Page <em>ChatRoom.razor <\/em>che permetta di svolgere le seguenti operazioni:<\/p>\n\n\n\n<p>Fornire all\u2019utente una mini interfaccia per connettersi,<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n@if (!this.isChatting)\n{\n    &amp;lt;p&amp;gt;\n        Inserisci il tuo nome per iniziare a chattare\n    &amp;lt;\/p&amp;gt;\n    &amp;lt;input type=&quot;text&quot; maxlength=&quot;32&quot; @bind=&quot;@username&quot; \/&amp;gt;\n    &amp;lt;button type=&quot;button&quot; @onclick=&quot;@Chat&quot;&amp;gt;&amp;lt;span class=&quot;oi oi-chat&quot; aria-hidden=&quot;true&quot;&amp;gt;&amp;lt;\/span&amp;gt; Chat!&amp;lt;\/button&amp;gt;\n    \/\/ Error messages\n    @if (message != null)\n    {\n        &amp;lt;div class=&quot;invalid-feedback&quot;&amp;gt;@message&amp;lt;\/div&amp;gt;\n        &amp;lt;small id=&quot;emailHelp&quot; class=&quot;form-text text-muted&quot;&amp;gt;@message&amp;lt;\/small&amp;gt;\n    }\n}\n<\/pre><\/div>\n\n\n<p>e notificare che l\u2019utente si \u00e8 appena connesso implementando il metodo <code>Chat <\/code>che comunicher\u00e0 al server sia di aggiungere un utente (<code>ChatHttpClient.AddUser<\/code>), sia di aggiungere un messaggio (<code>SendAsync<\/code>).<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n    public async Task Chat()\n    {\n        \/\/ check username is valid\n        if (string.IsNullOrWhiteSpace(username))\n        {\n            message = &quot;E&#039; necessario inserire il nome&quot;;\n            return;\n        };\n        try\n        {\n            this.isChatting = true;\n            await Task.Delay(1);\n           \n            chat.messages.Clear();\n             string baseUrl = navigationManager.BaseUri;\n            var um = new AddingUserModel(chat.Id, username);\n           await ChatHttpClient.AddUser(um) ;\n          \n            await SendAsync($&quot;&#x5B;Notice] {username} \u00e8 entrato in chat.&quot;);\n        }\n        catch (Exception e)\n        {\n            message = $&quot;ERROR: Failed to start chat client: {e.Message}&quot;;\n            isChatting = false;\n        }\n    }\n<\/pre><\/div>\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n    private void BroadcastMessage(string name, string message)\n    {\n        var response = ChatHttpClient.AddMessage(new AddingMessageModel(this.chatid, name, message));\n    }\n    private async Task SendAsync(string message)\n    {\n        if (isChatting &amp;amp;&amp;amp; !string.IsNullOrWhiteSpace(message))\n        {\n            BroadcastMessage(username, message);\n            newMessage = string.Empty;\n        }\n    }\n<\/pre><\/div>\n\n\n<p>Una volta fatto questo possiamo eseguire il nostro progetto ed ecco qui!<\/p>\n\n\n\n<figure class=\"wp-block-video\"><video height=\"720\" style=\"aspect-ratio: 1280 \/ 720;\" width=\"1280\" controls src=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2021\/07\/My-Movie-ee.mp4\"><\/video><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Conclusioni<\/h3>\n\n\n\n<p>Blazor e SignalR ci hanno permesso di sviluppare un\u2019applicazione web con la quale chattare con altri utenti, vedere chi di loro \u00e8 connesso, ricevere la notifica degli utenti appena connessi e capire quali di questi ha abbandonato.<\/p>\n\n\n\n<p>E intuibile che permettere agli sviluppatori di implementare un\u2019applicazione web utilizzando solo .NET e C# potrebbe essere una grande vittoria. Ti permetterebbe di sviluppare applicazioni web in real-time senza dover imparare altre tecnologie, ma utilizzando tutti i vantaggi del framework .NET &nbsp;e le peculiarit\u00e0 di linguaggi come C# 9.<\/p>\n\n\n\n<p>E\u2019 importante capire, tuttavia, se tale tecnologia sar\u00e0 veramente in grado di sostituire il mondo JavaScript e\/o TypeScript. Certo, anche se non esiste ancora il concetto di stateful hot reloading &nbsp;(compilazione in real-time delle nuove modifiche effettuate senza dover stoppare e ripartire il progetto), Microsoft &nbsp;\u00e8 molto vicina a farlo.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Link utili<\/h4>\n\n\n\n<p><a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/c-9-0-on-the-record\/\" target=\"_blank\" rel=\"noreferrer noopener\">C# 9.0 on the record di Mads Torgensen<\/a><\/p>\n\n\n\n<p><a href=\"https:\/\/www.dotnetcurry.com\/aspnet-core\/realtime-app-using-blazor-webassembly-signalr-csharp9\" target=\"_blank\" rel=\"noreferrer noopener\">Using Blazor WebAssembly, SignalR and C# 9 to create Full-stack Real time Applications<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Cos\u2019\u00e8 Blazor Blazor \u00e8 visto come un\u2019importante libreria per sviluppare applicazioni full stack usando .Net. Con l\u2019avvento delle Single Page Application, dove l\u2019interfaccia viene creata dinamicamente all\u2019interno della pagina HTML &#8230;<\/p>\n","protected":false},"author":7,"featured_media":3547,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"wds_primary_category":0,"footnotes":""},"categories":[90,129],"tags":[94,128,111,149,150,151],"class_list":["post-3471","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-net-core","category-blazor","tag-net-core","tag-blazor","tag-c","tag-razor-pages","tag-real-time","tag-signalr"],"_links":{"self":[{"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/posts\/3471","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/users\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/comments?post=3471"}],"version-history":[{"count":0,"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/posts\/3471\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/media\/3547"}],"wp:attachment":[{"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/media?parent=3471"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/categories?post=3471"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/tags?post=3471"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}