0

I am trying to produce the following XML using the class structure below.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<VirtualDesktopManager>
  <Categories>
    <Category Name="Category 1">
      <Desktops>
        <Desktop Name="Desktop 1">
          <Applications Name="Application 1" />
          <Applications Name="Application 2" />
        </Desktop>
      </Desktops>
    </Category>
  </Categories>
</VirtualDesktopManager>

When executing the code below, I get the exception: System.ArgumentException: 'Cannot insert a node or any ancestor of that node as a child of itself.'. The classes themselves do not have any circular references so I must be doing something wrong.

private static void Main ()
{
    var database = new Database();
    var category = new VirtualDesktopCategory();
    var desktop = new VirtualDesktop();
    var application = new VirtualDesktopApplication();

    category = new VirtualDesktopCategory() { Name = "Cat 1", };
    database.Categories.Add(category);
    desktop = new VirtualDesktop() { Name = "Desktop 1", };
    category.Desktops.Add(desktop);
    application = new VirtualDesktopApplication() { Name = "Application 1", };
    desktop.Applications.Add(application);
    application = new VirtualDesktopApplication() { Name = "Application 2", };
    desktop.Applications.Add(application);

    database.ToXmlDocument().InnerText.Dump();
}

public class Database
{
    public string Name { get; set; } = "";
    public List<VirtualDesktopCategory> Categories { get; private set; } = new();

    public XmlDocument ToXmlDocument()
    {
        var document = new XmlDocument();
        var xml = $@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes"" ?><VirtualDesktopManager></VirtualDesktopManager>";

        document.LoadXml(xml);
        document.DocumentElement?.AppendChild(this.ToXmlElement(document, document.DocumentElement));

        return document;
    }

    public XmlElement ToXmlElement(XmlDocument document, XmlElement elementParent)
    {
        var elementCategories = document.CreateElement("Categories");

        elementParent.AppendChild(elementCategories);

        foreach (var category in this.Categories)
        {
            // System.ArgumentException: Cannot insert a node or any ancestor of that node as a child of itself.
            elementCategories.AppendChild(category.ToXmlElement(document, elementCategories));
        }

        return elementCategories;
    }
}

public class VirtualDesktopCategory
{
    public string Name { get; set; } = "";
    public List<VirtualDesktop> Desktops { get; private set; } = new();

    public XmlElement ToXmlElement(XmlDocument document, XmlElement elementParent)
    {
        var elementCategory = document.CreateElement("Category");
        var elementDesktops = document.CreateElement("Desktops");

        elementCategory.AppendAttribute(document, nameof(this.Name), this.Name);

        elementParent.AppendChild(elementCategory);
        elementCategory.AppendChild(elementDesktops);

        foreach (var desktop in this.Desktops)
        {
            elementDesktops.AppendChild(desktop.ToXmlElement(document, elementDesktops));
        }

        return elementCategory;
    }
}

public class VirtualDesktop
{
    public string Name { get; set; } = "";
    public List<VirtualDesktopApplication> Applications { get; private set; } = new();

    public XmlElement ToXmlElement(XmlDocument document, XmlElement elementParent)
    {
        var elementDesktop = document.CreateElement("Desktop");
        var elementApplications = document.CreateElement("Applications");

        elementDesktop.AppendAttribute(document, nameof(this.Name), this.Name);

        elementParent.AppendChild(elementDesktop);
        elementDesktop.AppendChild(elementApplications);

        foreach (var application in this.Applications)
        {
            elementApplications.AppendChild(application.ToXmlElement(document, elementApplications));
        }

        return elementParent;
    }
}

public class VirtualDesktopApplication
{
    public string Name { get; set; } = "";

    public XmlElement ToXmlElement(XmlDocument document, XmlElement elementParent)
    {
        var elementApplication = document.CreateElement("Application");

        elementApplication.AppendAttribute(document, nameof(this.Name), this.Name);

        elementParent.AppendChild(elementApplication);

        return elementApplication;
    }
}

public static class Extensions
{
    public static XmlAttribute AppendAttribute(this XmlElement element, XmlDocument document, string name, string value)
    {
        var attribute = document.CreateAttribute(name);

        attribute.Value = value;
        element.Attributes.Append(attribute);

        return attribute;
    }
}

