Search code examples
c#linq-to-xml

Adding a new entry with Linq to XML at the right position (based on numerical attribute)


I have come up with this function to save or update an entry in the XML file.

public void SetOutlineInfo(string talksdatabase, int talknumber, string languageCode, bool talkExcluded, DateTime dateExcudedFrom, string excludedNote)
{
    try
    {
        XDocument doc = XDocument.Load(talksdatabase);
        var ptLang = doc.Descendants(languageCode).FirstOrDefault();
        if (ptLang != null)
        {
            var ptInfo = ptLang
                            .Descendants("PublicTalk")
                            .Where(p => p.Attribute("Number").Value == talknumber.ToString()).FirstOrDefault();
            if (ptInfo != null)
            {
                ptInfo.Attribute("Excluded").Value = talkExcluded ? "true" : "false";
                ptInfo.Attribute("ExcludedFromDate").Value = dateExcudedFrom.ToString("yyyy-MM-dd");
                ptInfo.Attribute("ExcludedNote").Value = excludedNote;
            }
            else
            {
                ptLang.Add(new XElement("PublicTalk",
                    new XAttribute("Number", talknumber),
                    new XAttribute("Excluded", talkExcluded),
                    new XAttribute("ExcludedFromDate", dateExcudedFrom.ToString("yyyy-MM-dd")),
                    new XAttribute("ExcludedNote", excludedNote)
                    ));
            }

            doc.Save(talksdatabase);
        }
    }
    catch (Exception ex)
    {
        SimpleLog.Log(ex);
    }
}

The only issue is that adding a new entry does not add it in the correct numerical position (the Number attribute).

I did some searching and they all show reading in records and sorting with orderby. But I am writing records.


As requested in the comments.

Here is an XML:

<?xml version="1.0" encoding="utf-8"?>
<PublicTalkTitles>
  <eng>
    <PublicTalk Number="47" Excluded="true" ExcludedFromDate="2019-04-01" ExcludedNote="S-147 19.03" />
  </eng>
</PublicTalkTitles>

When a add a new entry, say talk number 1:

<?xml version="1.0" encoding="utf-8"?>
<PublicTalkTitles>
  <eng>
    <PublicTalk Number="1" Excluded="true" ExcludedFromDate="2023-09-12" ExcludedNote="1" />
    <PublicTalk Number="47" Excluded="true" ExcludedFromDate="2019-04-01" ExcludedNote="S-147 19.03" />
  </eng>
</PublicTalkTitles>

Solution

  • You need to first filter for all nodes which are <= talknumber then take MaxBy that number. You need to parse it to an int in order to do this filtering and sorting correctly.

    Then that node is either:

    • the one you want
      • Change it
    • or the one before it
      • Use AddBeforeSelf
    • or null
      • Use AddFirst on the parent.

        var ptLang = doc.Element("PublicTalkTitles").Element(languageCode);
        if (ptLang != null)
        {
            var pt = ptLang.Elements("PublicTalk");
            var ptInfo = pt
                .Where(p => int.Parse(p.Attribute("Number").Value) <= talknumber)
                .MaxBy(p => int.Parse(p.Attribute("Number").Value));
            if (int.Parse(ptInfo?.Attribute("Number").Value ?? "0") == talknumber)
            {
                ptInfo.Attribute("Excluded").Value = talkExcluded ? "true" : "false";
                ptInfo.Attribute("ExcludedFromDate").Value = dateExcudedFrom.ToString("yyyy-MM-dd");
                ptInfo.Attribute("ExcludedNote").Value = excludedNote;
            }
            else
            {
                var newElem = new XElement("PublicTalk",
                                        new XAttribute("Number", talknumber),
                                        new XAttribute("Excluded", talkExcluded),
                                        new XAttribute("ExcludedFromDate", dateExcudedFrom.ToString("yyyy-MM-dd")),
                                        new XAttribute("ExcludedNote", excludedNote)
                                       );
                if (ptInfo != null)
                    ptInfo.AddAfterSelf(newElem);
                else
                    ptLang.AddFirst(newElem);
            }
        }
    

    dotnetfiddle

    If you don't have MaxBy you can instead use OrderByDescending.FirstOrDefault which is less efficient.

    var ptInfo = pt
        .Where(p => int.Parse(p.Attribute("Number").Value) <= talknumber)
        .OrderByDescending(p => int.Parse(p.Attribute("Number").Value))
        .FirstOrDefault();