4

I am attempting to serialize just part of a class. I've added XML attributes to the class members so that the generated XML tags are correctly named to match a spec regardless of what my properties are named. This works fine when serializing the main class. However, if I just want to serialize part of the class, I lose the XML attributes and the names go back to their defaults. Is there a way to retain the XML attributes when serializing just part of a class?

[XmlRoot ("someConfiguration")]
public class SomeConfiguration
{
    [XmlArray("bugs")]
    [XmlArrayItem("bug")]
    public List<string> Bugs { get; set; }
}

When I serialize the entire class, I get this (which is exactly as I would expect):

<someConfiguration>
  <bugs>
    <bug>Bug1</bug>
    <bug>Bug2</bug>
    <bug>Bug3</bug>
  </bugs>
</someConfiguration>

If I attempt to just serialize the 'Bugs' part of the class, I get this (note the XML attributes that change the tag names are all ignored):

<ArrayOfString>
  <string>Bug1</string>
  <string>Bug2</string>
  <string>Bug3</string>
</ArrayOfString>

I need to get this:

  <bugs>
    <bug>Bug1</bug>
    <bug>Bug2</bug>
    <bug>Bug3</bug>
  </bugs>

How do I get the partial class to serialize with the above tags?

Or better yet, is there a way to specify tag names when serializing a simple List<object>. So that you can specify the tag used for the list instead of it using <ArrayOfobject> and specify the tag used for the array items instead of <object>?

Curtis
  • 5,794
  • 8
  • 50
  • 77

3 Answers3

2

is there a way to specify tag names when serializing a simple List.

In general, depending on the exact scenario, it may be possible to get this to work. See MSDN's How to: Specify an Alternate Element Name for an XML Stream. The example there involves overriding serialization of a specific field, but it may be possible to use the same technique to override whole type names as well.

But it seems like an awful lot of trouble to me. Instead, why not just handle the serialization explicitly:

private static string SerializeByLinqAndToString<T>(
    List<T> data, string rootName, string elementName)
{
    XDocument document = new XDocument(
        new XElement(rootName, data.Select(s => new XElement(elementName, s))));

    return SaveXmlToString(document);
}

private static string SaveXmlToString(XDocument document)
{
    StringBuilder sb = new StringBuilder();

    using (XmlWriter xmlWriter = XmlWriter.Create(sb,
        new XmlWriterSettings { Indent = true, OmitXmlDeclaration = true }))
    {
        document.Save(xmlWriter);
    }

    return sb.ToString();
}

Call like this:

SomeConfiguration config = ...; // initialize as desired

string result = SerializeByLinq(config.Bugs, "bug", "bugs");

The above works only with a list of strings, or a list of types where the element content can be simply the result of calling ToString() on the instance of the type.

Using the full-blown serialization features in .NET might be worthwhile when dealing with complex types, but if all you've got is a simple list of strings, the LINQ-to-XML feature is very handy.

If you do have more complex types, you can transform each list element into an XElement for the DOM and serialize that:

private static string SerializeByLinq<T>(
    List<T> data, string rootName, string elementName = null)
{
    XDocument document = new XDocument(
        new XElement(rootName, data.Select(t =>
            ElementFromText(SerializeObject(t), elementName)
        )));

    return SaveXmlToString(document);
}

private static XElement ElementFromText(string xml, string name = null)
{
    StringReader reader = new StringReader(xml);
    XElement result = XElement.Load(reader);

    if (!string.IsNullOrEmpty(name))
    {
        result.Name = name;
    }

    return result;
}

private static string SerializeObject<T>(T o)
{
    XmlSerializer xmlSerializer = new XmlSerializer(typeof(T));
    StringWriter textWriter = new StringWriter();

    using (XmlWriter writer = XmlWriter.Create(textWriter,
        new XmlWriterSettings { Indent = true, OmitXmlDeclaration = true }))
    {
        xmlSerializer.Serialize(writer, o,
            new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty}));
    }

    return textWriter.ToString();
}

