Linq-to-XML XElement.Remove() leaves unwanted whitespace

Viewed 6693

I have an XDocument that I create from a byte array (received over tcp/ip).

I then search for specific xml nodes (XElements) and after retrieving the value 'pop' it off of the Xdocument by calling XElement.Remove(). After all of my parsing is complete, I want to be able to log the xml that I did not parse (the remaining xml in the XDocument). The problem is that there is extra whitespace that remains when XElement.Remove() is called. I want to know the best way to remove this extra whitespace while preserving the rest of the format in the remaining xml.

Example/Sample Code

If I receive the following xml over the socket:

<?xml version="1.0"?>
<catalog>
   <book id="bk101">
      <author>Gambardella, Matthew</author>
      <title>XML Developer's Guide</title>
      <genre>Computer</genre>
      <price>44.95</price>
      <publish_date>2000-10-01</publish_date>
      <description>An in-depth look at creating applications with XML.</description>
   </book>
</catalog>

And I use the following code to parse this xml and remove a number of the XElements:

private void socket_messageReceived(object sender, MessageReceivedEventArgs e)
{
     XDocument xDoc;
     try
     {
         using (MemoryStream xmlStream = new MemoryStream(e.XmlAsBytes))
         using (XmlTextReader reader = new XmlTextReader(xmlStream))
         {
             xDoc = XDocument.Load(reader);
         }

         XElement Author = xDoc.Root.Descendants("author").FirstOrDefault();
         XElement Title  = xDoc.Root.Descendants("title").FirstOrDefault();
         XElement Genre  = xDoc.Root.Descendants("genre").FirstOrDefault();

         // Do something with Author, Title, and Genre here...

         if (Author != null) Author.Remove();
         if (Title  != null) Title.Remove();
         if (Genre  != null) Genre.Remove();

         LogUnparsedXML(xDoc.ToString());

     }
     catch (Exception ex)
     {
         // Exception Handling here...
     }
}

Then the resulting string of xml sent to the LogUnparsedXML message would be:

<?xml version="1.0"?>
<catalog>
   <book id="bk101">



      <price>44.95</price>
      <publish_date>2000-10-01</publish_date>
      <description>An in-depth look at creating applications with XML.</description>
   </book>
</catalog>

In this contrived example it may not seem like a big deal, but in my actual application the leftover xml looks pretty sloppy. I have tried using the XDocument.ToString overload that takes a SaveOptions enum to no avail. I have also tried to call xDoc.Save to save out to a file using the SaveOptions enum. I did try experimenting with a few different linq queries that used XElement.Nodes().OfType<XText>() to try to remove the whitespace, but often I ended up taking the whitespace that I wish to preserve along with the whitespace that I am trying to get rid of.

Thanks in advance for assistance.

Joe

3 Answers

I have a simpler solution than the accepted answer that works for my case and appears to work for yours too. Perhaps there are some more complicated cases it will not work for though, I'm not sure.

Here is the code:

public static void RemoveWithNextWhitespace(this XElement element)
{
    if (element.PreviousNode is XText textNode)
    {
        textNode.Remove();
    }

    element
    .Remove();
}

Here is my LINQPad query with your use case:

void Main()
{
    var xDoc = XDocument.Parse(@"<?xml version=""1.0""?>
<catalog>
   <book id=""bk101"">
      <author>Gambardella, Matthew</author>
      <title>XML Developer's Guide</title>
      <genre>Computer</genre>
      <price>44.95</price>
      <publish_date>2000-10-01</publish_date>
      <description>An in-depth look at creating applications with XML.</description>
   </book>
</catalog>", LoadOptions.PreserveWhitespace);

    XElement Author = xDoc.Root.Descendants("author").FirstOrDefault();
    XElement Title = xDoc.Root.Descendants("title").FirstOrDefault();
    XElement Genre = xDoc.Root.Descendants("genre").FirstOrDefault();

    // Do something with Author, Title, and Genre here...

    if (Author != null) Author.RemoveWithNextWhitespace();
    if (Title != null) Title.RemoveWithNextWhitespace();
    if (Genre != null) Genre.RemoveWithNextWhitespace();

    xDoc.ToString().Dump();
}

static class Ext
{
    public static void RemoveWithNextWhitespace(this XElement element)
    {
        if (element.PreviousNode is XText textNode)
        {
            textNode.Remove();
        }

        element
        .Remove();
    }
}

The main reason why I didn't just use the accepted answer myself was because it did not leave my XML properly formatted in some cases. e.g. in your use case if I removed the "description" element it would leave something that looked like this:

<catalog>
   <book id="bk101">
      <genre>Computer</genre>
      <price>44.95</price>
      <publish_date>2000-10-01</publish_date>
         </book>
</catalog>

Reading xml via an XmlReader will preserve whitespace by default, including insignificant whitespace as you see here.

You should read it in ignoring whitespace by setting the appropriate xml reader setting:

using (var reader = XmlReader.Create(xmlStream, new XmlReaderSettings { IgnoreWhitespace = true }))

Note this doesn't remove significant whitespace (such as those in mixed content or in a scope preserving whitespace) so your formatting will remain.

Related