1

I want to use CsvReader and CsvDataReader to load the input CSV into a DataTable, where the colums in the destination data table defines the data types. But for columns e.g. of int type, the CSV parsing fails when an empty string (;;) is found in a CSV field.

How can i specify default int value (0) for the Credits column when empty string is found in the CSV input?

      // Construct the datatable
      DataTable dataTable = new DataTable();
      dataTable.Columns.Add(new DataColumn("Name", typeof(string)));
      dataTable.Columns.Add(new DataColumn("Credits", typeof(int)));
      dataTable.Columns.Add(new DataColumn("Notes", typeof(string)));
      dataTable.Columns.Add(new DataColumn("LastLogin", typeof(DateTime)));
      dataTable.Columns.Add(new DataColumn("Balance", typeof(decimal)));

      // Prepare CSV content
      StringBuilder sbInput = new StringBuilder();
      sbInput.AppendLine("Name;Credits;Notes;LastLogin;Balance");
      sbInput.AppendLine("Michael;2433;Good guy;2023-03-28;736,26");
      sbInput.AppendLine("John;;Bad guy;2023-04-01;-49,25");

      // Prepare the configuration
      CsvConfiguration csvConfiguration = new CsvConfiguration(CultureInfo.InvariantCulture)
      {
        NewLine = Environment.NewLine,
        Delimiter = ";",
        Mode = CsvMode.NoEscape,
        TrimOptions = TrimOptions.Trim,
        UseNewObjectForNullReferenceMembers = true
      };

      // Read the CSV stream
      using (var csvReader = new CsvReader(new StringReader(sbInput.ToString()), csvConfiguration))
      {
        using (var csvDataReader = new CsvDataReader(csvReader))
        {
            // Following exception thrown if John;;Bad guy 
            //   Input string was not in a correct format.Couldn't store <> in Credits Column.  Expected type is Int32.
            dataTable.Load(csvDataReader);
        }
      }
Brøgger
  • 25
  • 1
  • 5

1 Answers1

1

There are issues with CsvDataReader where it does not obey MissingFieldFound options or TypeConverters. This is briefly explained here: Handling DateTime fields from CsvDataReader that contains an empty string

A better solution in this modern era would be to abandon DataTable altogether an switch over to using a fully typed collection that you can bind to your data table or directly to the control as a datasource. - that discussion is out of scope...

Instead, we can bypass the CsvDataReader and use a TypeConverter to return the default value for the type if the string is empty:

https://dotnetfiddle.net/maLOt0

/// <summary>
/// Converts an <see cref="int"/> or <see cref="decimal"/> to and from a <see cref="string"/>. and treats empty strings as default values
/// </summary>
public class DefaultNumericConverter<T> : DefaultTypeConverter
{
    /// <summary>
    /// Converts the string to an object. int or decimal defaulting to zero.
    /// </summary>
    /// <remarks>Will revert to default implementation for non int or decimal expected types</remarks>
    /// <param name="text">The string to convert to an object.</param>
    /// <param name="row">The <see cref="IReaderRow"/> for the current record.</param>
    /// <param name="memberMapData">The <see cref="MemberMapData"/> for the member being created.</param>
    /// <returns>The object created from the string.</returns>
    public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
    {
        if (String.IsNullOrWhiteSpace(text))
            return default(T);
        
        var type = typeof(T);
        if (type == typeof(int))
        {
            if (int.TryParse(text, memberMapData.TypeConverterOptions.NumberStyles ?? NumberStyles.Integer, memberMapData.TypeConverterOptions.CultureInfo, out var i))
            {
                return i;
            }
        }
        else if (type == typeof(decimal))
        {
            if (decimal.TryParse(text, memberMapData.TypeConverterOptions.NumberStyles ?? NumberStyles.Currency, memberMapData.TypeConverterOptions.CultureInfo, out var m))
            {
                return m;
            }
        }
        return base.ConvertFromString(text, row, memberMapData);
    }
}

Then we can replace the default type converters for the types we want to default. You could do this in a more generic sense for multiple or all types, but that would be overkill if you didn't specifically need it.

    // Read the CSV stream
    using (var csvReader = new CsvReader(new StringReader(sbInput.ToString()), csvConfiguration))
    {
        csvReader.Context.TypeConverterCache.RemoveConverter<int>();
        csvReader.Context.TypeConverterCache.AddConverter<int>(new DefaultNumericConverter<int>());
        csvReader.Context.TypeConverterCache.RemoveConverter<decimal>();
        csvReader.Context.TypeConverterCache.AddConverter<decimal>(new DefaultNumericConverter<decimal>());

        // Unfortunately, it looks like CsvDataReader treats all values as strings and ignores the TypeConverters for other types. 
        // So... don't use it. https://stackoverflow.com/a/56064792/1690217
        csvReader.Read();
        csvReader.ReadHeader();
        while(csvReader.Read())
        {
            var row = dataTable.NewRow();
            foreach (DataColumn column in dataTable.Columns)
            {
                row[column.ColumnName] = csvReader.GetField(column.DataType, column.ColumnName);
            }
            dataTable.Rows.Add(row);
        }
    }
Chris Schaller
  • 13,704
  • 3
  • 43
  • 81