1

There is an error in XML document (8, 20). Inner 1: Unexpected XML declaration. The XML declaration must be the first node in the document, and no white space characters are allowed to appear before it.

OK, I understand this error.

How I get it, however, is what perplexes me.

I create the document with Microsoft's Serialize tool. Then, I turn around and attempt to read it back, again, using Microsoft's Deserialize tool.

I am not in control of writing the XML file in the correct format - that I can see.

Here is the single routine I use to read and write.

private string xmlPath = System.Web.Hosting.HostingEnvironment.MapPath(WebConfigurationManager.AppSettings["DATA_XML"]);
private object objLock = new Object();
public string ErrorMessage { get; set; }

public StoredMsgs Operation(string from, string message, FileAccess access) {
    StoredMsgs list = null;
    lock (objLock) {
        ErrorMessage = null;
        try {
            if (!File.Exists(xmlPath)) {
                var root = new XmlRootAttribute(rootName);
                var serializer = new XmlSerializer(typeof(StoredMsgs), root);
                if (String.IsNullOrEmpty(message)) {
                    from = "Code Window";
                    message = "Created File";
                }
                var item = new StoredMsg() {
                    From = from,
                    Date = DateTime.Now.ToString("s"),
                    Message = message
                };
                using (var stream = File.Create(xmlPath)) {
                    list = new StoredMsgs();
                    list.Add(item);
                    serializer.Serialize(stream, list);
                }
            } else {
                var root = new XmlRootAttribute("MessageHistory");
                var serializer = new XmlSerializer(typeof(StoredMsgs), root);
                var item = new StoredMsg() {
                    From = from,
                    Date = DateTime.Now.ToString("s"),
                    Message = message
                };
                using (var stream = File.Open(xmlPath, FileMode.Open, FileAccess.ReadWrite)) {
                    list = (StoredMsgs)serializer.Deserialize(stream);
                    if ((access == FileAccess.ReadWrite) || (access == FileAccess.Write)) {
                        list.Add(item);
                        serializer.Serialize(stream, list);
                    }
                }
            }
        } catch (Exception error) {
            var sb = new StringBuilder();
            int index = 0;
            sb.AppendLine(String.Format("Top Level Error: <b>{0}</b>", error.Message));
            var err = error.InnerException;
            while (err != null) {
                index++;
                sb.AppendLine(String.Format("\tInner {0}: {1}", index, err.Message));
                err = err.InnerException;
            }
            ErrorMessage = sb.ToString();
        }
    }
    return list;
}

Is something wrong with my routine? If Microsoft write the file, it seems to me that it should be able to read it back.

It should be generic enough for anyone to use.

Here is my StoredMsg class:

[Serializable()]
[XmlType("StoredMessage")]
public class StoredMessage {
    public StoredMessage() {
    }
    [XmlElement("From")]
    public string From { get; set; }
    [XmlElement("Date")]
    public string Date { get; set; }
    [XmlElement("Message")]
    public string Message { get; set; }
}

[Serializable()]
[XmlRoot("MessageHistory")]
public class MessageHistory : List<StoredMessage> {
}

The file it generates doesn't look to me like it has any issues.

xml screenshot

I saw the solution here:

Error: The XML declaration must be the first node in the document

But, in that case, it seems someone already had an XML document they wanted to read. They just had to fix it.

I have an XML document created my Microsoft, so it should be read back in by Microsoft.

