Un BuildProvider per sezioni di configurazione personalizzate
I BuildProviders sono una delle novità introdotte nella versione 2.0 si ASP.NET che hanno cambiato il modello di compilazione di ogni richiesta gestita dal nuovo motore .NET per applicazioni web.
Ora infatti, ogni estensione che può essere gestita dal .NET framework (quindi pagine .aspx, user controls .ascx e così via...), è stata legata a dei particolari oggetti che si preoccupano di generarne la relativa classe .NET che verrà quindi compilata a run-time per permettere il funzionamento della risorsa richiesta.
Questi oggetti sono chiamati, appunto, BuildProviders e non sono nient’altro che dei generatori di classi .NET (scritte nel linguaggio scelto nell’applicazione web).
Per esempio, il BuildProvider legato all’estensione .aspx è rappresentato dalla classe PageBuildProvider.
<system.web>
<compilation>
<buildProviders>
<add extension=".aspx"
type="System.Web.Compilation.PageBuildProvider" />
</buildProviders>
</compilation>
</system.web>
Questo componente, ogni volta che l’applicazione viene pre-compilata, genera una classe per ogni pagina .aspx presente nell’applicazione e la salva della directory dei file temporanei di ASP.NET (C:\WINDOWS\Microsoft.NET\Framework\v2.0.xxxxx\Temporary ASP.NET Files\<Nome applicazione>); queste classi verranno poi compilate per permettere il corretto funzionamento dell’applicazione.
Tutte le nuove features di ASP.NET 2, come le Master Page, i Themes, i Profili e le Risorse per la localizzazione sono state realizzate seguendo questo approccio.
Per vedere quali generatori sono stati legati a quali estensioni, basta aprire il file web.config di base per tutte le applicazioni (presente all’interno della directory C:\WINDOWS\Microsoft.NET\Framework\v2.0.xxxxx\CONFIG) ed andare all’elemento system.web/compilation/buildProviders.
Generazione tramite CodeDOM
Ora vi chiederete come avvenga effettivamente la generazione della varie classi legate alle risorse richieste. Per questo è stata inventata la tecnica denominata CodeDOM, acronimo di Code Document Object Model, cui è allegato il relativo namespace (System.CodeDom).
Questa tecnica permette di utilizzare un set di classi pre-costruite che possono rappresentare tutti gli elementi e la struttura di un documento di codice sorgente .NET, indipendentemente dal linguaggio in cui sarà scritto il sorgente finale. Esistono infatti classi per rappresentare metodi, proprietà, variabili e altri tipi di costrutti che, messi insieme, possono creare ogni tipo di documento di codice sorgente utile ai nostri scopi.
Come avrete sicuramente capito, la tecnica rappresentata dal CodeDOM è strettamente legata ai BuildProviders, poiché permette l’effettiva generazione del codice sorgente legato ad una particolare estensione.
Un esempio concreto
Come la maggiorparte delle nuove features di ASP.NET, anche la parte dedicata ai BuildProviders può essere facilmente estesa per permetterci di definire delle nostre estensioni personalizzate e creare le nostre strutture dati, in base al contenuto delle risorse legate a tale estensione.
Questo è possibile ereditando dalla classe BuildProvider, classe astratta che espone tutti i metodi utili per la creazione di codice sorgente in grado di rappresentare la risorsa richiesta. Per permettere tale creazione è necessario legare un estensione al BuildProvider personalizzato all’interno del web.config e inserire i file con tale estensione all’interno della directory App_Code; in questo modo, ogni volta che l’applicazione web viene compilata, il motore di ASP.NET cerca il generatore legato alle nostre estensioni custom e, tramite esso, crea le varie strutture dati, che saranno poi direttamente disponibili all’interno della nostra applicazione.
Come esempio, ho deciso di creare un BuildProvider per la generazione automatica di sezioni di configurazione personalizzate per le nostre applicazioni web, poiché il loro l’utilizzo è diventato ormai indispensabile in ogni progetto e mi è sembrato utile creare un sistema per poter automatizzare tutto il processo di creazione dei vari elementi di configurazione.
Per prima cosa dobbiamo definire il file di risorsa da legare al nostro BuildProvider. Tale file, creato con estensione .configSection, definisce la struttura della nostra sezione di configurazione, specificando tutti i vari elementi e i tipi di dato legati ad ogni proprietà di questi elementi. Questo il file utilizzato come esempio:
<configSection name="MySection"
type="Peppe.Config.MySection"
namespace="Peppe.Config">
<MySection>
<Author Name="System.String" Surname="System.String" Age="System.Int32" />
<Articles>
<Article ID="System.Int32" Title="System.String" />
</Articles>
</MySection>
</configSection>
Ora, dobbiamo creare la classe che eredita da BuildProvider e dobbiamo definire tutti i metodi per effettuare le seguenti operazioni:
- Lettura del file .configSection
- Creazione delle struttura di dati
- Generazione delle classi, tramite CodeDOM, per la sezione di configurazione personalizzata
L’unico metodo che dobbiamo sovrascrivere è il metodo GenerateCode, che è il metodo che viene chiamato in fase di compilazione per ogni file .configSection.
namespace Peppe.BuildProviders
{
public class ConfigurationSectionBuildProvider : BuildProvider
{
public ConfigurationSectionBuildProvider()
{
}
public override void GenerateCode(AssemblyBuilder assemblyBuilder)
{
string filename = base.VirtualPath;
ConfigurationSection section = FillSection(filename);
CodeCompileUnit code = GenerateCodeDom(section);
assemblyBuilder.AddCodeCompileUnit(this, code);
}
...
}
}
All’interno di questo metodo prima di tutto preleviamo il percorso del file da trattare richiamando la proprietà di base VirtualPath, poi ne creiamo la struttura dati e la incapsuliamo all’interno di un oggetto di tipo ConfigurationSection, che fa parte delle strutture che ho creato per gestire le informazioni prelevate dal file risorsa.
N.B.: NON è la classe ConfigurationSection del namespace System.Configuration
In base alle informazioni lette, poi, verranno create le varie classi per la creazione della sezione di configurazione personalizzata tramite CodeDOM. Il metodo che esegue questo compito è il metodo GenerateCodeDom.
internal CodeCompileUnit GenerateCodeDom(ConfigurationSection section)
{
CodeCompileUnit code = new CodeCompileUnit();
//namespace della configuration section
CodeNamespace ns = new CodeNamespace(section.Namespace);
//namespace da importare
CodeNamespaceImport import = null;
import = new CodeNamespaceImport("System");
ns.Imports.Add(import);
import = new CodeNamespaceImport("System.Configuration");
ns.Imports.Add(import);
import = new CodeNamespaceImport("System.Collections.Generic");
ns.Imports.Add(import);
code.Namespaces.Add(ns);
//classe che descrive la sezione, derivata da ConfigurationSection
CodeTypeDeclaration cSection = new CodeTypeDeclaration(section.Name);
cSection.IsClass = true;
cSection.BaseTypes.Add(new CodeTypeReference("System.Configuration.ConfigurationSection"));
//definisco le proprietà della sezione (se ce ne sono)
foreach (KeyValuePair<string, object> kvp in section.Properties)
{
InsertProperty(cSection, kvp, false);
}
//definisco i sottoelementi
foreach (ConfigurationElement element in section.Elements)
{
if (element.Elements.Count == 0)
{
// creo la struttura per un elemento semplice
}
else
{
// creo la struttura per una collezione di elementi
}
}
// Aggiungo la classe della sezione di configurazione al namespace
ns.Types.Add(cSection);
return code;
}
Una volta creato il namespace in cui inserire tutte le varie classi, e una volta importati i namespace utili, il metodo GenerateCodeDom prima crea la classe derivata da ConfigurationSection che rappresenta la nostra sezione personalizzata, poi effettua il parsing degli elementi figli per stabilire se si trattano di elementi semplici
<Author Name="System.String" Surname="System.String" Age="System.Int32" />
o di collezioni di elementi
<Articles>
<Article ID="System.Int32" Title="System.String" />
</Articles>
In base al tipo di elemento corrente, verranno generate classi diverse.
Se ho a che fare con un elemento semplice, dovrò semplicemente creare una classe che derivi da ConfigurationElement con tutti i suoi attributi.
CodeTypeDeclaration cElement = new CodeTypeDeclaration(element.Name);
cElement.IsClass = true;
cElement.BaseTypes.Add(new CodeTypeReference("System.Configuration.ConfigurationElement"));
//codice per definire le varie proprietà
ns.Types.Add(cElement);
Se invece ho a che fare con una collezione di elementi di configurazione, dovrò creare la classe che rappresenta la collezione, derivata da ConfigurationElementCollection, implementarne le proprietà CollectionType e ElementName e i metodi CreateNewElement e GetElementKey ed infine dovrò creare la classe che rappresenti il singolo elemento della collezione (anche essa che erediti da ConfigurationElement).
CodeTypeDeclaration cCollectionElement = new CodeTypeDeclaration(subElementCollectionName);
cCollectionElement.IsClass = true;
cCollectionElement.BaseTypes.Add(new CodeTypeReference("System.Configuration.ConfigurationElementCollection"));
//Setto l'attributo ConfigurationCollection(typeof(...))
CodeAttributeDeclaration att = new CodeAttributeDeclaration(
"ConfigurationCollection",
new CodeAttributeArgument(new CodeArgumentReferenceExpression("typeof(" + subElementName + ")")));
cCollectionElement.CustomAttributes.Add(att);
//metodo CreateNewElement
CodeMemberMethod mtdCreateNewElement = new CodeMemberMethod();
mtdCreateNewElement.Attributes = MemberAttributes.Override | MemberAttributes.Family;
mtdCreateNewElement.Name = "CreateNewElement";
mtdCreateNewElement.ReturnType = new CodeTypeReference("ConfigurationElement");
mtdCreateNewElement.Statements.Add(
new CodeMethodReturnStatement(
new CodeSnippetExpression("new " + subElementName + "()")));
cCollectionElement.Members.Add(mtdCreateNewElement);
//metodo GetElementKey
CodeMemberMethod mtdGetElementKey = new CodeMemberMethod();
mtdGetElementKey.Attributes = MemberAttributes.Override | MemberAttributes.Family;
mtdGetElementKey.Name = "GetElementKey";
mtdGetElementKey.ReturnType = new CodeTypeReference(typeof(object));
mtdGetElementKey.Parameters.Add(new CodeParameterDeclarationExpression("ConfigurationElement", "element"));
CodeAssignStatement assign = new CodeAssignStatement(
new CodeSnippetExpression(subElementName + " p "),
new CodeCastExpression(subElementName, new CodeSnippetExpression("element"))
);
mtdGetElementKey.Statements.Add(assign);
mtdGetElementKey.Statements.Add(
new CodeMethodReturnStatement(new CodeSnippetExpression("p." + currentProperty))
);
cCollectionElement.Members.Add(mtdGetElementKey);
//proprietà CollectionType
CodeMemberProperty propCollType = new CodeMemberProperty();
propCollType.Name = "CollectionType";
propCollType.Type = new CodeTypeReference("ConfigurationElementCollectionType");
propCollType.Attributes = MemberAttributes.Public | MemberAttributes.Override;
propCollType.HasGet = true;
propCollType.HasSet = false;
CodeMethodReturnStatement ret1 = new CodeMethodReturnStatement(
new CodeSnippetExpression("ConfigurationElementCollectionType.BasicMap")
);
propCollType.GetStatements.Add(ret1);
cCollectionElement.Members.Add(propCollType);
//proprietà ElementName
CodeMemberProperty propElementName = new CodeMemberProperty();
propElementName.Name = "ElementName";
propElementName.Type = new CodeTypeReference("String");
propElementName.Attributes = MemberAttributes.Family | MemberAttributes.Override;
propElementName.HasGet = true;
propElementName.HasSet = false;
CodeMethodReturnStatement ret2 = new CodeMethodReturnStatement(
new CodeSnippetExpression("\"" + subElementName + "\"")
);
propElementName.GetStatements.Add(ret2);
cCollectionElement.Members.Add(propElementName);
ns.Types.Add(cCollectionElement);
Installazione del BuildProvider
Una volta creato il mio BuildProvider, devo assegnargli l’estensione .configSection all’interno del web.config e definire la mia sezione di configurazione personalizzata:
<configSections>
<section name="MySection" type="Peppe.Config.MySection"/>
</configSections>
<MySection>
<Author Name="Giuseppe" Surname="Marchi" Age="23" />
<Articles>
<Article ID="1" Title="Configuration Section in ASP.NET 2.0" />
<Article ID="2" Title="Creazione di pagine asincrone" />
<Article ID="3" Title="Hello Workflow" />
</Articles>
</MySection>
<system.web>
<compilation debug="true">
<buildProviders>
<add extension=".configSection"
type="Peppe.BuildProviders.ConfigurationSectionBuildProvider, Peppe.BuildProviders" />
</buildProviders>
</compilation>
</system.web>
Fatto ciò, ho prima bisogno di compilare il progetto per poi poter inserire la mia configuration section personalizzata ed utilizzare le classi generate per leggerne le informazioni.
Prendendo sempre dall’esempio, all’interno delle pagine della mia applicazione web, posso leggere il nome dell’autore e gli articoli presenti nella sezione di configurazione, in questo modo:
MySection config = (MySection)ConfigurationManager.GetSection("MySection");
StringBuilder sb = new StringBuilder();
sb.AppendFormat("<h4>{0} {1}</h4>", config.Author.Name, config.Author.Surname);
sb.AppendFormat("Age: {0}<br />", config.Author.Age);
sb.Append("<hr /><h3>Articles</h3>");
foreach (Article art in config.Articles)
{
sb.AppendFormat("{0} - {1}<br />", art.ID, art.Title);
}
Response.Write(sb.ToString());
Conclusioni
L’utilizzo dei BuildProvider è fondamentale per il funzionamento di tutto il meccanismo di compilazione delle applicazioni ASP.NET 2.0, ed è anche una tecnica formidabile per estendere tale meccanismo ed automatizzare tante semplici procedure che ci troviamo ad affrontare ogni giorno, attraverso la creazione di documenti di codice sorgente indipendentemente dal linguaggio scelto dalla vostra applicazione web.
Link utili
La classe BuildProvider
Il namespace System.CodeDom
Generazione e compilazione dinamica di codice sorgente
indietro