Reputation: 45
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
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
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
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