CsvHelper icon indicating copy to clipboard operation
CsvHelper copied to clipboard

Write a CSV file with a dictionary field as separate column headers

Open nfplee opened this issue 2 years ago • 3 comments

Is it possible to create a CSV file which has a dictionary collection, where each key in the dictionary is mapped as separate column headers. This is the best I've come up with so far:

public class ProductViewModel {
    public int Id { get; set; }
    public string Name { get; set; }
    public IDictionary<string, object?> Attributes { get; set; }
}

public class ProductViewModelMap : ClassMap<ProductViewModel> {
    public ProductViewModelMap() {
        Map(p => p.Id);
        Map(p => p.Name);
        Map(m => m.Attributes).Name(new[] { "Attribute1", "Attribute2" });
    }
}

var products = new List<ProductViewModel> {
    new ProductViewModel { Id = 1, Name = "Product 1", Attributes = new Dictionary<string, object?> { { "Attribute1", 10 }, { "Attribute2", "Value" } } },
    new ProductViewModel { Id = 2, Name = "Product 2", Attributes = new Dictionary<string, object?> { { "Attribute1", 20 }, { "Attribute2", "Value" } } }
};

using (var writer = new StreamWriter("file.csv"))
using (var csv = new CsvWriter(writer, new CsvConfiguration(CultureInfo.InvariantCulture))) {
    csv.Context.RegisterClassMap<ProductViewModelMap>();
    await csv.WriteRecordsAsync(products);
}

However this produces:

Id,Name,Attribute1
1,Product 1,10,Value
2,Product 2,20,Value

But I would like:

Id,Name,Attribute1,Attribute2
1,Product 1,10,Value
2,Product 2,20,Value

I've tried numerous things but nothing seems to work. Ideally I'd like to use a ClassMap instead of writing the raw contents as I think this should be supported out of the box.

nfplee avatar May 17 '23 10:05 nfplee

why you've closed it? have you found the solution to get expected result Id,Name,Attribute1,Attribute2? I have the same problem now.

tattdogg avatar Aug 02 '24 16:08 tattdogg

Hi @b-maslennikov,

I'm not sure why I closed this out. For now I got around this by doing the following:

using (var writer = new StreamWriter("file.csv"))
using (var csv = new CsvWriter(writer, new CsvConfiguration(CultureInfo.InvariantCulture) {
    HasHeaderRecord = false
})) {
    csv.Context.RegisterClassMap<ProductViewModelMap>();
    csv.WriteHeader<ProductViewModel>(["Attribute1", "Attribute2"]);
    await csv.WriteRecordsAsync(products);
}```

Here's my extension method to manually write the header:

```cs
public static class CsvWriterExtensions {
    public static void WriteHeader<T>(this CsvWriter csv, IEnumerable<string> attributeNames) {
        foreach (var property in typeof(T).GetProperties().Where(p => p.Name != "Attributes")) {
            csv.WriteField(property.Name);
        }

        foreach (var attributeName in attributeNames) {
            csv.WriteField(attributeName);
        }

        csv.NextRecord();
    }
}

nfplee avatar Aug 06 '24 09:08 nfplee

My temporary solution: replace classmap with method that converts T to dynamic. + WriteDynamicHeader() It would great to have requested functionality...

tattdogg avatar Aug 08 '24 20:08 tattdogg