Search code examples
c#csvhelper

CSVHelper: replacing a default TypeConverter throws a TypeConverterException when reading


As stated in the title, when I replace a default converter on CSVReader.Configuration.TypeConverterCache it throws a TypeConverterException that says it used the default converter instead of the replacement. Defining the converter for that field in a ClassMap uses the replacement converter successfully.

Below is part of my application code that reproduces problem. The .csv file that I use has some unnecessary lines before the header so those are ignored but I have included them in the example.

class Program {
        static void Main(string[] args) {
            List<Item> items = new List<Item>();        

            var s = new StringBuilder();
            s.Append("WORD,,\r\n");
            s.Append(",地区版本备注,是否测试道具\r\n");
            s.Append("ID,Party,IsActive\r\n");
            s.Append("1,1,0\r\n");
            s.Append("2,1,1\r\n");
            s.Append("3,1,,\r\n");

            Console.WriteLine("Reading with replaced default converter.");
            using (var reader = new StringReader(s.ToString())) {
                using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) {
                    csv.Configuration.Delimiter = ",";
                    csv.Configuration.RegisterClassMap<ItemMapWithConverter>();
                    csv.Configuration.TypeConverterCache.RemoveConverter<bool>();
                    csv.Configuration.TypeConverterCache.AddConverter<bool>(new BooleanConverter());

                    Console.WriteLine($"Configuration boolean converter type :: {csv.Configuration.TypeConverterCache.GetConverter<bool>()}");

                    csv.Configuration.ShouldSkipRecord = ShouldSkipRecord;
                    bool foundHeader = false;
                    while (csv.Read()) {
                        try {
                            if (csv.Context.Record[0].StartsWith("ID")) {
                                csv.ReadHeader();
                                foundHeader = true;
                                continue;
                            }

                            if (foundHeader)
                                items.Add(csv.GetRecord<Item>());
                        }
                        catch (TypeConverterException ex) {
                            Console.WriteLine($"Exception :: {ex.Message}");
                            Console.WriteLine($"RawRecord :: {ex.ReadingContext.RawRecord}");
                            continue;
                        }
                    }
                }
            }
            Console.WriteLine();

            Console.WriteLine("Reading with converter defined in ClassMap.");
            using (var reader = new StringReader(s.ToString())) {
                using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) {
                    csv.Configuration.Delimiter = ",";
                    csv.Configuration.RegisterClassMap<ItemMapWithoutConverter>();

                    Console.WriteLine($"Configuration boolean converter type :: {csv.Configuration.TypeConverterCache.GetConverter<bool>()}");

                    csv.Configuration.ShouldSkipRecord = ShouldSkipRecord;
                    bool foundHeader = false;
                    while (csv.Read()) {
                        try {
                            if (csv.Context.Record[0].StartsWith("ID")) {
                                csv.ReadHeader();
                                foundHeader = true;
                                continue;
                            }

                            if (foundHeader)
                                items.Add(csv.GetRecord<Item>());
                        }
                        catch (TypeConverterException ex) {
                            Console.WriteLine($"Exception :: {ex.Message}");
                            Console.WriteLine($"RawRecord :: {ex.ReadingContext.RawRecord}");
                            continue;
                        }
                    }
                }
            }

            Console.ReadKey();
        }

        private static bool ShouldSkipRecord(string[] fields) {
            if (string.IsNullOrEmpty(fields[0]))
                return true;
            return false;
        }
    }

    public class Item{
        public int Id { get; set; }
        public int Class { get; set; }
        public bool IsActive { get; set; }

        public Item() { }
    }

    public class ItemMapWithConverter : ClassMap<Item> {
        public ItemMapWithConverter() {
            Map(m => m.Id).Name("ID");
            Map(m => m.Class).Name("Party");
            Map(m => m.IsActive).Name("IsActive");
        }
    }

    public class ItemMapWithoutConverter : ClassMap<Item> {
        public ItemMapWithoutConverter() {
            Map(m => m.Id).Name("ID");
            Map(m => m.Class).Name("Party");
            Map(m => m.IsActive).Name("IsActive").TypeConverter<BooleanConverter>();
        }
    }

    public class BooleanConverter : DefaultTypeConverter {
        public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData) {
            if (string.IsNullOrEmpty(text))
                return false;
            else
                return Convert.ToBoolean(Convert.ToInt32(text));
        }

        public override string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData) {
            if ((bool)value)
                return "1";
            else
                return "0";
        }
    }

So, am I doing something wrong when defining default converters or is this a bug?


Solution

  • There likely needs to be better documentation. You need to add your custom Converter before registering the ClassMap. Also, you don't need to remove the default bool converter first.

    csv.Configuration.Delimiter = ",";
    csv.Configuration.TypeConverterCache.AddConverter<bool>(new BooleanConverter());
    csv.Configuration.RegisterClassMap<ItemMapWithConverter>();