Come creare dinamicamente una classe?


222

Ho una lezione che assomiglia a questa:

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

E un oggetto List<Field>con valori:

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

Voglio creare una classe che assomigli a questa:

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

C'è un modo per fare questo?

Voglio che questo sia generato in fase di esecuzione. Non voglio che un file CS fisico risieda nel mio filesystem.


4
Vuoi usare quella classe in runtime o generare solo file?
Damian Leszczyński - Vash,

Voglio che questo sia generato in fase di esecuzione. Non voglio un file CS fisico residente nel mio filesystem. Ci scusiamo per non averlo menzionato prima.
ashwnacharya,

16
Puoi darci un'idea approssimativa di cosa intendi fare con questa lezione?
Giustino,

3
@Justin implementa interfacce risolte dal runtime, ad esempio.
AgentFire,

Si potrebbe dargli da mangiareSystem.ServiceModel.ChannelFactory<MyDynamicInterface>
Ilya Semenov il

Risposte:


298

Sì, puoi usare lo System.Reflection.Emitspazio dei nomi per questo. Non è semplice se non si ha esperienza con esso, ma è certamente possibile.

Modifica: questo codice potrebbe essere difettoso, ma ti darà l'idea generale e si spera che abbia un buon inizio verso l'obiettivo.

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);
        }
    }
}

2
Eccezionale!! Puoi anche dirmi come creare un oggetto del tipo restituito dal metodo CompileResultType ()?
ashwnacharya,

4
È possibile utilizzare System.Activator per questo. Aggiornerò la risposta con un esempio.
Daniels

4
Nota anche che dovrai usare la riflessione per esaminare, leggere e aggiornare i campi nel tuo tipo dinamico. Se vuoi intellisense e nessuna riflessione, devi avere una classe di base statica o un'interfaccia da cui la tua classe dinamica eredita e su cui puoi trasmettere. In tal caso è possibile modificare il metodo GetTypeBuilder () e modificare la chiamata moduleBuilder.DefineType per includere il tipo statico come ultimo parametro (è null ora)
danijels

2
qualcuno può spiegare come utilizzare l'oggetto dopo la sua creazione
HELP_ME

3
@bugz usa il codice sopra per creare la classe, quindi nella classe base puoi aggiungere questo metodo: void pubblico SetValue <T> (nome stringa, valore T) {GetType (). GetProperty (nome) .SetValue (questo, valore ); }
stricq,

71

Ci vorrà del lavoro, ma non è certamente impossibile.

Quello che ho fatto è:

  • Crea una sorgente C # in una stringa (non è necessario scrivere su un file),
  • Microsoft.CSharp.CSharpCodeProviderEseguilo attraverso (CompileAssemblyFromSource)
  • Trova il tipo generato
  • E crea un'istanza di quel tipo ( Activator.CreateInstance)

In questo modo puoi gestire il codice C # che già conosci, invece di dover emettere MSIL.

Ma questo funziona meglio se la tua classe implementa qualche interfaccia (o deriva da una baseclass), altrimenti come può il codice chiamante (leggi: compilatore) sapere su quella classe che verrà generata in fase di esecuzione?


7
Potrebbe voler vedere questa discussione: reflection-emit-vs-codedom
nawfal

1
@nawfal, ashwnacharya, voleva generare dinamicamente in fase di esecuzione la sua classe con i suoi membri originariamente contenuti in un elenco. Non penso che metterlo in un file generato anche in fase di esecuzione sarebbe una buona soluzione per le prestazioni.
sodjsn26fr,

39

Puoi anche creare dinamicamente una classe usando 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;
    }
}

Conservo tutti i campi di classe in un dizionario _fieldsinsieme ai loro tipi e valori. Entrambi i metodi sono in grado di ottenere o impostare il valore su alcune proprietà. È necessario utilizzare la dynamicparola chiave per creare un'istanza di questa classe.

L'uso con il tuo esempio:

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

Modifica: ed ecco come appare la mia classe Field:

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

    public string FieldName;

    public Type FieldType;
}

1
Mi è piaciuto questo approccio fino a quando non ho avuto bisogno di inizializzare i campi con il costruttore. cioè dynamic obj = new DynamicClass(fields){EmployeeId=123456;EmployeeName = "John"; Designation = "Tech Lead";}sarebbe davvero bello farlo.
rey_coder

14

