Search code examples
c#.net.net-4.8

XmlWriter: write in memory, finalize, write to file, then continue writing in memory


I've been trying to crack the following issue:

My program downloads large files and needs to keep track of them (and their specific versions) using an XML file. Now, downloading large files is always prone to errors, interrupts and similar mishaps, so it would be nice if my program would know that it already updated some files of an update, in case something crashes or the user needs to interrupt the downloads for a while.

This is how I use the XmlWriter (using a memory stream at the moment so the partial XML file does not corrupt the good one if anything crashes):

MemoryStream xmlStream = new MemoryStream();
XmlWriter xmlWriter = XmlWriter.Create(xmlStream, new XmlWriterSettings { Indent = true, Encoding = Encoding.UTF8 });
xmlWriter.WriteStartDocument();
xmlWriter.WriteStartElement("Files");

foreach (XmlNode filenode in filesToDownload) {
    // download happens ...
    // download was successful
    filenode.WriteTo(xmlWriter);
}

// all downloads done, finalize and write to file
xmlWriter.WriteEndElement();
xmlWriter.WriteEndDocument();
xmlWriter.Flush();
xmlWriter.Close();

Now, my problem is, I would like to update (read: write to) the local file after every successful download, but without WriteEndElement() and WriteEndDocument() the resulting file would not be valid XML. Ideally, I would "finalize" the XML, write it and then (somehow?) keep writing to that same XmlWriter.

I have thought about writing the file, then re-reading it, and re-writing all previous nodes to the memory stream after every download, but that hardly seems optimal.


Solution

  • Since after days of research I could not find a reasonable way to achieve this using the XmlWriter class (aside from writing to disk and re-reading the generated file into memory), I now use XDocument.

    I've written a new method to achieve my goal of writing XmlNodes to a file without finalizing an XmlWriter, which I will leave here for reference for the people from the future:

    /// <summary>
    /// Appends or replaces the given node to the root element in the given file, depending on whether a node with that name already exists.
    /// </summary>
    /// <param name="node">XmlNode to write to the file</param>
    /// <param name="xmlfile">Path of the file to write to</param>
    public static void AppendOrChangeNode(XmlNode node, string xmlfile) {
        XDocument doc = XDocument.Load(xmlfile);
        XElement root = doc.Element(XmlNames.ROOT_ELEMENT);
        XElement targetNode = root.Element(node.Name);
    // generate a node based on the elements it should have
    // this is specific to my project and would need to be adapted by you
        XElement nodeToWrite = new XElement(node.Name,
                new XElement("Version", patchnode.SelectNodes("Version")[0].InnerText),
                new XElement("Path", patchnode.SelectNodes("Path")[0].InnerText),
                );
        if (targetNode != null) { // node exists, replace
            targetNode.ReplaceWith(nodeToWrite);
        } else { // node does not exist, append
            root.Add(nodeToWrite);
        }
        doc.Save(xmlfile);
    }
    

    Attention: you may want to re-design your code to stop working with XmlNodes all together, if you're switching to XDocument and XElements anyway. However, working with XmlNodes was - regrettably - a requirement for my project.