【问题标题】:Add properties dynamically at run time in existing class c#在现有类 c# 中在运行时动态添加属性
【发布时间】:2017-05-19 10:09:20
【问题描述】:

我有一个 UI,我们可以在其中向表中添加以下值 Fields

  • 产品名称
  • 产品编号
  • 产品代码

我有一个现有的类 Product 有一些现有的属性

public class Product
{
    public string ProductID { get; set; }
    //product in a product search listing
    public string StoreName { get; set; }
    public string SearchToken { get; set; }

}

我正在寻找一种方法,当用户在表 Fields

中添加值时,该方法将在运行时(动态)在现有类 Product 中添加属性

【问题讨论】:

  • 你不能在运行时修改类定义,除非你遇到一些非常复杂的东西并编写一些部分类,在运行时编译成dll并将它们加载到内存中。听起来你只需要一个 Dictionary 这样你就可以存储值并给它们命名。
  • 其实忽略我之前关于在运行时编译部分类的说法,那也是不可能的。
  • 好的,谢谢 CathalMF
  • 我尝试过 System.Reflection.Emit 但没有成功。
  • @amethianil 你不能那样做。您是如何设想使用这些附加属性的?字典的想法是最好的。

标签: c# .net windows reflection reflection.emit


【解决方案1】:

我不知道在运行时定义属性的方法,但另一种实现所需的方法是在 C# 中使用称为 ExpandoObject 的动态对象。

您首先需要声明您的动态对象,它在内部使用一种字典,因此您可以向其中添加属性。

using System.Dynamic;
dynamic newobj = new ExpandoObject();

//I can add properties during compile time such as this
newobj.Test = "Yes";
newobj.JValue = 123;

//Or during runtime such as this (populated from two text boxes)
AddProperty(newobj, tbName.Text, tbValue.Text);

public void AddProperty(ExpandoObject expando, string propertyName, object propertyValue)
{
    var exDict = expando as IDictionary<string, object>;
    if (exDict.ContainsKey(propertyName))
        exDict[propertyName] = propertyValue;
    else
    exDict.Add(propertyName, propertyValue);
}

我在这里的解决方案中使用过一次:Flattern child/parent data with unknown number of columns

但是这些来源可能可以更好地解释它; https://www.oreilly.com/learning/building-c-objects-dynamically

https://weblog.west-wind.com/posts/2012/feb/08/creating-a-dynamic-extensible-c-expando-object

https://docs.microsoft.com/en-us/dotnet/articles/csharp/programming-guide/types/using-type-dynamic

但是,我不确定与使用简单的 Dictionary&lt;string, object&gt; 相比,这是否真的会给您带来任何真正的优势

【讨论】:

  • 好的 jason.kaisersmith 感谢您的回复。我看起来不像 ExpandoObject,因为我们只能在运行时使用这个 ExpandoObject 对象。但我希望新添加的属性将作为 properties 永久存在于我的类中。将来我们将使用它作为类属性。如果一个用户在表中添加了一个新字段,那么它也会自动添加为我的 Product 类中的属性
【解决方案2】:

正如其他人所指出的,听起来您的场景并不需要这种行为。但是,您所要求的 是 可能的。使用 System.Reflection.Emit,您可以在运行时动态扩展基类,甚至在运行时将其保存为已编译的 .dll。更棘手的部分是弄清楚如何从生产代码中将新类部署到生产环境,并在需要时覆盖以前的类。我会把这部分留给你自己弄清楚。根据您的其余问题,在为需要实现者扩展基类的框架创建自动化测试时,我也有类似的需求,因此我构建了一个迷你框架来执行此操作。你可以这样使用它:

    public void TestTypeCreator()
    {
        //create and save new type
        object _newProduct = DynamicTypeCreator
            .Create("NewProduct", typeof(Product), @"C:\PROD")
            .AddPassThroughCtors()
            .AddProperty("ProductName", typeof(string))
            .FinishBuildingAndSaveType("NewProduct.dll")
            .GetConstructor(new Type[0] { })
            .Invoke(new object[0] { });

        //set ProductName value
        _newProduct.GetType().GetProperty("ProductName").SetValue(_newProduct, "Cool Item");

        //get ProductName value
        string _prodName = _newProduct.GetType().GetProperty("ProductName").GetValue(_newProduct).ToString();

        //get StoreName value
        string _storeName = _newProduct.GetType().GetProperty("StoreName").GetValue(_newProduct).ToString();
    }

由于我还没有在任何地方发布此代码,我将在下面粘贴整个内容。这个“框架”非常有限,您可能必须为您的特定目的添加/修改它。来吧:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.IO;

