Compilación bajo demanda en C#. Generación y compilación de código dinámicamente.
Introducción
Con el CLR 2.0, y en su caso desde la aparición de Microsoft .NET Framework 2.0, tenemos la posibilidad de hacer uso en C# de un namespace de nombre Microsoft.CSharp. Un nombre de espacio que en muchas ocasiones pasa por desapercibido para el programador y que podría resolvernos un sinfin de problemas en determinadas situaciones concretas.
Dentro de esta librería, encontraremos tres clases, dos de ellas obsoletas y de nombres Compiler y CompilerError, y una tercera clase que es sobre la que quiero hacer hincapié, de nombre CSharpCodeProvider.
Con la clase CSharpCodeProvider podemos acceder a instancias del generador y compilador de código C#. Dicho de otra forma, podríamos indicar un conjunto de instrucciones de código determinado para por ejemplo, evaluar expresiones compilando ese código en tiempo de ejecución para consumirlo por el ensamblado que lance esa porción o instrucciones de código.
Esto es muy útil de acuerdo a diferentes escenarios en los cuales, el código podría ser volatil o estar expuesto a cambios determinados en cálculos, o acciones determinadas, lo cual nos forzaría a lanzar el código bajo demanda.
Escenario de partida de ejemplo
Para situarnos, pensemos en una situación teórica concreta en la cual nuestro ensamblado debe ejecutar en tiempo de ejecución un conjunto de instrucciones u otro según el valor de una determinada variable.
Una forma de resolver esto podría ser introduciendo una claúsula if que de acuerdo a esa variable ejecutase una porción de código u otra, pero pensemos más allá e imaginemos que esa porción de código pudiera variar continuamente, ¿cómo podríamos resolver esto?.
Poner muchas claúsulas if recogiendo todas las posibilidades podría resolvernos el problema, pero además de no ser operativo, es que tampoco tenemos claro de que recojamos todas las posibilidades realmente, y además, solo se ejecutaría en nuestro caso o true o false de acuerdo al valor de esa variable.
Además de no parecer una solución muy limpia, engordaríamos el código con morralla y tendríamos que recompilar la aplicación cada vez que hubiera un cambio (pensemos en que el ejemplo que expongo es muy concreto y que son cambios frecuentes en el tiempo).
Se me ocurre no obstante otra solución.
La disposición de unas instrucciones persistidas en fichero, base de datos, etc., que carguen las líneas de código del conjunto de instrucciones que queremos lanzar en la aplicación, y que no nos obligue a recompilar nuestra aplicación cada vez que se produce un cambio de cálculo en algunos de los conjuntos de código.
¿Y cómo hacer esto?.
Poniendo en práctica un ejemplo teórico
Utilizando como ya he adelantado antes el nombre de espacio Microsoft.CSharp, y concretamente la clase CSharpCodeProvider.
Pero para utilizar toda la funcionalidad de la generación y compilación de código de forma dinámica, deberemos hacer uso de otro nombre de espacio de nombre System.CodeDom.Compiler.
Este nombre de espacio contiene tipos que nos permite administración la generación y compilación de código en aquellos lenguajes de programación que sean compatibles con .NET, como el ejemplo que nos ocupa con C#.
CodeDOM (Code Document Object Model) es por su parte, una estructura de modelos de código fuente.
Dentro de este nombre de espacio encontraremos diferentes clases e interfaces listas para generar y compilar nuestro las instrucciones de código que queramos.
Así que nos ponemos manos a la obra.
He creado una aplicación Windows donde he insertado tres controles TextBox, dos contoles radioButton, un control Label y un control Button.
El aspecto de nuestra aplicación es el siguiente:
Como podemos ver en la imagen, dentro de la primera y de la segunda caja de texto, tenemos dos instrucciones de código que son las siguientes:
Para la primera caja de texto:
using System;
public class ClaseCalculo
{
public ClaseCalculo() {}
public int MiMetodo()
{
int i = {0};
return i * 2;
}
}
Para la segunda caja de texto:
using System;
public class ClaseCalculo
{
public ClaseCalculo() {}
public int MiMetodo()
{
int i = {0};
return i * 3;
}
}
Además, dentro de la caja de texto del valor de la variable int i, le vamos a dar el valor de la expresión, que obtendremos de la tercera caja de texto.
Finalmente, el código de nuestro ensamblado asociado al control Button será el siguiente:
using System.CodeDom.Compiler;
…
{
// Declaramos la clase CSharpCodeProvider.
CSharpCodeProvider csharpCodeProvider = new CSharpCodeProvider();
// Declaramos la clase CodeDomProvider.
CodeDomProvider codeDomProvider = CodeDomProvider.CreateProvider(«C#»);
CompilerParameters compilerParameters = new CompilerParameters();
// Generamos el codigo del ensamblado en memoria.
compilerParameters.GenerateInMemory = true;
// No generamos un fichero ejecutable.
compilerParameters.GenerateExecutable = false;
// Indicamos las referencias al ensamblado.
compilerParameters.ReferencedAssemblies.Add(«system.dll»);
// Recuperamos el valor de la expresique queremos ejecutar internamente.
int expression;
bool resultExpression = Int32.TryParse(this.textBox3.Text, out expression);
if (resultExpression)
{
// Preparamos las instrucciones de codigo de forma dinamica
// de acuerdo al valor de una variable o en este caso de un
// control de tipo radioButton.
string source = «»;
if (this.radioButton1.Checked)
{
source = this.textBox1.Text;
}
else
{
source = this.textBox2.Text;
}
// Despues de recuperar las instrucciones de codigo,
// preparamos el codigo junto al valor de la expresion.
source = source.Replace(«{0}», expression.ToString());
// Compilamos y obtenemos los resultados de la compilacion.
CompilerResults compilerResults = csharpCodeProvider.CompileAssemblyFromSource(compilerParameters, source);
// Miramos si hay errores o no.
if (compilerResults.Errors.Count > 0)
{
foreach (CompilerError compilerError in compilerResults.Errors)
{
MessageBox.Show(«Error compilando.»
+ Environment.NewLine
+ Environment.NewLine
+ String.Format(«Error en linea {0} y columna {1}.», compilerError.Line, compilerError.Column)
+ Environment.NewLine
+ compilerError.ErrorText);
}
}
else
{
// Obtenemos el valor
Type type = compilerResults.CompiledAssembly.GetType(«ClaseCalculo»);
Object objectEvaluator = Activator.CreateInstance(type);
MethodInfo methodInfo = type.GetMethod(«MiMetodo»);
var result = methodInfo.Invoke(objectEvaluator, new object[0]);
MessageBox.Show(String.Format(«Resultado: {0}», result));
}
}
else
{
MessageBox.Show(String.Format(«El valor de la expresion {0} no es valido.», this.textBox3.Text));
}
}
Una vez que hemos preparado nuestra aplicación, pasaremos a ejecutarla.
Para comprobar que nuestra aplicación ejecuta dinámicamente el código de las cajas de texto que contiene las instrucciones de código, vamos a alternar la ejecución de uno u otro conjunto de instrucciones haciendo clic sobre cada uno de los controles radioButton.
Observaremos que la aplicación devuelve dinámicamente un resultado u otro de acuerdo a nuestra selección.
Ahora bien, cambiemos un valor o una parte del conjunto de instrucciones de código de una u otra caja de texto y ejecutemos nuevamente el procedimiento.
Si lo hemos hecho bien, obtendremos un resultado positivo.
Si hemos cometido algún error, obtendremos un mensaje de error como se indica a continuación.
Como podemos ver, el compilador nos devuelve un mensaje de error que es el que en nuestro caso, he mostrado por pantalla.
Conclusión
Aún y así, debemos tener en cuenta la posibilidad de buscar una respuesta rápida por parte la aplicación.
La compilación bajo demanda o dinámica nos ofrece una salida según determinadas situaciones, pero tampoco debemos usarla como norma, ya que penaliza en rendimiento más que si no tuviésemos que generar y compilar código dinámicamente.
Dentro de la generación y compilación, por regla general CompileAssemblyFromSource es más lento que CompileAssemblyFromDom, pero en algunas situaciones, podemos encontrarnos con el caso inverso.
7 Responsesso far
Una aclaración, todo ésto ya existía en .NET 1.1
Y un consejo, basado en la experiencia 😉
Cuidado con abusar, cada vez se genera un NUEVO Assembly en memoria, con sus Types, MethodInfo, etc etc etc. Hace unos años tuve que aislar código de este estilo en otro AppDomain para poder descargar el AppDomain y no ir perdiendo memoria.
🙂
Hola campeón!
Otro posible uso (para mi más útil todavía) es el de poder parametrizar o adaptar tu aplicación para varios clientes finales.
Saludetes!
Hola Pablo,
Microsoft.CSharp y su clase CSharpCodeProvider que reside dentro de System.dll está efectivamente desde antes de Microsoft .NET Framework 2.0 (creía y estaba convencido de que era a partir de .NET Framework 2.0), concretamente y como muy apuntas, desde Microsoft .NET Framework 1.1 (muchas gracias por la aclaración). 🙂
Lo mismo ocurre con el nombre de espacio System.CodeDom.Compiler.
Sobre lo que comentas de tu experiencia, es así y efectivamente, hay que tener mucho cuidado con su uso sobre todo de cara al rendimiento y con la generación del ensamblado en memoria.
Es muy práctico, pero no hay que abusar y nos obliga a hacer estudios previos para saber si lo estamos aplicando bien o penaliza más que ayuda.
Sobre tu comentario Lluis, me parece otro punto de uso útil de esta posibilidad.
De hecho, con un mismo «esqueleto», podemos hacer uso de ese esqueleto de acuerdo al cliente y tan solo parametrizar los cambios basados en partes de código dinámicas.
Pero como apuntaba Pablo… mucho cuidado con su aplicación. 🙂
Saludos.
Siguiendo con este tema, encuentro muy util la posibilidad de incrustrar en nuestras aplicaciones el DLR. En concreto, hacer la extensión de la aplicación con IronPython.
Un saludo
Ahí estamos Javier.
Justamente la idea que tengo es la de abrir boca con esta entrada y hacer en cuanto pueda y el tiempo me lo permita una entrada sobre DLR. 🙂
Hola!!!
Yo creo que este artículo explica bien el contexto de compilación bajo demanda, pero estoy seguro que el C# aun es lo mejor medio de crear una página web.
Muy buen post me fue de mucha ayuda gracias