CsvHelper write mapper one property to multiple columns - c#

I'm using csvHelper (version 2.8.4) to write a class to csv.
My class looks like this:
public class classA
{
public int Amount { get; set; }
public Dictionary<string, string> Dict{ get; set; }
}
Is it possible to write a mapper that maps Dict property to multiple columns? using some sort of converter?
for example if the class has the values:
Amount = 15
Dict = new Dictionary<string,string>{["a1"] = "a2",["b1"] = "b2"}
I want the resulting csv to be:
Amount,a1,b1
15,a2,b2
Thanks!

Possibly the easiest way is going to be to manually write out the dictionary part.
*** Update to work with CsvHelper Version 2.8.4 ***
void Main()
{
var records = new List<classA>
{
new classA {
Amount = 15,
Dict = new Dictionary<string,string>{["a1"] = "a2",["b1"] = "b2"}
}
};
using (var csv = new CsvWriter(Console.Out))
{
var dict = records.First().Dict;
var properties = typeof(classA).GetProperties();
foreach (PropertyInfo property in properties)
{
if (property.Name != "Dict")
{
csv.WriteField(property.Name);
}
}
foreach (var item in dict)
{
csv.WriteField(item.Key);
}
csv.NextRecord();
foreach (var record in records)
{
foreach (PropertyInfo property in properties)
{
if (property.Name != "Dict")
{
csv.WriteField(property.GetValue(record));
}
}
foreach (var item in record.Dict)
{
csv.WriteField(item.Value);
}
csv.NextRecord();
}
}
}
// You can define other methods, fields, classes and namespaces here
public class classA
{
public int Amount { get; set; }
public Dictionary<string, string> Dict { get; set; }
}
*** Works for current Version 27.2.1 ***
void Main()
{
var records = new List<classA>
{
new classA { Amount = 15, Dict = new Dictionary<string,string>{["a1"] = "a2",["b1"] = "b2"} },
};
using (var csv = new CsvWriter(Console.Out, CultureInfo.InvariantCulture))
{
var dict = records.First().Dict;
csv.WriteHeader<classA>();
foreach (var item in dict)
{
csv.WriteField(item.Key);
}
csv.NextRecord();
foreach (var record in records)
{
csv.WriteRecord(record);
foreach (var item in record.Dict)
{
csv.WriteField(item.Value);
}
csv.NextRecord();
}
}
}
public class classA
{
public int Amount { get; set; }
public Dictionary<string, string> Dict { get; set; }
}

As mentioned in linked question, you may use ExpandoObject to serialize dictionary.
The following code will work for writing to CSV only, it's converting classA objects to ExpandoObject during serialization, including Amount property which is added manually.
public static List<dynamic> ToExpandoObjects(IReadOnlyList<classA> aObjects)
{
var allKeys = aObjects
.SelectMany(a => a.Dict.Keys)
.Distinct()
.ToHashSet();
var result = new List<dynamic>();
foreach (var a in aObjects)
{
var asExpando = new ExpandoObject();
var asDictionary = (IDictionary<string, object>)asExpando;
asDictionary[nameof(classA.Amount)] = a.Amount;
foreach (var key in allKeys)
{
if(a.Dict.TryGetValue(key, out var value))
asDictionary[key] = value;
else
asDictionary[key] = null;
}
result.Add(asExpando);
}
return result;
}
...
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(ToExpandoObjects(records));
}
E.g. called as:
var records = new[] {
new classA
{
Amount = 15,
Dict = new Dictionary<string,string>{["a1"] = "a2",["b1"] = "b2"}
},
new classA
{
Amount = 15,
Dict = new Dictionary<string,string>{["c1"] = "c2",["b1"] = "b2"}
}
};
StringBuilder sb = new StringBuilder();
using (var writer = new StringWriter(sb))
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(ToExpandoObjects(records));
}
Console.WriteLine(sb.ToString());
produces
Amount
a1
b1
c1
15
a2
b2
15
b2
c2

Related

csvHelper add new column beside last one

