JoshClose / CsvHelper

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

Add support for IDictionary<string, object?> #2161

Open nfplee opened 1 year ago

nfplee commented 1 year ago

Say I have the following model and mapping:

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

public class ProductViewModelMap : ClassMap<ProductViewModel>
{
    public ProductViewModelMap()
    {
        AutoMap(CultureInfo.InvariantCulture);
        Map(m => m.Attributes).Index(2, 3);
    }
}

Now if I say:

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 2" } } }
};

using (var writer = new StreamWriter("file.csv"))
using (var csv = new CsvWriter(writer, new CsvConfiguration(CultureInfo.InvariantCulture)
{
    HasHeaderRecord = false
}))
{
    writer.WriteLine("Id,Name,Attribute1,Attribute2");
    await csv.WriteRecordsAsync(products);
}

using (var reader = new StreamReader("file.csv"))
using (var csv = new CsvReader(reader, new CsvConfiguration(CultureInfo.InvariantCulture)))
{
    csv.Context.RegisterClassMap<ProductViewModelMap>();

    while (await csv.ReadAsync())
    {
        var record = csv.GetRecord<ProductViewModel>()!;
    }
}

I receive the error:

CsvHelper.TypeConversion.TypeConverterException: 'The conversion cannot be performed. Text: '10' MemberName: Attributes MemberType: System.Collections.Generic.IDictionary`2[[System.String, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Object, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]] TypeConverter: 'CsvHelper.TypeConversion.IDictionaryGenericConverter' IReader state: ColumnCount: 0 CurrentIndex: 2 HeaderRecord: ["Id","Name","Attribute1","Attribute2"] IParser state: ByteCount: 0 CharCount: 53 Row: 2 RawRow: 2 Count: 4 RawRecord: 1,Product 1,10,Value'

I added the following dictionary converter to fix my issue:

public class MyIDictionaryGenericConverter : IDictionaryConverter
{
    public override object? ConvertFromString(string? text, IReaderRow row, MemberMapData memberMapData)
    {
        var dictionary = new Dictionary<string, object?>();

        var indexEnd = memberMapData.IndexEnd < memberMapData.Index
            ? row.Parser.Count - 1
            : memberMapData.IndexEnd;

        for (var i = memberMapData.Index; i <= indexEnd; i++)
        {
            dictionary.Add(row.HeaderRecord[i], row.GetField(i));
        }

        return dictionary;
    }
}

But it would be nice if this was supported out of the box. Also it would be much nicer if there was a better way of adding a header for all the keys in the dictionary. The only way I have managed to achieve it, is to write it manually (as shown above).

nfplee commented 1 year ago

Just a follow up that I had to modify my converter to feed in the value types. For example:

public class MyIDictionaryGenericConverter : IDictionaryConverter
{
    private readonly Type[] _valueTypes;

    public MyIDictionaryGenericConverter(Type[] valueTypes)
    {
        _valueTypes = valueTypes;
    }

    public override object? ConvertFromString(string? text, IReaderRow row, MemberMapData memberMapData)
    {
        var dictionary = new Dictionary<string, object?>();

        var indexEnd = memberMapData.IndexEnd < memberMapData.Index
            ? row.Parser.Count - 1
            : memberMapData.IndexEnd;
        var counter = 0;

        for (var i = memberMapData.Index; i <= indexEnd; i++)
        {
            var value = row.GetField(i);
            var converter = row.Context.TypeConverterCache.GetConverter(_valueTypes[counter]);
            var field = !string.IsNullOrEmpty(value) ? converter.ConvertFromString(value, row, memberMapData) : null;
            dictionary.Add(row.HeaderRecord[i], field);
            counter++;
        }

        return dictionary;
    }
}