Search code examples
c#reflectioncsvhelper

Apply CsvHelper custom converter to all string properties of a set of classes


I'm using Josh Close's excellent CsvHelper library to read csv files and load them into a database using entity framework. This all works well except for one thing; CsvReader stores an empty string in the csv file as an empty string in the database and I would like this to be a NULL value instead. So what I've done is create a custom converter that takes care of this:

public class NullStringConverter : StringConverter
{
    public override object ConvertFromString(TypeConverterOptions options, string text)
    {
        if (string.IsNullOrEmpty(text))
            return null;
        else
            return base.ConvertFromString(options, text);
    }
}

I can apply this to a string property by using a either fluent map syntax or via an attribute and it will now insert a NULL instead of an empty string.

Since I have quite a few classes containing a number of string attributes I would like to avoid having to create Map statements for each and everyone of them. I created a generic Map class that enumerates all properties and applies the custom converter to all string properties. Here's what I have sofar

public class DefaultStringMap<TEntity> : CsvClassMap<TEntity> where TEntity : AbstractAmtSourceEntity
{
    public DefaultStringMap()
    {
        typeof(TEntity).GetProperties()
            .Where(p => p.PropertyType == typeof(string))
            .ToList()
            .ForEach(p => Map(m => p.Name).TypeConverter<NullStringConverter>());
    }
}

Here AbstractAmtSourceEntity is my base class for all my entity classes. Here's my reader class that actually gets the data:

public static void Read<TEntity>(TextReader reader, AmtSourceModel context) where TEntity : AbstractAmtSourceEntity { using (var csvReader = new CsvReader(reader)) { csvReader.Configuration.WillThrowOnMissingField = false; csvReader.Configuration.Delimiter = "|"; csvReader.Configuration.SkipEmptyRecords = true; csvReader.Configuration.RegisterClassMap<DefaultStringMap<Entity1>>(); csvReader.Configuration.RegisterClassMap<DefaultStringMap<Entity2>>(); etc... csvReader.Configuration.IgnoreReadingExceptions = true; csvReader.Configuration.ReadingExceptionCallback = (ex, row) => { _log.Warn($"Exception caught reading row {row}", ex); _log.Debug($"Exception detail: {ex.Data["CsvHelper"]}"); }; var records = csvReader.GetRecords<TEntity>(); context.Set<TEntity>().AddRange(records); context.SaveChanges(); } } This doesn't work however, the mapping is not applied, so obviously I'm missing something. Can anyone tell me what's missing here?


Solution

  • You can set converters globally.

    TypeConverterFactory.AddConverter( typeof( string ), new NullStringConverter() );
    // or
    TypeConverterFactory.AddConverter<string>( new NullStringConverter() );