C# CSV Parsing TypeConverter Example
Sometimes, you want to apply pre-defined conversions to csv field values, such as:
- Parsing the string in a certain way, like removing first/last character, or replacing whitespaces with underscores.
- Have a non-nullable type in a CSV model definition, while csv can actually contain empty values, so you want to provide a default value for them.
With CsvHelper
, this functionality is typically achieved using a custom TypeConverter
.
Let's say we have the following CSV model mapping definition::
public readonly record struct CsvRecord {
[Name("NAME")]
public readonly string Instrument { get; init; }
[Name("QTY")]
public readonly int Quantity { get; init; }
[Name("Profit / Loss")]
public readonly decimal PnL { get; init; }
}
Now, we know that Quantity
field sometimes contains empty values in CSV, and we would like to fill
those values with zeros.
To achieve this, we will subclass DefaultTypeConverter
:
internal class NullableQuantityTypeConverter : DefaultTypeConverter
{
public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
{
if (string.IsNullOrEmpty(text)) {
return 0;
}
return int.Parse(text, NumberStyles.Integer);
}
}
Next, we add this converter to the class map (usually put in the same file as the CsvRecord
definition):
public sealed class CsvRecordMap : ClassMap<CsvRecord>
{
public CsvRecordMap()
{
AutoMap(CultureInfo.InvariantCulture);
Map(m => m.Quantity).TypeConverter(new NullableQuantityTypeConverter());
}
}
Similarly, let's say we have a CSV field that describes some profit/loss in an accounting's notation, i.e., a values in parentheses means loss, and profit otherwise. Then we apply the following converter:
internal class PnLTypeConverter : DefaultTypeConverter
{
public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
{
if (string.IsNullOrEmpty(text)) {
return decimal.Zero;
}
if (text.StartsWith("(") && text.EndsWith(")")) {
return -decimal.Parse(text[1..^1]);
}
return decimal.Parse(text);
}
}
Then we add this converter again to the class map:
public sealed class CsvRecordMap : ClassMap<CsvRecord>
{
public CsvRecordMap()
{
AutoMap(CultureInfo.InvariantCulture);
Map(m => m.Quantity).TypeConverter(new NullableQuantityTypeConverter());
Map(m => m.PnL).TypeConverter(new PnLTypeConverter());
}
}
Finally, we need to register the class map when we read the CSV, for example:
using var csv = new CsvReader(stream, CultureInfo.InvariantCulture));
csv.Context.RegisterClassMap<CsvRecordMap>();
return csv.GetRecords<CsvRecord>().ToImmutableList();
If we need to test the same behavior in unit tests, you need to also implement ConvertToString
method
on your type converter subclass:
internal class PnLTypeConverter : DefaultTypeConverter
{
...
public override string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData) {
if ((decimal)value < 0) {
return "(" + base.ConvertToString(Math.Abs((decimal(value), row, memberMapData) + ")";
}
return base.ConvertToString(value, row, memberMapData);
}
}