Icono del sitio Programando a medianoche

Cómo crear una clase dinámicamente y ejecutarla

Revisando los newsgroups de Microsoft encontré una persona que preguntaba cómo hacer para “ejecutar un string”, o sea, escribir una fórmula matemática en una cadena de texto y luego obtener el resultado de la misma. En ese momento recordé que hace unos años tuve que hacer esto mismo utilizando .NET Framework 1.1, fue entonces cuando me decidí a buscar aquel código y escribir este artículo.
Para hacer esto necesitamos crear una clase, compilarla en memoria, luego instanciarla y por último ejecutar el método que devuelva el resultado en cuestión.

La clase la debemos crear en un string incluyendo los using necesarios, el namespace a utilizar y el método a ejecutar, el cual va a resolver la fórmula. Al compilador debemos pasarle varios parámetros a través de la clase CompilerParameters, donde vamos a indicarle que debe generar el ensamblado en memoria, que no debe generar un ejecutable y que no incluya (o si, depende de la necesidad) la información para debug.

CompilerParameters Parametros = new CompilerParameters()
{
    GenerateInMemory = true,
    GenerateExecutable = false,
    IncludeDebugInformation = false
};

Luego debemos crear el compilador con el método estático CreateProvider de la clase CodeDomProvider, al cual debemos pasarle el lenguaje que queremos utilizar, en nuestro ejemplo “CSharp”. Hay que tener en cuenta que el nombre del lenguaje es “case sensitive”, por lo que hay que tener cuidado de escribirlo con las mayúsculas y minúsculas correspondientes.

CodeDomProvider objCompiler = CodeDomProvider.CreateProvider("CSharp");

Una vez creado el compilador debemos pasarle la clase y los parámetros creados anteriormente para que genere el ensamblado necesario en memoria:

CompilerResults objResultados = objCompiler.CompileAssemblyFromSource(objParametros, strClase);

Por último debemos crear una instancia de la clase y llamar al método que creamos y que va a calcular la fórmula:

object objClase = objResultados.CompiledAssembly.CreateInstance("MiNamespace.MiClase", false, BindingFlags.CreateInstance, null, null, null, null);
return objClase.GetType().InvokeMember("MiMetodo", BindingFlags.InvokeMethod, null, objClase, null);

El método CreateInstance posee varios parámetros para globalización, parámetros para pasarle al método, etc., pero para nuestro ejemplo, al no necesitarlos, los vamos a establecer en null.

Una vez explicado lo que necesitamos hacer, les paso el código del método a ejecutar para que resuelva fórmulas o cualquier línea de C# (por ejemplo, búsquedas de cadenas de texto, etc.) y devuelva el valor:

namespace WebApplication1
{
    using System.CodeDom.Compiler;
    using System.Reflection;

    public static class Formula
    {
        ///
        /// Resuelve el valor de una fórmula
        /// 
        ///Fórmula a resolver
        /// Resultado
        /// double Resultado = Formula.Resolver("2 * 8 + 3");
        public static object Resolver(string Formula)
        {
            //Parámetros del compilador
            CompilerParameters objParametros = new CompilerParameters()
            {
                GenerateInMemory = true,
                GenerateExecutable = false,
                IncludeDebugInformation = false
            };

            //Clase
            string strClase =
                "using System;" +
                "namespace Scientia {" +
                "public class Formula {" +
                    "public object Ejecutar() {" +
                        "return " + Formula +
                ";}}}";

            //Compilo todo y ejecuto el método
            CodeDomProvider objCompiler = CodeDomProvider.CreateProvider("CSharp");

            //En .NET 1.1 usaba esta linea:
            //ICodeCompiler ICC = (new CSharpCodeProvider()).CreateCompiler();

            CompilerResults objResultados = objCompiler.CompileAssemblyFromSource(objParametros, strClase);
            object objClase = objResultados.CompiledAssembly.CreateInstance("Scientia.Formula",
                false, BindingFlags.CreateInstance, null, null, null, null);
            return objClase.GetType().InvokeMember("Ejecutar", BindingFlags.InvokeMethod, null, objClase, null);
        }
    }
}

Como ejemplo podemos crear una página donde el usuario pueda ingresar una fórmula y al pulsar en un botón se muestre el resultado en la misma. La página debería quedar así:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebApplication1._Default" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">	 
  <title>Ejemplo de Scientia® Soluciones Informáticas</title>	 
</head>	 
<body>	 
  <form id="form1" runat="server">	 
    <asp:panel DefaultButton="btnCalcular" runat="server">	 
      <asp:textbox ID="txtFormula" runat="server"></asp:textbox>	 
      <asp:button ID="btnCalcular" runat="server" Text="=" OnClick="btnCalcular_Click"></asp:button>	 
      <asp:label ID="lblResultado" runat="server"></asp:label>	 
    </asp:panel>	 
  </form>	 
</body>
</html>

Y en el code behind de ésta debería tener lo siguiente:

using System;

namespace WebApplication1
{
    public partial class _Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
        }

        protected void btnCalcular_Click(object sender, EventArgs e)
        {
            lblResultado.Text = Formula.Resolver(txtFormula.Text).ToString();
        }
    }
}

Espero que este artículo les sea de utilidad y, como siempre, les dejo el proyecto de ejemplo para Visual Studio® 2008.

Salir de la versión móvil