Search code examples
c#csvcsvhelper

Using CSVHelper how to deserialise an CSV with a list of Sub Item


Question:

Given to object FooBar that contains a List of Bar, with FooBar and Bar define as such:

class FooBar{
    int FooID {get;set;}
    string FooProperty1 {get;set;}
    List<Bar> Bars {get;set;};
}

class Bar{
    int BarID {get;set;}    
    string BarProperty1 {get;set;}  
    string BarProperty2 {get;set;}  
    string BarProperty3 {get;set;}
}

I get the following CSV as input:

1,FooProperty1,BarID_1,BarProperty1_1,BarProperty2_1,BarProperty3_1,BarID_2,BarProperty1_2,BarProperty2_2,BarProperty3_2

Where the field BarID, BarProperty1, BarProperty2, BarProperty3 are suffixed by their indice in the collection.

How do I deserialise this input into my object?

enter image description here


input Exemple:

1 instance of FooBar, and 2 sub Bar: 1,FooProperty1,BarID_1,BarProperty1_1,BarProperty2_1,BarProperty3_1,BarID_2,BarProperty1_2,BarProperty2_2,BarProperty3_2

1 instance of FooBar but no Bar:
1,FooProperty1


Tries:

I have try using Convert in order to map those property to a new instance of Bar like :

public class FooBarMap : ClassMap<FooBar> 
{
    public FooBarMap()
    {
        Map(m => m.FooID);
        Map(m => m.Bars).ConvertUsing(row =>
        {            
            var list = new List<Bar>
            {
                new Bar { 
                    BarProperty1 = row.GetField("BarProperty1_1"), 
                    BarProperty2 = row.GetField("BarProperty2_1"),
                    // .. Other Properties
                },
                new Bar {}, //.. Same on _2
            };
            return list;
        });
    }
}

Of course no control over the input. I would have been sending Json/Xml not CSV.


Solution

  • Its possible with a custom type converter but tricky.

    You need to decorate the property with an Index attribute (even though it's not used)

    public class FooBar
    {
        [Index(2)]
        public List<Bar> Bars { get; set; }
    }
    

    The converter is used for both reading and writing, so you need to override two methods:

      public class BarListConverter : DefaultTypeConverter
      {
        public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
        {
          var list = new List<Bar>();
          if (text == null) return list;
          do
          {
            var barIndex = list.Count + 1;
            var bar = new Bar
            {
              BarID = row.GetField<int>($"BarID_{barIndex}"),
              BarProperty1 = row.GetField<string>($"BarProperty1_{barIndex}"),
              BarProperty2 = row.GetField<string>($"BarProperty2_{barIndex}"),
              BarProperty3 = row.GetField<string>($"BarProperty3_{barIndex}")
            };
            list.Add(bar);
          } while (row.Context.CurrentIndex > 0 && row.Context.CurrentIndex < row.Context.Record.Length - 1);
          return list;
        }
    
        public override string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData)
        {
          var bars = value as List<Bar>;
          if (bars == null) return null;
          foreach (var bar in bars)
          {
            row.WriteField(bar.BarID);
            row.WriteField(bar.BarProperty1);
            row.WriteField(bar.BarProperty2);
            row.WriteField(bar.BarProperty3);
          }
          return null;
        }
      }
    }
    

    Reading:

      public List<FooBar> Reading()
      {
        using (var stream = new MemoryStream())
        using (var writer = new StreamWriter(stream))
        using (var reader = new StreamReader(stream))
        using (var csv = new CsvReader(reader))
        {
          writer.WriteLine(
            "FooID,FooProperty1,BarID_1,BarProperty1_1,BarProperty2_1,BarProperty3_1,BarID_2,BarProperty1_2,BarProperty2_2,BarProperty3_2");
          writer.WriteLine("1,Foo1,1,2,3,4,5,6,7,8");
          writer.Flush();
          stream.Position = 0;
    
          csv.Configuration.HeaderValidated = null;
          csv.Configuration.MissingFieldFound = null;
          csv.Configuration.TypeConverterCache.AddConverter<List<Bar>>(new BarListConverter());
    
          return csv.GetRecords<FooBar>().ToList();
        }
      }
    

    Writing:

      public string Writing(List<FooBar> data)
      {
        using (var stream = new MemoryStream())
        using (var writer = new StreamWriter(stream))
        using (var reader = new StreamReader(stream))
        using (var csv = new CsvWriter(writer))
        {
          csv.Configuration.HasHeaderRecord = false;
          csv.Configuration.TypeConverterCache.AddConverter<List<Bar>>(new BarListConverter());
          csv.WriteRecords(data);
          writer.Flush();
          stream.Position = 0;
    
          return reader.ReadToEnd();
        }