0

I have a site that imports bookings from an external XML file and stores them as nodes in an Umbraco site.

The issue:

Basically, The system in which I exported the bookings from has changed how it exports the XML and the original root node.

I am assuming this is an issue related to this root node as many answers on stack have mentioned this in regards to the error in the subject title, but none that I could see have covered how to deal with a dynamic rootnode.

What I used to have in the XML file:

<Bookings>
    <row id="1">
        <FirstBookingAttribute>....
    </row>
</Bookings>

Now is dynamic and can end up looking like:

<BookingsFebruary2017 company="CompanyNameHere">
     <row id="1">
        <firstBookingAttribute>....
     </row>
</BookingsFebruary2017>

Because of this I am not entirely sure how to set the XMLroot node in C#.

Incase this issue is not related to the root node, I have pasted the erroring code below:

Controller File

var reader = new StreamReader(tempFile);
var deserializer = new XmlSerializer(typeof(Bookings));
var xml = deserializer.Deserialize(reader);
var studentBookings = (Bookings) xml;

Model of Bookings

    public class Bookings
{
    [XmlElement("row")]
    public List<Booking> StudentBookings { get; set; }
}

Thank you

dbc
  • 104,963
  • 20
  • 228
  • 340
Chancerson
  • 15
  • 6
  • 2
    Can you not push back on the people who've changed this and point out that they're breaking *anyone*s ability to act meaningfully on this data in an automated fashion because that's *not* how XML should be used. – Damien_The_Unbeliever Feb 20 '17 at 15:54
  • In my case, it was because of wrong declaration of `XmlSerializer`. So check that also. – Mangesh Jun 02 '17 at 06:59

1 Answers1

1

XmlSerializer doesn't allow for dynamic root names out of the box. The expected root name is fixed, and is defined by the type name, the presence of an [XmlRoot] or [XmlType] attribute, and possibly the XmlSerializer constructor used.

Thus an easy solution is to pre-load the XML into an XDocument, modify the root name, and then deserialize:

XDocument doc;
using (var xmlReader = XmlReader.Create(tempFile))
{
    doc = XDocument.Load(xmlReader);
}

var rootName = doc.Root.Name;
doc.Root.Name = "Bookings";
var deserializer = new XmlSerializer(typeof(Bookings));
Bookings bookings;
using (var xmlReader = doc.CreateReader())
{
    bookings = (Bookings)deserializer.Deserialize(xmlReader);
}
bookings.RootName = rootName.LocalName;

Note the use of CreateReader() to deserialize directly from the loaded XML.

Another option would be to create a custom XmlSerializer for each file using new XmlSerializer(typeof(Bookings), new XmlRootAttribute { ElementName = rootName }), however there are two issues with this:

  • Each serializer so created must be cached in a hash table to prevent a severe memory leak (loading of duplicate dynamically created assemblies), as is explained in this answer and also the documentation.

  • But even if you do this, if you are reading many different files with many different root names, your memory use will grow rapidly and continually due to constant loading of new unique dynamically created XmlSerializer assemblies.

However, if you only had to load one single file, you could do something like this:

Bookings bookings = null;
using (var xmlReader = XmlReader.Create(tempFile))
{
    while (xmlReader.Read())
    {
        if (xmlReader.NodeType == XmlNodeType.Element)
        {
            var rootName = xmlReader.LocalName;
            var deserializer = new XmlSerializer(typeof(Bookings), new XmlRootAttribute { ElementName = rootName });
            bookings = (Bookings)deserializer.Deserialize(xmlReader);
            bookings.RootName = rootName;
            break;
        }
    }
}

If both solutions require too much memory, you could consider creating a subclassed XmlTextReader that allows for the the root element to be renamed on the fly while reading.

In both cases I modified Bookings to look as follows:

public class Bookings
{
    [XmlIgnore]
    public string RootName { get; set; }

    [XmlAttribute("company")]
    public string Company { get; set; }

    [XmlElement("row")]
    public List<Booking> StudentBookings { get; set; }
}
dbc
  • 104,963
  • 20
  • 228
  • 340
  • I have never understood why there is no "don't bother with namespaces" option somewhere. 99% of the time namespaces are just noise. – Christian Sauer Feb 20 '17 at 15:42
  • @ChristianSauer - yes, same as namespaces in e.g. C#. Just noise. Not at all that you would want to cut down on the possibility of conflicts or that you may have to interoperate across multiple independent vendors domain specific languages within the same single file. – Damien_The_Unbeliever Feb 20 '17 at 18:56
  • @Damien_The_Unbeliever I am working mostly against a single XML doc with a single namespace. And that's a case where using namespaces is just plain additional work with no benefit. – Christian Sauer Feb 21 '17 at 06:44
  • @dbc Thank you ever so much for the detailed answer. I followed your first set of code and implemented and all is working as expected now. Superb explanation. – Chancerson Feb 21 '17 at 13:42