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

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

Related

.NET Csv Helper - Type Conversion to Double

In one of the columns, regarding price, the data is in a weird format when the price starts with 0.
Example : instead of 0,60 the format would be ,60.
I tried to use the CsvHelper library from .NET nuget packages, and i would receive, of course an exception when reaching a line with this kind of values, because it could not convert this to double.
This library has a documentation regarding mapping, but i could not find anything documente on how to custom config my situation :
https://joshclose.github.io/CsvHelper/examples/configuration/class-maps/type-conversion/
Can someone give me a hint/tip for this situation ?
Here is my class setup:
public class Article
{
public int Id { get; set; }
public int ItemID { get; set; }
public string? Barcode { get; set; }
public double Price{ get; set; }
}
public class ArticleClassMap : ClassMap<Article>
{
public ArticleClassMap()
{
Map(m => m.ItemID).Name("itemID");
Map(m => m.Barcode).Name("barcode");
Map(m => m.Price).Name("price");
}
}
Here is how my file row with issues would look like :
1234|9999| ,60
Where 1234 would be the ItemId, 9999 EAN, and ,60 would represent a price of 0,60.
Here is how the CsvReader is setup :
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
using (var csv = new CsvReader(reader, config))
{
csv.Context.RegisterClassMap<ArticleClassMap>();
var records = csv.GetRecords<Article>().ToList();
return records;
}
}
I think you problem comes form ',' in your price normaly it should be '.'
One solution is custom convert commas to periods and then parse to double like this:
Map(m => m.ItemID).Name("itemID");
Map(m => m.Barcode).Name("barcode");
Map(m => m.Price).Convert(row =>
{
return Double.Parse(row.Row.GetField<string>("price").Replace(',', '.'));
});
After hours of trying, I found a working solution in my case :
I've created a customer converter:
public class PriceConverter : DefaultTypeConverter
{
public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
{
double result;
//replace space with 0 in position 5 of string text, if position 5 is an empty character
if (text.Length > 5 && text[5] == ' ')
{
text = text.Remove(5, 1).Insert(5, "0");
}
result = double.Parse(text);
return result;
}
public override string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData)
{
return value?.ToString();
}
}
This works in the case, because in the .csv file the positions are fixed!
So, before the comma, we always have the whole value.
Then I applied it to the mapping like this :
public ArticleClassMap()
{
Map(m => m.Price).Name("price").TypeConverter<PriceConverter>();
}
Set your culture. Here is an example with a culture that uses ,.
void Main()
{
var s = new StringBuilder();
s.Append("itemID|barcode|price\r\n");
s.Append("1234|9999| ,60\r\n");
var config = new CsvConfiguration(CultureInfo.GetCultureInfo("ru-RU"))
{
Delimiter = "|",
};
using (var reader = new StringReader(s.ToString()))
using (var csv = new CsvReader(reader, config))
{
csv.Context.RegisterClassMap<ArticleClassMap>();
csv.GetRecords<Article>().ToList().Dump();
}
}
public class Article
{
public int Id { get; set; }
public int ItemID { get; set; }
public string? Barcode { get; set; }
public double Price { get; set; }
}
public class ArticleClassMap : ClassMap<Article>
{
public ArticleClassMap()
{
Map(m => m.ItemID).Name("itemID");
Map(m => m.Barcode).Name("barcode");
Map(m => m.Price).Name("price");
}
}

Read CSV row and map to class with collection of subclass

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.

Parsing file with CsvHelper that has Extra Fields/Properties Not in the Class

I need to parse incoming CSV files that mostly map to a specific class. However, the clients are permitted to add extra "user defined" fields to the end of record if they wish. So the CSV might look something like:
Id,FirstName,LastName,MyExtraField1,MyExtraField2
1,John,Doe,foo,bar
2,Jane,Smith,foo2,bar2
My class has named properties for Id, FirstName and LastName, but not for MyExtraField1 and MyExtraField2.
If I created a new property on the class called "ExtraFields" that was a Dict is it possible to take any field in the CSV that is not matched to the class and stuff it in the ExtraFields dictionary? The key would be the name of the field from the header and then the value for that record. Or is there some other way to capture these fields that don't map to any property in the class?
I believe this gets what you are looking for.
static void Main(string[] args)
{
using (MemoryStream stream = new MemoryStream())
using (StreamWriter writer = new StreamWriter(stream))
using (StreamReader reader = new StreamReader(stream))
using (CsvReader csv = new CsvReader(reader))
{
writer.WriteLine("Id,FirstName,LastName,MyExtraField1,MyExtraField2");
writer.WriteLine("1,John,Doe,foo,bar");
writer.WriteLine("2,Jane,Smith,foo2,bar2");
writer.Flush();
stream.Position = 0;
csv.Read();
csv.ReadHeader();
var headers = csv.Context.HeaderRecord.ToList();
csv.Configuration.RegisterClassMap(new TestClassMap(headers.Skip(3)));
var records = csv.GetRecords<TestClass>().ToList();
}
}
public class TestClass
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Dictionary<string, string> ExtraFields { get; set; }
}
public sealed class TestClassMap : ClassMap<TestClass>
{
public TestClassMap(IEnumerable<string> headers)
{
Map(m => m.Id);
Map(m => m.FirstName);
Map(m => m.LastName);
Map(m => m.ExtraFields).ConvertUsing(row => headers.ToDictionary(h => h, h => row.GetField(h)));
}
}

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

Categories

Resources