Any pointers would be appreciated.

Raheel Khan
  • 14,205
  • 13
  • 80
  • 168
  • The following may be of interest: https://stackoverflow.com/a/73640395/10024425 – Tu deschizi eu inchid Aug 14 '23 at 15:30
  • 1
    Not particularly related, but any reason you're not using XmlSerializer? That would remove most of the boilerplate you have there – canton7 Aug 14 '23 at 15:30
  • @canton7: Yes, this is just sample code but I need finer control over what gets written versus what gets read so attribute-based serialization is not a good fit for my case. – Raheel Khan Aug 14 '23 at 15:32

2 Answers2

2

Additionally to canton7's answer, look at how you handle the returned node of (some of) your ToXmlElement methods.

Inside those ToXmlElement methods, you add the node to be returned to the given parent element. Then, outside of these methods in the respective foreach loops, you add that node again to the parent element once more.

  • You're correct, but that doesn't seem to have any adverse effects on the generated XML, interestingly. – canton7 Aug 14 '23 at 15:44
  • 2
    @canton7, true. I just checked the documentation for `AppendChild`, and it states "_If the newChild is already in the tree, it is removed from its original position and added to its target position._". So technically this has no adverse consequences as the node is added to the parent, then removed from it and then added to it again without other nodes being added/removed to the same parent inbetween. Still, this shows a design issue with the code in the question. I ponder now whether i should remove my answer or let it stay even though it doesn't really address the question... – EyesShriveledToRaisins Aug 14 '23 at 15:49
  • I say leave it -- it adds useful info, and I've upvoted it – canton7 Aug 14 '23 at 15:51
  • Thanks @EyesShriveledToRaisins. I say leave it and have upvoted it too. – Raheel Khan Aug 14 '23 at 15:57
1
public class VirtualDesktop
{
    public string Name { get; set; } = "";
    public List<VirtualDesktopApplication> Applications { get; private set; } = new();

    public XmlElement ToXmlElement(XmlDocument document, XmlElement elementParent)
    {
        var elementDesktop = document.CreateElement("Desktop");
        var elementApplications = document.CreateElement("Applications");

        elementDesktop.AppendAttribute(document, nameof(this.Name), this.Name);

        elementParent.AppendChild(elementDesktop);
        elementDesktop.AppendChild(elementApplications);

        foreach (var application in this.Applications)
        {
            elementApplications.AppendChild(application.ToXmlElement(document, elementApplications));
        }

        return elementParent;
    }
}

You're returning elementParent, rather than elementDesktop.

You had mis-identified the line throwing the exception. It was actually:

elementDesktops.AppendChild(desktop.ToXmlElement(document, elementDesktops));

So, something in desktop.ToXmlElement was the cause. A quick bit of trial-and-error shows that commenting out everything in VirtualDesktop.ToXmlElement doesn't fix it, so it's somewhere between the var elementDesktop = document.CreateElement("Desktop") and return elementParent;... Wait...


You're also serializing the XmlDocument incorrectly. You need to do something like:

using var sr = new StringWriter();
using (var writer = XmlWriter.Create(sr))
{
    database.ToXmlDocument().Save(writer);
}
Console.WriteLine(sr.ToString());

However, this will give you:

<?xml version="1.0" encoding="utf-16" standalone="yes"?>

To fix the encoding, you'll need to do something like this: https://stackoverflow.com/a/1564727/1086121


Working example, with the duplicate lines identified by @EyesShriveledToRaisins commented out.

canton7
  • 37,633
  • 3
  • 64
  • 77
  • I knew it had to be a silly mistake. Indeed, the return of `elementParent` as you pointed out needs to be `elementDesktop`. Thank you! – Raheel Khan Aug 14 '23 at 15:55
  • @RaheelKhan, FWIW producing a *minimal* reproducible sample (as in [mcve]), by removing code until all that was left was the code to produce the exception, would have made the problem very obvious. I effectively did the same thing by commenting out lines, and found the problem in under a minute. – canton7 Aug 14 '23 at 15:58