Jak dynamicznie stworzyć klasę?


221

Mam klasę, która wygląda tak:

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

I obiekt List<Field>z wartościami:

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

Chcę stworzyć klasę, która wygląda następująco:

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

Czy jest na to sposób?

Chcę, aby zostało to wygenerowane w czasie wykonywania. Nie chcę fizycznego pliku CS rezydującego w moim systemie plików.


4
Czy chcesz używać tej klasy w środowisku wykonawczym, czy tylko generować plik?
Damian Leszczyński - Vash

Chcę, aby zostało to wygenerowane w czasie wykonywania. Nie chcę fizycznego pliku CS rezydującego w moim systemie plików. Przepraszam, że nie wspominałem o tym wcześniej.
ashwnacharya

16
Czy możesz dać nam przybliżone wyobrażenie o tym, co zamierzasz zrobić z tą klasą?
Justin

3
@Justin implementuje na przykład interfejsy rozwiązane w czasie wykonywania.
AgentFire,

Można to nakarmićSystem.ServiceModel.ChannelFactory<MyDynamicInterface>
Ilya Semenov

Odpowiedzi:


297

Tak, możesz System.Reflection.Emitdo tego użyć przestrzeni nazw. Nie jest to proste, jeśli nie masz z tym doświadczenia, ale z pewnością jest to możliwe.

Edycja: ten kod może być wadliwy, ale da ci ogólny pomysł i, mam nadzieję, dobry start w kierunku celu.

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
Niesamowite!! Czy możesz mi również powiedzieć, jak utworzyć obiekt typu zwrócony przez metodę CompileResultType ()?
ashwnacharya

4
Możesz do tego użyć System.Activator. Zaktualizuję odpowiedź przykładem.
danijels

4
Pamiętaj również, że będziesz musiał użyć refleksji do badania, czytania i aktualizowania pól w swoim typie dynamicznym. Jeśli chcesz mieć inteligencję i brak refleksji, musisz mieć statyczną klasę bazową lub interfejs, z którego dziedziczy klasa dynamiczna i do której można ją rzutować. W takim przypadku możesz zmodyfikować metodę GetTypeBuilder () i zmienić wywołanie moduleBuilder.DefineType, aby uwzględnić typ statyczny jako ostatni parametr (jest teraz pusty)
danijels,

2
czy ktoś może wyjaśnić, jak korzystać z obiektu po jego utworzeniu
HELP_ME

3
@bugz użyj powyższego kodu, aby utworzyć klasę, a następnie w klasie podstawowej możesz dodać tę metodę: public void SetValue <T> (nazwa ciągu, wartość T) {GetType (). GetProperty (nazwa) .SetValue (ta wartość ); }
stricq,

71

To zajmie trochę pracy, ale z pewnością nie jest niemożliwe.

To co zrobiłem to:

  • Utwórz źródło C # w ciągu (nie musisz zapisywać do pliku),
  • Uruchom go przez Microsoft.CSharp.CSharpCodeProvider(CompileAssemblyFromSource)
  • Znajdź wygenerowany typ
  • I utwórz instancję tego typu ( Activator.CreateInstance)

W ten sposób możesz poradzić sobie z kodem C #, który już znasz, zamiast emitować MSIL.

Ale działa to najlepiej, jeśli twoja klasa implementuje jakiś interfejs (lub wywodzi się z klasy podstawowej), w przeciwnym razie skąd kod wywołujący (czytaj: kompilator) wie o tej klasie, która zostanie wygenerowana w czasie wykonywania?


7
Może chcę zobaczyć tę dyskusję: odbicie-emisja-kontra-wolność
nawfal

1
@nawfal, ashwnacharya, chciał dynamicznie generować w czasie wykonywania swoją klasę z członkami pierwotnie zawartymi na liście. Nie sądzę, aby umieszczenie go w pliku nawet wygenerowanym w czasie wykonywania byłoby dobrym rozwiązaniem wydajnościowym.
sodjsn26fr

38

Możesz także dynamicznie utworzyć klasę za pomocą 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;
    }
}

Wszystkie pola klas przechowuję w słowniku _fieldswraz z ich typami i wartościami. Obie metody mają na celu uzyskanie lub ustawienie wartości niektórych właściwości. Musisz użyć dynamicsłowa kluczowego, aby utworzyć instancję tej klasy.

Zastosowanie w twoim przykładzie:

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

Edycja: A oto jak wygląda moja klasa Field:

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

    public string FieldName;

    public Type FieldType;
}

1
Podobało mi się to podejście, dopóki nie musiałem inicjować pól za pomocą konstruktora. tj. dynamic obj = new DynamicClass(fields){EmployeeId=123456;EmployeeName = "John"; Designation = "Tech Lead";}byłoby naprawdę wspaniale to robić.
rey_coder

