in

DotNetSide

Dot Net South Italy Developers User Group

Articoli

Articoli pubblicati dagli iscritti a .netSide

Servizi basati su Provider Model in ASP.NET 2.0

Autore: Maurizio Tammacco

ASP .NET 2.0 si basa su una architettura altamente estensibile e configurabile, anche grazie all’utilizzo del modello basato sui provider. Questo modello, a fronte di una API disegnata per fornire una qualche funzionalità applicativa che prevede diverse specifiche implementazioni, permette di separare l’API stessa dagli algoritmi specifici delle varie implementazioni. Attraverso questa separazione è possibile utilizzare algoritmi differenti a fronte di una stessa API, attraverso una semplice modifica del file di configurazione dell’applicazione che utilizza l’API. Come già detto, ASP .NET 2.0 prevede già dei servizi basati sui provider, ad esempio Membership, Profile, Roles, Virtual Path, solo per citarne alcuni. Il servizio di Membership, che si occupa della creazione e validazione degli utenti, prevede già provider pronti all’uso a seconda del particolare repository che memorizza le informazioni degli utenti, che può essere un database Sql Server, Access, ActiveDirectory, oppure un repository non standard previsto dalla particolare logica di bussines utilizzata dall’ applicazione. Questo articolo espone i passi necessari per creare un servizio personalizzato basato sui provider, basandosi su un esempio concreto.

Definizione e vantaggi dell’utilizzo del provider model

Il provider design pattern è una variante del pattern Strategy, appartenente alla famiglia GOF (Gang of Four). Consiste semplicemente nella possibilità di “intercambiare” un algoritmo applicativo utilizzato da una API senza nessun impatto sull’interfaccia della funzionalità messa a disposizione dalla API stessa. Questo significa che una volta definita l’architettura di uno specifico servizio, qualsiasi sviluppatore può implementare un algoritmo specializzato senza nessun impatto sia sul servizio che sul codice che lo utilizza . Questo pattern architetturale è possibile implementarlo anche in ASP .NET 1.1, mentre la versione 2.0 lo utilizza in modo intensivo.

 

Quando si costruisce un servizio utilizzabile da più applicazioni, ad esempio il tracing ed il logging delle informazioni, l’implementazione dell’API di gestione potrebbe risultare eccessivamente rigida per ogni scenario possibile in cui si andrà ad utilizzare il servizio. Nell’esempio appena citato dovrebbe essere possibile utilizzare il servizio di tracing e logging a prescindere dal particolare sistema di memorizzazione delle informazioni utilizzato, che potrebbe essere il log degli eventi di Windows, un file di testo, un database Sql Server, un database Oracle, ecc. ecc..

L’utilizzo del provider model nella implementazione dei propri servizi applicativi permette di disaccoppiare l’implementazione del codice dell’API di gestione dalla specifica funzionalità richiesta dalla business logic. Grazie a questo disaccoppiamento, le funzionalità esposte dall’API del servizio saranno invocate dal codice che usufruisce del servizio stesso in modo trasparente ed indipendente rispetto alla specifica funzionalità richiesta dalla logica di business; quest’ultima è possibile implementarla come provider, ovvero come una classe che ridefinisce i metodi definiti da una classe astratta che stabilisce un contratto tra una funzionalità applicativa e la sua specifica implementazione.

Requisiti del servizio ed implementazione dell’API di gestione

Il servizio che andremo ad implementare secondo il provider model design pattern è un componente .NET 2.0 per la gestione del caching distribuito delle informazioni in una applicazione web, requisito normalmente associato alla presenza di una web farm. Questo argomento esula dagli obiettivi di questo articolo; tutto ciò che occorre sapere è che in presenza di una web farm il server che elebora una richiesta web può essere un qualsiasi server che fa parte della web farm, e per questo motivo non è possibile utilizzare il servizio di caching tradizionale; occorre invece persistere le informazioni da inserire in cache in un medium persistente, ad esempio un database. I requisiti, però, prevedono che l’utilizzatore finale del componente possa utilizzare come RDBMS sia Sql Server che Oracle.

