Dynamic creation of columns using csvHelper
Dictionary isn't supported but ExpandoObject is supported.
https://github.com/JoshClose/CsvHelper/blob/48e70742e06007dae3a635c418b7e3358f667c4f/src/CsvHelper.Tests/Writing/MultipleHeadersTest.cs
https://github.com/JoshClose/CsvHelper/blob/b74a2f95a101158f4cdedd25fae6e8392b58855b/src/CsvHelper.Tests/Writing/DynamicTests.cs
If you follow the first link above you'll find the WriteDynamicHeader method in use on lines 50 & 57.
With help of an extension method I create an ExpandoObject for each record and use CsvHelper to write that object.The Dictionary<string, object>
parameter named document
is what I wish to create the CSV record from.
public static class DictionaryCsvExtentions
{
public static dynamic BuildCsvObject(this Dictionary<string, object> document)
{
dynamic csvObj = new ExpandoObject();
foreach (var p in document)
{
AddProperty(csvObj, p.Key, p.Value);
}
return csvObj;
}
private static void AddProperty(ExpandoObject expando, string propertyName, object propertyValue)
{
var expandoDict = expando as IDictionary<string, object>;
if (expandoDict.ContainsKey(propertyName))
{
expandoDict[propertyName] = propertyValue;
}
else
{
expandoDict.Add(propertyName, propertyValue);
}
}
}
Now I can create an ExpandoObject from my dictionary like this
var csvObj = myDictonary.BuildCsvObject();
and with that, following Josh's tests in the link above we have all we need to use a dictionary fairly seamlessly with CsvHelper. I don't think this is a better solution to Michael's, just a different approach.
credit where credit is due the basic ExpandoObject from dictionary code is from here (where there is a lot more explanation!) https://www.oreilly.com/learning/building-c-objects-dynamically
I don't think that writing a dictionary is supported at this time. For one thing, CsvHelper would have a difficult time knowing what headers to write. Fortunately, it's not too complex to use CsvWriter manually, writing a field at a time. If we assume that each Worker has the same keys in customerField
then your code might look something like this.
var firstWorker = workerList.First();
var keys = firstWorker.customerField.Keys.ToList();
var headers = new []{ "name", "phone", "age"}.Concat(keys).ToList();
var csv = new CsvWriter( textWriter );
// Write the headers
foreach( var header in headers )
{
csv.WriteField(header);
}
csv.NextRecord();
// Write the rows
foreach( var item in workerList)
{
csv.WriteField(item.name);
csv.WriteField(item.phone);
csv.WriteField(item.age);
var dict = worker.customerField;
foreach (var key in keys)
{
csv.WriteField(dict[key]);
}
csv.NextRecord();
}
This code is untested, but should get you pretty close to the behavior you need. If the customerField
dictionary keys are not consistent in the list then the code would be a bit more complicated but it's still solvable.
Recently had the same problem and for the sake of completeness posting another answer. We had fairly complex ClassMap<Worker>
like class and did want to loose it. Also, we needed both CSV write and read hence the ExpandoObject
from the robs answer was needed as well. Eventually, the approach is like combination of the Michael Richardson and robs answer that should bring best of both worlds.
On top of that, to distinguish dictionary fields in the CSV file during read it is a good idea to prefix them with something like "customerField."
.
First we need conversion of dictionary to/from Worker.customerField:
public static class WorkerExtensions
{
const string CustomerFieldPrefix = nameof(Worker.customerField) + ".";
public static dynamic GetCustomerFieldExpando(this Worker worker)
{
var expando = new ExpandoObject() as IDictionary<string, object>;
foreach (var fieldPair in worker.customerField)
{
expando[CustomerFieldPrefix + fieldPair.Key] = fieldPair.Value ?? "";
}
return expando;
}
public static void SetCustomerField(this Worker worker, ExpandoObject expando)
{
var columnsToValues = expando as IDictionary<string, object>;
foreach (var columnValuePair in columnsToValues)
{
if (columnValuePair.Key.StartsWith(CustomerFieldPrefix)
&& columnValuePair.Key.Length > CustomerFieldPrefix.Length)
{
string key = columnValuePair.Key.Substring(CustomerFieldPrefix.Length);
worker.customerField[key] = columnValuePair.Value;
}
}
}
}
Next, CSV write can use both ClassMap
and ExpandoObject
and looks like this:
csv.Configuration.HasHeaderRecord = true;
csv.Configuration.RegisterClassMap<WorkerMap>();
csv.WriteHeader<Worker>();
(workers.First().GetCustomerFieldExpando() as IDictionary<string, object>)
.Keys.ToList().ForEach(key => csv.WriteField(key));
csv.NextRecord();
foreach (var worker in workers)
{
csv.WriteRecord(worker);
csv.WriteRecord(worker.GetCustomerFieldExpando());
csv.NextRecord();
}
Finally, CSV read can also combine both ClassMap
and ExpandoObject
:
List<Worker> workers = new List<Worker>();
csv.Configuration.HasHeaderRecord = true;
csv.Configuration.RegisterClassMap<WorkerMap>();
csv.Read();
csv.ReadHeader();
var columns = csv.Context.HeaderRecord;
while (csv.Read())
{
var worker = csv.GetRecord<Worker>();
workers.Add(worker);
ExpandoObject expando = csv.GetRecord<dynamic>();
worker.SetCustomerField(expando);
}
In case of CSV read things get more complicated if you want to read real types into dictionary values (not just strings). We needed predefined associations between dictionary keys and data types to be able to convert to proper types from the ExpandoObject
.