Magazine

Uno StaticSiteMapProvider molto flessibile

Andrea Boschin

28/05/2006

Illustriamo la realizzazione di un provider per sitemap che sia in grado di sostituire il provider per xml che non sempre è la scelta migliore. Il provider dell'esempio è in grado di sfuttare qualsiasi fonte dati.

0%100%
per esprimere un voto è necessario registrarsi al sito

ASP.NET 2.0

Elite.Web.Providers.zip ()

Non tutti lo sanno, ma il SiteMapProvider è un componente di ASP.NET che è in grado di procurare parecchi grattacapi. Non mi riferisco all'uso di XmlSiteMapProvider, il provider che tipicamente viene usato nei progetti più semplici quando è sufficiente un file xml per rappresentare la struttura di un sito web, ma piuttosto alle soluzioni più complesse che richiedono la realizzazione di un proprio provider. Il SiteMapProvider per la sua natura di elemento condiviso da tutte le pagine che compongono l'applicazione soffre di problemi di concorrenza che necessitano una gestione opportuna altrimenti sono fonte di errori imprevisti.

Quando si è costretti a scrivere in proprio un provider per la SiteMap la scelta spesso ricade sull'estensione di StaticSiteMapProvider, la classe presente nel framework che è la base della sitemap xml. Di primo acchito la scelta che è la più ovvia pare essere anche la strada più semplice, e infatti spesso si giunge ad ottenere il provider desiderato in breve tempo. A titolo di esempio consideriamo di voler realizzare un provider che legga la sitemap da database; Lo StaticSiteMapProvider mediante l'override di un paio di metodi consente di ottenere facilmente un provider funzionante, ma dopo qualche test sul campo ci si renderà conto che tale implementazione è prona a gravi errori dovuti ad una errata gestione della concorrenza. Questo succede perchè lo StaticSiteMapProvider è scritto in modo tale che mentre un thread legge la struttura della sitemap un'altro può intervenire e provocare l'azzeramento dell'immagine memorizzata. E' per questo che è necessario gestire la concorrenza alla sitemap con opportuni lock che impediscano l'accesso alla struttura contemporaneo a due diversi thread. Purtroppo, nonostante in rete esistano svariati esempi di come risolvere il problema, questo non è mai affrontato nella sua interezza e facilmente si incorre nuovamente in errate implementazioni.

Tutta questa dinamicità della sitemap in realtà il più delle volte non serve. quando ci troviamo di fronte all'uso di uno StaticSiteMapProvider spesso è più che sufficiente caricare la sitemap una volta per tutte e poi fruirla allo stesso modo da tutti i thread in sola lettura e quindi in modalità thread-safe. E' per questo che ho deciso di scrivere uno StaticSiteMapProvider che si comporti proprio in questo modo e che comunque consenta di fruire di tutte le caratteristiche della sitemap quali localizzazione e sicurezza. La sitemap da me realizzata non richiede l'estensione per ereditarietà qualora si voglia implementare una diversa sorgente dati, ma grazie all'adozione di una Abstract Factory la sua estensione avviene semplicemente realizzando una factory concreta referenziata nel file di configurazione. Questo consente di avere un engine di sitemap consolidato e testato dal punto di vista della concorrenza e nonostante ciò poter facilmente dare molte diverse implementazioni allo stesso fornendo diverse fonti da cui la sitemap possa essere letta. Nell'esempio allegato a questo articolo è presente una implementazione della factory che costruisce la sitemap leggendola da un database sql server, ma sarà molto semplice portare questo codice ad altri supporti di persistenza. Inoltre, questa sitemap ha la caratteristica di poter gestire facilmente una moltitudine di diverse applicazioni mappate sulla stessa virtual application o anche su application diverse. La root della sitemap infatti non è costituita dalla pagina principale di una applicazione ma invece da un nodo che accomuna diverse applicazioni il cui accesso avviene sulla base di una porzione di url.

Nella figura 1, è visibile il classdiagram dei nodi che compongono una sitemap. Questi nodi sono tutti estensione della classe SiteMapNode, ma aggiungono ad essa alcuni metodi utili per navigare comodamente la struttura dell'applicazione. La sitemap trova la sua radice in un nodo di tipo SiteMapRoot che apparirà solo una volta nella gerarchia. Al suo interno saranno presenti un certo numero di nodi di tipo SiteMapApplication che individuano la radice di una singola applicazione. Ad ogni applicazione sono associati uno o più ApplicationID. un ApplicationID è una stringa che identifica univcamente una applicazione. Nel file di configurazione sarà possibile associare uno o più ApplicationID ad una porzione di URL. Vediamo come:

<staticSiteMap>
    <applications>
        <add 
            
id="APP1" 
            
baseUrl="http://localhost/sitemapprovider"/>
        <add 
            
id="APP2" 
            
