Read CSV row and map to class with collection of subclass - c#

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.

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; }
}

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.

Related

Read CSV files without Header using CSVHelper

I have a lot of CSV files without header and need to read it in C#. I manually added header to one of these files and with the following code using CSVHelper I can read the files and show them in a GridView.
Now my question is, how can I read these files without a header? Or how can I add a header (a new record) using CSVHelper in the first line?
public Form1()
{
InitializeComponent();
List<Festival> records;
var config = new CsvConfiguration(CultureInfo.InvariantCulture) { Delimiter = ";" };
using (var reader = new StreamReader(#"File8.csv"))
using(var csv = new CsvReader(reader, config))
{
records = csv.GetRecords<Festival>().ToList();
}
dataGridView1.DataSource = records;
}
Class
public class Festival
{
public string Day { get; set; }
public string Start { get; set; }
public int Lenght { get; set; }
public string FilmName { get; set; }
public float Rating { get; set; }
}
csv sample
Mi;22:15;110;A;8
Mi;19:00;106;B;8
Mi;19:15;97;C;8.2
Add column-index mapping attributes to the target members:
public class Festival
{
[Index(0)]
public string Day { get; set; }
[Index(1)]
public string Start { get; set; }
[Index(2)]
public int Lenght { get; set; }
[Index(3)]
public string FilmName { get; set; }
[Index(4)]
public float Rating { get; set; }
}
And specify HasHeaderRecord = false in the config:
var config = new CsvConfiguration(CultureInfo.InvariantCulture) { Delimiter = ";", HasHeaderRecord = false };
If modifying the target model isn't desirable, implement a ClassMap instead:
public sealed class FestivalMap : ClassMap<Festival>
{
public FestivalMap()
{
Map(f => f.Day).Index(0);
Map(f => f.Start).Index(1);
Map(f => f.Lenght).Index(2);
Map(f => f.FilmName).Index(3);
Map(f => f.Rating).Index(4);
}
}
And register it like this before fetching the records (you still need to specify HasHeaderRecord = false in the config):
csv.Context.RegisterClassMap<FestivalMap>();
records = csv.GetRecords<Festival>().ToList();

Write an object with a List<T> field to CSV

I'm learning C# and object serialization. I need to save an object in a .csv file. I tried CsvHelper but couldn't find the answer to my question.
Suppose I have an object like this:
public class Record
{
...
public List<string> Collection { get; set; }
...
}
How do I use CsvHelper to store the values of Collection in a .csv file along with other primitive types in a record of csv file?
Collections are ignored by CsvHelper by default. However, using a ClassMap you can use Index to indicate you want a simple collection of string to be output with the other properties. (Not well documented.)
public class Program
{
public static void Main(string[] args)
{
var records = new List<Record>
{
new Record { Id = 1, Name = "Record1", Collection = new List<string>{"First", "Second", "Third"}},
new Record { Id = 2, Name = "Record2", Collection = new List<string>{"First", "Second"}},
};
using (var csv = new CsvWriter(Console.Out))
{
csv.Configuration.HasHeaderRecord = false;
csv.Configuration.RegisterClassMap<RecordMap>();
csv.WriteRecords(records);
}
Console.ReadKey();
}
}
public class RecordMap : ClassMap<Record>
{
public RecordMap()
{
Map(m => m.Id);
Map(m => m.Name);
Map(m => m.Collection).Index(3);
}
}
public class Record
{
public int Id { get; set; }
public string Name { get; set; }
public List<string> Collection { get; set; }
}
Outputs:
1,Record1,First,Second,Third
2,Record2,First,Second
If you know the max number of items in the Collection, you can also set an end index and have CsvHelper create the headings for each collection item.
public class RecordMap : ClassMap<Record>
{
public RecordMap()
{
Map(m => m.Id);
Map(m => m.Name);
Map(m => m.Collection).Index(3, 5);
}
}
Remove csv.Configuration.HasHeaderRecord = false; and now it will also print the header record for you.
Outputs:
Id,Name,Collection1,Collection2,Collection3
1,Record1,First,Second,Third
2,Record2,First,Second

Error exporting to CSV when there are reference maps

I have s Student class where each student record has a list of Results.
I need to export there results to CSV and I'm using CsvHelper.
public class Student
{
public string Id { get; set; }
public string Name { get; set; }
public Result[] Grades { get; set; }
}
public class Result
{
public string Subject { get; set; }
public decimal? Marks { get; set; }
}
I'm using Reference Maps to map the list of Results, but when exporting to CSV it throws and error.
Mapping Code
public sealed class StudentResultExportMap : ClassMap<Student>
{
public StudentResultExportMap ()
{
AutoMap();
References<GradesMap>(m => m.Grades);
}
}
public sealed class GradesMap: ClassMap<Result>
{
public GradesMap()
{
Map(m => m.Subject);
Map(m => m.Marks);
}
}
Error
Property 'System.String Subject' is not defined for type
'{namespace}.GetStudentResults+Result[]' Parameter name: property
Unfortunately References<GradesMap>(m => m.Grades); doesn't work for an array of Result. It would work for an individual result. I have one solution, which overrides the ToString() method of Result to flatten the grades. It might work for you, depending on what you need.
public class Result
{
public string Subject { get; set; }
public decimal? Marks { get; set; }
public override string ToString()
{
return $"{Subject} = {Marks}";
}
}
Make a slight change to your StudentResultExportMap. You can set the 2nd number on .Index(2, 7) to handle the max number of grades you think a student might have.
public sealed class StudentResultExportMap : ClassMap<Student>
{
public StudentResultExportMap()
{
AutoMap();
Map(m => m.Grades).Name("Grade").Index(2, 7);
}
}
You will then get Id, Name, Grade1, Grade2, Grade3, Grade4, Grade5, Grade6 with the toString() value of Result for each grade.
var records = new List<Student>
{
new Student{ Id = "1", Name = "First", Grades = new [] {
new Result { Subject = "Subject1", Marks = (decimal)2.5 } ,
new Result { Subject = "Subject2", Marks = (decimal)3.5 } }},
new Student{ Id = "2", Name = "Second", Grades = new [] {
new Result { Subject = "Subject1", Marks = (decimal)3.5 } ,
new Result { Subject = "Subject2", Marks = (decimal)4.0 } }}
};
using (var writer = new StreamWriter("path\\to\\StudentResults.csv"))
using (var csv = new CsvWriter(writer))
{
csv.Configuration.RegisterClassMap<StudentResultExportMap>();
csv.WriteRecords(records);
}

Get all values for A variable in a class

I have two classes one of them is Destinations and the other one is DestinationDetails
public class Destinations
{
public Destinations() { }
public string CarrierName { get; set; }
public List<DestinationDetails> Details { get; set; }
}
public class DestinationDetails
{
public DestinationDetails() { }
public string Destination { get; set; }
public string Type { get; set; }
}
I want to get all string "Destination" in the second class from List of objects from the first class
I have List<Destinations> and I don't want to use for loop or foreach statments
var dest = new Destinations();
//Initialize the details
var destNames = dest.Details.Select(d => d.Destination).ToList();
Are you looking for something like this?
var det = new Destinations();
det.Details = new List<DestinationDetails>();
det.Details.Add(new DestinationDetails() { Destination = "CA" });
det.Details.Add(new DestinationDetails() { Destination = "NJ" });
...
...
var details = new DestinationDetails();
details.Destination = string.Join(",",det.Details.Select(x => x.Destination).ToArray() );
Update:-
provided list of Destinations "allDet", you can get the list of strings as below:-
alldet.Where(x => x.Details != null).SelectMany(x => x.Details.Select(y => y.Destination)).ToList() //With out ToList() it will give you IEnumerable<String>
List<Destinations> AirportDestinations ; // this list has Destinations objects which have Details which have Destination
So by using SelectMany
List<string> cities.AddRange(AirportDestinations.Where(x => x.Details != null).SelectMany(d => d.Details.Select(s => s.Destination)));
Now you have all Destination in all objects in the list

Automapper overwrites missing source property on list with child objects

I have a problem with Automapper. I set up a test windows form application and below is the code. Also look at the comments after each MessageBox:
public class FirstClass
{
public string FirstProp { get; set; }
public IList<FirstClassChild> Children { get; set; }
}
public class FirstClassChild
{
public string FirstChildProp { get; set; }
}
public class SecondClass
{
public string FirstProp { get; set; }
public string SecondProp { get; set; }
public IList<SecondClassChild> Children { get; set; }
}
public class SecondClassChild
{
public string FirstChildProp { get; set; }
public string SecondChildProp { get; set; }
}
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
AutoMapper.Mapper.CreateMap<FirstClass, SecondClass>();
AutoMapper.Mapper.CreateMap<FirstClassChild, SecondClassChild>();
var f = new FirstClass { FirstProp = "FirstClass" };
f.Children = new List<FirstClassChild> { new FirstClassChild { FirstChildProp = "FirstClass" } };
var s = new SecondClass { FirstProp = "SecondClass", SecondProp = "SecondClass" };
s.Children = new List<SecondClassChild> { new SecondClassChild { FirstChildProp = "SecondClass", SecondChildProp = "SecondClass" } };
AutoMapper.Mapper.Map(f, s);
var fc = new FirstClassChild { FirstChildProp = "FirstClass" };
var sc = new SecondClassChild { FirstChildProp = "SecondClass", SecondChildProp = "SecondClass" };
AutoMapper.Mapper.Map(fc, sc);
MessageBox.Show(sc.FirstChildProp);//FirstClass as expected
MessageBox.Show(sc.SecondChildProp);//SecondClass as expected
MessageBox.Show(s.FirstProp);//FirstClass as expected
MessageBox.Show(s.SecondProp);//SecondClass as expected
MessageBox.Show(s.Children.First().FirstChildProp);//FirstClass as expected
MessageBox.Show(s.Children.First().SecondChildProp);//Empty not expected!!
}
}
What can I do to avoid this? Is this behavior expected?
Anyway can anyone guide me how make SecondClass childs SecondChildProp to remain "SecondClass" as it is before the mapping occurs.
I asked a similar question here and found another similar one here.
I think #PatrickSteele makes a very good point: how is AutoMapper supposed to map a source list to a dest list of existing objects, when the dest list may not necessarily bear any resemblance to the source list? i.e. "But what if one list has 3 and the other list has 5?"
If you are sure that FirstClass and SecondClass have the same number of Children, and if the FirstClass's Nth Child always corresponds to SecondClass's Nth child, you could try something like this:
Mapper.CreateMap<FirstClass, SecondClass>()
.ForMember(m => m.Children, o => o.Ignore())
.AfterMap((src, dest) =>
{
for (var i = 0; i < dest.Children.Count; i++)
Mapper.Map(src.Children[i], dest.Children[i]);
});
or if FirstChildProp is some kind of unique key:
Mapper.CreateMap<FirstClass, SecondClass>()
.ForMember(m => m.Children, o => o.Ignore())
.AfterMap((src, dest) =>
{
foreach (var dChild in dest.Children)
{
var sChild = src.Children.Single(c => c.FirstChildProp == dChild.FirstChildProp);
Mapper.Map(sChild, dChild);
}
});

Categories

Resources