Dapper
Dapper copied to clipboard
IDataReader.GetRowParser<string>() throws exception
Using .NET Core 6 Dapper v 2.0.123
using Dapper;
using Microsoft.Data.SqlClient;
using var connection = new SqlConnection("Server=localhost;Database=Test;Integrated Security=True;Encrypt=False");
var reader = connection.ExecuteReader("select 'test' as col");
var rowParser = reader.GetRowParser<string>();
reader.Read();
string result = rowParser(reader);
Throws exception:
System.InvalidCastException: 'Unable to cast object of type 'System.Func`2[System.Data.IDataReader,System.Object]' to type 'System.Func`2[System.Data.IDataReader,System.String]'.'
reader.GetRowParser<object>(typeof(string))
works, but then extra unboxing is needed: string result = (string)rowParser(reader)
You're right: this is a bug (but FYI, this is not an "extra unboxing", just a downcast). The first thing is to write a unit test for it (in DataReaderTests):
/// <summary>
/// See https://github.com/DapperLib/Dapper/issues/1822
/// </summary>
[Fact]
public void issue_number_1822()
{
var reader = connection.ExecuteReader("select 'test' as col");
var rowParser = reader.GetRowParser<string>();
reader.Read();
// This should not throw!
string result = rowParser(reader);
Assert.Equal("test", result);
}
And it's awfully red.
My firt idea to fix it was simply to rewrite GetRowParser
like this:
public static Func<IDataReader, T> GetRowParser<T>(this IDataReader reader, Type concreteType = null,
int startIndex = 0, int length = -1, bool returnNullIfFirstMissing = false)
{
concreteType ??= typeof(T);
var func = GetDeserializer(concreteType, reader, startIndex, length, returnNullIfFirstMissing);
// Always use a wrapper: this works for value and ref types (delegate casting is not an option).
return _ => (T)func(_);
}
Instead of:
public static Func<IDataReader, T> GetRowParser<T>(this IDataReader reader, Type concreteType = null,
int startIndex = 0, int length = -1, bool returnNullIfFirstMissing = false)
{
concreteType ??= typeof(T);
var func = GetDeserializer(concreteType, reader, startIndex, length, returnNullIfFirstMissing);
if (concreteType.IsValueType)
{
return _ => (T)func(_);
}
else
{
return (Func<IDataReader, T>)(Delegate)func;
}
}
But unfortunately, this breaks the GetSameReaderForSameShape
test. To make it happy (and avoid a wrapper that is a good thing), the actual fix is slighty more involved:
public static Func<IDataReader, T> GetRowParser<T>(this IDataReader reader, Type concreteType = null,
int startIndex = 0, int length = -1, bool returnNullIfFirstMissing = false)
{
concreteType ??= typeof(T);
var func = GetDeserializer(concreteType, reader, startIndex, length, returnNullIfFirstMissing);
if (func.Method.ReturnType != typeof(T))
{ // Always use a wrapper: this works for value and ref types (delegate casting is not an option).
return _ => (T)func(_);
}
return (Func<IDataReader, T>)(Delegate)func;
}
I'll submit a PR asap.
Just ran into this bug. Thanks for fixing @olivier-spinelli