Middleware in ASP.NET core

Cambiare la pipeline delle chiamate ASP.NET core grazie ai middleware.

il
Sviluppatore software, Collaboratore di IProgrammatori

Un middleware è una porzione di codice (classe) che viene eseguita ad ogni richiesta HTTP. Ogni middleware può decidere cosa fare con la richiesta:

  • Leggere / modificare i dati in ingresso
  • Far proseguire o interrompere la pipeline
  • Leggere / modificare i dati che la pipeline sta restituendo al chiamante

Per quanto possano sembrare concetti nuovi ed avanzati, nella pratica vengono usati in tutte le applicazioni ASP.NET core MVC. Pensiamo ad esempio ai seguenti metodi normalmente inseriti nel metodo Configure della classe Startup:

   if (env.IsDevelopment())
   {
      app.UseDeveloperExceptionPage();
   }
   app.UseAuthorization();
   app.UseRouting();

Con questo codice stiamo di fatto iniettando tre middleware nella pipeline:

  1. UseDeveloperExceptionPage: è un middleware che gestisce le eccezioni. Quando il codice eseguito nella pipeline genera un'eccezione, il middelware la intercetta e genera una pagina di errore che viene inviata al chiamante
  2. UseAuthorization: è un middleware che gestisce le autorizzazioni. Se l'utente ha i permessi, prosegue con la pipeline. In caso contrario manda al client una risposta con status-code "401 - Unauthorized"
  3. UseRouting: è il middleware che si occupa di gestire il routing, ovvero istanziare e richiamare il controller associato alla url richiesta

L'ordine in cui vengono aggiunti è molto importante, poichè definisce l'ordine con cui verranno eseguiti:

  • Il primo è il più esterno, pertanto il suo codice sarà il primo ad essere eseguito per le operazioni relative alla chiamata (da client a codice .NET), mentre sarà l'ultimo per quanto riguarda la risposta (da codice .NET a client)
  • L'ultimo è il più interno, pertanto il suo codice sarà l'ultimo ad essere eseguito per le operazioni relative alla chiamata (da client a codice .NET), mentre sarà il primo per quanto riguarda la risposta (da codice .NET a client)

Questo grafico illustra la pipeline di esecuzione del codice precedente:

Middleware pipeline

Creazione di un middleware personalizzato

Ora che sappiamo come opera un middleware, proviamo a scriverne uno nostro. Lo scopo sarà piuttosto semplice:

  • Le chiamate dispari verranno eseguite normalmente
  • Le chiamate pari, al contrario, torneranno sempre il testo "Middleware in esecuzione"

Tramite terminale creiamo la nostra soluzione:

dotnet new mvc -n LearningMiddleware

Apriamo la soluzione così creata da Visual Studio (o Visual Studio Code) e concentriamoci sul nostro middleware.

Come da definizione, il middleware è una classe che può essere agganciata alla pipeline. Perchè questo avvenga occorre rispettare alcune caratteristiche:

  • Avere un costruttore pubblico che accetti come primo parametro un RequestDelegate che è il delegato al prossimo elemento della pipeline
  • Avere un metodo pubblico chiamato Invoke o InvokeAsync il cui primo parametro è di tipo HttpContex ed è il contesto HTTP della chiamata

Sia il costruttore che il metodo Invoke / InvokeAsync possono avere parametri aggiuntivi che verranno gestiti in autonomia dal motore della dependency injection di ASP.NET core.

Scriviamo quindi la nostra classe rispettando i requisiti sopra descritti:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace LearningMiddleware
{

   /// <summary>
   /// Middleware personalizzato
   /// </summary>
   public class MyMiddleware
   {

      /// <summary>
      /// Puntatore al prossimo elemento nella pipeline
      /// </summary>
      private RequestDelegate Next { get; }

      /// <summary>
      /// Costruttore
      /// </summary>
      /// <param name="next">Puntatore al prossimo elemento nella pipeline</param>
      public MyMiddleware(RequestDelegate next)
      {
         Next = next ?? throw new ArgumentNullException(nameof(next));
      }

      /// <summary>
      /// Logica del middleware
      /// </summary>
      /// <param name="context">contesto http</param>
      public async Task InvokeAsync(HttpContext context)
      {
         RequestCounter++;
         if ((RequestCounter % 2) == 0)
         {
            await context.Response.WriteAsync("Middleware in esecuzione");
         }
         else
         {
            await Next(context);
         }
      }

      /// <summary>
      /// Contatore delle chiamate. E' statico così viene mantenuto fra le varie chiamate
      /// </summary>
      private static int RequestCounter = 0;
   }

}

A questo punto abbiamo un progetto MVC e un middleware da aggiungere alla pipeline. Per farlo basta aggiungere la chiamata al metodo UseMiddleware come prima riga del metodo Configure della classe Startup.

Dopo aver aggiunto suddetta riga, il metodo Configure apparirà in questo modo:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
   // Aggiunta del nostro middleware
   app.UseMiddleware<MyMiddleware>();

   ...
   Altre righe del metodo: lasciarle come sono
   ...
}

Siamo pronti per provare la nostra applicazione. Torniamo sul terminale, spostiamoci nella cartella LearningMiddleware creata in precedenza, quindi lanciamo il comando

dotnet run

Se tutto è andato a buon fine, si dovrebbe avviare l'applicazione.

Apriamo il browser e diamo come indirizzo: http://localhost:5001

Vedremo che la nostra applicazione verrà caricata. Facendo il refresh della pagina (pulsante sulla toolbar o F5), noteremo che si alterna la pagina del sito con la scritta "Middleware in esecuzione".