CsvHelper
CsvHelper copied to clipboard
Unexpected `InvalidCastException` in `TypeConverter<>` with nullable structs and value types
Describe the bug
When using the new generic TypeConverter<T>
with a nullable value type (like bool?
), writing null values does not work. I was working with Mongo's ObjectId
, but converted this sample to use bool
instead. This probably applies to any structs.
To Reproduce
using CsvHelper;
using CsvHelper.Configuration;
using CsvHelper.Configuration.Attributes;
using CsvHelper.TypeConversion;
using System.Globalization;
using System.Text;
namespace Tests;
public class CsvTests
{
private readonly CsvConfiguration _config = new(CultureInfo.InvariantCulture);
[Fact]
public async Task WriteAndRead_GetIdenticalResults()
{
// bool? test = null;
// Assert.True(test is Nullable<bool>);
var original = new Item[]
{
new(true),
new(false),
new(null),
};
var text = await WriteCsv(original);
// Assert.Equal("Test\r\nTrue\r\nFalse\r\n", text);
var parsed = await ReadCsv(text);
Assert.Equal(original, parsed);
}
public async Task<string> WriteCsv(IEnumerable<Item> items)
{
var stream = new MemoryStream();
// Write data to stream
var textWriter = new StreamWriter(stream);
var writer = new CsvWriter(textWriter, _config);
await writer.WriteRecordsAsync(items);
await writer.FlushAsync();
// Reset stream to beginning and read it
stream.Position = 0;
var reader = new StreamReader(stream);
return await reader.ReadToEndAsync();
}
public async Task<List<Item>> ReadCsv(string text)
{
var bytes = Encoding.Default.GetBytes(text);
var stream = new MemoryStream(bytes);
var streamReader = new StreamReader(stream);
var reader = new CsvReader(streamReader, _config);
return await reader.GetRecordsAsync<Item>().ToListAsync();
}
}
public record Item([property: TypeConverter(typeof(NullableValueTypeConverter))] bool? Test);
public sealed class NullableValueTypeConverter : TypeConverter<bool?>
{
public override bool? ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
{
if (bool.TryParse(text, out bool value))
return value;
return null;
}
public override string ConvertToString(bool? value, IWriterRow row, MemberMapData memberMapData)
{
return value?.ToString() ?? "";
}
}
Expected behavior
When testing my type converter, I expect to be able to read and write any valid value.
Screenshots
Additional context
I believe this is due to the cast in TypeConverter<>
. If you uncomment the first 2 lines of the test, you'll see that bool? null is bool?
doesn't work.
string ITypeConverter.ConvertToString(object value, IWriterRow row, MemberMapData memberMapData)
{
// this cast doesn't work as expected with nullable value types
return value is T v
? ConvertToString(v, row, memberMapData)
: throw new InvalidCastException();
}
Stack trace:
CsvHelper.WriterException: An unexpected error occurred. See inner exception for details.
CsvHelper.WriterException
An unexpected error occurred. See inner exception for details.
IWriter state:
Row: 4
Index: 0
HeaderRecord:
4
at CsvHelper.CsvWriter.WriteRecord[T](T record)
at CsvHelper.CsvWriter.WriteRecordsAsync[T](IEnumerable`1 records, CancellationToken cancellationToken)
...
System.InvalidCastException
Specified cast is not valid.
at CsvHelper.TypeConversion.TypeConverter`1.CsvHelper.TypeConversion.ITypeConverter.ConvertToString(Object value, IWriterRow row, MemberMapData memberMapData)
at lambda_method25(Closure, Item)
at CsvHelper.Expressions.RecordWriter.Write[T](T record)
at CsvHelper.Expressions.RecordManager.Write[T](T record)
at CsvHelper.CsvWriter.WriteRecord[T](T record)