Edit, thank you for the suggestion of using csvhelper, this is actually helping quite a lot.
What I have done is create a new method like so:
public static void AppendFile<T>(FileInfo fi, List<T> report)
{
var settings = new CsvConfiguration(new CultureInfo("en-GB"))
{
//Delimiter = ";"
};
using var stream = File.Open(fi.FullName, FileMode.Append);
using var writer = new StreamWriter(stream);
using (var csv = new CsvWriter(writer, settings))
{
csv.WriteRecords(report);
}
}
And gone through the example on the csvhelper site, creating a new class:
public class Foo
{
public int Id { get; set; }
public string Name { get; set; }
}
and then creating a new list:
public class Foo
{
public int Id { get; set; }
public string Name { get; set; }
}
and calling it like so:
AppendToFile.AppendFile(exportFile, records1);
This is working better than what I had before but, instead adding the new columns beside the last column, they are getting added at the bottom of the file.
For clarification,
what I'm trying to do:
what I'm getting:
As you'll be able to see, it's just being added as new rows rather than being separate columns, what do I need to change?
Another way you can accomplish your goal is to write each object to the row by hand using csvWriter.WriteHeader and csvWriter.WriteRecord.
void Main()
{
var settings = new CsvConfiguration(new CultureInfo("en-GB"))
{
Delimiter = ";"
};
var fooList = new List<Foo>()
{
new Foo { Id = 67, Name = "test1,test2"}
};
List<MyClass> records;
using (var reader = new StringReader("Author,Admin Owner\nChris Jones,\nJohn Thompson,\nBrian Oates,"))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
records = csv.GetRecords<MyClass>().ToList();
}
using var csvWriter = new CsvWriter(Console.Out, settings);
csvWriter.WriteHeader<MyClass>();
csvWriter.WriteHeader<Foo>();
csvWriter.NextRecord();
var i = 0;
foreach (var record in records)
{
csvWriter.WriteRecord(record);
if (i < fooList.Count())
{
csvWriter.WriteRecord(fooList[i]);
}
csvWriter.NextRecord();
i++;
}
}
public class MyClass
{
public string Author { get; set; }
[Name("Admin Owner")]
public string AdminOwner { get; set; }
}
public class Foo
{
public int Id { get; set; }
public string Name { get; set; }
}
I managed to figure out a way that worked for me, might not be the most efficient but it does work.
public static void AppendFile(FileInfo fi, List<string> newColumns, DataTable newRows)
{
var settings = new CsvConfiguration(new CultureInfo("en-GB"))
{
Delimiter = ";"
};
var dt = new DataTable();
using (var reader = new StreamReader(fi.FullName))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
using (var dataReader = new CsvDataReader(csv))
{
dt.Load(dataReader);
foreach (var title in newColumns)
{
dt.Columns.Add(title);
}
dt.Rows.Clear();
foreach (DataRow row in newRows.Rows)
{
dt.Rows.Add(row.ItemArray);
}
}
}
using var streamWriter = new StreamWriter(fi.FullName);
using var csvWriter = new CsvWriter(streamWriter, settings);
// Write columns
foreach (DataColumn column in dt.Columns)
{
csvWriter.WriteField(column.ColumnName);
}
csvWriter.NextRecord();
// Write row values
foreach (DataRow row in dt.Rows)
{
for (var i = 0; i < dt.Columns.Count; i++)
{
csvWriter.WriteField(row[i]);
}
csvWriter.NextRecord();
}
}
I start by getting the contents of the csv file into a data table and then adding in the new columns that I need. I then clear all the rows in the datatable and add new ones in (the data that is removed is added back in via the newRows parameter) and then write the datatable to the csv file

Create the List Type dynamically (or a different kind of collection?)