In this second example, you can omit the element name for the child, and it will just use whatever the type's set up to use already (e.g. the type name, or whatever [XmlRoot] is set to).

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • I tried this with a type other than "string" and it shows the 'ToString()' representation of the 'bug' objects instead of the XML representation. – Curtis Mar 15 '17 at 22:57
  • 1
    @Curtis: well, yes. Your question was asking about serializing a list of `string` values, not other objects. Other scenarios are necessarily going to be more complicated. In those cases, as I mentioned, you may well find that the serialization override technique I mentioned is required. – Peter Duniho Mar 15 '17 at 23:06
  • I tried calling serialize on the 't' that you pass into new XElement, but it ended up replacing the < with &lt and > with &gt etc... – Curtis Mar 15 '17 at 23:19
  • 1
    @Curtis: yes, that's expected. The `XElement` content has to be preserved, and so if it looks like XML, it has to be escaped/changed to entities to ensure it's preserved. You would have to extend the basic LINQ-to-XML technique to further serialize the `T` object into `XElement` objects to be included as the content. The above works only for `T` where `T.ToString()` produces the desired value (e.g. primitive types like `string`, `int`, etc. and user-defined types that work similarly). – Peter Duniho Mar 15 '17 at 23:22
  • @Curtis: I appreciate the idea behind your edit, but as soon as you find yourself calling `string.Replace()` as a way to deal with XML, you're usually headed in the wrong direction. A better approach would be to generate the XML (e.g. with the `Serialize()` method you had), then load that using `XDocument`, and finally copy that DOM into the DOM you're generating. If I have time, I'll look into adding something like _that_ in the answer above. – Peter Duniho Mar 16 '17 at 01:40
  • @Curtis: okay, I added that example. Does what you were doing before, but without the risk of a string search & replace going awry. – Peter Duniho Mar 16 '17 at 04:02
2

Just throwing this out there, you could wrap the List<> inside a custom class:

[XmlRoot("config")]
public class SomeConfiguration
{
    [XmlElement("bugs")]
    public BugList Bugs { get; set; }
    [XmlElement("trees")]
    public TreeList Trees { get; set; }
}

[XmlRoot("bugs")]
public class BugList 
{
    [XmlElement("bug")]
    public List<string> Items = new List<string>();
}

[XmlRoot("trees")]
public class TreeList
{
    [XmlElement("tree")]
    public List<string> Items = new List<string>();
}   

That will now allow you to serialize the individual Lists and they'll be rooted as you'd expect.

void Main()
{
    var config = new SomeConfiguration
    {
        Bugs = new BugList { Items = { "Bug1", "Bug2" } },
        Trees = new TreeList { Items = { "Tree1", "Tree2" } }
    };

    // Your config will work as normal.
    Debug.WriteLine(ToXml(config)); // <config> <bugs>.. <trees>..</config>

    // Your collections are now root-ed properly.
    Debug.WriteLine(ToXml(config.Bugs)); // <bugs><bug>Bug1</bug><bug>Bug2</bug></bugs>
    Debug.WriteLine(ToXml(config.Trees)); // <trees><tree>Tree1</tree><tree>Tree2</tree></trees>
}

public string ToXml<T>(T obj)
{
    var ser = new XmlSerializer(typeof(T));
    var emptyNs = new XmlSerializerNamespaces();
    emptyNs.Add("","");
    using (var stream = new MemoryStream())
    {
        ser.Serialize(stream, obj, emptyNs);
        return Encoding.ASCII.GetString(stream.ToArray());
    }
}
NPras
  • 3,135
  • 15
  • 29
0

Found a 'work-around' way to do it.. Instead of putting XMLArray and XMLArrayList attributes above the List<>:

[XmlRoot ("someConfiguration")]
public class SomeConfiguration
{
    [XmlArray("bugs")]
    [XmlArrayItem("bug")]
    public List<string> Bugs { get; set; }
}

Put an XmlElement attribute on the list which will specify the tag to be used for each element and not have a tag wrapping the list. Your class tag will in effect do that for you.

[XmlRoot ("bugs")]
public class SomeConfiguration
{
    [XmlElement("bug")]
    public List<string> Bugs { get; set; }
}

When you serialize the above, you will end up with:

  <bugs>
    <bug>Bug1</bug>
    <bug>Bug2</bug>
    <bug>Bug3</bug>
  </bugs>
Curtis
  • 5,794
  • 8
  • 50
  • 77
  • 1
    How does the above allow you to serialize _just_ the list? – Peter Duniho Mar 15 '17 at 23:04
  • You are correct. It is sort of a hack.. But ends up with the result I'm looking for. I actually prefer your method of doing it as it is MUCH more general and can be used to correctly serialize part of a class. I just need to figure out how to show the serialized instead of the t.ToString(). – Curtis Mar 15 '17 at 23:14