Comment créer dynamiquement une classe?


222

J'ai une classe qui ressemble à ceci:

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

Et un objet List<Field>avec des valeurs:

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

Je veux créer une classe qui ressemble à ceci:

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

Y a-t-il un moyen de faire ça?

Je veux que cela soit généré au moment de l'exécution. Je ne veux pas qu'un fichier CS physique réside dans mon système de fichiers.


4
Voulez-vous utiliser cette classe au moment de l'exécution ou générer uniquement un fichier?
Damian Leszczyński - Vash

Je veux que cela soit généré lors de l'exécution. Je ne veux pas qu'un fichier CS physique réside dans mon système de fichiers. Désolé de ne pas l'avoir mentionné plus tôt.
ashwnacharya

16
Pouvez-vous nous donner une idée approximative de ce que vous comptez faire avec ce cours?
Justin

3
@Justin implémente des interfaces résolues à l'exécution, par exemple.
AgentFire

On pourrait le nourrirSystem.ServiceModel.ChannelFactory<MyDynamicInterface>
Ilya Semenov

Réponses:


298

Oui, vous pouvez utiliser System.Reflection.Emit espace de noms pour cela. Ce n'est pas simple si vous n'en avez aucune expérience, mais c'est certainement possible.

Edit: Ce code peut être imparfait, mais il vous donnera une idée générale et, espérons-le, un bon départ vers l'objectif.

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
Impressionnant!! Pouvez-vous également me dire comment créer un objet du type retourné par la méthode CompileResultType ()?
ashwnacharya

4
Vous pouvez utiliser System.Activator pour cela. Je mettrai à jour la réponse avec un exemple.
danijels

4
Notez également que vous devrez utiliser la réflexion pour examiner, lire et mettre à jour les champs de votre type dynamique. Si vous voulez intellisense et aucune réflexion, vous devez avoir une classe de base statique ou une interface dont votre classe dynamique hérite et peut être castée. Dans ce cas, vous pouvez modifier la méthode GetTypeBuilder () et changer l'appel moduleBuilder.DefineType pour inclure le type statique comme dernier paramètre (est maintenant nul)
danijels

2
quelqu'un peut-il expliquer comment utiliser l'objet après sa création
HELP_ME

3
@bugz utilise le code ci-dessus pour créer la classe, puis dans la classe de base, vous pouvez ajouter cette méthode: public void SetValue <T> (nom de chaîne, valeur T) {GetType (). GetProperty (nom) .SetValue (this, valeur ); }
stricq

71

Il faudra du travail, mais ce n'est certainement pas impossible.

Ce que j'ai fait c'est:

  • Créer une source C # dans une chaîne (pas besoin d'écrire dans un fichier),
  • Exécutez-le via Microsoft.CSharp.CSharpCodeProvider(CompileAssemblyFromSource)
  • Trouver le type généré
  • Et créez une instance de ce Type ( Activator.CreateInstance)

De cette façon, vous pouvez gérer le code C # que vous connaissez déjà, au lieu d'avoir à émettre MSIL.

Mais cela fonctionne mieux si votre classe implémente une interface (ou est dérivée d'une classe de base), sinon comment le code appelant (lire: compilateur) doit-il connaître cette classe qui sera générée lors de l'exécution?


7
Pourrait vouloir voir cette discussion: réflexion-émettre-vs-codedom
nawfal

1
@nawfal, ashwnacharya, voulait générer dynamiquement au moment de l'exécution sa classe avec ses membres initialement contenus dans une liste. Je ne pense pas que le mettre dans un fichier même généré lors de l'exécution soit une bonne solution de performance.
sodjsn26fr le

39

Vous pouvez également créer dynamiquement une classe à l'aide de 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;
    }
}

Je stocke tous les champs de classe dans un dictionnaire _fieldsavec leurs types et valeurs. Les deux méthodes permettent d'obtenir ou de définir la valeur de certaines propriétés. Vous devez utiliser le dynamicmot clé pour créer une instance de cette classe.

L'utilisation avec votre exemple:

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

Edit: Et voici à quoi ressemble ma classe Field:

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

    public string FieldName;

    public Type FieldType;
}

1
J'ai aimé cette approche jusqu'à ce que j'aie besoin d'initialiser les champs avec le constructeur. c'est-à-dire que dynamic obj = new DynamicClass(fields){EmployeeId=123456;EmployeeName = "John"; Designation = "Tech Lead";}ce serait vraiment bien de faire ça.
rey_coder

