Handle invlaid entries in csv file with csv Helper - c#

I am currently using CSV helper to read contents of a csv file and output it to console. I have installed the csvHelper nuget package. However when I run the code I get the following error:
CsvHelper.TypeConversion.TypeConverterException: 'The conversion cannot be performed.
Text: ''
MemberType:
TypeConverter: 'CsvHelper.TypeConversion.Int32Converter''
I understand that this is due to the fact that the field population in the csv is empty. I would currently like to be able to validate the field and set it to 0. How can I do this with CSVhelper.
My code for reading the csv is:
class ReaderCsv
{
private string _cvsfilepath;
public ReaderCsv(string csvfilepath)
{
this._cvsfilepath = csvfilepath;
}
public List <Country> ReadAllCountries()
{
var countries = new List<Country>();
using (var sr = new StreamReader(_cvsfilepath))
using (var csv = new CsvReader(sr, System.Globalization.CultureInfo.InvariantCulture))
{
csv.Configuration.Delimiter = ",";
csv.Read();
csv.ReadHeader();
while (csv.Read())
{
var country= new Country();
{
country.CountryName = csv.GetField("CountryName");
country.CountryCode = csv.GetField("CountryCode");
country.Continent = csv.GetField("CountryCode");
country.Population = csv.GetField<int>("Population");
}
countries.Add(country);
}
}return countries;
}
}
}
my mapping class is
public class CountryMap : ClassMap<Country>
{
public CountryMap()
{
Map(m => m.CountryName);
Map(m => m.CountryCode);
Map(m => m.Continent);
Map(m => m.Population);
}
}

The CSV Helper provides overloads of GetField method to which you can pass a custom type converter.
https://joshclose.github.io/CsvHelper/api/CsvHelper/CsvReader/
Therefore; and not only for Int32 but for any type, here is an implementation using a custom generic type converter that returns the default value of the type if the conversion fails.
This does not mean that you have to swallow or ignore the exception. This converter will also give you the conversion error and the offending value so that you can handle this invalid data.
I also added a lineNumber variable to track on which line the invalid data resides.
I hope this helps.
public class Defaulter<T> : CsvHelper.TypeConversion.ITypeConverter
{
Exception conversionError;
string offendingValue;
public Exception GetLastError()
{
return conversionError;
}
public string GetOffendingValue()
{
return offendingValue;
}
object CsvHelper.TypeConversion.ITypeConverter.ConvertFromString(string text, IReaderRow row, CsvHelper.Configuration.MemberMapData memberMapData)
{
conversionError = null;
offendingValue = null;
try
{
return (T)Convert.ChangeType(text, typeof(T));
}
catch (Exception localConversionError)
{
conversionError = localConversionError;
}
return default(T);
}
string CsvHelper.TypeConversion.ITypeConverter.ConvertToString(object value, IWriterRow row, CsvHelper.Configuration.MemberMapData memberMapData)
{
return Convert.ToString(value);
}
}
And here is the modified version of your code to track the line number as well as to handle the error if you want:
public class ReaderCsv
{
private string _cvsfilepath;
public ReaderCsv(string csvfilepath)
{
this._cvsfilepath = csvfilepath;
}
public List<Country> ReadAllCountries()
{
var countries = new List<Country>();
using (var sr = new StreamReader(_cvsfilepath))
using (var csv = new CsvReader(sr, System.Globalization.CultureInfo.InvariantCulture))
{
csv.Configuration.Delimiter = ",";
csv.Read();
csv.ReadHeader();
Defaulter<int> customInt32Converter = new Defaulter<int>();
int lineNumber = 0;
while (csv.Read())
{
lineNumber++;
var country = new Country();
{
country.CountryName = csv.GetField("CountryName");
country.CountryCode = csv.GetField("CountryCode");
country.Continent = csv.GetField("CountryCode");
country.Population = csv.GetField<int>("Population", customInt32Converter);
if (customInt32Converter.GetLastError() != null)
{
// The last conversion has failed.
// Handle it here.
string errorMessage = "The conversion of Population field on line " + lineNumber + " has failed. The Population value was: [" + customInt32Converter.GetOffendingValue() + "]";
}
}
countries.Add(country);
}
}
return countries;
}
}
Regards.

