(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:
Related
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; }
}
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 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;
}
}
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; }
}
I am using the CsvHelper library to generate a CSV file from an IEnumerable<Person>, where Person is a basic class.
public class Person
{
public string DisplayName { get; set; }
public int Age { get; set; }
}
I need to write quoted headers, i.e. instead of "DisplayName", the column in the resulting file should be "Display Name".
How can I do this?
Create a ClassMap.
void Main()
{
using (var stream = new MemoryStream())
using (var writer = new StreamWriter(stream))
using (var reader = new StreamReader(stream))
using (var csv = new CsvWriter(writer))
{
var records = new List<Person>
{
new Test { DisplayName = "one", Age = 1},
new Test { DisplayName = "two", Age = 2 },
};
csv.Configuration.RegisterClassMap<PersonMap>();
csv.WriteRecords(records);
writer.Flush();
stream.Position = 0;
Console.WriteLine(reader.ReadToEnd());
}
}
public class Person
{
public string DisplayName { get; set; }
public int Age { get; set; }
}
public sealed class PersonMap : ClassMap<Person>
{
public PersonMap()
{
Map(m => m.DisplayName).Name("Display Name");
Map(m => m.Age);
}
}
Output:
Display Name,Age
one,1
two,2
I had no headers with workaround, so I did this quick workaround instead:
foreach (var property in typeof(MyCsvClass).GetProperties())
{
csvWriter.WriteField(property.Name.Replace('_', ' '));
}
csvWriter.NextRecord();
csvWriter.WriteRecords(models);
This takes the property names and replace underscore with space, so I could just name the properties with underscores instead of spaces, and it mapped correctly.
It requires, however, that you use the HasHeaderRecord = false option on the CsvWriter.