14

Je ne connais pas l'utilisation prévue de ces classes dynamiques, et la génération de code et la compilation au moment de l'exécution peuvent être effectuées, mais cela demande un certain effort. Peut-être que les types anonymes vous aideraient, quelque chose comme:

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

7
Vous ne pouvez pas coder en dur les noms de champ. Il leur fournit le sien Field.FieldName. Le fait de devoir coder en dur les noms de champ va à l'encontre de l'objectif. Si vous devez le faire, vous pourriez aussi bien créer la classe.
toddmo

14

Je sais que je rouvre cette ancienne tâche mais avec c # 4.0 cette tâche est absolument indolore.

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

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

pour plus d'informations, voir https://www.oreilly.com/learning/building-c-objects-dynamically


Oui, mais vous perdez la sécurité du type ici. Pouvons-nous faire quelque chose de similaire tout en préservant la sécurité des types?
hardQuestions

9

Vous voulez regarder CodeDOM . Il permet de définir des éléments de code et de les compiler. Citant MSDN:

... Ce graphique d'objet peut être rendu sous forme de code source à l'aide d'un générateur de code CodeDOM pour un langage de programmation pris en charge. Le CodeDOM peut également être utilisé pour compiler le code source dans un assembly binaire.


Je veux que cela soit généré lors de l'exécution. Je ne veux pas qu'un fichier CS physique réside dans mon système de fichiers. Désolé de ne pas l'avoir mentionné plus tôt.
ashwnacharya

1
@ashwnacharya: Vous pouvez utiliser CodeDOM à la fois pour générer le fichier source et le compiler lors de l'exécution!
Hemant

1
Attention cependant, le compilateur CodeDOM prend une chaîne brute, et donc vous pouvez envisager des «attaques par insertion de code» similaires à celles utilisées dans l'injection XSS et SQL.
cwap

6

Sur la base de la réponse de @ danijels, créez dynamiquement une classe dans 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

Vous pouvez également créer dynamiquement une classe en utilisant DynamicExpressions .

Étant donné que les dictionnaires ont des initialiseurs compacts et gèrent les collisions de touches, vous voudrez faire quelque chose comme ça.

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

Ou vous voudrez peut-être utiliser un convertisseur JSON pour construire votre objet chaîne sérialisé en quelque chose de gérable.

Puis en utilisant System.Linq.Dynamic;

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

  Type t = DynamicExpression.CreateClass(props);

Le reste utilise simplement 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

Pour ceux qui veulent créer une classe dynamique, juste des propriétés (c'est-à-dire POCO), et créer une liste de cette classe. En utilisant le code fourni plus tard, cela créera une classe dynamique et en créera une liste.

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

Voici les classes utilisées par le code précédent.

Remarque: Vous devrez également référencer la bibliothèque 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;
                }
            }
        }
    }

Grand morceau de code. Serait-il possible d'utiliser également AddRange sur la liste dynamique, pour éventuellement ajouter plusieurs enregistrements à la fois?
RickyTad

2

Vous pouvez envisager d'utiliser des modules et des classes dynamiques qui peuvent faire le travail. Le seul inconvénient est qu'il reste chargé dans le domaine d'application. Mais avec la version du framework .NET utilisée, cela pourrait changer. .NET 4.0 prend en charge les assemblys dynamiques de collection et vous pouvez donc recréer les classes / types de manière dynamique.


2

Hou la la! Merci pour cette réponse! Je lui ai ajouté quelques fonctionnalités pour créer un convertisseur "datatable to json" que je partage avec vous.

    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

Et dans le sous "compileresulttype", j'ai changé cela:

    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

Travaillez pour des personnes vraiment intéressées par ce type de programmation.

Mon conseil pour vous est que si vous déclarez quelque chose, essayez d'éviter la chaîne, donc si vous avez un champ Field, il est préférable d'utiliser la classe System.Type pour stocker le type de champ plutôt qu'une chaîne. Et dans l'intérêt des meilleures solutions au lieu de créer de nouvelles classes, essayez d'utiliser celles qui ont été créées FiledInfo au lieu de créer de nouvelles.


2
Le lien est mort: -1
Glenn Slayden

Glenn: une recherche rapide sur Google a révélé un lien de travail: pdfs.semanticscholar.org/326a/…
Andreas Pardeike

@AndreasPardeike Merci, je l'ai réparé dans l'article.
Glenn Slayden
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.