You could use ClassMap to give a default value for Population
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, CultureInfo.InvariantCulture))
{
writer.WriteLine("CountryName,CountryCode,Continent,Population");
writer.WriteLine("TestName1,TestCode1,TestContinent1,");
writer.WriteLine("TestName2,TestCode2,TestContinent2,2");
writer.Flush();
stream.Position = 0;
csv.Configuration.RegisterClassMap<CountryMap>();
var countries = csv.GetRecords<Country>().ToList();
}
Console.ReadKey();
}
}
public class CountryMap : ClassMap<Country>
{
public CountryMap()
{
Map(m => m.CountryName);
Map(m => m.CountryCode);
Map(m => m.Continent);
Map(m => m.Population).Default(0);
}
}
public class Country
{
public string CountryName { get; set; }
public string CountryCode { get; set; }
public string Continent { get; set; }
public int Population { get; set; }
}

Related

CSV mapper class does map from list

(updated version)
I'm reading dates from csv using csv mapper (CsvClassMap)
void Main()
{
using (var reader = new StreamReader("path\\to\\file.csv"))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
csv.Context.RegisterClassMap<LoanRecord >();
var records = csv.GetRecords<LoanRecord >();
}
}
public class LoanRecord : Model
{
public LoanRecord()
{
}
public string[] LoanStart_DateTime = new string[2];
public string[] LoanEnd_DateTime = new string[2];
}
Mapper class:
public sealed class LoanMapper: CsvClassMap<LoanRecord>
{
public LoanMapper()
{
Map(m => m.LoanStart_DateTime[0]).Index(0);
Map(m => m.LoanEnd_DateTime[0]).Index(1);
Map(m => m.LoanStart_DateTime[1]).Index(2);
Map(m => m.LoanEnd_DateTime[1]).Index(3);
}}
csv format:
LoanStart_DateTime1,LoanEnd_DateTime1,LoanStart_DateTime2,LoanEnd_DateTime2
01/12/2022 00:00,02/12/2022 00:00,23/05/2022 00:00,23/05/2022 03:00,
somehow my mapper class doesn't recognize the list 'Not a member access Parameter name: expression', any help would be appreciated.
Using the CSVHelper package, you can manually iterate over the csv as it's being read. It then just requires some indexing logic to create a class for each pair of columns (obviously changing the index increment if you need more/less columns per Class):
static void Main(string[] args)
{
using (var reader = new StreamReader(#"C:\code\local\CSVReader\Loans.csv"))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
var records = new LoanRecord();
records.Loans = new List<Loan>();
int index = 0;
csv.Read();
csv.ReadHeader();
while (csv.Read())
{
for(int i = 1; index < csv.HeaderRecord.Length; i++)
{
var loanItem = new Loan()
{
LoanStart = csv.GetField("LoanStart_DateTime" + i.ToString()),
LoanEnd = csv.GetField("LoanEnd_DateTime" + i.ToString())
};
records.Loans.Add(loanItem);
index += 2;
}
}
}
}
Classes:
public class LoanRecord
{
public List<Loan> Loans { get; set; }
}
public class Loan
{
public string LoanStart { get; set; }
public string LoanEnd { get; set; }
}
You'll then need to do something with your records but they'll be available as a list of loans:

Write CSV file with Upper Case Header

I am trying to write a csv file but with upper case headers.
Entries is a collection of objects.
using (var streamWriter = new StreamWriter(csvStream))
{
var csvConfig = new CsvConfiguration(CultureInfo.CurrentCulture)
{
PrepareHeaderForMatch = args => args.Header.ToUpper()
};
using (var csvWriter = new CsvWriter(streamWriter, csvConfig))
{
csvWriter.WriteRecords<T>(entries);
streamWriter.Flush();
}
}
I can see the file has been written but headers are not in capital letter. What is wrong here?
PrepareHeaderForMatch is for matching and mapping headers to your model when you want to Read a csv file not writing. for writing to csv file, you need to set Name attribute for your properties like this:
public class CsvModel
{
[Name("NAME")]
public string Name { get; set; }
[Name("FAMILY")]
public string Family { get; set; }
}
Or you can set header names in csv mapping:
public class CsvModelMap : ClassMap<CsvModel>
{
public CsvModelMap()
{
Map(m => m.Name).Index(0).Name("NAME");
Map(m => m.Family).Index(1).Name("FAMILY");
}
}
If you want a more generic approach to making all the headers uppercase, you can use the Map(Type classType, MemberInfo member, bool useExistingMap = true) overload of the Map method.
void Main()
{
var records = new List<CsvModel>
{
new CsvModel { Name = "name", Family = "first" },
};
using (var csv = new CsvWriter(Console.Out, CultureInfo.InvariantCulture))
{
var fooMap = new DefaultClassMap<CsvModel>();
var properties = typeof(CsvModel).GetProperties();
foreach (var property in properties)
{
fooMap.Map(typeof(CsvModel), property).Name(property.Name.ToUpper());
}
csv.Context.RegisterClassMap(fooMap);
csv.WriteRecords(records);
}
}
public class CsvModel
{
public string Name { get; set; }
public string Family { get; set; }
}

Using CSVhelper to update a single column into a previously written CSV file . I have the code in Java but can't translate it into C#

Essentially I have to read and update the CSVfile (only one column) with the current date after the test finishes executing(ie,there are some values written in at the start of the test execution and then I need to update the same file to input another value). I also have a DateTime error which isn't getting resolved no matter what I try.
Sample of CSV start of test
RunId ProductArea Product Component PageObject Control TimeTakenByLocatorJson
Run_645987 R201 BN2018.5 N778 BC1 C143
CSV one column Needs to get updated after test
( TimeTakenByLocatorJson)
RunId ProductArea Product Component PageObject Control TimeTakenByLocatorJson
Run_645987 R201 BN2018.5 N778 BC1 C143 2021-07-19
I've been trying to update a CSV file using CSVhelper. The code I have is in Java and when I tried translating the same code in C# it doesn't work.
This is the code in Java
public synchronized void writeEndCSV(String runId)
{
CSVWriter csvWriter = null;
try
{
String setupCSVLocation = Reporting.getSetupCSVLocation();
CSVReader csvReader = new CSVReader(new FileReader(setupCSVLocation));
List<String[]> records = csvReader.readAll();
for(int i=0;i<records.size();i++)
{
if(records.get(i)[SETUP_RUNID].equalsIgnoreCase(runId));
{
records.get(i)[SETUP_TimeTakenByLocatorJSON] = Reporting.getExecutionEndDate();
}
}
csvReader.close();
csvWriter = new CSVWriter(new FileWriter(setupCSVLocation));
csvWriter.writeAll(records);
csvWriter.flush();
csvWriter.close();
}
catch (Exception e)
{
e.printStackTrace();
}
}
This is my code in C# (I'm new to .Net so I'm not sure about many parts)
public void writeEnd(string runId)
{
var records = Enumerable.Empty<LocatorTime>();
try
{
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
// Don't write the header again.
HasHeaderRecord = false,
};
using (var reader = new StreamReader(#"D:\Reports\" + runId + #"\LocatorTime.csv"))
using (var csv = new CsvReader(reader, config))
{
//csv.Context.RegisterClassMap<LocatorTime>();
records = csv.GetRecords<LocatorTime>().ToList();
foreach (var record in records)
{
if (record.RunID == runId)
{
record.TimeTakenByLocatorJSON = DateTime.Now;
}
// Console.WriteLine("inside loop");
}
}//Endof Stream Reader
using (var stream = File.Open(#"D:\Reports\" + runId + #"\LocatorTime.csv", FileMode.Append)) //not sure what the file mode should be
using (var writer = new StreamWriter(stream))
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(records);
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
}//end func writeEnd
This is the class used for the csv file & are also the column names in the csvfile
public class LocatorTime
{
public string RunID { get; set; }
public string ProductArea { get; set; }
public string Product { get; set; }
public string Component { get; set; }
public string PageObject { get; set; }
public string Control { get; set; }
public DateTime TimeTakenByLocatorJSON //only this value needs to be written for update at end of exec
{
get;
set;
}/*error because of DateTime datatype how to resolve?*/ }//LocatorTimeClass
/*public void SetExeDate() //tried this for removing DateTime error, didn't work
{
DateTime today = DateTime.Today; // As DateTime
string s_today = today.ToString("yyyy-MM-dd"); // As String
//TimeTakenByLocatorJSON = s_today.Trim();
TimeTakenByLocatorJSON = Convert.ToDateTime(s_today);}
*/
public sealed class LocatorTimeMap : ClassMap<LocatorTime> //is mapping helpful for updating? currently commented out
{
public LocatorTimeMap()
{
Map(m => m.RunID).Index(0);
Map(m => m.ProductArea).Index(1);
Map(m => m.Product).Index(2);
Map(m => m.Component).Index(3);
Map(m => m.PageObject).Index(4);
Map(m => m.Control).Index(5);
Map(m => m.TimeTakenByLocatorJSON).Index(6); //error
}
}
I had used the below link as reference for trying to update the CSV file hence the use of "HasHeaderRecord = false"
https://joshclose.github.io/CsvHelper/examples/writing/appending-to-an-existing-file/
I noticed 3 things. 1st - In your sample data, you have 7 column headers, but only 6 columns of data. 2nd - Your class names for "RunId" and "TimeTakenByLocatorJson" don't exactly match the columns in your sample data. 3rd - Your config says "Don't write the header again.", but you are using it for reading.
For the 1st issue, I'm going to assume this was a misprint and I'll add another column of data.
For the 2nd issue, there are at least 3 ways to handle it. You already handled it one way by mapping to indexes in LocatorTimeMap. I'll give you a 2nd way by casting the header to lower case. A 3rd way is to use the name attribute
For the 3rd issue, the header is there for reading and I assume you want the header when you write it, so you can leave HasHeaderRecord = false out.
void Main()
{
writeEnd("Run_645987");
}
// You can define other methods, fields, classes and namespaces here
public void writeEnd(string runId)
{
var records = Enumerable.Empty<LocatorTime>();
try
{
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
// This will convert both the header coming in and your class property to lower case.
PrepareHeaderForMatch = args => args.Header.ToLower()
};
var input = new StringBuilder();
input.AppendLine("RunId,ProductArea,Product,Component,PageObject,Control,TimeTakenByLocatorJson");
input.AppendLine("Run_645987,R201,BN2018.5,N778,BC1,control1,2021-07-19");
using (var reader = new StringReader(input.ToString()))
//using (var reader = new StreamReader(#"D:\Reports\" + runId + #"\LocatorTime.csv"))
using (var csv = new CsvReader(reader, config))
{
records = csv.GetRecords<LocatorTime>().ToList();
foreach (var record in records)
{
if (record.RunID == runId)
{
record.TimeTakenByLocatorJSON = DateTime.Now;
}
}
}//Endof Stream Reader
//using (var stream = File.Open(#"D:\Reports\" + runId + #"\LocatorTime.csv", FileMode.Append)) //not sure what the file mode should be
//using (var writer = new StreamWriter(stream))
using (var csv = new CsvWriter(Console.Out, CultureInfo.InvariantCulture))
{
csv.WriteRecords(records);
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
}//end func writeEnd
public class LocatorTime
{
public string RunID { get; set; }
public string ProductArea { get; set; }
public string Product { get; set; }
public string Component { get; set; }
public string PageObject { get; set; }
public string Control { get; set; }
public DateTime TimeTakenByLocatorJSON //only this value needs to be written for update at end of exec
{
get;
set;
}
}//LocatorTimeClass

CSV Helper Wraps string field ' Name' into double quotes after delimiter?

I have some troubles with CSV result in my file,
I have written the next configuration in code ( I am working with CSVHelper library)
public class ReportModelMap : ClassMap<ReportModel>
{
public ReportModelMap()
{
Map(x => x.Name).Index(9).Name(" Name");
}
}
The customer requires to add the space between the 'Name' text => " Name".
However, the library wraps the string into ' Name' into double quotes, and for me, it's wrong behavior.
How can I make --Surname; Name-- instead of --Surname;" Name"--?
I can't find any specific configuration to fix it in CsvWriter
My saving logic if it needed
using (var writer = new StreamWriter(path))
using (var csvWriter = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csvWriter.Configuration.RegisterClassMap<ReportModelMap>();
csvWriter.Configuration.Delimiter = ";";
csvWriter.WriteRecords(ratingModels);
}
#Panagiotis Kanavos is correct. You can use ShouldQuote to override the quoting behavior for just that one heading.
void Main()
{
var ratingModels = new List<ReportModel>
{
new ReportModel { Id = 1, Surname = "Surname", Name = " Name" }
};
//using (var writer = new StreamWriter(path))
using (var csvWriter = new CsvWriter(Console.Out, CultureInfo.InvariantCulture))
{
csvWriter.Configuration.RegisterClassMap<ReportModelMap>();
csvWriter.Configuration.Delimiter = ";";
csvWriter.Configuration.ShouldQuote = (field, context) =>
{
if (!context.HasHeaderBeenWritten && field == " Name")
{
return false;
}
else
{
return ConfigurationFunctions.ShouldQuote(field, context);
}
};
csvWriter.WriteRecords(ratingModels);
}
}
// You can define other methods, fields, classes and namespaces here
public class ReportModelMap : ClassMap<ReportModel>
{
public ReportModelMap()
{
Map(x => x.Id).Index(0);
Map(x => x.Surname).Index(1);
Map(x => x.Name).Index(2).Name(" Name");
}
}
public class ReportModel
{
public int Id { get; set; }
public string Surname { get; set; }
public string Name { get; set; }
}

Using CsvHelper to iterate child objects into CSV file - Property 'Int32' is not defined for type

Using CSV Helper
I'm trying to output the contents of a collection each of which contains child collections.
I've successfully used CSV helper on single ienumerable collections before, however I'm stuck when it comes to including child objects.
As below:
Method to write out the CSV
public void DumpData(List<FzLogModelSessionChronological> logData)
{
var fileName = $#"csv-{DateTime.Now:yy-MM-dd hhmmss}.csv";
try
{
using (var writer = new StreamWriter($#"{_csvLocation}\{fileName}", true))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.Context.RegisterClassMap<SessionMap>();
csv.WriteRecords(logData);
}
}
}
catch (Exception ex)
{
}
finally
{
}
Mapping Classes
public sealed class SessionMap : ClassMap<FzLogModelSessionChronological>
{
public SessionMap()
{
Map(m => m.SessionId);
References<SesssionDataMap>(m => m.SessionData);
}
}
public sealed class SesssionDataMap : ClassMap<FzLogModel>
{
public SesssionDataMap()
{
Map(m => m.SessionId);
Map(m => m.CommandString);
Map(m => m.Command);
Map(m => m.SourceIp);
Map(m => m.User);
Map(m => m.ActionDateTime);
}
}
The CSV is created, the header rows are also created but it generates error on the first
Property 'Int32 SessionId' is not defined for type 'System.Collections.Generic.List`1[FileZillaFtpLogParser.Models.FzLogModel]'
Where am I going wrong?
Csv is not very good for creating lists of lists. If SessionData was FzLogModel instead of List<FzLogModel> then your code would work just fine. I found a solution that might work for you that I converted to work with the latest version of CsvHelper.
void Main()
{
var logData = new List<FzLogModelSessionChronological>(){
new FzLogModelSessionChronological{
SessionId = 1,
SessionData = new List<FzLogModel>(){
new FzLogModel {
SessionId = 1,
CommandString = "CommandString1",
Command = "Command1"
},
new FzLogModel {
SessionId = 2,
CommandString = "CommandString2",
Command = "Command2"
}
}
}
};
try
{
var config = new CsvConfiguration(CultureInfo.InvariantCulture){ HasHeaderRecord = false};
using (var csv = new CsvWriter(Console.Out, config))
{
csv.Context.TypeConverterCache.AddConverter<List<FzLogModel>>(new FzLogModelConverter());
csv.WriteRecords(logData);
}
}
catch (Exception ex)
{
ex.Dump();
}
finally
{
}
}
public class FzLogModelSessionChronological
{
public int SessionId { get; set; }
[Index(2)]
public List<FzLogModel> SessionData { get; set; }
}
public class FzLogModel
{
public int SessionId { get; set; }
public string CommandString { get; set; }
public string Command { get; set; }
}
public class FzLogModelConverter : DefaultTypeConverter
{
public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
{
var list = new List<FzLogModel>();
if (text == null) return list;
do
{
var barIndex = list.Count + 1;
var bar = new FzLogModel
{
SessionId = row.GetField<int>($"SessionId_{barIndex}"),
CommandString = row.GetField<string>($"CommandString_{barIndex}"),
Command = row.GetField<string>($"Command_{barIndex}")
};
list.Add(bar);
} while (row.CurrentIndex > 0 && row.CurrentIndex < row.Parser.Record.Length - 1);
return list;
}
public override string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData)
{
var bars = value as List<FzLogModel>;
if (bars == null) return null;
foreach (var bar in bars)
{
row.WriteField(bar.SessionId);
row.WriteField(bar.CommandString);
row.WriteField(bar.Command);
}
return null;
}
}

Categories