¿Cómo crear dinámicamente una clase?

222

Tengo una clase que se ve así:

public class Field
{
    public string FieldName;
    public string FieldType;
}

Y un objeto List<Field>con valores:

{"EmployeeID","int"},
{"EmployeeName","String"},
{"Designation","String"}

Quiero crear una clase que se vea así:

Class DynamicClass
{
    int EmployeeID,
    String EmployeeName,
    String Designation
}

¿Hay alguna forma de hacer esto?

Quiero que esto se genere en tiempo de ejecución. No quiero un archivo CS físico que resida en mi sistema de archivos.

ashwnacharya
fuente
44
¿Desea usar esa clase en tiempo de ejecución o solo generar un archivo?
Damian Leszczyński - Vash
Quiero que esto se genere en tiempo de ejecución. No quiero un archivo CS físico que resida en mi sistema de archivos. Perdón por no mencionar eso antes.
ashwnacharya
16
¿Puede darnos una idea aproximada de qué piensa hacer con esta clase?
Justin
3
@Justin implementa interfaces resueltas en tiempo de ejecución, por ejemplo.
AgentFire
Uno podría alimentarloSystem.ServiceModel.ChannelFactory<MyDynamicInterface>
Ilya Semenov

Respuestas:

298

Sí, puedes usar el System.Reflection.Emitespacio de nombres para esto. No es sencillo si no tienes experiencia con él, pero ciertamente es posible.

Editar: este código puede ser defectuoso, pero le dará la idea general y, con suerte, tendrá un buen comienzo hacia el objetivo.

using System;
using System.Reflection;
using System.Reflection.Emit;

namespace TypeBuilderNamespace
{
    public static class MyTypeBuilder
    {
        public static void CreateNewObject()
        {
            var myType = CompileResultType();
            var myObject = Activator.CreateInstance(myType);
        }
        public static Type CompileResultType()
        {
            TypeBuilder tb = GetTypeBuilder();
            ConstructorBuilder constructor = tb.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);

            // NOTE: assuming your list contains Field objects with fields FieldName(string) and FieldType(Type)
            foreach (var field in yourListOfFields)
                CreateProperty(tb, field.FieldName, field.FieldType);

            Type objectType = tb.CreateType();
            return objectType;
        }

        private static TypeBuilder GetTypeBuilder()
        {
            var typeSignature = "MyDynamicType";
            var an = new AssemblyName(typeSignature);
            AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);
            ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
            TypeBuilder tb = moduleBuilder.DefineType(typeSignature,
                    TypeAttributes.Public |
                    TypeAttributes.Class |
                    TypeAttributes.AutoClass |
                    TypeAttributes.AnsiClass |
                    TypeAttributes.BeforeFieldInit |
                    TypeAttributes.AutoLayout,
                    null);
            return tb;
        }

        private static void CreateProperty(TypeBuilder tb, string propertyName, Type propertyType)
        {
            FieldBuilder fieldBuilder = tb.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);

            PropertyBuilder propertyBuilder = tb.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);
            MethodBuilder getPropMthdBldr = tb.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes);
            ILGenerator getIl = getPropMthdBldr.GetILGenerator();

            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Ldfld, fieldBuilder);
            getIl.Emit(OpCodes.Ret);

            MethodBuilder setPropMthdBldr =
                tb.DefineMethod("set_" + propertyName,
                  MethodAttributes.Public |
                  MethodAttributes.SpecialName |
                  MethodAttributes.HideBySig,
                  null, new[] { propertyType });

            ILGenerator setIl = setPropMthdBldr.GetILGenerator();
            Label modifyProperty = setIl.DefineLabel();
            Label exitSet = setIl.DefineLabel();

            setIl.MarkLabel(modifyProperty);
            setIl.Emit(OpCodes.Ldarg_0);
            setIl.Emit(OpCodes.Ldarg_1);
            setIl.Emit(OpCodes.Stfld, fieldBuilder);

            setIl.Emit(OpCodes.Nop);
            setIl.MarkLabel(exitSet);
            setIl.Emit(OpCodes.Ret);

            propertyBuilder.SetGetMethod(getPropMthdBldr);
            propertyBuilder.SetSetMethod(setPropMthdBldr);
        }
    }
}
danijels
fuente
2
¡¡Increíble!! ¿Me puede decir también cómo crear un objeto del tipo devuelto por el Método CompileResultType ()?
ashwnacharya
44
Puede usar System.Activator para eso. Actualizaré la respuesta con un ejemplo.
danijels
44
Tenga en cuenta también que tendrá que usar la reflexión para examinar, leer y actualizar campos en su tipo dinámico. Si desea intellisense y no reflexionar, debe tener una clase base estática o una interfaz de la cual su clase dinámica herede y se pueda convertir. En ese caso, puede modificar el método GetTypeBuilder () y cambiar la llamada moduleBuilder.DefineType para incluir el tipo estático como último parámetro (ahora es nulo)
danijels
2
¿Alguien puede explicar cómo usar el objeto después de su creación
HELP_ME
3
@bugz usa el código anterior para crear la clase, luego en la clase base puedes agregar este método: public void SetValue <T> (nombre de cadena, valor T) {GetType (). GetProperty (nombre) .SetValue (este, valor ); }
stricq
71

