Questo post fa parte di una serie dedicata ad Aurelia e ASP.NET Core.
Ora che tutte le questioni legate alla maniera di referenziare le dipendenze lato client in un progetto ASP.NET Core sono chiare, possiamo finalmente cominciare a scrivere un po’ di codice nel nostro progetto.
Come tutti i progetti basati su framework SPA, un’applicazione web basata su Aurelia si realizza scrivendo pagine HTML e moduli JavaScript. Seguendo l’approccio MVVM (che è alle fondamenta di Aurelia e di cui parleremo in un prossimo post), le pagine HTML saranno le nostre View (conterranno cioè gli elementi grafici che saranno mostrati all’utente), i moduli JavaScript i nostri ViewModel (conterranno cioè i dati con i quali le viste saranno popolate con le informazioni utili all’utente).
Per ingraziarci i favori degli dei della programmazione, non possiamo che partire dal solito classico esempio: Hello World! Ovviamente nel nostro caso, pur usando ASP.NET Core, sarà un esempio in puro HTML.
Ripartiamo dal progetto creato un paio di post fa, e aggiungiamo il file index.html che, memori di quanto detto nello stesso post, salveremo nella cartella wwwroot e non in quella principale del progetto. Vogliamo infatti che questo file sia servito direttamente al client con una chiamata tipo http://hostname/index.html.
Ok, tutto sembra a posto, incrociamo le dita e premiamo F5. Visual Studio compila il progetto e apre il browser puntando alla root dello stesso (http://localhost:18156/). Ci aspetteremmo di vedere il nostro file caricato nel browser ma in realtà quello che otteniamo è la pagina seguente:
Potremmo pensare che si tratti di un problema di pagina di default: forse http://localhost:18156/ non viene risolto in http://localhost:18156/index.html per qualche problema di configurazione. Se però scriviamo nella barra degli indirizzi espressamente http://localhost:18156/index.html il risultato non cambia. In ogni caso non si capisce perché il server non ci restituisca un errore 404 (dando quindi l’impressione di trovare il file) ma ci serva poi un contenuto dalla provenienza ignota.
Niente panico, è tutto normale e corretto: nel mondo di ASP.NET Core, out-of-the-box abbiamo poco o nulla. Tutto quello che ci serve va aggiunto e configurato, compreso il fatto che l’applicazione serva i file statici presenti nella cartella wwwroot. ASP.NET Core non ci sta quindi servendo il file index.html (di base non sa come farlo) e questo è il motivo per cui i contenuto è diverso dalle aspettative.
Da dove viene allora il contenuto di cui sopra? Per trovare la soluzione facciamo un piccolo passo indietro e analizziamo un paio di file chiave in un progetto ASP.NET Core, che rappresentano quel poco che abbiamo out-of-the-box nel nostro progetto creato con il layout Empty.
Come prima cosa dobbiamo dire che in ASP.NET Core tutta la parte nascosta alla vista del programmatore che di fatto consentiva alla nostra applicazione di funzionare è – fortunatamente – sparita. Quando nel precedente post abbiamo descritto brevemente le novità di ASP.NET Core, ci siamo soffermati esclusivamente su quanto c’è di nuovo nel nostro progetto vuoto. In realtà chi è abituato a lavorare con ASP.NET avrà senza dubbio notato anche un’assenza abbastanza importante: in ASP.NET Core il file Global.asax è sparito e la parte di configurazione e inizializzazione che eravamo soliti scrivere nei quattro metodi di quella classe si trova ora nei file Program.cs e Startup.cs.
Il primo file, almeno nel nome, ha qualcosa di familiare. Se lo apriamo ci accorgiamo che non ci siamo sbagliati: sembra praticamente identico al file omonimo che viene creato in un’applicazione Console, con tanto di metodo static void Main. Se nelle precedenti versioni di ASP.NET qualcosa di magico consentiva ad un richiesta HTTP indirizzata ad IIS di essere veicolata verso la nostra applicazione, con ASP.NET Core tutto è nelle nostre mani. L’applicazione viene inizialmente lanciata come una “normale” applicazione Console per mezzo del suo classi entry point Main. Ed è proprio all’interno di tale metodo che possiamo definire il modo in cui dovrà funzionare, istanziando e configurando un oggetto di classe WebHostBuilder. Una configurazione di base viene preparata per noi dal template di progetto e per ora non abbiamo necessità di modificarla. Se volete addentrarvi nei dettagli trovate diversi post esplicativi a riguardo. Quello che ci interessa evidenziare è la chiamata UseStartup<T> che istruisce il WebHostBuilder sulla classe che si occuperà della configurazione dell’applicazione. Per default tale classe si chiama Startup:
Cioè detto, è chiaro che il successivo posto cui dare un’occhiata è il file Startup.cs che definisce la classe Startup cui fa riferimento il WebHostBuilder come punto di ingresso della nostra applicazione. Questa classe, che come noterete non deriva da alcuna classe base e non implementa alcuna interfaccia, è provvista di due metodi fondamentali: ConfigureServices e Configure.
Sorvolo sulla scelta dei nomi, in ogni caso il primo metodo serve a configurare lo IoC container built-in in ASP.NET Core con tutti i servizi che contiamo di usare nella nostra applicazione, compresi eventualmente quelli di “sistema” come MVC. ASP.NET Core è infatti (finalmente) dotato di un meccanismo di dependency injection intrinseco e integrato nel framework. Visto che in una prima fase non avremo bisogno di utilizzarlo e non voglio divagare inutilmente, per ora ignoreremo cosa sia la dependency injection, cosa sia uno IoC container e come si debba usare il metodo ConfigureServices. Limitiamoci semplicemente ad ignorarlo, lasciamolo desolantemente vuoto come è adesso e andiamo avanti.
Il metodo che più ci interessa è il metodo Configure, che serve a configurare i middleware che comporranno la pipeline di ASP.NET Core. Chi ha avuto modo di arrivare ad ASP.NET Core transitando per OWIN si troverà più a suo agio con questi “strani” termini. Per tutti gli altri cerchiamo di fare un po’ di chiarezza.
Quando il processo che ospita la nostra applicazione riceve una richiesta HTTP, questa viene presa in carico e incanalata come fosse su una catena di montaggio o se scorresse lungo una tubatura petrolifera (detta in inglese pipeline). Nel nostro caso i vari pezzi di questa condotta sono ovviamente componenti software (detti middleware) che, uno dopo l’altro vengono chiamati in causa per esaminare la richiesta e valutare le opportune operazioni necessarie a gestirla. Quando un middleware ritiene di poter fornire una risposta, il processo si arresta e la stessa viene incanalata nel verso opposto, ripassando per tutte le componenti precedentemente visitate, fino a giungere all’utente finale. In caso contrario il middleware è responsabile di passare il controllo al componente successivo:
Questo consente ad ogni middleware di poter agire in diversi modi e momenti:
- un middleware dedicato all’autorizzazione agirà tipicamente nella fase di ricezione delle richiesta, verificando le credenziali dell’utente e rigettandola nel caso non sia sufficienti o consentendo che questa passi ai componenti successivi in caso contrario;
- un middleware dedicato al logging agirà molto probabilmente in entrambe le direzioni, prima tracciando le informazioni sulla richiesta in ingresso e poi quelle sulla risposta in uscita;
- un middleware dedicato a fornire risposte (come ad esempio un framework tipo MVC) agirà infine come “terminale” della “pipeline”, interpretando la richiesta e esaudendola.
L’esatto funzionamento di questa architettura esula dai nostri scopi, ma trovate una ricca documentazione in materia, partendo ad esempio dalla documentazione di ASP.NET Core.
Aggiungendo uno o più middleware alla pipeline di ASP.NET Core è quindi possibile configurare l’applicazione affinché si comporti esattamente come desiderato, senza che ulteriori processi non necessari rallentino le operazioni compromettendo le performance.
Aggiungere un middleware è in genere molto semplice. Il metodo Configure prevede infatti un parametro in ingresso di tipo IApplicationBuilder che è fornito di un conveniente metodo UseMiddleware<T> atto appunto all’uopo.
In realtà quasi mai avremo bisogno di ricorrere a tale metodo. Ogni middleware è infatti generalmente fornito di un metodo di estensione che si applica direttamente all’interfaccia IApplicationBuilder, consentendo di scrivere ad esempio direttamente UseRouter invece che UseMiddleware<RouterMiddleware> con il non trascurabile beneficio di poter passare dei parametri opzionali di configurazione “strongly typed”.
Ora che la teoria è un po’ più chiara, torniamo alla pratica ed esaminiamo il metodo Configure. Come si vede il template Empty aggiunge ben poco alla pipeline e la parte che ci interessa maggiormente al momento è l’istruzione app.Run(…) che serve a configurare un particolare middleware che funge sempre da anello terminale della catena e che per tale motivo ha una sintassi leggermente diversa. Il template Empty lo configura usando un anonymous method che, senza prestare attenzione al contenuto della richiesta, si limita a restituire come risposta sempre e comunque il testo “Hello World!”:
Ecco dunque da dove arriva il risultato che avevamo ottenuto in precedenza. Ed ecco dove dobbiamo mettere le mani per fare in modo che al posto di questo testo statico ci venga servito il nostro file index.html.
Quello che ci serve è aggiungere alla nostra pipeline un middleware che intercetti la chiamata, si accorga che esiste un file in wwwroot che corrisponde al percorso della richiesta e lo serva al browser direttamente dal file system. Fortunatamente per noi, questioni come questa (ed altre tipiche delle sviluppo di applicazioni web) sono state già risolte e codificate in ASP.NET Core. Si tratta solo di attivare la funzionalità in maniera appropriata perché, come già detto, out-of-the-box la pipeline è completamente vuota per non appesantirla con operazioni superflue che comporterebbero un inutile degrado delle prestazioni.
Posizioniamoci quindi dentro al metodo Configure e modifichiamolo in modo che appaia come segue:
In questo modo stiamo chiedendo al framework di intercettare le chiamate ai file statici e servirle direttamente (UseStaticFiles) e di fare in modo che le chiamate alla root sia reindirizzate verso un file di default (UseDefaultFiles). Come si vede dal tratteggio rosso però, il due metodi non vengono riconosciuti dal compilatore e, per quanto ci siamo detti finora, la cosa non ci meraviglia affatto: ASP.NET Core è una federazione di NuGet package e ovviamente il middleware di gestione dei file statici si trova in un package dedicato (sempre al fine di alleggerire il prodotto finale) che per ora non abbiamo referenziato nel progetto.
Cogliamo l’occasione per sfruttare una funzionalità di Visual Studio 2015 che semplifica questo genere di situazioni: come si vede nello screenshot, in corrispondenza della riga di codice che riporta la chiamata a UseStaticFiles, Visual Studio mostra una lampadina accesa; è il segnale che è in grado di suggerirci un modo per risolvere il problema. Se in passato ciò che l’ambiente di sviluppo era in grado di fare era solo proporci l’aggiunta della clausola using al file corrente, oggi è anche in grado di individuare la necessità di aggiungere un nuovo NuGet package e di prendersi carico dell’operazione in totale autonomia:
A questo punto siamo pronti a lanciare nuovamente l’applicazione e questa volta il risultato dovrebbe essere quello atteso:
Attenzione perché il middleware aggiunto da UseDefaultFiles si limita ad analizzare la richiesta ed eventualmente modificarla: se la URL richiesta è la root del progetto, questa viene sostituita con una URL che fa riferimento al primo dei default file ("default.htm", "default.html", "index.htm", "index.html") rintracciati nella cartella wwwroot. Il middleware non si occupa di servire il file stesso, ragion per cui è sempre necessario aggiungere la chiamata a UseStaticFiles ed è sempre necessario che tale chiamata sia successiva a quella ad UseDefaultFiles per assicurarsi che il middleware che serve effettivamente il file possa lavorare sulla URL già opportunamente modificata. In alternativa è possibile usare il metodo UseFileServer che integra i due precedenti nella giusta sequenza.
Happy coding!
P.S. Il codice sorgente è disponibile su GitHub, basta scaricare la release con lo stesso nome del post (Hello ASP.NET Core! in questo caso).