Alex
Alex

Reputation: 1420

C# Retention for XML File

I am trying create a function that takes in x retention days, which as a result compares the "date" tags in my XML file, and deletes entries accordingly.

<?xml version="1.0" encoding="utf-8"?>
<root>
  <OFBM time="13:17" date="06.10.2017" saveName="Unnamed save">
    <folder name="file:///C:/Users/AD/Downloads" />
    <folder name="file:///C:/Users/AD/Desktop/t" />
    <folder name="file:///C:/Users/AD/Dropbox/Development/DelOldX/DelOldX" />
  </OFBM>
  <OFBM time="13:17" date="31.08.2017" saveName="Unnamed save">
    <folder name="file:///C:/Users/AD/Downloads" />
    <folder name="file:///C:/Users/AD/Desktop/t" />
    <folder name="file:///C:/Users/AD/Dropbox/Development/DelOldX/DelOldX" />
  </OFBM>
  <OFBM time="13:17" date="31.08.2017" saveName="Unnamed save">
    <folder name="file:///C:/Users/AD/Downloads" />
    <folder name="file:///C:/Users/AD/Desktop/t" />
    <folder name="file:///C:/Users/AD/Dropbox/Development/DelOldX/DelOldX" />
  </OFBM>
</root>

For example my retentionDays value is 6, today is 6th of October (06.10), so everything before 1st of October should be deleted. I wrote up a function that does this, however it deletes the date attribute, not the whole element

My function:

        public void CleanXML()
        {
        int days = Int32.Parse(tbRetentionDays.Text);
        DateTime minDate = DateTime.Now.AddDays(-days);
        var root = XElement.Load(pathToXml);
        foreach (XElement el in root.Elements("OFBM"))
        {

            foreach (XAttribute el2 in el.Attributes("date"))
            {
                string rawDate = el2.Value;
                DateTime xmlDate = Convert.ToDateTime(rawDate);

                if (xmlDate < minDate)
                {
                    Console.WriteLine(xmlDate + " lower than " + minDate + " Retention: " + days);
                    el.Remove();
                }
            } 
        }
        root.Save(pathToXml);
    }

Upvotes: 1

Views: 189

Answers (3)

rene
rene

Reputation: 42444

When you call el.Remove(); you're modifying the collection of elements you're iterating over. That causes that not all elements are visited and therefor removed. One approach could be to store the elements that needs to be deleted first and once you've completed that, remove each individual element.

Your code needs to be adapted like so:

public void CleanXML(string daysText)
{
   int days = Int32.Parse(daysText);
   DateTime minDate = DateTime.Now.AddDays(-days);
   var root = XElement.Load(pathToXml);
   // keep list of items to be removed
   var remove = new List<XElement>();
   foreach (XElement el in root.Elements("OFBM"))
   {
       foreach (XAttribute el2 in el.Attributes("date"))
       {
           string rawDate = el2.Value;
           DateTime xmlDate = Convert.ToDateTime(rawDate);

           if (xmlDate < minDate)
           {
               Console.WriteLine(xmlDate + " lower than " + minDate + " Retention: " + days);
               // keep a reference to this element
               remove.Add(el);
           }
       } 
    }
    // remove individual elements
    foreach(var element in remove)
    {
      element.Remove();
    }
    root.Save(pathToXml);
}

If you don't want to have that explicit list you can rewrite your Linq query a bit so you obtain the list of elements to be remove and materialize that list. The main iterator would look like this in that case:

foreach (XElement el in root
    .Elements("OFBM")
    .Where(elem => elem.Attribute("date") != null
                && Convert.ToDateTime(elem.Attribute("date").Value) < minDate
    ).ToList()) // the ToList is mandatory here
    {
       el.Remove();
    }

Upvotes: 1

Sergey Berezovskiy
Sergey Berezovskiy

Reputation: 236228

You should check if days text can be parsed into integer

int retentionInDays;
if (!Int32.TryParse(daysText, out retentionInDays))
   return; // log, throw etc

Thus you don't check time attribute in xml, then you can use DateTime.Today. But I would recommend you to check both date and time in your xml file.

var minDate = DateTime.Today.AddDays(-retentionInDays);
var xdoc = XDocument.Load(pathToXml);
var provider = CultureInfo.InvariantCulture   

Next you just select expired nodes and remove them all. As @rene noticed, if you remove node during enumeration in foreach, then enumeration finishes (though I wonder why we don't have something like CollectionModified exception here). Note that you should parse date manually because xml date string does not have default format - by default month goes before day and "06.10.2017" will be converted to June 10th instead of October 6th. "31.08.2017" will produce FormatException. So, use ParseExact here.

var expiredEntries = 
   from e in xdoc.Root.Elements("OFBM")
   let date = DateTime.ParseExact((string)e.Attribute("date"), @"dd\.MM\.yyyy", provider)
   where date < minDate
   select e;

expiredEntries.Remove(); // removes all selected nodes
xdoc.Save(pathToXml);

If you need to log all nodes before removing them, you can enumerate expired entries.

Upvotes: 0

LukeChastain
LukeChastain

Reputation: 94

It looks like there is a localization issue where its reading the first number as month instead of day. Its reading 06.10.2017 as June 10th. Try using DateTime.ParseExact(rawDate, "dd.MM.yyyy") instead. That way, you won't have to worry about the region settings of the machine your code is running on.

Upvotes: 0

Related Questions