Como criar dinamicamente uma classe?

221

Eu tenho uma classe que se parece com isso:

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

E um objeto List<Field> com valores:

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

Eu quero criar uma classe que se parece com isso:

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

Há alguma maneira de fazer isso?

Eu quero que isso seja gerado em tempo de execução. Eu não quero um arquivo CS físico residindo no meu sistema de arquivos.

ashwnacharya
fonte
4
Deseja usar essa classe em tempo de execução ou apenas gerar arquivo?
Damian Leszczyński - Vash
Eu quero que isso seja gerado em tempo de execução. Eu não quero um arquivo CS físico residindo no meu sistema de arquivos. Desculpe por não mencionar isso antes.
Ashwnacharya #
16
Você pode nos dar uma idéia aproximada do que você pretende fazer com esta classe?
Justin
3
@ Justin implementa interfaces resolvidas em tempo de execução, por exemplo.
AgentFire
Alguém poderia alimentá-lo comSystem.ServiceModel.ChannelFactory<MyDynamicInterface>
Ilya Semenov

Respostas:

297

Sim, você pode usar um System.Reflection.Emitespaço para nome para isso. Não é fácil se você não tem experiência com isso, mas é certamente possível.

Edit: Este código pode ser falho, mas lhe dará a idéia geral e, com sorte, um bom começo em direção à meta.

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
fonte
2
Impressionante!! Você também pode me dizer como criar um objeto do tipo retornado pelo método CompileResultType ()?
Ashwnacharya #
4
Você pode usar o System.Activator para isso. Vou atualizar a resposta com um exemplo.
Danijels 5/10/10
4
Observe também que você precisará usar a reflexão para examinar, ler e atualizar os campos no seu tipo dinâmico. Se você deseja inteligência e nenhuma reflexão, precisa ter uma classe base estática ou interface da qual sua classe dinâmica herda e possa ser convertida. Nesse caso, você pode modificar GetTypeBuilder () e mudança moduleBuilder.DefineType chamada para incluir o tipo estático como o último parâmetro (é nulo agora)
danijels
2
alguém pode explicar como usar o objeto após a sua criação
HELP_ME
3
@bugz use o código acima para criar a classe, e na classe base você poderá adicionar este método: public void SetValue <T> (nome da string, valor T) {GetType (). GetProperty (name) .SetValue (this, value ); }
stricq
71

Vai levar algum trabalho, mas certamente não é impossível.

O que eu fiz é:

  • Crie uma fonte C # em uma string (não é necessário gravar em um arquivo),
  • Execute-o através do Microsoft.CSharp.CSharpCodeProvider(CompileAssemblyFromSource)
  • Encontre o tipo gerado
  • E crie uma instância desse Type ( Activator.CreateInstance)

Dessa forma, você pode lidar com o código C # que você já conhece, em vez de precisar emitir o MSIL.

Mas isso funciona melhor se a sua classe implementa alguma interface (ou é derivada de alguma classe básica); caso contrário, como o código de chamada (read: compiler) deve saber sobre essa classe que será gerada no tempo de execução?

Hans Keing
fonte
7
Pode querer ver essa discussão: reflexão-emitem-vs-CodeDOM
Nawfal
1
@nawfal, ashwnacharya, queria gerar dinamicamente em tempo de execução sua classe com seus membros originalmente contidos em uma lista. Não acho que colocá-lo em um arquivo gerado mesmo em tempo de execução seria uma boa solução de desempenho.
sodjsn26fr 27/04
38

Você também pode criar dinamicamente uma 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;
    }
}

Eu armazeno todos os campos de classe em um dicionário, _fieldsjuntamente com seus tipos e valores. Os dois métodos são para obter ou definir valor para algumas das propriedades. Você deve usar a dynamicpalavra-chave para criar uma instância desta classe.

O uso com seu exemplo:

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: E aqui está como fica minha classe Field:

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

    public string FieldName;

    public Type FieldType;
}
Termininja
fonte
1
Eu gostei dessa abordagem até precisar inicializar os campos com o construtor. ou seja, dynamic obj = new DynamicClass(fields){EmployeeId=123456;EmployeeName = "John"; Designation = "Tech Lead";}seria ótimo fazer isso.
Rey_coder
14

Eu sei que reabrir esta tarefa antiga, mas com o c # 4.0 essa tarefa é absolutamente indolor.

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

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

para mais, consulte https://www.oreilly.com/learning/building-c-objects-dynamically

