In uno scorso articolo abbiamo iniziato a conoscere GraphQL e il modo innovativo in cui permette di realizzare API per il web (https://www.aspitalia.com/articoli/asp.net-core3/creare-api-graphql-aspnetcore-hotchocolate.aspx).
Oltre alle già citate query e mutation, ovvero le operazioni di lettura e scrittura, possiamo compiere anche un terzo tipo di operazione chiamato subscription. Grazie ad essa, il client può mettersi in ascolto di eventi che si verificano lato server ed essere notificato in tempo reale via WebSocket.
Le subscription possono risultare utili in varie situazioni:
- Quando vogliamo che il client sia notificato di mutation apportate da altri client, come in un'applicazione chat in cui i vari utenti si scambiano messaggi;
- Quando il client deve essere notificato del completamento di un'operazione asincrona di lunga durata iniziata da lui stesso, come la produzione di un report complesso;
- Quando abbiamo applicazioni eventually consistent, in cui la mutation non altera immediatamente i dati ma viene propagata nel sistema da un bus messaggi o un event store.
Inviare subscription dal client
Le subscription rispettano la stessa sintassi di query e mutation. Come si vede nel seguente esempio, inviamo una subscription per essere notificati della creazione di nuovi prodotti. Quando ciò avviene, il client riceverà dal server i valori dei campi id, name e price del prodotto appena creato.subscription { onProductCreate { id name price } }
Eventualmente possiamo indicare degli argomenti, per fare in modo che il client riceva solo quegli eventi che rispettano determinati criteri. Ad esempio possiamo indicare un categoryId per ricevere solo i nuovi prodotti appartenenti a una certa categoria.
subscription { onProductCreate (categoryId: 1) { id name price } }
Ciò che differenza le subscription è che sono inviate al server tramite WebSocket anziché tramite una comune richiesta ajax. In questo modo, il client potrà mantenere attiva una connessione bidirezionale che il server userà per inviare gli eventi in tempo reale.
Per agevolare questo compito, esistono librerie JavaScript come Apollo Client (https://www.apollographql.com/docs/react/) che può essere integrato in applicazioni React, Vue, Angular o mobile. Ciò non significa che usare tali librerie sia obbligatorio. Infatti, con JavaScript possiamo stabilire noi stessi la connessione WebSocket e inviare dati seguendo il protocollo delle subscription di GraphQL (https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md.
const subscription = `subscription { onProductCreate (categoryId: 1) { id name price } }`; //Creo la connessione WebSocket indicando graphql-ws come protocollo applicativo const socket = new WebSocket("wss://localhost:5001/api", "graphql-ws"); //All'apertura della connessione invio la subscription socket.onopen = function(e) { console.log("Connessione stabilita, invio la subscription..."); socket.send(JSON.stringify({type:"connection_init",payload:{}})); socket.send(JSON.stringify({id:"1",type:"start",payload:{query:subscription}})); }; //Il server ha inviato dati socket.onmessage = function(event) { const data = JSON.parse(event.data); switch (data.type) { case "connection_ack": //Subscription attiva console.log("Il server conferma che la sottoscrizione è attiva, da ora riceveremo eventi!"); break; case "data": //Stampo il nuovo prodotto in console console.log("Sono arrivati dati!", data.payload.data.onProductCreate); //Ora che abbiamo ricevuto i dati, potremmo arrestare la subscription //socket.send(JSON.stringify({id:"1",type:"stop"})); break; } };
Il codice di questo esempio è minimale e non implementa alcuni accorgimenti importanti, come la riconnessione del client in caso di disconnessioni accidentali. Per questo è consigliabile usare una delle librerie JavaScript a disposizione.
Definire le subscription dal server
Vediamo ora come usare HotChocolate per definire una subscription lato server. Iniziamo installando i necessari pacchetti NuGet.dotnet add package HotChocolate.AspNetCore dotnet add package HotChocolate.Subscriptions dotnet add package HotChocolate.Subscriptions.InMemory
HotChocolate è in grado di gestire le connessioni dei client grazie a un provider in-memory ma per applicazioni distribuite è più indicato usare il pacchetto HotChocolate.Subscriptions.Redis che fornirà il provider per Redis.
Ora crediamo il file ProductSubscriptions.cs all'interno di una directory qualsiasi, come Models/Subscriptions.
public class ProductSubscriptions { //Dichiaro che la subscription richiede un argomento categoryId public Product OnProductCreate(int categoryId, IEventMessage message) { //Il metodo verrà invocato al verificarsi dell'evento //Restituisco l'intero oggetto al client return (Product)message.Payload; } }
Ora registriamo questa subscription nel metodo ConfigureServices della classe Startup.
var schemaBuilder = SchemaBuilder .New() //Qui query e mutation aggiunte nell'articolo precedente .AddQueryType<ProductQueries>() .AddMutationType<ProductMutations>() //Ed ecco la subscription .AddSubscriptionType<ProductSubscriptions>() ; services.AddGraphQL(schemaBuilder); //Usiamo il subscription provider in-memory services.AddInMemorySubscriptionProvider();
E nel metodo Configure della classe Startup usiamo il middleware che abilita WebSocket.
const string queryPath = "/api"; app.UseWebSockets().UseGraphQL(queryPath);
Ora dobbiamo occuparci di notificare l'evento al verificarsi della mutation CreateProduct che già avevamo definito in precedenza.
public async Task<Product> CreateProduct( Product input, [Service] ProductDbContext dbContext, [Service] IEventSender eventSender) //Questo è il servizio che ci permette di notificare eventi. { //OMISSIS: Qui persistiamo i dati con il DbContext //Creiamo l'evento var productCreateEvent = new OnProductCreate(product); //Notifichiamo l'evento await eventSender.SendAsync(productCreateEvent); return product; }
L'evento OnProductCreate è una classe che dobbiamo definire in un file di codice qualsiasi, come Models/Events/OnProductCreate.cs.
public class OnProductCreate : EventMessage { public OnProductCreate(int categoryId, Product product) : base(CreateEventDescription(categoryId), product) { } private static EventDescription CreateEventDescription(int categoryId) { //HotChocolate userà questo descrittore per determinare //quali client riceveranno l'evento return new EventDescription("onProductCreate", new ArgumentNode("categoryId", new IntValueNode(categoryId))); } }
Ora è tutto pronto: avviando il debug dell'applicazione noteremo che il client riceve eventi nel momento in cui viene creato un prodotto nella categoria indicata.
Grazie a HotChocolate siamo riusciti a realizzare una API che abbraccia query, mutation e subscription. Uno dei vantaggi è che tutte queste operazioni sono descritte da un'unica documentazione integrata nel Playground, che favorisce la loro trovabilità e l'integrazione con altri sistemi.
Commenti
Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.
Approfondimenti
Effettuare il log delle chiamate a function di GPT in ASP.NET Web API
Supporto ai tipi DateOnly e TimeOnly in Entity Framework Core
Eseguire query verso tipi non mappati in Entity Framework Core
Utilizzare Copilot con Azure Cosmos DB
Gestione degli stili CSS con le regole @layer
Criptare la comunicazione con mTLS in Azure Container Apps
Creare un webhook in Azure DevOps
Triggerare una pipeline su un altro repository di Azure DevOps
Eseguire operazioni sui blob con Azure Storage Actions
Eseguire i worklow di GitHub su runner potenziati
Evitare il flickering dei componenti nel prerender di Blazor 8
Disabilitare automaticamente un workflow di GitHub (parte 2)