Dapper icon indicating copy to clipboard operation
Dapper copied to clipboard

IDataReader.GetRowParser<string>() throws exception

Open p21x opened this issue 2 years ago • 2 comments

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)

p21x avatar Sep 06 '22 14:09 p21x

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.

olivier-spinelli avatar Sep 22 '22 08:09 olivier-spinelli

Just ran into this bug. Thanks for fixing @olivier-spinelli

arj03 avatar Sep 07 '23 11:09 arj03