I am writing a class that reads different kinds of CSV files. It picks out the important information based on Model classes, where the properties of the model class are the column names that I want to grab. For example, I could have an OutlookModel with columns FromAddress and ToAddress. Or I could have a SalesforceModel with totally different columns.
When the reader class parses through the rows and columns, it loads up the cells into an instance of the model class. In the code below, the argument className = OutlookModel. The most relevant lines of code here are the signature and the return...
protected void MapColumns(string row, string className, List<OutlookModel> list)
{
string[] cols = row.Split(',');
// create a model to save the important columns
var model = Activator.CreateInstance(nameSpace, nameSpace + className);
int j = 0;
if (cols.Length > 0)
{
foreach (var c in cols)
{
// is this column index one of our important columns?
if (Ordinals.ContainsKey(j))
{
// this is a column we care about, so set the model property
model.GetType().GetProperty(Ordinals[j]).SetValue(model, c);
}
j++;
}
}
list.Add(model);
}
The problem I am having is the collection of model objects. If I define the object as List< OutlookModel > in the arguments, then the method is not extensible. If I define it as List< object >, then (i think) I have to cast the inside list to use my properties which are all different between the models.
I am fairly new to C#. Is there a better way to capture these different model types into a list/array/collection/whatever so that I can then apply logic to the lists?
So first of all i suggest to add a custom attribute to mark the properties you want to read from the csv, so you don't run into any problem when you have to add something later and you don't have to rely on too many magic strings. Here is my test setup:
class ReadFromCsvAttribute : Attribute { }
class OutlookModel
{
public int DontSetThisValueFromCsv { get; set; }
[ReadFromCsv]
public string FromAddress { get; set; }
[ReadFromCsv]
public string ToAddress { get; set; }
}
class SalesForceModel
{
[ReadFromCsv]
public string Name { get; set; }
[ReadFromCsv]
public string Age { get; set; }
}
static void Main(string[] args)
{
string outlookSample = "Id,FromAddress,ToAddress,Useless\r\n" +
"1,a#b.com,c#d.com,asdf\r\n" +
"3,y#z.com,foo#bar.com,baz";
string salesForceSample = "Id,Name,Age\r\n" +
"1,John,30\r\n" +
"2,Doe,100";
var outlook = ReadFromCsv<OutlookModel>(outlookSample);
var salesForce = ReadFromCsv<SalesForceModel>(salesForceSample);
}
I put together this generic method to read whatever model you want from the data:
static List<T> ReadFromCsv<T>(string data)
{
var objs = new List<T>();
var rows = data.Split(new[] {"\r\n"}, StringSplitOptions.None);
//create index, header dict
var headers = rows[0].Split(',').Select((value, index) => new {value, index})
.ToDictionary(pair => pair.index, pair => pair.value);
//get properties to find and cache them for the moment
var propertiesToFind = typeof (T).GetProperties().Where(x => x.GetCustomAttributes<ReadFromCsvAttribute>().Any());
//create index, propertyinfo dict
var indexToPropertyDict =
headers.Where(kv => propertiesToFind.Select(x => x.Name).Contains(kv.Value))
.ToDictionary(x => x.Key, x => propertiesToFind.Single(p => p.Name == x.Value));
foreach (var row in rows.Skip(1))
{
var obj = (T)Activator.CreateInstance(typeof(T));
var cells = row.Split(',');
for (int i = 0; i < cells.Length; i++)
{
if (indexToPropertyDict.ContainsKey(i))
{
//set data
indexToPropertyDict[i].SetValue(obj, cells[i]);
}
}
objs.Add(obj);
}
return objs;
}
Here's another sample. Since you're new to c#, I've avoided linq and extension methods as much as possible. Just copy it into a console app and run.
Also, I like theHennyy recommendation of using .net attributes to describe a class but only if you have full control of your ecosystem.
public class Account
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class LastNameAccount
{
public string LastName { get; set; }
public string Address { get; set; }
}
public class Program
{
public static void Main(string[] args)
{
Test1();
}
private static void Test1()
{
/*
* defines the result of your CSV parsing.
*/
List<string> csvColumns = new List<string> { "FirstName", "LastName" };
List<List<string>> csvRows = new List<List<string>>() {
new List<string>(){"John","Doe"},
new List<string>(){"Bill", "Nie"}
};
//Map the CSV files to Account type and output it
var accounts = Map<Account>(csvColumns, csvRows);
if (accounts != null)
{
foreach (var a in accounts)
{
Console.WriteLine("Account: {0} {1}", a.FirstName, a.LastName);
}
}
//Map the CSV files to LastNameAccount type and output it
var accounts2 = Map<LastNameAccount>(csvColumns, csvRows);
if (accounts2 != null)
{
foreach (var a in accounts2)
{
Console.WriteLine("Last Name Account: {0} {1}", a.LastName, a.Address);
}
}
}
private static List<T> Map<T>(List<string> columns, List<List<string>> rows)
where T : class, new()
{
//reflect the type once and get valid columns
Type typeT = typeof(T);
Dictionary<int, PropertyInfo> validColumns = new Dictionary<int, PropertyInfo>();
for (int columnIndex = 0; columnIndex < columns.Count; columnIndex++)
{
var propertyInfo = typeT.GetProperty(columns[columnIndex]);
if (propertyInfo != null)
{
validColumns.Add(columnIndex, propertyInfo);
}
}
//start mapping to T
List<T> output = null;
if (validColumns.Count > 0)
{
output = new List<T>();
foreach (var row in rows)
{
//create new T
var tempT = new T();
//populate T's properties
foreach (var col in validColumns)
{
var propertyInfo = col.Value;
var columnIndex = col.Key;
propertyInfo.SetValue(tempT, row[columnIndex]);
}
//add it
output.Add(tempT);
}
}
return output;
}
}

