IL FILE DI PROGETTO .CSPROJ

NET COREAnalizziamo il file di progetto. Questo contenuto serve a dare istruzioni alla piattaforma di Build su come deve essere compilata e pubblicata la nostra applicazione. TargetFramework identifica la versione di .NET Core, nel nostro caso la 8. Nel momento in cui uscirà la versione nove ci basterà aggiornare questo valore. Non vi è riferimento ad alcun file sorgente, ossia file aventi estensione .cs in quanto non è necessario. Sarà la piattaforma di Build ad occuparsi di cercare e compilare tutti i sorgenti del progetto, non è più necessario specificarli tutti, caso mai si indicano i file che devono essere esclusi dalla compilazione. Facciamo una prova ed escludiamo il file Startup.cs.

Compile remove

Compiliamo ed otteniamo un errore in quanto il file non viene trovato nella classe Program.cs

Errore

ItemGroup raggruppa tutti i riferimenti ai pacchetti NuGet installati nel progetto. Al momento non ne abbiamo aggiunti. Proviamo ad aggiungere la dipendenza di SQLite, database che useremo nel progetto. Andiamo su NuGet.org e digitiamo SQLite. Come vedi la dipendenza è stata aggiunta con il seguente comando: dotnet add package Microsoft.Data.Sqlite.Core –version 8.0.1

sqlite

LA DIRECTORY BIN E OBJ E LA COMPILAZIONE DEL PROGETTO

La piattaforma di Build fa parte dell’SDK, la eseguiamo sul server di sviluppo non su quello di produzione dove va installato il runtime. Vediamo le fasi della compilazione di un progetto scatenate con il comando dotnet build.

Compilazione

Con il processo di compilazione otteniamo due nuove directory, bin e obj. obj è una sorta di directory temporanea dove vengono prodotti file che serviranno alla compilazione. La directory di output, dove ci sono i file della fase di compilazione, è la bin. MyCourse.dll è l’assembly che contiene tutto il codice compilato della nostra applicazione, il file MyCourse.pdb è molto utile durante la fase di debug del codice. Contiene la mappatura tra le linee di codice C# e le istruzioni contenute in MyCourse.dll. Le directory bin e obj le possiamo cancellare in qualsiasi momento in quanto vengono ricreate tali e quali dal comando dotnet build.

Directory bin

Vediamo la fase di esecuzione dell’applicazione.

Esecuzione

È necessario un ulteriore step di compilazione per trasformare il codice IL in codice macchina per quel particolare processore e architettura in uso. Questa fase di compilazione è totalmente trasparente e automatica, e noi non dobbiamo fare nulla. Possiamo chiederci perché Microsoft ha introdotto questa nuova fase di compilazione, cioè il codice C# non poteva essere compilato direttamente in linguaggio macchina? Il motivo è che le dll che contengono codice IL ci consentono di rendere la nostra applicazione facilmente portabile, supponiamo di volerla vendere, grazie alla presenza degli assembly possiamo venderla a chiunque indipendentemente da quale piattaforma voglia utilizzare; quindi, chi compra l’applicazione la può posizionare su Windows o su Linux senza necessità di una ulteriore compilazione.

PREPARARE IL WEB HOST DALLA CLASSE PROGRAM

Da .NET 6.0 possiamo fare a meno della classe Startup.cs, infatti Microsoft ha introdotto un nuovo approccio nella configurazione dell’applicazione. Questo nuovo approccio si chiama Minimal Hosting Model, e quindi potremmo cavarcela con il solo file Program.cs. Attenzione però, questo approccio è opzionale, e noi continueremo per il momento ad usare la vecchia configurazione. Vediamo la configurazione minimale della classe Startup.cs.

Classe Startup.cs

I metodi ConfigureService e Configure servono per configurare due aspetti dell’applicazione, dal metodo Configure decidiamo quali Middleware attivare, i Middleware sono moduli eseguiti ad ogni richiesta dell’utente, mentre ConfigureService serve ad attivare la Dependencies Injection e i servizi. Dopo la fase di Build, l’applicazione è pronta per essere eseguita. Quando viene eseguita entra in gioco la classe program.cs, che permette di ricevere richieste web dagli utenti, contiene il punto di ingresso della nostra applicazione, il metodo Main è il primo che viene chiamato dal .NET Core runtime.

Program.cs

La prima cosa che viene fatta nel metodo Main è la creazione di un Web Host, che contiene tutto quello che è a diretto contatto con il mondo esterno. Un componente in grado di ricevere richieste http dall’utente si chiama web server. Microsoft ha realizzato Kestrel che vive all’interno del processo dotnet, questa è una grossa differenza rispetto ad altre tecnologie come PHP, Apache, Nginx che vivono su processi separati. Il compito di Kestrel è raccogliere le richieste http, le trasforma un po’ e le rappresenta con un oggetto HttpContext.