namespace ConsoleCommon
{
public interface IBaseObject
{
    IEmptyObject AddPassThroughCtors();
}
public interface IEmptyObject
{
    IAfterProperty AddProperty(string name, Type type);
}
public interface IAfterProperty : IEmptyObject, IFinishBuild
{
    IAfterAttribute AddPropertyAttribute(Type attrType, Type[] ctorArgTypes, params object[] ctorArgs);
}
public interface IAfterAttribute : IEmptyObject, IFinishBuild
{

}
public interface IFinishBuild
{
    Type FinishBuildingType();
    Type FinishBuildingAndSaveType(string assemblyFileName);
}
public static class DynamicTypeCreator
{
    public static IBaseObject Create(string className, Type parentType)
    {
        return new DynamicTypeCreatorBase().Create(className, parentType);
    }
    public static IBaseObject Create(string className, Type parentType, string dir)
    {
        return new DynamicTypeCreatorBase().Create(className, parentType, dir);
    }
}
public class PropertyBuilding
{
    public PropertyBuilding(PropertyBuilder propertyBuild, MethodBuilder getBuild, MethodBuilder setBuild)
    {
        propertyBuilder = propertyBuild;
        getBuilder = getBuild;
        setBuilder = setBuild;
    }
    public PropertyBuilder propertyBuilder { get; }
    public MethodBuilder getBuilder { get; }
    public MethodBuilder setBuilder { get; }
}
public class DynamicTypeCreatorBase : IBaseObject, IEmptyObject, IAfterProperty, IAfterAttribute
{
    TypeBuilder _tBuilder;
    List<PropertyBuilding> _propBuilds = new List<PropertyBuilding>();
    AssemblyBuilder _aBuilder;
    /// <summary>
    /// Begins creating type using the specified name.
    /// </summary>
    /// <param name="className">Class name for new type</param>
    /// <param name="parentType">Name of base class. Use object if none</param>
    /// <returns></returns>
    public IBaseObject Create(string className, Type parentType)
    {
        return Create(className, parentType, "");
    }
    /// <summary>
    /// Begins creating type using the specified name and saved in the specified directory.
    /// Use this overload to save the resulting .dll in a specified directory.
    /// </summary>
    /// <param name="className">Class name for new type</param>
    /// <param name="parentType">Name of base class. Use object if none</param>
    /// <param name="dir">Directory path to save .dll in</param>
    /// <returns></returns>
    public IBaseObject Create (string className, Type parentType, string dir)
    {
        _parentType = parentType;
        //Define type
        AssemblyName _name = new AssemblyName(className);
        if (string.IsNullOrWhiteSpace(dir))
        {
            _aBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(_name, AssemblyBuilderAccess.RunAndSave);
        }
        else _aBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(_name, AssemblyBuilderAccess.RunAndSave, dir);
        ModuleBuilder _mBuilder = _aBuilder.DefineDynamicModule(_name.Name, _name.Name + ".dll");
        _tBuilder = _mBuilder.DefineType(className, TypeAttributes.Public | TypeAttributes.Class, parentType);
        return this;
    }
    /// <summary>
    /// Adds constructors to new type that match all constructors on base type.
    /// Parameters are passed to base type.
    /// </summary>
    /// <returns></returns>
    public IEmptyObject AddPassThroughCtors()
    {
        foreach(ConstructorInfo _ctor in _parentType.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
        {
            ParameterInfo[] _params = _ctor.GetParameters();
            Type[] _paramTypes = _params.Select(p => p.ParameterType).ToArray();
            Type[][] _reqModifiers = _params.Select(p => p.GetRequiredCustomModifiers()).ToArray();
            Type[][] _optModifiers = _params.Select(p => p.GetOptionalCustomModifiers()).ToArray();
            ConstructorBuilder _ctorBuild = _tBuilder.DefineConstructor(MethodAttributes.Public, _ctor.CallingConvention, _paramTypes, _reqModifiers, _optModifiers);
            for (int i = 0; i < _params.Length; i++)
            {
                ParameterInfo _param = _params[i];
                ParameterBuilder _prmBuild = _ctorBuild.DefineParameter(i + 1, _param.Attributes, _param.Name);
                if (((int)_param.Attributes & (int)ParameterAttributes.HasDefault) != 0) _prmBuild.SetConstant(_param.RawDefaultValue);

                foreach(CustomAttributeBuilder _attr in GetCustomAttrBuilders(_param.CustomAttributes))
                {
                    _prmBuild.SetCustomAttribute(_attr);
                }
            }

            //ConstructorBuilder _cBuilder = _tBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Any, argTypes);
            ILGenerator _ctorGen = _ctorBuild.GetILGenerator();
            _ctorGen.Emit(OpCodes.Nop);
            //arg0=new obj, arg1-arg3=passed params. Push onto stack for call to base class
            _ctorGen.Emit(OpCodes.Ldarg_0);
            for (int i = 1; i <= _params.Length; i++) _ctorGen.Emit(OpCodes.Ldarg, i);
            _ctorGen.Emit(OpCodes.Call, _ctor);
            _ctorGen.Emit(OpCodes.Ret);
        }
        return this;
    }
    /// <summary>
    /// Adds a new property to type with specified name and type.
    /// </summary>
    /// <param name="name">Name of property</param>
    /// <param name="type">Type of property</param>
    /// <returns></returns>
    public IAfterProperty AddProperty(string name, Type type)
    {
        //base property
        PropertyBuilder _pBuilder = _tBuilder.DefineProperty(name, PropertyAttributes.None, type, new Type[0] { });
        //backing field
        FieldBuilder _fBuilder = _tBuilder.DefineField($"m_{name}", type, FieldAttributes.Private);

        //get method
        MethodAttributes _propAttrs = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;
        MethodBuilder _getBuilder = _tBuilder.DefineMethod($"get_{name}", _propAttrs, type, Type.EmptyTypes);
        ILGenerator _getGen = _getBuilder.GetILGenerator();
        _getGen.Emit(OpCodes.Ldarg_0);
        _getGen.Emit(OpCodes.Ldfld, _fBuilder);
        _getGen.Emit(OpCodes.Ret);

        //set method
        MethodBuilder _setBuilder = _tBuilder.DefineMethod($"set_{name}", _propAttrs, null, new Type[] { type });
        ILGenerator _setGen = _setBuilder.GetILGenerator();
        _setGen.Emit(OpCodes.Ldarg_0);
        _setGen.Emit(OpCodes.Ldarg_1);
        _setGen.Emit(OpCodes.Stfld, _fBuilder);
        _setGen.Emit(OpCodes.Ret);

        _propBuilds.Add(new PropertyBuilding(_pBuilder, _getBuilder, _setBuilder));
        return this;
    }
    /// <summary>
    /// Adds an attribute to a property just added.
    /// </summary>
    /// <param name="attrType">Type of attribute</param>
    /// <param name="ctorArgTypes">Types of attribute's cstor parameters</param>
    /// <param name="ctorArgs">Values to pass in to attribute's cstor. Must match in type and order of cstorArgTypes parameter</param>
    /// <returns></returns>
    public IAfterAttribute AddPropertyAttribute(Type attrType, Type[] ctorArgTypes, params object[] ctorArgs)
    {
        if (ctorArgTypes.Length != ctorArgs.Length) throw new Exception("Type count must match arg count for attribute specification");
        ConstructorInfo _attrCtor = attrType.GetConstructor(ctorArgTypes);
        for (int i = 0; i < ctorArgTypes.Length; i++)
        {
            CustomAttributeBuilder _attrBuild = new CustomAttributeBuilder(_attrCtor, ctorArgs);
            _propBuilds.Last().propertyBuilder.SetCustomAttribute(_attrBuild);
        }
        return this;
    }
    /// <summary>
    /// Completes building type, compiles it, and returns the resulting type
    /// </summary>
    /// <returns></returns>
    public Type FinishBuildingType()
    {
        foreach(var _pBuilder in _propBuilds)
        {
            _pBuilder.propertyBuilder.SetGetMethod(_pBuilder.getBuilder);
            _pBuilder.propertyBuilder.SetSetMethod(_pBuilder.setBuilder);
        }

        Type _paramsType = _tBuilder.CreateType();
        return _paramsType;
    }
    /// <summary>
    /// Completes building type, compiles it, saves it, and returns the resultying type.
    /// Assembly is saved in the calling assembly's directory or in the dir specified in the Create method.
    /// </summary>
    /// <param name="assemblyFileName">Filename of the assembly</param>
    /// <returns></returns>
    public Type FinishBuildingAndSaveType(string assemblyFileName)
    {
        Type _newType = FinishBuildingType();
        Save(assemblyFileName);
        return _newType;
    }
    #region Helpers
    private CustomAttributeBuilder[] GetCustomAttrBuilders(IEnumerable<CustomAttributeData> customAttributes)
    {
        return customAttributes.Select(attribute => {
            object[] attributeArgs = attribute.ConstructorArguments.Select(a => a.Value).ToArray();
            PropertyInfo[] namedPropertyInfos = attribute.NamedArguments.Select(a => a.MemberInfo).OfType<PropertyInfo>().ToArray();
            object[] namedPropertyValues = attribute.NamedArguments.Where(a => a.MemberInfo is PropertyInfo).Select(a => a.TypedValue.Value).ToArray();
            FieldInfo[] namedFieldInfos = attribute.NamedArguments.Select(a => a.MemberInfo).OfType<FieldInfo>().ToArray();
            object[] namedFieldValues = attribute.NamedArguments.Where(a => a.MemberInfo is FieldInfo).Select(a => a.TypedValue.Value).ToArray();
            return new CustomAttributeBuilder(attribute.Constructor, attributeArgs, namedPropertyInfos, namedPropertyValues, namedFieldInfos, namedFieldValues);
        }).ToArray();
    }
    /// <summary>
    /// Requires admin privileges. 
    /// To save in a specified dir, use the Create overload that requires a 'dir' parameter.
    /// </summary>
    /// <param name="assemblyFileName"></param>
    private void Save(string assemblyFileName)
    {
        string _assemblyName = assemblyFileName;
        if (!Path.HasExtension(assemblyFileName) || Path.GetExtension(assemblyFileName).ToLower() != ".dll")
            _assemblyName += ".dll";
        _aBuilder.Save(_assemblyName);
    }
    #endregion
}
}

【讨论】:

    【解决方案3】:

    定义:

    public class Product
    {
        public string ProductID { get; set; }
        //product in a product search listing
        public string StoreName { get; set; }
        public string SearchToken { get; set; }
        public Dictionary<string, object> Fields { get; set; }
    }
    

    用法:

    var Prod = new Product();
    Prod.Fields.Add("MyCustomFieldString", "my value here");
    Prod.Fields.Add("MCustomFieldInt", 123);
    
    MessageBox.Show(Prod.Fields[MyCustomFieldString].ToString());
    MessageBox.Show(Prod.Fields[MCustomFieldInt].ToString());
    

    【讨论】:

    • 好的,谢谢 CathalMF,但我希望这个新添加的属性将作为产品类中的永久属性存在
    • @amethianil 永久属性将要求您更改代码,这意味着在运行时编译和替换 dll。简单地添加一个新的搜索列似乎很疯狂。
    • @Enerccio 修改答案,如果答案没有帮助,请投反对票。
    【解决方案4】:

    你需要澄清(也许对你自己)你到底需要什么,或者我会说,你想要什么。

    编译代码后,会根据您的类型\方法\字段生成元数据。有许多表(更具体的 45 个)来描述您的数据。

    因此,如果您想为某种类型添加新属性,您需要向元数据表添加新行。要添加新行,您需要编译代码。