baseUrl="http://localhost/sitemapprovider/subdir"/>
        <add 
            
id="APP3" 
            
baseUrl="http://localhost-dev/sitemapprovider"/>
    <
/applications>
<
/staticSiteMap>

In questa struttura viene definita la mappatura tra alcuni ApplicationID (APP1, APP2, APP3) e i relativi url. Interessante è comprendere che in fase di discovery dell'ApplicationID l'engine cercherà sempre di fornire l'ApplicationID che corrisponde al più lungo path associabile. Così se stiamo navigando al'interno dihttp://localhost/sitemapprovider/subdir il nostro ApplicationID sarà APP2 anche se lo stesso path corrisponde anche ad APP1, per il semplice motivo che APP2 fornisce il match più lungo. Il nodo di tipo SiteMapRoot fornisce un metodo GetApplicationByID() che si occupa poi di ottenere l'applicazione che corrisponde all'ApplicationID trovato.

I nodi di tipo SiteMapApplication contengono istanze di nodo di tipo SiteMapPage, che a loro volta possono contenerne altre così da creare una vera e propria gerarchia. Quando si sta navigando in una applicazione, il runtime dapprima farà uso del metodo GetApplicationById() per trovare l'applicazione corrente ed in seguito userà il metodo FindPageByUrl() per trovare la pagina che corrisponde a quella che stiamo visitando. La sitemap da me realizzata in questa fase fornisce una possibile estensibilità tramite l'uso della clase SiteMapPage. Tale classe definisce un metodo Match() cui verrà proposto l'url corrente, così da verificare se esso corrisponde al nodo; Effettuando l'override di questa classe - che in fase di creazione deve essere prodotta dalla stessa SiteMapFactory - è possibile influire sul modo in cui il runtime risolve gli url durante la navigazione. A titolo di esempio sarebbe molto semplice creare un nodo di tipo RegExSiteMapPage che fornisca come risultato di Match il confronto dell'url con una regular expression anzichè un confronto stringa-stringa. Grazie a questa caratteristica è molto semplice adattare questa sitemap ad un engine di url rewriting preesistente e così facendo ottenere la sitemap all'interno di un CMS vero e proprio.

In quest'ottica occorre tenere presente come avviene la creazione della sitemap: In fase di inizializzazione del provider, il runtime chiamerà una sola volta il metodo CreateSiteMap() della SiteMapFactory, concreta per mezzo della classe base astratta. In questa fase viene fornita alla factory una collection di parametri ottenuti dal parsing del file di configurazione. Questo consente ad una factory di ottenere alcuni parametri di configurazione impostati all'interno della definizione del provider. nel caso della SqlSiteMapFactory ad esempio viene richiesto di impostare una proprietà connectionStringName che fornisce una accesso alla connectionstring da utilizzare per accedere al database di configurazione. Il metodo CreateSiteMap() è responsabile di leggere la versione persistente della sitemap nel modo che preferisce e fornire una descrizione della mappa per mezzo di una classe NodeInfo la quale funge da contenitore dei dati che poi daranno vita alla mappa vera e propria. Questa astrazione si è resa necessaria proprio per consentire un migliore disaccoppiamento tra provider e factory lasciando al provider l'onere di costruire la sitemap vera e propria e alla factory esclusivamente quello di leggere la rappresentazione dallo storage. La classe NodeInfo che implementa la classe IAttributeAccessor e quindi può contenere parametri di configurazione addizionali rispetto alla classe base, verrà in seguito scandita in modo gerarchico e ad ogni nodo verrà creata l'istanza di SiteMapNode corrispondente sia esso un SiteMapRoot, una SiteMapApplication o una SiteMapPage. E' in quest'ultimo caso che la corrispondente istanza di NodeInfo verrà inoltrata nuovamente alla SiteMapFactory che avrà in questo caso l'onere di creare la SiteMapPage sulla base delle informazioni, base o addizionali, in essa contenute. E' in questo punto che si chiude il cerchio. La CreateSiteMap() potrebbe ad esempio gestire un attributo addizionale di NodeInfo che determini il modo in cui CreatePage() in seguito dovrà costruire la pagina, dando ad essa l'occasione di fornire una istanza di classe che esprima un diverso modo di effettuare il Match().

Una volta che la sitemap è stata creata, nulla più può provocarne l'azzeramento a meno che non avvenga un riavvio dell'applicazione stessa. Questo fa si che la sitemap sia completamente staticizzata e il suo accesso sarà notevolmente più rapido perchè non sottoposto a numerosi lock per impedire la concorrenza. Certo queso pone anche alcuni svantaggi. Non sarà possibile adottare questo tipo di sitemap in applicazioni allinterno delle quali le pagine vengano create dinamicamente, ma a prescindere dal fatto che questo non è il caso più frequente, in taluni casi si potranno prevedere dei riavvii forzati dell'applicazione oppure un set di metodi aggiuntivi che provoca il caricamento ex-novo della sitemap gestendo solo in tali occasioni eventuali problematiche di concorrenza.