14

Wiem, że ponownie otwieram to stare zadanie, ale w wersji c # 4.0 to zadanie jest absolutnie bezbolesne.

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

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

więcej informacji na https://www.oreilly.com/learning/building-c-objects-dynamically


Tak, ale tutaj tracisz bezpieczeństwo typu. Czy możemy zrobić coś podobnego, zachowując bezpieczeństwo typu?
toughQuestions

13

Nie wiem, jakie jest zamierzone użycie takich klas dynamicznych, ale można generować kod i kompilować w czasie wykonywania, ale wymaga to trochę wysiłku. Może pomogą Ci typy anonimowe , na przykład:

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

7
Nie można na stałe kodować nazw pól. Dostarcza im swoje Field.FieldName. Konieczność twardego kodowania nazw pól nie pozwala na osiągnięcie celu. Jeśli musisz to zrobić, równie dobrze możesz stworzyć klasę.
toddmo

9

Chcesz spojrzeć na CodeDOM . Pozwala definiować elementy kodu i kompilować je. Cytując MSDN:

... Ten wykres obiektowy można renderować jako kod źródłowy za pomocą generatora kodów CodeDOM dla obsługiwanego języka programowania. CodeDOM można również wykorzystać do kompilacji kodu źródłowego w zestawie binarnym.


Chcę, aby zostało to wygenerowane w czasie wykonywania. Nie chcę fizycznego pliku CS rezydującego w moim systemie plików. Przepraszam, że nie wspominałem o tym wcześniej.
ashwnacharya

1
@ashwnacharya: Użytkownik może skorzystać CodeDOM zarówno generowania pliku źródłowego i kompilowanie go przy starcie!
Hemant,

1
Uważaj jednak, że kompilator CodeDOM pobiera nieprzetworzony ciąg znaków, dlatego możesz rozważyć „ataki polegające na wstawieniu kodu” podobne do tych, które są używane we wstrzykiwaniu XSS i SQL.
cwap

6

Na podstawie odpowiedzi @ danijels dynamicznie utwórz klasę w 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

Możesz także dynamicznie utworzyć klasę za pomocą DynamicExpressions .

Ponieważ „słowniki mają kompaktowe inicjalizatory i obsługują kolizje klawiszy, będziesz chciał zrobić coś takiego.

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

Lub możesz użyć konwertera JSON do skonstruowania zserializowanego obiektu łańcuchowego w coś zarządzalnego.

Następnie za pomocą System.Linq.Dynamic;

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

  Type t = DynamicExpression.CreateClass(props);

Reszta korzysta tylko z 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

Dla tych, którzy chcą stworzyć klasę dynamiczną, po prostu właściwości (tj. POCO) i utwórz listę tej klasy. Używając kodu podanego później, utworzy to klasę dynamiczną i utworzy jej listę.

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

Oto klasy, których używa poprzedni kod.

Uwaga: musisz także odwołać się do biblioteki 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;
                }
            }
        }
    }

Świetny kawałek kodu. Czy można by również użyć AddRange na liście dynamicznej, aby ostatecznie dodać kilka rekordów jednocześnie?
RickyTad

2

Możesz spojrzeć na użycie dynamicznych modułów i klas, które mogą wykonać zadanie. Jedyną wadą jest to, że pozostaje załadowany w domenie aplikacji. Ale przy użyciu wersji .NET Framework może się to zmienić. .NET 4.0 obsługuje kolekcjonujące zestawy dynamiczne, dzięki czemu można dynamicznie odtwarzać klasy / typy.


2

Łał! Dziękuję za tę odpowiedź! Dodałem kilka funkcji, aby utworzyć konwerter „datatable to json”, który udostępniam tobie.

    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

I w sub „compileresulttype” zmieniłem to:

    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

Pracuj dla osób, które naprawdę są zainteresowane tego rodzaju programowaniem.

Moja rada dla Ciebie jest taka, że ​​jeśli coś deklarujesz, staraj się unikać łańcucha, więc jeśli masz klasę Field, lepiej jest użyć klasy System.Type do przechowywania typu pola niż łańcucha. A ze względu na najlepsze rozwiązania zamiast tworzenia nowych klas, spróbuj użyć tych, które zostały utworzone FiledInfo zamiast tworzenia nowych.


2
Link nie żyje: -1
Glenn Slayden

Glenn: szybki googling ujawnił działający link: pdfs.semanticscholar.org/326a/…
Andreas Pardeike

@AndreasPardeike Dzięki, naprawiłem to w artykule.
Glenn Slayden
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.