So di riaprire questo vecchio compito ma con c # 4.0 questo compito è assolutamente indolore.

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

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

per ulteriori informazioni, consultare https://www.oreilly.com/learning/building-c-objects-dynamically


Sì, ma perdi la sicurezza del tipo qui. Possiamo fare qualcosa di simile preservando la sicurezza dei tipi?
toughQuestions il

13

Non conosco l'uso previsto di tali classi dinamiche, e la generazione del codice e la compilazione del tempo di esecuzione possono essere fatte, ma richiede un certo sforzo. Forse i tipi anonimi potrebbero aiutarti, qualcosa del tipo:

var v = new { EmployeeID = 108, EmployeeName = "John Doe" };

7
Non è possibile codificare i nomi dei campi. Li sta fornendo con i suoi Field.FieldName. Dover codificare i nomi dei campi per sconfiggere lo scopo. Se devi farlo, potresti anche creare la classe.
toddmo

9

Vuoi dare un'occhiata a CodeDOM . Permette di definire elementi di codice e compilarli. Citando MSDN:

... Questo grafico a oggetti può essere reso come codice sorgente usando un generatore di codice CodeDOM per un linguaggio di programmazione supportato. CodeDOM può anche essere utilizzato per compilare il codice sorgente in un assembly binario.


Voglio che questo sia generato in fase di esecuzione. Non voglio un file CS fisico residente nel mio filesystem. Ci scusiamo per non averlo menzionato prima.
ashwnacharya,

1
@ashwnacharya: puoi usare CodeDOM sia per generare il file sorgente che per compilarlo in fase di esecuzione!
Hemant,

1
Attenzione, tuttavia, che il compilatore CodeDOM prende una stringa non elaborata e quindi si potrebbe considerare "attacchi di inserimento del codice" simili a quelli utilizzati nell'iniezione XSS e SQL.
cwap

6

Basato sulla risposta di @ danijels, crea dinamicamente una classe in 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

6

Puoi anche creare dinamicamente una classe utilizzando DynamicExpressions .

Poiché "Dizionario ha inizializzatori compatti e gestisce le collisioni chiave, ti consigliamo di fare qualcosa del genere.

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

Oppure potresti voler usare un convertitore JSON per costruire l'oggetto stringa serializzato in qualcosa di gestibile.

Quindi utilizzando System.Linq.Dynamic;

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

  Type t = DynamicExpression.CreateClass(props);

Il resto sta solo usando 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);
}  

4

Per coloro che vogliono creare una classe dinamica solo proprietà (ad esempio POCO) e creare un elenco di questa classe. Utilizzando il codice fornito in seguito, ciò creerà una classe dinamica e creerà un elenco di questo.

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"});

Ecco le classi utilizzate dal codice precedente.

Nota: è inoltre necessario fare riferimento alla libreria 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;
                }
            }
        }
    }

Ottimo pezzo di codice. Sarebbe possibile utilizzare anche AddRange nell'elenco dinamico, per aggiungere eventualmente più record alla volta?
RickyTad,

2

Puoi guardare usando moduli e classi dinamici che possono fare il lavoro. L'unico svantaggio è che rimane caricato nel dominio dell'app. Ma con la versione di .NET framework utilizzata, ciò potrebbe cambiare. .NET 4.0 supporta assembly dinamici da collezione e quindi è possibile ricreare dinamicamente le classi / i tipi.


2

Wow! Grazie per quella risposta! Ho aggiunto alcune funzionalità per creare un convertitore "datatable to json" che condivido con te.

    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

E nel sub "compileresulttype", ho cambiato che:

    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


-1

Runtime Code Generation with JVM and CLR - Peter Sestoft

Lavora per le persone che sono veramente interessate a questo tipo di programmazione.

Il mio consiglio per te è che se dichiari qualcosa cerca di evitare la stringa, quindi se hai la classe Field è meglio usare la classe System.Type per memorizzare il tipo di campo piuttosto che una stringa. E per il bene delle migliori soluzioni invece della creazione, le nuove classi cercano di usare quelle create da FiledInfo invece della creazione di nuove.


2
Il link è morto: -1
Glenn Slayden,

Glenn: una rapida ricerca su Google ha rivelato un collegamento funzionante: pdfs.semanticscholar.org/326a/…
Andreas Pardeike,

@AndreasPardeike Grazie, l'ho riparato nell'articolo.
Glenn Slayden,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.