JoshClose / CsvHelper

Library to help reading and writing CSV files
http://joshclose.github.io/CsvHelper/
Other
4.74k stars 1.06k forks source link

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

Open nfplee opened 1 year ago

nfplee commented 1 year ago

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.

b-maslennikov commented 2 months ago

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

nfplee commented 2 months ago

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();
    }
}
b-maslennikov commented 1 month ago

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