L’implementazione di un servizio basato sui provider prevede i seguenti passaggi:

  • Scrittura dell’interfaccia dei vari provider che si intendono implementare a fronte di un servizio (data caching nell’esempio)
  • Scrittura dei vari provider previsti, ovvero componenti utilizzabili in modo intercambiabile a fronte della stessa API, e gestiti mediante file di configurazione
  • Scrittura dell’API di gestione del servizio, che fornisce l’interfaccia del servizio stesso ai client che ne usufruiscono
  • Configurazione del servizio e dei providers attraverso il file di configurazione della applicazione

Interfaccia dei providers

L’interfaccia dei providers consiste in una classe astratta che espone metodi pubblici che ogni provider specifico andrà a ridefinire nella propria implementazione. Nella fattispecie la classe astratta definisce due metodi per leggere e scrivere i valori dalla cache:

 

Copy Code
1 public abstract class DataCacheProviderBase : ProviderBase 2 { 3 public abstract string ApplicationName { get; set; } 4 5 public abstract void SetCachePersistent(string key, object value); 6 public abstract object GetCachePersistent(string key); 7 } 8 9 10 public class DataCacheProviderCollection : ProviderCollection 11 { 12 public new DataCacheProviderBase this[string name] 13 { 14 get { return (DataCacheProviderBase)base[name]; } 15 } 16 17 public override void Add(ProviderBase provider) 18 { 19 if (provider == null) 20 throw new ArgumentNullException("provider", "provider can’t be null"); 21 22 if (!(provider is DataCacheProviderBase)) 23 throw new ArgumentException 24 ("Invalid provider type", "provider"); 25 26 base.Add(provider); 27 } 28 } 29

La classe astratta DataCacheProviderBase eredita dalla classe System.ConfigurationProvider.ProviderBase. Quest’ultima rappresenta la radice da cui derivano tutte le classi che implementano un provider, ed espone due proprietà virtuali a sola lettura ed un metodo anch’esso virtuale. Le proprietà sono “Name” e “Description” che indicano rispettivamente il nome del provider e la sua descrizione così come appaiono nel file di configurazione descritto più avanti nell’articolo. Inoltre, viene definita un’altra classe, DataCacheProviderCollection che eredita da ProviderCollection , che permette la gestione dell’insieme dei providers definiti nel file di configurazione, e precisamente l’aggiunta di un provider e l’ottenimento dell’istanza di uno specifico provider dato il suo nome.

Creazione di provider specifici

Dopo aver creato l’astrazione di un provider di data caching è possibile creare uno o più provider specifici che ridefiniscono le proprietà o i metodi astratti della classe DataCacheProviderBase, fornendo in questo modo una implementazione specifica del servizio. Nel nostro esempio creiamo la classe SqlDataCacheProvider per fornire il data caching persistente su Sql Server. Seguendo lo stesso principio visto sin’ora è possibile costruire facilmente, il provider OracleDataCacheProvider oppure FileDataCacheProvider per fornire implementazioni differenti dello stesso servizio.

 

