I am using CsvHelper to convert dapper objects to CsvFiles. I am using classmaps to map properties for indices and name mapping. The issue is I need a row with the table title before the records are written as mentioned below:
My old code without the title:
using (var writer = new StreamWriter(#"C:\Users\NPandian\Desktop\test.csv", false, System.Text.Encoding.UTF8))
using (var csvWriter = new CsvWriter(writer))
{
var ReportName = "Test Class";
csvWriter.Configuration.RegisterClassMap(classMap);
csvWriter.WriteRecords(records);
writer.Close();
}
Old Csv:
My Current work around code:
using (var writer = new StringWriter())
using (var csvWriter = new CsvWriter(writer))
{
var ReportName = "Test Class";
csvWriter.Configuration.RegisterClassMap(classMap);
csvWriter.WriteRecords(records);
writer.Close();
return $"ReportName:, {ReportName},\n{csvWriter.Context.Writer.ToString()}";
}
My Questions:
1) Is it possible with CsvHelper?
2) If so How?
You can write fields and rows by hand.
void Main()
{
var records = new List<Foo>
{
new Foo { Id = 1, Name = "one" },
new Foo { Id = 2, Name = "two" },
};
using (var writer = new StringWriter())
using (var csv = new CsvWriter(writer))
{
csv.Configuration.RegisterClassMap<FooMap>();
csv.WriteField("Title:");
csv.WriteField("Title");
csv.NextRecord();
csv.WriteRecords(records);
writer.ToString().Dump();
}
}
public class Foo
{
public int Id { get; set; }
public string Name { get; set; }
}
public class FooMap : ClassMap<Foo>
{
public FooMap()
{
Map(m => m.Id).Index(0).Name("S.No.");
Map(m => m.Name).Index(1);
}
}
Output:
Title:,Title
S.No.,Name
1,one
2,two
CSV files have no titles. The question describes a flat text report, not a CSV file. CsvHelper, as the name implies, is a helper library that writes CSVs, it's not a full featured report generator.
The library allows other code to write arbitrary text before or after it finished though, as it works on top of an abstract TextWriter instead of a stream or file. One could even write extra text between records if required.
You can use the writer you already have to write whatever you want before or after the call to csvWriter.WriteRecords(records);, eg :
using (var writer = new StreamWriter(#"C:\Users\NPandian\Desktop\test.csv", false, System.Text.Encoding.UTF8))
using (var csvWriter = new CsvWriter(writer))
{
var ReportName = "Test Class";
csvWriter.Configuration.RegisterClassMap(classMap);
//Doesn't matter where this is called as long as it is before `WriteRecords`
writer.WriteLine($"ReportName:, {ReportName}");
csvWriter.WriteRecords(records);
//No need to explicitly close, that's what `using` is for
}
CsvWriter accepts any TextWriter object and just writes its data to that writer. It doesn't try to modify it in any other way. It won't affect any other text already written to that TextWriter
For CSVHelper version 15.0.0 and above use:
void Main()
{
var records = new List<Foo>
{
new Foo { Id = 1, Name = "one" },
new Foo { Id = 2, Name = "two" },
};
using (var writer = new StringWriter())
using (var csv = new CsvWriter(writer, CultureInfo.CurrentCulture))
{
csv.Configuration.RegisterClassMap<FooMap>();
csv.WriteField("Title:");
csv.WriteField("Title");
csv.NextRecord();
csv.WriteRecords(records);
writer.ToString().Dump();
}
}
public class Foo
{
public int Id { get; set; }
public string Name { get; set; }
}
public class FooMap : ClassMap<Foo>
{
public FooMap()
{
Map(m => m.Id).Index(0).Name("S.No.");
Map(m => m.Name).Index(1);
}
}
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; }
}
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
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; }
}
I have an implementation of CSV helper which currently reads CSV's in the traditional format:
Name, Address, Age
"Foo", "Foo's address", 24
"Bar", "Bar's address", 19
I use a class map to map the fields by name to my Person object in the following way:
using (var reader = new StreamReader(file, Encoding.UTF8))
{
using (var csvReader = new CsvReader(reader))
{
csvReader.Configuration.RegisterClassMap<ContentMapper>();
var records = csvReader.GetRecords<Person>().ToArray();
}
}
I need an implementation which reads the exact same data but in a vertical format which originates from data where the column names are vertical in the first column and the data follows in columns instead of rows.
Name, "Foo", "Bar"
Address, "Foo's address", "Bar's address"
Age, 24, 19
What would be the best way to handle a CSV in this format whilst retaining the original mapping?
Try this one
public class Person
{
public string Name { get; set; }
public string Address { get; set; }
public int Age { get; set; }
public List<Person> ReadFile(string path)
{
char[] charsToTrim1 = {'\\', ' ', '"', '\"'};
var fileData = File.ReadAllLines(path);
var outputData = new List<Person>();
for (var i = 0; i < fileData.Length; i++)
{
var tmpData = fileData[i].Split(',');
for (var j = 0; j < tmpData.Length; j++)
{
var t1 = tmpData[j].Trim(charsToTrim1);
if (j == 0)
continue;
switch (i)
{
case 0:
{
var tmPerson = new Person {Name = t1};
outputData.Add(tmPerson);
}
break;
case 1:
{
outputData[j - 1].Address = t1;
}
break;
case 2:
{
outputData[j - 1].Age = Convert.ToInt32(t1);
}
break;
}
}
}
return outputData;
}
}
This could probably be cleaned up a bit, but it does seem to work.
Use CsvHelper to pull in the records as List<dynamic>
Rotate the records into a new List<dynamic> so the first field in each row becomes the property name of the dynamic record.
Use CsvHelper to write the new list to memory.
Use CsvHelper to read the records back in using your ClassMap
public class Program
{
public static void Main(string[] args)
{
var flippedRecords = new List<dynamic>();
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("Name,Foo,Bar");
writer.WriteLine("Address,Foo's address,\"Bar's address with, comma\"");
writer.WriteLine("Age,24,19");
writer.Flush();
stream.Position = 0;
csv.Configuration.HasHeaderRecord = false;
// Get the records from the CSV file.
var records = csv.GetRecords<dynamic>().ToList();
// Rotate the records into a new dynamic list.
var rows = new List<IDictionary<string, object>>();
foreach (var row in records)
{
rows.Add(row as IDictionary<string, object>);
}
for (int i = 2; i <= rows[0].Count; i++)
{
var flippedRecord = new ExpandoObject() as IDictionary<string, object>;
foreach (var row in rows)
{
flippedRecord.Add((string)row["Field1"], row["Field" + i]);
}
flippedRecords.Add(flippedRecord);
}
}
using (MemoryStream stream = new MemoryStream())
using (StreamWriter writer = new StreamWriter(stream))
using (CsvWriter csvWriter = new CsvWriter(writer))
using (StreamReader reader = new StreamReader(stream))
using (CsvReader csvReader = new CsvReader(reader))
{
// Write the new list to memory
csvWriter.WriteRecords(flippedRecords);
writer.Flush();
stream.Position = 0;
// Read in the person records using a ClassMap.
csvReader.Configuration.RegisterClassMap<PersonMap>();
var people = csvReader.GetRecords<Person>().ToArray();
}
}
}
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
public int Age { get; set; }
}
public class PersonMap : ClassMap<Person>
{
public PersonMap()
{
Map(m => m.FirstName).Name("Name");
Map(m => m.Address);
Map(m => m.Age);
}
}
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.