Twitter Facebook RSS Feed

sábado, 14 de noviembre de 2009 a las 19:13hs por Gustavo Cantero (The Wolf)

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.
Descargar proyecto de ejemplo

31 comentarios »

  1. Gerson dice:

    Coño que cosa tan complicada carajo!

  2. carlos de la barrera dice:

    Estimado Gustavo,

    Hoy he estado trabajando con su código y básicamente he creado la clase en el namespace de mi proyecto y pegado sú codigo pero me sale una excepción, seguro que estoy haciendo algo mal.

    la excepcion que me aparece es la siguiente : No se puede cargar el archivo o ensamblado ‘file:///C:\Users\Carlos\AppData\Local\Temp\rctk1y4p.dll’ ni una de sus dependencias. El sistema no puede encontrar el archivo especificado.

    Este es el código de la clase, estoy trabajando en VS2008: Espero no molestarlo.
    muchas gracias
    saludos
    Carlos de la B.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.CodeDom.Compiler;
    using System.Reflection;

    namespace wrappermanager
    {
    public static class ClassCalculus
    {
    //codigo por: Gustavo Cantero
    //https://www.programandoamedianoche.com

    public static object Resolver(string Formula)
    {
    //Parámetros del compilador
    CompilerParameters objParametros = new CompilerParameters()
    {
    GenerateInMemory = true,
    GenerateExecutable = false,
    IncludeDebugInformation = false
    };

    //Clase
    string strClase = «»;
    strClase = «using System;» + «namespace wrappermanager {» + «public class ClassCalculus {» + «public object Resolver() {» + «return » + Formula + «;}}}»;

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

    CompilerResults objResultados = objCompiler.CompileAssemblyFromSource(objParametros, strClase);
    object objClase = objResultados.CompiledAssembly.CreateInstance(«wrappermanager.Formula», false, BindingFlags.CreateInstance, null, null, null, null);

    return objClase.GetType().InvokeMember(«Resolver», BindingFlags.InvokeMethod, null, objClase, null);
    }
    }
    }

    • Carlos:
      Creo que el problema está en que tanto la clase precompilada (ClassCalculus) como la que compilas en tiempo de ejecución tienen el mismo nombre de namespace, clase y método, por lo tanto el CLR no sabe que ensamblado utilizar. Prueba cambiando el namespace de la clase a crear, por ejemplo, por «wrappermanagerDyn» tanto en el string (strClase) como al momento de crear la instancia con el método CreateInstance.
      Por favor cuéntame si funcionó.
      Saludos.

  3. carlos de la barrera dice:

    Muchas Gracias por la Respuesta Gustavo,

    He cambiado el namespace de la clase linea strClase, ha quedado así.
    strClase = «using System;» + «namespace wrappermanagerDyn {» + «public static ClassCalculus {» + «public object Resolver() {» + «return » + Formula + «;}}}»

    También he cambiado el namespace en la linea del CreateInstance, ha quedado así.
    object objClase = objResultados.CompiledAssembly.CreateInstance(«wrappermanagerDyn.ClassCalculus», false, BindingFlags.CreateInstance, null, null, null, null);

    y aparece el error nuevamente. El error se produce en tiempo de ejecución. y señala la linea,
    object objectClase = …

    Estoy instanciando está clase desde otra, la cual toma la formula desde un textbox.

    Muchas Gracias por su ayuda.
    saludos
    Carlos de la Barrera.

    • Carlos:
      El otro punto que veo que, según creo, está erroneo es que la clase a crear dinámicamente es del tipo «static», por lo cual no se puede instanciar. Prueba utilizando esta linea (es igual a la tuya pero sin el static) y cuéntame como te fue.

      strClase =
      "using System;" +
      "namespace wrappermanagerDyn {" +
      "public class ClassCalculus {" +
      "public object Resolver() {" +
      "return " +
      Formula +
      ";}}}";

      Saludos.

  4. carlos de la barrera dice:

    Hola Gustavo,

    Lo he probado y el resultado es el mismo.

    Saludos
    Carlos.

  5. carlos de la barrera dice:

    Hola Gustavo,

    He probado el proyecto que me ha enviado y funciona perfectamente, no se que estare haciendo mal con el mío.

    Estare escarbando en los códigos y comparando las cosas que ha hecho y que hice.
    Muchas Gracias por el Support.

    Saludos ooordiales desde Barcelona
    Carlos de la Barrera

  6. Eduard dice:

    Buenas,

    estuve peleando la semana pasada con el código, ya que como a Carlos, a mí me generaba el mismo error. Finalmente lo solucioné añadiendo a la colección ReferencedAssemblies del objeto objParametros todas las librerias necesarias ( objParametros.Add(«System.dll») )

    Espero sirva de ayuda,
    Gracias

  7. Eduard dice:

    Buenas,

    de nada! Sólo corregir un error, en el ejemplo que dí, es objParametros.ReferencedAssemblies.Add(«System.dll»), por supuesto.

  8. […] en tiempo de ejecución Publicado el 21 enero, 2011 por mdnrbls Basado en la entrada Cómo crear una clase dinamicamente y ejecutarla de Gustavo Cantero del blog […]

  9. Edmundo Dante dice:

    Excelente artículo; por afición lo estudie y he pasado dos días muy entretenido, aunque es mi primer acercamiento a la programación quizás a alguien le sirva lo siguiente: se requiere descargar e instalar las versiones mas nuevas de C# y IIS, visual studio mandan error de version al editar los archivos y me pareció muy complicado, pero lo edite con notepad y compile en linea de comando fácilmente una vez que incluí el path del compilador en variables del sistema, mando errores similares pero indicando donde se encuentra la dll generada con la opcion /t del compilador en el archivo web.config en opción quedo listo, lo hice en 3 equipos para garantizar que funcionaba y la primera vez donde mas tarde fue en instalar y crear el sitio en IIS.

  10. Deiver Cordero dice:

    ¿Por casualidad no tienes un ejemplo en VB net? y una pregunta ¿puede usarse vbscript en este ejemplo?

    • No tengo un ejemplo para VB.Net, pero no debería ser muy dificil de crear, sólo debes hacer que en lugar de crear una clase con la sintaxis de C# la cree con la de VB.NET y reemplazar la linea que dice

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

      por

      Dim objCompiler As CodeDomProvider = CodeDomProvider.CreateProvider("VB");

      En vbscript es aún más facil, simplemente debes utilizar la función Eval para que te devuelva el resultado de lo que quieras ejecutar, por ejemplo, en el código que escribí se le pasa una fórmula a la clase para que la ejecute y devuelva el resultado, en VBScript sólo hay que hacer algo así:

      Dim formula, resultado
      formula = "2+2"
      resultado = Eval(formula)

      Suerte!

  11. Erik dice:

    buenos días quería pedirte ayuda con interop. lo que pasa es lo siguiente.

    Me pidieron que en una tabla pueda agregar en un campo el nombre y el tipo de un atributo. ej.

    NombreCliente – string
    EdadCliente – int
    DocumentoIndentidad -string

    y en un formulario en tiempo de ejecución se me pueda crear esos registros de la tabla como atributos en el formulario o en una clase pero que sea en tiempo de ejecución.
    Esto con la finalidad de que la clase clientes sea dinamica y si le agrego registros en la tabla estos se puedan crear como atributos en mi formulario.

    saludos

    a la espera de tu respuesta

  12. Edgar M.Fco dice:

    Hola Amigo tengo una pregunta.
    Tengo una tabla X en SQL, esta tabla puede mutar, como puede crecer en columnas o disminuir. Cual seria la mejor opcion a tu parecer para crear una clase dinamicamente de esta tabla y poder trabajar con ella.

    No es que quiera el codigo hecho ya bastante has hecho con este magnifico aporte , solo ideas para poder implementar algo solido.

    • Hola, Edgar.
      Yo creo que para eso te conviene utilizar la clase ExpandoObject, que te permite agregarle propiedades a un objeto del tipo dynamic.
      Por ejemplo, podrías hacer algo así:

      dynamic employee, manager;
      
      employee = new ExpandoObject();
      employee.Name = "John Smith";
      employee.Age = 33;
      
      manager = new ExpandoObject();
      manager.Name = "Allison Brown";
      manager.Age = 42;
      manager.TeamSize = 10;

      Fijate que las propiedades Name, Age y TeamSize no existen en la definición de la clase ExpandoObject, sino que las crea dinámicamente.
      Espero te sirva.
      Suerte!

  13. David Moreno dice:

    Buenos Días

    Tengo una pregunta , y si quisiera hacer solo una linea de código y que se ejecute dentro de la misma clase y no en otra ?

  14. David Moreno dice:

    lo que pasa es que estoy haciendo un fileupload de manera en que solo me toque subir la dirección y se cargue automáticamente , de manera que para que cargue el nombre estoy haciendo lo siguiente :
    definí tres archivos todos con el mismo nombre y la única diferencia el numero final

    string NombreControl = Sarlaft.ID;
    string Num = NombreControl.Replace(«FSarlaft»,»»);
    string NombreSarlaft= «LbSarlaft»+Num+».Text=»+serverFileName+»;»;
    Donde sarlaft es el nombre del input file y num traeria el numero final , lo que quiero es ejecutar el string NombreSarlaft

    • Entonces podrías hacer algo así (reemplazando el tipo «MiPagina» por la clase de tu página):

      string strClase =
          "using System;" +
          "namespace MiNamespace  {" +
          "public class MiClase {" +
              "public void Ejecutar(MiPagina miPagina) {" +
                  "return miPagina.LbSarlaft" + Num + ".Text=\"" + serverFileName + '"' +
          ";}}}";

      y pasarle por parámetro una referencia de tu página:

      objClase.GetType().InvokeMember("Ejecutar", BindingFlags.InvokeMethod, null, objClase, new object[] { this });

      Espero te sirva.
      Saludos.

  15. Lorena Espinoza dice:

    Hola, ando con un problemita. Yo lo que quiero hacer es llamar con un «String» a un metodo o clase. Las Clases y los métodos que llamare ya existen en el proyecto. La idea en general es que tengo una tabla que va a contener el nombre del metodo o clase que va a ejecutar.

    foreach(DataRow r in Dt.rows)
    {
         r["Nombre_Clase"](Parametro1, Parametro2); // Algo asi... pero cual es la sintaxis para que este texto se interprete o se considere como el nombre del metodo o clase y lo ejecute
    }

    ayuda porfa 🙂

    • Hola, Lorena.
      No podes «llamar» a una clase, pero si a sus métodos.
      Para esto podrías utilizar reflection, haciendo algo como esto:

      miClase miObjeto = new miClase();
      Type tipoDeMiClase = typeof(miClase);
      foreach(DataRow r in Dt.rows)
      {
      	MethodInfo miMetodo = tipoDeMiClase.GetMethod(r["Nombre_Metodo"]);
      	miMetodo.Invoke(miObjeto, new object[]{ Parametro1, Parametro2 });
      }

      Seguro con esto podés hacer lo que necesitás.
      Por favor después contame si te sirvió.
      Saludos.

  16. Leo dice:

    Hola.
    Estoy teniendo un problema al generar código dinámicamente. Recibo el mismo error que el que indica este mensaje al principio: «No se puede cargar el archivo o ensamblado…»
    Lo raro es que el códgio está en una dll (Capa BL) que es referenciada desde una aplicación Windows Form y desde un capa de Servicios Web.
    Cuando la llamo desde la aplicación Windows Forms anda bien.
    Cuando la llamo desde la aplicación de servicios Web me tira este error.

    El código que uso es el siguiente:

    Private Function GenerarInstanciaVBNet(ByVal pCodigo As String) As Object
            Dim mResultadoCompilacion As System.CodeDom.Compiler.CompilerResults
            Try
                Dim mVBProvider As VBCodeProvider = New VBCodeProvider
    
                Dim mCompilador As System.CodeDom.Compiler.ICodeCompiler = mVBProvider.CreateCompiler()
                Dim mParametros As System.CodeDom.Compiler.CompilerParameters = New System.CodeDom.Compiler.CompilerParameters
    
                mParametros.ReferencedAssemblies.Add("System.dll")
                mParametros.ReferencedAssemblies.Add("System.xml.dll")
                mParametros.ReferencedAssemblies.Add("System.data.dll")
                mParametros.ReferencedAssemblies.Add("DataGenBL.dll")
                mParametros.CompilerOptions = "/t:library"
                mParametros.GenerateInMemory = True
                Dim sb As System.Text.StringBuilder = New System.Text.StringBuilder("")
                sb.Append("Imports System" & vbCrLf)
                sb.Append("Imports System.Xml" & vbCrLf)
                sb.Append("Imports System.Data" & vbCrLf)
                sb.Append("Imports System.Data.SqlClient" & vbCrLf)
                sb.Append("Imports Microsoft.VisualBasic" & vbCrLf)
                sb.Append("Imports DataGenBL" & vbCrLf)
    
                sb.Append("Namespace GeneradorDatos  " & vbCrLf)
                sb.Append("Class ClaseEval " & vbCrLf)
                sb.Append("public Function  EvaluarCodigo(_Fila as TablaGeneracion) as Object " & vbCrLf)
                'sb.Append("YourNamespace.YourBaseClass thisObject = New YourNamespace.YourBaseClass()")
    
                sb.Append(pCodigo & vbCrLf)
                sb.Append("End Function " & vbCrLf)
                sb.Append("End Class " & vbCrLf)
                sb.Append("End Namespace" & vbCrLf)
                mResultadoCompilacion = mCompilador.CompileAssemblyFromSource(mParametros, sb.ToString())
                Dim mAssembly As System.Reflection.Assembly = mResultadoCompilacion.CompiledAssembly
                Return mAssembly.CreateInstance("GeneradorDatos.ClaseEval")
    
            Catch ex As IO.FileNotFoundException
                If Not IsNothing(mResultadoCompilacion) Then
                    Throw New EvaluadorColumnaCompilationException("Errores de compilación", ex, mResultadoCompilacion.Errors)
                Else
                    Throw New Exception("Error de compilación")
                End If
            Catch ex As Exception
                Dim exBase As Exception = ExcepcionPrimitiva(ex)
                Throw New Exception(exBase.Message & " " & exBase.Source & " " & exBase.TargetSite.Name)
            End Try
        End Function

    _Fila es un parámetro que le paso en tiempo de ejecución del tipo TablaGeneracion (definido en la misma BL)

    Si por ejemplo paso como código lo siguiente:

    If _Fila.Columnas("Sexo").DatoGenerado = 1 Then
        Return _Fila.ColumnasGeneradas("Nombre masculino").DatoGenerado & " " & _Fila.Columnas("Apellido").DatoGenerado
    Else
        Return _Fila.ColumnasGeneradas("Nombre de mujer").DatoGenerado & " " & _Fila.Columnas("Apellido").DatoGenerado
    End If

    Debería compilarme el siguiente código:

    Imports System
    Imports System.Xml
    Imports System.Data
    Imports System.Data.SqlClient
    Imports Microsoft.VisualBasic
    Imports DataGenBL
    Namespace GeneradorDatos  
    Class ClaseEval 
    public Function  EvaluarCodigo(_Fila as TablaGeneracion) as Object 
    If _Fila.Columnas("Sexo").DatoGenerado = 1 Then
        Return _Fila.ColumnasGeneradas("Nombre masculino").DatoGenerado & " " & _Fila.Columnas("Apellido").DatoGenerado
    Else
        Return _Fila.ColumnasGeneradas("Nombre de mujer").DatoGenerado & " " & _Fila.Columnas("Apellido").DatoGenerado
    End If
    End Function 
    End Class 
    End Namespace

    Desde la aplicación Winforms anda perfecto, pero cuando se invoca desde la aplicación de sevicios aparece el error.

    Ya verifiqué que estén todas las referencias bien.
    Alguna ayuda?
    Gracias

  17. Harold Toval dice:

    Muy excelente aporte, un 10

  18. Vladimir dice:

    Muy buen aporte, Gracias.

    Tengo una pregunta, es posible crear variables del tipo de este tipo de clase creada.

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.