JoshClose / CsvHelper

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

Custom IEnumerable type cannot be parsed using ITypeConverterFactory #2153

Closed francescodente closed 8 months ago

francescodente commented 1 year ago

Describe the bug I have a custom Option<T> type that I use to represent optional types as a replacement to nullable types. This type also implements IEnumerable<T>. In order to convert fields of that type, I have written a custom ITypeConverterFactory and registered it in the CsvContext. However, when I try to use GetField<Option<int>> for example, the program fails with the following exception:

CsvHelper.TypeConversion.TypeConverterException : Converting IEnumerable types is not supported for a single field. If you want to do this, create your own ITypeConverter and register it in the TypeConverterFactory by calling AddConverter.

To Reproduce The csv file looks like this:

Integer,String
,bar
1,
public readonly record struct Option<T> : IEnumerable<T>
{
    private readonly T _value;

    internal Option(T value)
    {
        IsPresent = true;
        _value = value;
    }

    public bool IsPresent { get; }
}

public class CsvOptionConverter<T> : DefaultTypeConverter
{
    private readonly ITypeConverter _wrappedTypeConverter;

    public CsvOptionConverter(TypeConverterCache typeConverterCache)
    {
        _wrappedTypeConverter = typeConverterCache.GetConverter(typeof(T));
    }

    public override object? ConvertFromString(string? text, IReaderRow row, MemberMapData memberMapData) =>
        GetOptionFromString(text, row, memberMapData);

    private Option<T> GetOptionFromString(string? text, IReaderRow row, MemberMapData memberMapData)
    {
        if (string.IsNullOrEmpty(text) || memberMapData.TypeConverterOptions.NullValues.Contains(text))
        {
            return new Option<T>();
        }
        return new Option<T>((T)_wrappedTypeConverter.ConvertFromString(text, row, memberMapData));
    }
}

public class CsvOptionConverterFactory : ITypeConverterFactory
{
    public bool CanCreate(Type type) =>
        type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Option<>);

    public bool Create(Type type, TypeConverterCache cache, out ITypeConverter typeConverter)
    {
        var wrappedType = type.GetGenericArguments()[0];
        var converterType = typeof(CsvOptionConverter<>).MakeGenericType(wrappedType);
        typeConverter = (Activator.CreateInstance(converterType, cache) as ITypeConverter)!;
        return true;
    }
}

using var reader = new StreamReader(...);
using var csv = new CsvReader(reader, _csvConfiguration);
csv.Context.TypeConverterCache.AddConverterFactory(new CsvOptionConverterFactory());
csv.Read();
csv.ReadHeader();
while (csv.Read())
{
    Console.WriteLine($"Integer: {csv.GetField<Option<int>>("Integer")}");
    Console.WriteLine($"String: {csv.GetField<Option<string>>("String")}");
}

Expected behavior The csv gets parsed correctly and does not throw any exceptions.

StefanBertels commented 8 months ago

I just ran into this issue (for same reason). Registered TypeConverterFactory is not consulted for types implementing IEnumerable<T>, i.e. CanCreate(...) does not get called.

JoshClose commented 8 months ago

Fixed in version 31.0.0.