WebHost

Esistono anche altre componenti all’interno del Web Host, come il componente di Logging utile se vogliamo scrivere valori diagnostici, un altro componente è la configurazione, dovremmo essere pur in grado di inviare parametri di configurazione all’applicazione.

Modularità

Tutte queste componenti sono modulari, cioè possono essere cambiate a nostro piacimento, anche il Web Server può essere cambiato con HTTP.sys, che ha però l’inconveniente di lavorare solo su Windows e il pregio di autenticare gli utenti con i loro account di dominio. Una cosa importante da dire è che .NET Core è auto contenuto, volendo possiamo installare anche il runtime e distribuire il tutto su una chiavetta USB. Con un doppio click l’applicazione verrà messa in esecuzione.

CONFIGURARE I PARAMETRI DI AVVIO CON LAUNCHSETTINGS.JSON

Quando avviamo l’applicazione con il comando dotnet run Kestrel sarà in ascolto su questi due end point.

end point

Questi end point sono impostati nel file launchSettings.json che si trova nella directory properties. Il nome dell’ambiente è molto importante quando, come vedremo, dobbiamo differenziare il comportamento tra sviluppo e produzione. Ad esempio, se si verifica un errore in fase di Development, vogliamo avere tutte le informazioni sulla natura dell’errore, mentre in ambiente Production è bene non diffondere certe informazioni, come il codice che stiamo sviluppando. In questo caso si visualizzerà un messaggio di cortesia per l’utente mentre le informazioni dettagliate dell’errore le scriveremo in un file di log.

LaunchSettings.json

USARE MIDDLEWARE NELLA CLASSE STARTUP

Tutte queste differenze comportamentali tra sviluppo e produzione vengono gestite attraverso dei Middleware. Il codice dell’applicazione che riceve l’oggetto HttpContext, è strutturato in Middleware che leggeranno questo oggetto, se necessario lo modificano e lo passano al Middleware successivo. Possono fare molte cose, accedere a un database, scrivere un file di log etc. Questi Middleware o sono prodotti da Microsoft, oppure li possiamo scaricare da NuGet o produrre noi stessi. È un meccanismo completamente modulare e saremo noi a decidere quanti e quali componenti utilizzare.

Middleware

Nel metodo Configure della classe Startup.cs, andremo a inserire tutta la serie di Middleware che ci servono in un determinato ambiente.

Configure

Il primo Middleware che abbiamo aggiunto quando siamo in sviluppo è il UseDeveloperExceptionPage() che si occupa di creare una schermata in caso di errore. L’ordine di aggiunta è molto importante, avendo messo questo Middleware per primo esso può catturare eventuali errori che possono sollevarsi nei Middleware successivi e catturare una schermata di errore dettagliata. Il Middleware successivo non ha nome, cattura l’oggetto HttpContext e scrive nella risposta di output. Vediamo come funziona tutto questo. Le stringhe hanno un metodo toUpper() per convertirle in maiuscolo e supponiamo che vogliamo che sia la variabile nome ad essere convertita. Se mettiamo in esecuzione l’applicazione, si verifica un errore non gestito catturato dal Middleware precedente.

Errore

Diciamo che il Middleware ci è stato molto utile a scovare l’errore; tuttavia, in produzione ciò non va fatto in quanto stiamo divulgando troppe informazioni sul nostro codice. Se proviamo a cambiare la stringa nel file launchSettings.json da Development in Production, il Middleware non verrà attivato e quello che otteniamo è un banale errore http 500.

Errore 500

Vediamo la situazione precedente graficamente, quella cioè in cui il Middleware interviene e gestisce l’errore.

Middleware Exception Page

L’altro caso è il seguente:

No Middleware

GESTIRE I FILE STATICI E LA DIRECTORY WWWROOT

I file statici vengono prelevati dal web server Kestrel così come sono dal disco e serviti all’utente. Tutti i file statici devono essere posti all’interno della cartella wwwroot. Se proviamo ad inserire un’immagine in wwwroot e facciamo una richiesta ci accorgiamo che questa non viene servita. Questo perché non abbiamo configurato il Middleware.

Middleware file statici

Il Middleware per i file statici analizzando HttpContext si è reso conto che la richiesta poteva essere servita da lui stesso; quindi, ha immediatamente restituito il contenuto al client.

logo.svg

