C# Xml serialization, collection and root element

c#, collections, root-node, serialization, xml

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 !)

Problem

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 : - Coding my own XmlSerializer - 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...

Original source

Related problems