Avalonia solo in codiceEnglish

Dalla documentazione di RAVAL, la libreria MIT dei libri: la stessa che sta nel suo repository.

RServer — API JSON su Kestrel

Sorgenti: RAVAL\HTTP\ (RServer.cs, RRequest.cs) · namespace RAVAL. Spiegazione completa con esempi: TUTORIAL.md, capitolo 14; thread e await nel capitolo 2.

Un programma (con o senza finestre) risponde a richieste HTTP con dati JSON. Ogni percorso è collegato a un metodo con un nome che riceve la RRequest e restituisce l'oggetto da mandare; niente lambda, niente controller, niente attributi.

var server = new RServer(5080)
  .Get("/api/macchine", Elenco)                  // object? Elenco(RRequest r)
  .Get("/api/macchine/{id}", Macchina)
  .Post("/api/ordini", NuovoOrdine)
  .OnUiThread();                                 // i metodi possono toccare i controlli
server.Start();                                  // torna quando ascolta
...
server.Stop();

RServer

MembroFa
new RServer(porta = 0)il server; 0 = una porta libera scelta dal sistema
Get, Post, Put, Delete(percorso, metodo)un percorso con il suo metodo object? M(RRequest r); parametri tra graffe: /api/x/{id}
GetAsync, PostAsync(percorso, metodo)lo stesso con un metodo async Task<object?> M(RRequest r)
OnUiThread()i metodi girano sul thread dell'interfaccia (serve un'app Avalonia avviata)
OnAllInterfaces()ascolta su tutte le schede di rete, non solo su 127.0.0.1
Jsonle JsonSerializerOptions (camelCase, come JsonSerializerDefaults.Web); si cambiano prima dell'avvio
Start() · Stop()avvio e arresto senza await: tornano quando il server ascolta / è fermo
Run()avvia e resta fermo finché un altro thread chiama Stop: new Thread(server.Run).Start()
WaitStarted(limite)aspetta che il server avviato con Run sia in ascolto
StartAsync() · StopAsync()avvio e arresto con await
Dispose() · DisposeAsync()come Stop, per using e await using
Avviato, Addresses, Portalo stato; Addresses ha la porta vera («http://127.0.0.1:51234»)

Percorsi e opzioni si danno prima dell'avvio: dopo, InvalidOperationException.

RRequest

MembroDà
Param(nome)parametro del percorso, poi della query (?nome=); null se manca
Int(nome)lo stesso come int; se non è un numero lancia RHttpError(400)
Body<T>()il corpo JSON come oggetto; se manca o è sbagliato lancia RHttpError(400)
Textil corpo come testo, già letto
Header(nome)un'intestazione; null se manca
Metodo, PercorsoGET/POST..., il percorso chiesto
Contestol'HttpContext di ASP.NET Core, per tutto il resto

La risposta

Il metodo...Risposta
restituisce un oggetto200, JSON
restituisce null204, senza corpo
restituisce RResponse (Created(x), NotFound(msg), Error(codice, msg), new RResponse(codice, x))quel codice, con il suo corpo
lancia RHttpError(codice, messaggio)quel codice con { "error": "messaggio" } (RErrorMessage)
lancia un'altra eccezione500 con { "error": messaggio }; il server continua
percorso che non esiste404 di Kestrel, senza corpo

Perché è fatta così

  • Kestrel senza il resto di ASP.NET. WebApplication.CreateSlimBuilder, il log di ASP.NET spento, un MapMethods per percorso con un RequestDelegate che è un metodo della classe privata Rotta. Il programma vede solo RRequest e oggetti, non HttpContext e middleware.
  • Metodi con un nome. Come in tutto RAVAL: il metodo riceve la richiesta già letta (corpo compreso) e restituisce un oggetto; la chiamata sul thread dell'interfaccia passa per la classe Chiamata, così il Dispatcher riceve un metodo e non una lambda.
  • Errori che diventano risposte. Un parametro sbagliato o un corpo illeggibile sono un 400 senza che il metodo debba controllarli; un'eccezione qualsiasi è un 500 e non ferma il server.
  • Tre modi di avviarlo. Start/Stop per chi non usa ancora await, Run per chi ragiona con un thread dedicato (fermo su un ManualResetEventSlim che StopAsync accende), StartAsync/StopAsync per chi usa await.
  • Solo su 127.0.0.1 per default. Farsi raggiungere dalla rete è una scelta esplicita (OnAllInterfaces), perché da quel momento chiunque nella rete può chiamare l'API.
  • FrameworkReference Microsoft.AspNetCore.App, non un pacchetto: Kestrel arriva dal framework condiviso dell'SDK. Chi usa RAVAL come ProjectReference lo eredita nel suo runtimeconfig.json; chi usa la DLL con HintPath (come Albatros) no, e finché non usa RServer l'assembly ASP.NET non viene mai caricato.

Inciampi

  • StopAsync().GetAwaiter().GetResult() dal thread dell'interfaccia si blocca per sempre. Dentro StopAsync di ASP.NET un await torna sul contesto di chi chiama; se chi chiama è il thread dell'interfaccia fermo ad aspettare, nessuno va avanti. Per questo Start e Stop sono Task.Run(AvviaAsync) e Task.Run(FermaAsync) più GetAwaiter().GetResult(): il lavoro gira sul pool, dove non c'è un contesto da riprendere. TEST_RAVAL lo prova chiamando Stop dal thread dell'interfaccia.
  • ListenLocalhost non accetta la porta 0. Per «una porta libera su 127.0.0.1» si usa Listen(IPAddress.Loopback, 0); la porta vera si legge poi da app.Urls (Addresses).
  • Con OnUiThread una richiesta fatta dal thread dell'interfaccia, aspettata con Result, si blocca: il server aspetta il thread, il thread aspetta la risposta. Nelle prove si fa partire la richiesta e si fa girare il Dispatcher finché non arriva (classe AttesaCompito in TEST_RAVAL\ProveHttp.cs).
  • StopAsync dà alle richieste in corso al più 5 secondi, poi chiude comunque: una richiesta ferma ad aspettare il thread dell'interfaccia non tiene il programma aperto per sempre.