Compilazione dinamica con C#

Rate this Content 0 Votes

Livello: Avanzato - Autore: Marco Minerva

Tempo fa ho partecipato ad una discussione sul forum Microsoft dedicato a C# in cui si parlava della compilazione dinamica di codice C#. Prendendo spunto dall'argomento trattato, ho realizzato una libreria, chiamata Light Script Engine, che permette di aggiungere alle proprie applicazioni un semplice motore di scripting, con possibilità di passare parametri di input e gestire i valori di ritorno. In questo articolo voglio mostrare alcuni esempi del suo utilizzo.

La prima cosa da fare è creare un nuovo progetto e aggiungere ad esso un riferimento alla libreria, che può essere scaricata da MSDN Code Gallery oppure, più comodamente, tramite NuGet:

Il cuore dello script engine è il metodo ScriptEngine.Run, che prende tre argomenti in ingresso:

  • sourceCode, ovvero il codice sorgente che vogliamo compilare;
  • context, un oggetto di tipo ScriptContext che possiamo utilizzare per passare argomenti allo script e leggere gli eventuali valori di ritorno;
  • errors, la lista degli eventuali errori che si sono verificati durante la compilazione.

Diamo uno sguardo alla classe ScriptContext, che ci servirà per l'interazione con lo script:

public class ScriptContext
{
    public object Arguments { get; set; }
    public object Result { get; set; }
    public Dictionary<object, object> Parameters { get; set; }
    public Exception Error { get; set; }

    public ScriptContext()
    {
        Parameters = new Dictionary<object, object>();
    }
}

La sua struttura è molto semplice e contiene 4 proprietà:

  • un oggetto, Arguments, a cui assegniamo gli argomenti da passare allo script;
  • un oggetto, Result, in cui possiamo impostare il valore di ritorno;
  • un dizionario, Parameters, utile per lo scambio di ulteriori valori da e verso lo script;
  • una eccezione, Error, che contiene l'eventuale errore verificatosi durante l'esecuzione dello script.

Ogni script che creiamo ha accesso ad una variabile di tipo ScriptContext e nome Context, che può utilizzare in lettura e scrittura. La compilazione vera e propria viene eseguita utilizzando la classe CSharpCodeProvider.

Partiamo da un esempio semplice: supponiamo di voler creare uno script che determina se l'anno corrente è bisestile e che restituisce tale informazione al termine dell'esecuzione.

var currentYear = DateTime.Now.Year;
Context.Result = DateTime.IsLeapYear(currentYear);

Innanzi tutto recuperiamo l'anno corrente e quindi utilizziamo la funzione DateTime.IsLeapYear per sapere se ci troviamo in un anno bisestile. Questo valore viene assegnato alla proprietà Context.Result, in modo che sia accessibile al termine dell'esecuzione.

Supponendo che esso sia contenuto in una variabile di nome script, il seguente codice lo manda in esecuzione:

ScriptEngine engine = new ScriptEngine();   
ScriptContext context = new ScriptContext();       
CompilerErrorCollection errors = null;

bool compilationSucceeded = engine.Run(script, context, out errors);

Se la compilazione ha successo, il metodo Run restitusce true. Altrimenti, in errors possiamo trovare la lista degli errori di compilazione, con le stesse informazioni che otterremmo compilando il codice direttamente da Visual Studio. Se proviamo ad eseguire lo script precedente, possiamo verificare che la variabile context.Result assumerà il valore true (nel caso di esecuzione nell'anno di scrittura di questo articolo, ovvero 2012).

Il passaggio di parametro è altrettanto semplice. Vediamo uno script che controlla se il numero passato è pari o dispari:

var number = Convert.ToInt32(Context.Arguments);
if (number % 2 == 0)
	Context.Result = true;
else
	Context.Result = false;

Il codice è abbastanza autoesplicativo: convertiamo in numero l'oggetto contenente gli argomenti e lo dividiamo per due per verificare se è pari.

Per passare allo script il valore di input, non dobbiamo fare altro che assegnare la proprietà context.Arguments prima di richiamare il metodo Run. Ad esempio:

context.Arguments = 11;
bool compilationSucceeded = engine.Run(script, context, out errors);

Questo esempio ci serve anche per mostrare un'altra situazione: gli errori di esecuzione dello script. Se ad esempio impostiamo la stringa "Marco" come argomento, la compilazione continuerà ad avere successo, ma durante l'esecuzione otterremo una eccezione di tipo FormatException (formato della stringa di input non corretto). Per verificare gli eventuali errori, dopo aver eseguito il metodo Run dello ScriptEngine, dobbiamo controllare la proprietà Error dello ScriptContext: se ha un valore diverso da null, essa rappresenta l'eccezione che si è verificata durante l'esecuzione.

Il codice visto finora è molto semplice, ma lo script engine dà accesso a tutta la Base Class Library di .NET, quindi possiamo ad esempio scrivere qualcosa del tipo:

/* Estrae il codice della città dagli argomenti */
string code = Convert.ToString(Context.Arguments);

/* Crea la richiesta HTTP e recupera lo stream con la risposta */
string url = string.Format("http://weather.yahooapis.com/forecastrss?w={0}&u=c", code);
var webRequest = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(url);
var webResponse = (System.Net.HttpWebResponse)webRequest.GetResponse();
var xmlReader = System.Xml.XmlReader.Create(webResponse.GetResponseStream());

/* Usa LINQ To XML per estrarre le informazioni metereologiche dalla risposta */
string WEATHER_NAMESPACE = "http://xml.weather.yahoo.com/ns/rss/1.0";
var data = System.Xml.Linq.XDocument.Load(xmlReader);
var condition = data.Descendants(System.Xml.Linq.XName.Get("condition", WEATHER_NAMESPACE)).FirstOrDefault();
var text = condition.Attribute("text").Value;
var temperature = condition.Attribute("temp").Value;

webResponse.Close();
Context.Result = string.Format("{0}, {1}° C", text, temperature);

Questo esempio utilizza il servizio Yahoo! Weather per recuperare un file XML contenente le previsioni della città di cui è stato indicato il codice (che si può recuperare visitando la pagina http://weather.yahoo.com), quindi sfrutta le potenzialità di LINQ To XML per estrarre la descrizione della condizione e la temperatura attuale.

La libreria scaricabile da MSDN Code Gallery comprende anche un'applicazione WPF che offre una serie di codici già impostati, in modo da valutare immediatamente le potenzialità dello script engine, e una semplice interfaccia per la compilazione e la visualizzazione degli eventuali errori:

Sicuramente c'è molto da aggiungere, questa libreria si limita a fornire il nucleo di base dello script engine, ma fornisce un'idea delle potenzialità di uno strumento del genere: Light Script Engine potrebbe essere il punto di partenza su cui costruire un linguaggio di scripting per la scrittura di macro con cui l'utente può personalizzare le proprie applicazioni.