Search code examples
c#xmlserializationcollectionsroot-node

C# Xml serialization, collection and root element


My app serializes objects in streams. Here is a sample of what I need :

<links>
  <link href="/users" rel="users" />
  <link href="/features" rel="features" />
</links>

In this case, the object is a collection of 'links' object.

-----------First version

At first I used the DataContractSerializer, however you cannot serialize members as attributes (source)

Here is the object :

[DataContract(Name="link")]
public class LinkV1
{
    [DataMember(Name="href")]
    public string Url { get; set; }

    [DataMember(Name="rel")]
    public string Relationship { get; set; }
}

And here is the result :

<ArrayOflink xmlns:i="...." xmlns="...">
  <link>
    <href>/users</href>
    <rel>users</rel>
  </link>
  <link>
    <href>/features</href>
    <rel>features</rel>
  </link>
</ArrayOflink>

----------- Second version

Ok, not quiet what I want, so I tried the classic XmlSerializer, but... oh nooo, you cannot specify the name of the root element & of the collection's elements if the root element is a collection...

Here is the code :

[XmlRoot("link")]
public class LinkV2
{
    [XmlAttribute("href")]
    public string Url { get; set; }

    [XmlAttribute("rel")]
    public string Relationship { get; set; }
}

Here is the result :

<ArrayOfLinkV2>
  <LinkV2 href="/users" rel="users" />
  <LinkV2 href="/features" rel="features" />
  <LinkV2 href="/features/user/{keyUser}" rel="featuresByUser" />
</ArrayOfLinkV2>

----------- Third version

using XmlSerializer + a root element :

[XmlRoot("trick")]
public class TotallyUselessClass
{
    [XmlArray("links"), XmlArrayItem("link")]
    public List<LinkV2> Links { get; set; }
}

And its result :

 <trick>
  <links>
    <link href="/users" rel="users" />
    <link href="/features" rel="features" />
    <link href="/features/user/{keyUser}" rel="featuresByUser" />
  </links>
</trick>

Nice, but I don't want that root node !! I want my collection to be the root node.

Here are the contraints :

  • the serialization code is generic, it works with anything serializable
  • the inverse operation (deserialization) have to work too
  • I don't want to regex the result (I serialize directly in an output stream)

What are my solutions now :

  1. Coding my own XmlSerializer
  2. Trick XmlSerializer when it works with a collection (I tried, having it find a XmlRootElement and plurialize it to generate its own XmlRootAttribute, but that causes problem when deserializing + the items name still keeps the class name)

Any idea ?

What really bother me in that issue, is that what I want seems to be really really really simple...


Solution

  • Ok, here is my final solution (hope it helps someone), that can serialize a plain array, List<>, HashSet<>, ...

    To achieve this, we'll need to tell the serializer what root node to use, and it's kind of tricky...

    1) Use 'XmlType' on the serializable object

    [XmlType("link")]
    public class LinkFinalVersion
    {
        [XmlAttribute("href")]
        public string Url { get; set; }
    
        [XmlAttribute("rel")]
        public string Relationship { get; set; }
    }
    

    2) Code a 'smart-root-detector-for-collection' method, that will return a XmlRootAttribute

    private XmlRootAttribute XmlRootForCollection(Type type)
    {
        XmlRootAttribute result = null;
    
        Type typeInner = null;
        if(type.IsGenericType)
        {
            var typeGeneric = type.GetGenericArguments()[0];
            var typeCollection = typeof (ICollection<>).MakeGenericType(typeGeneric);
            if(typeCollection.IsAssignableFrom(type))
                typeInner = typeGeneric;
        }
        else if(typeof (ICollection).IsAssignableFrom(type)
            && type.HasElementType)
        {
            typeInner = type.GetElementType();
        }
    
        // yeepeeh ! if we are working with a collection
        if(typeInner != null)
        {
            var attributes = typeInner.GetCustomAttributes(typeof (XmlTypeAttribute), true);
            if((attributes != null)
                && (attributes.Length > 0))
            {
                var typeName = (attributes[0] as XmlTypeAttribute).TypeName + 's';
                result = new XmlRootAttribute(typeName);
            }
        }
        return result;
    }
    

    3) Push that XmlRootAttribute into the serializer

    // hack : get the XmlRootAttribute if the item is a collection
    var root = XmlRootForCollection(type);
    // create the serializer
    var serializer = new XmlSerializer(type, root);
    

    I told you it was tricky ;)


    To improve this, you can :

    A) Create a XmlTypeInCollectionAttribute to specify a custom root name (If the basic pluralization does not fit your need)

    [XmlType("link")]
    [XmlTypeInCollection("links")]
    public class LinkFinalVersion
    {
    }
    

    B) If possible, cache your XmlSerializer (in a static Dictionary for example).

    In my testing, instanciating a XmlSerializer without the XmlRootAttributes takes 3ms. If you specify an XmlRootAttribute, it takes around 80ms (Just to have a custom root node name !)