在C#中动态创建属性以与XmlSerializer

时间:2016-01-22 12:03:33

标签: c# xml xmlserializer castle-dynamicproxy

我有一个接受xml消息的API。假设我从这个API获得了对象Thing,如下所示:

<Thing shape="circle" color="red"/>

并映射到:

[XmlRoot("Thing")]
public class Thing {
    [XmlAttribute("shape")]
    public string Shape { get; set; }

    [XmlAttribute("color")]
    public string Color { get; set; }
}

现在我想更新这个对象,例如。颜色是绿色。 API要求我以下列格式发送它:

<Thing color="green" o_color="red"/>

有没有办法动态生成o_ *属性?因此,当它们在构造函数之外设置时,它们的旧值存储在XmlSerializer映射到o_的某些生成属性中?我知道我可以简单地手动创建这些属性,但对于更大的对象来说,这是一项繁琐的工作。我已经尝试使用Castle的动态代理,我已经在项目中使用了它,但它似乎无法添加这样的属性(或者我还没有发现该怎么做)

2 个答案:

答案 0 :(得分:0)

您需要解决几个问题。首先,您必须在每个序列化对象中保持状态,该状态将告诉您属性是否已被更改,以及其原始值是什么。那么你的第二个问题是你需要根据这个维护状态动态构建一个新对象。通过我提供的实现,您需要确定它是否满足您的性能需求,因为它正在进行大量的反射。您还必须通过其他解决方案来减轻其复杂性。我在这个解决方案中看到的优势是不必维护两个非常相似的对象。

<强> !!重要!! 此代码未准备好生产。我在代码中留下了一些需要刷新的待办事项。

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;

namespace ConsoleApplication1
{
    public abstract class PropertyStateTracker
    {
        private class PropertyDetails
        {
            public object CurrentValue { get; set; }
            public bool HasChanged { get; set; }
            public object OriginalValue { get; set; }
        }

        private readonly Dictionary<string, PropertyDetails> propertyState =
            new Dictionary<string, PropertyDetails>();

        protected TProperty Get<TProperty>(Expression<Func<TProperty>> propertySelector)
        {
            string name = GetNameFromExpression(propertySelector);
            PropertyDetails data;
            if (!propertyState.TryGetValue(name, out data))
            {
                Set(propertySelector, default(TProperty));
                return default(TProperty);
            }
            return (TProperty)data.CurrentValue;
        }

        protected void Set<TProperty>(Expression<Func<TProperty>> propertySelector, TProperty value)
        {
            string name = GetNameFromExpression(propertySelector);
            PropertyDetails data;
            if (!propertyState.TryGetValue(name, out data))
            {
                data = new PropertyDetails() { OriginalValue = value, CurrentValue = value, HasChanged = false };
                propertyState[name] = data;
            }
            else
            {
                data.CurrentValue = value;
                data.HasChanged = true;
            }
        }

        [XmlIgnore]
        public IEnumerable<string> ChangedProperties
        {
            get
            {
                foreach (var property in propertyState)
                {
                    if (property.Value.HasChanged)
                    {
                        yield return property.Key;
                    }
                }
            }
        }

        public bool HasChanged<TProperty>(Expression<Func<TProperty>> propertySelector)
        {
            string name = GetNameFromExpression(propertySelector);
            return HasChanged(name);
        }

        public bool HasChanged(string propertyName)
        {
            EnsurePropertyExists(propertyName);
            PropertyDetails data;
            if (!propertyState.TryGetValue(propertyName, out data))
            {
                return false;
            }
            return data.HasChanged;
        }

        public TProperty GetOriginalValue<TProperty>(Expression<Func<TProperty>> propertySelector)
        {
            string name = GetNameFromExpression(propertySelector);
            return (TProperty)GetOriginalValue(name);
        }

        public object GetOriginalValue(string propertyName)
        {
            EnsurePropertyExists(propertyName);
            PropertyDetails data;
            if (!propertyState.TryGetValue(propertyName, out data))
            {
                return GetDefaultValue(GetPropertyInfo(propertyName).PropertyType);
            }
            return data.OriginalValue;
        }

        public TProperty GetCurrentValue<TProperty>(Expression<Func<TProperty>> propertySelector)
        {
            string name = GetNameFromExpression(propertySelector);
            return (TProperty)GetCurrentValue(name);
        }