Tomará algo de trabajo, pero ciertamente no es imposible.

Lo que he hecho es:

  • Cree una fuente de C # en una cadena (no es necesario escribir en un archivo),
  • Ejecútelo a través de Microsoft.CSharp.CSharpCodeProvider(CompileAssemblyFromSource)
  • Encuentra el tipo generado
  • Y crea una instancia de ese tipo ( Activator.CreateInstance)

De esta forma, puede manejar el código C # que ya conoce, en lugar de tener que emitir MSIL.

Pero esto funciona mejor si su clase implementa alguna interfaz (o se deriva de alguna clase base), de lo contrario, ¿cómo es que el código de llamada (léase: compilador) debe saber sobre esa clase que se generará en tiempo de ejecución?

Hans Kein
fuente
77
Tal vez quiera ver esta discusión: Reflexión-emisión-vs-codificado
nawfal
1
@nawfal, ashwnacharya, quería generar dinámicamente en tiempo de ejecución su clase con sus miembros originalmente contenidos en una lista. No creo que ponerlo en un archivo incluso generado en tiempo de ejecución sea una buena solución de rendimiento.
sodjsn26fr
39

También puede crear dinámicamente una clase utilizando DynamicObject .

public class DynamicClass : DynamicObject
{
    private Dictionary<string, KeyValuePair<Type, object>> _fields;

    public DynamicClass(List<Field> fields)
    {
        _fields = new Dictionary<string, KeyValuePair<Type, object>>();
        fields.ForEach(x => _fields.Add(x.FieldName,
            new KeyValuePair<Type, object>(x.FieldType, null)));
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        if (_fields.ContainsKey(binder.Name))
        {
            var type = _fields[binder.Name].Key;
            if (value.GetType() == type)
            {
                _fields[binder.Name] = new KeyValuePair<Type, object>(type, value);
                return true;
            }
            else throw new Exception("Value " + value + " is not of type " + type.Name);
        }
        return false;
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        result = _fields[binder.Name].Value;
        return true;
    }
}

Almaceno todos los campos de clase en un diccionario _fieldsjunto con sus tipos y valores. Ambos métodos pueden obtener o establecer el valor de algunas de las propiedades. Debe usar la dynamicpalabra clave para crear una instancia de esta clase.

El uso con su ejemplo:

var fields = new List<Field>() { 
    new Field("EmployeeID", typeof(int)),
    new Field("EmployeeName", typeof(string)),
    new Field("Designation", typeof(string)) 
};

