Di Davide Vernole
Le novità introdotte con ASP.NET 2.0 permettono di eseguire molte funzionalità in modo più semplice e rapido rispetto al passato. Molti di noi hanno ormai preso confidenza con le API più note, come ad esempio la Membership API, introdotte con questa versione di ASP.NET; pochi però hanno avuto modo di sperimentare a fondo altre classi, meno pubblicizzate, ma ugualmente potenti. È il caso della classe di cui parleremo in questo articolo: il VirtualPathProvider. Questa classe astratta, contenuta nel namespace System.Web.Hosting, permette di sostituire la fonte di approvvigionamento delle risorse (file e cartelle) della nostra applicazione.Come noto, sin dalla sua prima versione, ASP.NET ha introdotto un sistema di compilazione dinamica per alcune estensioni di file (aspx, asmx, asax e ashx). Questo permette di fornire una nuova risorsa (ad esempio una pagina aspx) con la certezza che quest’ultima sia analizzata e compilata prima del suo utilizzo. Precedentemente al rilascio della versione 2.0, non era però possibile indicare all’engine di ASP.NET da dove recuperare le risorse da analizzare e compilare. Ora, creando un nostro VirtualPathProvider, è possibile guidare il parser di ASP.NET affinché utilizzi uno specifico repository di file e cartelle.
.gif)
In questa pagina
VirtualPathProvider
Un esempio di utilizzo
Conclusioni e riferimenti utili
VirtualPathProvider
Il VirtualPathProvider è una classe astratta che fornisce un insieme di metodi in grado di permettere ad un’applicazione web di recuperare risorse (file e cartelle) da un file system virtuale. Abbiamo quindi la possibilità di verticalizzare il parser di ASP.NET indicando da dove le risorse dell’applicazione devono essere recuperate. Per esempio, possiamo decidere che le pagine, le immagini, i temi ed altre risorse persistano in un database, come SQL Server, invece che sul tradizionale file system del sistema operativo.
Un interessante meccanismo legato alla verticalizzazione del parser di ASP.NET è la catena dei VirtualPathProvider in grado di garantire una ricerca in cascata delle risorse ed un funzionamento misto tra risorse reali e risorse virtuali.
L’ordine di utilizzo dipende dall’ordine di registrazione dei provider. L’ultimo provider registrato sarà il primo ad essere utilizzato. Se questo non sarà in grado di risolvere il path della risorsa richiesta, lascerà al penultimo tale incombenza, e così via fino a raggiungere il provider di default di ASP.NET che cercherà di risolvere il path sul file system. Il VirtualPathProvider di default è il MapPathBasedVirtualProvider. Il passaggio da un provider al successivo non è però una funzionalità automatica. Per garantire questo funzionamento si deve utilizzare la proprietà Prevoius nell’implementazione della classe astratta VirtualPathProvider.
Non tutte le cartelle ed i file di un’applicazione web possono però essere gestiti in modo virtuale. La tabella seguente riassume quali risorse possano essere virtualizzate e quali no:
Un esempio di utilizzo
Prima di creare il nostro VirtualPathProvider seguiremo alcuni passi propedeutici allo scopo di definire la struttura del database e di organizzare in modo più strutturato il nostro codice.
Iniziamo definendo la nostra tabella del database che costituirà il repository del file system del nostro provider.
.jpg)
La tabella contiene i campi necessari ad una descrizione minima delle nostre entità (file o cartella). Pensando ad un’implementazione più articolata intesa a soddisfare esigenze più evolute, come la gestione di file immagine, tale struttura dovrà essere ampliata al fine di supportare contenuti binary e non solo testuali.
Creata la tabella, occorre realizzare le stored procedure che il nostro provider utilizzerà per alimentare le richieste dell’engine di ASP.NET. Due di queste in particolare, richiedono la nostra attenzione per capire come siano recuperate le informazioni dalla nostra base dati, e sono: spGetEntity e spGetChildren.
La spGetEntity restituisce una specifica risorsa partendo dal percorso virtuale passato come parametro. Sarà utilizzata per il recupero delle informazioni dal database affinché queste possano essere utilizzate dal sistema virtuale che si vuole creare.
CREATE PROCEDURE dbo.spGetEntity
@VirtualPath NVARCHAR(512)
AS
SET NOCOUNT ON
SELECT
Id,
ParentId,
IsDirectory,
[Name],
VirtualPath,
FileContent,
LastChange
FROM
FileSystem
WHERE
@VirtualPath = VirtualPath
RETURN
La spGetChildren sarà invece utilizzata per popolare le collezioni di file e directory di una cartella identificata dal parametro @VirtualPath.
CREATE PROCEDURE dbo.spGetChildren
@VirtualPath NVARCHAR(512)
AS
SET NOCOUNT ON
SELECT
Id,
ParentId,
IsDirectory,
[Name],
VirtualPath,
FileContent,
LastChange
FROM
FileSystem
WHERE
(SELECT Id FROM FileSystem WHERE VirtualPath=@VirtualPath) = ParentId
RETURN
Realizzata la struttura del database, è ora possibile pensare alla realizzazione di un’entità in grado di contenere le proprietà di un file o di una cartella. Implementiamo quindi una classe FSEntity ed il suo manager FSEntityManager come indicato dal codice seguente:
using System;
using System.Data;
using System.Configuration;
using System.Collections.Generic;
public class FSEntity
{
private int id;
public int Id
{
get { return id; }
set { id = value; }
}
private int parentId;
public int ParentId
{
get { return parentId; }
set { parentId = value; }
}
private bool isDirectory;
public bool IsDirectory
{
get { return isDirectory; }
set { isDirectory = value; }
}
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
private string virtualPath;
public string VirtualPath
{
get { return virtualPath; }
set { virtualPath = value; }
}
private string content;
public string Content
{
get { return content; }
set { content = value; }
}
private DateTime lastChange;
public DateTime LastChange
{
get { return lastChange; }
set { lastChange = value; }
}
}
La classe FSEntity viene gestita da un manager che permette alle altre classi della soluzione di interagire con questa entità. L’implementazione della classe utilizza un semplice SqlHelper che si limita ad occuparsi della gestione delle comunicazioni da e verso l’istanza del nostro SQL Server e del popolamento degli oggetti FSEntity e List<FSEntity> utilizzati come elemento di trasporto delle informazioni verso l’implementazione vera e propria del nostro VirtualPathProvider.
using System;
using System.Data;
using System.Configuration;
using System.Web.Security;
using System.Collections.Generic;
using System.Data.SqlClient;
internal class FSEntityManager
{
internal static List<FSEntity> GetChildren(string virtualPath)
{
List<FSEntity> entities = new List<FSEntity>();
SqlHelper sql = new
SqlHelper(ConfigurationManager.ConnectionStrings["DbCon"].ConnectionString);
SqlDataReader dr = sql.GetDataReader("spGetChildren", CommandType.StoredProcedure,
new SqlParameter("@VirtualPath", virtualPath));
if (dr.HasRows)
{
while (dr.Read())
{
FSEntity entity = new FSEntity();
entity.Id = (int)dr["Id"];
entity.ParentId = (int)dr["ParentId"];
entity.IsDirectory = (bool)dr["IsDirectory"];
entity.Name = dr["Name"] as string;
entity.VirtualPath = dr["VirtualPath"] as string;
entity.Content = dr["FileContent"] as string;
entity.LastChange = (DateTime)dr["LastChange"];
entities.Add(entity);
}
}
dr.Close();
return entities;
}
internal static FSEntity GetEntity(string virtualPath)
{
FSEntity entity = null;
SqlHelper sql = new
SqlHelper(ConfigurationManager.ConnectionStrings["DbCon"].ConnectionString);
SqlDataReader dr = sql.GetDataReader("spGetEntity", CommandType.StoredProcedure,
new SqlParameter("@VirtualPath", virtualPath));
if (dr.HasRows)
{
dr.Read();
entity = new FSEntity();
entity.Id = (int)dr["Id"];
entity.ParentId = !dr.IsDBNull(1) ? (int)dr["ParentId"] : -1;
entity.IsDirectory = (bool)dr["IsDirectory"];
entity.Name = dr["Name"] as string;
entity.VirtualPath = dr["VirtualPath"] as string;
entity.Content = dr["FileContent"] as string;
entity.LastChange = (DateTime)dr["LastChange"];
}
dr.Close();
return entity;
}
...
}
Predisposto l’ambiente propedeutico, siamo in grado di procedere con l’implementazione delle classi che costituiscono il cuore del nostro provider.
Questo file system virtuale necessita, in primis, di due entità capaci di descrivere un file o una cartella virtuale. Per farlo, utilizzeremo rispettivamente la classe VirtualFile e la classe VirtualDirectorypresenti nel namespace System.Web.Hosting.
Iniziamo creando la nostra classe CustomFile che deve riscrivere il metodo Open della classe astratta VirtualFile. Aggiungiamo la proprietà Exists in modo da permettere al nostro provider di capire se un file richiesto esiste, o meno, nel repository. Otteniamo così il codice seguente:
using System;
using System.Web.Hosting;
using System.IO;
using System.Text;
public class CustomFile: VirtualFile
{
private FSEntity entity;
private bool exists = false;
public bool Exists
{
get { return exists; }
}
public CustomFile(string virtualPath): base(virtualPath)
{
entity = FSEntityManager.GetEntity(virtualPath);
If (entity != null)
if (!String.IsNullOrEmpty(entity.Content))
exists = true;
}
public override System.IO.Stream Open()
{
Stream stream = new MemoryStream();
StreamWriter writer = new StreamWriter(stream, Encoding.Unicode);
writer.Write(entity.Content);
writer.Flush();
stream.Seek(0, SeekOrigin.Begin);
return stream;
}
}
Come si può notare, le classi create precedentemente vengono utilizzate per reperire le informazioni necessarie a definire se un file esiste e quale sia il suo contenuto, permettendo così al provider di soddisfare le richieste dell’engine di ASP.NET.
Analogamente, implementiamo la classe CustomDirectory ereditando da VirtualDirectory. In questo caso, oltre ad inserire la proprietà Exists, dobbiamo riscrivere le proprietà Children, Directories e Files che servono ad indicare i contenuti di un cartella virtuale.
using System;
using System.Web.Hosting;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Data;
using System.Configuration;
public class CustomDirectory: VirtualDirectory
{
private bool exists = false;
public bool Exists
{
get { return exists; }
}
public CustomDirectory(string virtualPath) : base(virtualPath)
{
//Verifica l'esistenza della directory
FSEntity dir = FSEntityManager.GetEntity(virtualPath);
if (dir != null)
if (dir.IsDirectory)
exists = true;
//Verifica i figli della directory
List<FSEntity> entities = FSEntityManager.GetChildren(virtualPath);
if (entities.Count > 0)
{
foreach(FSEntity entity in entities)
{
if (entity.IsDirectory)
{
CustomDirectory cd = new CustomDirectory(entity.VirtualPath);
this.directories.Add(cd);
this.children.Add(cd);
}
else
{
CustomFile cf = new CustomFile(entity.VirtualPath);
this.files.Add(cf);
this.children.Add(cf);
}
}
}
}
private List<VirtualFileBase> children = new List<VirtualFileBase>();
public override System.Collections.IEnumerable Children
{
get { return children; }
}
private List<CustomDirectory> directories = new List<CustomDirectory>();
public override System.Collections.IEnumerable Directories
{
get { return this.directories; }
}
private List<CustomFile> files = new List<CustomFile>();
public override System.Collections.IEnumerable Files
{
get { return this.files; }
}
}
Non ci resta che preparare il nostro provider virtuale creando una classe che eredita da VirtualPathProvider e che chiameremo CustomFileSystem. L’implementazione richiede che almeno i metodi FileExists, GetFile, DirectoryExists e GetDirectory siano riscritti dalla classe derivata per un corretto funzionamento.
using System;
using System.Web.Hosting;
using System.Web.Caching;
using System.Collections;
public class CustomFileSystem: VirtualPathProvider
{
public override bool DirectoryExists(string virtualDir)
{
CustomDirectory cd = new CustomDirectory(virtualDir);
if (cd.Exists)
return cd.Exists;
else
return Previous.DirectoryExists(virtualDir);
}
public override bool FileExists(string virtualPath)
{
CustomFile cf = new CustomFile(virtualPath);
if (cf.Exists)
return cf.Exists;
else
return Previous.FileExists(virtualPath);
}
public override VirtualDirectory GetDirectory(string virtualDir)
{
CustomDirectory cd = new CustomDirectory(virtualDir);
if (cd != null)
return cd;
else
return Previous.GetDirectory(virtualDir);
}
public override VirtualFile GetFile(string virtualPath)
{
CustomFile cf = new CustomFile(virtualPath);
if (cf != null)
return cf;
else
return Previous.GetFile(virtualPath);
}
public override string GetFileHash(string virtualPath, System.Collections.IEnumerable
virtualPathDependencies)
{
HashCodeCombiner hcc = new HashCodeCombiner();
foreach (string virtualDependency in virtualPathDependencies)
{
FSEntity entity = FSEntityManager.GetEntity(virtualDependency);
if (entity == null)
{
// It could be on the file system
return Previous.GetFileHash(virtualPath,
virtualPathDependencies);
}
hcc.AddLong(entity.LastChange.Ticks);
}
return hcc.CombinedHashString;
}
public override CacheDependency GetCacheDependency
(string virtualPath, IEnumerable virtualPathDependencies,
DateTime utcStart)
{
//Implementare SqlDependency
return null;
}
}
Come evidenzia il codice sopra riportato, la nostra implementazione riscrive anche altri due metodi: GetHashFile e GetCacheDependency. Questi metodi aiutano ASP.NET a capire quando un file è stato modificato. Questo permettere di evitare compilazioni quando non necessario, e di poter contare sull’ultima versione compilata, quando disponibile. Nell’esempio non è stata implementata la CacheDependency; ma è fortemente consigliato farlo, qualora decideste di utilizzare questo provider in una vostra implementazione reale.
Prima di poter utilizzare il nuovo oggetto, dobbiamo capire come far conoscere ad ASP.NET che vogliamo utilizzare un nostro VirtualPathProvider. La modalità richiesta consiste nel registrare il provider all’avvio dell’applicazione. Possiamo inserire un nostro oggetto nella cartella App_Code con un metodo statico AppInitialize, oppure utilizzare il metodo delegato alla gestione dell’evento Application_OnStart del Global.asax. La registrazione avviene utilizzando il metodo statico RegisterVirtualPathProvider della classe HostingEnvironment. Ecco l’esempio usato dalla nostra soluzione:
using System;
using System.Web.Hosting;
public static class StartUp
{
public static void AppInitialize()
{
CustomFileSystem cfs = new CustomFileSystem();
HostingEnvironment.RegisterVirtualPathProvider(cfs);
}
}
La registrazione permette di inserire il provider nell’elenco dei VirtualPathProvider dell’istanza dell’applicazione; elenco che è utilizzato per creare la catena di provider virtuali di cui abbiamo parlato in precedenza.
Conclusioni e riferimenti utili
Abbiamo visto come è possibile costruire con relativa semplicità un provider alternativo per la fornitura di risorse alla nostra applicazione web basata su ASP.NET. Le classi astratte utilizzate permettono di coprire diversi scenari di utilizzo alcuni dei quali già identificati, mentre altri sono ancora da svelare.
Per un ulteriore approfondimento, vedi: