12

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...

2
  • I would cheat, and serialize it 'as it comes', then apply an xsl transform to the resultant xml to massage it to what you need. Alternatively, have you looked at [MessageContract]? It offers a lot more customization over the output than DataContract Commented Aug 2, 2012 at 16:14
  • XSL cheating is not an option as the serializer writes directly into a stream... and thanks for the [MessageContract] lead, I learned something today ^^ however it's deeply related to WCF and does not allow to finely control a simple XML Serialization (I may be wrong, jsut discovered the attribute) Commented Aug 2, 2012 at 16:37

3 Answers 3

9

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

Sign up to request clarification or add additional context in comments.

Comments

5

XmlSerializer should be able to do what you need, but it is highly dependent on the initial structure and setup. I use it in my own code to generate remarkably similar things.

public class Links<Link> : BaseArrayClass<Link> //use whatever base collection extension you actually need here
{
    //...stuff...//
}

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

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

now, serializing the Links class should generate exactly what you are looking for.

The problem with XmlSerializer is when you give it generics, it responds with generics. List implemets Array somewhere in there and the serialized result will nearly always be ArrayOf<X>. To get around that you can name the property, or the class root. The closes to what you need is probably the Second Version from your examples. Im assuming you attempted direct serialization of an object List Links. That wouldn't work because you didn't specify the root node. Now, a similar approach can be found here. In this one they specify the XmlRootAttribute when declaring the serializer. yours would look like this:

XmlSerializer xs = new XmlSerializer(typeof(List<Link>), new XmlRootAttribute("Links"));

2 Comments

Your solution is very close to the final solution, but I cannot specify directly a root attribute name in the XmlSerializer constructor (the serialization code is elsewhere), I tried 'deducting' it via serialization, that worked for the root node, but not for the elements. The solution has been given by Mike with the 'XmlType' attribute I did not know.
combined solution bits. Yay. Glad I could help though =D
1

Here you go...

 class Program
{
    static void Main(string[] args)
    {

        Links ls = new Links();
        ls.Link.Add(new Link() { Name = "Mike", Url = "www.xml.com" });
        ls.Link.Add(new Link() { Name = "Jim", Url = "www.xml.com" });
        ls.Link.Add(new Link() { Name = "Peter", Url = "www.xml.com" });

        XmlSerializer xmlSerializer = new XmlSerializer(typeof(Links));

        StringWriter stringWriter = new StringWriter();

        xmlSerializer.Serialize(stringWriter, ls);

        string serializedXML = stringWriter.ToString();

        Console.WriteLine(serializedXML);

        Console.ReadLine();
    }
}

[XmlRoot("Links")]
public class Links
{
    public Links()
    {
        Link = new List<Link>();
    }

    [XmlElement]
    public List<Link> Link { get; set; }
}

[XmlType("Link")]
public class Link
{
    [XmlAttribute("Name")]
    public string Name { get; set; }


    [XmlAttribute("Href")]
    public string Url { get; set; }

}

1 Comment

Thanks for your answer. Your solution has a few problems -- 1) does not work at it should, as the elements are not renames (you cheated, you called the type 'Link' so the output elements are <Link ...>) you can get around this by specifying the element name in the XmlElement node ---- 2) I'd prefer to avoid aving to create a containing structure as it will cause a problem for the OTHER serialization format (the same objects are used for multiple serialization formats) -- However you gave me a part of the final solution with the XmlTypeAttribute : thank you !

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.