Search code examples
csvhelper

GetRecord<T> returns null if a single reading exception occurs - CSVHelper library .NET


I am using the CSVHelper library for .NET https://github.com/JoshClose/CsvHelper

I have a class-map which looks as follows:

public class MyMap : ClassMap<Product>
{
    Map(c => c.DateVariable).Name("NonDateColumn");
    // Other valid map statements
}

When calling to get a record, if just a single map statement fails or has a type mismatch the row comes back as null.

var record = reader.GetRecord<T>();

I'm looking for whatever data was mapped successfully to come back, and the default values to be used for the rest of the record.

Is there a way to achieve this?


Solution

  • I don't think there is a way to do that out of the box. You could, however, create your own custom converters that return default values if they can't parse the data. For DateTime, I took the code from DateTimeConverter in the GitHub project and modified it to return DateTime.MinValue if it couldn't parse the field.

    public class Program
    {
        static void Main(string[] args)
        {
            using (var stream = new MemoryStream())
            using (var writer = new StreamWriter(stream))
            using (var reader = new StreamReader(stream))
            using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
            {
                writer.WriteLine("Id,NonDateVariable,Bar");
                writer.WriteLine("1,Not a date,bar value");
                writer.WriteLine("2,2020/07/16,another value");
                writer.Flush();
                stream.Position = 0;
    
                csv.Configuration.TypeConverterCache.AddConverter<DateTime>(new BadDateConverter());
                csv.Configuration.RegisterClassMap<FooClassMap>();
    
                var records = csv.GetRecords<Foo>();
            }
        }
    }
    
    public class Foo
    {
        public int Id { get; set; }
        public DateTime DateVariable { get; set; }
        public string Bar { get; set; }
    }
    
    
    public class FooClassMap : ClassMap<Foo>
    {
        public FooClassMap()
        {
            Map(m => m.Id);
            Map(m => m.DateVariable).Name("NonDateVariable");
            Map(m => m.Bar);
        }
    }
    
    public class BadDateConverter : DefaultTypeConverter
    {
        public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
        {
            if (text == null)
            {
                return base.ConvertFromString(null, row, memberMapData);
            }
    
            var formatProvider = (IFormatProvider)memberMapData.TypeConverterOptions.CultureInfo.GetFormat(typeof(DateTimeFormatInfo)) ?? memberMapData.TypeConverterOptions.CultureInfo;
            var dateTimeStyle = memberMapData.TypeConverterOptions.DateTimeStyle ?? DateTimeStyles.None;
    
            if (memberMapData.TypeConverterOptions.Formats == null || memberMapData.TypeConverterOptions.Formats.Length == 0)
            {
                if (DateTime.TryParse(text, formatProvider, dateTimeStyle, out var d))
                {
                    return d;
                }
                else
                {
                    return DateTime.MinValue;
                }
            }
            else
            {
                if (DateTime.TryParseExact(text, memberMapData.TypeConverterOptions.Formats, formatProvider, dateTimeStyle, out var d))
                {
                    return d;
                }
                else
                {
                    return DateTime.MinValue;
                }
            }
        }
    }
    

    You could also create a wrapper converter to use on any field.

    public class NullIfErrorConverter : DefaultTypeConverter
    {
        public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
        {
            var type = 
                (memberMapData.Member as PropertyInfo)?.PropertyType ??
                (memberMapData.Member as FieldInfo)?.FieldType ??
                throw new InvalidOperationException($"Invalid member type. '{memberMapData.Member.MemberType}'");
                
            var typeConverter = row.Configuration.TypeConverterCache.GetConverter(type);
            memberMapData.TypeConverter = typeConverter;
            try 
            {
                return typeConverter.ConvertFromString(text, row, memberMapData);
            }
            catch
            {
                return null;
            }
        }
    }
    

    Make sure you attach it to members in the mapping and not globally though, since it's using the global type cache.