Copy Code
1 [SqlClientPermission(SecurityAction.Demand, Unrestricted = true)] 2 public class SqlDataCacheProvider : DataCacheProviderBase 3 { 4 private string applicationName; 5 private string connectionString; 6 7 public override string ApplicationName 8 { 9 get { return applicationName; } 10 set { applicationName = value; } 11 } 12 13 public string ConnectionString 14 { 15 get { return connectionString; } 16 set { connectionString = value; } 17 } 18 19 public override void Initialize(string name, NameValueCollection config) 20 { 21 if (config == null) 22 throw new ArgumentNullException("config"); 23 24 if (String.IsNullOrEmpty(name)) 25 name = "SqlDataCacheProvider"; 26 27 if (string.IsNullOrEmpty(config["description"])) 28 { 29 config.Remove("description"); 30 config.Add("description", 31 "SQL data cache provider"); 32 } 33 34 base.Initialize(name, config); 35 applicationName = config["applicationName"]; 36 37 if (string.IsNullOrEmpty(applicationName)) 38 applicationName = "/"; 39 40 config.Remove("applicationName"); 41 connectionString = config["connectionString"]; 42 if (String.IsNullOrEmpty(connectionString)) 43 throw new ProviderException 44 ("Empty or missing connectionString"); 45 46 config.Remove("connectionString"); 47 if (config.Count > 0) 48 { 49 string attr = config.GetKey(0); 50 if (!String.IsNullOrEmpty(attr)) 51 throw new ProviderException 52 ("Unrecognized attribute: " + attr); 53 } 54 } 55 56 public override object GetCachePersistent(string key) 57 { 58 return "Welcome from SqlDataCacheProvider!"; 59 } 60 61 public override void SetCachePersistent(string key, object value) 62 { 63 throw new System.NotImplementedException("The method or operation is not 64 implemented."); 65 } 66 } 67

Il provider SqlDataCacheProvider ridefinisce le proprietà ed i metodi della classe astratta DataCacheProviderBase, aggiungendo una proprietà specifica, “ConnectionString”, ovvero la stringa di connessione al database che conterrà le informazioni di caching.

API di gestione del servizio

L’API di gestione del servizio consiste in una classe che espone metodi pubblici ai client utilizzatori del servizio stesso. Questi metodi permettono di richiamare la funzionalità specifica richiesta mediante una delega al corrispondente metodo del provider predefinito caricato dinamicamente:

 