RIEPILOGO DELLA SEZIONE

 Un’applicazione ASP.NET Core si programma con un linguaggio come C#, che è un linguaggio “compilato”. Vuol dire che prima di poter essere eseguito, il codice che abbiamo scritto sui file .cs deve attraversare una fase di compilazione che lo trasformerà in IL (Intermediate Language), che è un linguaggio byte code di livello più basso e perciò più vicino al codice eseguibile da una CPU. Questo processo di compilazione serve a far incontrare le esigenze degli sviluppatori, che vogliono lavorare con un linguaggio espressivo come C# e le esigenze delle macchine, che riescono a lavorare in maniera tanto più efficiente quanto più sono semplici le istruzioni da elaborare.

Un assembly .dll è portabile su una qualsiasi delle piattaforme supportate da .NET Core. Nel momento in cui viene eseguita l’applicazione, gli assembly sono caricati in memoria e, al bisogno, passano attraverso una nuova fase di compilazione ad opera del compiler JIT (Just-In-Time) che “traduce” in tempo reale le istruzioni dell’Intermediate Language in codice direttamente eseguibile dalla specifica CPU su cui viene eseguito.

LA PIATTAFORMA DI BUILD

Con .NET Core SDK è fornita una piattaforma di build in cui si trova il componente MSBuild che orchestra la compilazione e la pubblicazione del progetto. È in grado di trattare file di diverso tipo:

  • I file di codice con estensione .cs vengono passati al compilatore di linguaggio C# chiamato Roslyn e l’Intermediate Language risultante è inserito all’interno di uno o più assembly, cioè file .dll che troveremo all’interno della directory bin;
  • Vengono prodotti anche dei file .json che indicano a .NET Core Runtime qual è la versione di .NET Core usata e da quali altre librerie l’applicazione ASP.NET Core dipende;
  • In fase di pubblicazione, vengono anche copiati dei file statici come il contenuto della directory wwwroot o il file web.config che agevola la messa in produzione alle spalle del webserver IIS che agirà da reverse proxy (su questo torneremo in futuro).

Per eseguire la compilazione abbiamo a disposizione vari comandi:

  • dotnet build esegue la compilazione ma non copia i file statici;
  • dotnet publish esegue sia la compilazione e copia anche i file statici. Questo è il comando da eseguire quando vogliamo preparare tutto il contenuto in una directory che poi andremo a copiare sul server di produzione via FTP o con altra tecnica;
  • dotnet run scatena implicitamente la build (se necessario), dato che è un requisito per poter eseguire l’applicazione.

Quando un’applicazione è stata compilata, possiamo eseguire il comando dotnet MyCourse.dll per avviarla, dove MyCourse.dll è il percorso dell’assembly che è stato creato a seguito della build. Questo è il comando da eseguire sul server di produzione, dato che lì abbiamo installato .NET Core Runtime, che NON contiene la piattaforma di build e quindi i comandi come dotnet build e dotnet run non funzionerebbero.

PERSONALIZZARE LA BUILD

Il file .csproj contiene le istruzioni per la piattaforma di build, che così potrà compilare correttamente il progetto. Alcune delle informazioni che riporta sono:

  • La cosiddetta project SDK che indica alla piattaforma di build come compilare un’applicazione ASP.NET Core, che ha peculiarità diverse da quelle di altri tipi di applicazione;
  • Il target framework, ovvero la specifica versione di .NET Core che intendiamo usare;
  • Un elenco di file statici da includere all’atto della pubblicazione, oppure di file di codice da escludere dalla compilazione;
  • Un elenco di riferimenti a pacchetti NuGet da cui la nostra applicazione dipende.

IL WEB HOST

La classe Program, contenuta nel file Program.cs, possiede il metodo Main che è anche detto l’entry point dell’applicazione perché è il primo ad essere invocato al suo avvio. Dal metodo Main viene preparato il web host che è l’insieme dei componenti essenziali che servono all’applicazione a relazionarsi con il mondo esterno. Ad esempio, uno di tali componenti è il web server Kestrel, che permette all’applicazione di ricevere richieste HTTP. I componenti del web host possono essere configurati o sostituiti a piacimento, in base alle specifiche esigenze dell’applicazione che dobbiamo realizzare.

Per la maggior parte degli scenari, la configurazione di default del web host è più che sufficiente. Tuttavia, se vogliamo sbirciare come viene costruito il web host di default, possiamo consultare il codice sorgente di ASP.NET Core nel suo repository GitHub. Ecco un link allo specifico punto in cui viene preparato:

https://github.com/aspnet/MetaPackages/blob/d417aacd7c0eff202f7860fe1e686aa5beeedad7/src/Microsoft.AspNetCore/WebHost.cs#L148

