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?
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.