Reading the dictionary element values

I have the below code where I want to read the value of dictionary 'filter' using for loop.'filter[1]'again has two values. Since dictionary is key value pair, how do I access the element value below.
class program
{
public string RecSys { get; set; }
public string AnsSys { get; set; }
public string IsAddOn { get; set; }
public string Country { get; set; }
public static void Main()
{
program p1 = new program();
program p2 = new program();
program p3 = new program();
List<program> List = new List<program>();
p1.RecSys = "RY1";
p1.AnsSys = "CSCLRC";
p1.IsAddOn = "P";
p1.Country = "India";
List.Add(p1);
p2.RecSys = "RY1";
p2.AnsSys = "APEX";
p2.IsAddOn = "primary";
p2.Country = "Pakistan";
List.Add(p2);
p3.RecSys = "RY1";
p3.AnsSys = "APEX";
p3.IsAddOn = "Addon";
p3.Country = "Pakistan";
List.Add(p3);
var filter = List.GroupBy(item => new { item.RecSys, item.AnsSys }).ToDictionary(grp => grp.Key, grp => grp.ToList()).Values;
for (int i = 0; i < filter.Count; i++)
{
// read the values. 'filter[1]'again has two values
}
}
}
You can fetch the values using two foreach loop like this:-
foreach (var item in filter)
{
foreach (var innerItem in item)
{
Console.WriteLine(innerItem.IsAddOn);
Console.WriteLine(innerItem.Country);
//and so on..
}
}
Or else if you want all the Values of dictionary at once then you can flatten it using SelectMany:-
var filter = List.GroupBy(item => new { item.RecSys, item.AnsSys })
.ToDictionary(grp => grp.Key, grp => grp.ToList())
.SelectMany(x => x.Value);
and finally iterate over the items using a single foreach loop:-
foreach (var item in filter)
{
Console.WriteLine(item.Country);
Console.WriteLine(item.AnsSys);
//and so on..
}

Iterate through the collection and add items in List<T>