    那么Refelection.Emit 是什么?是一种在您的应用程序中动态创建新数据的方法。有了这个,你可以创建一个动态程序集、动态类型、动态方法,添加一些 IL 就可以了。但是,所有这些都在记忆中。这不是你想要的。

    所以,或者使用Emit\ExpandoObject动态数据,或者通过将属性添加到代码并编译它来静态数据。

    你还有一种在运行时方式和编译方式之间的方式,使用它调用Mono.Cecil,你可以通过代码将新属性添加到类型,然后编译并将新的dll加载到你的AppDomain 或者只是关闭应用程序并重新午餐。它可以是自动的,只需通过代码即可。没有魔法,它会将数据添加到程序集中的元数据表中,但它可以缩短手动执行此操作的方式。

    但我相信您知道还有更多选择。我您使用的是Visual Studio,您可能听说过Edit and Continue (EnC)。此功能实际上是向您的 running 程序集写入新代码,在运行中将新数据(并且只有新数据)添加到元数据中。

    所以,我不能告诉你你应该怎么做,因为我不知道你的具体情况,而且有些情况下人们在不需要极端解决方案时想要极端的解决方案,但如果你真的必须选择将新数据添加到您已经运行的程序集中,EnC 可以是一个解决方案。要使用EnC,请查看Roslyn 代码中的功能(您可能需要检查他们的测试以了解如何使用)。之后,看看@Josh Varty 示例here。

    在我写完之后,我真的 (x3) 不认为你需要为你需要的东西而疯狂。我确信有更好、更简单的方法来做到这一点。

    【讨论】:

    • 好的,谢谢 Dudi Keleti - 我用过 ExpandoObjexct
    【解决方案5】:

    向现有类添加一个属性,如下所示:

     class MyClass
      {
        public int Id { get; set; }// Existing property
        public List<dynamic> Information { get; set; } 
        // Added above property to handle new properties which will come dynamically  
      }
    
           //-------- While Implementing ----
            MyClass obj = new MyClass();
            obj.Id = 1111; // Existing Property
            obj.Information = new List<dynamic>();
    
            obj.Information.Add(new ExpandoObject());
            obj.Information[0].Name= "Gyan"; // New Property 
            obj.Information[0].Age = 22; // New Property 
    

    【讨论】:

      【解决方案6】:

      我发现使用 DynamicObject 类对于在运行时动态添加属性很有用。

      【讨论】:

        猜你喜欢
        • 2019-02-15
        • 2013-03-27
        • 2017-04-22
        • 2013-01-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-03-08
        相关资源
        最近更新 更多