Nielson R. O.
Nielson R. O.

Reputation: 45

How to serialize an array to XML with dynamic tag names

I need to serialize an array of objects to XML on C# with dynamic tag names. I have created two classes that i need to serialize into a xml, but i can't figure out how to create tags with dynamic names.

For example, i have the following classes:

[System.Xml.Serialization.XmlRootAttribute(IsNullable = false)]
public class GeneralInformation
{

    private Info[] addInfoList;

    /// <remarks/>
    [System.Xml.Serialization.XmlArray("InfoList")]
    public Info[] AddInfoList
    {
        get
        {
            return this.addInfoList;
        }
        set
        {
            this.addInfoList = value;
        }
    }
}

public class Info
{
    private string infoMessage;

    /// <remarks/>
    [System.Xml.Serialization.XmlElement("InfoName")]
    public string InfoMessage
    {
        get
        {
            return this.infoMessage;
        }
        set
        {
            this.infoMessage = value;
        }
    }
}

If I add some simple data and serialize it, I get this:

<?xml version="1.0" encoding="utf-16"?>
<GeneralInformation xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <InfoList>
        <Info>
            <InfoName>Test1</InfoName>
        </Info>
        <Info>
            <InfoName>Test2</InfoName>
        </Info>
        <Info>
            <InfoName>Test3</InfoName>
        </Info>
    </InfoList>
</GeneralInformation>

But i need to enumerate the tag "Info" with the index of the array + 1. Is that possible? The result would look like this.

<?xml version="1.0" encoding="utf-16"?>
<GeneralInformation xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <InfoList>
        <Info001>
            <InfoName>Test1</InfoName>
        </Info001>
        <Info002>
            <InfoName>Test2</InfoName>
        </Info002>
        <Info003>
            <InfoName>Test3</InfoName>
        </Info003>
    </InfoList>
</GeneralInformation>

Note I only need to serialize my GeneralInformation to XML, not deserialize.

Upvotes: 2

Views: 2453

Answers (3)

Alexander Petrov
Alexander Petrov

Reputation: 14231

You can use custom xml writer.

public class CustomWriter : XmlTextWriter
{
    private int counter = 1;

    public CustomWriter(TextWriter writer) : base(writer) { }
    public CustomWriter(Stream stream, Encoding encoding) : base(stream, encoding) { }
    public CustomWriter(string filename, Encoding encoding) : base(filename, encoding) { }

    public override void WriteStartElement(string prefix, string localName, string ns)
    {
        if (localName == "Info")
        {
            base.WriteStartElement(prefix, localName + counter.ToString("0##"), ns);
            counter++;
        }
        else
        {
            base.WriteStartElement(prefix, localName, ns);
        }
    }
}

Use:

var xs = new XmlSerializer(typeof(GeneralInformation));

using (var writer = new CustomWriter(Console.Out))
{
    writer.Formatting = Formatting.Indented;
    xs.Serialize(writer, data);
}

Namespaces:

using System;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

Upvotes: 0

dbc
dbc

Reputation: 116785

The values of your <InfoList> element would most naturally be represented as a Dictionary<string, Info>, but unfortunately XmlSerializer does not support dictionaries.

Instead, since you want to collect your Info objects in an array, you can use the approach from this answer to Deserialize XML with XmlSerializer where XmlElement names differ but have same content and serialize your Info [] array via an [XmlAnyElement("InfoList")] public XElement surrogate property inside which the Info instances are serialized to named elements by constructing a nested XmlSerializer.

First, define your GeneralInformation as follows:

[System.Xml.Serialization.XmlRootAttribute(IsNullable = false)]
public class GeneralInformation
{

    private Info[] addInfoList;

    /// <remarks/>
    [XmlIgnore]
    public Info[] AddInfoList
    {
        get
        {
            return this.addInfoList;
        }
        set
        {
            this.addInfoList = value;
        }
    }

    const string InfoPrefix = "Info";
    const string InfoListPrefix = "InfoList";

    [XmlAnyElement("InfoList")]
    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public XElement AddInfoListXml
    {
        get
        {
            if (addInfoList == null)
                return null;
            return new XElement(InfoListPrefix,
                addInfoList
                .Select((info, i) => new KeyValuePair<string, Info>(InfoPrefix + (i + 1).ToString("D3", NumberFormatInfo.InvariantInfo), info))
                .SerializeToXElements((XNamespace)""));
        }
        set
        {
            if (value == null)
            {
                addInfoList = null;
            }
            else
            {
                addInfoList = value
                    .Elements()
                    .Where(e => e.Name.LocalName.StartsWith(InfoPrefix))
                    .DeserializeFromXElements<Info>()
                    .Select(p => p.Value)
                    .ToArray();
            }
        }
    }
}

Then, grab XmlKeyValueListHelper and XmlSerializerFactory verbatim from Deserialize XML with XmlSerializer where XmlElement names differ but have same content:

public static class XmlKeyValueListHelper
{
    const string RootLocalName = "Root";

    public static XElement [] SerializeToXElements<T>(this IEnumerable<KeyValuePair<string, T>> dictionary, XNamespace ns)
    {
        if (dictionary == null)
            return null;
        ns = ns ?? "";
        var serializer = XmlSerializerFactory.Create(typeof(T), RootLocalName, ns.NamespaceName);
        var array = dictionary
            .Select(p => new { p.Key, Value = p.Value.SerializeToXElement(serializer, true) })
            // Fix name and remove redundant xmlns= attributes.  XmlWriter will add them back if needed.
            .Select(p => new XElement(ns + p.Key, p.Value.Attributes().Where(a => !a.IsNamespaceDeclaration), p.Value.Elements()))
            .ToArray();
        return array;
    }

    public static IEnumerable<KeyValuePair<string, T>> DeserializeFromXElements<T>(this IEnumerable<XElement> elements)
    {
        if (elements == null)
            yield break;
        XmlSerializer serializer = null;
        XNamespace ns = null;
        foreach (var element in elements)
        {
            if (serializer == null || element.Name.Namespace != ns)
            {
                ns = element.Name.Namespace;
                serializer = XmlSerializerFactory.Create(typeof(T), RootLocalName, ns.NamespaceName);
            }
            var elementToDeserialize = new XElement(ns + RootLocalName, element.Attributes(), element.Elements());
            yield return new KeyValuePair<string, T>(element.Name.LocalName, elementToDeserialize.Deserialize<T>(serializer));
        }
    }

    public static XmlSerializerNamespaces NoStandardXmlNamespaces()
    {
        var ns = new XmlSerializerNamespaces();
        ns.Add("", ""); // Disable the xmlns:xsi and xmlns:xsd lines.
        return ns;
    }

    public static XElement SerializeToXElement<T>(this T obj)
    {
        return obj.SerializeToXElement(null, NoStandardXmlNamespaces());
    }

    public static XElement SerializeToXElement<T>(this T obj, XmlSerializerNamespaces ns)
    {
        return obj.SerializeToXElement(null, ns);
    }

    public static XElement SerializeToXElement<T>(this T obj, XmlSerializer serializer, bool omitStandardNamespaces)
    {
        return obj.SerializeToXElement(serializer, (omitStandardNamespaces ? NoStandardXmlNamespaces() : null));
    }

    public static XElement SerializeToXElement<T>(this T obj, XmlSerializer serializer, XmlSerializerNamespaces ns)
    {
        var doc = new XDocument();
        using (var writer = doc.CreateWriter())
            (serializer ?? new XmlSerializer(obj.GetType())).Serialize(writer, obj, ns);
        var element = doc.Root;
        if (element != null)
            element.Remove();
        return element;
    }

    public static T Deserialize<T>(this XContainer element, XmlSerializer serializer)
    {
        using (var reader = element.CreateReader())
        {
            object result = (serializer ?? new XmlSerializer(typeof(T))).Deserialize(reader);
            return (T)result;
        }
    }
}

public static class XmlSerializerFactory
{
    // To avoid a memory leak the serializer must be cached.
    // https://stackoverflow.com/questions/23897145/memory-leak-using-streamreader-and-xmlserializer
    // This factory taken from 
    // https://stackoverflow.com/questions/34128757/wrap-properties-with-cdata-section-xml-serialization-c-sharp/34138648#34138648

    readonly static Dictionary<Tuple<Type, string, string>, XmlSerializer> cache;
    readonly static object padlock;

    static XmlSerializerFactory()
    {
        padlock = new object();
        cache = new Dictionary<Tuple<Type, string, string>, XmlSerializer>();
    }

    public static XmlSerializer Create(Type serializedType, string rootName, string rootNamespace)
    {
        if (serializedType == null)
            throw new ArgumentNullException();
        if (rootName == null && rootNamespace == null)
            return new XmlSerializer(serializedType);
        lock (padlock)
        {
            XmlSerializer serializer;
            var key = Tuple.Create(serializedType, rootName, rootNamespace);
            if (!cache.TryGetValue(key, out serializer))
                cache[key] = serializer = new XmlSerializer(serializedType, new XmlRootAttribute { ElementName = rootName, Namespace = rootNamespace });
            return serializer;
        }
    }
}

Notes:

  • By specifying the "InfoList" name in the [XmlAnyElement("InfoList")] constructor applied to the public XElement AddInfoListXml property at the root level, only elements named <InfoList> and their contents will be deserialized via the surrogate.

  • The original AddInfoList property must be marked with [XmlIgnore] so it is not serialized along with the surrogate.

  • Replacing the AddInfoList array with a custom collection that implements IXmlSerializable would be another way to solve this problem, however implementing IXmlSerializable correctly can be quite tricky. See How do you deserialize XML with dynamic element names? for an example which is simpler than the one shown here, since the elements in that question contain only text content.

Your sample XML can now be deserialized and re-serialized as shown in the sample .Net fiddle here, generating the following XML:

<GeneralInformation xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <InfoList>
    <Info001>
      <InfoName>Test1</InfoName>
    </Info001>
    <Info002>
      <InfoName>Test2</InfoName>
    </Info002>
    <Info003>
      <InfoName>Test3</InfoName>
    </Info003>
  </InfoList>
</GeneralInformation>

Upvotes: 0

jdweng
jdweng

Reputation: 34421

Using XDocument :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;

namespace ConsoleApplication45
{
    class Program
    {

        static void Main(string[] args)
        {
            string xmlIdent = "<?xml version=\"1.0\" encoding=\"utf-16\"?>" +
                "<GeneralInformation xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">" +
                "</GeneralInformation>";

            XDocument doc = XDocument.Parse(xmlIdent);

            XElement generalInfo = doc.Root;
            XElement infoList = new XElement("InfoList");
            generalInfo.Add(infoList);

            for (int i = 0; i < 10; i++)
            {
                infoList.Add(new XElement("Infor" + i.ToString("0##"), new XElement("InfoName", "Test" + i.ToString("0##"))));
            }


        }

    }
}

//<?xml version="1.0" encoding="utf-16"?>
//<GeneralInformation xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
//    <InfoList>
//        <Info001>
//            <InfoName>Test1</InfoName>
//        </Info001>
//        <Info002>
//            <InfoName>Test2</InfoName>
//        </Info002>
//        <Info003>
//            <InfoName>Test3</InfoName>
//        </Info003>
//    </InfoList>
//</GeneralInformation>

Upvotes: 3

Related Questions