Comunicazione realtime tra ASP.NET Core e Javascript con GraphQL

di Moreno Gentili, in ASP.NET Core,

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

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

I più letti di oggi