Si può notare che al suo interno vengono configurati:

  • Il web server Kestrel e l’integrazione con IIS;
  • La definizione delle fonti di configurazione (che vedremo in seguito);
  • Il logging su console;
  • Il service provider (che vedremo in seguito).

I MIDDLEWARE

La classe Startup, definita all’interno del file Startup.cs, viene anch’essa coinvolta durante la costruzione del web host. Questa classe possiede un metodo Configure da cui indichiamo quali middleware usare nell’applicazione.

middleware sono componenti che offrono una propri funzionalità specifica e sono chiamati a gestire le richieste HTTP in ingresso. Sono disposti l’uno dopo l’altro in una serie ordinata che è anche chiamata “pipeline”. Ogni middleware ha facoltà di:

  • Interagire con l’oggetto HttpContext che rappresenta il contesto di esecuzione corrente. Questo oggetto può essere usato dal middleware per leggere le informazioni passate con la richiesta, per aggiungervi informazioni arbitrarie o riscriverla del tutto;
  • Quando ha terminato il suo lavoro, il middleware può cedere la gestione della richiesta al middleware successivo, oppure impedire che questo avvenga. Quindi, non è detto che ogni richiesta venga gestita da tutti i middleware presenti nella serie;
  • Produrre una risposta per il client o riscrivere o integrare la risposta prodotta in precedenza da un altro middleware.

L’ordine in cui usiamo i middleware è importante: il primo middleware della serie sarà il primo a gestire la richiesta, ma anche l’ultimo a poter intervenire sulla risposta che viene inviata all’utente.

Ogni middleware offre una funzionalità specifica come ad esempio:

  • Intercettare gli errori (ad esempio invocando app.UseDeveloperExceptionPage());
  • Servire file statici (invocando app.UseStaticFiles());
  • Loggare le richieste;
  • Autenticare e autorizzare l’utente;
  • Fare routing o riscrivere l’url della richiesta;
  • Comprimere le risposte con l’algoritmo Gzip, in modo che richiedano meno banda;
  • Regolare l’accesso all’applicazione in base all’indirizzo IP di provenienza;
  • … e così via.

Come regola generale, possiamo dire che ogni funzionalità applicativa deve essere aggiunta con un middleware.

CONTENUTI STATICI E LA DIRECTORY WWWROOT

Se vogliamo che l’applicazione sia in grado di fornire file statici, dovremo aggiungere l’apposito middleware invocando app.UseStaticFiles() dal metodo Configure della classe Startup. Ogni file statico, come un’immagine jpg, un foglio di stile .css o un file di codice .js, deve essere posizionato all’interno di wwwroot o di una delle sue sottodirectory. Infatti, qualsiasi cosa si trovi all’esterno di wwwroot non sarà visualizzabile nel browser o comunque scaricabile mediante richiesta web. Perciò ogni file riservato, come un database Sqlite o un file di log dovrà essere posto al di fuori di wwwroot.

INTERCETTARE GLI ERRORI

Allo stesso modo, se vogliamo intercettare gli errori e visualizzarli in una pagina informativa, dovremo aggiungere anche in questo caso un apposito middleware con app.UseDeveloperExceptionPage(). Questo middleware è consigliato solo in ambiente di sviluppo, dato che divulga informazioni sul nostro codice sorgente. Per questo motivo, è importante aggiungerlo in maniera condizionale grazie a un if nel metodo Configure della classe Startup.

  1. if(env.IsDevelopment())
  2. {
  3.   UseDeveloperExceptionPage();
  4. }

LAUNCHSETTINGS.JSON

Il nome dell’ambiente può essere controllato dal file Properties/launchSettings.json che si trova nel progetto. Impostare opportunamente il nome dell’ambiente ci può permettere di configurare l’applicazione e i suoi middleware in maniera leggermente diversa per assecondare le varie esigenze. Il nome dell’ambiente può essere impostato arbitrariamente ma, di solito, quelli usati sono i seguenti:

  • Development lo usiamo mentre stiamo sviluppando l’applicazione sul nostro PC o Mac;
  • Staging lo usiamo durante una fase di test che si svolge subito prima di entrare in produzione. L’applicazione si trova su un server quanto più simile a quello di produzione, per verificare se funziona bene in condizioni diverse da quelle del PC in cui è stata sviluppata;
  • Production lo usiamo quando l’applicazione è in produzione e viene eseguita sul server, in modo che sia accessibile dai suoi utenti.

Impostare gli indirizzi di avvio dell’applicazione

Per default, un’applicazione ASP.NET Core è raggiungibile dagli indirizzi http://localhost:5000 e https://localhost:5001 ma possiamo ridefinire questi indirizzi dal file Properties/launchSettings.json.