I have a CSV files that can be structured similar to the following:
Header1,Header2,Header3
1,2,3
5,,6
4,4,4
When using Josh Close's CsvHelper and calling GetRecords<T> as per:
List<TestData> data = csvReader.GetRecords<TestData>();
The list of data does not contain the second row. I've tinkered with the settings and tried to implement a double converter that accepts an empty string and returns '0' when empty however the row still gets discarded. I'm trying to avoid doing a manual get for each field. However, I would still be happy with a row by row solution, i.e. csvReader.GetRecord<TestData>() nested in a loop.
I have the following test code:
public class When_importing_csv_with_missing_filed
{
[Test]
public void Dont_discard_the_row_with_missing_field()
{
using (TextReader textReader = new StreamReader("Test.csv"))
{
Assert.IsTrue(File.Exists("Test.csv"));
var reader = new CsvReader(textReader);
reader.Configuration.RegisterClassMap<TestMap>();
reader.Configuration.IgnoreReadingExceptions = true;
reader.Configuration.SkipEmptyRecords = false;
List<TestData> testData = reader.GetRecords<TestData>().ToList();
}
}
}
public class TestMap : CsvClassMap<TestData>
{
public override void CreateMap()
{
Map(m => m.Header1).Name("Header1").TypeConverter<DoubleConverter>();
Map(m => m.Header2).Name("Header2").TypeConverter<NullValueTypeConverter>();
Map(m => m.Header3).Name("Header3").TypeConverter<DoubleConverter>();
}
}
public class NullValueTypeConverter : DoubleConverter
{
public override object ConvertFromString(TypeConverterOptions options, string text)
{
return String.IsNullOrEmpty(text) ? 0 : base.ConvertFromString(options, text);
}
public override bool CanConvertFrom(Type type)
{
return type == typeof(string);
}
}
public class TestData
{
public double? Header1 { get; set; }
public double? Header2 { get; set; }
public double? Header3 { get; set; }
}
Over to you..
This seems to work completely fine for me.
Code:
void Main()
{
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( "Header1,Header2,Header3" );
writer.WriteLine( "1,2,3" );
writer.WriteLine( "5,,6" );
writer.WriteLine( "4,4,4" );
writer.Flush();
stream.Position = 0;
csv.Configuration.RegisterClassMap<TestMap>();
csv.Configuration.IgnoreReadingExceptions = true;
csv.Configuration.SkipEmptyRecords = false;
var records = csv.GetRecords<TestData>().ToList();
records.Dump();
}
}
public class TestData
{
public double? Header1 { get; set; }
public double? Header2 { get; set; }
public double? Header3 { get; set; }
}
public class TestMap : CsvClassMap<TestData>
{
public override void CreateMap()
{
Map(m => m.Header1).Name("Header1");
Map(m => m.Header2).Name("Header2");
Map(m => m.Header3).Name("Header3");
}
}
Result:
Header1 Header2 Header3
1 2 3
5 null 6
4 4 4
Related
Imagine a collection of EF dbSet like this :
public class Employee {
...
public string FirstName { get; set; }
public List<Badge> Badge { get; set; }
}
public class Badge {
public long CSN { get; set; }
public int EmployeeId { get; set; }
public int Type { get; set; }
}
This models are used in my SGBB and I want to use it to import data from a CSV file. But this file has a small difference. It give only one badge like this :
FIRSTNAME;CSN;TYPE
Jerome;12345;1
I have used a CollectionGenericConverter, to initialize the List with a new record.
Map(m => m.Firstname).Name("Firstname");
Map(m => m.Badges).Name("CSN").TypeConverter<BadgeConverter>();
...
public class BadgeConverter : CollectionGenericConverter {
public override object ConvertFromString(String text, IReaderRow row, MemberMapData memberMapData) {
return new List<Badge> {
new Badge {
CSN = Convert.ToInt16(text)
}
};
}
}
I have just a problem with the second value, using a second converter reset the list of badges :
Map(m => m.Badges).Name("Type").TypeConverter<AnotherOneBadgeConverter>();
And set directly the first item not work :
Map(m => m.Badges[0].Type).Name("Type");
How to do that ?
Something like this may work for you.
public class Program
{
public 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("FIRSTNAME;CSN;TYPE");
writer.WriteLine("Jerome;12345;1");
writer.Flush();
stream.Position = 0;
csv.Configuration.Delimiter = ";";
csv.Configuration.RegisterClassMap<EmployeeMap>();
var records = csv.GetRecords<Employee>().ToList();
}
}
}
public class Employee
{
public string FirstName { get; set; }
public List<Badge> Badge { get; set; }
}
public class Badge
{
public long CSN { get; set; }
public int EmployeeId { get; set; }
public int Type { get; set; }
}
public class EmployeeMap: ClassMap<Employee>
{
public EmployeeMap()
{
Map(m => m.FirstName).Name("FIRSTNAME");
Map(m => m.Badge).ConvertUsing(row =>
{
var list = new List<Badge>
{
new Badge { CSN = row.GetField<long>("CSN"), Type = row.GetField<int>("TYPE") },
};
return list;
});
}
}
I have an exception thrown when loading this CSV file :/ which is quite simple :
public class BreederTemp
{
public BreederTemp ()
{
}
public int Id { get; set; }
public string Affix { get; set; }
public bool Affix_Prefix { get; set; }
}
public sealed class BreederClassMap : ClassMap<BreederTemp>
{
public BreederClassMap()
{
AutoMap();
Map(m => m.Id);
Map(m => m.Affix);
Map(m => m.Affix_Prefix);
}
}
class Program
{
static void Main(string[] args)
{
// [...]
System.IO.TextReader textReader = new StreamReader(CsvFile, Encoding.Default);
var csv = new CsvReader(textReader);
csv.Configuration.RegisterClassMap<BreederClassMap>();
csv.Configuration.Delimiter = ";";
csv.Configuration.Encoding = Encoding.Default;
csv.Read();
var packs = csv.GetRecords<BreederTemp>().ToList();
// Header matching [id] names exception
}
}
Here is the CSV test file I'm using. I tried to export from XLSS to CSV-UTF8 or CSV without any changes.
Id;Affix;Affix_Prefix
1;(sans affixe);0
2;Des garrigues du loup de canebas;0
3;De Dan Jourdain;0
Did I miss something ? thanks for help
Hi I'm using "CsvHelper" (v 2.16.3), I want to use prefix in my object that will append the child prefix to the parent prefix.
But I didn't manged to do so,
Does CsvJelper support that?
For example:
My Model:
public class ExampleModel
{
public string status { get; set; }
public ExampleModelNode leftNode { get; set; }
public ExampleModelNode rightNode { get; set; }
}
public class ExampleModelNode
{
public string status { get; set; }
public NodeData data { get; set; }
}
public class NodeData
{
public string data { get; set; }
}
My mapping class:
class ExampleModelMap : CsvClassMap<ExampleModel>
{
public ExampleModelMap()
{
Map(m => m.status);
References<ExampleModelNodeMap>(m => m.leftNode).Prefix("Right_");
References<ExampleModelNodeMap>(m => m.rightNode).Prefix("Left_");
}
}
class ExampleModelNodeMap : CsvClassMap<ExampleModelNode>
{
public ExampleModelNodeMap()
{
AutoMap(false,true);
References<NodeDataMap>(m => m.data).Prefix("Data_");
}
}
class NodeDataMap : CsvClassMap<NodeData>
{
public NodeDataMap()
{
AutoMap(false,true);
}
}
Init and running:
ExampleModel exampleModel = new ExampleModel()
{
status = "A",
rightNode = new ExampleModelNode()
{
status = "L",
data = new NodeData()
{
data = "L Data"
}
},
leftNode = new ExampleModelNode()
{
status = "R",
data = new NodeData()
{
data = "R Data"
}
}
};
StringBuilder sb = new StringBuilder();
TextWriter writer = new StringWriter(sb);
var csv = new CsvWriter(writer);
csv.Configuration.RegisterClassMap(new ExampleModelMap());
csv.Configuration.PrefixReferenceHeaders = true;
csv.WriteHeader(typeof(ExampleModel));
csv.WriteRecord(exampleModel);
writer.Flush();
writer.Close();
File.WriteAllText("csv.csv", sb.ToString());
The output is:
But the expected output that I want is:
Does CsvHelper support that? How can I get this mapping?
I'm trying to read two types of records out of a CSV file with the following structure:
PlaceName,Longitude,Latitude,Elevation
NameString,123.456,56.78,40
Date,Count
1/1/2012,1
2/1/2012,3
3/1/2012,10
4/2/2012,6
I know this question has been covered previously in
Reading multiple classes from single csv file using CsvHelper
Multiple Record Types in One File?
but when I run my implementation it gets a CsvMissingFieldException saying that Fields 'Date' do not exist in the CSV file. I have two definition and map classes, one for the location and the other for the counts, which are:
public class LocationDefinition
{
public string PlaceName { get; set; }
public double Longitude { get; set; }
public double Latitude { get; set; }
public double Elevation { get; set; }
}
public sealed class LocationMap : CsvClassMap<LocationDefinition>
{
public LocationMap()
{
Map(m => m.PlaceName).Name("PlaceName");
Map(m => m.Longitude).Name("Longitude");
Map(m => m.Latitude).Name("Latitude");
Map(m => m.Elevation).Name("Elevation");
}
}
public class CountDefinition
{
public DateTime Date { get; set; }
public int Count { get; set; }
}
public sealed class CountMap : CsvClassMap<CountDefinition>
{
public CountMap()
{
Map(m => m.Date).Name("Date");
Map(m => m.Count).Name("Count");
}
}
The code that I have for reading the csv file is:
LocationDefinition Location;
var Counts = new List<CountDefinition>();
using (TextReader fileReader = File.OpenText(#"Path\To\CsvFile"))
using (var csvReader = new CsvReader(fileReader))
{
csvReader.Configuration.RegisterClassMap<LocationMap>();
csvReader.Configuration.RegisterClassMap<CountMap>();
// Only reads a single line of Location data
csvReader.Read();
LocationData = csvReader.GetRecord<LocationDefinition>();
csvReader.Read(); // skip blank line
csvReader.Read(); // skip second header section
// Read count data records
while (csvReader.Read())
{
var tempCount = csvReader.GetRecord<CountDefinition>();
Counts.Add(tempCount);
}
}
The exception gets thrown on the tempCount line. From what I can tell it still expects a Location record, but I would have thought GetRecord<CountDefinition> would specify the record type. I've also tried ClearRecordCache and unregistering the LocationMap to no avail.
How should this code be changed to get it to read a csv file of this structure?
Try this
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace ConsoleApplication1
{
enum State
{
FIND_RECORD,
GET_LOCATION,
GET_DATES
}
class Program
{
const string FILENAME = #"c:\temp\test.txt";
static void Main(string[] args)
{
StreamReader reader = new StreamReader(FILENAME);
State state = State.FIND_RECORD;
LocationDefinition location = null;
string inputLine = "";
while ((inputLine = reader.ReadLine()) != null)
{
inputLine = inputLine.Trim();
if (inputLine.Length == 0)
{
state = State.FIND_RECORD;
}
else
{
switch (state)
{
case State.FIND_RECORD :
if (inputLine.StartsWith("PlaceName"))
{
state = State.GET_LOCATION;
}
else
{
if (inputLine.StartsWith("Date"))
{
state = State.GET_DATES;
}
}
break;
case State.GET_DATES :
if (location.dates == null) location.dates = new CountDefinition();
location.dates.dates.Add(new CountDefinition(inputLine));
break;
case State.GET_LOCATION :
location = new LocationDefinition(inputLine);
break;
}
}
}
}
}
public class LocationDefinition
{
public static List<LocationDefinition> locations = new List<LocationDefinition>();
public CountDefinition dates { get; set; }
public string PlaceName { get; set; }
public double Longitude { get; set; }
public double Latitude { get; set; }
public double Elevation { get; set; }
public LocationDefinition(string location)
{
string[] array = location.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
PlaceName = array[0];
Longitude = double.Parse(array[1]);
Latitude = double.Parse(array[2]);
Elevation = double.Parse(array[3]);
locations.Add(this);
}
}
public class CountDefinition
{
public List<CountDefinition> dates = new List<CountDefinition>();
public DateTime Date { get; set; }
public int Count { get; set; }
public CountDefinition() { ;}
public CountDefinition(string count)
{
string[] array = count.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
Date = DateTime.Parse(array[0]);
Count = int.Parse(array[1]);
dates.Add(this);
}
}
}
I got a response from Josh Close on the issue tracker:
CsvReader not recognising different registered class maps
Here is his answer to this question:
Since you don't have a single header, you'll need to ignore headers
and use indexes instead. This brings up an idea though. I could have
the ReadHeader method parse headers for a specific record type.
Here is an example that should work for you though.
void Main()
{
LocationDefinition Location;
var Counts = new List<CountDefinition>();
using (var stream = new MemoryStream())
using (var reader = new StreamReader(stream))
using (var writer = new StreamWriter(stream))
using (var csvReader = new CsvReader(reader))
{
writer.WriteLine("PlaceName,Longitude,Latitude,Elevation");
writer.WriteLine("NameString,123.456,56.78,40");
writer.WriteLine();
writer.WriteLine("Date,Count");
writer.WriteLine("1/1/2012,1");
writer.WriteLine("2/1/2012,3");
writer.WriteLine("3/1/2012,10");
writer.WriteLine("4/2/2012,6");
writer.Flush();
stream.Position = 0;
csvReader.Configuration.HasHeaderRecord = false;
csvReader.Configuration.RegisterClassMap<LocationMap>();
csvReader.Configuration.RegisterClassMap<CountMap>();
csvReader.Read(); // get header
csvReader.Read(); // get first record
var locationData = csvReader.GetRecord<LocationDefinition>();
csvReader.Read(); // skip blank line
csvReader.Read(); // skip second header section
// Read count data records
while (csvReader.Read())
{
var tempCount = csvReader.GetRecord<CountDefinition>();
Counts.Add(tempCount);
}
}
}
public class LocationDefinition
{
public string PlaceName { get; set; }
public double Longitude { get; set; }
public double Latitude { get; set; }
public double Elevation { get; set; }
}
public sealed class LocationMap : CsvClassMap<LocationDefinition>
{
public LocationMap()
{
Map(m => m.PlaceName);
Map(m => m.Longitude);
Map(m => m.Latitude);
Map(m => m.Elevation);
}
}
public class CountDefinition
{
public DateTime Date { get; set; }
public int Count { get; set; }
}
public sealed class CountMap : CsvClassMap<CountDefinition>
{
public CountMap()
{
Map(m => m.Date);
Map(m => m.Count);
}
}
public class Class1
{
[CsvField(Name = "Field1")]
public int Field1 { get; set; }
[CsvField(Name = "Field2")]
public int Field2 { get; set; }
[CsvField(Ignore = true)]
public Class2 Class2 { get; set; }
[CsvField(Ignore = true)]
public Class3 Class3 { get; set; }
}
public class Class2
{
[CsvField(Name = "Field3")]
public int Field3 { get; set; }
[CsvField(Name = "Field4")]
public int Field4 { get; set; }
}
public class Class3
{
[CsvField(Name = "Field5")]
public int Field5 { get; set; }
[CsvField(Name = "Field6")]
public int Field6 { get; set; }
}
I'm using CSVHelper to write data into CSV file.
I need write Class1 with header like this:
Field1, Field2, Field3, Field4, Field5, Field6
How I can do this?
This is an old question, so you likely already have an answer. You have a few options.
Option 1 (that I know will work) Documentation
You just need to manually write out the contents of the CSV, below is some code that will get you started, but you'll need to modify based on how the contents of your objects are stored.
using (var stream = new MemoryStream())
{
using (var streamWriter = new StreamWriter(stream))
using (var csv = new CsvWriter(streamWriter))
{
// Write out header
csv.WriteField("Field1");
csv.WriteField("Field2");
csv.WriteField("Field3");
csv.WriteField("Field4");
csv.WriteField("Field5");
csv.WriteField("Field6");
// Write out end line
csv.NextRecord();
//Pseudocode
foreach (var item in Class1Collection)
{
csv.WriteField(item.Field1);
csv.WriteField(item.Field2);
csv.WriteField(item.Class2.Field3);
csv.WriteField(item.Class2.Field4);
csv.WriteField(item.Class3.Field5);
csv.WriteField(item.Class3.Field6);
// Write out end line
csv.NextRecord();
}
}
}
Option 2 (have used, but not like this) Documentation
Your second option is to write a custom CSVMap that tells the CSVWriter how to handle the nested classes. I'm not sure how to deal with the name, so you might have to work through that.
public sealed class Class1CSVMap : CsvClassMap<RemittanceFormModel>
{
public Class1CSVMap()
{
Map(m => m.Field1).Name("Field1");
Map(m => m.Field2).Name("Field2");
Map(m => m.Class2).Name("Field3,Field4").TypeConverter<Class2Converter>();
Map(m => m.Class3).Name("Field5,Field6").TypeConverter<Class3Converter>();
}
}
Then you have your converter, one for Class2 and one for Class3
public class Class2Converter : DefaultTypeConverter
{
public override string ConvertToString(TypeConverterOptions options, object model)
{
var result = string.Empty;
var classObject = model as Class2;
if (classObject != null)
{
result = string.Format("{0},{1}", classObject.Field3, classObject.Field4);
}
return result;
}
}
Option 3 (have never used) Documentation
You can do an inline converter instead of creating a separate class. I've never tried this, but it should work.
public sealed class Class1CSVMap : CsvClassMap<Class1>
{
public Class1CSVMap()
{
Map(m => m.Field1).Name("Field1");
Map(m => m.Field2).Name("Field2");
Map(m => m.Class2).Name("Field3,Field4").ConvertUsing(row => string.Format("{0},{1}", row.Field3, row.Field4); );
Map(m => m.Class3).Name("Field5,Field6").ConvertUsing(row => string.Format("{0},{1}", row.Field5, row.Field6); );
}
}
look at this
foreach (var item in Collection)
{
csv.WriteField(item.Field1);
csv.WriteField(item.Field2);
csv.WriteField(item.Class2.Field3);
csv.WriteField(item.Class2.Field4);
csv.WriteField(item.Class3.Field5);
csv.WriteField(item.Class3.Field6);
// Write out end line
csv.NextRecord();
}
}
}