dynamic obj = new DynamicClass(fields);

//set
obj.EmployeeID = 123456;
obj.EmployeeName = "John";
obj.Designation = "Tech Lead";

obj.Age = 25;             //Exception: DynamicClass does not contain a definition for 'Age'
obj.EmployeeName = 666;   //Exception: Value 666 is not of type String

//get
Console.WriteLine(obj.EmployeeID);     //123456
Console.WriteLine(obj.EmployeeName);   //John
Console.WriteLine(obj.Designation);    //Tech Lead

Editar: Y así es como se ve mi clase Field:

public class Field
{
    public Field(string name, Type type)
    {
        this.FieldName = name;
        this.FieldType = type;
    }

    public string FieldName;

    public Type FieldType;
}
Termininja
fuente
1
Me gustó este enfoque hasta que necesité inicializar los campos con el constructor. es decir dynamic obj = new DynamicClass(fields){EmployeeId=123456;EmployeeName = "John"; Designation = "Tech Lead";}, sería realmente genial hacer esto.
rey_coder 01 de
14

Sé que reabrí esta antigua tarea, pero con c # 4.0 esta tarea es absolutamente indolora.

dynamic expando = new ExpandoObject();
expando.EmployeeID=42;
expando.Designation="unknown";
expando.EmployeeName="curt"

//or more dynamic
AddProperty(expando, "Language", "English");

para más información, consulte https://www.oreilly.com/learning/building-c-objects-dynamically

usuario1235183
fuente
Sí, pero pierdes la seguridad de tipos aquí. ¿Podemos hacer algo similar mientras preservamos la seguridad de los tipos?
toughQuestions
13

No sé el uso previsto de tales clases dinámicas, y se puede hacer la generación de código y la compilación del tiempo de ejecución, pero requiere un poco de esfuerzo. Quizás los tipos anónimos te ayudarían, algo como:

var v = new { EmployeeID = 108, EmployeeName = "John Doe" };
Amittai Shapira
fuente
77
No puede codificar los nombres de los campos. Él les proporciona el suyo Field.FieldName. Tener que codificar los nombres de los campos invalida el propósito. Si tiene que hacer eso, también podría crear la clase.
toddmo
9

Quieres mirar CodeDOM . Permite definir elementos de código y compilarlos. Citando MSDN:

... Este gráfico de objeto se puede representar como código fuente utilizando un generador de código CodeDOM para un lenguaje de programación compatible. CodeDOM también se puede utilizar para compilar código fuente en un ensamblado binario.

Hemant
fuente
Quiero que esto se genere en tiempo de ejecución. No quiero un archivo CS físico que resida en mi sistema de archivos. Perdón por no mencionar eso antes.
ashwnacharya
1
@ashwnacharya: Usted puede utilizar CodeDOM tanto para generar el archivo de origen y la compilación en tiempo de ejecución!
Hemant
1
Sin embargo, tenga en cuenta que el compilador CodeDOM toma una cadena sin procesar y, por lo tanto, es posible que desee considerar "ataques de inserción de código" similares a los utilizados en XSS e inyección SQL.
cwap
6

Según la respuesta de @ danijels, cree dinámicamente una clase en VB.NET:

Imports System.Reflection
Imports System.Reflection.Emit

Public Class ObjectBuilder

Public Property myType As Object
Public Property myObject As Object

Public Sub New(fields As List(Of Field))
    myType = CompileResultType(fields)
    myObject = Activator.CreateInstance(myType)
End Sub

Public Shared Function CompileResultType(fields As List(Of Field)) As Type
    Dim tb As TypeBuilder = GetTypeBuilder()
    Dim constructor As ConstructorBuilder = tb.DefineDefaultConstructor(MethodAttributes.[Public] Or MethodAttributes.SpecialName Or MethodAttributes.RTSpecialName)

    For Each field In fields
        CreateProperty(tb, field.Name, field.Type)
    Next

    Dim objectType As Type = tb.CreateType()
    Return objectType
