CsvHelper icon indicating copy to clipboard operation
CsvHelper copied to clipboard

Unexpected `InvalidCastException` in `TypeConverter<>` with nullable structs and value types

Open fjmorel opened this issue 11 months ago • 0 comments

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

image

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)

fjmorel avatar Mar 04 '24 17:03 fjmorel