Alla scoperta del VirtualPathProvider: un intero sito dentro un assembly
Uno degli aspetti a mio parere più affascinanti del Provider Model di ASP.NET è il VirtualPathProvider, cioè quel componente - che fa capo al nuovo namespace System.Web.Hosting - nella maggior parte dei casi del tutto sconosciuto ma che consente di raggiungere dei risultati impressionanti. In questo articolo vi condurrò nella realizzazione di un interessante esperimento il cui obbiettivo è di prendere confidenza con questo provider e con esso di prendere coscienza di parte dell'anatomia della pipeline di ASP.NET.
Il VirtualPathProvider
Una delle certezze che accompagnano nel proprio lavoro i programmatori del web è che alla fine dei conti un sito altro non è che una cartella nel filesystem contenente svariati tipi di file dai compiti più disparati. Pagine HTML, pagine ASPX, Fogli di Stile e Javascript, immagini e video, sono contenuti che tipicamente occupano delle zone ben definite all'interno di un filesystem e niente di più. Questa certezza, dopo aver subito un primo scossone con l'introduzione dei meccanismi di URL rewrite mediante i quali è diventato possibile associare a singoli file un numero pressochè indefinito di path virtuali, ha subito la spallata definitiva con l'uscita di ASP.NET 2.0 e in particolare con il VirtualPathProvider.
Questo componente, sul quale esiste davvero poca letteratura ha un ruolo chiave nel dare una risposta alle richieste che provengono dal browser attravero la HttpPipeline. Esso si colloca immediatamente dopo l'inizio della ProcessRequest(), e quindi dopo che gli HttpModule e HttpHandlerFactory sono stati processati (almeno per la parte di ingresso del ciclo di vita di una pagina ASPX) ma viene utilizzato solo all'interno di alcuni tipi di HttpHandler come ad esempio quello che gestisce i file statici e quello delle pagine aspx. Il suo compito è di fornire l'accesso alla struttura del filesystem recuperando le informazioni dei file e directory che compongono la web application. Il provider dispone di metodi rivolti alla gestione dei file (FileExists e GetFile, GetFileHash) e per la gestione delle directory (DirectoryExists e GetDirectory) inoltre usa delle classi specializzate per rappresentare file e directory denominate VirtualFile e VirtualDirectory. Di particolare interesse soprattutto VirtualFile che grazie al metodo Open() consente di ottenere lo stream legato allo specifico file.
All'interno del framework esiste una sola implementazione di VirtualPathProvider, denominata MapPathBasedVirtualProvider che come si può intuire dal nome ha lo scopo di dare accesso al filesystem fisico dedotto in base alla mappatura del path virtuale. Tale provider, instanziato di default dal runtime è anche quello su cui ricadono tutte le chiamate non direttamente gestite qualora si decida di registrare un proprio provider custom. In questo modo si instaura un meccanismo di ricerca a catena su diversi provider che consente di avere più repository per le pagine del sito il cui ordine di priorità sarà determinato dall'ordine di registrazione dei provider.
Se facciamo riferimento all'handler responsabile della gestione delle pagine ASPX, durante la fase della pipeline cui appartiene il VirtualPathProvider esso ne utilizza i metodi per avere accesso ad esempio alle pagine che poi provvederà a compilare e salvare nella apposita directory temporanea di ASP.NET. Curiosando tra i metodi troviamo GetFileHash() e GetCacheDependency() che vengono utilizzati con lo scopo di verificare se sono intervenute modifiche al file e quindi decidere se necessita di essere ricompilato. Come si può intuire questa fase è alla base del funzionamento di ASP.NET dato che è situato proprio nelle fasi cruciali in cui i file di markup divengono le classi che poi verranno eseguite. E però altrettanto chiaro che avere la possibilità di intervenire in queste fasi apre la strada a delle soluzioni decisamente potenti e rivoluzionarie. Ma andiamo con ordine e vediamo come implementare un nostro VirtualPathProvider.
Implementare e registrare un VirtualPathProvider
Il VirtualPathProvider pur facendo parte della grande famiglia cui appartengono i più blasonati MembershipProvider e RoleProvider in realtà ha degli aspetti che lo distinguono molto da questi ultimi. La classe VirtualPathProvider non fa capo alla gerarchia di classi da cui nascono gli altri provider (ProviderBase) e pur trattandosi di una classe astratta essa non espone alcun metodo astratto ma solo dei metodi virtuali. Il motivo di questa scelta è molto semplice; estendendo la classe VirtualPathProvider è possibile scegliere il tipo di gestione che si intende adottare, ad esempio non gestendo le directory ma esclusivamente i file oppure rivolgendo la propria attenzione esclusivamente ad una particolare directory. Ecco perchè quindi la classe VirtualPathProvider altro non è che un contenitore di metodi virtuali che non fanno praticamente nulla fintanto che non si decide di effettuarne l'override invece che come nel caso degli altri provider obbligare all'implementazione di tutti i metodi astratti.
Come ho anticipato poche righe orsono, il VirtualPathProvider deve essere registrato e in questo le differenze rispetto a ProviderBase diventano ancora più marcate. Infatti, diversamente da quello che si può pensare, la registrazione non avviene nel file di configurazione ma per mezzo di uno specifico metodo della classe HostingEnvironment. La ragione di quesa scelta mi è oscura ma suppongo sia legata al fatto che registrare un componente necessario per l'accesso al filesystem all'interno di un file deve essere sembrato un controsenso, anche se in realtà il web.config è uno dei pochissimi file che il runtime non raggiunge per mezzo di tale provider. Ecco quindi come registrare il provider:
1: public class VirtualPathProviderHttpModule : IHttpModule
2: {
3: #region IHttpModule Members
4:
5: /// <summary>
6: /// Disposes of the resources (other than memory) used by the module that implements <see cref="T:System.Web.IHttpModule"></see>.
7: /// </summary>
8: public void Dispose()
9: {
10: }
11:
12: /// <summary>
13: /// Initializes a module and prepares it to handle requests.
14: /// </summary>
15: public void Init(HttpApplication context)
16: {
17: HostingEnvironment.RegisterVirtualPathProvider(new ResourceVirtualPathProvider());
18: }
19:
20: #endregion
21: }
Il modulo riportato nell'esempio, che utilizza il metodo Init() in realtà è solo uno dei modi in cui si può ottenere tale effetto. Un'altro modo potrebbe essere quello di estendere la classe HttpApplication in cui appare un analogo metodo Init() oppure come suggerito dalla documentazione usare una classe statica in cui appaia un metodo AppInitialize() che il runtime chiama proprio per questo tipo di attività. Quello che deve essere chiaro è che la registrazione deve essere compiuta nei primissimi momenti della vita della pipeline dato che altrimenti all'arrivo degli httpHandler sarebbe evidentemente troppo tardi.
Magie del VirtualPathProvider: un sito in un assembly...
Allo scopo di dimostrare le potenzialità del VirtualPathProvider, ho provveduto a realizzare un semplice esempio che analizzerò nelle sue parti per meglio spiegarne l'anatomia. Lo scopo di questo esempio è quello di celare all'interno di un solo assembly tutti i file che fanno parte di un sito web, e per questo scopo ho scelto di utilizzare le Embedded Resources. Si tratta come dicevo di un esempio estremizzato, che personalmente non consiglierei mai di utilizzare in un ambito produttivo per le implicazioni prestazionali che esso concerne, ma tuttavia si tratta di un interessante esercizio nella materia che ora intendiamo apprendere.
Per prima cosa provvediamo a creare l'ambiente di sviluppo. Il nostro scopo è quello di avere una serie di file aspx, asp, cs,js, che vengano compilati come risorse all'interno di un Assembly. Per ottenere questo scopo è necessario creare un sito web con il Web Application Project. Grazie a questo tipo di progetto reintrodotto con la Service Pack 1 di Visual Studio 2005 potremo lavorare il sito come consueto per poi convertire i file in risorse embedded ed effettuare il Publish dei soli file necessari e quindi del solo assembly che contiene tutto il sito. Nel progetto di esempio sono contenute, oltre ad una pagina ASPX, anche una master page, una immagine un css e il web.config che come vedremo più avanti ci servirà per redirigere alcune tipologie di file. Dopo aver realizzato alcune paginette di esempio, è giunto il momento di implementare il provider e quindi provvediamo a estendere la classe VirtualPathProvider:
1: public class ResourceVirtualPathProvider : System.Web.Hosting.VirtualPathProvider
2: {
3: /// <summary>
4: /// Gets a value that indicates whether a file exists in the virtual file system.
5: /// </summary>
6: /// <param name="virtualPath">The path to the virtual file.</param>
7: /// <returns>
8: /// true if the file exists in the virtual file system; otherwise, false.
9: /// </returns>
10: public override bool FileExists(string virtualPath)
11: {
12: if (Resources.Exists(virtualPath))
13: return true;
14:
15: return this.Previous.FileExists(virtualPath);
16: }
17:
18: /// <summary>
19: /// Gets a virtual file from the virtual file system.
20: /// </summary>
21: /// <param name="virtualPath">The path to the virtual file.</param>
22: /// <returns>
23: /// A descendent of the <see cref="T:System.Web.Hosting.VirtualFile"></see> class that represents a file in the virtual file system.
24: /// </returns>
25: public override System.Web.Hosting.VirtualFile GetFile(string virtualPath)
26: {
27: if (Resources.Exists(virtualPath))
28: return new ResourceVirtualFile(virtualPath);
29:
30: return this.Previous.GetFile(virtualPath);
31: }
32: }
Il provider implementato nel box è davvero minimale. Mi sono adoperato per fare in modo che tutta la logica - che ora non ci interessa particolarmente - sia spostata nella classe Resources che ha l'incarico di accedere all'assembly in esecuzione, trovare le risorse ed estrarle quando serve. E' evidente che in questa implementazione si gestiscono esclusivamente i file. Questo non significa però che si potranno usare solo file nella root del sito web, ma che semplicemente non sarà gestita la presenza di directory generate diamicamente. Se per un attimo diamo uno sguardo alla classe VirtualDirectory si vede che essa espone una proprietà Files di tipo IEnumerable che ci consentirebbe di gestire appunto la presenza di directory generate secondo una qualche logica particolare. Implementando solo la parte relativa ai file riceveremmo lo stesso l'intero path virtuale che nel nostro caso sarà trasformato nella notazione puntata tipica dei namespace:
/test/admin/users.aspx
test.admin.users.aspx
Grazie a questo stratagemma potremmo accedere alle risorse embedded così come vengono generate da Visual Studio che appunto per ogni cartella di progetto genera un apposito namespace. Un altro particolare importante per comprendere al meglio l'esempio è la classe ResourceVirtualFile. Tale classe, riportata nel riquadro sottostante, è quella che contiene il riferimento alla singola risorsa e che viene restituira dal metodo GetFile del provider:
1: public class ResourceVirtualFile : System.Web.Hosting.VirtualFile
2: {
3: private string virtualPath;
4:
5: /// <summary>
6: /// Initializes a new instance of the <see cref="ResourceVirtualFile"/> class.
7: /// </summary>
8: /// <param name="virtualPath">The virtual path to the resource represented by this instance.</param>
9: public ResourceVirtualFile(string virtualPath)
10: : base(virtualPath)
11: {
12: this.virtualPath = virtualPath;
13: }
14:
15: /// <summary>
16: /// When overridden in a derived class, returns a read-only stream to the virtual resource.
17: /// </summary>
18: /// <returns>A read-only stream to the virtual file.</returns>
19: public override Stream Open()
20: {
21: return Resources.GetResouceStream(this.virtualPath);
22: }
23: }
come si vede tale classe si appoggia ancora una volta alla nostra classe statica di accesso alle risorse. La cosa importante da comprendere è come il metodo Open debba restituire uno stream al contenuto del file richiesto.
A questo punto, dopo essersi assicurati che tutte le risorse siano marcate come Embedded Resource nel riquadro delle proprietà di Visual Studio 2005 (vedi figura), possiamo modificare il web.config per introdurre alcuni cambiamenti importanti. Innanzitutto va considerato che è nostra necessità che non solo i file aspx e master vengano richiesti tramite il VirtualPathProvider, ma anche jpeg e css. Questo implica che dovremmo istruire il web server in modo che giri tutte le richieste riguardanti tali file al runtime di ASP.NET. La cosa più immediata da fare è di andare nel pannello di controllo di Internet Information Server e assicurarci che ad ognuna delle estensioni richieste sia associata la ISAPI extension di ASP.NET. Questo però è un lavoro certosino e nel nostro caso anche a rischio di errori. Infatti è molto facile dimenticare di registrare una estensione e quindi compromettere il funzionamento del progetto. Per questo il mio consiglio, avendo a disposizione Internet Information Server 6.0 è di configurare il wildcard mapping. Per raggiungere questo pannello dopo aver aperto l'Internet Information Services Manager bisogna selezionare le proprietà il sito o la virtual directory e premere il pulsante "Configuration" nella sezione "Application Settings". E' opportuno restringere questa impostazione all'area minore possibile e quindi nel nostro caso al solo sito web piuttosto che agire sulla configurazione del server nel suo complesso perchè tale mappatura ha delle implicazioni riguardanti le prestazioni che non si possono trascurare.
Una volta applicata l'impostazione dovremmo istruire il runtime di ASP.NET per quanto riguarda i file CSS e JPG. Tali file infatti nel machine.config sono configurati per l'adozione del DefaultHttpHandler che non fa altro che fornire l'accesso diretto al file escludendo l'uso del VirtualPathProvider. La nostra scelta invece dovrà andare allo StaticFileHandler che invece basa il suo funzionamento proprio sul provider e quindi ci consentirà di gestire anche queste estensioni. Esse dovranno essere configurare giocoforza una per una, ma questo lavoro va fatto una sola volta e non si deve applicare invece ad IIS:
<httpHandlers>
<add path="*.css" verb="*" type="System.Web.StaticFileHandler" validate="false" />
<add path="*.js" verb="*" type="System.Web.StaticFileHandler" validate="false" />
<add path="*.jpeg" verb="*" type="System.Web.StaticFileHandler" validate="false" />
<add path="*.jpg" verb="*" type="System.Web.StaticFileHandler" validate="false" />
<add path="*.gif" verb="*" type="System.Web.StaticFileHandler" validate="false" />
<add path="*.png" verb="*" type="System.Web.StaticFileHandler" validate="false" />
</httpHandlers>
Ora il nostro sito è pronto per essere testato. E' inutile testarlo dal WebServer di sviluppo perchè in realtà la directory di sviluppo contiene fisicamente i file e quindi non saremmo in grado di renderci conto del reale funzionamento. Piuttosto potremmo scegliere Publish dal menù di Visual Studio e vedremo che il risultato sarà un solo assembly nella cartella bin e il web.config. Tuttavia se tutte le configurazioni sono eseguite a dovere tutto funzionerà perfettamente.
Limiti e considerazioni
Ci sono alcuni limiti che vanno tenuti in considerazione quando si sceglie di implementare un VirtualPathProvider. In particolare esistono alcuni file il cui accesso non può in alcun modo essere demandato al VirtualPathProvider. Oltre a web.config lo stesso discorso vale anche per il global.asax, e per le risorse localizzate nelle cartelle App_GlobalResource e App_LocalResources. Inoltre sono sclusi anche App_Data e App_Code. Per virtualizzare invece l'accesso ai temi si sarà costretti ad implementare il supporto per le directory perchè il runtime chiede semplicemente un riferimento alla cartella App_Themes dal quale poi desume scorrendo la proprietà Files la presenza o meno dei temi assegnati alle pagine.
Un'altro particolare deve essere preso in considerazione: fino alla precedente versione di ASP.NET l'accesso ai file applicativi nel filesystem - per intenderci quelli consumati dall'applicazione durante la sua normale vita - avveniva per mezzo del consueto Server.MapPath() e mediante l'uso di Stream e Reader ottenuti per mezzo delle classi presenti in System.IO. In una applicazione il cui filesystem possa essere virtualizzato questo è un grosso problema perchè la vincola all'accesso diretto ad un filesystem fisico. E' per questo motivo che è molto più sicuro fare uso del VirtualPathProvider anche per questo tipo di incombenze:
Stream stream = VirtualPathProvider.OpenFile("filecustom.txt");
OpenFile è un metodo statico che funge da scorciatoia. Naturalmente nulla ci vieta di utilizzare tutti i metodi di istanza del provider corrente avendo accesso diretto ad esso medianta la proprietà VirtualPathProvider della class HostingEnvironment. Così facendo renderemo virtualizzabili anche tutti i file applicativi.
Conclusioni
Il progetto di esempio non è certo un risultato che potremmo mettere in produzione su un webserver. Infatti l'accesso alle risorse, in particolare ai file jpeg e ai css, non è decisamente accettabile in termini di prestazioni. Tuttavia in particolari casi potrebbe essere accettabile l'inserimento nell'assembly delle pagine ASPX, dei controlli ASCX, delle master page e dei temi, in sostanza di tutto ciò che il runtime ha l'abitudine di compilare al primo accesso. Nulla ci vieterebbe di adottare una simile
-
Grazie, grazie, grazie. Stavo impazzendo alle 2 del mattino! Tutto perfetto sotto IIS 5.1 ma niente sotto il 6. Senza la dritta sugli <httpHandlers> non ne sarei mai uscito.
di
John Martelli
-
09/01/2008 17.18.38
indietro