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; }
}
Related
(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:
I'm trying to create a .dat file with a pipe (|) separator.
I'm not sure how I can set custom column names I can use in a .dat file.
I have a class that would look like this :
public partial class ExportData
{
[DatColumnName="Test Code 123"]
public string Code { get; set; }
[DatColumnName="Test Name 123"]
public string Name { get; set; }
[DatColumnName="Test Address_123"]
public string Address { get; set; }
}
As a result, I would like to get something like this in the file:
Test Code 123|Test Name 123|Test Address_123
1|Name1|Address1
etc.
I guess below code will works for you.
I could handle it without custom attribute.
Code
public static class Extensions
{
public static string Piped<T>(this IList<T> source)
{
var sb = new StringBuilder();
var pInfo = typeof(T).GetProperties().Where(x => x.GetCustomAttributes<DisplayNameAttribute>().Any());
bool first = true;
foreach (var i in source)
{
if (first)
{
//foreach (var h in pInfo.Select(x => x.GetCustomAttribute<DisplayNameAttribute>().DisplayName))
//{
// sb.Append(h);
// sb.Append('|');
//}
sb.Append(string.Join('|', pInfo.Select(x => x.GetCustomAttribute<DisplayNameAttribute>().DisplayName)));
sb.Append(Environment.NewLine);
first = false;
}
foreach (var y in i.GetType().GetProperties().Where(x => x.GetCustomAttributes<DisplayNameAttribute>().Any()))
{
sb.Append(i.GetType().GetProperty(y.Name).GetValue(i, null).ToString());
sb.Append('|');
}
sb.Append(Environment.NewLine);
}
return sb.ToString();
}
}
Usage
public partial class ExportData
{
[DisplayName("Test Code 123")]
public string Code { get; set; }
[DisplayName("Test Name 123")]
public string Name { get; set; }
[DisplayName("whatever you want")]
public string Address { get; set; }
}
static void Main(string[] args)
{
var lst = new List<ExportData>() {
new ExportData{ Code = "c1", Name ="n1", Address = "a1" },
new ExportData{ Code = "c2", Name ="n2", Address = "a2" },
new ExportData{ Code = "c3", Name ="n3", Address = "a3" },
};
Console.WriteLine(lst.Piped());
Console.ReadKey();
}
Result
Test Code 123|Test Name 123|Whatever you want|
c1|n1|a1|
c2|n2|a2|
c3|n3|a3|
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 would like to deserialize CSVs to objects, mapping by varied column names, as illustrated by the following examples:
Input 1
Id;Name;Document
1;Matheus;555777
2;Clarice;567890
Input 2
"Id_Person";"First_Name";"Phone"
3;"John";"999-9999"
public class People
{
public int PersonId { get; set; }
public string Name { get; set; }
public string Doc { get; set; }
}
Note that the column names change by file, and a column can even be missing.
I would like to map "Id" and "Id_Person" to the PersonId property, and so on.
How to do it?
Actually found something that solved my problem: CsvHelper
Setting up:
public sealed class PessoaCSVMap : ClassMap<Pessoas>
{
public PessoaCSVMap()
{
Map(m => m.NomeCompleto).Name("Nome", "Name");
Map(m => m.Documento).Name("Documento", "Doc", "CPF");
Map(m => m.Email1).Name("Email", "Email1", "E-mail", "E-mail1");
Map(m => m.PessoaId).Ignore();
}
}
Using:
const string CSV = "Nome;Email;bleu\nMatheus;matheus.lacerda#email.com.br;blau\nClarice;null;null";
CsvReader csv = new CsvReader(new StringReader(CSV));
csv.Configuration.RegisterClassMap<PessoaCSVMap>();
csv.Configuration.Delimiter = ";";
csv.Configuration.HeaderValidated = null;
csv.Configuration.MissingFieldFound = null;
List<Pessoas> pessoas = csv.GetRecords<Pessoas>().ToList();
Assuming you already can read a specific CSV Line, this one manages to give a lot of flexibility with some reflection and generics . Not a works for all method, but it's easy to adapt it for different cases :
public IEnumerable<T> ProccessCSV<T>(StreamReader document, char divider)
where T : class, new()
{
string currentLine = reader.ReadLine();
string[] usedHeaders = currentLine.Split(divider);
while ((currentLine = reader.ReadLine()) != null)
{
var fields = currentLine.Split(divider);
var result = new T();
for (var i = 0; i < usedHeaders.Length; i++)
{
var value = fields[i];
var propInfo = typeof(T).GetProperties()[i];
result.GetType()
.GetProperty(propInfo.Name)
.SetValue(result, value);
}
yield return result;
}
}
Hope this works as a backbone to a possible solution.
For the first case, you'll need to use a separator ';' and a class like :
public class Person
{
public string Id { get; set; }
public string Name { get; set; }
public string Document { get; set; }
}
Based on : https://www.pluralsight.com/guides/microsoft-net/building-a-generic-csv-writer-reader-using-reflection
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.