End Function

Private Shared Function GetTypeBuilder() As TypeBuilder
    Dim typeSignature = "MyDynamicType"
    Dim an = New AssemblyName(typeSignature)
    Dim assemblyBuilder As AssemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run)
    Dim moduleBuilder As ModuleBuilder = assemblyBuilder.DefineDynamicModule("MainModule")
    Dim tb As TypeBuilder = moduleBuilder.DefineType(typeSignature, TypeAttributes.[Public] Or TypeAttributes.[Class] Or TypeAttributes.AutoClass Or TypeAttributes.AnsiClass Or TypeAttributes.BeforeFieldInit Or TypeAttributes.AutoLayout, Nothing)
    Return tb
End Function

Private Shared Sub CreateProperty(tb As TypeBuilder, propertyName As String, propertyType As Type)
    Dim fieldBuilder As FieldBuilder = tb.DefineField("_" & propertyName, propertyType, FieldAttributes.[Private])

    Dim propertyBuilder As PropertyBuilder = tb.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, Nothing)
    Dim getPropMthdBldr As MethodBuilder = tb.DefineMethod("get_" & propertyName, MethodAttributes.[Public] Or MethodAttributes.SpecialName Or MethodAttributes.HideBySig, propertyType, Type.EmptyTypes)
    Dim getIl As ILGenerator = getPropMthdBldr.GetILGenerator()

    getIl.Emit(OpCodes.Ldarg_0)
    getIl.Emit(OpCodes.Ldfld, fieldBuilder)
    getIl.Emit(OpCodes.Ret)

    Dim setPropMthdBldr As MethodBuilder = tb.DefineMethod("set_" & propertyName, MethodAttributes.[Public] Or MethodAttributes.SpecialName Or MethodAttributes.HideBySig, Nothing, {propertyType})

    Dim setIl As ILGenerator = setPropMthdBldr.GetILGenerator()
    Dim modifyProperty As Label = setIl.DefineLabel()
    Dim exitSet As Label = setIl.DefineLabel()

    setIl.MarkLabel(modifyProperty)
    setIl.Emit(OpCodes.Ldarg_0)
    setIl.Emit(OpCodes.Ldarg_1)
    setIl.Emit(OpCodes.Stfld, fieldBuilder)

    setIl.Emit(OpCodes.Nop)
    setIl.MarkLabel(exitSet)
    setIl.Emit(OpCodes.Ret)

    propertyBuilder.SetGetMethod(getPropMthdBldr)
    propertyBuilder.SetSetMethod(setPropMthdBldr)
End Sub

End Class
Nikita Silverstruk
fuente
6

También puede crear dinámicamente una clase utilizando DynamicExpressions .

Dado que 'Dictionary's tiene inicializadores compactos y maneja colisiones clave, querrás hacer algo como esto.

  var list = new Dictionary<string, string> {
    {
      "EmployeeID",
      "int"
    }, {
      "EmployeeName",
      "String"
    }, {
      "Birthday",
      "DateTime"
    }
  };

O puede que desee utilizar un convertidor JSON para construir su objeto de cadena serializada en algo manejable.

Luego usando System.Linq.Dynamic;

  IEnumerable<DynamicProperty> props = list.Select(property => new DynamicProperty(property.Key, Type.GetType(property.Value))).ToList();

  Type t = DynamicExpression.CreateClass(props);

El resto es solo usar System.Reflection.

  object obj = Activator.CreateInstance(t);
  t.GetProperty("EmployeeID").SetValue(obj, 34, null);
  t.GetProperty("EmployeeName").SetValue(obj, "Albert", null);
  t.GetProperty("Birthday").SetValue(obj, new DateTime(1976, 3, 14), null);
}  
Latencia
fuente
4

