Search code examples
c#xmlxml-binding

how to derive xml element name from an attribute value of a class using annotations?


I have properties that have ids and values and a name. Can I represent all those with a single class using XmlElement/XmlArray C# annotations? I would like to derive the xml element name from the class attribute name;

my class would look like:

public class Property {
   public string name; //could be enum
   public int id; 
   public string value;
}

e.g:

new Property("property1name",2,"testvalue");
new Property("property2name",10,"anothervalue");

I would like to have xml that looks like:

<property1name><id>2</id><value>testvalue</value></property1name>
<property2name><id>10</id><value>anothervalue</value></property2name>

instead of the usual

<property><name>property1name</name><id>2</id><value>testvalue</value></property>
<property><name>property2name</name><id>10</id><value>anothervalue</value></property>

In other words the xmlelement gets its name from attribute name of the class Property


Solution

  • Update

    And here's a quick adaptation to handle your Property class. First, a List<T> subclass that implements IXmlSerializable:

    public interface IHasElementName
    {
        string ElementName { get; set; }
    }
    
    public class XmlNamedElementList<T> : List<T>, IXmlSerializable where T : IHasXmlElementName
    {
        // https://msdn.microsoft.com/en-us/library/System.Xml.Serialization.XmlSerializer%28v=vs.110%29.aspx
        // Any serializer created with the "XmlSerializer(typeof(T), rootAttribute)" must be cached
        // to avoid resource & memory leaks.
        class ValueSerializerCache
        {
            // By using a nested class with a static constructor, we defer generation of the XmlSerializer until it's actually required.
            static ValueSerializerCache()
            {
                var rootAttribute = new XmlRootAttribute();
                rootAttribute.ElementName = ValueTypeName;
                rootAttribute.Namespace = ValueTypeNamespace;
                serializer = new XmlSerializer(typeof(T), rootAttribute);
            }
    
            static readonly XmlSerializer serializer;
    
            internal static XmlSerializer Serializer { get { return serializer; } }
        }
    
        static string ValueTypeName { get { return typeof(T).DefaultXmlElementName(); } }
    
        static string ValueTypeNamespace { get { return typeof(T).DefaultXmlElementNamespace(); } }
    
        #region IXmlSerializable Members
    
        XmlSchema IXmlSerializable.GetSchema()
        {
            return null;
        }
    
        void IXmlSerializable.ReadXml(XmlReader reader)
        {
            if (reader.IsEmptyElement)
            {
                reader.Read();
                return;
            }
    
            var typeName = ValueTypeName;
            reader.ReadStartElement(); // Advance to the first sub element of the list element.
            while (reader.NodeType == XmlNodeType.Element)
            {
                var name = reader.Name;
                using (var subReader = reader.ReadSubtree())
                {
                    var doc = XDocument.Load(subReader);
                    if (doc != null && doc.Root != null)
                    {
                        doc.Root.Name = doc.Root.Name.Namespace + typeName;
                        using (var docReader = doc.CreateReader())
                        {
                            var obj = ValueSerializerCache.Serializer.Deserialize(docReader);
                            if (obj != null)
                            {
                                T value = (T)obj;
                                value.ElementName = XmlConvert.DecodeName(name);
                                Add(value);
                            }
                        }
                    }
                }
                // Move past the end of item element
                reader.Read();
            }
            // Move past the end of the list element
            reader.ReadEndElement();
        }
    
        void IXmlSerializable.WriteXml(XmlWriter writer)
        {
            foreach (var value in this)
            {
                XDocument doc = new XDocument();
                using (var subWriter = doc.CreateWriter())
                {
                    // write xml into the writer
                    ValueSerializerCache.Serializer.Serialize(subWriter, value);
                }
    
                if (doc.Root == null)
                    continue;
                doc.Root.Name = doc.Root.Name.Namespace + XmlConvert.EncodeName(value.ElementName);
                // Remove redundant namespaces.
                foreach (var attr in doc.Root.Attributes().ToList())
                {
                    if (!attr.IsNamespaceDeclaration || string.IsNullOrEmpty(attr.Value))
                        continue;
                    var prefix = writer.LookupPrefix(attr.Value);
                    if ((prefix == attr.Name.LocalName)
                        || (prefix == string.Empty && attr.Name == "xmlns"))
                        attr.Remove();
                }
    
                doc.Root.WriteTo(writer);
            }
        }
    
        #endregion
    }
    
    public static class XmlSerializationHelper
    {
        static Attribute GetCustomAttribute(MemberInfo element, Type attributeType)
        {
            return Attribute.GetCustomAttribute(element, attributeType);
        }
    
        static T GetCustomAttribute<T>(MemberInfo element) where T : Attribute
        {
            return (T)GetCustomAttribute(element, typeof(T));
        }
    
        public static string DefaultXmlElementName(this Type type)
        {
            var xmlType = GetCustomAttribute<XmlTypeAttribute>(type);
            if (xmlType != null && !string.IsNullOrEmpty(xmlType.TypeName))
                return xmlType.TypeName;
            var xmlRoot = GetCustomAttribute<XmlRootAttribute>(type);
            if (xmlRoot != null && !string.IsNullOrEmpty(xmlRoot.ElementName))
                return xmlRoot.ElementName;
            return type.Name;
        }
    
        public static string DefaultXmlElementNamespace(this Type type)
        {
            var xmlType = GetCustomAttribute<XmlTypeAttribute>(type);
            if (xmlType != null && !string.IsNullOrEmpty(xmlType.Namespace))
                return xmlType.Namespace;
            var xmlRoot = GetCustomAttribute<XmlRootAttribute>(type);
            if (xmlRoot != null && !string.IsNullOrEmpty(xmlRoot.Namespace))
                return xmlRoot.Namespace;
            return string.Empty;
        }
    
        public static string GetXml<T>(this T obj)
        {
            return GetXml(obj, false);
        }
    
        public static string GetXml<T>(this T obj, bool omitNamespace)
        {
            return GetXml(obj, new XmlSerializer(obj.GetType()), omitNamespace);
        }
    
        public static string GetXml<T>(this T obj, XmlSerializer serializer)
        {
            return GetXml(obj, serializer, false);
        }
    
        public static string GetXml<T>(T obj, XmlSerializer serializer, bool omitStandardNamespaces)
        {
            XmlSerializerNamespaces ns = null;
            if (omitStandardNamespaces)
            {
                ns = new XmlSerializerNamespaces();
                ns.Add("", ""); // Disable the xmlns:xsi and xmlns:xsd lines.
            }
            return GetXml(obj, serializer, ns);
        }
    
        public static string GetXml<T>(T obj, XmlSerializerNamespaces ns)
        {
            return GetXml(obj, new XmlSerializer(obj.GetType()), ns);
        }
    
        public static string GetXml<T>(T obj, XmlSerializer serializer, XmlSerializerNamespaces ns)
        {
            using (var textWriter = new StringWriter())
            {
                XmlWriterSettings settings = new XmlWriterSettings();
                settings.Indent = true;        // For cosmetic purposes.
                settings.IndentChars = "    "; // For cosmetic purposes.
                using (var xmlWriter = XmlWriter.Create(textWriter, settings))
                {
                    if (ns != null)
                        serializer.Serialize(xmlWriter, obj, ns);
                    else
                        serializer.Serialize(xmlWriter, obj);
                }
                return textWriter.ToString();
            }
        }
    }
    

    And use it like:

    public class Property : IHasElementName
    {
        public Property()
        {
        }
    
        public Property(string name, int id, string value)
        {
            this.name = name;
            this.id = id;
            this.value = value;
        }
    
        [XmlIgnore]
        public string name; //could be enum
    
        public int id;
        public string value;
    
        #region IHasElementName Members
    
        [XmlIgnore]
        string IHasElementName.ElementName { get { return name; }  set { name = value; } }
    
        #endregion
    }
    
    public class RootObject
    {
        public RootObject()
        {
            this.Properties = new XmlNamedElementList<Property>();
        }
    
        public XmlNamedElementList<Property> Properties { get; set; }
    }
    
    public static class TestClass
    {
        public static void Test()
        {
            var root = new RootObject
            {
                // Characters " <> first" in the first element name are for testing purposes.
    
                Properties = new XmlNamedElementList<Property> { new Property { id = 1, value = "1", name = "first" }, new Property("property1name", 2, "testvalue"), new Property("property2name", 10, "anothervalue") }
            };
    
            var xml = root.GetXml();
            Debug.WriteLine(xml);
        }
    }
    

    Which produces XML as follows:

    <?xml version="1.0" encoding="utf-16"?>
    <RootObject xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
        <Properties>
            <_x0020__x003C__x003E__x0020_first>
                <id>1</id>
                <value>1</value>
            </_x0020__x003C__x003E__x0020_first>
            <property1name>
                <id>2</id>
                <value>testvalue</value>
            </property1name>
            <property2name>
                <id>10</id>
                <value>anothervalue</value>
            </property2name>
        </Properties>
    </RootObject>
    

    Original Answer

    As requested, here's an implementation of IXmlSerializable on a List<KeyValuePair<string, T>> in which the Key string becomes the element name in the collection.

    What you would probably want to do is to adapt this to serialize a List<IHasElementName> where:

    public interface IHasElementName
    {
        string ElementName { get; set; }
    }
    
    public class Property : IHasElementName
    {
        [XmlIgnore]
        public string name; //could be enum
    
        public int id;
        public string value;
    
        #region IHasElementName Members
    
        [XmlIgnore]
        string IHasElementName.ElementName
        {
            get
            {
                return name;
            }
            set
            {
                name = value;
            }
        }
    
        #endregion
    }
    

    If the name is actually an Enum, you could return the enum string representation from HasElementName.ElementName.

    The list looks like:

    public class XmlKeyValueList<T> : List<KeyValuePair<string, T>>, IXmlSerializable
    {
        // TODO: validate that the "Key" string using XmlConvert.VerifyName.
    
        // https://msdn.microsoft.com/en-us/library/System.Xml.Serialization.XmlSerializer%28v=vs.110%29.aspx
        // Any serializer created with the "XmlSerializer(typeof(T), rootAttribute)" must be cached
        // to avoid resource & memory leaks.
        class ValueSerializerCache
        {
            // By using a nested class with a static constructor, we defer generation of the XmlSerializer until it's actually required.
            static ValueSerializerCache()
            {
                var rootAttribute = new XmlRootAttribute();
                rootAttribute.ElementName = ValueTypeName;
                rootAttribute.Namespace = ValueTypeNamespace;
                serializer = new XmlSerializer(typeof(T), rootAttribute);
            }
    
            static readonly XmlSerializer serializer;
    
            internal static XmlSerializer Serializer { get { return serializer; } }
        }
    
        static string ValueTypeName { get { return typeof(T).DefaultXmlElementName(); } }
    
        static string ValueTypeNamespace { get { return typeof(T).DefaultXmlElementNamespace(); } }
    
        #region IXmlSerializable Members
    
        XmlSchema IXmlSerializable.GetSchema()
        {
            return null;
        }
    
        void IXmlSerializable.ReadXml(XmlReader reader)
        {
            var typeName = ValueTypeName;
    
            reader.ReadStartElement(); // Advance to the first sub element of the list element.
            while (reader.NodeType == XmlNodeType.Element)
            {
                var name = reader.Name;
                using (var subReader = reader.ReadSubtree())
                {
                    var doc = XDocument.Load(subReader);
                    if (doc != null && doc.Root != null)
                    {
                        doc.Root.Name = typeName;
                        using (var docReader = doc.CreateReader())
                        {
                            var obj = ValueSerializerCache.Serializer.Deserialize(docReader);
                            if (obj != null)
                            {
                                Add(new KeyValuePair<string, T>(name, (T)obj));
                            }
                        }
                    }
                }
                // Move past the XmlNodeType.Element
                if (reader.NodeType == XmlNodeType.EndElement)
                    reader.Read();
            }
        }
    
        void IXmlSerializable.WriteXml(XmlWriter writer)
        {
            foreach (var pair in this)
            {
                XDocument doc = new XDocument();
                using (var subWriter = doc.CreateWriter())
                {
                    // write xml into the writer
                    ValueSerializerCache.Serializer.Serialize(subWriter, pair.Value);
                }
                if (doc.Root == null)
                    continue;
                doc.Root.Name = pair.Key;
                // Remove redundant namespaces.
                foreach (var attr in doc.Root.Attributes().ToList())
                {
                    if (!attr.IsNamespaceDeclaration || string.IsNullOrEmpty(attr.Value))
                        continue;
                    if (writer.LookupPrefix(attr.Value) == attr.Name.LocalName)
                        attr.Remove();
                }
    
                doc.Root.WriteTo(writer);
            }
        }
    
        #endregion
    }
    
    public static class XmlSerializationHelper
    {
        public static string DefaultXmlElementName(this Type type)
        {
            var xmlType = type.GetCustomAttribute<XmlTypeAttribute>();
            if (xmlType != null && !string.IsNullOrEmpty(xmlType.TypeName))
                return xmlType.TypeName;
            var xmlRoot = type.GetCustomAttribute<XmlRootAttribute>();
            if (xmlRoot != null && !string.IsNullOrEmpty(xmlRoot.ElementName))
                return xmlRoot.ElementName;
            return type.Name;
        }
    
        public static string DefaultXmlElementNamespace(this Type type)
        {
            var xmlType = type.GetCustomAttribute<XmlTypeAttribute>();
            if (xmlType != null && !string.IsNullOrEmpty(xmlType.Namespace))
                return xmlType.Namespace;
            var xmlRoot = type.GetCustomAttribute<XmlRootAttribute>();
            if (xmlRoot != null && !string.IsNullOrEmpty(xmlRoot.Namespace))
                return xmlRoot.Namespace;
            return string.Empty;
        }
    }