La nuova StaticSiteMapProvider è realizzata estendendo la classe SiteMapProvider anzichè quella della preesistente sitemap statica, proprio per consentire la riscrittura totale dell'engine che effettua il calcolo dei nodi. Al suo interno sono presenti dei meccanismi che gestiscono il lock nella fasi di precaricamento e che effettuano il caching dei nodi della sitemap allo scopo di velocizzare l'accesso in alcuni casi. In alcuni casi tale policy si è rivelata obbligatoria. Infatti il legame indissolubile che esiste tra SiteMapNode e SiteMapProvider (il secondo è argomento del costruttore del primo), fa si che ad esempio si sia costretti a gestire una collection dei nodi figli per ognuno dei nodi della sitemap esternamente al SiteMapNode vero e proprio. Infatti pur avendo esso una proprietà ChildNodes, questa fa riferimento al corrispondente metodo GetChildNodes() del provider per cui non è possibile semplicemente aggiungere i nodi alla collection del nodo padre, ma occorre gestire una collection esterna, definita nel provider popolata anch'essa in fase di creazine della sitemap. In definitiva tra SiteMapNode e SiteMapProvider è stabilito l'so di una design pattern Bridge che ha lo scopo di semplificare al massimo l'accesso al provider anche quando le operazioni vengano effettuate sul nodo.

Per chiudere questo lungo articolo, vale la pena di vedere come sono realizzate le funzionalità di localizzazione e securityTrimming. Esse vengono attivate per mezzo dell'attributo enableLocalization la prima e securityTrimmingEnabled la seconda. Nel primo caso il meccanismo di localizzazione è perfettamente gestito dai SiteMapNode stessi, in modo del tutto analogo a quanto avviene per le altre tipologie di SiteMap. All'interno di ogni proprietà localizzabile, Title, Description e tutti gli attributi addizionali dei nodi si verifica questo flag e nel caso in cui esso sia sollevato si utilizzano i metodi opportuni per leggere le risorse globali o locali componendo il nome della risorsa con ResourceKey + propertyName. Inoltre si farà uso del valore specificato in ResourceClass per trovare la posizione in cui sono memorizzate le risorse. Per qanto riguarda il securityTrimming, esso passa per l'applicazione di un metodo filtro ReturnNodeIfAccessible() che dato in ingresso un SiteMapNode lo fa valutare dal metodo IsAccessibleToUser del proprio provider. Nel caso in cui esso non sia accessibile restituirà null, altrimenti il nodo stesso. Estendendo StaticSiteMapProvider è possibile effettuare l'override di tale metodo e fornire delle diverse interpretazioni di ruoli o accessi:

/// <summary>
/// 
Determine if a node is accessible to the current user
/// </summary>
/// <param name="context">
Http Context of the request</param>
/// <param name="node">
node to check</param>
/// <returns>
true if node is accessible</returns>
public override bool IsAccessibleToUser(HttpContext context, SiteMapNode node)
{
    
if (node.Roles != null)
    {
        
foreach (string role in node.Roles)
        {
            
if ((role != "*") &&
               ((context.User == 
null) || 
                !context.User.IsInRole(role)))
            {
                
continue;
            }
            
return true;
        }
    }

    
return false;
}

il metodo qui riportato valuta i permessi allo stesso modo con cui essi vengono valutati dal framework. Tale metodo alla resa dei conti sarà uno dei più importanti della sitemap quando attivato perchè verrà eseguito innumerevoli volte per cui effettuandone la riscrittura è opportuno cercare la massima ottimizzazione del suo funzionamento perchè esso può avere un impatto notevole sulle prestazioni. Ecco infine come configurare la sitemap nel web.config:

<siteMap defaultProvider="StaticSiteMapProvider"> 
    <providers>
        <add
             
name ="StaticSiteMapProvider"
            
type="Elite.Web.Providers.StaticSiteMapProvider, Elite.Web.Providers"
            
siteMapFactory ="Elite.Web.Providers.Database.SqlSiteMapFactory, 
Elite.Web.Providers.Database"
            
securityTrimmingEnabled ="true"
            
enableLocalization ="true"
            
connection="sitemap" />
    <
/providers>
<
/siteMap>

Il progetto allegato all'articolo è un valido esempio di come operare per realizzare una sitemap personalizzata. Al suo interno si potranno trovare anche quali sono le facilitazioni che il framework fornisce a chi intende sviluppare uno strato di provider. Nonostante ciò sono certo che il codice preso così com'è può gia essere un buon "Application Block" riutilizzabile.

Commenti
Nome

Sito web
Commento


indietro