user1235183
fonte
Sim, mas você perde a segurança de tipo aqui. Podemos fazer algo semelhante, preservando a segurança do tipo?
toughQuestions
13

Não sei o uso pretendido dessas classes dinâmicas, e a geração de código e a compilação em tempo de execução podem ser feitas, mas é preciso algum esforço. Talvez tipos anônimos ajudassem você, algo como:

var v = new { EmployeeID = 108, EmployeeName = "John Doe" };
Amittai Shapira
fonte
7
Você não pode codificar os nomes dos campos. Ele está fornecendo a eles o dele Field.FieldName. Ter que codificar os nomes dos campos anula o objetivo. Se você precisar fazer isso, crie a classe.
toddmo
9

Você quer ver o CodeDOM . Permite definir elementos de código e compilá-los. Citando o MSDN:

... Este gráfico de objeto pode ser renderizado como código fonte usando um gerador de código CodeDOM para uma linguagem de programação suportada. O CodeDOM também pode ser usado para compilar o código-fonte em um assembly binário.

Hemant
fonte
Eu quero que isso seja gerado em tempo de execução. Eu não quero um arquivo CS físico residindo no meu sistema de arquivos. Desculpe por não mencionar isso antes.
Ashwnacharya 5/10/10
1
@ashwnacharya: Você pode usar o CodeDOM para gerar o arquivo de origem e compilá-lo em tempo de execução!
Hemant
1
Cuidado, porém, que o compilador do CodeDOM usa uma sequência bruta e, portanto, convém considerar "ataques de inserção de código" semelhantes aos usados ​​na injeção de XSS e SQL.
Cwap #
6

Com base na resposta de @ danijels, crie dinamicamente uma classe no 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
fonte
6

Você também pode criar dinamicamente uma classe usando DynamicExpressions .

Como os 'Dictionary' têm inicializadores compactos e lidam com colisões importantes, você deve fazer algo assim.

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

Ou você pode usar um conversor JSON para construir seu objeto de seqüência de caracteres serializado em algo gerenciável.

Em seguida, usando System.Linq.Dynamic;

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

  Type t = DynamicExpression.CreateClass(props);

O resto está apenas 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);
}  
Latência
fonte
4

Para aqueles que desejam criar uma classe dinâmica, apenas propriedades (por exemplo, POCO) e crie uma lista dessa classe. Usando o código fornecido posteriormente, isso criará uma classe dinâmica e criará uma lista disso.

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

Aqui estão as classes que o código anterior usa.

Nota: Você também precisará fazer referência à 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
fonte
Grande pedaço de código. Seria possível usar também AddRange na lista dinâmica, para eventualmente adicionar vários registros por vez?
RickyTad 03/04
2

Você pode usar módulos e classes dinâmicos que podem fazer o trabalho. A única desvantagem é que ele permanece carregado no domínio do aplicativo. Mas com a versão do .NET framework sendo usada, isso pode mudar. O .NET 4.0 suporta assemblies dinâmicos colecionáveis ​​e, portanto, você pode recriar as classes / tipos dinamicamente.

Saravanan
fonte
2

Uau! Obrigado por essa resposta! Adicionei alguns recursos a ele para criar um conversor "datatable para json" que eu compartilho com você.

    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 no sub "compileresulttype", mudei isso:

    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
fonte
0

Você pode usar System.Runtime.Remoting.Proxies.RealProxy. Isso permitirá que você use código "normal" em vez de material de tipo de montagem de baixo nível.

Veja a resposta do RealProxy a esta pergunta para um bom exemplo:

Como interceptar uma chamada de método em c #?

Ted Bigham
fonte
-1

Runtime Code Generation with JVM and CLR - Peter Sestoft

Trabalhe para pessoas realmente interessadas neste tipo de programação.

Minha dica para você é que, se você declarar algo, tente evitar a string, portanto, se você tiver a classe Field, é melhor usar a classe System.Type para armazenar o tipo de campo do que uma string. E, para obter as melhores soluções, em vez de criar novas classes, tente usar as que foram criadas como FiledInfo, em vez de criar novas.

Damian Leszczyński - Vash
fonte
2
O link está morto: -1
Glenn Slayden
Glenn: a googling rápida revelou uma ligação de trabalho: pdfs.semanticscholar.org/326a/...
Andreas Pardeike
@AndreasPardeike Obrigado, eu o consertei no artigo.
Glenn Slayden