Community
  • 1
  • 1
  • An XML document can have at most one declaration (at the start) and can have at most one root element. – user2864740 Aug 24 '15 at 23:43
  • 1
    The problem is that you are *adding* to the file. You deserialize, then re-serialize to the same stream *without rewinding and resizing to zero*. This gives you multiple [root elements](https://en.wikipedia.org/wiki/Root_element), which is disallowed by the XML standard. – dbc Aug 24 '15 at 23:59
  • See here for a ways to handle documents with multiple root elements: [XML Error: There are multiple root elements](https://stackoverflow.com/questions/5042902/xml-error-there-are-multiple-root-elements) You also need to [omit the XML declaration](http://stackoverflow.com/questions/6833538/how-to-create-an-xml-using-xml-writer-without-declaration-element). – dbc Aug 25 '15 at 00:02

1 Answers1

2

The problem is that you are adding to the file. You deserialize, then re-serialize to the same stream without rewinding and resizing to zero. This gives you multiple root elements:

<?xml version="1.0"?>
<StoredMessage>
</StoredMessage
<?xml version="1.0"?>
<StoredMessage>
</StoredMessage

Multiple root elements, and multiple XML declarations, are invalid according to the XML standard, thus the .NET XML parser throws an exception in this situation by default.

For possible solutions, see XML Error: There are multiple root elements, which suggests you either:

  1. Enclose your list of StoredMessage elements in some synthetic outer element, e.g. StoredMessageList.

    This would require you to load the list of messages from the file, add the new message, and then truncate the file and re-serialize the entire list when adding a single item. Thus the performance may be worse than in your current approach, but the XML will be valid.

  2. When deserializing a file containing concatenated root elements, create an XML writer using XmlReaderSettings.ConformanceLevel = ConformanceLevel.Fragment and iteratively walk through the concatenated root node(s) and deserialize each one individually as shown, e.g., here. Using ConformanceLevel.Fragment allows the reader to parse streams with multiple root elements (although multiple XML declarations will still cause an error to be thrown).

    Later, when adding a new element to the end of the file using XmlSerializer, seek to the end of the file and serialize using an XML writer returned from XmlWriter.Create(TextWriter, XmlWriterSettings) with XmlWriterSettings.OmitXmlDeclaration = true. This prevents output of multiple XML declarations as explained here.

For option #2, your Operation would look something like the following:

private string xmlPath = System.Web.Hosting.HostingEnvironment.MapPath(WebConfigurationManager.AppSettings["DATA_XML"]);
private object objLock = new Object();
public string ErrorMessage { get; set; }

const string rootName = "MessageHistory";
static readonly XmlSerializer serializer = new XmlSerializer(typeof(StoredMessage), new XmlRootAttribute(rootName));

public MessageHistory Operation(string from, string message, FileAccess access)
{
    var list = new MessageHistory();
    lock (objLock)
    {
        ErrorMessage = null;
        try
        {
            using (var file = File.Open(xmlPath, FileMode.OpenOrCreate))
            {
                list.AddRange(XmlSerializerHelper.ReadObjects<StoredMessage>(file, false, serializer));
                if (list.Count == 0 && String.IsNullOrEmpty(message))
                {
                    from = "Code Window";
                    message = "Created File";
                }
                var item = new StoredMessage()
                {
                    From = from,
                    Date = DateTime.Now.ToString("s"),
                    Message = message
                };
                if ((access == FileAccess.ReadWrite) || (access == FileAccess.Write))
                {
                    file.Seek(0, SeekOrigin.End);
                    var writerSettings = new XmlWriterSettings
                    {
                        OmitXmlDeclaration = true,
                        Indent = true, // Optional; remove if compact XML is desired.
                    };
                    using (var textWriter = new StreamWriter(file))
                    {
                        if (list.Count > 0)
                            textWriter.WriteLine();
                        using (var xmlWriter = XmlWriter.Create(textWriter, writerSettings))
                        {
                            serializer.Serialize(xmlWriter, item);
                        }
                    }
                }
                list.Add(item);
            }
        }
        catch (Exception error)
        {
            var sb = new StringBuilder();
            int index = 0;
            sb.AppendLine(String.Format("Top Level Error: <b>{0}</b>", error.Message));
            var err = error.InnerException;
            while (err != null)
            {
                index++;
                sb.AppendLine(String.Format("\tInner {0}: {1}", index, err.Message));
                err = err.InnerException;
            }
            ErrorMessage = sb.ToString();
        }
    }
    return list;
}

Using the following extension method adapted from Read nodes of a xml file in C#:

public partial class XmlSerializerHelper
{
    public static List<T> ReadObjects<T>(Stream stream, bool closeInput = true, XmlSerializer serializer = null)
    {
        var list = new List<T>();

        serializer = serializer ?? new XmlSerializer(typeof(T));
        var settings = new XmlReaderSettings
        {
            ConformanceLevel = ConformanceLevel.Fragment,
            CloseInput = closeInput,
        };
        using (var xmlTextReader = XmlReader.Create(stream, settings))
        {
            while (xmlTextReader.Read())
            {   // Skip whitespace
                if (xmlTextReader.NodeType == XmlNodeType.Element)
                {
                    using (var subReader = xmlTextReader.ReadSubtree())
                    {
                        var logEvent = (T)serializer.Deserialize(subReader);
                        list.Add(logEvent);
                    }
                }
            }
        }

        return list;
    }    
}

Note that if you are going to create an XmlSerializer using a custom XmlRootAttribute, you must cache the serializer to avoid a memory leak.

Sample fiddle.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • I've been looking at that linked example. Is there something I'm missing? What writes the closing XML piece to the root node? Does **Serialize** not write the closing root node? –  Aug 25 '15 at 01:11
  • It looks like what I may wind up doing is ditching Microsoft's tool in favor of reading and writing the nodes manually. Their solution seemed like a good idea, but it doesn't appear it will work for what I want to use it for. –  Aug 25 '15 at 03:01
  • If you set ConformanceLevel to Fragment on the XmlWriter then Serialize fails with: InvalidOperationException: WriteStartDocument cannot be called on writers created with ConformanceLevel.Fragment. – poizan42 Nov 01 '17 at 15:56
  • @poizan42 - been a while since I answered this. If you're looking to serialize multiple root objects into a single concatenated file, take a look at the more recent [Consecutive XML serialization causes - Token StartElement in state EndRootElement would result in an invalid XML document](https://stackoverflow.com/q/44008900/3744182), which has a [working fiddle](https://dotnetfiddle.net/aEsug5). – dbc Nov 01 '17 at 17:08
  • That doesn't change the fact that you can't use Serialize with a standard XmlTextWriter with ConformanceLevel set to Fragment. Note that the example in your link specifically does not do that, even though it sets OmitXmlDeclaration to true which has no effect in that case. – poizan42 Nov 01 '17 at 17:22
  • The only way to do it is either subclassing the writer as shown here: https://stackoverflow.com/a/6833634/1555496, or subclassing the reader and setting eof to true when encountering the closing element of the root element, see https://gist.github.com/poizan42/bde0e3c4e235791aadcaeb92adc1bfa9 – poizan42 Nov 01 '17 at 17:27
  • @poizan42 - answer updated. You're correct that using `XmlWriterSettings.ConformanceLevel = ConformanceLevel.Fragment` is wrong, so I removed that. However [`XmlTextWriter`](https://msdn.microsoft.com/en-us/library/system.xml.xmltextwriter(v=vs.110).aspx) and [`XmlTextReader`](https://msdn.microsoft.com/en-us/library/system.xml.xmltextreader.aspx) are both deprecated according to their docs, so I updated the answer with code showing how to accomplish OP's requirement with `XmlReader.Create()` and `XmlWriter.Create()`. – dbc Nov 01 '17 at 19:24