Para aquellos que desean crear una clase dinámica solo propiedades (es decir, POCO), y crear una lista de esta clase. Usando el código proporcionado más adelante, esto creará una clase dinámica y creará una lista de esto.

var properties = new List<DynamicTypeProperty>()
{
    new DynamicTypeProperty("doubleProperty", typeof(double)),
    new DynamicTypeProperty("stringProperty", typeof(string))
};

// create the new type
var dynamicType = DynamicType.CreateDynamicType(properties);
// create a list of the new type
var dynamicList = DynamicType.CreateDynamicList(dynamicType);

// get an action that will add to the list
var addAction = DynamicType.GetAddAction(dynamicList);

// call the action, with an object[] containing parameters in exact order added
addAction.Invoke(new object[] {1.1, "item1"});
addAction.Invoke(new object[] {2.1, "item2"});
addAction.Invoke(new object[] {3.1, "item3"});

Aquí están las clases que usa el código anterior.

Nota: También deberá hacer referencia a la biblioteca Microsoft.CodeAnalysis.CSharp.

       /// <summary>
    /// A property name, and type used to generate a property in the dynamic class.
    /// </summary>
    public class DynamicTypeProperty
    {
        public DynamicTypeProperty(string name, Type type)
        {
            Name = name;
            Type = type;
        }
        public string Name { get; set; }
        public Type Type { get; set; }
    }

   public static class DynamicType
    {
        /// <summary>
        /// Creates a list of the specified type
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static IEnumerable<object> CreateDynamicList(Type type)
        {
            var listType = typeof(List<>);
            var dynamicListType = listType.MakeGenericType(type);
            return (IEnumerable<object>) Activator.CreateInstance(dynamicListType);
        }

        /// <summary>
        /// creates an action which can be used to add items to the list
        /// </summary>
        /// <param name="listType"></param>
        /// <returns></returns>
        public static Action<object[]> GetAddAction(IEnumerable<object> list)
        {
            var listType = list.GetType();
            var addMethod = listType.GetMethod("Add");
            var itemType = listType.GenericTypeArguments[0];
            var itemProperties = itemType.GetProperties();

            var action = new Action<object[]>((values) =>
            {
                var item = Activator.CreateInstance(itemType);

                for(var i = 0; i < values.Length; i++)
                {
                    itemProperties[i].SetValue(item, values[i]);
                }

                addMethod.Invoke(list, new []{item});
            });

            return action;
        }

        /// <summary>
        /// Creates a type based on the property/type values specified in the properties
        /// </summary>
        /// <param name="properties"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public static Type CreateDynamicType(IEnumerable<DynamicTypeProperty> properties)
        {
            StringBuilder classCode = new StringBuilder();

            // Generate the class code
            classCode.AppendLine("using System;");
            classCode.AppendLine("namespace Dexih {");
            classCode.AppendLine("public class DynamicClass {");

            foreach (var property in properties)
            {
                classCode.AppendLine($"public {property.Type.Name} {property.Name} {{get; set; }}");
            }
            classCode.AppendLine("}");
            classCode.AppendLine("}");

            var syntaxTree = CSharpSyntaxTree.ParseText(classCode.ToString());

            var references = new MetadataReference[]
            {
                MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location),
                MetadataReference.CreateFromFile(typeof(DictionaryBase).GetTypeInfo().Assembly.Location)
            };

            var compilation = CSharpCompilation.Create("DynamicClass" + Guid.NewGuid() + ".dll",
                syntaxTrees: new[] {syntaxTree},
                references: references,
                options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

            using (var ms = new MemoryStream())
            {
                var result = compilation.Emit(ms);

                if (!result.Success)
                {
                    var failures = result.Diagnostics.Where(diagnostic =>
                        diagnostic.IsWarningAsError ||
                        diagnostic.Severity == DiagnosticSeverity.Error);

                    var message = new StringBuilder();

                    foreach (var diagnostic in failures)
                    {
                        message.AppendFormat("{0}: {1}", diagnostic.Id, diagnostic.GetMessage());
                    }

                    throw new Exception($"Invalid property definition: {message}.");
                }
                else
                {

                    ms.Seek(0, SeekOrigin.Begin);
                    var assembly = System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromStream(ms);
                    var dynamicType = assembly.GetType("Dexih.DynamicClass");
                    return dynamicType;
                }
            }
        }
    }