Copy Code
1 public class DataCacheService 2 { 3 private static DataCacheProviderBase provider; 4 private static DataCacheProviderCollection providers; 5 private static object lockObj = new object(); 6 7 public DataCacheProviderBase Provider { get { return provider; } } 8 9 public DataCacheProviderCollection Providers { get { return providers; } } 10 11 public static void SetCachePersistent (string key, object value) 12 { 13 LoadProviders(); 14 provider.SetCachePersistent (key, value); 15 } 16 17 public static object GetCachePersistent (string key) 18 { 19 // Utilizzare il seguente metodo per istanziare tutti i providers definiti 20 //LoadProviders(); 21 22 // Utilizzare il seguente metodo per istanziare solo il provider predefinito 23 LoadDefaultProvider(); 24 return provider.GetCachePersistent (key); 25 } 26 27 private static void LoadProviders() 28 { 29 if (provider == null) 30 { 31 lock (lockObj) 32 { 33 if (provider == null) 34 { 35 DataCacheProviderConfigurationSection section = WebConfigurationManager.GetSection("providers/dataCacheProvider") as DataCacheProviderConfigurationSection; 36 if (section == null) 37 throw new ProviderException("Unable to load provider configuration 38 section"); 39 40 providers = new DataCacheProviderCollection(); 41 ProvidersHelper.InstantiateProviders 42 (section.Providers, providers, 43 typeof(DataCacheProviderBase)); 44 provider = providers[section.DefaultProvider]; 45 46 if (provider == null) 47 throw new ProviderException 48 ("Unable to load default DataCacheProvider"); 49 } 50 } 51 } 52 } 53 } 54

Il metodo privato LoadProviders costituisce la parte essenziale di tutto il meccanismo. Esso prevede che tutti i provider registrati siano istanziati dinamicamente, e che il provider predefinito sia assegnato alla variabile privata a livello di classe denominata “provider” in modo da poter essere utilizzata per invocarne i suoi metodi. La creazione dell’istanza dei providers è effettuata attraverso il metodo statico IstantiateProviders della classe ProvidersHelper situata nel namespace System.Web.Configuration. Se guardiamo all’interno di tale metodo con il noto tool Reflector , notiamo che per ogni provider definito nella apposita sezione del file di configurazione della applicazione è invocato il metodo statico IstantiateProvider che crea una istanza dello stesso attraverso la chiamata al metodo statico CreateInstance della classe Activator, e la memorizza nella collection “providers”. Questo comportamento potrebbe non essere ottimale soprattutto in presenza di parecchi providers definiti, in quanto per ognuno è creata una istanza ma solo per uno di essi possono essere invocati i relativi metodi, ovvero il “default provider”. In questo caso è possibile evitare overhead inutili istanziando solamente il provider predefinito mediante l’invocazione del metodo IstantiateProvider della classe ProvidersHelpers. Il metodo privato LoadProvider mostra tale utilizzo:

 

Copy Code
1 private static void LoadDefaultProvider() 2 { 3 if (provider == null) 4 { 5 lock (lockObj) 6 { 7 if (provider == null) 8 { 9 DataCacheProviderConfigurationSection section = WebConfigurationManager.GetSection("providers/dataCacheProvider") 10 as DataCacheProviderConfigurationSection; 11 12 if (section == null) 13 throw new ProviderException("Unable to load provider 14 configuration section"); 15 16 provider = (DataCacheProviderBase) 17 ProvidersHelper.InstantiateProvider( section.Providers[section.DefaultProvider], 18 typeof(DataCacheProviderBase)); 19 20 if (provider == null) 21 throw new ProviderException 22 ("Unable to load default DataCacheProvider"); 23 } 24 } 25 } 26 } 27

Configurazione dei providers

Affinchè una applicazione possa utilizzare un servizio basato su providers è necessario modificare il file di configurazione (web.config), inserendo la sezione “provider”. In tale sezione sono elencati tutti i providers disponibili per un determinato servizio, e per ognuno di essi sono definiti i parametri di configurazione, come mostrato nel seguente esempio:

 

Copy Code
1 <providers> 2 <dataCacheProvider defaultProvider="SqlDataCacheProvider"> 3 <providers> 4 <clear/> 5 <add name="SqlDataCacheProvider" 6 type="DemoDataCacheProviders.DataCacheService.SqlDataCacheProvider, DataCacheProviders" 7 connectionString="Server=TESTCACHE;Database=DBCache;User ID=DbCacheUser;Password=password;Trusted_Connection=False;" 8 description="Sql data cache provider"/> 9 <add name="OracleDataCacheProvider" 10 type="DemoDataCacheProviders.DataCacheService.OracleDataCacheProvider, DataCacheProviders" 11 connectionString="Server=TESTCACHE;Database=DBCache;User ID=DbCacheUser;Password=password;Trusted_Connection=False;" 12 description="Oracle data cache provider"/> 13 </providers> 14 </dataCacheProvider> 15 </providers> 16

L’ attributo “defaultProvider” permette di indicare il nome del provider attivo in un determinato momento. E’ possibile sostituire questo nome con uno degli altri providers definiti nella sezione di configurazione affinchè si realizzi l’intercambiabilità dell’algoritmo di implementazione della funzionalità. Inoltre, per poter leggere la sezione di configurazione dei providers, è necessario definire la relativa classe di gestione ed indicarla nel file di configurazione. Ecco un esempio di utilizzo:

 

Copy Code
1 <sectionGroup name="providers"> 2 <section name="dataCacheProvider" 3 type="DemoDataCacheProviders.DataCacheService.DataCacheProviderConfigurationSection, DataCacheService" allowDefinition="MachineToApplication" restartOnExternalChanges="true"/> 4 </sectionGroup> 5

Per ogni provider definito attraverso l’attributo “add” è possibile definire, oltre al nome, descrizione e tipo, anche altri parametri di configurazione specifici (“connectionString” nell’esempio). Questi valori sono passati al metodo “Inizialize” del provider attraverso il parametro “config” di tipo NameValueCollection, in modo da inizializzarlo correttemente.

Ciò che occorre sapere sull’utilizzo dei providers

Come già evidenziato dal codice di esempio, esiste in memoria una sola istanza di una particolare provider, e ciò significa che le informazioni di stato sono mantenute tra chiamate consecutive, quindi l’istanza del provider è state-full. Quest’unica istanza è però circoscritta all’Application Domain in cui è in esecuzione l’assembly che richiede il caricamento dinamico; ciò significa che per ogni AppDomain è presente in memoria una sola istanza del provider, ma più AppDomain caricheranno più istanze. Inoltre, poiché una sola istanza è condivisa tra le varie richieste, è fondamentale proteggere il codice dall’accesso contemporaneo da parte di differenti thread.

Ogni provider è inizializzato attraverso la chiamata al metodo Iniziatize. Il runtime ASP .NET assicura che questo metodo sia invocato una sola volta; questo comportamento viene garantito dall’invocazione del metodo Inizialize della classe base System.Configuration.Provider.ProviderBase, il quale solleva una eccezione se il provider è inizializzato più di una volta, e, altro aspetto molto importante, deve essere sottoposto a override nella classe derivata. Inoltre, deve sollevare una eccezione per tutti i parametri di configurazione obbligatori ma non impostati ed assegnare un valore predefinito per quelli non obbligatori.

Una classe provider ha l’obbligo di ridefinire tutti i metodi o proprietà astratti della sua classe base, ma questo non significa che li deve implementare tutti. Infatti l’implementazione è facoltativa e quindi può essere parziale rispetto al contratto imposto dalla classe base; laddove non è prevista il metodo o la proprietà deve sollevare immediatamente una eccezione del tipo System.NotSupportedException oppure System.NotImplementedException.

La classe del provider che fornisce la specifica funzionalità è istanziata dinamicamente a run-time. Per questo motivo l’applicazione client che utilizza un servizio basato su provider non deve mantenere alcun riferimento al provider stesso, né tantomeno deve interagire con esso creando una istanza ed invocando direttamente i suoi metodi.

Un altro aspetto da considerare riguarda eventuali metodi di supporto o variabili definite all’interno della classe provider, oltre a quelli definiti dalla classe astratta. Questi membri devono essere necessariamente tutti privati, in modo tale da non esporli all’interno della classe del servizio che invoca il provider predefinito, la quale potrebbe utilizzarli e quindi violare l’integrità del provider.

Per lo stesso motivo un provider non deve esporre tipi pubblici, ad esempio una classe personalizzata per gestire le eccezioni, poiché in questo caso la classe deve essere necessariamente conosciuta dal servizio e dagli altri providers dello stesso servizio, i quali potrebbereo essere scritti da un differente team di sviluppo. All’uopo il .NET Framework 2.0 mette a disposizione la classe System.Configuration.Provider.ProviderException che deve essere usata per sollevare eccezioni non previste altrimenti.

Infine, nella sezione di configurazione dovrebbero essere indicati i parametri di funzionamento relativi ai vari provider, e non quelli relativi all’intero servizio. Nel nostro esempio di servizio di data caching persistente, i parametri che regolano il caching delle informazioni (AbsoluteExpiration, SlidingExpiration, ecc) sono relativi all’intero servizio e non ad ogni singolo provider.

Allegato al presente articolo è disponibile una soluzione che comprende 4 progetti: il servizio DataCacheService basato su providers, l’interfacca utilizzata dai provider (DataCacheProviderBase), l’implementazione di due providers specifici (DataCacheProviders) ed infine una semplicissima pagina web che invoca il servizio.

Only published comments... Oct 15 2006, 12:59 PM by DotNetSide Staff
Filed under:

Comments

 

Maurizio Tammacco's blog said:

E' disponibile il mio primo articolo per il nuovo user group del sud Italia dotnetSide, dedicato alle...

October 16, 2006 2:05 PM
 

Maurizio Tammacco's blog said:

Negli ultimi tempi mi &#232; capitato spesso di lavorare su applicazioni che utilizzano Oracle come database

April 17, 2008 5:01 PM
Powered by Community Server (Commercial Edition), by Telligent Systems