        public object GetCurrentValue(string propertyName)
        {
            EnsurePropertyExists(propertyName);
            PropertyDetails data;
            if (!propertyState.TryGetValue(propertyName, out data))
            {
                return GetDefaultValue(GetPropertyInfo(propertyName).PropertyType);
            }
            return data.CurrentValue;
        }

        public void Reset()
        {
            foreach (var property in propertyState)
            {
                property.Value.OriginalValue = property.Value.CurrentValue;
                property.Value.HasChanged = false;
            }
        }

        private void EnsurePropertyExists(string propertyName)
        {
            PropertyInfo property = GetPropertyInfo(propertyName);
            if (property == null)
            {
                throw new ArgumentException(string.Format("A property named '{0}' was not found for type '{1}'",
                    propertyName, this.GetType().Name));
            }
        }

        private PropertyInfo GetPropertyInfo(string propertyName)
        {
            Type type = this.GetType();

            var property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
            return property;
        }

        private static object GetDefaultValue(Type t)
        {
            if (t.IsValueType)
                return Activator.CreateInstance(t);

            return null;
        }

        private static string GetNameFromExpression<TMember>(Expression<Func<TMember>> lambda)
        {
            // check to make sure a non-null lambda was provided.
            if (lambda == null)
            {
                throw new ArgumentNullException("lambda");
            }

            Expression expression = lambda.Body;

            // is the expression's body a unary expression.
            var unaryExpression = expression as UnaryExpression;
            if (unaryExpression != null && unaryExpression.NodeType == ExpressionType.Convert)
            {
                expression = unaryExpression.Operand;
            }

            // is the expression's body a parameter expression.
            var parameterExpression = expression as ParameterExpression;
            if (parameterExpression != null)
            {
                return parameterExpression.Name;
            }

            // is the expression's body a member expression.
            var memberExpression = expression as MemberExpression;
            if (memberExpression != null)
            {
                return memberExpression.Member.Name;
            }

            // is the expression's body a method call expression.
            var methodCallExpression = expression as MethodCallExpression;
            if (methodCallExpression != null)
            {
                return methodCallExpression.Method.Name;
            }

            // unable to derive name. throw an exception.
            throw new Exception(
                string.Format("Failed to derive name from expression '{0}'",
                expression));
        }
    }



    [XmlRoot("Thing")]
    public class Thing : PropertyStateTracker
    {
        [XmlAttribute("shape")]
        public string Shape
        {
            get { return Get(() => Shape); }
            set { Set(() => Shape, value); }
        }

        [XmlAttribute("color")]
        public string Color
        {
            get { return Get(() => Color); }
            set { Set(() => Color, value); }
        }
    }


    class Program
    {

        private static long count = 0;

        static void Main(string[] args)
        {
            Thing thingInstance;
            System.Xml.Serialization.XmlSerializer serializer = new XmlSerializer(typeof(Thing));
            string rawData = "<Thing shape=\"circle\" color=\"red\"/>";
            using (System.IO.StringReader reader = new System.IO.StringReader(rawData))
            {
                thingInstance = (Thing)serializer.Deserialize(reader);
            }

            thingInstance.Color = "green";

            // these two variables should be reused every time a new proxy is created. you dont want too many dynamic assemblies.
            var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("DynamicThingAssembly"), AssemblyBuilderAccess.Run);
            var moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicThingModule");

            // TODO: need to figure out a way to reuse the proxyTypes being generated or you will have a 
            // memory leak. the type is unique based on the properties that have been modified, so you should be able to use 
            // the state stored inside of thingInstance to figure this out. I leave this up to you to implement.
            Type proxyType = CreateProxy(moduleBuilder, thingInstance);
            var proxy = Activator.CreateInstance(proxyType);

            foreach (var propertyName in thingInstance.ChangedProperties)
            {

                proxyType.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase)
                    .SetValue(proxy, thingInstance.GetCurrentValue(propertyName));
                proxyType.GetProperty("O_" + propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase)
                    .SetValue(proxy, thingInstance.GetOriginalValue(propertyName));
            }

            // Important this XmlSerializer should be cached, otherwise you will have a memory leak in your program.
            System.Xml.Serialization.XmlSerializer serializer2 = new XmlSerializer(proxyType, new XmlRootAttribute("Thing"));