Gary Holland
fuente
Gran pieza de código. ¿Sería posible usar también AddRange en la lista dinámica, para eventualmente agregar varios registros a la vez?
RickyTad
2

Puede ver el uso de módulos dinámicos y clases que pueden hacer el trabajo. La única desventaja es que permanece cargada en el dominio de la aplicación. Pero con la versión de .NET framework utilizada, eso podría cambiar. .NET 4.0 admite ensamblajes dinámicos coleccionables y, por lo tanto, puede recrear las clases / tipos dinámicamente.

Saravanan
fuente
2

¡Guauu! ¡Gracias por esa respuesta! Le agregué algunas características para crear un convertidor de "tabla de datos a json" que comparto con usted.

    Public Shared Sub dt2json(ByVal _dt As DataTable, ByVal _sb As StringBuilder)
    Dim t As System.Type

    Dim oList(_dt.Rows.Count - 1) As Object
    Dim jss As New JavaScriptSerializer()
    Dim i As Integer = 0

    t = CompileResultType(_dt)

    For Each dr As DataRow In _dt.Rows
        Dim o As Object = Activator.CreateInstance(t)

        For Each col As DataColumn In _dt.Columns
            setvalue(o, col.ColumnName, dr.Item(col.ColumnName))
        Next

        oList(i) = o
        i += 1
    Next

    jss = New JavaScriptSerializer()
    jss.Serialize(oList, _sb)


End Sub

Y en "compileresulttype" sub, cambié eso:

    For Each column As DataColumn In _dt.Columns
        CreateProperty(tb, column.ColumnName, column.DataType)
    Next


Private Shared Sub setvalue(ByVal _obj As Object, ByVal _propName As String, ByVal _propValue As Object)
    Dim pi As PropertyInfo
    pi = _obj.GetType.GetProperty(_propName)
    If pi IsNot Nothing AndAlso pi.CanWrite Then
        If _propValue IsNot DBNull.Value Then
            pi.SetValue(_obj, _propValue, Nothing)

        Else
            Select Case pi.PropertyType.ToString
                Case "System.String"
                    pi.SetValue(_obj, String.Empty, Nothing)
                Case Else
                    'let the serialiser use javascript "null" value.
            End Select

        End If
    End If

End Sub
Foxontherock
fuente
0

Puede usar System.Runtime.Remoting.Proxies.RealProxy. Le permitirá usar código "normal" en lugar de cosas de tipo ensamblaje de bajo nivel.

Vea la respuesta de RealProxy a esta pregunta para un buen ejemplo:

¿Cómo intercepto una llamada al método en C #?

Ted Bigham
fuente
-1

Runtime Code Generation with JVM and CLR - Peter Sestoft

Trabaje para personas que estén realmente interesadas en este tipo de programación.

Mi consejo para ti es que si declaras algo, intenta evitar la cadena, por lo que si tienes clase Field, es mejor usar la clase System.Type para almacenar el tipo de campo que una cadena. Y en aras de las mejores soluciones en lugar de crear nuevas clases, intente utilizar las que se han creado FiledInfo en lugar de crear nuevas.

Damian Leszczyński - Vash
fuente
2
Link está muerto: -1
Glenn Slayden
Glenn: un rápido google reveló un enlace de trabajo: pdfs.semanticscholar.org/326a/…
Andreas Pardeike
@AndreasPardeike Gracias, lo reparé en el artículo.
Glenn Slayden