I have following classes:-
public class SiteMapSection
{
public string sectionUrl { get; set; }
public List<SiteMapSubSection> subSection { get; set; }
}
public class SiteMapSubSection
{
public string subSectionUrl { get; set; }
public List<SiteMapArticle> article { get; set; }
}
public class SiteMapArticle
{
public string url { get; set; }
}
I'm using SiteMapSection class as a Type in the list:-
List<SiteMapSection> siteMapSection = new List<SiteMapSection>();
Now, i'm trying to add items in 'siteMapSection' list, as given below:-
foreach (var section in sections)
{
.....
siteMapSection.Add(new SiteMapSection { sectionUrl = section.Url });
.....
foreach (var subsection in subsections)
{
.....
siteMapSection.Add(new SiteMapSubSection { ??stuck_here?? });
.....
var articles = GetNextArticles(0, subSectionId, true, false);
.....
foreach(var article in articles)
{
siteMapSection.Add(new SiteMapArticle { ??stuck_here?? });
}
}
}
How do I iterate through the collection and add items in List siteMapSection.
Updated Code, this also not works i see only siteMapSection.Add(sms) item got added but other nested still empty
List<SiteMapSection> siteMapSection = new List<SiteMapSection>();
SectionArticle sa = new SectionArticle();
foreach (BE.Section section in Sections.Find(websiteId, parentSectionId))
{
int sectionId = section.Id;
var sms = new SiteMapSection();
sms.sectionUrl = Sections.VirtualPath(section) + ".aspx";
var _subsections = new List<SiteMapSubSection>();
foreach (BE.Section subsection in Sections.Find(websiteId, sectionId))
{
int subSectionId = subsection.Id;
var smss = new SiteMapSubSection();
smss.subSectionUrl = Sections.VirtualPath(subsection) + ".aspx";
var articles = sa.GetArticlesForSection(websiteId, subSectionId, 10);
var _articles = new List<SiteMapArticle>();
foreach (var article in articles)
{
var sma = new SiteMapArticle();
sma.url = article.Code + ".aspx";
_articles.Add(sma);
}
_subsections.Add(smss);
}
siteMapSection.Add(sms);
}
I just realized that you are trying to add different types to List<SiteMapSection>. You can not add different types to a generic list. When creating a list you are defining the types which are allowed in the list, where as you are trying to add different types.
You need to change
siteMapSection.Add(new SiteMapSubSection { ??stuck_here?? });
to
siteMapSection.Add(new SiteMapSection { ??stuck_here?? });
If you provide a bit more context perhaps we could give you a better approach in general.
Hope this helps.
foreach (var section in sections)
{
.....
var sms = new SiteMapSection { sectionUrl = section.Url };
sms.subSection = new List<SiteMapSubSection>();
.....
foreach (var subsection in subsections)
{
.....
var smss = new new SiteMapSubSection { subsection }
ssms.article = new List<SiteMapArticle>();
.....
var articles = GetNextArticles(0, subSectionId, true, false);
.....
foreach(var article in articles)
{
smss.article.Add(new SiteMapArticle { article });
}
sms.subSection.Add(ssms);
}
siteMapSection.Add(sms);
}

Can i store serializable array data in DataColumn?

I am trying to automatically convert object's properties to DataTable (object is array and has properties that instantiated from special class which has value type).
The code:
static public DataTable f_GetDataTableFromClassObject(object _objInstance)
{
// geri dönecek datatable
DataTable dataTable = new DataTable();
// nesnenin propertyleri içinde dolanalım ve datatable içine kolon olarak ekleyelim.
foreach (var p in f_GetProperties(_objInstance))
{
if (p.PropertyType.IsArray)
{
if (p.PropertyType.BaseType.Attributes.ToString().IndexOf("Serializable")>-1)
{
// Now i want to store to DataColumn this properties which is instantiated DifferentClass[] and Serializable
}
}
else
{
dataTable.Columns.Add(new DataColumn(p.Name, p.PropertyType));
}
}
// ve tablomuz.
return dataTable;
}
What should i do to store this Array in DataColumn?
class Program
{
static void Main(string[] args)
{
Person cenk = new Person() { adi = "Cenk", yasi = 18 };
List<Person> lst = new List<Person>()
{
cenk,
new Person() {adi = "Cem", yasi = 17, harfler = new[] {"a","b","c"}},
new Person() {adi = "Canan", yasi = 16, harfler = new[] {"a","b","c"}}
};
DataTable dataTable = new DataTable();
PropertyInfo[] pinfo = props();
//var s = pinfo.Select(p => dataTable.Columns.Add(new DataColumn(p.Name, (p.PropertyType.FullName).GetType())));
foreach (var p in pinfo)
{
dataTable.Columns.Add(new DataColumn(p.Name, p.PropertyType));
}
foreach (Person person in lst)
{
DataRow dr = dataTable.NewRow();
foreach (PropertyInfo info in person.GetType().GetProperties())
{
object oo = person.GetType().GetProperty(info.Name).GetValue(person, null);
dr[info.Name] = oo;
}
dataTable.Rows.Add(dr);
}
}
static public PropertyInfo[] props()
{
return (new Person()).GetType().GetProperties();
}
}
public class Person
{
public string adi { get; set; }
public int yasi { get; set; }
public string[] harfler { get; set; }
}

Categories

Resources