            StringBuilder sb = new StringBuilder();
            using (System.IO.StringWriter writer = new System.IO.StringWriter(sb))
            {
                serializer2.Serialize(writer, proxy);
            }

            Console.Write(sb.ToString());
        }

        private static Type CreateProxy(ModuleBuilder moduleBuilder, Thing thing)
        {
            var typeName = "DynamicType" + System.Threading.Interlocked.Increment(ref count).ToString("X5");
            TypeBuilder typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public);

            Type t = typeof(Thing);
            foreach (var propertyName in thing.ChangedProperties)
            {
                var propertyInfo = t.GetProperty(propertyName,
                    BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);

                CreateProperty(typeBuilder, propertyInfo.Name, propertyInfo.PropertyType);
                CreateProperty(typeBuilder, "O_" + propertyInfo.Name, propertyInfo.PropertyType);
            }

            return typeBuilder.CreateType();
        }

        private static void CreateProperty(TypeBuilder typeBuilder, string propertyName, Type propertyType)
        {
            var fieldBuilder = typeBuilder.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);

            // The last argument of DefineProperty is null, because the
            // property has no parameters. (If you don't specify null, you must
            // specify an array of Type objects. For a parameterless property,
            // use an array with no elements: new Type[] {})
            var propertyBuilder = typeBuilder.DefineProperty(
                propertyName, PropertyAttributes.HasDefault, propertyType, null);


            var attributeConstructor = typeof(XmlAttributeAttribute).GetConstructor(new Type[] { typeof(string) });
            propertyBuilder.SetCustomAttribute(
                new CustomAttributeBuilder(
                    attributeConstructor,
                    new object[] { propertyName.ToLower() }));


            // The property set and property get methods require a special
            // set of attributes.
            MethodAttributes getSetAttr =
                MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;


            MethodBuilder getPropertyMethodBuilder =
                typeBuilder.DefineMethod("get_" + propertyName,
                                  getSetAttr,
                                  propertyType,
                                  Type.EmptyTypes);

            // Create the get methods body.
            ILGenerator getPropertyMethodILGenerator = getPropertyMethodBuilder.GetILGenerator();
            getPropertyMethodILGenerator.Emit(OpCodes.Ldarg_0);
            getPropertyMethodILGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
            getPropertyMethodILGenerator.Emit(OpCodes.Ret);


            // Define the "set" accessor method for CustomerName.
            MethodBuilder setPropertyMethodBuilder =
                typeBuilder.DefineMethod("set_" + propertyName,
                                           getSetAttr,
                                           null,
                                           new Type[] { propertyType });

            // Create the set methods body.
            ILGenerator setPropertyMethodILGenerator = setPropertyMethodBuilder.GetILGenerator();
            setPropertyMethodILGenerator.Emit(OpCodes.Ldarg_0);
            setPropertyMethodILGenerator.Emit(OpCodes.Ldarg_1);
            setPropertyMethodILGenerator.Emit(OpCodes.Stfld, fieldBuilder);
            setPropertyMethodILGenerator.Emit(OpCodes.Ret);

            // Last, we must map the two methods created above to our PropertyBuilder to 
            // their corresponding behaviors, "get" and "set" respectively. 
            propertyBuilder.SetGetMethod(getPropertyMethodBuilder);
            propertyBuilder.SetSetMethod(setPropertyMethodBuilder);



        }
    }
}

答案 1 :(得分:0)

你喜欢这种方法吗?

Thing thing;
var xs = new XmlSerializer(typeof(Thing));
thing = (Thing)xs.Deserialize(inputStream);

// Stored the old value in the property that will not be used.
thing.Shape = thing.Color;

thing.Color = "green"; // set new value

// Rename unused Shape property to o_color
var attributes = new XmlAttributes { XmlAttribute = new XmlAttributeAttribute("o_color") };
var overrides = new XmlAttributeOverrides();
overrides.Add(typeof(Thing), "Shape", attributes);

xs = new XmlSerializer(typeof(Thing), overrides);
xs.Serialize(outputStream, thing);

Color属性用于其预期目的:存储新的颜色值。

虽然您的示例中未返回Shape属性,但我们使用的前一个颜色值已预先重命名。

当然,这只适用于这种特殊情况:数据类型匹配。