Home > Software design >  Read CSV row and map to class with collection of subclass
Read CSV row and map to class with collection of subclass

Time:02-24

I am reading in a CSV file. There are no headers. I need to map it to a class which has a collection of sub objects. I know the amount of objects in the collection.

Public Class Foo{
public int id {get; set;}
public Bar[] bars {get; set;}
public class Bar{
public int id {get; set;}
public string str {get; set;}
}
}

I am trying to accomplish this using CSVHelper I have tried creating a mapper like below. However I just get the following error: CsvHelper.TypeConversion.TypeConverterException: 'The conversion cannot be performed.

   public sealed class Mapper : ClassMap<Foo>
        {
            public Mapper()
            {
                Map(m => m.id).Index(0);
                Map(m => m.bars).Index(1, 2);
            }
        }

It seems the Index overload with 2 parameters is expecting to just convert collections of values as opposed to objects constructed from multiple columns.

My actual code has a collection size of 80, with objects with 5 fields on them so bringing them out onto the base Foo object is not ideal.

I know I can pull out the CSV as a string and string split by lines and commas and iterate through them manually but using a proper CSV library seemed cleaner and less prone to oversights.

I see there is also the option to add a References with a map to it

 References<BarMap>(m => m.Bars);

public sealed class BarMap : ClassMap<Bar>
        {
            public BarMap()
            {
                Map(m => m.id).Index(0);
                Map(m => m.str).Index(1);
            }
        }

But I cannot see how I can appropriately set the Indexes for it. The reference does not allow specifying an index.

CodePudding user response:

I don't think it is possible to automatically map the file to your class, but I've achieved the required result using a DTO class. Considering the data is:

0,0,0,Lorem 
1,0,1,Ipsum
2,1,0,Dolor
3,1,1,Sit
4,1,2,Amet

Running the following code

public static void Main(string[] args)
{
    var config = new CsvConfiguration(CultureInfo.InvariantCulture)
    {
        HasHeaderRecord = false,
    };

    IEnumerable<FoobarDto> records = null;

    using (var reader = new StreamReader("file.csv"))
    using (var csv = new CsvReader(reader, config))
    {
        csv.Context.RegisterClassMap<FoobarDtoMap>();
        records = csv.GetRecords<FoobarDto>().ToList();
    }

    var finalRecords = records.GroupBy(x => x.Id).Select(x => new Foo { Id = x.Key, Bars = x.Select(f => f.Bar).ToArray() });
}

public class FoobarDto
{
    public int Id { get; set; }
    public Foo.Bar Bar { get; set; }
}

public class Foo
{
    public int Id { get; set; }
    public Bar[] Bars { get; set; }
    public class Bar
    {
        public int Id { get; set; }
        public string Str { get; set; }
    }
}

public sealed class FoobarDtoMap : ClassMap<FoobarDto>
{
    public FoobarDtoMap()
    {
        Map(m => m.Id).Index(1);
        Map(m => m.Bar.Id).Index(2);
        Map(m => m.Bar.Str).Index(3);
    }
}

It gives you the proper result.
Please note that there should be a unique column at index 0 for CsvHelper to correctly parse every line of the csv file.

CodePudding user response:

You should be able to use Convert in your mapping to get the bars. I assumed two Bar records, but you can change the for loop to account for different numbers of Bar.

void Main()
{
    var input = "1,1,Dolor,2,Lorem\n2,3,Sit,4,Ipsum";

    var config = new CsvConfiguration(CultureInfo.InvariantCulture)
    {
        HasHeaderRecord = false,
    };

    using (var reader = new StringReader(input))
    using (var csv = new CsvReader(reader, config))
    {
        csv.Context.RegisterClassMap<Mapper>();
        var records = csv.GetRecords<Foo>().ToList();       
    }
}

public sealed class Mapper : ClassMap<Foo>
{
    public Mapper()
    {
        Map(m => m.id).Index(0);
        Map(m => m.bars).Convert(args =>
        {
            var bars = new List<Bar>();

            for (int i = 1; i < 4; i  = 2)
            {
                var bar = new Bar 
                { 
                    id = args.Row.GetField<int>(i), 
                    str = args.Row.GetField<string>(i   1)
                };

                bars.Add(bar);
            }

            return bars.ToArray();
        });
    }
}

public class Foo
{
    public int id { get; set; }
    public Bar[] bars { get; set; }
}

public class Bar
{
    public int id { get; set; }
    public string str { get; set; }
}
  • Related